@zentauri-ui/zentauri-components 2.1.5 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +12 -8
  2. package/cli/cli.integration.test.ts +36 -0
  3. package/cli/index.mjs +91 -12
  4. package/cli/index.test.ts +180 -0
  5. package/cli/props.json +609 -14
  6. package/cli/registry.json +22 -0
  7. package/cli/rewrite-imports.mjs +29 -4
  8. package/cli/rewrite-imports.test.ts +35 -0
  9. package/dist/{chunk-RENXBUZY.js → chunk-5ELR6MIN.js} +6 -6
  10. package/dist/{chunk-RENXBUZY.js.map → chunk-5ELR6MIN.js.map} +1 -1
  11. package/dist/chunk-5FU57ZVQ.js +19 -0
  12. package/dist/{chunk-D2GISTDL.js.map → chunk-5FU57ZVQ.js.map} +1 -1
  13. package/dist/chunk-74SKXGTM.js +4 -0
  14. package/dist/chunk-74SKXGTM.js.map +1 -0
  15. package/dist/{chunk-WBZKMSXW.mjs → chunk-7UXPXCKV.mjs} +3 -3
  16. package/dist/{chunk-WBZKMSXW.mjs.map → chunk-7UXPXCKV.mjs.map} +1 -1
  17. package/dist/chunk-COCPCZMR.mjs +77 -0
  18. package/dist/chunk-COCPCZMR.mjs.map +1 -0
  19. package/dist/chunk-CYKSS5S5.mjs +128 -0
  20. package/dist/chunk-CYKSS5S5.mjs.map +1 -0
  21. package/dist/chunk-DBNGLT5U.mjs +221 -0
  22. package/dist/chunk-DBNGLT5U.mjs.map +1 -0
  23. package/dist/{chunk-BL6UVCV7.mjs → chunk-FUCW5GPE.mjs} +36 -11
  24. package/dist/chunk-FUCW5GPE.mjs.map +1 -0
  25. package/dist/chunk-G7FVHZRB.js +225 -0
  26. package/dist/chunk-G7FVHZRB.js.map +1 -0
  27. package/dist/chunk-HMDH4BQJ.js +123 -0
  28. package/dist/chunk-HMDH4BQJ.js.map +1 -0
  29. package/dist/chunk-I7EBE7BD.js +98 -0
  30. package/dist/chunk-I7EBE7BD.js.map +1 -0
  31. package/dist/{chunk-PAG5CTLN.mjs → chunk-KVSRUAXP.mjs} +3 -3
  32. package/dist/{chunk-PAG5CTLN.mjs.map → chunk-KVSRUAXP.mjs.map} +1 -1
  33. package/dist/chunk-LHBJD57K.mjs +143 -0
  34. package/dist/chunk-LHBJD57K.mjs.map +1 -0
  35. package/dist/chunk-OYAJG2BO.js +83 -0
  36. package/dist/chunk-OYAJG2BO.js.map +1 -0
  37. package/dist/chunk-PG7LQVU6.js +86 -0
  38. package/dist/chunk-PG7LQVU6.js.map +1 -0
  39. package/dist/chunk-PTU5ZAYX.js +145 -0
  40. package/dist/chunk-PTU5ZAYX.js.map +1 -0
  41. package/dist/chunk-QKO5DA4N.mjs +81 -0
  42. package/dist/chunk-QKO5DA4N.mjs.map +1 -0
  43. package/dist/chunk-T7PIKDUZ.js +130 -0
  44. package/dist/chunk-T7PIKDUZ.js.map +1 -0
  45. package/dist/chunk-TDK5TVJE.mjs +3 -0
  46. package/dist/chunk-TDK5TVJE.mjs.map +1 -0
  47. package/dist/{chunk-NZSZE36T.js → chunk-TJ2EWPER.js} +42 -10
  48. package/dist/chunk-TJ2EWPER.js.map +1 -0
  49. package/dist/chunk-VBNW2B4D.mjs +3 -0
  50. package/dist/chunk-VBNW2B4D.mjs.map +1 -0
  51. package/dist/chunk-W6DO36XD.mjs +96 -0
  52. package/dist/chunk-W6DO36XD.mjs.map +1 -0
  53. package/dist/chunk-XR3J46TZ.js +4 -0
  54. package/dist/chunk-XR3J46TZ.js.map +1 -0
  55. package/dist/chunk-ZOHCADDL.mjs +121 -0
  56. package/dist/chunk-ZOHCADDL.mjs.map +1 -0
  57. package/dist/design-system/audio-player.d.ts +61 -0
  58. package/dist/design-system/audio-player.d.ts.map +1 -0
  59. package/dist/design-system/data-table.d.ts +8 -0
  60. package/dist/design-system/data-table.d.ts.map +1 -0
  61. package/dist/design-system/facade.js +11 -10
  62. package/dist/design-system/facade.js.map +1 -1
  63. package/dist/design-system/facade.mjs +10 -9
  64. package/dist/design-system/facade.mjs.map +1 -1
  65. package/dist/design-system/index.d.ts +2 -0
  66. package/dist/design-system/index.d.ts.map +1 -1
  67. package/dist/hooks/useTableFilter.js +6 -116
  68. package/dist/hooks/useTableFilter.js.map +1 -1
  69. package/dist/hooks/useTableFilter.mjs +1 -118
  70. package/dist/hooks/useTableFilter.mjs.map +1 -1
  71. package/dist/hooks/useTableSort.js +6 -91
  72. package/dist/hooks/useTableSort.js.map +1 -1
  73. package/dist/hooks/useTableSort.mjs +1 -93
  74. package/dist/hooks/useTableSort.mjs.map +1 -1
  75. package/dist/hooks/useVirtualList.js +6 -76
  76. package/dist/hooks/useVirtualList.js.map +1 -1
  77. package/dist/hooks/useVirtualList.mjs +1 -78
  78. package/dist/hooks/useVirtualList.mjs.map +1 -1
  79. package/dist/ui/audio-player/audio-player-base.d.ts +20 -0
  80. package/dist/ui/audio-player/audio-player-base.d.ts.map +1 -0
  81. package/dist/ui/audio-player/audio-player.d.ts +6 -0
  82. package/dist/ui/audio-player/audio-player.d.ts.map +1 -0
  83. package/dist/ui/audio-player/index.d.ts +5 -0
  84. package/dist/ui/audio-player/index.d.ts.map +1 -0
  85. package/dist/ui/audio-player/types.d.ts +44 -0
  86. package/dist/ui/audio-player/types.d.ts.map +1 -0
  87. package/dist/ui/audio-player/variants.d.ts +12 -0
  88. package/dist/ui/audio-player/variants.d.ts.map +1 -0
  89. package/dist/ui/audio-player.js +556 -0
  90. package/dist/ui/audio-player.js.map +1 -0
  91. package/dist/ui/audio-player.mjs +545 -0
  92. package/dist/ui/audio-player.mjs.map +1 -0
  93. package/dist/ui/buttons/animated.js +13 -12
  94. package/dist/ui/buttons/animated.js.map +1 -1
  95. package/dist/ui/buttons/animated.mjs +11 -10
  96. package/dist/ui/buttons/animated.mjs.map +1 -1
  97. package/dist/ui/buttons.js +15 -13
  98. package/dist/ui/buttons.mjs +13 -11
  99. package/dist/ui/checkbox.js +7 -123
  100. package/dist/ui/checkbox.js.map +1 -1
  101. package/dist/ui/checkbox.mjs +2 -126
  102. package/dist/ui/checkbox.mjs.map +1 -1
  103. package/dist/ui/data-table/data-table-base.d.ts +6 -0
  104. package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
  105. package/dist/ui/data-table/data-table.d.ts +6 -0
  106. package/dist/ui/data-table/data-table.d.ts.map +1 -0
  107. package/dist/ui/data-table/index.d.ts +4 -0
  108. package/dist/ui/data-table/index.d.ts.map +1 -0
  109. package/dist/ui/data-table/types.d.ts +92 -0
  110. package/dist/ui/data-table/types.d.ts.map +1 -0
  111. package/dist/ui/data-table/variants.d.ts +8 -0
  112. package/dist/ui/data-table/variants.d.ts.map +1 -0
  113. package/dist/ui/data-table.js +620 -0
  114. package/dist/ui/data-table.js.map +1 -0
  115. package/dist/ui/data-table.mjs +611 -0
  116. package/dist/ui/data-table.mjs.map +1 -0
  117. package/dist/ui/dynamic-stepper.js +23 -22
  118. package/dist/ui/dynamic-stepper.js.map +1 -1
  119. package/dist/ui/dynamic-stepper.mjs +12 -11
  120. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  121. package/dist/ui/inputs.js +7 -138
  122. package/dist/ui/inputs.js.map +1 -1
  123. package/dist/ui/inputs.mjs +2 -141
  124. package/dist/ui/inputs.mjs.map +1 -1
  125. package/dist/ui/pagination.js +25 -225
  126. package/dist/ui/pagination.js.map +1 -1
  127. package/dist/ui/pagination.mjs +13 -227
  128. package/dist/ui/pagination.mjs.map +1 -1
  129. package/dist/ui/table.js +1 -0
  130. package/dist/ui/table.mjs +1 -0
  131. package/package.json +1 -1
  132. package/src/design-system/audio-player.ts +109 -0
  133. package/src/design-system/data-table.ts +20 -0
  134. package/src/design-system/index.ts +2 -0
  135. package/src/ui/audio-player/audio-player-base.tsx +557 -0
  136. package/src/ui/audio-player/audio-player.test.tsx +485 -0
  137. package/src/ui/audio-player/audio-player.tsx +8 -0
  138. package/src/ui/audio-player/index.ts +24 -0
  139. package/src/ui/audio-player/types.ts +57 -0
  140. package/src/ui/audio-player/variants.ts +43 -0
  141. package/src/ui/data-table/data-table-base.tsx +701 -0
  142. package/src/ui/data-table/data-table.test.tsx +389 -0
  143. package/src/ui/data-table/data-table.tsx +11 -0
  144. package/src/ui/data-table/index.ts +24 -0
  145. package/src/ui/data-table/types.ts +121 -0
  146. package/src/ui/data-table/variants.ts +21 -0
  147. package/dist/chunk-BL6UVCV7.mjs.map +0 -1
  148. package/dist/chunk-D2GISTDL.js +0 -19
  149. package/dist/chunk-NZSZE36T.js.map +0 -1
