{"version":3,"file":"Root.mjs","names":[],"sources":["../../../src/components/surface/Root.tsx"],"sourcesContent":["import { useTimelineEngine } from '#react/hooks/core/useTimelineEngine';\nimport { clamp, toSeconds } from '@techsquidtv/canvas-timeline-utils';\nimport React, { useEffect, useRef } from 'react';\ninterface ActivePointer {\n  clientX: number;\n  clientY: number;\n  startX: number;\n  startY: number;\n}\n\nexport const Root = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ children, className = '', ...props }, forwardedRef) => {\n    const engine = useTimelineEngine();\n    const internalRef = useRef<HTMLDivElement>(null);\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    useEffect(() => {\n      if (!internalRef.current) {\n        return;\n      }\n      const el = internalRef.current;\n\n      // Set initial viewport size.\n      const initialRect = el.getBoundingClientRect();\n      engine.setViewportWidth(initialRect.width);\n      engine.setViewportHeight(initialRect.height);\n\n      const resizeObserver = new ResizeObserver((entries) => {\n        for (const entry of entries) {\n          engine.setViewportWidth(entry.contentRect.width);\n          engine.setViewportHeight(entry.contentRect.height);\n        }\n      });\n\n      resizeObserver.observe(el);\n\n      return () => {\n        resizeObserver.disconnect();\n      };\n    }, [engine]);\n\n    useEffect(() => {\n      const el = internalRef.current;\n      if (!el) {\n        return;\n      }\n\n      const handleWheel = (event: WheelEvent) => {\n        // Zoom or pan based on standard conventions (e.g. shift+wheel or pinch zoom).\n        if (event.shiftKey) {\n          event.preventDefault();\n          engine.setScrollLeft(Math.max(0, engine.scrollLeft + event.deltaY));\n        } else if (event.ctrlKey || event.metaKey) {\n          event.preventDefault();\n          const zoomDelta = event.deltaY * -0.001;\n          const newScale = Math.max(0.01, engine.zoomScale * (1 + zoomDelta));\n          engine.setZoomScale(newScale);\n        } else {\n          event.preventDefault();\n          engine.setScrollLeft(Math.max(0, engine.scrollLeft + event.deltaX));\n          engine.setScrollTop(Math.max(0, engine.scrollTop + event.deltaY));\n        }\n      };\n\n      el.addEventListener('wheel', handleWheel, { passive: false });\n\n      return () => {\n        el.removeEventListener('wheel', handleWheel);\n      };\n    }, [engine]);\n\n    // Track active pointers for multi-touch navigation (panning & pinch-to-zoom)\n    const activePointers = useRef<Map<number, ActivePointer>>(new Map());\n    const isDragging = useRef(false);\n    const startScrollLeft = useRef(0);\n    const startScrollTop = useRef(0);\n    const startZoomScale = useRef(1);\n    const startPinchDistance = useRef(0);\n\n    const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.target === internalRef.current) {\n        engine.selectClip(null);\n      }\n\n      e.currentTarget.setPointerCapture(e.pointerId);\n\n      activePointers.current.set(e.pointerId, {\n        clientX: e.clientX,\n        clientY: e.clientY,\n        startX: e.clientX,\n        startY: e.clientY,\n      });\n\n      if (activePointers.current.size === 1) {\n        isDragging.current = true;\n        startScrollLeft.current = engine.scrollLeft;\n        startScrollTop.current = engine.scrollTop;\n      } else if (activePointers.current.size === 2) {\n        isDragging.current = false;\n        const pts = Array.from(activePointers.current.values());\n        const dx = pts[0].clientX - pts[1].clientX;\n        const dy = pts[0].clientY - pts[1].clientY;\n        startPinchDistance.current = Math.hypot(dx, dy);\n        startZoomScale.current = engine.zoomScale;\n        startScrollLeft.current = engine.scrollLeft;\n        startScrollTop.current = engine.scrollTop;\n      }\n    };\n\n    const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      const pt = activePointers.current.get(e.pointerId);\n      if (!pt) {\n        return;\n      }\n\n      pt.clientX = e.clientX;\n      pt.clientY = e.clientY;\n\n      if (activePointers.current.size === 1 && isDragging.current) {\n        const deltaX = e.clientX - pt.startX;\n        const deltaY = e.clientY - pt.startY;\n        engine.setScrollLeft(Math.max(0, startScrollLeft.current - deltaX));\n        engine.setScrollTop(Math.max(0, startScrollTop.current - deltaY));\n      } else if (activePointers.current.size === 2) {\n        const pts = Array.from(activePointers.current.values());\n        const dx = pts[0].clientX - pts[1].clientX;\n        const dy = pts[0].clientY - pts[1].clientY;\n        const currentDistance = Math.hypot(dx, dy);\n\n        if (startPinchDistance.current > 10) {\n          const ratio = currentDistance / startPinchDistance.current;\n          const newScale = clamp(\n            startZoomScale.current * ratio,\n            engine.minZoomScale,\n            engine.maxZoomScale\n          );\n\n          // Zoom centered around the pinch midpoint\n          const midX = (pts[0].clientX + pts[1].clientX) / 2;\n          if (internalRef.current) {\n            const rect = internalRef.current.getBoundingClientRect();\n            const localMidX = midX - rect.left;\n\n            // Timestamp at midpoint before the zoom change\n            const timeAtMid = engine.pixelToTime(localMidX);\n\n            engine.setZoomScale(newScale);\n\n            // Re-calculate scrollLeft to keep timeAtMid exactly under the midpoint pointer\n            const newScrollLeft = Math.max(0, toSeconds(timeAtMid) * newScale - localMidX);\n            engine.setScrollLeft(newScrollLeft);\n          } else {\n            engine.setZoomScale(newScale);\n          }\n        }\n      }\n    };\n\n    const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n      try {\n        e.currentTarget.releasePointerCapture(e.pointerId);\n      } catch {\n        // Ignore\n      }\n      activePointers.current.delete(e.pointerId);\n\n      if (activePointers.current.size === 0) {\n        isDragging.current = false;\n        engine.settle();\n      } else if (activePointers.current.size === 1) {\n        // Transition smoothly back to 1-finger dragging\n        isDragging.current = true;\n        const remainingPointerId = Array.from(activePointers.current.keys())[0];\n        const pt = activePointers.current.get(remainingPointerId);\n        if (pt) {\n          pt.startX = pt.clientX;\n          pt.startY = pt.clientY;\n        }\n        startScrollLeft.current = engine.scrollLeft;\n        startScrollTop.current = engine.scrollTop;\n      }\n    };\n\n    return (\n      <div\n        ref={ref}\n        className={`relative overflow-hidden w-full h-full timeline-root select-none touch-none ${className}`}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  }\n);\n\nRoot.displayName = 'Timeline.Root';\n"],"mappings":"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"}