{"version":3,"file":"useTimelineVerticalRangeControl.mjs","names":[],"sources":["../../../src/hooks/viewport/useTimelineVerticalRangeControl.ts"],"sourcesContent":["import { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { useTimelineViewport } from '#react/hooks/viewport/useTimelineViewport';\nimport { useRangeScrollbar } from '#react/rangeScrollbar';\nimport type {\n  RangeScrollbarHandleSide,\n  RangeScrollbarRootProps,\n  RangeScrollbarValue,\n  RangeScrollbarValueChangeDetails,\n  UseRangeScrollbarResult,\n} from '#react/rangeScrollbar';\nimport { defaultTimelineInteractionGeometry } from '@techsquidtv/canvas-timeline-core';\nimport type { TimelineTrackHeightUpdate } from '@techsquidtv/canvas-timeline-core';\nimport { clamp } from '@techsquidtv/canvas-timeline-utils';\nimport { useCallback, useMemo, useRef } from 'react';\nconst KEYBOARD_NUDGE_PX = 40;\nconst KEYBOARD_PAGE_NUDGE_RATIO = 0.8;\nconst MIN_VERTICAL_TRACK_HEIGHT = defaultTimelineInteractionGeometry.collapsedTrackHeight;\nconst MAX_VERTICAL_TRACK_HEIGHT = defaultTimelineInteractionGeometry.trackHeight * 4;\n\nfunction formatPixels(value: number) {\n  return `${Math.round(value)} pixels`;\n}\n\ninterface VerticalZoomDragSession {\n  id: number;\n  side: RangeScrollbarHandleSide;\n  startValue: RangeScrollbarValue;\n  trackHeights: Map<string, number>;\n}\n\n/**\n * Controlled props required by `RangeScrollbar.Root` for vertical timeline scrolling.\n */\nexport type TimelineVerticalRangeControlRootProps = Pick<\n  RangeScrollbarRootProps,\n  | 'keyboardPageStep'\n  | 'keyboardStep'\n  | 'max'\n  | 'min'\n  | 'minSpan'\n  | 'onValueChange'\n  | 'orientation'\n  | 'value'\n> &\n  Required<Pick<RangeScrollbarRootProps, 'getAriaValueText'>>;\n\n/**\n * State and control props for rendering a vertical timeline scrollbar.\n */\nexport interface UseTimelineVerticalRangeControlResult {\n  /** Formatted visible vertical range and height. */\n  valueText: string;\n  /** Formatted visible top offset. */\n  startValueText: string;\n  /** Formatted visible bottom offset. */\n  endValueText: string;\n  /** Generic range scrollbar state derived from vertical timeline viewport state. */\n  range: UseRangeScrollbarResult;\n  /** Props that wire `RangeScrollbar.Root` to timeline vertical scroll state. */\n  rootProps: TimelineVerticalRangeControlRootProps;\n  /** Full vertical track stack height represented by the scrollbar domain, in pixels. */\n  contentHeight: number;\n  /** Visible viewport top offset in pixels. */\n  viewStartPixels: number;\n  /** Visible viewport bottom offset in pixels. */\n  viewEndPixels: number;\n  /** Visible viewport height in pixels. */\n  viewHeight: number;\n  /** Current vertical timeline scroll offset in pixels. */\n  scrollTop: number;\n  /** Maximum vertical timeline scroll offset in pixels. */\n  maxScrollTop: number;\n  /** Handles controlled range changes by panning or vertically zooming track rows. */\n  onValueChange: (value: RangeScrollbarValue, details: RangeScrollbarValueChangeDetails) => void;\n}\n\n/**\n * Adapts generic range scrollbar state to the timeline's vertical track viewport.\n *\n * The hook derives a controlled `{ start, end }` value from `scrollTop`,\n * `viewportHeight`, and track-stack bounds. Thumb changes pan the engine-owned\n * vertical scroll position so canvas rows and external DOM chrome can stay in sync.\n * Handle changes scale expanded track heights, giving the range handles a\n * vertical zoom behavior that matches the horizontal viewport scrollbar's shape.\n *\n * @returns Timeline vertical metrics plus `RangeScrollbar.Root` control props.\n */\nexport function useTimelineVerticalRangeControl(): UseTimelineVerticalRangeControlResult {\n  const engine = useTimelineEngine();\n  const viewport = useTimelineViewport();\n  const zoomDragSessionRef = useRef<VerticalZoomDragSession | null>(null);\n  const metrics = useMemo(() => {\n    const viewHeight = viewport.viewportHeight;\n    const contentHeight = Math.max(viewHeight, viewport.maxScrollTop + viewHeight);\n    const viewStartPixels = Math.min(viewport.scrollTop, viewport.maxScrollTop);\n    const viewEndPixels = Math.min(contentHeight, viewStartPixels + viewHeight);\n    const value = { start: viewStartPixels, end: viewEndPixels };\n    const minSpan = Math.min(viewHeight, defaultTimelineInteractionGeometry.trackHeight);\n\n    return {\n      contentHeight,\n      maxScrollTop: viewport.maxScrollTop,\n      minSpan,\n      scrollTop: viewport.scrollTop,\n      value,\n      viewEndPixels,\n      viewHeight,\n      viewStartPixels,\n    };\n  }, [viewport.maxScrollTop, viewport.scrollTop, viewport.viewportHeight]);\n\n  const onValueChange = useCallback(\n    (nextValue: RangeScrollbarValue, details: RangeScrollbarValueChangeDetails) => {\n      if (details.reason !== 'handle-drag' && details.reason !== 'handle-keyboard') {\n        zoomDragSessionRef.current = null;\n        engine.setScrollTop(nextValue.start);\n        return;\n      }\n\n      const dragSession =\n        details.reason === 'handle-drag' && details.dragSessionId !== undefined\n          ? zoomDragSessionRef.current?.id === details.dragSessionId &&\n            zoomDragSessionRef.current.side === details.side\n            ? zoomDragSessionRef.current\n            : {\n                id: details.dragSessionId,\n                side: details.side ?? 'end',\n                startValue: details.dragStartValue ?? metrics.value,\n                trackHeights: new Map(\n                  engine.tracks.map((track) => [\n                    track.id,\n                    track.height ?? defaultTimelineInteractionGeometry.trackHeight,\n                  ])\n                ),\n              }\n          : null;\n\n      if (dragSession) {\n        zoomDragSessionRef.current = dragSession;\n      }\n\n      const startValue = dragSession?.startValue ?? metrics.value;\n      const currentSpan = Math.max(metrics.minSpan, startValue.end - startValue.start);\n      const nextSpan = Math.max(metrics.minSpan, nextValue.end - nextValue.start);\n      const scaleFactor = currentSpan / nextSpan;\n      const nextScrollTop =\n        details.side === 'start'\n          ? nextValue.end * scaleFactor - metrics.viewHeight\n          : nextValue.start * scaleFactor;\n\n      if (Number.isFinite(scaleFactor) && Math.abs(scaleFactor - 1) > 0.001) {\n        const heightUpdates: TimelineTrackHeightUpdate[] = [];\n        for (const track of engine.tracks) {\n          if (track.collapsed) {\n            continue;\n          }\n\n          const currentHeight =\n            dragSession?.trackHeights.get(track.id) ??\n            track.height ??\n            defaultTimelineInteractionGeometry.trackHeight;\n          const nextHeight = clamp(\n            Math.round(currentHeight * scaleFactor),\n            MIN_VERTICAL_TRACK_HEIGHT,\n            MAX_VERTICAL_TRACK_HEIGHT\n          );\n          heightUpdates.push({ trackId: track.id, height: nextHeight });\n        }\n        engine.setTrackHeights(heightUpdates, { scrollTop: nextScrollTop });\n        return;\n      }\n\n      engine.setScrollTop(nextScrollTop);\n    },\n    [engine, metrics.minSpan, metrics.value, metrics.viewHeight]\n  );\n\n  const range = useRangeScrollbar({\n    min: 0,\n    max: metrics.contentHeight,\n    value: metrics.value,\n    minSpan: metrics.minSpan,\n    onValueChange,\n  });\n\n  const valueText = `${formatPixels(metrics.viewStartPixels)} to ${formatPixels(metrics.viewEndPixels)}, height ${formatPixels(metrics.viewHeight)}`;\n  return {\n    valueText,\n    startValueText: formatPixels(metrics.viewStartPixels),\n    endValueText: formatPixels(metrics.viewEndPixels),\n    range,\n    rootProps: {\n      getAriaValueText: (value, details) =>\n        details.part === 'thumb' ? valueText : formatPixels(value),\n      min: 0,\n      max: metrics.contentHeight,\n      value: metrics.value,\n      minSpan: metrics.minSpan,\n      orientation: 'vertical',\n      keyboardStep: KEYBOARD_NUDGE_PX,\n      keyboardPageStep: metrics.viewHeight * KEYBOARD_PAGE_NUDGE_RATIO,\n      onValueChange,\n    },\n    contentHeight: metrics.contentHeight,\n    viewStartPixels: metrics.viewStartPixels,\n    viewEndPixels: metrics.viewEndPixels,\n    viewHeight: metrics.viewHeight,\n    scrollTop: metrics.scrollTop,\n    maxScrollTop: metrics.maxScrollTop,\n    onValueChange,\n  };\n}\n"],"mappings":"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"}