@@ -0,0 +1,557 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+ import type { KeyboardEvent, MouseEvent, PointerEvent } from "react";
13
+
14
+ import { cn } from "../../lib/utils";
15
+
16
+ import type {
17
+ AudioPlayerCtx,
18
+ AudioPlayerProgressProps,
19
+ AudioPlayerProps,
20
+ AudioPlayerTimeProps,
21
+ AudioPlayerVolumeProps,
22
+ } from "./types";
23
+ import {
24
+ audioPlayerBarVariants,
25
+ audioPlayerTimeVariants,
26
+ audioPlayerTrackVariants,
27
+ audioPlayerVariants,
28
+ } from "./variants";
29
+
30
+ export const AudioPlayerContext = createContext<AudioPlayerCtx | null>(null);
31
+
32
+ export function useAudioPlayer(): AudioPlayerCtx {
33
+ const ctx = useContext(AudioPlayerContext);
34
+ if (!ctx) {
35
+ throw new Error("useAudioPlayer must be used within <AudioPlayer>");
36
+ }
37
+ return ctx;
38
+ }
39
+
40
+ function formatTime(seconds: number): string {
41
+ if (!isFinite(seconds) || isNaN(seconds)) return "0:00";
42
+ const m = Math.floor(seconds / 60);
43
+ const s = Math.floor(seconds % 60);
44
+ return `${m}:${s.toString().padStart(2, "0")}`;
45
+ }
46
+
47
+ export function AudioPlayerBase(props: AudioPlayerProps) {
48
+ const {
49
+ className,
50
+ appearance = "default",
51
+ size = "md",
52
+ shape = "rounded",
53
+ src,
54
+ children,
55
+ autoPlay = false,
56
+ loop = false,
57
+ onEnded,
58
+ onPlay,
59
+ onPause,
60
+ onTimeUpdate,
61
+ ref,
62
+ ...rest
63
+ } = props;
64
+
65
+ const audioRef = useRef<HTMLAudioElement>(null);
66
+ const [isPlaying, setIsPlaying] = useState(false);
67
+ const [currentTime, setCurrentTime] = useState(0);
68
+ const [duration, setDuration] = useState(0);
69
+ const [volume, setVolumeState] = useState(1);
70
+ const [muted, setMuted] = useState(false);
71
+
72
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
73
+
74
+ const callbacksRef = useRef({ onEnded, onPlay, onPause, onTimeUpdate });
75
+ useEffect(() => {
76
+ callbacksRef.current = { onEnded, onPlay, onPause, onTimeUpdate };
77
+ });
78
+
79
+ useEffect(() => {
80
+ setIsPlaying(false);
81
+ setCurrentTime(0);
82
+ setDuration(0);
83
+ }, [src]);
84
+
85
+ useEffect(() => {
86
+ const audio = audioRef.current;
87
+ if (!audio) return;
88
+
89
+ const handleTimeUpdate = () => {
90
+ setCurrentTime(audio.currentTime);
91
+ callbacksRef.current.onTimeUpdate?.(audio.currentTime, audio.duration);
92
+ };
93
+ const handleDurationChange = () => {
94
+ if (isFinite(audio.duration)) setDuration(audio.duration);
95
+ };
96
+ const handlePlay = () => {
97
+ setIsPlaying(true);
98
+ callbacksRef.current.onPlay?.();
99
+ };
100
+ const handlePause = () => {
101
+ setIsPlaying(false);
102
+ callbacksRef.current.onPause?.();
103
+ };
104
+ const handleEnded = () => {
105
+ setIsPlaying(false);
106
+ callbacksRef.current.onEnded?.();
107
+ };
108
+ const handleVolumeChange = () => {
109
+ setVolumeState(audio.volume);
110
+ setMuted(audio.muted);
111
+ };
112
+
113
+ audio.addEventListener("timeupdate", handleTimeUpdate);
114
+ audio.addEventListener("durationchange", handleDurationChange);
115
+ audio.addEventListener("loadedmetadata", handleDurationChange);
116
+ audio.addEventListener("play", handlePlay);
117
+ audio.addEventListener("pause", handlePause);
118
+ audio.addEventListener("ended", handleEnded);
119
+ audio.addEventListener("volumechange", handleVolumeChange);
120
+
121
+ return () => {
122
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
123
+ audio.removeEventListener("durationchange", handleDurationChange);
124
+ audio.removeEventListener("loadedmetadata", handleDurationChange);
125
+ audio.removeEventListener("play", handlePlay);
126
+ audio.removeEventListener("pause", handlePause);
127
+ audio.removeEventListener("ended", handleEnded);
128
+ audio.removeEventListener("volumechange", handleVolumeChange);
129
+ };
130
+ }, []);
131
+
132
+ const play = useCallback(() => {
133
+ audioRef.current?.play().catch(() => {});
134
+ }, []);
135
+
136
+ const pause = useCallback(() => {
137
+ audioRef.current?.pause();
138
+ }, []);
139
+
140
+ const toggle = useCallback(() => {
141
+ const audio = audioRef.current;
142
+ if (!audio) return;
143
+ if (audio.paused) {
144
+ audio.play().catch(() => {});
145
+ } else {
146
+ audio.pause();
147
+ }
148
+ }, []);
149
+
150
+ const reset = useCallback(() => {
151
+ const audio = audioRef.current;
152
+ if (!audio) return;
153
+ audio.pause();
154
+ audio.currentTime = 0;
155
+ setIsPlaying(false);
156
+ setCurrentTime(0);
157
+ }, []);
158
+
159
+ const seek = useCallback((seconds: number) => {
160
+ const audio = audioRef.current;
161
+ if (!audio || !isFinite(audio.duration) || audio.duration <= 0) return;
162
+ const nextTime = Math.max(0, Math.min(seconds, audio.duration));
163
+ audio.currentTime = nextTime;
164
+ setDuration(audio.duration);
165
+ setCurrentTime(nextTime);
166
+ }, []);
167
+
168
+ const seekByPercent = useCallback((percent: number) => {
169
+ const audio = audioRef.current;
170
+ if (
171
+ !audio ||
172
+ !isFinite(percent) ||
173
+ !isFinite(audio.duration) ||
174
+ audio.duration <= 0
175
+ ) {
176
+ return;
177
+ }
178
+ const clamped = Math.max(0, Math.min(percent, 100));
179
+ const nextTime = (clamped / 100) * audio.duration;
180
+ audio.currentTime = nextTime;
181
+ setDuration(audio.duration);
182
+ setCurrentTime(nextTime);
183
+ }, []);
184
+
185
+ const setVolume = useCallback((vol: number) => {
186
+ const audio = audioRef.current;
187
+ if (!audio || !isFinite(vol)) return;
188
+ const nextVolume = Math.max(0, Math.min(vol, 1));
189
+ audio.volume = nextVolume;
190
+ setVolumeState(nextVolume);
191
+ }, []);
192
+
193
+ const toggleMute = useCallback(() => {
194
+ const audio = audioRef.current;
195
+ if (!audio) return;
196
+ const nextMuted = !audio.muted;
197
+ audio.muted = nextMuted;
198
+ setMuted(nextMuted);
199
+ }, []);
200
+
201
+ const ctx = useMemo<AudioPlayerCtx>(
202
+ () => ({
203
+ isPlaying,
204
+ currentTime,
205
+ duration,
206
+ progress,
207
+ volume,
208
+ muted,
209
+ play,
210
+ pause,
211
+ toggle,
212
+ reset,
213
+ seek,
214
+ seekByPercent,
215
+ setVolume,
216
+ toggleMute,
217
+ size: size ?? "md",
218
+ shape: shape ?? "rounded",
219
+ appearance: appearance ?? "default",
220
+ }),
221
+ [
222
+ isPlaying,
223
+ currentTime,
224
+ duration,
225
+ progress,
226
+ volume,
227
+ muted,
228
+ play,
229
+ pause,
230
+ toggle,
231
+ reset,
232
+ seek,
233
+ seekByPercent,
234
+ setVolume,
235
+ toggleMute,
236
+ size,
237
+ shape,
238
+ appearance,
239
+ ],
240
+ );
241
+
242
+ return (
243
+ <AudioPlayerContext.Provider value={ctx}>
244
+ <div
245
+ ref={ref}
246
+ data-slot="audio-player"
247
+ className={cn(
248
+ audioPlayerVariants({ appearance, size, shape }),
249
+ className,
250
+ )}
251
+ {...rest}
252
+ >
253
+ <audio
254
+ ref={audioRef}
255
+ src={src}
256
+ autoPlay={autoPlay}
257
+ loop={loop}
258
+ preload="metadata"
259
+ className="hidden"
260
+ aria-hidden="true"
261
+ />
262
+ {children}
263
+ </div>
264
+ </AudioPlayerContext.Provider>
265
+ );
266
+ }
267
+
268
+ AudioPlayerBase.displayName = "AudioPlayer";
269
+
270
+ export function AudioPlayerProgress({
271
+ className,
272
+ ref,
273
+ ...rest
274
+ }: AudioPlayerProgressProps) {
275
+ const { progress, seekByPercent, size, shape } = useAudioPlayer();
276
+ const trackRef = useRef<HTMLDivElement>(null);
277
+ const draggingRef = useRef(false);
278
+
279
+ const getPercentFromEvent = useCallback((clientX: number): number => {
280
+ const el = trackRef.current;
281
+ if (!el) return 0;
282
+ const { left, width } = el.getBoundingClientRect();
283
+ return Math.max(0, Math.min(((clientX - left) / width) * 100, 100));
284
+ }, []);
285
+
286
+ const handleClick = useCallback(
287
+ (e: MouseEvent<HTMLDivElement>) => {
288
+ if (draggingRef.current) return;
289
+ seekByPercent(getPercentFromEvent(e.clientX));
290
+ },
291
+ [getPercentFromEvent, seekByPercent],
292
+ );
293
+
294
+ const handlePointerDown = useCallback(
295
+ (e: PointerEvent<HTMLDivElement>) => {
296
+ draggingRef.current = true;
297
+ e.currentTarget.setPointerCapture?.(e.pointerId);
298
+ seekByPercent(getPercentFromEvent(e.clientX));
299
+ },
300
+ [getPercentFromEvent, seekByPercent],
301
+ );
302
+
303
+ const handlePointerMove = useCallback(
304
+ (e: PointerEvent<HTMLDivElement>) => {
305
+ if (!draggingRef.current) return;
306
+ seekByPercent(getPercentFromEvent(e.clientX));
307
+ },
308
+ [getPercentFromEvent, seekByPercent],
309
+ );
310
+
311
+ const handlePointerUp = useCallback(() => {
312
+ draggingRef.current = false;
313
+ }, []);
314
+
315
+ const handleKeyDown = useCallback(
316
+ (e: KeyboardEvent<HTMLDivElement>) => {
317
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
318
+ e.preventDefault();
319
+ seekByPercent(Math.min(progress + 1, 100));
320
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
321
+ e.preventDefault();
322
+ seekByPercent(Math.max(progress - 1, 0));
323
+ } else if (e.key === "Home") {
324
+ e.preventDefault();
325
+ seekByPercent(0);
326
+ } else if (e.key === "End") {
327
+ e.preventDefault();
328
+ seekByPercent(100);
329
+ }
330
+ },
331
+ [progress, seekByPercent],
332
+ );
333
+
334
+ return (
335
+ <div
336
+ ref={(node) => {
337
+ (trackRef as React.MutableRefObject<HTMLDivElement | null>).current =
338
+ node;
339
+ if (typeof ref === "function") ref(node);
340
+ else if (ref)
341
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
342
+ }}
343
+ data-slot="audio-player-progress"
344
+ role="slider"
345
+ aria-label="Audio progress"
346
+ aria-valuenow={Math.round(progress)}
347
+ aria-valuemin={0}
348
+ aria-valuemax={100}
349
+ tabIndex={0}
350
+ className={cn(
351
+ audioPlayerTrackVariants({ size, shape }),
352
+ "group",
353
+ className,
354
+ )}
355
+ onClick={handleClick}
356
+ onPointerDown={handlePointerDown}
357
+ onPointerMove={handlePointerMove}
358
+ onPointerUp={handlePointerUp}
359
+ onPointerCancel={handlePointerUp}
360
+ onKeyDown={handleKeyDown}
361
+ {...rest}
362
+ >
363
+ <div
364
+ data-slot="audio-player-bar"
365
+ className={cn(
366
+ audioPlayerBarVariants(),
367
+ "rounded-[inherit] group-hover:opacity-90 transition-opacity",
368
+ )}
369
+ style={{ transform: `scaleX(${progress / 100})` }}
370
+ />
371
+ </div>
372
+ );
373
+ }
374
+
375
+ AudioPlayerProgress.displayName = "AudioPlayerProgress";
376
+
377
+ export function AudioPlayerTime({
378
+ className,
379
+ format = formatTime,
380
+ }: AudioPlayerTimeProps) {
381
+ const { currentTime, duration } = useAudioPlayer();
382
+ return (
383
+ <div
384
+ data-slot="audio-player-time"
385
+ className={cn(
386
+ audioPlayerTimeVariants(),
387
+ "flex items-center gap-1",
388
+ className,
389
+ )}
390
+ >
391
+ <span aria-label="Current time">{format(currentTime)}</span>
392
+ <span aria-hidden="true">/</span>
393
+ <span aria-label="Total duration">{format(duration)}</span>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ AudioPlayerTime.displayName = "AudioPlayerTime";
399
+
400
+ export function AudioPlayerVolume({
401
+ className,
402
+ ref,
403
+ ...rest
404
+ }: AudioPlayerVolumeProps) {
405
+ const { volume, muted, setVolume, toggleMute } = useAudioPlayer();
406
+ const trackRef = useRef<HTMLDivElement>(null);
407
+ const draggingRef = useRef(false);
408
+
409
+ const getVolumeFromEvent = useCallback((clientX: number): number => {
410
+ const el = trackRef.current;
411
+ if (!el) return 0;
412
+ const { left, width } = el.getBoundingClientRect();
413
+ return Math.max(0, Math.min((clientX - left) / width, 1));
414
+ }, []);
415
+
416
+ const handlePointerDown = useCallback(
417
+ (e: PointerEvent<HTMLDivElement>) => {
418
+ draggingRef.current = true;
419
+ e.currentTarget.setPointerCapture?.(e.pointerId);
420
+ setVolume(getVolumeFromEvent(e.clientX));
421
+ },
422
+ [getVolumeFromEvent, setVolume],
423
+ );
424
+
425
+ const handlePointerMove = useCallback(
426
+ (e: PointerEvent<HTMLDivElement>) => {
427
+ if (!draggingRef.current) return;
428
+ setVolume(getVolumeFromEvent(e.clientX));
429
+ },
430
+ [getVolumeFromEvent, setVolume],
431
+ );
432
+
433
+ const handlePointerUp = useCallback(() => {
434
+ draggingRef.current = false;
435
+ }, []);
436
+
437
+ const handleClick = useCallback(
438
+ (e: MouseEvent<HTMLDivElement>) => {
439
+ if (draggingRef.current) return;
440
+ setVolume(getVolumeFromEvent(e.clientX));
441
+ },
442
+ [getVolumeFromEvent, setVolume],
443
+ );
444
+
445
+ const displayVolume = muted ? 0 : volume;
446
+
447
+ const handleKeyDown = useCallback(
448
+ (e: KeyboardEvent<HTMLDivElement>) => {
449
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
450
+ e.preventDefault();
451
+ setVolume(Math.min(volume + 0.05, 1));
452
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
453
+ e.preventDefault();
454
+ setVolume(Math.max(volume - 0.05, 0));
455
+ } else if (e.key === "Home") {
456
+ e.preventDefault();
457
+ setVolume(0);
458
+ } else if (e.key === "End") {
459
+ e.preventDefault();
460
+ setVolume(1);
461
+ }
462
+ },
463
+ [volume, setVolume],
464
+ );
465
+
466
+ return (
467
+ <div
468
+ data-slot="audio-player-volume"
469
+ className={cn("flex items-center gap-2", className)}
470
+ {...rest}
471
+ >
472
+ <button
473
+ type="button"
474
+ aria-label={muted ? "Unmute" : "Mute"}
475
+ onClick={toggleMute}
476
+ className="shrink-0 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--audio-fill,#0f172a)]"
477
+ >
478
+ {displayVolume === 0 ? (
479
+ <svg
480
+ className="h-4 w-4"
481
+ viewBox="0 0 24 24"
482
+ fill="none"
483
+ stroke="currentColor"
484
+ strokeWidth={2}
485
+ strokeLinecap="round"
486
+ strokeLinejoin="round"
487
+ aria-hidden="true"
488
+ >
489
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
490
+ <line x1="23" y1="9" x2="17" y2="15" />
491
+ <line x1="17" y1="9" x2="23" y2="15" />
492
+ </svg>
493
+ ) : displayVolume < 0.5 ? (
494
+ <svg
495
+ className="h-4 w-4"
496
+ viewBox="0 0 24 24"
497
+ fill="none"
498
+ stroke="currentColor"
499
+ strokeWidth={2}
500
+ strokeLinecap="round"
501
+ strokeLinejoin="round"
502
+ aria-hidden="true"
503
+ >
504
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
505
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
506
+ </svg>
507
+ ) : (
508
+ <svg
509
+ className="h-4 w-4"
510
+ viewBox="0 0 24 24"
511
+ fill="none"
512
+ stroke="currentColor"
513
+ strokeWidth={2}
514
+ strokeLinecap="round"
515
+ strokeLinejoin="round"
516
+ aria-hidden="true"
517
+ >
518
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
519
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
520
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
521
+ </svg>
522
+ )}
523
+ </button>
524
+ <div
525
+ ref={(node) => {
526
+ (trackRef as React.MutableRefObject<HTMLDivElement | null>).current =
527
+ node;
528
+ if (typeof ref === "function") ref(node);
529
+ else if (ref)
530
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current =
531
+ node;
532
+ }}
533
+ role="slider"
534
+ aria-label="Volume"
535
+ aria-valuenow={Math.round(displayVolume * 100)}
536
+ aria-valuemin={0}
537
+ aria-valuemax={100}
538
+ tabIndex={0}
539
+ className="relative h-1.5 w-20 cursor-pointer overflow-hidden rounded-full bg-[var(--zui-audio-player-track-bg,var(--zui-surface-muted,#0000001a))] dark:bg-[var(--zui-audio-player-track-bg-dark,var(--zui-surface-muted-dark,#ffffff1a))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--audio-fill,#0f172a)]"
540
+ onClick={handleClick}
541
+ onPointerDown={handlePointerDown}
542
+ onPointerMove={handlePointerMove}
543
+ onPointerUp={handlePointerUp}
544
+ onPointerCancel={handlePointerUp}
545
+ onKeyDown={handleKeyDown}
546
+ >
547
+ <div
548
+ data-slot="audio-player-volume-bar"
549
+ className="h-full origin-left [background:var(--audio-fill)] rounded-[inherit]"
550
+ style={{ transform: `scaleX(${displayVolume})` }}
551
+ />
552
+ </div>
553
+ </div>
554
+ );
555
+ }
556
+
557
+ AudioPlayerVolume.displayName = "AudioPlayerVolume";