import { useEffect, useRef } from "react"; import { useEditor } from "@/editor/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; import { useEdgeAutoScroll } from "@/timeline/hooks/use-edge-auto-scroll"; import { timelineTimeToPixels } from "@/timeline"; import { PlayheadController, type PlayheadConfig, } from "@/timeline/controllers/playhead-controller"; interface UseTimelinePlayheadProps { zoomLevel: number; rulerRef: React.RefObject; rulerScrollRef: React.RefObject; tracksScrollRef: React.RefObject; playheadRef?: React.RefObject; } export function useTimelinePlayhead({ zoomLevel, rulerRef, rulerScrollRef, tracksScrollRef, playheadRef, }: UseTimelinePlayheadProps) { const editor = useEditor(); const isShiftHeldRef = useShiftKey(); // isScrubbing drives useEdgeAutoScroll — the controller sets it on the editor, // so this reactive read naturally reflects whether scrubbing is active. const isScrubbing = useEditor((e) => e.playback.getIsScrubbing()); const config: PlayheadConfig = { zoomLevel, duration: editor.timeline.getTotalDuration(), getActiveProjectFps: () => editor.project.getActive()?.settings.fps ?? null, isShiftHeld: () => isShiftHeldRef.current, getIsPlaying: () => editor.playback.getIsPlaying(), getRulerEl: () => rulerRef.current, getRulerScrollEl: () => rulerScrollRef.current, getTracksScrollEl: () => tracksScrollRef.current, getPlayheadEl: () => playheadRef?.current ?? null, getSceneTracks: () => editor.scenes.getActiveScene().tracks, getSceneBookmarks: () => editor.scenes.getActiveScene()?.bookmarks ?? [], seek: (time) => editor.playback.seek({ time }), setScrubbing: (scrubbing) => editor.playback.setScrubbing({ isScrubbing: scrubbing }), setTimelineViewState: (viewState) => editor.project.setTimelineViewState({ viewState }), }; const configRef = useRef(config); configRef.current = config; const ctrlRef = useRef(null); if (!ctrlRef.current) { ctrlRef.current = new PlayheadController({ configRef }); } const ctrl = ctrlRef.current; // Scroll → keep playhead position in sync with scroll offset. useEffect(() => { const scrollEl = rulerScrollRef.current; if (!scrollEl) return; const handler = () => ctrl.updatePlayheadLeft(editor.playback.getCurrentTime()); scrollEl.addEventListener("scroll", handler, { passive: true }); return () => scrollEl.removeEventListener("scroll", handler); }, [ctrl, editor.playback, rulerScrollRef]); // Playback events → update playhead position and auto-scroll during playback. useEffect(() => { const handler = (time: number) => ctrl.handlePlaybackUpdate(time); ctrl.updatePlayheadLeft(editor.playback.getCurrentTime()); const unsubscribeUpdate = editor.playback.onUpdate(handler); const unsubscribeSeek = editor.playback.onSeek(handler); return () => { unsubscribeUpdate(); unsubscribeSeek(); }; }, [ctrl, editor.playback]); useEdgeAutoScroll({ isActive: isScrubbing, getMouseClientX: () => ctrl.getLastMouseClientX(), rulerScrollRef, tracksScrollRef, contentWidth: timelineTimeToPixels({ time: editor.timeline.getTotalDuration(), zoomLevel, }), }); useEffect(() => () => ctrl.destroy(), [ctrl]); return { handlePlayheadMouseDown: ctrl.onPlayheadMouseDown, handleRulerMouseDown: ctrl.onRulerMouseDown, }; }