summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/use-timeline-playhead.ts
diff options
context:
space:
mode:
authorHyteq <DerpsDer.es@gmail.com>2025-07-20 10:48:40 +0300
committerHyteq <DerpsDer.es@gmail.com>2025-07-20 10:48:40 +0300
commiteab8ec3ba81b86fdd4ade920d999c28fb34785b1 (patch)
tree619fc6930f50fa36d6f1f94ee81fb8bf6d4bedef /apps/web/src/hooks/use-timeline-playhead.ts
parentd040ca53cb4029c9c58bd7e6b6dfd1eed2694b31 (diff)
downloadopencut-eab8ec3ba81b86fdd4ade920d999c28fb34785b1.tar.gz
format, cleanup, deps
Diffstat (limited to 'apps/web/src/hooks/use-timeline-playhead.ts')
-rw-r--r--apps/web/src/hooks/use-timeline-playhead.ts250
1 files changed, 125 insertions, 125 deletions
diff --git a/apps/web/src/hooks/use-timeline-playhead.ts b/apps/web/src/hooks/use-timeline-playhead.ts
index 9eb06485..73dca7a5 100644
--- a/apps/web/src/hooks/use-timeline-playhead.ts
+++ b/apps/web/src/hooks/use-timeline-playhead.ts
@@ -1,125 +1,125 @@
-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>;
-}
-
-/**
- * useTimelinePlayhead
- *
- * Custom hook to manage playhead (scrubbing) logic for a timeline editor.
- * Handles mouse interaction for timeline ruler, calculates time from mouse,
- * and enables smooth scrubbing without unnecessary re-renders.
- */
-export function useTimelinePlayhead({
- currentTime,
- duration,
- zoomLevel,
- seek,
- rulerRef,
- rulerScrollRef,
-}: UseTimelinePlayheadProps) {
- // Get current project info (especially FPS) from global store
- const { activeProject } = useProjectStore();
-
- // Ref to track if currently scrubbing
- const isScrubbingRef = useRef(false);
-
- // Ref to hold the playhead time while scrubbing (does not trigger re-render)
- const scrubTimeRef = useRef<number | null>(null);
-
- // State to force re-render when scrubbing ends
- const [_, forceRerender] = useState(0);
-
- /**
- * Determines the actual playhead position:
- * - If scrubbing, use the value in scrubTimeRef
- * - Otherwise, use the currentTime (controlled by external state)
- */
- const playheadPosition =
- isScrubbingRef.current && scrubTimeRef.current !== null
- ? scrubTimeRef.current
- : currentTime;
-
- /**
- * Calculate timeline time (in seconds) based on mouse X position.
- * - Gets bounding rect of the ruler
- * - Adjusts for scroll position if ruler is scrollable
- * - Converts X pixel offset to seconds using current zoom level
- * - Snaps to nearest frame using FPS
- */
- 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;
-
- // Calculate how many pixels represent one second, depending on zoom
- 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]
- );
-
- /**
- * Handle mouse down event on the ruler:
- * - Starts scrubbing and updates playhead immediately
- * - Registers mousemove/mouseup events to allow scrubbing
- * - Updates time as mouse moves and seeks to new time
- * - Cleans up listeners and triggers re-render when scrubbing ends
- */
- const handleRulerMouseDown = useCallback(
- (e: React.MouseEvent) => {
- if (e.button !== 0) return;
- e.preventDefault();
- const time = getTimeFromMouse(e);
-
- isScrubbingRef.current = true;
- scrubTimeRef.current = time;
- seek(time);
-
- // Mouse move handler: update playhead and seek to new time
- const onMouseMove = (e: MouseEvent) => {
- const t = getTimeFromMouse(e);
- if (t !== scrubTimeRef.current) {
- scrubTimeRef.current = t;
- seek(t);
- }
- };
-
- // Mouse up handler: stop scrubbing, cleanup, and force re-render
- const onMouseUp = () => {
- isScrubbingRef.current = false;
- window.removeEventListener("mousemove", onMouseMove);
- window.removeEventListener("mouseup", onMouseUp);
- forceRerender((v) => v + 1);
- };
-
- // Attach listeners to window for drag outside the ruler area
- window.addEventListener("mousemove", onMouseMove);
- window.addEventListener("mouseup", onMouseUp);
- },
- [getTimeFromMouse, seek]
- );
-
- return {
- playheadPosition, // Current playhead position (real-time while scrubbing)
- handleRulerMouseDown, // Attach to the ruler's onMouseDown
- };
-}
+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>;
+}
+
+/**
+ * useTimelinePlayhead
+ *
+ * Custom hook to manage playhead (scrubbing) logic for a timeline editor.
+ * Handles mouse interaction for timeline ruler, calculates time from mouse,
+ * and enables smooth scrubbing without unnecessary re-renders.
+ */
+export function useTimelinePlayhead({
+ currentTime,
+ duration,
+ zoomLevel,
+ seek,
+ rulerRef,
+ rulerScrollRef,
+}: UseTimelinePlayheadProps) {
+ // Get current project info (especially FPS) from global store
+ const { activeProject } = useProjectStore();
+
+ // Ref to track if currently scrubbing
+ const isScrubbingRef = useRef(false);
+
+ // Ref to hold the playhead time while scrubbing (does not trigger re-render)
+ const scrubTimeRef = useRef<number | null>(null);
+
+ // State to force re-render when scrubbing ends
+ const [_, forceRerender] = useState(0);
+
+ /**
+ * Determines the actual playhead position:
+ * - If scrubbing, use the value in scrubTimeRef
+ * - Otherwise, use the currentTime (controlled by external state)
+ */
+ const playheadPosition =
+ isScrubbingRef.current && scrubTimeRef.current !== null
+ ? scrubTimeRef.current
+ : currentTime;
+
+ /**
+ * Calculate timeline time (in seconds) based on mouse X position.
+ * - Gets bounding rect of the ruler
+ * - Adjusts for scroll position if ruler is scrollable
+ * - Converts X pixel offset to seconds using current zoom level
+ * - Snaps to nearest frame using FPS
+ */
+ 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;
+
+ // Calculate how many pixels represent one second, depending on zoom
+ 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],
+ );
+
+ /**
+ * Handle mouse down event on the ruler:
+ * - Starts scrubbing and updates playhead immediately
+ * - Registers mousemove/mouseup events to allow scrubbing
+ * - Updates time as mouse moves and seeks to new time
+ * - Cleans up listeners and triggers re-render when scrubbing ends
+ */
+ const handleRulerMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.button !== 0) return;
+ e.preventDefault();
+ const time = getTimeFromMouse(e);
+
+ isScrubbingRef.current = true;
+ scrubTimeRef.current = time;
+ seek(time);
+
+ // Mouse move handler: update playhead and seek to new time
+ const onMouseMove = (e: MouseEvent) => {
+ const t = getTimeFromMouse(e);
+ if (t !== scrubTimeRef.current) {
+ scrubTimeRef.current = t;
+ seek(t);
+ }
+ };
+
+ // Mouse up handler: stop scrubbing, cleanup, and force re-render
+ const onMouseUp = () => {
+ isScrubbingRef.current = false;
+ window.removeEventListener("mousemove", onMouseMove);
+ window.removeEventListener("mouseup", onMouseUp);
+ forceRerender((v) => v + 1);
+ };
+
+ // Attach listeners to window for drag outside the ruler area
+ window.addEventListener("mousemove", onMouseMove);
+ window.addEventListener("mouseup", onMouseUp);
+ },
+ [getTimeFromMouse, seek],
+ );
+
+ return {
+ playheadPosition, // Current playhead position (real-time while scrubbing)
+ handleRulerMouseDown, // Attach to the ruler's onMouseDown
+ };
+}