{"version":3,"file":"useTimelineKeyframeTangentDrag.mjs","names":[],"sources":["../../../src/hooks/keyframes/useTimelineKeyframeTangentDrag.ts"],"sourcesContent":["import { TimelineEditGesture } from '#react/hooks/editing/timelineEditGesture';\nimport { timelineCommandFail, timelineCommandOk } from '@techsquidtv/canvas-timeline-core';\nimport type {\n  TimelineCommandResult,\n  TimelineKeyframeBezierHandle,\n  TimelineKeyframePropertyId,\n  TimelineKeyframeSegmentGeometryOptions,\n  TimelineKeyframeSide,\n  TimelineKeyframeTangentHandle,\n  TimelineKeyframeSegment,\n} from '@techsquidtv/canvas-timeline-core';\nimport { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n/** Pointer data needed to begin a Bezier tangent handle drag by ids. */\nexport interface TimelineKeyframeTangentDragIdStartInput {\n  /** Clip owning the segment. */\n  clipId: string;\n  /** Segment whose tangent is being dragged. */\n  segmentId: string;\n  /** Anchor keyframe whose side will be edited. */\n  keyframeId: string;\n  /** Keyframe side being edited. */\n  side: TimelineKeyframeSide;\n  /** Supplied handles use `TimelineKeyframeTangentDragHandleStartInput` instead. */\n  tangentHandle?: never;\n}\n\n/** Pointer data needed to begin a Bezier tangent handle drag from hit-test geometry. */\nexport interface TimelineKeyframeTangentDragHandleStartInput {\n  /** Tangent entry from the initiating hit test. */\n  tangentHandle: TimelineKeyframeTangentHandle;\n  /** Initial pointer position, used to preserve the padded grab offset. */\n  viewportX?: number;\n  /** Initial pointer position, used to preserve the padded grab offset. */\n  viewportY?: number;\n}\n\n/** Pointer data needed to begin a Bezier tangent handle drag. */\nexport type TimelineKeyframeTangentDragStartInput =\n  | TimelineKeyframeTangentDragIdStartInput\n  | TimelineKeyframeTangentDragHandleStartInput;\n\n/** Pointer data needed to update a Bezier tangent handle drag. */\nexport interface TimelineKeyframeTangentDragMoveInput {\n  /** Current pointer X in timeline viewport coordinates. */\n  viewportX: number;\n  /** Current pointer Y in timeline viewport coordinates, including the ruler area. */\n  viewportY: number;\n}\n\n/** Options accepted by `useTimelineKeyframeTangentDrag`. */\nexport interface UseTimelineKeyframeTangentDragOptions extends TimelineKeyframeSegmentGeometryOptions {\n  /** Keyframe property used to scope tangent drag geometry. */\n  property: TimelineKeyframePropertyId;\n}\n\n/** Result returned by `useTimelineKeyframeTangentDrag`. */\nexport interface UseTimelineKeyframeTangentDragResult {\n  /** Whether a Bezier tangent handle drag is currently active. */\n  dragging: boolean;\n  /** Starts a Bezier tangent handle drag. */\n  startKeyframeTangentDrag: (input: TimelineKeyframeTangentDragStartInput) => TimelineCommandResult;\n  /** Updates the active Bezier tangent handle drag preview. */\n  moveKeyframeTangentDrag: (\n    input: TimelineKeyframeTangentDragMoveInput\n  ) => TimelineCommandResult<TimelineKeyframeTangentDragUpdate>;\n  /** Ends the active Bezier tangent handle drag and settles history. */\n  endKeyframeTangentDrag: () => TimelineCommandResult;\n  /** Cancels the drag and restores committed state without an undo entry. */\n  cancelKeyframeTangentDrag: () => TimelineCommandResult;\n}\n\n/** Successful Bezier tangent handle drag update payload. */\nexport interface TimelineKeyframeTangentDragUpdate {\n  /** Clip owning the edited keyframe. */\n  clipId: string;\n  /** Segment whose tangent was dragged. */\n  segmentId: string;\n  /** Anchor keyframe whose side changed. */\n  keyframeId: string;\n  /** Keyframe side that changed. */\n  side: TimelineKeyframeSide;\n  /** Updated Bezier tangent handle. */\n  handle: TimelineKeyframeBezierHandle;\n}\n\ninterface ActiveTangentDrag {\n  gesture: TimelineEditGesture;\n  segment: TimelineKeyframeSegment;\n  valueTop: number;\n  valueHeight: number;\n  offsetX: number;\n  offsetY: number;\n  clipId: string;\n  segmentId: string;\n  keyframeId: string;\n  side: TimelineKeyframeSide;\n}\n\nfunction clampRatio(value: number) {\n  return Math.max(0, Math.min(1, value));\n}\n\nfunction isFiniteViewportPoint(input: TimelineKeyframeTangentDragMoveInput) {\n  return Number.isFinite(input.viewportX) && Number.isFinite(input.viewportY);\n}\n\n/**\n * Headless Bezier tangent drag behavior shared by package and custom timeline UIs.\n *\n * @param options - Drag geometry aligned with the renderer and hit-test layer.\n */\nexport function useTimelineKeyframeTangentDrag(\n  options: UseTimelineKeyframeTangentDragOptions\n): UseTimelineKeyframeTangentDragResult {\n  const engine = useTimelineEngine();\n  const activeDragRef = useRef<ActiveTangentDrag | null>(null);\n  const [dragging, setDragging] = useState(false);\n\n  useEffect(() => {\n    return () => {\n      if (!activeDragRef.current) {\n        return;\n      }\n\n      activeDragRef.current.gesture.cancel();\n      activeDragRef.current = null;\n      setDragging(false);\n    };\n  }, [engine]);\n\n  const geometry = useMemo(\n    () => ({\n      collapsedTrackHeight: options.collapsedTrackHeight,\n      edgeThreshold: options.edgeThreshold,\n      keyframeSize: options.keyframeSize,\n      keyframeValuePadding: options.keyframeValuePadding,\n      overscanPixels: options.overscanPixels,\n      property: options.property,\n      rulerHeight: options.rulerHeight,\n      selectedClipOnly: options.selectedClipOnly,\n      selectedKeyframeOnly: options.selectedKeyframeOnly,\n      tangentHandleSize: options.tangentHandleSize,\n      touchEdgeThreshold: options.touchEdgeThreshold,\n      trackHeight: options.trackHeight,\n      viewportHeight: options.viewportHeight,\n      viewportWidth: options.viewportWidth,\n    }),\n    [\n      options.collapsedTrackHeight,\n      options.edgeThreshold,\n      options.keyframeSize,\n      options.keyframeValuePadding,\n      options.overscanPixels,\n      options.property,\n      options.rulerHeight,\n      options.selectedClipOnly,\n      options.selectedKeyframeOnly,\n      options.tangentHandleSize,\n      options.touchEdgeThreshold,\n      options.trackHeight,\n      options.viewportHeight,\n      options.viewportWidth,\n    ]\n  );\n\n  const findTangentHandle = useCallback(\n    (input: TimelineKeyframeTangentDragStartInput) => {\n      if (input.tangentHandle !== undefined) {\n        return input.tangentHandle;\n      }\n\n      return engine.keyframes\n        .getKeyframeSegments(geometry)\n        .flatMap((segment) => segment.handles)\n        .find(\n          (handle) =>\n            handle.clip.id === input.clipId &&\n            handle.segmentId === input.segmentId &&\n            handle.keyframe.id === input.keyframeId &&\n            handle.side === input.side\n        );\n    },\n    [engine, geometry]\n  );\n\n  const startKeyframeTangentDrag = useCallback(\n    (input: TimelineKeyframeTangentDragStartInput): TimelineCommandResult => {\n      if (activeDragRef.current) {\n        return timelineCommandFail('unsupported', 'A tangent drag is already active.');\n      }\n      if (\n        input.tangentHandle !== undefined &&\n        ((input.viewportX !== undefined && !Number.isFinite(input.viewportX)) ||\n          (input.viewportY !== undefined && !Number.isFinite(input.viewportY)))\n      ) {\n        return timelineCommandFail(\n          'invalid-input',\n          'Tangent drag requires finite viewport coordinates.'\n        );\n      }\n      const handle = findTangentHandle(input);\n      const found = handle === undefined ? undefined : engine.geometry.getClip(handle.clip.id);\n\n      if (!found || !handle) {\n        return timelineCommandFail('not-found');\n      }\n      if (found.track.locked) {\n        return timelineCommandFail('locked');\n      }\n\n      const segment = engine.keyframes\n        .getKeyframeSegments(geometry)\n        .find(\n          (candidate) =>\n            candidate.clip.id === handle.clip.id && candidate.segmentId === handle.segmentId\n        );\n      const clipRect = engine.geometry.getClipRect(handle.clip.id, geometry);\n      if (!segment || !clipRect) {\n        return timelineCommandFail('not-found');\n      }\n      engine.cancelEdit();\n      activeDragRef.current = {\n        gesture: new TimelineEditGesture(engine),\n        segment,\n        valueTop: clipRect.y + (options.keyframeValuePadding ?? 7),\n        valueHeight: Math.max(1, clipRect.height - 2 * (options.keyframeValuePadding ?? 7)),\n        offsetX:\n          input.tangentHandle !== undefined && input.viewportX !== undefined\n            ? input.viewportX - handle.point.x\n            : 0,\n        offsetY:\n          input.tangentHandle !== undefined && input.viewportY !== undefined\n            ? input.viewportY - handle.point.y\n            : 0,\n        clipId: handle.clip.id,\n        segmentId: handle.segmentId,\n        keyframeId: handle.keyframe.id,\n        side: handle.side,\n      };\n      setDragging(true);\n\n      return timelineCommandOk();\n    },\n    [engine, findTangentHandle, geometry, options.keyframeValuePadding]\n  );\n\n  const moveKeyframeTangentDrag = useCallback(\n    (\n      input: TimelineKeyframeTangentDragMoveInput\n    ): TimelineCommandResult<TimelineKeyframeTangentDragUpdate> => {\n      const activeDrag = activeDragRef.current;\n      if (!activeDrag) {\n        return timelineCommandFail<TimelineKeyframeTangentDragUpdate>('unsupported');\n      }\n      if (!isFiniteViewportPoint(input)) {\n        return timelineCommandFail<TimelineKeyframeTangentDragUpdate>(\n          'invalid-input',\n          'Timeline keyframe tangent drag requires finite viewport coordinates.'\n        );\n      }\n\n      if (!activeDrag.gesture.isCurrent()) {\n        return timelineCommandFail('unsupported', 'The tangent preview was replaced.');\n      }\n      const segment = activeDrag.segment;\n      const deltaX = segment.endPoint.x - segment.startPoint.x;\n      if (Math.abs(deltaX) < 0.000001) {\n        return timelineCommandFail('unsupported');\n      }\n      const nextX = clampRatio(\n        (input.viewportX - activeDrag.offsetX - segment.startPoint.x) / deltaX\n      );\n      const yValue = clampRatio(\n        1 - (input.viewportY - activeDrag.offsetY - activeDrag.valueTop) / activeDrag.valueHeight\n      );\n      const nextHandle = { x: nextX, y: yValue };\n\n      const preview = activeDrag.gesture.publish({\n        type: 'keyframes',\n        edits: [\n          {\n            type: 'sides',\n            clipId: activeDrag.clipId,\n            keyframeId: activeDrag.keyframeId,\n            [activeDrag.side]: { interpolation: 'bezier', handle: nextHandle },\n          },\n        ],\n      });\n      if (!preview.valid) {\n        return timelineCommandFail(preview.reason ?? 'unsupported', preview.message);\n      }\n      const keyframe = preview.changedClips\n        .find((clip) => clip.id === activeDrag.clipId)\n        ?.keyframes?.find((key) => key.id === activeDrag.keyframeId);\n      const side = keyframe?.[activeDrag.side];\n      if (!side?.handle) {\n        return timelineCommandFail<TimelineKeyframeTangentDragUpdate>('unsupported');\n      }\n\n      return timelineCommandOk({\n        clipId: activeDrag.clipId,\n        segmentId: activeDrag.segmentId,\n        keyframeId: activeDrag.keyframeId,\n        side: activeDrag.side,\n        handle: side.handle,\n      });\n    },\n    []\n  );\n\n  const finish = useCallback((commit: boolean): TimelineCommandResult => {\n    const active = activeDragRef.current;\n    if (!active) {\n      return timelineCommandFail('unsupported');\n    }\n    activeDragRef.current = null;\n    setDragging(false);\n    if (commit) {\n      return active.gesture.commit();\n    }\n    active.gesture.cancel();\n    return timelineCommandOk();\n  }, []);\n  const endKeyframeTangentDrag = useCallback(() => finish(true), [finish]);\n  const cancelKeyframeTangentDrag = useCallback(() => finish(false), [finish]);\n\n  return useMemo(\n    () => ({\n      dragging,\n      startKeyframeTangentDrag,\n      moveKeyframeTangentDrag,\n      endKeyframeTangentDrag,\n      cancelKeyframeTangentDrag,\n    }),\n    [\n      cancelKeyframeTangentDrag,\n      dragging,\n      endKeyframeTangentDrag,\n      moveKeyframeTangentDrag,\n      startKeyframeTangentDrag,\n    ]\n  );\n}\n"],"mappings":";;;;;AAmGA,SAAS,WAAW,OAAe;CACjC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,SAAS,sBAAsB,OAA6C;CAC1E,OAAO,OAAO,SAAS,MAAM,SAAS,KAAK,OAAO,SAAS,MAAM,SAAS;AAC5E;;;;;;AAOA,SAAgB,+BACd,SACsC;CACtC,MAAM,SAAS,kBAAkB;CACjC,MAAM,gBAAgB,OAAiC,IAAI;CAC3D,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,gBAAgB;EACd,aAAa;GACX,IAAI,CAAC,cAAc,SACjB;GAGF,cAAc,QAAQ,QAAQ,OAAO;GACrC,cAAc,UAAU;GACxB,YAAY,KAAK;EACnB;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,WAAW,eACR;EACL,sBAAsB,QAAQ;EAC9B,eAAe,QAAQ;EACvB,cAAc,QAAQ;EACtB,sBAAsB,QAAQ;EAC9B,gBAAgB,QAAQ;EACxB,UAAU,QAAQ;EAClB,aAAa,QAAQ;EACrB,kBAAkB,QAAQ;EAC1B,sBAAsB,QAAQ;EAC9B,mBAAmB,QAAQ;EAC3B,oBAAoB,QAAQ;EAC5B,aAAa,QAAQ;EACrB,gBAAgB,QAAQ;EACxB,eAAe,QAAQ;CACzB,IACA;EACE,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV,CACF;CAEA,MAAM,oBAAoB,aACvB,UAAiD;EAChD,IAAI,MAAM,kBAAkB,KAAA,GAC1B,OAAO,MAAM;EAGf,OAAO,OAAO,UACX,oBAAoB,QAAQ,CAAC,CAC7B,SAAS,YAAY,QAAQ,OAAO,CAAC,CACrC,MACE,WACC,OAAO,KAAK,OAAO,MAAM,UACzB,OAAO,cAAc,MAAM,aAC3B,OAAO,SAAS,OAAO,MAAM,cAC7B,OAAO,SAAS,MAAM,IAC1B;CACJ,GACA,CAAC,QAAQ,QAAQ,CACnB;CAEA,MAAM,2BAA2B,aAC9B,UAAwE;EACvE,IAAI,cAAc,SAChB,OAAO,oBAAoB,eAAe,mCAAmC;EAE/E,IACE,MAAM,kBAAkB,KAAA,MACtB,MAAM,cAAc,KAAA,KAAa,CAAC,OAAO,SAAS,MAAM,SAAS,KAChE,MAAM,cAAc,KAAA,KAAa,CAAC,OAAO,SAAS,MAAM,SAAS,IAEpE,OAAO,oBACL,iBACA,oDACF;EAEF,MAAM,SAAS,kBAAkB,KAAK;EACtC,MAAM,QAAQ,WAAW,KAAA,IAAY,KAAA,IAAY,OAAO,SAAS,QAAQ,OAAO,KAAK,EAAE;EAEvF,IAAI,CAAC,SAAS,CAAC,QACb,OAAO,oBAAoB,WAAW;EAExC,IAAI,MAAM,MAAM,QACd,OAAO,oBAAoB,QAAQ;EAGrC,MAAM,UAAU,OAAO,UACpB,oBAAoB,QAAQ,CAAC,CAC7B,MACE,cACC,UAAU,KAAK,OAAO,OAAO,KAAK,MAAM,UAAU,cAAc,OAAO,SAC3E;EACF,MAAM,WAAW,OAAO,SAAS,YAAY,OAAO,KAAK,IAAI,QAAQ;EACrE,IAAI,CAAC,WAAW,CAAC,UACf,OAAO,oBAAoB,WAAW;EAExC,OAAO,WAAW;EAClB,cAAc,UAAU;GACtB,SAAS,IAAI,oBAAoB,MAAM;GACvC;GACA,UAAU,SAAS,KAAK,QAAQ,wBAAwB;GACxD,aAAa,KAAK,IAAI,GAAG,SAAS,SAAS,KAAK,QAAQ,wBAAwB,EAAE;GAClF,SACE,MAAM,kBAAkB,KAAA,KAAa,MAAM,cAAc,KAAA,IACrD,MAAM,YAAY,OAAO,MAAM,IAC/B;GACN,SACE,MAAM,kBAAkB,KAAA,KAAa,MAAM,cAAc,KAAA,IACrD,MAAM,YAAY,OAAO,MAAM,IAC/B;GACN,QAAQ,OAAO,KAAK;GACpB,WAAW,OAAO;GAClB,YAAY,OAAO,SAAS;GAC5B,MAAM,OAAO;EACf;EACA,YAAY,IAAI;EAEhB,OAAO,kBAAkB;CAC3B,GACA;EAAC;EAAQ;EAAmB;EAAU,QAAQ;CAAoB,CACpE;CAEA,MAAM,0BAA0B,aAE5B,UAC6D;EAC7D,MAAM,aAAa,cAAc;EACjC,IAAI,CAAC,YACH,OAAO,oBAAuD,aAAa;EAE7E,IAAI,CAAC,sBAAsB,KAAK,GAC9B,OAAO,oBACL,iBACA,sEACF;EAGF,IAAI,CAAC,WAAW,QAAQ,UAAU,GAChC,OAAO,oBAAoB,eAAe,mCAAmC;EAE/E,MAAM,UAAU,WAAW;EAC3B,MAAM,SAAS,QAAQ,SAAS,IAAI,QAAQ,WAAW;EACvD,IAAI,KAAK,IAAI,MAAM,IAAI,MACrB,OAAO,oBAAoB,aAAa;EAQ1C,MAAM,aAAa;GAAE,GANP,YACX,MAAM,YAAY,WAAW,UAAU,QAAQ,WAAW,KAAK,MAKtC;GAAG,GAHhB,WACb,KAAK,MAAM,YAAY,WAAW,UAAU,WAAW,YAAY,WAAW,WAEzC;EAAE;EAEzC,MAAM,UAAU,WAAW,QAAQ,QAAQ;GACzC,MAAM;GACN,OAAO,CACL;IACE,MAAM;IACN,QAAQ,WAAW;IACnB,YAAY,WAAW;KACtB,WAAW,OAAO;KAAE,eAAe;KAAU,QAAQ;IAAW;GACnE,CACF;EACF,CAAC;EACD,IAAI,CAAC,QAAQ,OACX,OAAO,oBAAoB,QAAQ,UAAU,eAAe,QAAQ,OAAO;EAK7E,MAAM,QAHW,QAAQ,aACtB,MAAM,SAAS,KAAK,OAAO,WAAW,MAAM,CAAC,EAC5C,WAAW,MAAM,QAAQ,IAAI,OAAO,WAAW,UAAU,EAAA,GACrC,WAAW;EACnC,IAAI,CAAC,MAAM,QACT,OAAO,oBAAuD,aAAa;EAG7E,OAAO,kBAAkB;GACvB,QAAQ,WAAW;GACnB,WAAW,WAAW;GACtB,YAAY,WAAW;GACvB,MAAM,WAAW;GACjB,QAAQ,KAAK;EACf,CAAC;CACH,GACA,CAAC,CACH;CAEA,MAAM,SAAS,aAAa,WAA2C;EACrE,MAAM,SAAS,cAAc;EAC7B,IAAI,CAAC,QACH,OAAO,oBAAoB,aAAa;EAE1C,cAAc,UAAU;EACxB,YAAY,KAAK;EACjB,IAAI,QACF,OAAO,OAAO,QAAQ,OAAO;EAE/B,OAAO,QAAQ,OAAO;EACtB,OAAO,kBAAkB;CAC3B,GAAG,CAAC,CAAC;CACL,MAAM,yBAAyB,kBAAkB,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC;CACvE,MAAM,4BAA4B,kBAAkB,OAAO,KAAK,GAAG,CAAC,MAAM,CAAC;CAE3E,OAAO,eACE;EACL;EACA;EACA;EACA;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;CACF,CACF;AACF"}