summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/use-timeline-playhead.ts
blob: 8aee5b1f682befb170c932e5ddab68e0f294e4c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { snapTimeToFrame } from "@/constants/timeline-constants";
import { useProjectStore } from "@/stores/project-store";
import { useRef, useState, useCallback } from "react";

interface UseTimelinePlayheadProps {
  currentTime: number;
  duration: number;
  zoomLevel: number;
  seek: (time: number) => void;
  rulerRef: React.RefObject<HTMLDivElement>;
  rulerScrollRef: React.RefObject<HTMLDivElement>;
}

export function useTimelinePlayhead({
  currentTime,
  duration,
  zoomLevel,
  seek,
  rulerRef,
  rulerScrollRef,
}: UseTimelinePlayheadProps) {
  const { activeProject } = useProjectStore();

  const isScrubbingRef = useRef(false);
  const scrubTimeRef = useRef<number | null>(null);
  const [_, forceRerender] = useState(0);

  const playheadPosition =
    isScrubbingRef.current && scrubTimeRef.current !== null
      ? scrubTimeRef.current
      : currentTime;

  const getTimeFromMouse = useCallback(
    (e: MouseEvent | React.MouseEvent) => {
      const ruler = rulerRef.current;
      const scrollArea = rulerScrollRef.current?.querySelector(
        "[data-radix-scroll-area-viewport]"
      ) as HTMLElement;

      if (!ruler || !scrollArea) return 0;

      const rect = ruler.getBoundingClientRect();
      const scrollLeft = scrollArea.scrollLeft;
      const x = e.clientX - rect.left + scrollLeft;

      const pixelsPerSecond = 50 * zoomLevel;
      const rawTime = Math.max(0, Math.min(duration, x / pixelsPerSecond));
      const time = snapTimeToFrame(rawTime, activeProject?.fps || 30);
      return time;
    },
    [rulerRef, rulerScrollRef, duration, zoomLevel, activeProject?.fps]
  );

  const handleRulerMouseDown = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      const time = getTimeFromMouse(e);
      if (time === undefined) return;

      isScrubbingRef.current = true;
      scrubTimeRef.current = time;
      seek(time);

      const onMouseMove = (e: MouseEvent) => {
        const t = getTimeFromMouse(e);
        if (t !== scrubTimeRef.current) {
          scrubTimeRef.current = t;
          seek(t);
        }
      };

      const onMouseUp = () => {
        isScrubbingRef.current = false;
        window.removeEventListener("mousemove", onMouseMove);
        window.removeEventListener("mouseup", onMouseUp);
        forceRerender((v) => v + 1);
      };

      window.addEventListener("mousemove", onMouseMove);
      window.addEventListener("mouseup", onMouseUp);
    },
    [getTimeFromMouse, seek]
  );

  return {
    playheadPosition,
    handleRulerMouseDown,
  };
}