import { useState, useEffect, useCallback } from "react"; interface UseTimelinePlayheadProps { currentTime: number; duration: number; zoomLevel: number; seek: (time: number) => void; rulerRef: React.RefObject; rulerScrollRef: React.RefObject; tracksScrollRef: React.RefObject; } export function useTimelinePlayhead({ currentTime, duration, zoomLevel, seek, rulerRef, rulerScrollRef, tracksScrollRef, }: UseTimelinePlayheadProps) { // Playhead scrubbing state const [isScrubbing, setIsScrubbing] = useState(false); const [scrubTime, setScrubTime] = useState(null); // Ruler drag detection state const [isDraggingRuler, setIsDraggingRuler] = useState(false); const [hasDraggedRuler, setHasDraggedRuler] = useState(false); const playheadPosition = isScrubbing && scrubTime !== null ? scrubTime : currentTime; // --- Playhead Scrubbing Handlers --- const handlePlayheadMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Prevent ruler drag from triggering setIsScrubbing(true); handleScrub(e); }, [duration, zoomLevel] ); // 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, rulerRef] ); // Mouse move/up event handlers 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, ]); // --- 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 * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50 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, rulerScrollRef, tracksScrollRef]); return { playheadPosition, handlePlayheadMouseDown, handleRulerMouseDown, isDraggingRuler, }; }