summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline/timeline-playhead.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-26 22:25:57 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-26 22:25:57 +0200
commitd154937ac2fdf5877095c4a0f1076cba04d7c3d1 (patch)
treef8485b9682e0e9108313c7d9b370b176e4ae55d5 /apps/web/src/components/editor/timeline/timeline-playhead.tsx
parent906f95399d3a0119af0e57c2cb31fafbcdc31391 (diff)
downloadopencut-d154937ac2fdf5877095c4a0f1076cba04d7c3d1.tar.gz
feat: lock playhead to frame (instead of floating) and add autoscroll to timeline
Diffstat (limited to 'apps/web/src/components/editor/timeline/timeline-playhead.tsx')
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx75
1 files changed, 67 insertions, 8 deletions
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<HTMLDivElement>(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 (
<div
@@ -72,7 +132,6 @@ export function TimelinePlayhead({
top: 0,
height: `${totalHeight}px`,
width: "2px", // Slightly wider for better click target
- zIndex: 100,
}}
onMouseDown={handlePlayheadMouseDown}
>