diff --git a/packages/playground/src/dom/Scene.tsx b/packages/playground/src/dom/Scene.tsx
index e75392b..9bee6e5 100644
--- a/packages/playground/src/dom/Scene.tsx
+++ b/packages/playground/src/dom/Scene.tsx
@@ -1,6 +1,6 @@
import studio from '@theatre/studio'
-import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
-import useDrag from '@theatre/studio/uiComponents/useDrag'
+import type {UseDragOpts} from './useDrag'
+import useDrag from './useDrag'
import React, {useLayoutEffect, useMemo, useState} from 'react'
import type {IProject, ISheet} from '@theatre/core'
import {types as t} from '@theatre/core'
@@ -105,14 +105,19 @@ export const Scene: React.FC<{project: IProject}> = ({project}) => {
{
setBoxes((boxes) => [...boxes, String(++lastBoxId)])
}}
diff --git a/packages/playground/src/dom/useDrag.ts b/packages/playground/src/dom/useDrag.ts
new file mode 100644
index 0000000..7f21101
--- /dev/null
+++ b/packages/playground/src/dom/useDrag.ts
@@ -0,0 +1,150 @@
+import {useLayoutEffect, useRef} from 'react'
+
+const noop = () => {}
+
+function createCursorLock(cursor: string) {
+ const el = document.createElement('div')
+ el.style.cssText = `
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 9999999;`
+
+ el.style.cursor = cursor
+ document.body.appendChild(el)
+ const relinquish = () => {
+ document.body.removeChild(el)
+ }
+
+ return relinquish
+}
+
+export type UseDragOpts = {
+ disabled?: boolean
+ dontBlockMouseDown?: boolean
+ lockCursorTo?: string
+ onDragStart?: (event: MouseEvent) => void | false
+ onDragEnd?: (dragHappened: boolean) => void
+ onDrag: (dx: number, dy: number, event: MouseEvent) => void
+}
+
+export default function useDrag(
+ target: HTMLElement | undefined | null,
+ opts: UseDragOpts,
+) {
+ const optsRef = useRef(opts)
+ optsRef.current = opts
+
+ const modeRef = useRef<'dragStartCalled' | 'dragging' | 'notDragging'>(
+ 'notDragging',
+ )
+
+ const stateRef = useRef<{
+ dragHappened: boolean
+ startPos: {
+ x: number
+ y: number
+ }
+ }>({dragHappened: false, startPos: {x: 0, y: 0}})
+
+ useLayoutEffect(() => {
+ if (!target) return
+
+ const getDistances = (event: MouseEvent): [number, number] => {
+ const {startPos} = stateRef.current
+ return [event.screenX - startPos.x, event.screenY - startPos.y]
+ }
+
+ let relinquishCursorLock = noop
+
+ const dragHandler = (event: MouseEvent) => {
+ if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
+ relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
+ }
+ if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
+ modeRef.current = 'dragging'
+
+ const deltas = getDistances(event)
+ optsRef.current.onDrag(deltas[0], deltas[1], event)
+ }
+
+ const dragEndHandler = () => {
+ removeDragListeners()
+ modeRef.current = 'notDragging'
+
+ optsRef.current.onDragEnd &&
+ optsRef.current.onDragEnd(stateRef.current.dragHappened)
+ relinquishCursorLock()
+ relinquishCursorLock = noop
+ }
+
+ const addDragListeners = () => {
+ document.addEventListener('mousemove', dragHandler)
+ document.addEventListener('mouseup', dragEndHandler)
+ }
+
+ const removeDragListeners = () => {
+ document.removeEventListener('mousemove', dragHandler)
+ document.removeEventListener('mouseup', dragEndHandler)
+ }
+
+ const preventUnwantedClick = (event: MouseEvent) => {
+ if (optsRef.current.disabled) return
+ if (stateRef.current.dragHappened) {
+ if (
+ !optsRef.current.dontBlockMouseDown &&
+ modeRef.current !== 'notDragging'
+ ) {
+ event.stopPropagation()
+ event.preventDefault()
+ }
+ stateRef.current.dragHappened = false
+ }
+ }
+
+ const dragStartHandler = (event: MouseEvent) => {
+ const opts = optsRef.current
+ if (opts.disabled === true) return
+
+ if (event.button !== 0) return
+ const resultOfStart = opts.onDragStart && opts.onDragStart(event)
+
+ if (resultOfStart === false) return
+
+ if (!opts.dontBlockMouseDown) {
+ event.stopPropagation()
+ event.preventDefault()
+ }
+
+ modeRef.current = 'dragStartCalled'
+
+ const {screenX, screenY} = event
+ stateRef.current.startPos = {x: screenX, y: screenY}
+ stateRef.current.dragHappened = false
+
+ addDragListeners()
+ }
+
+ const onMouseDown = (e: MouseEvent) => {
+ dragStartHandler(e)
+ }
+
+ target.addEventListener('mousedown', onMouseDown)
+ target.addEventListener('click', preventUnwantedClick)
+
+ return () => {
+ removeDragListeners()
+ target.removeEventListener('mousedown', onMouseDown)
+ target.removeEventListener('click', preventUnwantedClick)
+ relinquishCursorLock()
+
+ if (modeRef.current !== 'notDragging') {
+ optsRef.current.onDragEnd &&
+ optsRef.current.onDragEnd(modeRef.current === 'dragging')
+ }
+ modeRef.current = 'notDragging'
+ }
+ }, [target])
+}