{"version":3,"file":"useTimelineTrackDropTargets.mjs","names":[],"sources":["../../../src/hooks/tracks/useTimelineTrackDropTargets.ts"],"sourcesContent":["import { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { useTimelineGeometryRevision } from '#react/hooks/core/useTimelineGeometryRevision';\nimport { useTimelineSelector } from '#react/hooks/core/useTimelineSelector';\nimport type {\n  Clip,\n  TimelineClipDropFailureReason,\n  TimelineInteractionGeometry,\n  TimelineTrackGeometryOptions,\n  TimelineTrackHitTestResult,\n  Track,\n  TimelineReadonly,\n  TrackHitTestInput,\n} from '@techsquidtv/canvas-timeline-core';\nimport { useCallback, useMemo } from 'react';\n/**\n * Context passed to custom clip track-drop guards.\n *\n * @remarks\n *\n * The context contains the clip being moved, the source row captured at drag\n * start, and the candidate target row currently under the pointer. Use it to\n * express app policy such as visual-only tracks, audio-only tracks, locked\n * groups, or modifier-key cross-kind moves.\n *\n * target tracks.\n */\nexport interface TimelineTrackDropContext {\n  /** Clip being moved. */\n  clip: TimelineReadonly<Clip>;\n  /** Track that contained the clip at drag start. */\n  sourceTrack: TimelineReadonly<Track>;\n  /** Candidate destination track. */\n  targetTrack: TimelineReadonly<Track>;\n  /** Source track index at drag start. */\n  sourceTrackIndex: number;\n  /** Candidate destination track index. */\n  targetTrackIndex: number;\n}\n\n/** Result of resolving whether a clip may drop on a track. */\nexport interface TimelineTrackDropResult {\n  /** Whether the target track accepts the clip. */\n  canDrop: boolean;\n  /** Machine-readable failure reason when `canDrop` is false. */\n  reason: TimelineClipDropFailureReason | null;\n  /** Whether the engine should permit a cross-kind transfer. */\n  allowCrossKindTrackMove: boolean;\n}\n\n/**\n * Custom policy for accepting or rejecting track drop targets.\n *\n * target tracks.\n */\nexport type TimelineTrackDropGuard = (\n  context: TimelineTrackDropContext\n) => boolean | TimelineTrackDropResult;\n\n/**\n * Options accepted by `useTimelineTrackDropTargets`.\n *\n * @remarks\n *\n * Geometry options should match the renderer and interaction layer. The optional\n * guard runs after built-in checks for missing, locked, and same-kind tracks.\n *\n *\n * @see {@link TimelineTrackDropGuard}\n * @see {@link useTimelineClipDrag}\n */\nexport interface UseTimelineTrackDropTargetsOptions extends TimelineTrackGeometryOptions {\n  /** Optional app policy for accepting, rejecting, or expanding drop targets. */\n  canDropClipOnTrack?: TimelineTrackDropGuard;\n}\n\n/**\n * Result returned by `useTimelineTrackDropTargets`.\n *\n * @remarks\n *\n * Use this result to build custom track-row drop previews, or indirectly through\n * {@link useTimelineClipDrag}. `trackTargets` are viewport-space rows in\n * timeline order and `canDropClipOnTrack` applies both engine and app policy.\n *\n */\nexport interface UseTimelineTrackDropTargetsResult {\n  /** Viewport-space track rows in timeline order. */\n  trackTargets: TimelineTrackHitTestResult[];\n  /** Hit-tests timeline tracks in viewport coordinates. */\n  getTrackAtViewportPoint: (input: TrackHitTestInput) => TimelineTrackHitTestResult | null;\n  /** Resolves whether one clip may drop on one candidate track. */\n  canDropClipOnTrack: (\n    clipId: string,\n    targetTrackId: string,\n    sourceTrackId?: string\n  ) => TimelineTrackDropResult;\n}\n\nconst acceptedDropResult: TimelineTrackDropResult = {\n  canDrop: true,\n  reason: null,\n  allowCrossKindTrackMove: false,\n};\n\nfunction isTrackDropTarget(\n  target: TimelineTrackHitTestResult | null\n): target is TimelineTrackHitTestResult {\n  return target !== null;\n}\n\nfunction normalizeGuardResult(\n  result: boolean | TimelineTrackDropResult,\n  allowCrossKindTrackMove: boolean\n): TimelineTrackDropResult {\n  if (typeof result === 'boolean') {\n    return {\n      canDrop: result,\n      reason: result ? null : 'unsupported',\n      allowCrossKindTrackMove: result ? allowCrossKindTrackMove : false,\n    };\n  }\n\n  return {\n    canDrop: result.canDrop,\n    reason: result.canDrop ? null : (result.reason ?? 'unsupported'),\n    allowCrossKindTrackMove: result.canDrop\n      ? result.allowCrossKindTrackMove || allowCrossKindTrackMove\n      : false,\n  };\n}\n\n/**\n * Builds headless track drop targets for cross-track clip movement.\n *\n * @remarks\n *\n * This hook is a geometry and policy helper for custom clip drag layers. It\n * does not start pointer capture or mutate clips on its own; combine it with\n * {@link useTimelineClipDrag} or your own pointer lifecycle when building custom\n * interaction chrome.\n *\n * @param options - Track geometry and optional drop policy used to resolve compatible tracks.\n * @returns Track hit targets, viewport hit testing, and drop-policy resolution helpers.\n *\n * @example\n * ```tsx\n * import { useTimelineTrackDropTargets } from '@techsquidtv/canvas-timeline-react';\n *\n * export function TrackDropOverlay({ clipId }: { clipId: string }) {\n *   const targets = useTimelineTrackDropTargets({\n *     canDropClipOnTrack: ({ sourceTrack, targetTrack }) => sourceTrack.kind === targetTrack.kind,\n *   });\n *\n *   return targets.trackTargets.map((target) => {\n *     const result = targets.canDropClipOnTrack(clipId, target.track.id);\n *\n *     return (\n *       <div key={target.track.id} data-valid-drop-target={result.canDrop}>\n *         {target.track.name ?? target.track.id}\n *       </div>\n *     );\n *   });\n * }\n * ```\n *\n * @see {@link TimelineTrackDropContext}\n * @see {@link useTimelineClipDrag}\n * @see {@link https://canvastimeline.com/docs/tracks-and-clips | Tracks and clips}\n */\nexport function useTimelineTrackDropTargets(\n  options: UseTimelineTrackDropTargetsOptions = {}\n): UseTimelineTrackDropTargetsResult {\n  const customCanDropClipOnTrack = options.canDropClipOnTrack;\n  const collapsedTrackHeight = options.collapsedTrackHeight;\n  const edgeThreshold = options.edgeThreshold;\n  const rulerHeight = options.rulerHeight;\n  const touchEdgeThreshold = options.touchEdgeThreshold;\n  const trackHeight = options.trackHeight;\n  const viewportWidth = options.viewportWidth;\n  const engine = useTimelineEngine();\n  const state = useTimelineSelector((state) => ({ tracks: state.tracks }));\n  const revision = useTimelineGeometryRevision();\n\n  const geometry = useMemo<TimelineInteractionGeometry>(\n    () => ({\n      collapsedTrackHeight,\n      edgeThreshold,\n      rulerHeight,\n      touchEdgeThreshold,\n      trackHeight,\n    }),\n    [collapsedTrackHeight, edgeThreshold, rulerHeight, touchEdgeThreshold, trackHeight]\n  );\n\n  const trackTargets = useMemo(() => {\n    void revision;\n    const tracks = state.tracks;\n    return engine.geometry\n      .getTrackRects({ ...geometry, viewportWidth })\n      .map((rect) => {\n        const track = tracks[rect.trackIndex];\n        return track === undefined\n          ? null\n          : {\n              track,\n              trackIndex: rect.trackIndex,\n              rect,\n            };\n      })\n      .filter(isTrackDropTarget);\n  }, [engine, geometry, revision, state.tracks, viewportWidth]);\n\n  const getTrackAtViewportPoint = useCallback(\n    (input: TrackHitTestInput) =>\n      engine.geometry.getTrackAtPoint({\n        ...geometry,\n        viewportWidth,\n        ...input,\n      }),\n    [engine, geometry, viewportWidth]\n  );\n\n  const canDropClipOnTrack = useCallback(\n    (clipId: string, targetTrackId: string, sourceTrackId?: string): TimelineTrackDropResult => {\n      const found = engine.geometry.getClip(clipId);\n      if (!found) {\n        return { canDrop: false, reason: 'not-found', allowCrossKindTrackMove: false };\n      }\n\n      const tracks = state.tracks;\n      const sourceTrack =\n        sourceTrackId === undefined\n          ? tracks[found.trackIndex]\n          : (tracks.find((track) => track.id === sourceTrackId) ?? null);\n      const targetTrack = tracks.find((track) => track.id === targetTrackId) ?? null;\n\n      if (!sourceTrack || !targetTrack) {\n        return { canDrop: false, reason: 'invalid-track', allowCrossKindTrackMove: false };\n      }\n      if (found.clip.movable === false || sourceTrack.locked || targetTrack.locked) {\n        return { canDrop: false, reason: 'locked', allowCrossKindTrackMove: false };\n      }\n\n      const sourceTrackIndex = tracks.findIndex((track) => track.id === sourceTrack.id);\n      const targetTrackIndex = tracks.findIndex((track) => track.id === targetTrack.id);\n      const sameKind = sourceTrack.kind === targetTrack.kind;\n\n      if (!sameKind && !customCanDropClipOnTrack) {\n        return {\n          canDrop: false,\n          reason: 'incompatible-track-kind',\n          allowCrossKindTrackMove: false,\n        };\n      }\n\n      if (!customCanDropClipOnTrack) {\n        return acceptedDropResult;\n      }\n\n      return normalizeGuardResult(\n        customCanDropClipOnTrack({\n          clip: found.clip,\n          sourceTrack,\n          targetTrack,\n          sourceTrackIndex,\n          targetTrackIndex,\n        }),\n        !sameKind\n      );\n    },\n    [customCanDropClipOnTrack, engine, state.tracks]\n  );\n\n  return useMemo(\n    () => ({\n      trackTargets,\n      getTrackAtViewportPoint,\n      canDropClipOnTrack,\n    }),\n    [canDropClipOnTrack, getTrackAtViewportPoint, trackTargets]\n  );\n}\n"],"mappings":";;;;;AAkGA,MAAM,qBAA8C;CAClD,SAAS;CACT,QAAQ;CACR,yBAAyB;AAC3B;AAEA,SAAS,kBACP,QACsC;CACtC,OAAO,WAAW;AACpB;AAEA,SAAS,qBACP,QACA,yBACyB;CACzB,IAAI,OAAO,WAAW,WACpB,OAAO;EACL,SAAS;EACT,QAAQ,SAAS,OAAO;EACxB,yBAAyB,SAAS,0BAA0B;CAC9D;CAGF,OAAO;EACL,SAAS,OAAO;EAChB,QAAQ,OAAO,UAAU,OAAQ,OAAO,UAAU;EAClD,yBAAyB,OAAO,UAC5B,OAAO,2BAA2B,0BAClC;CACN;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,4BACd,UAA8C,CAAC,GACZ;CACnC,MAAM,2BAA2B,QAAQ;CACzC,MAAM,uBAAuB,QAAQ;CACrC,MAAM,gBAAgB,QAAQ;CAC9B,MAAM,cAAc,QAAQ;CAC5B,MAAM,qBAAqB,QAAQ;CACnC,MAAM,cAAc,QAAQ;CAC5B,MAAM,gBAAgB,QAAQ;CAC9B,MAAM,SAAS,kBAAkB;CACjC,MAAM,QAAQ,qBAAqB,WAAW,EAAE,QAAQ,MAAM,OAAO,EAAE;CACvE,MAAM,WAAW,4BAA4B;CAE7C,MAAM,WAAW,eACR;EACL;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAsB;EAAe;EAAa;EAAoB;CAAW,CACpF;CAEA,MAAM,eAAe,cAAc;EAEjC,MAAM,SAAS,MAAM;EACrB,OAAO,OAAO,SACX,cAAc;GAAE,GAAG;GAAU;EAAc,CAAC,CAAC,CAC7C,KAAK,SAAS;GACb,MAAM,QAAQ,OAAO,KAAK;GAC1B,OAAO,UAAU,KAAA,IACb,OACA;IACE;IACA,YAAY,KAAK;IACjB;GACF;EACN,CAAC,CAAC,CACD,OAAO,iBAAiB;CAC7B,GAAG;EAAC;EAAQ;EAAU;EAAU,MAAM;EAAQ;CAAa,CAAC;CAE5D,MAAM,0BAA0B,aAC7B,UACC,OAAO,SAAS,gBAAgB;EAC9B,GAAG;EACH;EACA,GAAG;CACL,CAAC,GACH;EAAC;EAAQ;EAAU;CAAa,CAClC;CAEA,MAAM,qBAAqB,aACxB,QAAgB,eAAuB,kBAAoD;EAC1F,MAAM,QAAQ,OAAO,SAAS,QAAQ,MAAM;EAC5C,IAAI,CAAC,OACH,OAAO;GAAE,SAAS;GAAO,QAAQ;GAAa,yBAAyB;EAAM;EAG/E,MAAM,SAAS,MAAM;EACrB,MAAM,cACJ,kBAAkB,KAAA,IACd,OAAO,MAAM,cACZ,OAAO,MAAM,UAAU,MAAM,OAAO,aAAa,KAAK;EAC7D,MAAM,cAAc,OAAO,MAAM,UAAU,MAAM,OAAO,aAAa,KAAK;EAE1E,IAAI,CAAC,eAAe,CAAC,aACnB,OAAO;GAAE,SAAS;GAAO,QAAQ;GAAiB,yBAAyB;EAAM;EAEnF,IAAI,MAAM,KAAK,YAAY,SAAS,YAAY,UAAU,YAAY,QACpE,OAAO;GAAE,SAAS;GAAO,QAAQ;GAAU,yBAAyB;EAAM;EAG5E,MAAM,mBAAmB,OAAO,WAAW,UAAU,MAAM,OAAO,YAAY,EAAE;EAChF,MAAM,mBAAmB,OAAO,WAAW,UAAU,MAAM,OAAO,YAAY,EAAE;EAChF,MAAM,WAAW,YAAY,SAAS,YAAY;EAElD,IAAI,CAAC,YAAY,CAAC,0BAChB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,yBAAyB;EAC3B;EAGF,IAAI,CAAC,0BACH,OAAO;EAGT,OAAO,qBACL,yBAAyB;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACA;EACF,CAAC,GACD,CAAC,QACH;CACF,GACA;EAAC;EAA0B;EAAQ,MAAM;CAAM,CACjD;CAEA,OAAO,eACE;EACL;EACA;EACA;CACF,IACA;EAAC;EAAoB;EAAyB;CAAY,CAC5D;AACF"}