summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline
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
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')
-rw-r--r--apps/web/src/components/editor/timeline/index.tsx5
-rw-r--r--apps/web/src/components/editor/timeline/timeline-playhead.tsx75
2 files changed, 70 insertions, 10 deletions
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 */}
<div className="flex bg-panel sticky top-0 z-10">
{/* Track Labels Header */}
- <div className="w-48 flex-shrink-0 bg-muted/30 border-r flex items-center justify-between px-3 py-2">
+ <div className="w-48 flex-shrink-0 bg-panel border-r flex items-center justify-between px-3 py-2">
{/* Empty space */}
<span className="text-sm font-medium text-muted-foreground opacity-0">
.
@@ -651,7 +652,7 @@ export function Timeline() {
{tracks.length > 0 && (
<div
ref={trackLabelsRef}
- className="w-48 flex-shrink-0 border-r border-black overflow-y-auto"
+ className="w-48 flex-shrink-0 border-r border-black overflow-y-auto z-[200] bg-panel"
data-track-labels
>
<ScrollArea className="w-full h-full" ref={trackLabelsScrollRef}>
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}
>