summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline/timeline-ruler.tsx
blob: fbe5ae1ff92cb3dd07ae791324de6ce854a62da7 (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
119
120
121
122
123
124
125
126
127
import { useMemo } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { useTimelinePlayheadRuler } from "./timeline-playhead";

export interface TimelineRulerProps {
	duration: number;
	zoomLevel: number;
	currentTime: number;
	seek: (time: number) => void;
	rulerRef: React.RefObject<HTMLDivElement>;
	rulerScrollRef: React.RefObject<HTMLDivElement>;
	tracksScrollRef: React.RefObject<HTMLDivElement>;
	playheadRef: React.RefObject<HTMLDivElement>;
	handleSelectionMouseDown: (e: React.MouseEvent) => void;
	handleTimelineContentClick: (e: React.MouseEvent) => void;
}

export function TimelineRuler({
	duration,
	zoomLevel,
	currentTime,
	seek,
	rulerRef,
	rulerScrollRef,
	tracksScrollRef,
	playheadRef,
	handleSelectionMouseDown,
	handleTimelineContentClick,
}: TimelineRulerProps) {
	// Timeline playhead ruler handlers
	const { handleRulerMouseDown } = useTimelinePlayheadRuler({
		currentTime,
		duration,
		zoomLevel,
		seek,
		rulerRef,
		rulerScrollRef,
		tracksScrollRef,
		playheadRef,
	});

	// Memoize the expensive ruler markers calculation
	const rulerMarkers = useMemo(() => {
		// Calculate appropriate time interval based on zoom level
		const getTimeInterval = (zoom: number) => {
			const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
			if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
			if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
			if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
			if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out
			if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out
			if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out
			return 30; // Every 30s when extremely zoomed out
		};

		const formatTime = (seconds: number, interval: number) => {
			const hours = Math.floor(seconds / 3600);
			const minutes = Math.floor((seconds % 3600) / 60);
			const secs = seconds % 60;

			if (hours > 0) {
				return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`;
			} else if (minutes > 0) {
				return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`;
			} else if (interval >= 1) {
				return `${Math.floor(secs)}s`;
			} else {
				return `${secs.toFixed(1)}s`;
			}
		};

		const interval = getTimeInterval(zoomLevel);
		const markerCount = Math.ceil(duration / interval) + 1;
		const markers = [];

		for (let i = 0; i < markerCount; i++) {
			const time = i * interval;
			if (time > duration) break;

			const isMainMarker =
				time % (interval >= 1 ? Math.max(1, interval) : 1) === 0;
			const leftPosition =
				time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;

			markers.push(
				<div
					key={i}
					className={`absolute top-0 bottom-0 z-[99] ${
						isMainMarker
							? "border-l border-muted-foreground/40"
							: "border-l border-muted-foreground/20"
					}`}
					style={{ left: `${leftPosition}px` }}
				>
					<span
						className={`absolute top-1 left-1 text-[0.6rem] z-[99] ${
							isMainMarker
								? "text-muted-foreground font-medium"
								: "text-muted-foreground/70"
						}`}
					>
						{formatTime(time, interval)}
					</span>
				</div>,
			);
		}

		return markers;
	}, [duration, zoomLevel]); // Only recalculate when duration or zoomLevel changes

	return (
		<div
			className="sticky top-0 bg-card/[0.99] border-b border-muted/30 z-[99]"
			onMouseDown={handleSelectionMouseDown}
			onClick={handleTimelineContentClick}
			data-ruler-area
		>
			<div
				ref={rulerRef}
				className="relative h-5 select-none cursor-default pb-1 z-[99]"
				onMouseDown={handleRulerMouseDown}
			>
				{rulerMarkers}
			</div>
		</div>
	);
}