{"version":3,"file":"KeyframeTangentInteractionLayer.mjs","names":[],"sources":["../../../src/components/interactions/KeyframeTangentInteractionLayer.tsx"],"sourcesContent":["import { consumeTimelineDoubleTap } from '#react/components/interactions/tapState';\nimport { useKeyframePointer } from '#react/components/interactions/useKeyframePointer';\nimport { useTimelineKeyframeSegments, useTimelineKeyframeTangentDrag } from '#react/hooks';\nimport { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { defaultTimelineInteractionGeometry } from '@techsquidtv/canvas-timeline-core';\nimport type {\n  TimelineEngine,\n  TimelineInteractionGeometry,\n  TimelineKeyframePropertyId,\n  TimelineKeyframeTangentHandle,\n  TimelineKeyframeTangentHandleHitTestResult,\n} from '@techsquidtv/canvas-timeline-core';\nimport React, { useCallback, useMemo, useRef, useState } from 'react';\n/**\n * Details passed to a Bezier tangent handle double-click or double-tap callback.\n */\nexport interface KeyframeTangentHandleDoubleClickDetails {\n  /** Timeline engine owning the keyframe. */\n  engine: TimelineEngine;\n  /** Original pointer event. */\n  event: PointerEvent;\n}\n\n/**\n * Props for the delegated Bezier tangent interaction layer.\n */\nexport interface KeyframeTangentInteractionLayerProps\n  extends\n    Omit<React.HTMLAttributes<HTMLDivElement>, keyof TimelineInteractionGeometry>,\n    TimelineInteractionGeometry {\n  /** Keyframe property to render and hit-test. */\n  property: TimelineKeyframePropertyId;\n  /** Only render tangent handles owned by selected clips. Defaults to true. */\n  selectedClipOnly?: boolean;\n  /** Only render tangent handles touching selected keyframes. Defaults to true. */\n  selectedKeyframeOnly?: boolean;\n  /** Extra pixels around the viewport included in visible segment queries. */\n  overscanPixels?: number;\n  /** Keyframe affordance square size in CSS pixels. */\n  keyframeSize?: number;\n  /** Bezier control handle square size in CSS pixels. */\n  tangentHandleSize?: number;\n  /**\n   * Invisible pointer padding in CSS pixels added around each Bezier handle.\n   *\n   * Presses inside the padded area target the tangent handle instead of falling\n   * through to lower interaction layers such as the clip layer. Defaults to 8.\n   */\n  hitPadding?: number;\n  /** Vertical padding used when mapping keyframe values into a clip row. */\n  keyframeValuePadding?: number;\n  /** Optional handler for double-click or double-tap gestures on tangent handles. */\n  onTangentHandleDoubleClick?: (\n    handle: TimelineKeyframeTangentHandle,\n    details: KeyframeTangentHandleDoubleClickDetails\n  ) => void;\n  /** Optional accessible label formatter for a canvas-rendered tangent handle. */\n  getTangentHandleAriaLabel?: (handle: TimelineKeyframeTangentHandleHitTestResult) => string;\n}\n\n/** Delegated tangent editor with one pointer/focus target and keyboard control. */\nexport const KeyframeTangentInteractionLayer = React.forwardRef<\n  HTMLDivElement,\n  KeyframeTangentInteractionLayerProps\n>(\n  (\n    {\n      property,\n      selectedClipOnly = true,\n      selectedKeyframeOnly = true,\n      rulerHeight = defaultTimelineInteractionGeometry.rulerHeight,\n      trackHeight,\n      collapsedTrackHeight,\n      edgeThreshold,\n      touchEdgeThreshold,\n      overscanPixels,\n      keyframeSize,\n      tangentHandleSize,\n      hitPadding = 8,\n      keyframeValuePadding,\n      onTangentHandleDoubleClick,\n      getTangentHandleAriaLabel,\n      onKeyDown,\n      className = '',\n      style,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const engine = useTimelineEngine();\n    const root = useRef<HTMLDivElement>(null);\n    const [identity, setIdentity] = useState<string | null>(null);\n    const geometry = useMemo(\n      () => ({\n        property,\n        selectedClipOnly,\n        selectedKeyframeOnly,\n        rulerHeight,\n        trackHeight,\n        collapsedTrackHeight,\n        edgeThreshold,\n        touchEdgeThreshold,\n        overscanPixels,\n        keyframeSize,\n        tangentHandleSize,\n        keyframeValuePadding,\n      }),\n      [\n        property,\n        selectedClipOnly,\n        selectedKeyframeOnly,\n        rulerHeight,\n        trackHeight,\n        collapsedTrackHeight,\n        edgeThreshold,\n        touchEdgeThreshold,\n        overscanPixels,\n        keyframeSize,\n        tangentHandleSize,\n        keyframeValuePadding,\n      ]\n    );\n    const segments = useTimelineKeyframeSegments(geometry);\n    const drag = useTimelineKeyframeTangentDrag(geometry);\n    const identify = (handle: TimelineKeyframeTangentHandle) =>\n      JSON.stringify([handle.clip.id, handle.segmentId, handle.side]);\n    const current =\n      segments.visibleTangentHandles.find((handle) => identify(handle) === identity) ??\n      segments.visibleTangentHandles[0];\n    const cancelPointer = useKeyframePointer({\n      root,\n      rulerHeight,\n      priority: 1,\n      hitTest: (point) => {\n        const candidates = segments.visibleTangentHandles.filter(\n          ({ rect }) =>\n            point.x >= rect.x - hitPadding &&\n            point.x <= rect.x + rect.width + hitPadding &&\n            point.y >= rect.y - hitPadding &&\n            point.y <= rect.y + rect.height + hitPadding\n        );\n        candidates.sort(\n          (a, b) =>\n            Math.hypot(point.x - a.point.x, point.y - a.point.y) -\n            Math.hypot(point.x - b.point.x, point.y - b.point.y)\n        );\n        return candidates[0] ?? null;\n      },\n      hover: (handle) => {\n        if (handle && !drag.dragging) {\n          setIdentity(identify(handle));\n        }\n      },\n      start: (handle, point, event) => {\n        setIdentity(identify(handle));\n        if (!handle.canEdit) {\n          return false;\n        }\n        engine.keyframes.selectKeyframes(\n          [{ clipId: handle.clip.id, keyframeId: handle.anchorKeyframe.id }],\n          'add'\n        );\n        if (onTangentHandleDoubleClick && consumeTimelineDoubleTap(event)) {\n          onTangentHandleDoubleClick(handle, { engine, event });\n          return false;\n        }\n        return drag.startKeyframeTangentDrag({\n          tangentHandle: handle,\n          viewportX: point.x,\n          viewportY: point.y,\n        }).ok;\n      },\n      move: (point) => {\n        drag.moveKeyframeTangentDrag({ viewportX: point.x, viewportY: point.y });\n      },\n      end: (cancelled) => {\n        if (cancelled) {\n          drag.cancelKeyframeTangentDrag();\n        } else {\n          drag.endKeyframeTangentDrag();\n        }\n      },\n    });\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented) {\n        return;\n      }\n      if (event.key === 'Escape') {\n        event.preventDefault();\n        cancelPointer();\n        return;\n      }\n      if (!current) {\n        return;\n      }\n      if (event.key === '[' || event.key === ']') {\n        event.preventDefault();\n        const index = segments.visibleTangentHandles.indexOf(current);\n        const next =\n          segments.visibleTangentHandles[\n            (index + (event.key === ']' ? 1 : segments.visibleTangentHandles.length - 1)) %\n              segments.visibleTangentHandles.length\n          ];\n        if (next) {\n          setIdentity(identify(next));\n        }\n        return;\n      }\n      if (!current.canEdit) {\n        return;\n      }\n      if (event.key === 'Home') {\n        event.preventDefault();\n        engine.keyframes.updateClipKeyframeSide({\n          clipId: current.clip.id,\n          keyframeId: current.keyframe.id,\n          side: current.side,\n          patch: { handle: null },\n        });\n        return;\n      }\n      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {\n        return;\n      }\n      event.preventDefault();\n      const step = event.shiftKey ? 0.1 : event.altKey ? 0.001 : 0.01;\n      const x = Math.max(\n        0,\n        Math.min(\n          1,\n          current.tangent.x +\n            (event.key === 'ArrowRight' ? step : event.key === 'ArrowLeft' ? -step : 0)\n        )\n      );\n      const y = Math.max(\n        0,\n        Math.min(\n          1,\n          current.tangent.y +\n            (event.key === 'ArrowUp' ? step : event.key === 'ArrowDown' ? -step : 0)\n        )\n      );\n      engine.keyframes.updateClipKeyframeSide({\n        clipId: current.clip.id,\n        keyframeId: current.keyframe.id,\n        side: current.side,\n        patch: { interpolation: 'bezier', handle: { x, y } },\n      });\n    };\n    const ref = useCallback(\n      (node: HTMLDivElement | null) => {\n        root.current = node;\n        if (typeof forwardedRef === 'function') {\n          forwardedRef(node);\n        } else if (forwardedRef) {\n          forwardedRef.current = node;\n        }\n      },\n      [forwardedRef]\n    );\n    const label = current\n      ? (getTangentHandleAriaLabel?.(current) ??\n        `${property} ${current.side} tangent, time ${Math.round(current.tangent.x * 100)} percent, value ${Math.round(current.tangent.y * 100)} percent`)\n      : `${property} tangents`;\n    return (\n      <div\n        {...props}\n        ref={ref}\n        className={`timeline-keyframe-tangent-interaction-layer ${className}`}\n        role=\"group\"\n        tabIndex={current ? 0 : -1}\n        aria-label={label}\n        onKeyDown={handleKeyDown}\n        style={{ top: rulerHeight, ...style }}\n      >\n        <span className=\"timeline-sr-only\" aria-live=\"polite\">\n          {drag.dragging ? '' : label}\n        </span>\n        {current && (\n          <>\n            <svg className=\"timeline-keyframe-tangent-lines\" aria-hidden=\"true\">\n              <line\n                className=\"timeline-keyframe-tangent-line\"\n                x1={current.anchorPoint.x}\n                y1={current.anchorPoint.y - rulerHeight}\n                x2={current.point.x}\n                y2={current.point.y - rulerHeight}\n              />\n            </svg>\n            <div\n              className=\"timeline-keyframe-tangent-handle\"\n              aria-hidden=\"true\"\n              data-keyframe-id={current.keyframe.id}\n              data-side={current.side}\n              data-active={drag.dragging ? 'true' : undefined}\n              data-editable={current.canEdit ? 'true' : undefined}\n              style={{\n                transform: `translate(${current.rect.x - hitPadding}px, ${current.rect.y - rulerHeight - hitPadding}px)`,\n                width: current.rect.width + hitPadding * 2,\n                height: current.rect.height + hitPadding * 2,\n              }}\n            >\n              <div\n                className=\"timeline-keyframe-tangent-handle-shape\"\n                style={{ width: current.rect.width, height: current.rect.height }}\n              />\n            </div>\n          </>\n        )}\n      </div>\n    );\n  }\n);\nKeyframeTangentInteractionLayer.displayName = 'Timeline.KeyframeTangentInteractionLayer';\n"],"mappings":"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"}