{"version":3,"file":"RangeSelector.mjs","names":[],"sources":["../../../src/components/controls/RangeSelector.tsx"],"sourcesContent":["import { formatTimelineTimeValue } from '#react/accessibility';\nimport { useTimelineInOutRangeControl } from '#react/hooks';\nimport type { TimelineInOutRangeControlOptions } from '#react/hooks';\nimport { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { useTimelineSelector } from '#react/hooks/core/useTimelineSelector';\nimport { Slider } from '@base-ui/react/slider';\nimport type { TimelineEngine } from '@techsquidtv/canvas-timeline-core';\nimport { toSeconds } from '@techsquidtv/canvas-timeline-utils';\nimport type { RationalTime } from '@techsquidtv/canvas-timeline-utils';\nimport React from 'react';\n/** Timeline range boundary controlled by an In/Out grabber. */\nexport type InOutBoundary = 'in' | 'out';\n\n/** Render-prop payload for custom range selector grabber children. */\nexport interface RangeSelectorGrabberRenderProps {\n  /** Whether the grabber is actively being dragged. */\n  dragging: boolean;\n  /** Current boundary time. */\n  time: RationalTime;\n  /** Timeline engine that owns the boundary. */\n  engine: TimelineEngine;\n  /** Which In/Out boundary this grabber controls. */\n  boundary: InOutBoundary;\n}\n\n/** Custom node or render prop used inside an In/Out range selector grabber. */\nexport type RangeSelectorGrabberChildren =\n  | React.ReactNode\n  | ((props: RangeSelectorGrabberRenderProps) => React.ReactNode);\n\n/**\n * Props for the RangeSelector Root component.\n */\nexport interface RangeSelectorRootProps extends Omit<\n  React.ComponentPropsWithoutRef<typeof Slider.Root>,\n  'value' | 'defaultValue' | 'onValueChange' | 'onValueCommitted'\n> {\n  /** Optional custom slider parts rendered inside the range selector root. */\n  children?: React.ReactNode;\n  /** Whether pointer-driven changes should snap to timeline targets. */\n  snap?: boolean;\n  /** Called after a range value commit has settled the engine. */\n  onValueCommitted?: TimelineInOutRangeControlOptions['onValueCommitted'];\n}\n\n/**\n * Headless, provider-aware root component for selection/loop boundary range sliders.\n * Wired directly to the `TimelineEngine`'s inPoint and outPoint state.\n */\nexport const RangeSelectorRoot = React.forwardRef<HTMLDivElement, RangeSelectorRootProps>(\n  (\n    { children, className = '', min = 0, max, onPointerDown, onValueCommitted, step, ...props },\n    ref\n  ) => {\n    const { snap = false, ...rootProps } = props;\n    const rangeControl = useTimelineInOutRangeControl({ min, max, onValueCommitted, snap, step });\n\n    return (\n      <Slider.Root\n        ref={ref}\n        {...rootProps}\n        {...rangeControl.rootProps}\n        className={['timeline-range-selector', className].filter(Boolean).join(' ')}\n        onPointerDown={(e) => {\n          e.stopPropagation();\n          onPointerDown?.(e);\n        }}\n      >\n        {children}\n      </Slider.Root>\n    );\n  }\n);\n\nRangeSelectorRoot.displayName = 'Timeline.RangeSelector.Root';\n\n/**\n * Props for the high-level timeline range selector overlay.\n */\nexport interface RangeSelectorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  /** Minimum timeline time in seconds. Defaults to 0. */\n  min?: number;\n  /** Maximum timeline time in seconds. Defaults to timeline content end or 100. */\n  max?: number;\n  /** Whether pointer-driven boundary drags should snap to timeline targets. */\n  snap?: boolean;\n  /** Fallback slider step used when deriving an empty content max. */\n  step?: number;\n  /** Custom composable In-point grabber node or render prop function. */\n  inPointChildren?: RangeSelectorGrabberChildren;\n  /** Custom composable Out-point grabber node or render prop function. */\n  outPointChildren?: RangeSelectorGrabberChildren;\n}\n\n/**\n * Slider control wrapper for range selection.\n */\nexport const RangeSelectorControl = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentPropsWithoutRef<typeof Slider.Control>\n>(({ className = '', ...props }, ref) => {\n  return (\n    <Slider.Control\n      ref={ref}\n      className={['timeline-range-selector-control', className].filter(Boolean).join(' ')}\n      {...props}\n    />\n  );\n});\n\nRangeSelectorControl.displayName = 'Timeline.RangeSelector.Control';\n\n/**\n * Slider track wrapper for range selection.\n */\nexport const RangeSelectorTrack = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentPropsWithoutRef<typeof Slider.Track>\n>(({ className = '', ...props }, ref) => {\n  return (\n    <Slider.Track\n      ref={ref}\n      className={['timeline-range-selector-track', className].filter(Boolean).join(' ')}\n      {...props}\n    />\n  );\n});\n\nRangeSelectorTrack.displayName = 'Timeline.RangeSelector.Track';\n\n/**\n * Slider indicator wrapper for range selection.\n */\nexport const RangeSelectorIndicator = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentPropsWithoutRef<typeof Slider.Indicator>\n>(({ className = '', ...props }, ref) => {\n  return (\n    <Slider.Indicator\n      ref={ref}\n      className={['timeline-range-selector-indicator', className].filter(Boolean).join(' ')}\n      {...props}\n    />\n  );\n});\n\nRangeSelectorIndicator.displayName = 'Timeline.RangeSelector.Indicator';\n\nfunction getDefaultThumbLabel(index: number | undefined) {\n  if (index === 0) {\n    return 'In point';\n  }\n  if (index === 1) {\n    return 'Out point';\n  }\n  return 'Timeline range point';\n}\n\n/**\n * Slider thumb wrapper for range selection.\n */\nexport const RangeSelectorThumb = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentPropsWithoutRef<typeof Slider.Thumb>\n>(({ className = '', getAriaLabel, getAriaValueText, index, ...props }, ref) => {\n  const ariaLabel = props['aria-label'];\n  const ariaValueText = props['aria-valuetext'];\n\n  return (\n    <Slider.Thumb\n      ref={ref}\n      className={['timeline-range-selector-thumb', className].filter(Boolean).join(' ')}\n      getAriaLabel={\n        getAriaLabel ?? (ariaLabel === undefined ? () => getDefaultThumbLabel(index) : undefined)\n      }\n      getAriaValueText={\n        getAriaValueText ??\n        (ariaValueText === undefined\n          ? (_formattedValue, value) => formatTimelineTimeValue(value)\n          : undefined)\n      }\n      index={index}\n      {...props}\n    />\n  );\n});\n\nRangeSelectorThumb.displayName = 'Timeline.RangeSelector.Thumb';\n\ninterface RangeBoundaryGrabberProps {\n  boundary: InOutBoundary;\n  children?: RangeSelectorGrabberChildren;\n  engine: TimelineEngine;\n  time: RationalTime;\n}\n\nfunction RangeBoundaryGrabber({ boundary, children, engine, time }: RangeBoundaryGrabberProps) {\n  const [dragging, setDragging] = React.useState(false);\n  const cleanupRef = React.useRef<(() => void) | null>(null);\n\n  React.useEffect(() => {\n    return () => {\n      cleanupRef.current?.();\n    };\n  }, []);\n\n  const renderChildren =\n    typeof children === 'function'\n      ? children({\n          boundary,\n          dragging,\n          engine,\n          time,\n        })\n      : children;\n  const defaultChildren = (\n    <div className=\"timeline-time-grabber-highlight timeline-range-selector-grabber-highlight\">\n      <div className=\"timeline-time-grabber-line timeline-range-selector-grabber-line\" />\n      <div className=\"timeline-time-grabber-handle timeline-range-selector-grabber-handle\" />\n    </div>\n  );\n\n  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (event.defaultPrevented || (event.pointerType !== 'touch' && event.button !== 0)) {\n      return;\n    }\n\n    const target = event.currentTarget;\n    const cleanup = () => {\n      window.removeEventListener('pointerup', cleanup);\n      window.removeEventListener('pointercancel', cleanup);\n      target.removeEventListener('lostpointercapture', cleanup);\n      cleanupRef.current = null;\n      setDragging(false);\n    };\n\n    cleanupRef.current?.();\n    cleanupRef.current = cleanup;\n    window.addEventListener('pointerup', cleanup);\n    window.addEventListener('pointercancel', cleanup);\n    target.addEventListener('lostpointercapture', cleanup);\n    setDragging(true);\n  };\n\n  return (\n    <RangeSelectorThumb\n      aria-label={boundary === 'in' ? 'In point' : 'Out point'}\n      className=\"timeline-range-selector-grabber\"\n      data-boundary={boundary}\n      data-index={boundary === 'in' ? 0 : 1}\n      index={boundary === 'in' ? 0 : 1}\n      onPointerDown={handlePointerDown}\n      title={boundary === 'in' ? 'Drag In point' : 'Drag Out point'}\n    >\n      {renderChildren ?? defaultChildren}\n    </RangeSelectorThumb>\n  );\n}\n\nconst RangeSelectorComponent = React.forwardRef<HTMLDivElement, RangeSelectorProps>(\n  (\n    {\n      className = '',\n      inPointChildren,\n      max,\n      min = 0,\n      onPointerDown,\n      outPointChildren,\n      snap = true,\n      step,\n      style,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const engine = useTimelineEngine();\n    const state = useTimelineSelector((state) => ({\n      inPoint: state.inPoint,\n      outPoint: state.outPoint,\n    }));\n    const internalRef = React.useRef<HTMLDivElement>(null);\n\n    const resolveMax = React.useCallback(() => {\n      if (max !== undefined) {\n        return max;\n      }\n      const contentMax = toSeconds(engine.maxContentTime);\n      return contentMax > min ? contentMax : Math.max(100, min + (step ?? 0.01));\n    }, [engine, max, min, step]);\n\n    const updateGeometry = React.useCallback(() => {\n      if (!internalRef.current) {\n        return;\n      }\n\n      const width = Math.max(0, resolveMax() - min) * engine.zoomScale;\n      const x = min * engine.zoomScale - engine.scrollLeft;\n      internalRef.current.style.width = `${width}px`;\n      internalRef.current.style.transform = `translateX(${x}px)`;\n    }, [engine, min, resolveMax]);\n\n    const ref = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        internalRef.current = node;\n        if (typeof forwardedRef === 'function') {\n          forwardedRef(node);\n        } else if (forwardedRef) {\n          forwardedRef.current = node;\n        }\n      },\n      [forwardedRef]\n    );\n\n    React.useEffect(() => {\n      updateGeometry();\n      const unsubRender = engine.on('render', updateGeometry);\n      const unsubSettled = engine.on('state:settled', updateGeometry);\n      const unsubHistory = engine.on('history:change', updateGeometry);\n      return () => {\n        unsubRender();\n        unsubSettled();\n        unsubHistory();\n      };\n    }, [engine, updateGeometry]);\n\n    const resolvedMax = resolveMax();\n\n    return (\n      <RangeSelectorRoot\n        ref={ref}\n        className={['timeline-range-selector-overlay', className].filter(Boolean).join(' ')}\n        max={resolvedMax}\n        min={min}\n        onPointerDown={onPointerDown}\n        snap={snap}\n        step={step}\n        style={style}\n        {...props}\n      >\n        <RangeSelectorControl>\n          <RangeSelectorTrack>\n            <RangeSelectorIndicator />\n            {state.inPoint ? (\n              <RangeBoundaryGrabber boundary=\"in\" engine={engine} time={state.inPoint}>\n                {inPointChildren}\n              </RangeBoundaryGrabber>\n            ) : null}\n            {state.outPoint ? (\n              <RangeBoundaryGrabber boundary=\"out\" engine={engine} time={state.outPoint}>\n                {outPointChildren}\n              </RangeBoundaryGrabber>\n            ) : null}\n          </RangeSelectorTrack>\n        </RangeSelectorControl>\n      </RangeSelectorRoot>\n    );\n  }\n);\n\nRangeSelectorComponent.displayName = 'Timeline.RangeSelector';\n\n// oxlint-disable-next-line react-refresh/only-export-components\nexport const RangeSelector = Object.assign(RangeSelectorComponent, {\n  Root: RangeSelectorRoot,\n  Control: RangeSelectorControl,\n  Track: RangeSelectorTrack,\n  Indicator: RangeSelectorIndicator,\n  Thumb: RangeSelectorThumb,\n});\n"],"mappings":"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"}