Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import clamp from "lodash/clamp";
import { type FC, type ReactElement, useCallback, useRef } from "react";
import { cn } from "../../utils/bem";
import type { TimelineMinimapProps } from "./Types";
 
import "./Seeker.scss";
 
export interface SeekerProps {
  position: number;
  length: number;
  seekOffset: number;
  seekVisible: number;
  step: number;
  leftOffset?: number;
  minimap?: ReactElement<TimelineMinimapProps> | null;
  onIndicatorMove: (position: number) => void;
  onSeek: (position: number) => void;
}
 
export const Seeker: FC<SeekerProps> = ({
  position,
  length,
  seekOffset,
  seekVisible,
  onIndicatorMove,
  onSeek,
  minimap,
  step,
  ...props
}) => {
  const leftOffset = (props.leftOffset ?? 150) / step;
  const rootRef = useRef<HTMLDivElement>();
  const seekerRef = useRef<HTMLDivElement>();
  const viewRef = useRef<HTMLDivElement>();
 
  const showIndicator = seekVisible > 0;
 
  // The indicator width is set wider by 1.5, to account for the pixel sizing of the position indicator width and placement
  // to align better with the viewable timeline scroll.
  const width = `${((Math.ceil(seekVisible) - Math.floor(leftOffset) + 1.5) / length) * 100}%`;
  const offsetLimit = length - (seekVisible - leftOffset);
  const windowOffset = `${(Math.min(seekOffset, offsetLimit) / length) * 100}%`;
  const seekerOffset = (position / length) * 100;
 
  const onIndicatorDrag = useCallback(
    (e) => {
      const indicator = viewRef.current!;
      const dimensions = rootRef.current!.getBoundingClientRect();
      const indicatorWidth = indicator.clientWidth;
 
      const startDrag = e.pageX;
      const startOffset = startDrag - dimensions.left - indicatorWidth / 2;
      const parentWidth = dimensions.width;
      const limit = parentWidth - indicatorWidth;
 
      const jump = clamp(Math.ceil(length * (startOffset / parentWidth)), 0, limit);
 
      onIndicatorMove?.(jump);
 
      const onMouseMove = (e: globalThis.MouseEvent) => {
        const newOffset = clamp(startOffset + (e.pageX - startDrag), 0, limit);
        const percent = newOffset / parentWidth;
 
        onIndicatorMove?.(Math.ceil(length * percent));
      };
 
      const onMouseUp = () => {
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
      };
 
      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    },
    [length],
  );
 
  const onSeekerDrag = useCallback(
    (e: globalThis.MouseEvent) => {
      const indicator = seekerRef.current!;
      const dimensions = rootRef.current!.getBoundingClientRect();
      const indicatorWidth = indicator.clientWidth;
 
      const startDrag = e.pageX;
      const startOffset = startDrag - dimensions.left - indicatorWidth / 2;
      const parentWidth = dimensions.width;
 
      const jump = (e: globalThis.MouseEvent) => {
        const limit = parentWidth - indicator.clientWidth;
        const newOffset = clamp(startOffset + (e.pageX - startDrag), 0, limit);
        const percent = newOffset / parentWidth;
        const newPosition = Math.ceil(length * percent);
 
        onSeek?.(newPosition);
      };
 
      jump(e);
 
      const onMouseMove = (e: globalThis.MouseEvent) => {
        jump(e);
      };
 
      const onMouseUp = () => {
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
      };
 
      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    },
    [length],
  );
 
  const onDrag = useCallback(
    (e: MouseEvent) => {
      e.preventDefault();
      e.stopPropagation();
 
      if (e.target === viewRef.current) {
        onIndicatorDrag(e);
      } else {
        onSeekerDrag(e);
      }
    },
    [onIndicatorDrag, onSeekerDrag],
  );
 
  return (
    <div className={cn("seeker").toClassName()} ref={rootRef as any} onMouseDown={onDrag}>
      <div className={cn("seeker").elem("track").toClassName()} />
      {showIndicator && (
        <div
          className={cn("seeker").elem("indicator").toClassName()}
          ref={viewRef as any}
          style={{ left: windowOffset, width }}
        />
      )}
      <div
        className={cn("seeker").elem("position").toClassName()}
        ref={seekerRef as any}
        style={{ left: `${seekerOffset}%` }}
      />
      <div className={cn("seeker").elem("minimap").toClassName()}>{minimap}</div>
    </div>
  );
};