diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index f4c2473..ee0a256 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -101,6 +101,28 @@ export function Timeline() { const [isScrubbing, setIsScrubbing] = useState(false); const [scrubTime, setScrubTime] = useState(null); + // Dynamic timeline width calculation based on playhead position and duration + const dynamicTimelineWidth = Math.max( + (duration || 0) * 50 * zoomLevel, // Base width from duration + (currentTime + 30) * 50 * zoomLevel, // Width to show current time + 30 seconds buffer + timelineRef.current?.clientWidth || 1000 // Minimum width + ); + + // Scroll synchronization and auto-scroll to playhead + const rulerScrollRef = useRef(null); + const tracksScrollRef = useRef(null); + const isUpdatingRef = useRef(false); + const lastRulerSync = useRef(0); + const lastTracksSync = useRef(0); + + // New refs for direct playhead DOM manipulation + const rulerPlayheadRef = useRef(null); + const tracksPlayheadRef = useRef(null); + + // Refs to store initial mouse and scroll positions for drag calculations + const initialMouseXRef = useRef(0); + const initialTimelineScrollLeftRef = useRef(0); + // Update timeline duration when tracks change useEffect(() => { const totalDuration = getTotalDuration(); @@ -640,6 +662,57 @@ export function Timeline() { }; }, [isInTimeline]); + // --- Scroll synchronization effect --- + useEffect(() => { + const rulerViewport = rulerScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + const tracksViewport = tracksScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + if (!rulerViewport || !tracksViewport) return; + const handleRulerScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastRulerSync.current < 16) return; + lastRulerSync.current = now; + isUpdatingRef.current = true; + tracksViewport.scrollLeft = rulerViewport.scrollLeft; + isUpdatingRef.current = false; + }; + const handleTracksScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastTracksSync.current < 16) return; + lastTracksSync.current = now; + isUpdatingRef.current = true; + rulerViewport.scrollLeft = tracksViewport.scrollLeft; + isUpdatingRef.current = false; + }; + rulerViewport.addEventListener('scroll', handleRulerScroll); + tracksViewport.addEventListener('scroll', handleTracksScroll); + return () => { + rulerViewport.removeEventListener('scroll', handleRulerScroll); + tracksViewport.removeEventListener('scroll', handleTracksScroll); + }; + }, []); + + // --- Playhead auto-scroll effect --- + useEffect(() => { + const rulerViewport = rulerScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + const tracksViewport = tracksScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + if (!rulerViewport || !tracksViewport) return; + const playheadPx = playheadPosition * 50 * zoomLevel; + const viewportWidth = rulerViewport.clientWidth; + const scrollMin = 0; + const scrollMax = rulerViewport.scrollWidth - viewportWidth; + // Center the playhead if it's not visible (100px buffer) + const desiredScroll = Math.max( + scrollMin, + Math.min(scrollMax, playheadPx - viewportWidth / 2) + ); + if ( + playheadPx < rulerViewport.scrollLeft + 100 || + playheadPx > rulerViewport.scrollLeft + viewportWidth - 100 + ) { + rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; + } + }, [playheadPosition, duration, zoomLevel]); + return (
- +
{ // Calculate the clicked time position and seek to it @@ -958,17 +1031,12 @@ export function Timeline() { {/* Timeline Tracks Content */}
-
- {/* Timeline grid and clips area (with left margin for sifdebar) */} +
)}
-
+