{"version":3,"file":"useTimelineClipDrag.mjs","names":[],"sources":["../../../src/hooks/clips/useTimelineClipDrag.ts"],"sourcesContent":["import { timelineCommandFail, timelineCommandOk } from '@techsquidtv/canvas-timeline-core';\nimport type {\n  TimelineCommandResult,\n  ClipViewportRect,\n  TimelineClipDropFeedback,\n  TimelineClipMoveResult,\n  TimelineInteractionGeometry,\n  TimelineTrackHitTestResult,\n  TimelineReadonly,\n} from '@techsquidtv/canvas-timeline-core';\nimport { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { TimelineEditGesture } from '#react/hooks/editing/timelineEditGesture';\nimport { useTimelineTrackDropTargets } from '#react/hooks/tracks/useTimelineTrackDropTargets';\nimport type {\n  TimelineTrackDropGuard,\n  TimelineTrackDropResult,\n} from '#react/hooks/tracks/useTimelineTrackDropTargets';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n/** Pointer data needed to begin a clip body drag. */\nexport interface TimelineClipDragStartInput {\n  /** Clip being dragged. */\n  clipId: string;\n  /** Pointer client X captured at drag start. */\n  clientX: number;\n  /** Pointer Y in timeline viewport coordinates, including the ruler area. */\n  viewportY: number;\n  /** Optional clip rect from the initiating hit test. */\n  clipRect?: ClipViewportRect;\n}\n\n/** Pointer data needed to update a clip body drag. */\nexport interface TimelineClipDragMoveInput {\n  /** Current pointer client X. */\n  clientX: number;\n  /** Current pointer Y in timeline viewport coordinates, including the ruler area. */\n  viewportY: number;\n}\n\n/**\n * Options accepted by `useTimelineClipDrag`.\n *\n * @remarks\n *\n * Pass renderer-aligned geometry so pointer Y coordinates resolve to the same\n * track rows users see on screen. `canDropClipOnTrack` lets applications enforce\n * domain rules such as preventing audio clips from moving to visual tracks\n * unless a modifier key or tool mode allows it.\n *\n *\n * @see {@link useTimelineTrackDropTargets}\n * @see {@link https://canvastimeline.com/docs/tracks-and-clips | Tracks and clips}\n */\nexport interface UseTimelineClipDragOptions extends TimelineInteractionGeometry {\n  /** Portion of another track the pointer must enter before snapping vertically. Defaults to 0.3. */\n  verticalSnapThreshold?: number;\n  /** Minimum vertical pixels required before snapping vertically. Defaults to 8. */\n  minVerticalSnapPixels?: number;\n  /** Optional viewport width used for track row geometry. */\n  viewportWidth?: number;\n  /** Optional app policy for accepting, rejecting, or expanding drop targets. */\n  canDropClipOnTrack?: TimelineTrackDropGuard;\n}\n\n/** Result returned by `useTimelineClipDrag`. */\nexport interface UseTimelineClipDragResult {\n  /** Whether a clip body drag is currently active. */\n  dragging: boolean;\n  /** Current transient drop feedback snapshot. Use `useTimelineClipDropFeedback` for live updates. */\n  dropFeedback: TimelineClipDropFeedback;\n  /** Starts a clip body drag. */\n  startClipDrag: (input: TimelineClipDragStartInput) => TimelineCommandResult;\n  /** Updates the active clip body drag preview. */\n  moveClipDrag: (input: TimelineClipDragMoveInput) => TimelineCommandResult<TimelineClipMoveResult>;\n  /** Ends the active clip body drag and settles history. */\n  endClipDrag: () => TimelineCommandResult;\n  /** Cancels the active drag and discards its preview. */\n  cancelClipDrag: () => TimelineCommandResult;\n}\n\ninterface ActiveClipDrag {\n  gesture: TimelineEditGesture;\n  clipId: string;\n  startClientX: number;\n  startLeft: number;\n  sourceTrackId: string;\n  sourceTrackIndex: number;\n  sourceClipIndex: number;\n  previousStartTime: TimelineClipMoveResult['previousStartTime'];\n  previousEndTime: TimelineClipMoveResult['previousEndTime'];\n  activeTargetTrackId: string;\n  activeTargetTrackIndex: number;\n  allowCrossKindTrackMove: boolean;\n  trackTargets: TimelineReadonly<TimelineTrackHitTestResult>[];\n}\n\nfunction clampRatio(value: number) {\n  return Math.max(0, Math.min(1, value));\n}\n\nfunction findTrackTargetAtY(\n  trackTargets: TimelineReadonly<TimelineTrackHitTestResult>[],\n  viewportY: number\n) {\n  return (\n    trackTargets.find(({ rect }) => viewportY >= rect.y && viewportY < rect.y + rect.height) ?? null\n  );\n}\n\nfunction getTrackPenetration(\n  target: TimelineReadonly<TimelineTrackHitTestResult>,\n  activeTargetTrackIndex: number,\n  viewportY: number\n) {\n  if (target.trackIndex === activeTargetTrackIndex) {\n    return {\n      ratio: 1,\n      pixels: target.rect.height,\n    };\n  }\n\n  const pixels =\n    target.trackIndex > activeTargetTrackIndex\n      ? viewportY - target.rect.y\n      : target.rect.y + target.rect.height - viewportY;\n\n  return {\n    ratio: clampRatio(pixels / Math.max(1, target.rect.height)),\n    pixels: Math.max(0, pixels),\n  };\n}\n\n/**\n * Headless clip body drag behavior shared by canvas and custom timeline UIs.\n *\n * @remarks\n *\n * Use this when building a custom interaction layer around canvas-painted clips.\n * The hook handles drag lifecycle, snapping preparation, cross-track drop\n * policy, transient drop feedback, and commit/settle behavior. Package\n * consumers using the standard DOM chrome can render `Timeline.ClipInteractionLayer`\n * instead.\n *\n * @param options - Drag geometry, vertical snap sensitivity, and optional drop policy.\n * @returns Clip drag state and commands for pointer-driven body moves.\n *\n * @example\n * ```tsx\n * import { useTimelineClipDrag } from '@techsquidtv/canvas-timeline-react';\n *\n * export function CustomClipDragHandle({ clipId }: { clipId: string }) {\n *   const drag = useTimelineClipDrag();\n *\n *   return (\n *     <button\n *       type=\"button\"\n *       aria-pressed={drag.dragging}\n *       onPointerDown={(event) => {\n *         drag.startClipDrag({\n *           clipId,\n *           clientX: event.clientX,\n *           viewportY: event.nativeEvent.offsetY,\n *         });\n *       }}\n *     >\n *       Move clip\n *     </button>\n *   );\n * }\n * ```\n *\n * @see {@link useTimelineClipDropFeedback}\n * @see {@link useTimelineTrackDropTargets}\n * @see {@link https://canvastimeline.com/demos/basic-editor-surface | Basic editor surface demo}\n */\nexport function useTimelineClipDrag(\n  options: UseTimelineClipDragOptions = {}\n): UseTimelineClipDragResult {\n  const engine = useTimelineEngine();\n  const activeDragRef = useRef<ActiveClipDrag | null>(null);\n  const [dragging, setDragging] = useState(false);\n  const dropTargets = useTimelineTrackDropTargets({\n    canDropClipOnTrack: options.canDropClipOnTrack,\n    collapsedTrackHeight: options.collapsedTrackHeight,\n    edgeThreshold: options.edgeThreshold,\n    rulerHeight: options.rulerHeight,\n    touchEdgeThreshold: options.touchEdgeThreshold,\n    trackHeight: options.trackHeight,\n    viewportWidth: options.viewportWidth,\n  });\n\n  useEffect(() => {\n    return () => {\n      const active = activeDragRef.current;\n      if (!active) {\n        return;\n      }\n\n      activeDragRef.current = null;\n      setDragging(false);\n      if (active.gesture.cancel()) {\n        engine.clearClipDropFeedback();\n      }\n    };\n  }, [engine]);\n\n  const publishFeedback = useCallback(\n    (\n      activeDrag: ActiveClipDrag,\n      hoveredTrackId: string | null,\n      dropResult: TimelineTrackDropResult | null,\n      penetrationRatio: number\n    ) => {\n      engine.setClipDropFeedback({\n        activeClipId: activeDrag.clipId,\n        sourceTrackId: activeDrag.sourceTrackId,\n        hoveredTrackId,\n        activeTargetTrackId: activeDrag.activeTargetTrackId,\n        valid: dropResult?.canDrop ?? false,\n        reason: dropResult?.canDrop ? null : (dropResult?.reason ?? null),\n        penetrationRatio,\n      });\n    },\n    [engine]\n  );\n\n  const startClipDrag = useCallback(\n    (input: TimelineClipDragStartInput): TimelineCommandResult => {\n      if (activeDragRef.current) {\n        return timelineCommandFail('unsupported', 'A drag is already active.');\n      }\n      if (!Number.isFinite(input.clientX) || !Number.isFinite(input.viewportY)) {\n        return timelineCommandFail('invalid-input');\n      }\n      const found = engine.geometry.getClip(input.clipId);\n      const rect =\n        input.clipRect ??\n        engine.geometry.getClipRect(input.clipId, {\n          collapsedTrackHeight: options.collapsedTrackHeight,\n          edgeThreshold: options.edgeThreshold,\n          rulerHeight: options.rulerHeight,\n          touchEdgeThreshold: options.touchEdgeThreshold,\n          trackHeight: options.trackHeight,\n        });\n\n      if (!found || !rect) {\n        return timelineCommandFail('not-found');\n      }\n      if (found.track.locked || found.clip.movable === false) {\n        return timelineCommandFail('locked');\n      }\n\n      const trackTargets = dropTargets.trackTargets;\n      engine.prepareSnapping({ ignoreClipId: input.clipId, operation: 'move' });\n      engine.cancelEdit();\n\n      activeDragRef.current = {\n        gesture: new TimelineEditGesture(engine),\n        clipId: input.clipId,\n        startClientX: input.clientX,\n        startLeft: rect.x,\n        sourceTrackId: found.track.id,\n        sourceTrackIndex: found.trackIndex,\n        sourceClipIndex: found.clipIndex,\n        previousStartTime: { ...found.clip.timelineStart },\n        previousEndTime: { ...found.clip.timelineEnd },\n        activeTargetTrackId: found.track.id,\n        activeTargetTrackIndex: found.trackIndex,\n        allowCrossKindTrackMove: false,\n        trackTargets,\n      };\n      setDragging(true);\n\n      engine.setClipDropFeedback({\n        activeClipId: input.clipId,\n        sourceTrackId: found.track.id,\n        hoveredTrackId: found.track.id,\n        activeTargetTrackId: found.track.id,\n        valid: true,\n        reason: null,\n        penetrationRatio: 1,\n      });\n\n      return timelineCommandOk();\n    },\n    [\n      dropTargets.trackTargets,\n      engine,\n      options.collapsedTrackHeight,\n      options.edgeThreshold,\n      options.rulerHeight,\n      options.touchEdgeThreshold,\n      options.trackHeight,\n    ]\n  );\n\n  const moveClipDrag = useCallback(\n    (input: TimelineClipDragMoveInput): TimelineCommandResult<TimelineClipMoveResult> => {\n      const activeDrag = activeDragRef.current;\n      if (!activeDrag) {\n        return timelineCommandFail<TimelineClipMoveResult>('unsupported');\n      }\n      if (!activeDrag.gesture.isCurrent()) {\n        return timelineCommandFail('unsupported', 'The drag preview was replaced.');\n      }\n      if (!Number.isFinite(input.clientX) || !Number.isFinite(input.viewportY)) {\n        return timelineCommandFail('invalid-input');\n      }\n\n      const hoveredTarget = findTrackTargetAtY(activeDrag.trackTargets, input.viewportY);\n      let dropResult: TimelineTrackDropResult | null = null;\n      let penetrationRatio = 0;\n\n      if (hoveredTarget) {\n        const penetration = getTrackPenetration(\n          hoveredTarget,\n          activeDrag.activeTargetTrackIndex,\n          input.viewportY\n        );\n        penetrationRatio = penetration.ratio;\n        dropResult = dropTargets.canDropClipOnTrack(\n          activeDrag.clipId,\n          hoveredTarget.track.id,\n          activeDrag.sourceTrackId\n        );\n\n        const canActivate =\n          hoveredTarget.track.id === activeDrag.activeTargetTrackId ||\n          (penetration.ratio >= (options.verticalSnapThreshold ?? 0.3) &&\n            penetration.pixels >= (options.minVerticalSnapPixels ?? 8));\n\n        if (canActivate && dropResult.canDrop) {\n          activeDrag.activeTargetTrackId = hoveredTarget.track.id;\n          activeDrag.activeTargetTrackIndex = hoveredTarget.trackIndex;\n          activeDrag.allowCrossKindTrackMove = dropResult.allowCrossKindTrackMove;\n        }\n      }\n\n      publishFeedback(activeDrag, hoveredTarget?.track.id ?? null, dropResult, penetrationRatio);\n\n      const deltaX = input.clientX - activeDrag.startClientX;\n      const preview = activeDrag.gesture.publish({\n        type: 'move',\n        overwrite: true,\n        clipId: activeDrag.clipId,\n        startTime: engine.pixelToTime(activeDrag.startLeft + deltaX),\n        targetTrackId: activeDrag.activeTargetTrackId,\n        allowCrossKindTrackMove: activeDrag.allowCrossKindTrackMove,\n      });\n\n      return preview.valid && preview.moveResult\n        ? timelineCommandOk(preview.moveResult)\n        : timelineCommandFail(preview.reason ?? 'unsupported');\n    },\n    [\n      dropTargets,\n      engine,\n      options.minVerticalSnapPixels,\n      options.verticalSnapThreshold,\n      publishFeedback,\n    ]\n  );\n\n  const endClipDrag = useCallback((): TimelineCommandResult => {\n    const active = activeDragRef.current;\n    if (!active) {\n      return timelineCommandFail('unsupported');\n    }\n\n    activeDragRef.current = null;\n    setDragging(false);\n    const current = active.gesture.isCurrent();\n    const result = active.gesture.commit();\n    if (current) {\n      engine.clearClipDropFeedback();\n    }\n    return result;\n  }, [engine]);\n\n  const cancelClipDrag = useCallback((): TimelineCommandResult => {\n    const active = activeDragRef.current;\n    if (!active) {\n      return timelineCommandFail('unsupported');\n    }\n\n    activeDragRef.current = null;\n    setDragging(false);\n    if (active.gesture.cancel()) {\n      engine.clearClipDropFeedback();\n    }\n    return timelineCommandOk();\n  }, [engine]);\n\n  return useMemo(\n    () => ({\n      dragging,\n      dropFeedback: engine.getClipDropFeedback(),\n      startClipDrag,\n      moveClipDrag,\n      endClipDrag,\n      cancelClipDrag,\n    }),\n    [cancelClipDrag, dragging, endClipDrag, engine, moveClipDrag, startClipDrag]\n  );\n}\n"],"mappings":"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"}