import { useRef, useCallback } from "react"; import { useClockStore } from "../store/useClockStore"; import type { ClockBlock } from "../assets/types"; export function useTimelineDrag() { const { duration, blocks, moveBlocks, updateBlock, selectBlocks, selectedBlockIds, setAlignmentTick } = useClockStore(); const dragState = useRef<{ blockId: string; mode: "move" | "resize"; startX: number; laneWidth: number; shiftKey: boolean; wasSelected: boolean; groupOrigStarts: Map; origDuration: number; } | null>(null); const pxToTick = useCallback((laneWidth: number, px: number) => Math.round((px / laneWidth) * duration), [duration]); const onPointerDownBlock = (e: React.PointerEvent, block: ClockBlock, mode: "move" | "resize", laneElement: HTMLElement | null) => { e.stopPropagation(); if (!laneElement) return; (e.target as HTMLElement).setPointerCapture(e.pointerId); const wasSelected = selectedBlockIds.has(block.id); const groupIds = mode === "move" && wasSelected ? selectedBlockIds : new Set([block.id]); // Convert object to map for quick lookup const blockValues = Object.values(blocks); dragState.current = { blockId: block.id, mode, startX: e.clientX, laneWidth: laneElement.clientWidth, shiftKey: e.shiftKey, wasSelected, groupOrigStarts: new Map(blockValues.filter((b) => groupIds.has(b.id)).map((b) => [b.id, b.start])), origDuration: block.duration, }; }; const onPointerMove = (e: React.PointerEvent) => { const drag = dragState.current; if (!drag) return; const deltaTicks = pxToTick(drag.laneWidth, e.clientX - drag.startX); if (drag.mode === "resize") { updateBlock(drag.blockId, { duration: Math.max(1, drag.origDuration + deltaTicks) }); setAlignmentTick(null); return; } // Group move clamping let clampDelta = deltaTicks; drag.groupOrigStarts.forEach((origStart) => { const proposed = origStart + clampDelta; if (proposed < 0) clampDelta = Math.max(clampDelta, -origStart); if (proposed > duration - 1) clampDelta = Math.min(clampDelta, duration - 1 - origStart); }); // Batch update via store const updates: Record = {}; drag.groupOrigStarts.forEach((origStart, blockId) => { updates[blockId] = { start: origStart + clampDelta }; }); moveBlocks(updates); // Alignment logic const draggedNewStart = updates[drag.blockId].start; const match = Object.values(blocks).some((b) => !drag.groupOrigStarts.has(b.id) && b.start === draggedNewStart); setAlignmentTick(match ? draggedNewStart : null); }; const onPointerUp = (e: React.PointerEvent) => { const drag = dragState.current; if (!drag) return; setAlignmentTick(null); dragState.current = null; const moved = Math.abs(e.clientX - drag.startX) >= 3; if (drag.mode === "move" && !moved) { if (drag.shiftKey) { selectBlocks((prev) => { const next = new Set(prev); next.has(drag.blockId) ? next.delete(drag.blockId) : next.add(drag.blockId); return next; }); } else { selectBlocks(new Set([drag.blockId])); } } else if (drag.mode === "move" && moved && !drag.wasSelected) { selectBlocks(new Set([drag.blockId])); } }; return { onPointerDownBlock, onPointerMove, onPointerUp, pxToTick }; }