import { useCallback, useEffect, useRef } from 'react'; import { preventAll, safeInvoke } from './events'; import { clamp } from './math'; import usePointerTap from '../ui/usePointerTap'; import { MIN_TIMESTEP, MAX_TIMESTEP, applyDomTransform } from './workspaceEngine'; const DRAG_HYSTERESIS_PX = 4; const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX; const EDGE_COLLISION_THRESHOLD = 0.5; const getEventTargetElement = (event) => { if (typeof Element === 'undefined' || !event) { return null; } const candidate = event.target || (event.nativeEvent ? event.nativeEvent.target : null); return candidate instanceof Element ? candidate : null; }; const useDocumentDrag = (options = {}) => { const { engine, layoutRef, dragTransformsRef, itemRefs, documentLookup, ensureDocumentSize, resolveBaseMetrics, bringToFront, setDraggingId, canvasSize, openOverlayForDoc, recalcVisibleDocIds, settings, containerRef, onInspectDocument, onDocumentStackSelect, selectedDocumentIds, markLayoutDirty, } = options; const { canvasPadding = 24, defaultCanvasWidth = 1024, defaultCanvasHeight = 680, debugDrag = false, } = settings || {}; useEffect( () => () => { engine?.disposeInertiaAnimations?.(); }, [engine], ); const tapHandler = usePointerTap({ delay: 220, onSingle: () => {}, onDouble: ({ data, event }) => { if (!data || !data.docId) { return; } if (event?.altKey) { openOverlayForDoc(data.docId, data.originInfo); return; } if (typeof onInspectDocument === 'function') { onInspectDocument(data.docId, event); } }, }); const dragStateRef = useRef(null); const setDragTransform = useCallback((docKey, transform) => { if (!docKey) { return; } const map = dragTransformsRef?.current; if (!map) { return; } map.set(String(docKey), transform); }, [dragTransformsRef]); const clearDragTransforms = useCallback(() => { const map = dragTransformsRef?.current; if (!map || typeof map.clear !== 'function') { return; } map.clear(); }, [dragTransformsRef]); const commitActiveDragTransforms = useCallback((docIds = null) => { const map = dragTransformsRef?.current; if (!map || !map.size) { return; } const keys = Array.isArray(docIds) && docIds.length ? docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean) : Array.from(map.keys()); keys.forEach((key) => { const transform = map.get(key); if (!transform) { return; } const previous = layoutRef.current.get(key) || {}; layoutRef.current.set(key, { ...previous, centerX: transform.centerX, centerY: transform.centerY, rotation: transform.rotation ?? previous.rotation ?? 0, }); }); markLayoutDirty?.(); }, [dragTransformsRef, layoutRef, markLayoutDirty]); const finishDrag = useCallback( (pointerId, { clearTransforms = true } = {}) => { const state = dragStateRef.current; if (state && state.pointerId === pointerId) { const capturedTarget = state.capturedTarget; if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') { try { capturedTarget.releasePointerCapture(pointerId); } catch (error) { if (debugDrag) { void error; } } } } dragStateRef.current = null; setDraggingId(null); engine?.endDrag?.(); if (clearTransforms) { clearDragTransforms(); } }, [clearDragTransforms, debugDrag, engine, setDraggingId], ); const handlePointerDown = useCallback( (event, docIdInput, options = {}) => { const targetElement = getEventTargetElement(event); if (targetElement && typeof targetElement.closest === 'function' && targetElement.closest('[data-desk-tag-chip="true"]')) { return; } preventAll(event); const docId = docIdInput != null ? docIdInput : null; const docKey = docId != null ? String(docId) : null; if (!docKey) { return; } engine?.cancelInertiaAnimation?.(docKey); const doc = documentLookup.get(docKey); if (!doc) { return; } const stackDocIdsOptionRaw = options?.stackDocIds; const stackDocIdsOption = Array.isArray(stackDocIdsOptionRaw) ? stackDocIdsOptionRaw .map((value) => (value != null ? String(value) : null)) .filter(Boolean) : null; const stackSelectionAppliedInitial = Boolean(options?.stackSelectionApplied); const wasSelectedAtPointerDown = Boolean(options?.wasSelected); const pointerModifierActive = typeof options?.modifierActive === 'boolean' ? options.modifierActive : Boolean(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey); const stackReplace = Boolean(options?.stackReplace); let selectionIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.map((id) => String(id)) : []; if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) { selectionIds = [docKey]; } if (stackDocIdsOption && stackDocIdsOption.length) { const selectionSet = new Set(selectionIds); stackDocIdsOption.forEach((value) => { if (value != null) { selectionSet.add(String(value)); } }); selectionIds = Array.from(selectionSet); } const metaOrCtrl = event.metaKey || event.ctrlKey; if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) { selectionIds = [...selectionIds, docKey]; } selectionIds = selectionIds .map((id) => String(id)) .filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id)); if (!selectionIds.includes(docKey)) { selectionIds.unshift(docKey); } if (!selectionIds.length) { selectionIds = [docKey]; } const isGroupDrag = selectionIds.length > 1; if (isGroupDrag) { selectionIds.forEach((id) => { if (id !== docKey) { engine?.cancelInertiaAnimation?.(id); } }); } const sizeInfo = ensureDocumentSize(doc) || { width: 0, height: 0 }; const docWidth = sizeInfo.width || 320; const docHeight = sizeInfo.height || 240; const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight); const normalizedBaseScale = Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1; const entry = layoutRef.current.get(docKey) || null; const defaultCenterX = canvasPadding + docWidth / 2; const defaultCenterY = canvasPadding + docHeight / 2; const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX; const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY; const modifierPressed = pointerModifierActive; if (!modifierPressed) { if (isGroupDrag) { const layout = layoutRef.current; const ordered = [...selectionIds] .filter((id, index, array) => array.indexOf(id) === index) .sort((a, b) => { const aZ = layout.get(a)?.z ?? 0; const bZ = layout.get(b)?.z ?? 0; return aZ - bZ; }); ordered.forEach((id) => { bringToFront(id === docKey ? docId : id); }); } else { bringToFront(docId); } } if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) { layoutRef.current.set(docKey, { ...entry, centerX, centerY }); } const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null; if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') { try { capturedTarget.setPointerCapture(event.pointerId); } catch (error) { if (debugDrag) { void error; } } } const containerRect = containerRef?.current?.getBoundingClientRect?.() || null; const containerLeft = containerRect?.left || 0; const containerTop = containerRect?.top || 0; const pointerCanvasX = event.clientX - containerLeft; const pointerCanvasY = event.clientY - containerTop; const pointerOffsetX = pointerCanvasX - centerX; const pointerOffsetY = pointerCanvasY - centerY; const initialRotationDeg = entry?.rotation ?? 0; const initialRotationRad = (initialRotationDeg * Math.PI) / 180; const cosInitial = Math.cos(-initialRotationRad); const sinInitial = Math.sin(-initialRotationRad); const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial; const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial; const groupItems = selectionIds.map((id) => { const itemDoc = documentLookup.get(id); const itemSize = ensureDocumentSize(itemDoc) || sizeInfo; const itemWidth = itemSize.width || docWidth; const itemHeight = itemSize.height || docHeight; const itemEntry = layoutRef.current.get(id) || null; const itemCenterX = typeof itemEntry?.centerX === 'number' ? itemEntry.centerX : canvasPadding + itemWidth / 2; const itemCenterY = typeof itemEntry?.centerY === 'number' ? itemEntry.centerY : canvasPadding + itemHeight / 2; const baseOffsetX = itemCenterX - centerX; const baseOffsetY = itemCenterY - centerY; const initialRotation = itemEntry?.rotation ?? 0; const targetRotation = initialRotation; return { docId: id, width: itemWidth, height: itemHeight, currentCenterX: itemCenterX, currentCenterY: itemCenterY, baseOffsetX, baseOffsetY, offsetX: baseOffsetX, offsetY: baseOffsetY, initialRotation, displayRotation: initialRotation, targetRotation, }; }); const eventTimestamp = typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp) ? event.timeStamp : typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(); const hasStackSource = Array.isArray(stackDocIdsOption) && stackDocIdsOption.length > 1; dragStateRef.current = { docId: docKey, docKey, pointerId: event.pointerId, originCenterX: centerX, originCenterY: centerY, currentCenterX: centerX, currentCenterY: centerY, startX: event.clientX, startY: event.clientY, rotation: entry?.rotation ?? 0, restRotation: entry?.rotation ?? 0, dynamicRotation: 0, angularVelocity: 0, moved: false, locked: false, width: docWidth, height: docHeight, dragScale: 1, baseScale: normalizedBaseScale, capturedTarget, lastClientX: event.clientX, lastClientY: event.clientY, lastTimestamp: eventTimestamp, localPointerOffsetX, localPointerOffsetY, containerRectLeft: containerLeft, containerRectTop: containerTop, isGroup: isGroupDrag, activeDocIds: selectionIds, groupItems, groupElevated: !isGroupDrag, stackDocIds: hasStackSource ? stackDocIdsOption : null, stackSelectionApplied: stackSelectionAppliedInitial || !hasStackSource, stackReplace, }; const state = dragStateRef.current; clearDragTransforms(); state.groupItems.forEach((item) => { if (!item?.docId) { return; } setDragTransform(item.docId, { centerX: item.currentCenterX, centerY: item.currentCenterY, rotation: item.displayRotation ?? item.initialRotation ?? 0, width: item.width, height: item.height, scale: item.docId === state.docKey ? state.dragScale || 1 : 1, }); }); engine?.beginDrag?.(state.activeDocIds); setDraggingId(docKey); if (isGroupDrag) { groupItems.forEach((item) => { if (item.docId === docKey) { return; } const node = itemRefs.current.get(item.docId); if (node) { item.displayRotation = item.initialRotation; const itemEntry = layoutRef.current.get(item.docId) || null; applyDomTransform(node, { centerX: item.currentCenterX, centerY: item.currentCenterY, width: item.width, height: item.height, rotation: item.displayRotation ?? 0, scale: 1, zIndex: itemEntry?.z, }); } }); } }, [ bringToFront, canvasPadding, containerRef, documentLookup, engine, ensureDocumentSize, layoutRef, resolveBaseMetrics, selectedDocumentIds, setDraggingId, debugDrag, itemRefs, clearDragTransforms, setDragTransform, ]); const handlePointerMove = useCallback( (event) => { const state = dragStateRef.current; if (!state) { return; } if (state.pointerId !== event.pointerId) { return; } preventAll(event); if (state.isGroup) { const containerRect = containerRef?.current?.getBoundingClientRect?.(); if (containerRect) { state.containerRectLeft = containerRect.left; state.containerRectTop = containerRect.top; } const pointerCanvasX = event.clientX - state.containerRectLeft; const pointerCanvasY = event.clientY - state.containerRectTop; const deltaX = event.clientX - state.startX; const deltaY = event.clientY - state.startY; if (!state.moved) { const distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared < DRAG_HYSTERESIS_SQUARED) { return; } state.moved = true; if ( !state.stackSelectionApplied && Array.isArray(state.stackDocIds) && state.stackDocIds.length > 0 ) { safeInvoke(onDocumentStackSelect, state.stackDocIds, event, { replace: state.stackReplace }); state.stackSelectionApplied = true; } if (!state.groupElevated) { const layout = layoutRef.current; const sortedGroup = state.activeDocIds .filter((id) => id !== state.docKey) .sort((a, b) => { const aZ = layout.get(a)?.z ?? 0; const bZ = layout.get(b)?.z ?? 0; return aZ - bZ; }); sortedGroup.forEach((id) => bringToFront(id)); bringToFront(state.docKey); state.groupElevated = true; } } const docWidth = state.width; const docHeight = state.height; const halfWidth = docWidth / 2; const halfHeight = docHeight / 2; const canvasWidth = canvasSize.width || defaultCanvasWidth; const canvasHeight = canvasSize.height || defaultCanvasHeight; const minCenterX = canvasPadding + halfWidth; const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth); const minCenterY = canvasPadding + halfHeight; const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight); const desiredCenterX = pointerCanvasX - state.localPointerOffsetX; const desiredCenterY = pointerCanvasY - state.localPointerOffsetY; const centerX = clamp(desiredCenterX, minCenterX, maxCenterX); const centerY = clamp(desiredCenterY, minCenterY, maxCenterY); state.currentCenterX = centerX; state.currentCenterY = centerY; state.groupItems.forEach((item) => { const isPrimary = item.docId === state.docKey; if (isPrimary) { item.currentCenterX = centerX; item.currentCenterY = centerY; item.offsetX = item.baseOffsetX ?? 0; item.offsetY = item.baseOffsetY ?? 0; item.displayRotation = state.rotation ?? item.displayRotation ?? 0; } else { const decay = 0.82; const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay; const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay; item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX; item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY; const targetX = centerX + item.offsetX; const targetY = centerY + item.offsetY; const smoothing = 0.18; item.currentCenterX += (targetX - item.currentCenterX) * smoothing; item.currentCenterY += (targetY - item.currentCenterY) * smoothing; const halfW = item.width / 2; const halfH = item.height / 2; const minX = canvasPadding + halfW; const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW); const minY = canvasPadding + halfH; const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH); item.currentCenterX = clamp(item.currentCenterX, minX, maxX); item.currentCenterY = clamp(item.currentCenterY, minY, maxY); const rotationBlend = 0.16; item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend; } const entry = layoutRef.current.get(item.docId) || null; const payload = { centerX: item.currentCenterX, centerY: item.currentCenterY, rotation: item.displayRotation ?? 0, width: item.width, height: item.height, scale: isPrimary ? state.dragScale || 1 : 1, zIndex: entry?.z, }; setDragTransform(item.docId, payload); const node = itemRefs.current.get(item.docId); applyDomTransform(node, payload); }); state.lastClientX = event.clientX; state.lastClientY = event.clientY; state.lastTimestamp = typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp) ? event.timeStamp : typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(); return; } if (state.locked) { return; } const deltaX = event.clientX - state.startX; const deltaY = event.clientY - state.startY; const docWidth = state.width; const docHeight = state.height; const halfWidth = docWidth / 2; const halfHeight = docHeight / 2; const containerRect = containerRef?.current?.getBoundingClientRect?.(); if (containerRect) { state.containerRectLeft = containerRect.left; state.containerRectTop = containerRect.top; } const containerLeft = state.containerRectLeft; const containerTop = state.containerRectTop; const pointerCanvasX = event.clientX - containerLeft; const pointerCanvasY = event.clientY - containerTop; const entry = layoutRef.current.get(state.docKey) || {}; const rotationDeg = state.rotation ?? entry.rotation ?? 0; const rotationRad = (rotationDeg * Math.PI) / 180; const cosRot = Math.cos(rotationRad); const sinRot = Math.sin(rotationRad); const rotatedOffsetX = state.localPointerOffsetX * cosRot - state.localPointerOffsetY * sinRot; const rotatedOffsetY = state.localPointerOffsetX * sinRot + state.localPointerOffsetY * cosRot; const previousCenterX = Number.isFinite(state.currentCenterX) ? state.currentCenterX : state.originCenterX; const previousCenterY = Number.isFinite(state.currentCenterY) ? state.currentCenterY : state.originCenterY; const absCos = Math.abs(cosRot); const absSin = Math.abs(sinRot); const rotatedHalfWidth = absCos * halfWidth + absSin * halfHeight; const rotatedHalfHeight = absSin * halfWidth + absCos * halfHeight; const canvasWidth = canvasSize.width || defaultCanvasWidth; const canvasHeight = canvasSize.height || defaultCanvasHeight; const minCenterX = canvasPadding + rotatedHalfWidth; const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - rotatedHalfWidth); const minCenterY = canvasPadding + rotatedHalfHeight; const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - rotatedHalfHeight); const pointerRelativePrevX = pointerCanvasX - previousCenterX; const pointerRelativePrevY = pointerCanvasY - previousCenterY; const cosInversePrev = Math.cos(-rotationRad); const sinInversePrev = Math.sin(-rotationRad); const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev; const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev; const pointerInsideRelativeToPrev = Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight; const desiredCenterX = pointerCanvasX - rotatedOffsetX; const desiredCenterY = pointerCanvasY - rotatedOffsetY; const clampedCenterX = clamp(desiredCenterX, minCenterX, maxCenterX); const clampedCenterY = clamp(desiredCenterY, minCenterY, maxCenterY); if (!state.moved) { const distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared < DRAG_HYSTERESIS_SQUARED) { return; } bringToFront(state.docKey); state.moved = true; } const collidedWithHorizontalEdge = Math.abs(desiredCenterY - clampedCenterY) > EDGE_COLLISION_THRESHOLD; const collidedWithVerticalEdge = Math.abs(desiredCenterX - clampedCenterX) > EDGE_COLLISION_THRESHOLD; const collidedWithEdge = collidedWithHorizontalEdge || collidedWithVerticalEdge; let currentCenterX = clampedCenterX; let currentCenterY = clampedCenterY; if (collidedWithEdge && !pointerInsideRelativeToPrev) { currentCenterX = previousCenterX; currentCenterY = previousCenterY; } state.currentCenterX = currentCenterX; state.currentCenterY = currentCenterY; const layoutEntry = layoutRef.current.get(state.docKey) || null; const transformPayload = { centerX: currentCenterX, centerY: currentCenterY, rotation: rotationDeg, width: state.width, height: state.height, scale: state.dragScale || 1, zIndex: layoutEntry?.z, }; setDragTransform(state.docKey, transformPayload); const primaryNode = itemRefs.current.get(state.docKey); applyDomTransform(primaryNode, transformPayload); const offsetX = pointerCanvasX - currentCenterX; const offsetY = pointerCanvasY - currentCenterY; const cosInverseCurrent = Math.cos(-rotationRad); const sinInverseCurrent = Math.sin(-rotationRad); const pointerLocalX = offsetX * cosInverseCurrent - offsetY * sinInverseCurrent; const pointerLocalY = offsetX * sinInverseCurrent + offsetY * cosInverseCurrent; const pointerInsideCard = Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight; const currentTimestamp = typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp) ? event.timeStamp : typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(); const previousTimestamp = state.lastTimestamp ?? currentTimestamp; let dt = (currentTimestamp - previousTimestamp) / 1000; if (!Number.isFinite(dt) || dt <= 0) { dt = MIN_TIMESTEP; } dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP); state.lastClientX = event.clientX; state.lastClientY = event.clientY; state.lastTimestamp = currentTimestamp; const rotationForOffsetDeg = state.rotation || 0; const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180; const cosInverse = Math.cos(-rotationForOffsetRad); const sinInverse = Math.sin(-rotationForOffsetRad); const pointerRelativeX = pointerCanvasX - currentCenterX; const pointerRelativeY = pointerCanvasY - currentCenterY; const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse; const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse; if (!collidedWithEdge || pointerInsideCard) { state.localPointerOffsetX = updatedLocalOffsetX; state.localPointerOffsetY = updatedLocalOffsetY; } void debugDrag; }, [ bringToFront, canvasPadding, canvasSize.height, canvasSize.width, defaultCanvasHeight, defaultCanvasWidth, containerRef, layoutRef, itemRefs, debugDrag, onDocumentStackSelect, setDragTransform, ], ); const handlePointerUp = useCallback( (event) => { const state = dragStateRef.current; if (!state || state.pointerId !== event.pointerId) { finishDrag(event.pointerId); return; } if (state.isGroup) { engine?.finalizeGroupDrag?.(state); commitActiveDragTransforms(state.activeDocIds); finishDrag(event.pointerId); recalcVisibleDocIds(); return; } if (state.moved) { commitActiveDragTransforms([state.docKey]); const inertiaState = { restRotation: state.restRotation, dynamicRotation: state.dynamicRotation, angularVelocity: state.angularVelocity, rotation: state.rotation, width: state.width, height: state.height, dragScale: state.dragScale || 1, }; const docId = state.docKey; finishDrag(event.pointerId); engine?.startInertiaAnimation?.(docId, inertiaState); return; } const docId = state.docKey; const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; if (!metaPressed) { bringToFront(docId); } const originInfo = { rotation: state.rotation || 0, scale: state.baseScale || 1, width: state.width, height: state.height, }; const docKey = docId != null ? String(docId) : null; const doc = docKey ? documentLookup.get(docKey) : null; tapHandler(event, { docId, originInfo, docTitle: doc?.title || 'document', }); finishDrag(event.pointerId); }, [ bringToFront, commitActiveDragTransforms, documentLookup, engine, finishDrag, recalcVisibleDocIds, tapHandler, ], ); const handlePointerCancel = useCallback( (event) => { const state = dragStateRef.current; if (state && state.pointerId === event.pointerId && state.moved) { if (state.isGroup) { engine?.finalizeGroupDrag?.(state); commitActiveDragTransforms(state.activeDocIds); finishDrag(event.pointerId); recalcVisibleDocIds(); return; } commitActiveDragTransforms([state.docKey]); const inertiaState = { restRotation: state.restRotation, dynamicRotation: state.dynamicRotation, angularVelocity: state.angularVelocity, rotation: state.rotation, width: state.width, height: state.height, dragScale: state.dragScale || 1, }; const docId = state.docKey; finishDrag(event.pointerId); engine?.startInertiaAnimation?.(docId, inertiaState); return; } finishDrag(event.pointerId); }, [commitActiveDragTransforms, engine, finishDrag, recalcVisibleDocIds], ); return { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel, }; }; export default useDocumentDrag;