From 17ef81007495ccf6c02c77c77815b7413d9d127a Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Fri, 27 Jun 2025 00:30:32 +0530 Subject: [PATCH 1/2] feat: Improve timeline playhead smooth scrolling and sync --- apps/web/src/components/editor/timeline.tsx | 92 ++++++++++++++++++--- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ae6b98d..46aaf13 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -100,6 +100,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(); @@ -562,6 +584,61 @@ 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); + }; + }, [duration, zoomLevel]); + + // --- 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 (60px buffer) + const desiredScroll = Math.max( + scrollMin, + Math.min(scrollMax, playheadPx - viewportWidth / 2) + ); + if ( + playheadPx < rulerViewport.scrollLeft + 60 || + playheadPx > rulerViewport.scrollLeft + viewportWidth - 60 + ) { + rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; + } + // Keep both scrolls in sync + if (rulerViewport.scrollLeft !== tracksViewport.scrollLeft) { + rulerViewport.scrollLeft = tracksViewport.scrollLeft = Math.max(rulerViewport.scrollLeft, tracksViewport.scrollLeft); + } + }, [playheadPosition, duration, zoomLevel]); + return (
- +
{ // Calculate the clicked time position and seek to it @@ -876,17 +953,12 @@ export function Timeline() { {/* Timeline Tracks Content */}
-
- {/* Timeline grid and clips area (with left margin for sifdebar) */} +
)}
-
+
From f3c45ee8925f1fa25684a19f7e054cec5742ef76 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Fri, 27 Jun 2025 00:50:37 +0530 Subject: [PATCH 2/2] refactor: Update playhead auto-scroll buffer and remove conflicting sync --- apps/web/src/components/editor/timeline.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 46aaf13..2d60102 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -611,7 +611,7 @@ export function Timeline() { rulerViewport.removeEventListener('scroll', handleRulerScroll); tracksViewport.removeEventListener('scroll', handleTracksScroll); }; - }, [duration, zoomLevel]); + }, []); // --- Playhead auto-scroll effect --- useEffect(() => { @@ -622,21 +622,17 @@ export function Timeline() { const viewportWidth = rulerViewport.clientWidth; const scrollMin = 0; const scrollMax = rulerViewport.scrollWidth - viewportWidth; - // Center the playhead if it's not visible (60px buffer) + // 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 + 60 || - playheadPx > rulerViewport.scrollLeft + viewportWidth - 60 + playheadPx < rulerViewport.scrollLeft + 100 || + playheadPx > rulerViewport.scrollLeft + viewportWidth - 100 ) { rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; } - // Keep both scrolls in sync - if (rulerViewport.scrollLeft !== tracksViewport.scrollLeft) { - rulerViewport.scrollLeft = tracksViewport.scrollLeft = Math.max(rulerViewport.scrollLeft, tracksViewport.scrollLeft); - } }, [playheadPosition, duration, zoomLevel]); return (