{"version":3,"file":"useTimelineClipTrim.mjs","names":[],"sources":["../../../src/hooks/clips/useTimelineClipTrim.ts"],"sourcesContent":["import { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { runTimelineCommand } from '#react/hooks/core/runTimelineCommand';\nimport { TimelineEditGesture } from '#react/hooks/editing/timelineEditGesture';\nimport { timelineCommandFail, timelineCommandOk } from '@techsquidtv/canvas-timeline-core';\nimport type {\n  TimelineCommandResult,\n  TimelineEditPreview,\n  TimelineTrimEditCommand,\n} from '@techsquidtv/canvas-timeline-core';\nimport { addRational, fromSeconds } from '@techsquidtv/canvas-timeline-utils';\nimport type { RationalTime } from '@techsquidtv/canvas-timeline-utils';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\n/** Data captured when a clip edge starts a pointer trim. */\nexport interface TimelineClipTrimStartInput {\n  /** Clip whose edge will move. */\n  clipId: string;\n  /** Edge being trimmed. */\n  edge: TimelineTrimEditCommand['edge'];\n  /** Initial horizontal client coordinate. */\n  clientX: number;\n}\n\n/** Result returned by `useTimelineClipTrim`. */\nexport interface UseTimelineClipTrimResult {\n  /** Whether this hook owns an active trim gesture. */\n  trimming: boolean;\n  /** Captures the clip edge and zoom, and prepares snapping. */\n  startClipTrim: (input: TimelineClipTrimStartInput) => TimelineCommandResult;\n  /** Publishes a non-mutating preview using the captured start geometry. */\n  moveClipTrim: (\n    input: Pick<TimelineClipTrimStartInput, 'clientX'>\n  ) => TimelineCommandResult<TimelineEditPreview>;\n  /** Revalidates and commits this gesture as one undo step. A click alone creates no edit. */\n  endClipTrim: () => TimelineCommandResult;\n  /** Discards this gesture's preview and snap guides without changing the document. */\n  cancelClipTrim: () => TimelineCommandResult;\n}\n\ninterface ActiveTrim extends TimelineClipTrimStartInput {\n  startTime: RationalTime;\n  zoomScale: number;\n  gesture: TimelineEditGesture;\n}\n\n/**\n * Headless pointer trimming for custom DOM or canvas clip edges.\n *\n * Capture the pointer on your element after a successful start; forward moves,\n * pointer up, and cancellation to the corresponding commands. The hook captures\n * start geometry once, delegates snapping and edit policy to Core, and cancels\n * its preview on unmount. It has no DOM or per-frame state subscription.\n * Compose with `useTimelineEditPreview` for live preview UI.\n *\n * @returns Trim gesture state and start, move, commit, and cancel commands.\n * @example\n * ```tsx\n * const trim = useTimelineClipTrim();\n * return <button\n *   aria-label=\"Trim clip end\"\n *   onPointerDown={(event) => {\n *     if (trim.startClipTrim({ clipId: 'clip', edge: 'end', clientX: event.clientX }).ok) {\n *       event.currentTarget.setPointerCapture(event.pointerId);\n *     }\n *   }}\n *   onPointerMove={(event) => { trim.moveClipTrim({ clientX: event.clientX }); }}\n *   onPointerUp={() => { trim.endClipTrim(); }}\n *   onPointerCancel={() => { trim.cancelClipTrim(); }}\n *   onLostPointerCapture={() => { trim.cancelClipTrim(); }}\n * />;\n * ```\n */\nexport function useTimelineClipTrim(): UseTimelineClipTrimResult {\n  const engine = useTimelineEngine();\n  const activeRef = useRef<ActiveTrim | null>(null);\n  const [trimming, setTrimming] = useState(false);\n\n  useEffect(\n    () => () => {\n      const active = activeRef.current;\n      activeRef.current = null;\n      if (active) {\n        active.gesture.cancel();\n        setTrimming(false);\n      }\n    },\n    [engine]\n  );\n\n  const startClipTrim = useCallback(\n    (input: TimelineClipTrimStartInput): TimelineCommandResult => {\n      if (activeRef.current) {\n        return timelineCommandFail('unsupported', 'A trim is already active.');\n      }\n      if (!Number.isFinite(input.clientX) || (input.edge !== 'start' && input.edge !== 'end')) {\n        return timelineCommandFail('invalid-input');\n      }\n      const found = engine.geometry.getClip(input.clipId);\n      if (!found) {\n        return timelineCommandFail('not-found');\n      }\n      if (found.track.locked || found.clip.resizable === false) {\n        return timelineCommandFail('locked');\n      }\n      engine.cancelEdit();\n      engine.prepareSnapping({ ignoreClipId: input.clipId, operation: 'trim' });\n      activeRef.current = {\n        ...input,\n        startTime: {\n          ...(input.edge === 'start' ? found.clip.timelineStart : found.clip.timelineEnd),\n        },\n        zoomScale: engine.zoomScale,\n        gesture: new TimelineEditGesture(engine),\n      };\n      setTrimming(true);\n      return timelineCommandOk();\n    },\n    [engine]\n  );\n\n  const moveClipTrim = useCallback(\n    (\n      input: Pick<TimelineClipTrimStartInput, 'clientX'>\n    ): TimelineCommandResult<TimelineEditPreview> =>\n      runTimelineCommand(() => {\n        const active = activeRef.current;\n        if (!active) {\n          return timelineCommandFail('unsupported');\n        }\n        if (!Number.isFinite(input.clientX)) {\n          return timelineCommandFail('invalid-input');\n        }\n        if (!active.gesture.isCurrent()) {\n          return timelineCommandFail('unsupported', 'The trim preview was replaced.');\n        }\n        const preview = active.gesture.publish({\n          type: 'trim',\n          overwrite: true,\n          clipId: active.clipId,\n          edge: active.edge,\n          newTime: addRational(\n            active.startTime,\n            fromSeconds((input.clientX - active.clientX) / active.zoomScale, active.startTime.r)\n          ),\n        });\n        return preview.valid\n          ? timelineCommandOk(preview)\n          : timelineCommandFail(preview.reason ?? 'unsupported', preview.message);\n      }),\n    []\n  );\n\n  const endClipTrim = useCallback((): TimelineCommandResult => {\n    const active = activeRef.current;\n    if (!active) {\n      return timelineCommandFail('unsupported');\n    }\n    activeRef.current = null;\n    setTrimming(false);\n    return active.gesture.commit();\n  }, []);\n\n  const cancelClipTrim = useCallback((): TimelineCommandResult => {\n    const active = activeRef.current;\n    if (!active) {\n      return timelineCommandFail('unsupported');\n    }\n    activeRef.current = null;\n    setTrimming(false);\n    active.gesture.cancel();\n    return timelineCommandOk();\n  }, []);\n\n  return useMemo(\n    () => ({ trimming, startClipTrim, moveClipTrim, endClipTrim, cancelClipTrim }),\n    [trimming, startClipTrim, moveClipTrim, endClipTrim, cancelClipTrim]\n  );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,sBAAiD;CAC/D,MAAM,SAAS,kBAAkB;CACjC,MAAM,YAAY,OAA0B,IAAI;CAChD,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,sBACc;EACV,MAAM,SAAS,UAAU;EACzB,UAAU,UAAU;EACpB,IAAI,QAAQ;GACV,OAAO,QAAQ,OAAO;GACtB,YAAY,KAAK;EACnB;CACF,GACA,CAAC,MAAM,CACT;CAEA,MAAM,gBAAgB,aACnB,UAA6D;EAC5D,IAAI,UAAU,SACZ,OAAO,oBAAoB,eAAe,2BAA2B;EAEvE,IAAI,CAAC,OAAO,SAAS,MAAM,OAAO,KAAM,MAAM,SAAS,WAAW,MAAM,SAAS,OAC/E,OAAO,oBAAoB,eAAe;EAE5C,MAAM,QAAQ,OAAO,SAAS,QAAQ,MAAM,MAAM;EAClD,IAAI,CAAC,OACH,OAAO,oBAAoB,WAAW;EAExC,IAAI,MAAM,MAAM,UAAU,MAAM,KAAK,cAAc,OACjD,OAAO,oBAAoB,QAAQ;EAErC,OAAO,WAAW;EAClB,OAAO,gBAAgB;GAAE,cAAc,MAAM;GAAQ,WAAW;EAAO,CAAC;EACxE,UAAU,UAAU;GAClB,GAAG;GACH,WAAW,EACT,GAAI,MAAM,SAAS,UAAU,MAAM,KAAK,gBAAgB,MAAM,KAAK,YACrE;GACA,WAAW,OAAO;GAClB,SAAS,IAAI,oBAAoB,MAAM;EACzC;EACA,YAAY,IAAI;EAChB,OAAO,kBAAkB;CAC3B,GACA,CAAC,MAAM,CACT;CAEA,MAAM,eAAe,aAEjB,UAEA,yBAAyB;EACvB,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QACH,OAAO,oBAAoB,aAAa;EAE1C,IAAI,CAAC,OAAO,SAAS,MAAM,OAAO,GAChC,OAAO,oBAAoB,eAAe;EAE5C,IAAI,CAAC,OAAO,QAAQ,UAAU,GAC5B,OAAO,oBAAoB,eAAe,gCAAgC;EAE5E,MAAM,UAAU,OAAO,QAAQ,QAAQ;GACrC,MAAM;GACN,WAAW;GACX,QAAQ,OAAO;GACf,MAAM,OAAO;GACb,SAAS,YACP,OAAO,WACP,aAAa,MAAM,UAAU,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU,CAAC,CACrF;EACF,CAAC;EACD,OAAO,QAAQ,QACX,kBAAkB,OAAO,IACzB,oBAAoB,QAAQ,UAAU,eAAe,QAAQ,OAAO;CAC1E,CAAC,GACH,CAAC,CACH;CAEA,MAAM,cAAc,kBAAyC;EAC3D,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QACH,OAAO,oBAAoB,aAAa;EAE1C,UAAU,UAAU;EACpB,YAAY,KAAK;EACjB,OAAO,OAAO,QAAQ,OAAO;CAC/B,GAAG,CAAC,CAAC;CAEL,MAAM,iBAAiB,kBAAyC;EAC9D,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QACH,OAAO,oBAAoB,aAAa;EAE1C,UAAU,UAAU;EACpB,YAAY,KAAK;EACjB,OAAO,QAAQ,OAAO;EACtB,OAAO,kBAAkB;CAC3B,GAAG,CAAC,CAAC;CAEL,OAAO,eACE;EAAE;EAAU;EAAe;EAAc;EAAa;CAAe,IAC5E;EAAC;EAAU;EAAe;EAAc;EAAa;CAAc,CACrE;AACF"}