| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- 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<string, number>;
- 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<string, { start: number }> = {};
- 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 };
- }
|