From bb65d4fb96d3726448100ea8d95e5eab92e6ddba Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 10 Jul 2025 23:55:08 +0200 Subject: refactor: timeline playhead component and hook --- apps/web/src/components/editor/timeline.tsx | 180 ++++++---------------------- 1 file changed, 38 insertions(+), 142 deletions(-) (limited to 'apps/web/src/components/editor/timeline.tsx') diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index f3e20fda..15c51879 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -44,6 +44,11 @@ import { SelectValue, } from "../ui/select"; import { TimelineTrackContent } from "./timeline-track"; +import { + TimelinePlayhead, + TimelinePlayheadTracks, + useTimelinePlayheadRuler, +} from "./timeline-playhead"; import type { DragData, TimelineTrack } from "@/types/timeline"; import { getTrackHeight, @@ -109,14 +114,6 @@ export function Timeline() { additive: boolean; } | null>(null); - // Playhead scrubbing state - const [isScrubbing, setIsScrubbing] = useState(false); - const [scrubTime, setScrubTime] = useState(null); - - // Add new state for ruler drag detection - const [isDraggingRuler, setIsDraggingRuler] = useState(false); - const [hasDraggedRuler, setHasDraggedRuler] = useState(false); - // Dynamic timeline width calculation based on playhead position and duration const dynamicTimelineWidth = Math.max( (duration || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Base width from duration @@ -131,6 +128,17 @@ export function Timeline() { const lastRulerSync = useRef(0); const lastTracksSync = useRef(0); + // Timeline playhead ruler handlers + const { handleRulerMouseDown, isDraggingRuler } = useTimelinePlayheadRuler({ + currentTime, + duration, + zoomLevel, + seek, + rulerRef, + rulerScrollRef, + tracksScrollRef, + }); + // Update timeline duration when tracks change useEffect(() => { const totalDuration = getTotalDuration(); @@ -460,92 +468,6 @@ export function Timeline() { } }; - // --- Playhead Scrubbing Handlers --- - const handlePlayheadMouseDown = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); // Prevent ruler drag from triggering - setIsScrubbing(true); - handleScrub(e); - }, - [duration, zoomLevel] - ); - - // Add new ruler mouse down handler - const handleRulerMouseDown = useCallback( - (e: React.MouseEvent) => { - // Only handle left mouse button - if (e.button !== 0) return; - - // Don't interfere if clicking on the playhead itself - if ((e.target as HTMLElement).closest(".playhead")) return; - - e.preventDefault(); - setIsDraggingRuler(true); - setHasDraggedRuler(false); - - // Start scrubbing immediately - setIsScrubbing(true); - handleScrub(e); - }, - [duration, zoomLevel] - ); - - const handleScrub = useCallback( - (e: MouseEvent | React.MouseEvent) => { - const ruler = rulerRef.current; - if (!ruler) return; - const rect = ruler.getBoundingClientRect(); - const x = e.clientX - rect.left; - const time = Math.max(0, Math.min(duration, x / (50 * zoomLevel))); - setScrubTime(time); - seek(time); // update video preview in real time - }, - [duration, zoomLevel, seek] - ); - - useEffect(() => { - if (!isScrubbing) return; - const onMouseMove = (e: MouseEvent) => { - handleScrub(e); - // Mark that we've dragged if ruler drag is active - if (isDraggingRuler) { - setHasDraggedRuler(true); - } - }; - const onMouseUp = (e: MouseEvent) => { - setIsScrubbing(false); - if (scrubTime !== null) seek(scrubTime); // finalize seek - setScrubTime(null); - - // Handle ruler click vs drag - if (isDraggingRuler) { - setIsDraggingRuler(false); - // If we didn't drag, treat it as a click-to-seek - if (!hasDraggedRuler) { - handleScrub(e); - } - setHasDraggedRuler(false); - } - }; - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("mouseup", onMouseUp); - return () => { - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", onMouseUp); - }; - }, [ - isScrubbing, - scrubTime, - seek, - handleScrub, - isDraggingRuler, - hasDraggedRuler, - ]); - - const playheadPosition = - isScrubbing && scrubTime !== null ? scrubTime : currentTime; - const dragProps = { onDragEnter: handleDragEnter, onDragOver: handleDragOver, @@ -718,33 +640,6 @@ export function Timeline() { }; }, []); - // --- 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 * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const viewportWidth = rulerViewport.clientWidth; - const scrollMin = 0; - const scrollMax = rulerViewport.scrollWidth - viewportWidth; - // 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 + 100 || - playheadPx > rulerViewport.scrollLeft + viewportWidth - 100 - ) { - rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; - } - }, [playheadPosition, duration, zoomLevel]); - return (
-
-
+ {/* Playhead in ruler */} +
@@ -1101,17 +997,17 @@ export function Timeline() { ))} - {/* Playhead for tracks area (scrubbable) */} - {tracks.length > 0 && ( -
- )} + {/* Playhead for tracks area */} + )}
-- cgit v1.3.1