summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/timeline/timeline-ruler.tsx
blob: 07a4ad5da94b8c357e1d6a3444f65223ed83880b (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
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-[97] ${
            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-[97] ${
              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-[97]"
      onMouseDown={handleSelectionMouseDown}
      onClick={handleTimelineContentClick}
      data-ruler-area
    >
      <div
        ref={rulerRef}
        className="relative h-5 select-none cursor-default pb-1 z-[97]"
        onMouseDown={handleRulerMouseDown}
      >
        {rulerMarkers}
      </div>
    </div>
  );
}