import { useCallback, useRef } from 'react'; import { useDesktopContext } from './context'; import { preventAll } from './events'; import { clamp, formatTransform } from './math'; import usePointerTap from '../ui/usePointerTap'; const DRAG_HYSTERESIS_PX = 4; const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX; const MIN_TIMESTEP = 1 / 120; const MAX_TIMESTEP = 1 / 20; const MAX_DYNAMIC_ROTATION = 4; const MAX_ANGULAR_VELOCITY = 180; const ANGULAR_DAMPING = 11; const TORQUE_TO_ACCELERATION = 0.006; const SETTLE_ANGULAR_VELOCITY = 1.2; const EDGE_COLLISION_THRESHOLD = 0.5; const useDocumentDrag = () => { const { layoutRef, itemRefs, documentLookup, ensureDocumentSize, resolveBaseMetrics, bringToFront, setDraggingId, syncLayoutSnapshot, canvasSize, openOverlayForDoc, recalcVisibleDocIds, settings, containerRef, onDocumentOpen, onInspectDocument, selectedDocumentIds, markLayoutDirty, } = useDesktopContext(); const applyTransform = useCallback( (docId, centerX, centerY, width, height, rotation, scale = 1) => { const node = itemRefs.current.get(docId); if (!node) { return; } node.style.transform = formatTransform( centerX - width / 2, centerY - height / 2, rotation, scale, ); }, [itemRefs], ); const finalizeGroupDrag = useCallback( (dragState) => { if (!dragState?.groupItems) { return; } dragState.groupItems.forEach((item) => { if (!item) { return; } const entryItem = layoutRef.current.get(item.docId) || {}; const centerX = item.currentCenterX ?? entryItem.centerX ?? dragState.originCenterX; const centerY = item.currentCenterY ?? entryItem.centerY ?? dragState.originCenterY; const rotation = item.displayRotation ?? entryItem.rotation ?? 0; layoutRef.current.set(item.docId, { ...entryItem, centerX, centerY, rotation, }); applyTransform( item.docId, centerX, centerY, item.width, item.height, rotation, item.docId === dragState.docKey ? dragState.dragScale || 1 : 1, ); }); markLayoutDirty?.(); }, [applyTransform, layoutRef, markLayoutDirty], ); const tapHandler = usePointerTap({ delay: 220, onSingle: ({ data, event }) => { if (!data || !data.docId) { return; } if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { return; } if (typeof onInspectDocument === 'function') { onInspectDocument(data.docId); return; } onDocumentOpen?.(data.docId); }, onDouble: ({ data, event }) => { if (!data || !data.docId) { return; } if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { return; } openOverlayForDoc(data.docId, data.originInfo); }, }); const dragStateRef = useRef(null); const inertiaAnimationsRef = useRef(new Map()); const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings; const cancelInertiaAnimation = useCallback((docId) => { if (typeof window === 'undefined') { inertiaAnimationsRef.current.delete(docId); return; } const existing = inertiaAnimationsRef.current.get(docId); if (existing && typeof window.cancelAnimationFrame === 'function') { window.cancelAnimationFrame(existing.frameId); } inertiaAnimationsRef.current.delete(docId); }, []); const integrateRotation = useCallback( (simulationState, dt, torque = 0, dampingOverride = null) => { const { docId } = simulationState; const entry = layoutRef.current.get(docId); if (!entry) { return true; } const centerX = Number(entry.centerX); const centerY = Number(entry.centerY); if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) { return true; } const torqueAcceleration = torque * TORQUE_TO_ACCELERATION; let angularVelocity = simulationState.angularVelocity + torqueAcceleration * dt; angularVelocity = clamp(angularVelocity, -MAX_ANGULAR_VELOCITY, MAX_ANGULAR_VELOCITY); const dampingConstant = typeof dampingOverride === 'number' ? dampingOverride : ANGULAR_DAMPING; const dampingFactor = Math.exp(-dampingConstant * dt); angularVelocity *= dampingFactor; let dynamicRotation = simulationState.dynamicRotation + angularVelocity * dt; if (dynamicRotation > MAX_DYNAMIC_ROTATION) { dynamicRotation = MAX_DYNAMIC_ROTATION; angularVelocity = Math.min(angularVelocity, 0); } else if (dynamicRotation < -MAX_DYNAMIC_ROTATION) { dynamicRotation = -MAX_DYNAMIC_ROTATION; angularVelocity = Math.max(angularVelocity, 0); } simulationState.angularVelocity = angularVelocity; simulationState.dynamicRotation = dynamicRotation; simulationState.rotation = simulationState.restRotation + dynamicRotation; const rotation = simulationState.rotation; layoutRef.current.set(docId, { ...entry, rotation }); const node = itemRefs.current.get(docId); if (node) { node.style.transform = formatTransform( centerX - simulationState.width / 2, centerY - simulationState.height / 2, rotation, simulationState.dragScale || 1, ); } const isSettled = Math.abs(angularVelocity) < SETTLE_ANGULAR_VELOCITY; return isSettled; }, [itemRefs, layoutRef], ); const startInertiaAnimation = useCallback( (docId, baseState) => { if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') { return; } cancelInertiaAnimation(docId); const now = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(); const simulationState = { ...baseState, docId, dragScale: baseState.dragScale || 1, lastTimestamp: now, }; const step = (timestamp) => { const safeTimestamp = Number.isFinite(timestamp) ? timestamp : now + 16; const previous = simulationState.lastTimestamp; let dt = (safeTimestamp - previous) / 1000; if (!Number.isFinite(dt) || dt <= 0) { dt = MIN_TIMESTEP; } dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP); simulationState.lastTimestamp = safeTimestamp; const settled = integrateRotation(simulationState, dt, 0); if (settled) { inertiaAnimationsRef.current.delete(docId); syncLayoutSnapshot(); return; } simulationState.frameId = window.requestAnimationFrame(step); }; simulationState.frameId = window.requestAnimationFrame(step); inertiaAnimationsRef.current.set(docId, simulationState); }, [cancelInertiaAnimation, integrateRotation, syncLayoutSnapshot], ); const finishDrag = useCallback( (pointerId) => { const state = dragStateRef.current; if (!state || state.pointerId !== pointerId) { return; } const capturedTarget = state.capturedTarget; if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') { try { capturedTarget.releasePointerCapture(pointerId); } catch (error) { if (debugDrag) { console.warn('[desk] releasePointerCapture failed', error); } } } dragStateRef.current = null; setDraggingId((current) => (current === state.docId ? null : current)); syncLayoutSnapshot(); }, [debugDrag, setDraggingId, syncLayoutSnapshot], ); const handlePointerDown = useCallback( (event, docIdInput, options = {}) => { if (debugDrag) { console.log( '[desk] handlePointerDown fired for doc', docIdInput, 'button', event.button, 'pointerType', event.pointerType, 'pointerId', event.pointerId, ); } preventAll(event); const docId = docIdInput != null ? docIdInput : null; const docKey = docId != null ? String(docId) : null; if (!docKey) { return; } cancelInertiaAnimation(docId); const doc = documentLookup.get(docKey); if (!doc) { return; } const stackDocIdsOption = Array.isArray(options?.stackDocIds) ? options.stackDocIds .map((value) => (value != null ? String(value) : null)) .filter(Boolean) : null; let selectionIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.map((id) => String(id)) : []; if (stackDocIdsOption && stackDocIdsOption.length) { selectionIds = stackDocIdsOption; } const metaOrCtrl = event.metaKey || event.ctrlKey; if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) { selectionIds = [...selectionIds, docKey]; } let groupDocIds = []; if (stackDocIdsOption && stackDocIdsOption.length) { groupDocIds = stackDocIdsOption.filter((id, index, array) => { const unique = array.indexOf(id) === index; return unique && documentLookup.has(id); }); } else if (selectionIds.includes(docKey) && selectionIds.length > 1) { groupDocIds = selectionIds .map((id) => String(id)) .filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id)); } if (!groupDocIds.includes(docKey)) { groupDocIds.unshift(docKey); } groupDocIds = groupDocIds.filter((id, index, array) => array.indexOf(id) === index); if (!groupDocIds.length) { groupDocIds = [docKey]; } const isGroupDrag = groupDocIds.length > 1; if (isGroupDrag) { groupDocIds.forEach((id) => { if (id !== docKey) { 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 = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; if (!modifierPressed) { if (isGroupDrag) { groupDocIds.forEach((id) => bringToFront(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) { console.warn('[desk] setPointerCapture failed', 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 stackRandom = () => Math.random(); const groupItems = groupDocIds.map((id, index) => { 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 radius = index === 0 ? 0 : 24 + index * 8; const offsetAngle = (index * 1.618 + stackRandom() * 0.5) * Math.PI; const offsetX = Math.cos(offsetAngle) * radius; const offsetY = Math.sin(offsetAngle) * radius; const initialRotation = itemEntry?.rotation ?? 0; const targetRotation = initialRotation; return { docId: id, width: itemWidth, height: itemHeight, currentCenterX: itemCenterX, currentCenterY: itemCenterY, offsetX, offsetY, 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(); dragStateRef.current = { docId, docKey, pointerId: event.pointerId, originCenterX: centerX, originCenterY: 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, groupDocIds, groupItems, groupElevated: !isGroupDrag, }; setDraggingId(docId); if (isGroupDrag) { groupItems.forEach((item) => { if (item.docId === docKey) { return; } const node = itemRefs.current.get(item.docId); if (node) { item.displayRotation = item.initialRotation; node.style.transform = formatTransform( item.currentCenterX - item.width / 2, item.currentCenterY - item.height / 2, item.displayRotation, 1, ); } }); } }, [ bringToFront, canvasPadding, cancelInertiaAnimation, containerRef, documentLookup, ensureDocumentSize, layoutRef, resolveBaseMetrics, selectedDocumentIds, setDraggingId, debugDrag, itemRefs, ], ); const handlePointerMove = useCallback( (event) => { const state = dragStateRef.current; if (!state) { if (debugDrag) { console.log('[desk] handlePointerMove: no drag state for pointer', event.pointerId); } return; } if (state.pointerId !== event.pointerId) { if (debugDrag) { console.log( '[desk] handlePointerMove: pointer mismatch expected', state.pointerId, 'got', 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.groupElevated) { const layout = layoutRef.current; const sortedGroup = state.groupDocIds .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.docId); 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); const primaryEntry = layoutRef.current.get(state.docKey) || {}; const primaryRotation = state.rotation ?? primaryEntry.rotation ?? 0; layoutRef.current.set(state.docKey, { ...primaryEntry, centerX, centerY, }); const primaryNode = itemRefs.current.get(state.docId); if (primaryNode) { primaryNode.style.transform = formatTransform( centerX - docWidth / 2, centerY - docHeight / 2, primaryRotation, state.dragScale || 1, ); } state.groupItems.forEach((item) => { const isPrimary = item.docId === state.docKey; if (isPrimary) { item.currentCenterX = centerX; item.currentCenterY = centerY; item.offsetX *= 0.92; item.offsetY *= 0.92; item.displayRotation = state.rotation ?? item.displayRotation ?? 0; } else { item.offsetX *= 0.92; item.offsetY *= 0.92; if (Math.abs(item.offsetX) < 1) item.offsetX = 0; if (Math.abs(item.offsetY) < 1) item.offsetY = 0; 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 entryItem = layoutRef.current.get(item.docId) || {}; layoutRef.current.set(item.docId, { ...entryItem, centerX: item.currentCenterX, centerY: item.currentCenterY, rotation: item.displayRotation ?? entryItem.rotation ?? 0, }); applyTransform( item.docId, item.currentCenterX, item.currentCenterY, item.width, item.height, item.displayRotation ?? entryItem.rotation ?? 0, isPrimary ? state.dragScale || 1 : 1, ); }); 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(); recalcVisibleDocIds(); return; } if (state.locked) { if (debugDrag) { console.log('[desk] handlePointerMove: locked drag for doc', state.docId); } return; } const entry = layoutRef.current.get(state.docId); if (!entry) { 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 rotationDeg = 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(entry.centerX) ? entry.centerX : state.originCenterX; const previousCenterY = Number.isFinite(entry.centerY) ? entry.centerY : 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.docId); 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; } const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY }; layoutRef.current.set(state.docId, updated); applyTransform( state.docId, currentCenterX, currentCenterY, state.width, state.height, rotationDeg, state.dragScale || 1, ); const primaryNode = itemRefs.current.get(state.docId); if (primaryNode) { primaryNode.style.transform = formatTransform( currentCenterX - state.width / 2, currentCenterY - state.height / 2, rotationDeg, state.dragScale || 1, ); } 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; } if (debugDrag) { console.log('[desk] handlePointerMove: moved doc', state.docId, 'to', currentCenterX, currentCenterY); } recalcVisibleDocIds(); }, [ bringToFront, canvasPadding, canvasSize.height, canvasSize.width, defaultCanvasHeight, defaultCanvasWidth, containerRef, layoutRef, itemRefs, applyTransform, recalcVisibleDocIds, debugDrag, ], ); const handlePointerUp = useCallback( (event) => { const state = dragStateRef.current; if (!state || state.pointerId !== event.pointerId) { finishDrag(event.pointerId); return; } if (state.isGroup) { finalizeGroupDrag(state); finishDrag(event.pointerId); recalcVisibleDocIds(); return; } if (state.moved) { 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.docId; finishDrag(event.pointerId); startInertiaAnimation(docId, inertiaState); return; } const docId = state.docId; 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, documentLookup, finishDrag, finalizeGroupDrag, startInertiaAnimation, recalcVisibleDocIds, tapHandler, ], ); const handlePointerCancel = useCallback( (event) => { const state = dragStateRef.current; if (state && state.pointerId === event.pointerId && state.moved) { if (state.isGroup) { finalizeGroupDrag(state); finishDrag(event.pointerId); recalcVisibleDocIds(); return; } 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.docId; finishDrag(event.pointerId); startInertiaAnimation(docId, inertiaState); return; } finishDrag(event.pointerId); }, [finalizeGroupDrag, finishDrag, recalcVisibleDocIds, startInertiaAnimation], ); return { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel, }; }; export default useDocumentDrag;