summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline/timeline-content-click.tsx
blob: 7f4676c7b0d53a8021bce2a62b0d1d9e3994f830 (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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { useCallback } from "react";
import { useProjectStore } from "@/stores/project-store";
import {
	TIMELINE_CONSTANTS,
	snapTimeToFrame,
} from "@/constants/timeline-constants";

export interface TimelineContentClickProps {
	duration: number;
	zoomLevel: number;
	seek: (time: number) => void;
	rulerScrollRef: React.RefObject<HTMLDivElement>;
	tracksScrollRef: React.RefObject<HTMLDivElement>;
	clearSelectedElements: () => void;
	isSelecting: boolean;
	justFinishedSelecting: boolean;
	playheadRef: React.RefObject<HTMLDivElement>;
}

export function useTimelineContentClick({
	duration,
	zoomLevel,
	seek,
	rulerScrollRef,
	tracksScrollRef,
	clearSelectedElements,
	isSelecting,
	justFinishedSelecting,
	playheadRef,
}: TimelineContentClickProps) {
	const { activeProject } = useProjectStore();

	// Timeline content click to seek handler
	const handleTimelineContentClick = useCallback(
		(e: React.MouseEvent) => {
			// Don't seek if this was a selection box operation
			if (isSelecting || justFinishedSelecting) {
				return;
			}

			// Don't seek if clicking on timeline elements, but still deselect
			if ((e.target as HTMLElement).closest(".timeline-element")) {
				return;
			}

			// Don't seek if clicking on playhead
			if (playheadRef.current?.contains(e.target as Node)) {
				return;
			}

			// Don't seek if clicking on track labels
			if ((e.target as HTMLElement).closest("[data-track-labels]")) {
				clearSelectedElements();
				return;
			}

			// Clear selected elements when clicking empty timeline area
			clearSelectedElements();

			// Determine if we're clicking in ruler or tracks area
			const isRulerClick = (e.target as HTMLElement).closest(
				"[data-ruler-area]",
			);

			let mouseX: number;
			let scrollLeft = 0;

			if (isRulerClick) {
				// Calculate based on ruler position
				const rulerContent = rulerScrollRef.current?.querySelector(
					"[data-radix-scroll-area-viewport]",
				) as HTMLElement;
				if (!rulerContent) return;
				const rect = rulerContent.getBoundingClientRect();
				mouseX = e.clientX - rect.left;
				scrollLeft = rulerContent.scrollLeft;
			} else {
				// Calculate based on tracks content position
				const tracksContent = tracksScrollRef.current?.querySelector(
					"[data-radix-scroll-area-viewport]",
				) as HTMLElement;
				if (!tracksContent) return;
				const rect = tracksContent.getBoundingClientRect();
				mouseX = e.clientX - rect.left;
				scrollLeft = tracksContent.scrollLeft;
			}

			const rawTime = Math.max(
				0,
				Math.min(
					duration,
					(mouseX + scrollLeft) /
						(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
				),
			);

			// Use frame snapping for timeline clicking
			const projectFps = activeProject?.fps || 30;
			const time = snapTimeToFrame(rawTime, projectFps);

			seek(time);
		},
		[
			duration,
			zoomLevel,
			seek,
			rulerScrollRef,
			tracksScrollRef,
			clearSelectedElements,
			isSelecting,
			justFinishedSelecting,
			playheadRef,
			activeProject?.fps,
		],
	);

	return { handleTimelineContentClick };
}