diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/editor/timeline/timeline-playhead.tsx | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/editor/timeline/timeline-playhead.tsx')
| -rw-r--r-- | apps/web/src/components/editor/timeline/timeline-playhead.tsx | 228 |
1 files changed, 74 insertions, 154 deletions
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index a1be5b26..58d0adb0 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -1,172 +1,92 @@ "use client"; -import { useRef, useState, useEffect } from "react"; -import { TimelineTrack } from "@/types/timeline"; +import { useRef } from "react"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead"; +import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead"; +import { useEditor } from "@/hooks/use-editor"; interface TimelinePlayheadProps { - currentTime: number; - duration: number; - zoomLevel: number; - tracks: TimelineTrack[]; - seek: (time: number) => void; - rulerRef: React.RefObject<HTMLDivElement>; - rulerScrollRef: React.RefObject<HTMLDivElement>; - tracksScrollRef: React.RefObject<HTMLDivElement>; - trackLabelsRef?: React.RefObject<HTMLDivElement>; - timelineRef: React.RefObject<HTMLDivElement>; - playheadRef?: React.RefObject<HTMLDivElement>; - isSnappingToPlayhead?: boolean; + zoomLevel: number; + rulerRef: React.RefObject<HTMLDivElement>; + rulerScrollRef: React.RefObject<HTMLDivElement>; + tracksScrollRef: React.RefObject<HTMLDivElement>; + timelineRef: React.RefObject<HTMLDivElement>; + playheadRef?: React.RefObject<HTMLDivElement>; + isSnappingToPlayhead?: boolean; } export function TimelinePlayhead({ - currentTime, - duration, - zoomLevel, - tracks, - seek, - rulerRef, - rulerScrollRef, - tracksScrollRef, - trackLabelsRef, - timelineRef, - playheadRef: externalPlayheadRef, - isSnappingToPlayhead = false, + zoomLevel, + rulerRef, + rulerScrollRef, + tracksScrollRef, + timelineRef, + playheadRef: externalPlayheadRef, + isSnappingToPlayhead = false, }: TimelinePlayheadProps) { - const internalPlayheadRef = useRef<HTMLDivElement>(null); - const playheadRef = externalPlayheadRef || internalPlayheadRef; - const [scrollLeft, setScrollLeft] = useState(0); + const editor = useEditor(); + const duration = editor.timeline.getTotalDuration(); + const internalPlayheadRef = useRef<HTMLDivElement>(null); + const playheadRef = externalPlayheadRef || internalPlayheadRef; - const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({ - currentTime, - duration, - zoomLevel, - seek, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, - }); + const { playheadPosition, handlePlayheadMouseDown } = useTimelinePlayhead({ + zoomLevel, + rulerRef, + rulerScrollRef, + tracksScrollRef, + playheadRef, + }); - // Track scroll position to lock playhead to frame - useEffect(() => { - const tracksViewport = tracksScrollRef.current; + const timelineContainerHeight = + tracksScrollRef.current?.clientHeight ?? + timelineRef.current?.clientHeight ?? + 400; + const totalHeight = Math.max(0, timelineContainerHeight - 4); - if (!tracksViewport) return; + const timelinePosition = + playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + const leftPosition = timelinePosition; - const handleScroll = () => { - setScrollLeft(tracksViewport.scrollLeft); - }; + const handlePlayheadKeyDown = ( + event: React.KeyboardEvent<HTMLDivElement>, + ) => { + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; - // Set initial scroll position - setScrollLeft(tracksViewport.scrollLeft); + event.preventDefault(); + const step = 1 / Math.max(1, editor.project.getActive().settings.fps); + const direction = event.key === "ArrowRight" ? 1 : -1; + const nextTime = Math.max( + 0, + Math.min(duration, playheadPosition + direction * step), + ); - tracksViewport.addEventListener("scroll", handleScroll); - return () => tracksViewport.removeEventListener("scroll", handleScroll); - }, [tracksScrollRef]); + editor.playback.seek({ time: nextTime }); + }; - // Use timeline container height minus a few pixels for breathing room - const timelineContainerHeight = timelineRef.current?.offsetHeight || 400; - const totalHeight = timelineContainerHeight - 4; + return ( + <div + ref={playheadRef} + role="slider" + aria-label="Timeline playhead" + aria-valuemin={0} + aria-valuemax={duration} + aria-valuenow={playheadPosition} + tabIndex={0} + className="pointer-events-auto absolute z-60" + style={{ + left: `${leftPosition}px`, + top: 0, + height: `${totalHeight}px`, + width: "2px", + }} + onMouseDown={handlePlayheadMouseDown} + onKeyDown={handlePlayheadKeyDown} + > + <div className="bg-foreground absolute left-0 h-full w-0.5 cursor-col-resize" /> - // Get dynamic track labels width, fallback to 0 if no tracks or no ref - const trackLabelsWidth = - tracks.length > 0 && trackLabelsRef?.current - ? trackLabelsRef.current.offsetWidth - : 0; - - // 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; - 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 - ref={playheadRef} - className="absolute pointer-events-auto z-40" - style={{ - left: `${leftPosition}px`, - top: 0, - height: `${totalHeight}px`, - width: "2px", // Slightly wider for better click target - }} - onMouseDown={handlePlayheadMouseDown} - > - {/* The playhead line spanning full height */} - <div - className={`absolute left-0 w-0.5 cursor-col-resize h-full ${isSnappingToPlayhead ? "bg-foreground" : "bg-foreground"}`} - /> - - {/* Playhead dot indicator at the top (in ruler area) */} - <div - className={`absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`} - /> - </div> - ); + <div + className={`absolute top-1 left-1/2 size-3 -translate-x-1/2 transform rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`} + /> + </div> + ); } - -// Also export a hook for getting ruler handlers -export function useTimelinePlayheadRuler({ - currentTime, - duration, - zoomLevel, - seek, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, -}: Omit<TimelinePlayheadProps, "tracks" | "trackLabelsRef" | "timelineRef">) { - const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayhead({ - currentTime, - duration, - zoomLevel, - seek, - rulerRef, - rulerScrollRef, - tracksScrollRef, - playheadRef, - }); - - return { handleRulerMouseDown, isDraggingRuler }; -} - -export { TimelinePlayhead as default }; |
