From d154937ac2fdf5877095c4a0f1076cba04d7c3d1 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sat, 26 Jul 2025 22:25:57 +0200 Subject: feat: lock playhead to frame (instead of floating) and add autoscroll to timeline --- apps/web/src/components/editor/timeline/index.tsx | 5 +- .../editor/timeline/timeline-playhead.tsx | 75 +++++++++++++++++++--- 2 files changed, 70 insertions(+), 10 deletions(-) (limited to 'apps/web/src/components/editor/timeline') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index bcaece89..bdb4433c 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -537,12 +537,13 @@ export function Timeline() { tracks={tracks} timelineRef={timelineRef} trackLabelsRef={trackLabelsRef} + tracksScrollRef={tracksScrollRef} isVisible={showSnapIndicator} /> {/* Timeline Header with Ruler */}
{/* Track Labels Header */} -
+
{/* Empty space */} . @@ -651,7 +652,7 @@ export function Timeline() { {tracks.length > 0 && (
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index d0aea7fa..b33166c0 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -1,11 +1,8 @@ "use client"; -import { useRef } from "react"; +import { useRef, useState, useEffect } from "react"; import { TimelineTrack } from "@/types/timeline"; -import { - TIMELINE_CONSTANTS, - getTotalTracksHeight, -} from "@/constants/timeline-constants"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead"; interface TimelinePlayheadProps { @@ -39,6 +36,8 @@ export function TimelinePlayhead({ }: TimelinePlayheadProps) { const internalPlayheadRef = useRef(null); const playheadRef = externalPlayheadRef || internalPlayheadRef; + const [scrollLeft, setScrollLeft] = useState(0); + const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({ currentTime, duration, @@ -50,6 +49,25 @@ export function TimelinePlayhead({ playheadRef, }); + // Track scroll position to lock playhead to frame + useEffect(() => { + const tracksViewport = tracksScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; + + if (!tracksViewport) return; + + const handleScroll = () => { + setScrollLeft(tracksViewport.scrollLeft); + }; + + // Set initial scroll position + setScrollLeft(tracksViewport.scrollLeft); + + tracksViewport.addEventListener("scroll", handleScroll); + return () => tracksViewport.removeEventListener("scroll", handleScroll); + }, [tracksScrollRef]); + // Use timeline container height minus a few pixels for breathing room const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; const totalHeight = timelineContainerHeight - 8; // 8px padding from edges @@ -59,9 +77,51 @@ export function TimelinePlayhead({ tracks.length > 0 && trackLabelsRef?.current ? trackLabelsRef.current.offsetWidth : 0; - const leftPosition = - trackLabelsWidth + + + // Calculate position locked to timeline content (accounting for scroll) + const timelinePosition = playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + const rawLeftPosition = trackLabelsWidth + timelinePosition - scrollLeft; + + // Get the timeline content width and viewport width for right boundary + const timelineContentWidth = + duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + const tracksViewport = tracksScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; + const viewportWidth = tracksViewport?.clientWidth || 1000; + + // Constrain playhead to never appear outside the timeline area + const leftBoundary = trackLabelsWidth; + const rightBoundary = Math.min( + trackLabelsWidth + timelineContentWidth - scrollLeft, // Don't go beyond timeline content + trackLabelsWidth + viewportWidth // Don't go beyond viewport + ); + + const leftPosition = Math.max( + leftBoundary, + Math.min(rightBoundary, rawLeftPosition) + ); + + // Debug logging when playhead might go outside + if (rawLeftPosition < leftBoundary || rawLeftPosition > rightBoundary) { + console.log( + "PLAYHEAD VISUAL DEBUG:", + JSON.stringify({ + playheadPosition, + timelinePosition, + trackLabelsWidth, + scrollLeft, + rawLeftPosition, + constrainedLeftPosition: leftPosition, + leftBoundary, + rightBoundary, + timelineContentWidth, + viewportWidth, + zoomLevel, + }) + ); + } return (
-- cgit v1.3.1