bobinejs 0.0.0-stage → 0.2.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,1720 @@
1
+ "use client";
2
+
3
+ // src/utils.ts
4
+ import { clsx } from "clsx";
5
+ import { twMerge } from "tailwind-merge";
6
+ function cn(...inputs) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+
10
+ // ../../components/ui/media-icons.tsx
11
+ import * as React from "react";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ var base = {
14
+ xmlns: "http://www.w3.org/2000/svg",
15
+ width: 16,
16
+ height: 16,
17
+ viewBox: "0 0 24 24",
18
+ "aria-hidden": true
19
+ };
20
+ function PlayIcon(props) {
21
+ return /* @__PURE__ */ jsx("svg", { ...base, fill: "currentColor", ...props, children: /* @__PURE__ */ jsx("path", { d: "M6.87 4.9v14.2a1.5 1.5 0 0 0 2.31 1.26l11.07-7.1a1.5 1.5 0 0 0 0-2.52L9.18 3.64A1.5 1.5 0 0 0 6.87 4.9Z" }) });
22
+ }
23
+ function PauseIcon(props) {
24
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "currentColor", ...props, children: [
25
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "3.5", width: "5", height: "17", rx: "1.6" }),
26
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "3.5", width: "5", height: "17", rx: "1.6" })
27
+ ] });
28
+ }
29
+ function VolumeIcon({ muted: muted2, ...props }) {
30
+ const fade = "origin-[11px_12px] transition-[opacity,transform] duration-200 ease-media motion-reduce:transition-none";
31
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
32
+ /* @__PURE__ */ jsx(
33
+ "path",
34
+ {
35
+ d: "M11 5.1v13.8a1 1 0 0 1-1.63.77L5.5 16.5h-2A1.5 1.5 0 0 1 2 15V9a1.5 1.5 0 0 1 1.5-1.5h2l3.87-3.17A1 1 0 0 1 11 5.1Z",
36
+ fill: "currentColor",
37
+ stroke: "none"
38
+ }
39
+ ),
40
+ /* @__PURE__ */ jsxs("g", { className: fade, style: { opacity: muted2 ? 0 : 1, transform: muted2 ? "scale(0.6)" : "none" }, children: [
41
+ /* @__PURE__ */ jsx("path", { d: "M15 9.5a3.5 3.5 0 0 1 0 5" }),
42
+ /* @__PURE__ */ jsx("path", { d: "M17.8 6.7a7.5 7.5 0 0 1 0 10.6" })
43
+ ] }),
44
+ /* @__PURE__ */ jsxs("g", { className: fade, style: { opacity: muted2 ? 1 : 0, transform: muted2 ? "none" : "scale(0.6)" }, children: [
45
+ /* @__PURE__ */ jsx("path", { d: "m16 9.5 5 5" }),
46
+ /* @__PURE__ */ jsx("path", { d: "m21 9.5-5 5" })
47
+ ] })
48
+ ] });
49
+ }
50
+ function EnterFullscreenIcon(props) {
51
+ return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "M4 9V6a2 2 0 0 1 2-2h3M15 4h3a2 2 0 0 1 2 2v3M20 15v3a2 2 0 0 1-2 2h-3M9 20H6a2 2 0 0 1-2-2v-3" }) });
52
+ }
53
+ function ExitFullscreenIcon(props) {
54
+ return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "M9 4v3a2 2 0 0 1-2 2H4M20 9h-3a2 2 0 0 1-2-2V4M15 20v-3a2 2 0 0 1 2-2h3M4 15h3a2 2 0 0 1 2 2v3" }) });
55
+ }
56
+ function PictureInPictureIcon(props) {
57
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
58
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "4.5", width: "19", height: "15", rx: "2.5" }),
59
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "11.5", width: "6.5", height: "5", rx: "1", fill: "currentColor", stroke: "none" })
60
+ ] });
61
+ }
62
+ var captionLetters = "M10.5 10.2a2.2 2.2 0 1 0 0 3.6M17 10.2a2.2 2.2 0 1 0 0 3.6";
63
+ function CaptionsIcon({ active, ...props }) {
64
+ const maskId = `cc${React.useId().replace(/[^\w-]/g, "")}`;
65
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
66
+ /* @__PURE__ */ jsxs("mask", { id: maskId, children: [
67
+ /* @__PURE__ */ jsx("rect", { width: "24", height: "24", fill: "white" }),
68
+ /* @__PURE__ */ jsx("path", { d: captionLetters, stroke: "black" })
69
+ ] }),
70
+ /* @__PURE__ */ jsx(
71
+ "rect",
72
+ {
73
+ x: "2.5",
74
+ y: "4.5",
75
+ width: "19",
76
+ height: "15",
77
+ rx: "3",
78
+ fill: "currentColor",
79
+ mask: `url(#${maskId})`,
80
+ className: "transition-opacity duration-200 ease-media motion-reduce:transition-none",
81
+ style: { opacity: active ? 1 : 0 }
82
+ }
83
+ ),
84
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "4.5", width: "19", height: "15", rx: "3" }),
85
+ /* @__PURE__ */ jsx(
86
+ "path",
87
+ {
88
+ d: captionLetters,
89
+ className: "transition-opacity duration-200 ease-media motion-reduce:transition-none",
90
+ style: { opacity: active ? 0 : 1 }
91
+ }
92
+ )
93
+ ] });
94
+ }
95
+ function SettingsIcon(props) {
96
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
97
+ /* @__PURE__ */ jsx("path", { d: "M4 7h9M17 7h3M4 17h3M11 17h9" }),
98
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "7", r: "2" }),
99
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "17", r: "2" })
100
+ ] });
101
+ }
102
+ function RetryIcon(props) {
103
+ return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
104
+ /* @__PURE__ */ jsx("path", { d: "M3.5 12a8.5 8.5 0 1 0 2.6-6.1" }),
105
+ /* @__PURE__ */ jsx("path", { d: "M3.5 4v4.5H8" })
106
+ ] });
107
+ }
108
+ function CheckIcon(props) {
109
+ return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "m5 12.5 4.5 4.5L19 7.5" }) });
110
+ }
111
+ function SeekIcon({ direction, ...props }) {
112
+ return /* @__PURE__ */ jsx("svg", { ...base, fill: "currentColor", ...props, style: { transform: direction === "back" ? "scaleX(-1)" : void 0, ...props.style }, children: /* @__PURE__ */ jsx("path", { d: "M3 6.7v10.6a1 1 0 0 0 1.55.83L12 13.2v4.1a1 1 0 0 0 1.55.83l7.9-5.3a1 1 0 0 0 0-1.66l-7.9-5.3A1 1 0 0 0 12 6.7v4.1L4.55 5.87A1 1 0 0 0 3 6.7Z" }) });
113
+ }
114
+
115
+ // ../../components/ui/media.tsx
116
+ import * as React2 from "react";
117
+
118
+ // ../../lib/media-store.ts
119
+ var MEDIA_EVENTS = [
120
+ "play",
121
+ "pause",
122
+ "playing",
123
+ "ended",
124
+ "waiting",
125
+ "canplay",
126
+ "seeking",
127
+ "seeked",
128
+ "timeupdate",
129
+ "durationchange",
130
+ "loadedmetadata",
131
+ "progress",
132
+ "volumechange",
133
+ "ratechange",
134
+ "emptied",
135
+ "loadstart",
136
+ "error",
137
+ "enterpictureinpicture",
138
+ "leavepictureinpicture"
139
+ ];
140
+ var TRACK_EVENTS = ["addtrack", "removetrack", "change"];
141
+ var initialState = {
142
+ paused: true,
143
+ ended: false,
144
+ started: false,
145
+ waiting: false,
146
+ currentTime: 0,
147
+ duration: 0,
148
+ bufferedEnd: 0,
149
+ volume: 1,
150
+ muted: false,
151
+ playbackRate: 1,
152
+ fullscreen: false,
153
+ pictureInPicture: false,
154
+ error: null,
155
+ hasSource: false,
156
+ textTracks: [],
157
+ textTrack: -1,
158
+ cues: []
159
+ };
160
+ function readBufferedEnd(media) {
161
+ const { buffered, currentTime } = media;
162
+ for (let i = 0; i < buffered.length; i++) {
163
+ if (buffered.start(i) <= currentTime && buffered.end(i) >= currentTime) {
164
+ return buffered.end(i);
165
+ }
166
+ }
167
+ return buffered.length ? buffered.end(buffered.length - 1) : 0;
168
+ }
169
+ function captionTracks(media) {
170
+ if (!media?.textTracks) return [];
171
+ return Array.from(media.textTracks).filter(
172
+ (track) => track.kind === "captions" || track.kind === "subtitles"
173
+ );
174
+ }
175
+ function cueText(cue) {
176
+ const vtt = cue;
177
+ const text = typeof vtt.getCueAsHTML === "function" ? vtt.getCueAsHTML().textContent : vtt.text?.replace(/<[^>]+>/g, "");
178
+ return (text ?? "").trim();
179
+ }
180
+ function defaultTrack(media, tracks) {
181
+ const el = Array.from(media?.querySelectorAll("track") ?? []).find((track) => track.default);
182
+ return el ? tracks.indexOf(el.track) : -1;
183
+ }
184
+ var sameList = (a, b) => a.length === b.length && a.every((value, i) => Object.is(value, b[i]));
185
+ var sameTracks = (a, b) => a.length === b.length && a.every((t, i) => t.label === b[i].label && t.language === b[i].language && t.kind === b[i].kind);
186
+ function readPreferences(key) {
187
+ try {
188
+ return JSON.parse(localStorage.getItem(key) ?? "{}");
189
+ } catch {
190
+ return {};
191
+ }
192
+ }
193
+ function createMediaStore({ persistKey } = {}) {
194
+ let state = initialState;
195
+ let media = null;
196
+ let container = null;
197
+ let cueTrack = null;
198
+ let metadata = null;
199
+ let trackChosen = false;
200
+ const listeners = /* @__PURE__ */ new Set();
201
+ const set = (patch) => {
202
+ let changed = false;
203
+ for (const key in patch) {
204
+ const k = key;
205
+ const next = patch[k];
206
+ const prev = state[k];
207
+ if (k === "cues" ? !sameList(prev, next) : k === "textTracks" ? !sameTracks(prev, next) : !Object.is(prev, next)) {
208
+ changed = true;
209
+ break;
210
+ }
211
+ }
212
+ if (!changed) return;
213
+ state = { ...state, ...patch };
214
+ listeners.forEach((l) => l());
215
+ };
216
+ const sync = () => {
217
+ if (!media) return;
218
+ set({
219
+ paused: media.paused,
220
+ ended: media.ended,
221
+ started: state.started || !media.paused || media.currentTime > 0,
222
+ currentTime: media.currentTime,
223
+ duration: Number.isFinite(media.duration) ? media.duration : 0,
224
+ bufferedEnd: readBufferedEnd(media),
225
+ volume: media.volume,
226
+ muted: media.muted,
227
+ playbackRate: media.playbackRate,
228
+ pictureInPicture: typeof document !== "undefined" && document.pictureInPictureElement === media,
229
+ error: media.error,
230
+ hasSource: Boolean(media.src || media.currentSrc || media.querySelector("source[src]"))
231
+ });
232
+ };
233
+ const readCues = () => {
234
+ const active = cueTrack?.activeCues;
235
+ set({ cues: active ? Array.from(active).map(cueText).filter(Boolean) : [] });
236
+ };
237
+ const bindCueTrack = (track) => {
238
+ if (cueTrack === track) return;
239
+ cueTrack?.removeEventListener("cuechange", readCues);
240
+ cueTrack = track;
241
+ cueTrack?.addEventListener("cuechange", readCues);
242
+ };
243
+ const syncTracks = (requested = state.textTrack) => {
244
+ const tracks = captionTracks(media);
245
+ let active = requested === -1 && !trackChosen ? defaultTrack(media, tracks) : requested;
246
+ tracks.forEach((track, i) => {
247
+ if (track.mode === "showing") active = i;
248
+ });
249
+ if (active >= tracks.length) active = -1;
250
+ tracks.forEach((track, i) => {
251
+ const mode = i === active ? "hidden" : "disabled";
252
+ if (track.mode !== mode) track.mode = mode;
253
+ });
254
+ bindCueTrack(tracks[active] ?? null);
255
+ set({
256
+ textTracks: tracks.map((track, i) => ({
257
+ label: track.label || track.language || `Track ${i + 1}`,
258
+ language: track.language,
259
+ kind: track.kind
260
+ })),
261
+ textTrack: active
262
+ });
263
+ readCues();
264
+ };
265
+ const onTracksChange = () => syncTracks();
266
+ const applyMediaSession = () => {
267
+ if (typeof navigator === "undefined" || !("mediaSession" in navigator) || !metadata) return;
268
+ const session = navigator.mediaSession;
269
+ session.metadata = new MediaMetadata(metadata);
270
+ const handlers = [
271
+ ["play", () => actions.play()],
272
+ ["pause", () => actions.pause()],
273
+ ["seekbackward", (d) => actions.seekBy(-(d.seekOffset ?? 10))],
274
+ ["seekforward", (d) => actions.seekBy(d.seekOffset ?? 10)],
275
+ ["seekto", (d) => d.seekTime !== void 0 && actions.seek(d.seekTime)]
276
+ ];
277
+ for (const [action, handler] of handlers) {
278
+ try {
279
+ session.setActionHandler(action, handler);
280
+ } catch {
281
+ }
282
+ }
283
+ };
284
+ const updatePositionState = () => {
285
+ if (!metadata || !media || typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
286
+ if (!Number.isFinite(media.duration) || media.duration <= 0) return;
287
+ try {
288
+ navigator.mediaSession.setPositionState({
289
+ duration: media.duration,
290
+ playbackRate: media.playbackRate,
291
+ position: Math.min(media.currentTime, media.duration)
292
+ });
293
+ } catch {
294
+ }
295
+ };
296
+ const savePreferences = () => {
297
+ if (!persistKey || !media) return;
298
+ try {
299
+ const prefs = {
300
+ volume: media.volume,
301
+ muted: media.muted,
302
+ playbackRate: media.playbackRate
303
+ };
304
+ localStorage.setItem(persistKey, JSON.stringify(prefs));
305
+ } catch {
306
+ }
307
+ };
308
+ const onEvent = (event) => {
309
+ switch (event.type) {
310
+ case "emptied":
311
+ set({ started: false, waiting: false });
312
+ break;
313
+ case "waiting":
314
+ set({ waiting: true });
315
+ break;
316
+ case "playing":
317
+ case "canplay":
318
+ case "pause":
319
+ case "error":
320
+ set({ waiting: false });
321
+ break;
322
+ case "play":
323
+ applyMediaSession();
324
+ break;
325
+ case "volumechange":
326
+ case "ratechange":
327
+ savePreferences();
328
+ break;
329
+ case "loadedmetadata":
330
+ syncTracks();
331
+ break;
332
+ }
333
+ if (event.type === "durationchange" || event.type === "ratechange" || event.type === "seeked") {
334
+ updatePositionState();
335
+ }
336
+ sync();
337
+ };
338
+ const onFullscreenChange = () => {
339
+ const el = document.fullscreenElement;
340
+ const video = media;
341
+ set({
342
+ fullscreen: !!el && (el === container || el === media) || !!video?.webkitDisplayingFullscreen
343
+ });
344
+ };
345
+ const attach = (el) => {
346
+ if (media === el) return;
347
+ if (media) {
348
+ MEDIA_EVENTS.forEach((e) => media.removeEventListener(e, onEvent));
349
+ TRACK_EVENTS.forEach((e) => media.textTracks?.removeEventListener?.(e, onTracksChange));
350
+ media.removeEventListener("webkitbeginfullscreen", onFullscreenChange);
351
+ media.removeEventListener("webkitendfullscreen", onFullscreenChange);
352
+ bindCueTrack(null);
353
+ }
354
+ media = el;
355
+ if (!el) {
356
+ state = initialState;
357
+ listeners.forEach((l) => l());
358
+ return;
359
+ }
360
+ if (persistKey) {
361
+ const prefs = readPreferences(persistKey);
362
+ if (typeof prefs.volume === "number") el.volume = Math.min(Math.max(prefs.volume, 0), 1);
363
+ if (typeof prefs.muted === "boolean") el.muted = prefs.muted;
364
+ if (typeof prefs.playbackRate === "number") el.playbackRate = prefs.playbackRate;
365
+ }
366
+ MEDIA_EVENTS.forEach((e) => el.addEventListener(e, onEvent));
367
+ TRACK_EVENTS.forEach((e) => el.textTracks?.addEventListener?.(e, onTracksChange));
368
+ el.addEventListener("webkitbeginfullscreen", onFullscreenChange);
369
+ el.addEventListener("webkitendfullscreen", onFullscreenChange);
370
+ sync();
371
+ syncTracks();
372
+ };
373
+ const setContainer = (el) => {
374
+ if (typeof document === "undefined") return;
375
+ document.removeEventListener("fullscreenchange", onFullscreenChange);
376
+ container = el;
377
+ if (el) document.addEventListener("fullscreenchange", onFullscreenChange);
378
+ };
379
+ const play = async () => {
380
+ if (!media) return;
381
+ try {
382
+ await media.play();
383
+ } catch {
384
+ }
385
+ };
386
+ const pause = () => media?.pause();
387
+ const actions = {
388
+ play,
389
+ pause,
390
+ toggle: () => media?.paused || media?.ended ? play() : pause(),
391
+ seek: (time) => {
392
+ if (!media) return;
393
+ const max = Number.isFinite(media.duration) ? media.duration : time;
394
+ media.currentTime = Math.min(Math.max(time, 0), max);
395
+ sync();
396
+ },
397
+ seekBy: (delta) => actions.seek((media?.currentTime ?? 0) + delta),
398
+ /** Jump to a fraction (0–1) of the duration. */
399
+ seekToRatio: (ratio) => actions.seek(ratio * (media?.duration || 0)),
400
+ setVolume: (volume) => {
401
+ if (!media) return;
402
+ media.volume = Math.min(Math.max(volume, 0), 1);
403
+ media.muted = media.volume === 0;
404
+ },
405
+ toggleMute: () => {
406
+ if (!media) return;
407
+ if (media.muted && media.volume === 0) media.volume = 1;
408
+ media.muted = !media.muted;
409
+ },
410
+ setPlaybackRate: (rate) => {
411
+ if (media) media.playbackRate = rate;
412
+ },
413
+ /** Select a caption track by index, or -1 to turn captions off. */
414
+ setTextTrack: (index) => syncTracksExplicit(index),
415
+ toggleCaptions: () => {
416
+ if (!state.textTracks.length) return;
417
+ syncTracksExplicit(state.textTrack === -1 ? 0 : -1);
418
+ },
419
+ /** Reload the current source after an error. */
420
+ retry: () => {
421
+ if (!media) return;
422
+ media.load();
423
+ void play();
424
+ },
425
+ setMetadata: (next) => {
426
+ metadata = next;
427
+ if (media && !media.paused) applyMediaSession();
428
+ },
429
+ toggleFullscreen: async () => {
430
+ if (typeof document === "undefined") return;
431
+ if (document.fullscreenElement) {
432
+ await document.exitFullscreen().catch(() => {
433
+ });
434
+ return;
435
+ }
436
+ if (container?.requestFullscreen) {
437
+ await container.requestFullscreen().catch(() => {
438
+ });
439
+ return;
440
+ }
441
+ ;
442
+ media?.webkitEnterFullscreen?.();
443
+ },
444
+ togglePictureInPicture: async () => {
445
+ if (!(media instanceof HTMLVideoElement)) return;
446
+ try {
447
+ if (document.pictureInPictureElement) await document.exitPictureInPicture();
448
+ else await media.requestPictureInPicture();
449
+ } catch {
450
+ }
451
+ }
452
+ };
453
+ const syncTracksExplicit = (index) => {
454
+ trackChosen = true;
455
+ captionTracks(media).forEach((track, i) => {
456
+ if (i !== index && track.mode === "showing") track.mode = "disabled";
457
+ });
458
+ syncTracks(index);
459
+ };
460
+ return {
461
+ getState: () => state,
462
+ getServerState: () => initialState,
463
+ getMedia: () => media,
464
+ subscribe: (listener) => {
465
+ listeners.add(listener);
466
+ return () => listeners.delete(listener);
467
+ },
468
+ attach,
469
+ setContainer,
470
+ ...actions
471
+ };
472
+ }
473
+ function formatTime(seconds, reference = seconds) {
474
+ if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
475
+ const s = Math.floor(seconds % 60);
476
+ const m = Math.floor(seconds / 60 % 60);
477
+ const h = Math.floor(seconds / 3600);
478
+ const ss = s.toString().padStart(2, "0");
479
+ const hours = h > 0 || Number.isFinite(reference) && reference >= 3600;
480
+ return hours ? `${h}:${m.toString().padStart(2, "0")}:${ss}` : `${m}:${ss}`;
481
+ }
482
+ function describeMediaError(error) {
483
+ switch (error?.code) {
484
+ case 1:
485
+ return "Playback was aborted.";
486
+ case 2:
487
+ return "A network error stopped the download.";
488
+ case 3:
489
+ return "This media could not be decoded.";
490
+ case 4:
491
+ return "This media couldn't be loaded.";
492
+ default:
493
+ return "Something went wrong during playback.";
494
+ }
495
+ }
496
+
497
+ // ../../components/ui/media.tsx
498
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
499
+ var MediaContext = React2.createContext(null);
500
+ var MediaOptionsContext = React2.createContext({ tooltips: false, hotkeys: false });
501
+ function useMediaStore() {
502
+ const store = React2.useContext(MediaContext);
503
+ if (!store) throw new Error("Media components must be rendered inside <MediaRoot>.");
504
+ return store;
505
+ }
506
+ function useMedia(selector) {
507
+ const store = useMediaStore();
508
+ return React2.useSyncExternalStore(
509
+ store.subscribe,
510
+ () => selector(store.getState()),
511
+ () => selector(store.getServerState())
512
+ );
513
+ }
514
+ function useComposedRefs(a, b, c) {
515
+ return React2.useCallback((node) => composeRefs(a, b, c)(node), [a, b, c]);
516
+ }
517
+ function composeRefs(...refs) {
518
+ return (node) => {
519
+ const cleanups = refs.map((ref) => {
520
+ if (typeof ref === "function") return ref(node);
521
+ if (ref) ref.current = node;
522
+ });
523
+ return () => {
524
+ cleanups.forEach((cleanup, i) => {
525
+ const ref = refs[i];
526
+ if (typeof cleanup === "function") cleanup();
527
+ else if (typeof ref === "function") ref(null);
528
+ else if (ref) ref.current = null;
529
+ });
530
+ };
531
+ };
532
+ }
533
+ var clamp = (value) => Math.min(Math.max(value, 0), 1);
534
+ var SETTINGS_OPEN = "[data-slot='media-settings'][data-open]";
535
+ var holdAwakeSelector = () => window.matchMedia("(hover: hover)").matches ? `[data-slot='media-controls']:hover, ${SETTINGS_OPEN}` : SETTINGS_OPEN;
536
+ function MediaRoot({
537
+ className,
538
+ idleTimeout = 2e3,
539
+ persistKey,
540
+ metadata,
541
+ hotkeys = true,
542
+ tooltips = false,
543
+ onPointerMove,
544
+ onPointerDown,
545
+ onPointerLeave,
546
+ onKeyDown,
547
+ ref,
548
+ ...props
549
+ }) {
550
+ const [store] = React2.useState(() => createMediaStore({ persistKey }));
551
+ const [active, setActive] = React2.useState(false);
552
+ const timer = React2.useRef(void 0);
553
+ const rootEl = React2.useRef(null);
554
+ const state = React2.useSyncExternalStore(store.subscribe, store.getState, store.getServerState);
555
+ const rootRef = useComposedRefs(ref, rootEl, store.setContainer);
556
+ const options = React2.useMemo(() => ({ tooltips, hotkeys }), [tooltips, hotkeys]);
557
+ React2.useEffect(() => () => clearTimeout(timer.current), []);
558
+ const { title, artist, album, artwork } = metadata ?? {};
559
+ const artworkKey = JSON.stringify(artwork ?? null);
560
+ React2.useEffect(() => {
561
+ store.setMetadata(title || artist || album ? { title, artist, album, artwork: JSON.parse(artworkKey) ?? void 0 } : null);
562
+ }, [store, title, artist, album, artworkKey]);
563
+ const wake = () => {
564
+ setActive(true);
565
+ clearTimeout(timer.current);
566
+ timer.current = setTimeout(function sleep() {
567
+ if (rootEl.current?.querySelector(holdAwakeSelector())) {
568
+ timer.current = setTimeout(sleep, idleTimeout);
569
+ return;
570
+ }
571
+ setActive(false);
572
+ }, idleTimeout);
573
+ };
574
+ const handleKeyDown = (event) => {
575
+ onKeyDown?.(event);
576
+ if (!hotkeys || event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
577
+ const target = event.target;
578
+ if (target.closest("input, textarea, select, [contenteditable='true'], [data-slot='media-settings']")) return;
579
+ const onButton = target.tagName === "BUTTON";
580
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
581
+ const keymap = {
582
+ " ": () => store.toggle(),
583
+ k: () => store.toggle(),
584
+ j: () => store.seekBy(-10),
585
+ l: () => store.seekBy(10),
586
+ ArrowLeft: () => store.seekBy(-5),
587
+ ArrowRight: () => store.seekBy(5),
588
+ ArrowUp: () => store.setVolume(store.getState().volume + 0.1),
589
+ ArrowDown: () => store.setVolume(store.getState().volume - 0.1),
590
+ m: () => store.toggleMute(),
591
+ f: () => store.toggleFullscreen(),
592
+ c: () => store.toggleCaptions(),
593
+ i: () => store.togglePictureInPicture(),
594
+ Home: () => store.seek(0),
595
+ End: () => store.seekToRatio(1)
596
+ };
597
+ for (let digit = 0; digit <= 9; digit++) {
598
+ keymap[String(digit)] = () => store.seekToRatio(digit / 10);
599
+ }
600
+ const action = keymap[key];
601
+ if (!action || key === " " && onButton) return;
602
+ event.preventDefault();
603
+ action();
604
+ wake();
605
+ };
606
+ const idle = !active && !state.paused;
607
+ return /* @__PURE__ */ jsx2(MediaContext.Provider, { value: store, children: /* @__PURE__ */ jsx2(MediaOptionsContext.Provider, { value: options, children: /* @__PURE__ */ jsx2(
608
+ "div",
609
+ {
610
+ ref: rootRef,
611
+ "data-slot": "media-root",
612
+ "data-paused": state.paused || void 0,
613
+ "data-playing": !state.paused || void 0,
614
+ "data-started": state.started || void 0,
615
+ "data-waiting": state.waiting || void 0,
616
+ "data-fullscreen": state.fullscreen || void 0,
617
+ "data-error": state.error ? "" : void 0,
618
+ "data-captions": state.textTrack !== -1 && state.cues.length > 0 ? "" : void 0,
619
+ "data-idle": idle || void 0,
620
+ "data-active": active || void 0,
621
+ className: cn(
622
+ /* no gray tap flash, no long-press callout over the video, and no
623
+ double-tap-zoom wait before a tap registers */
624
+ "group/media relative isolate touch-manipulation [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none]",
625
+ idle && "cursor-none",
626
+ className
627
+ ),
628
+ onPointerMove: (event) => {
629
+ onPointerMove?.(event);
630
+ if (event.pointerType === "mouse") wake();
631
+ },
632
+ onPointerDown: (event) => {
633
+ onPointerDown?.(event);
634
+ wake();
635
+ },
636
+ onPointerLeave: (event) => {
637
+ onPointerLeave?.(event);
638
+ if (event.pointerType !== "mouse") return;
639
+ clearTimeout(timer.current);
640
+ setActive(false);
641
+ },
642
+ onKeyDown: handleKeyDown,
643
+ ...props
644
+ }
645
+ ) }) });
646
+ }
647
+ function useMediaElement(ref, src, loader) {
648
+ const store = useMediaStore();
649
+ const mediaRef = useComposedRefs(ref, store.attach);
650
+ React2.useEffect(() => {
651
+ const media = store.getMedia();
652
+ if (!loader || !src || !media) return;
653
+ return loader(media, src) ?? void 0;
654
+ }, [store, loader, src]);
655
+ return mediaRef;
656
+ }
657
+ function MediaVideo({ className, ref, src, loader, ...props }) {
658
+ const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
659
+ return /* @__PURE__ */ jsx2(
660
+ "video",
661
+ {
662
+ ref: mediaRef,
663
+ "data-slot": "media-video",
664
+ playsInline: true,
665
+ preload: "metadata",
666
+ ...props,
667
+ src: loader ? void 0 : src,
668
+ controls: false,
669
+ className: cn("block", className)
670
+ }
671
+ );
672
+ }
673
+ function MediaAudio({ ref, src, loader, ...props }) {
674
+ const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
675
+ return /* @__PURE__ */ jsx2(
676
+ "audio",
677
+ {
678
+ ref: mediaRef,
679
+ "data-slot": "media-audio",
680
+ preload: "metadata",
681
+ ...props,
682
+ src: loader ? void 0 : src,
683
+ controls: false
684
+ }
685
+ );
686
+ }
687
+ var buttonClass = "pointer-coarse:after:absolute pointer-coarse:after:-inset-1 pointer-coarse:after:content-[''] group/media-button relative inline-grid size-7 shrink-0 cursor-pointer place-items-center rounded-md outline-none transition-[background-color,scale,opacity] duration-150 ease-media hover:bg-current/15 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:enabled:active:scale-[0.96] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0";
688
+ function MediaTooltip({ label, shortcut, align = "center" }) {
689
+ const { tooltips, hotkeys } = React2.useContext(MediaOptionsContext);
690
+ if (!tooltips || !label) return null;
691
+ return /* @__PURE__ */ jsxs2(
692
+ "span",
693
+ {
694
+ "aria-hidden": "true",
695
+ "data-slot": "media-tooltip",
696
+ className: cn(
697
+ "pointer-events-none absolute bottom-full mb-2 flex items-center gap-1.5 rounded-md bg-foreground px-1.5 py-0.5 text-[11px] leading-4 font-medium whitespace-nowrap text-background opacity-0",
698
+ "transition-[opacity,translate] duration-150 ease-media motion-safe:translate-y-0.5 motion-reduce:transition-none",
699
+ "group-hover/media-button:translate-y-0 group-hover/media-button:opacity-100 group-hover/media-button:delay-300",
700
+ "group-focus-visible/media-button:translate-y-0 group-focus-visible/media-button:opacity-100",
701
+ "group-aria-expanded/media-button:hidden",
702
+ align === "start" && "left-0",
703
+ align === "center" && "left-1/2 -translate-x-1/2",
704
+ align === "end" && "right-0"
705
+ ),
706
+ children: [
707
+ label,
708
+ hotkeys && shortcut && /* @__PURE__ */ jsx2("kbd", { className: "font-mono opacity-60", children: shortcut })
709
+ ]
710
+ }
711
+ );
712
+ }
713
+ function MediaButton({ className, type = "button", tooltip, shortcut, tooltipAlign, children, ...props }) {
714
+ return /* @__PURE__ */ jsxs2("button", { type, "data-slot": "media-button", className: cn(buttonClass, className), ...props, children: [
715
+ children,
716
+ /* @__PURE__ */ jsx2(MediaTooltip, { label: tooltip, shortcut, align: tooltipAlign })
717
+ ] });
718
+ }
719
+ function IconSwap({ show, a, b }) {
720
+ const layer = "col-start-1 row-start-1 grid place-items-center transition-[opacity,scale,filter] duration-200 ease-media motion-reduce:transition-none";
721
+ return /* @__PURE__ */ jsxs2("span", { className: "grid", children: [
722
+ /* @__PURE__ */ jsx2("span", { className: cn(layer, show === "a" ? "opacity-100" : "scale-50 opacity-0 blur-[2px]"), children: a }),
723
+ /* @__PURE__ */ jsx2("span", { className: cn(layer, show === "b" ? "opacity-100" : "scale-50 opacity-0 blur-[2px]"), children: b })
724
+ ] });
725
+ }
726
+ function MediaPlayButton({ onClick, ...props }) {
727
+ const store = useMediaStore();
728
+ const paused = useMedia((s) => s.paused);
729
+ const hasSource = useMedia((s) => s.hasSource);
730
+ return /* @__PURE__ */ jsx2(
731
+ MediaButton,
732
+ {
733
+ "aria-label": paused ? "Play" : "Pause",
734
+ tooltip: paused ? "Play" : "Pause",
735
+ shortcut: "K",
736
+ disabled: !hasSource,
737
+ onClick: (event) => {
738
+ onClick?.(event);
739
+ store.toggle();
740
+ },
741
+ ...props,
742
+ children: /* @__PURE__ */ jsx2(IconSwap, { show: paused ? "a" : "b", a: /* @__PURE__ */ jsx2(PlayIcon, {}), b: /* @__PURE__ */ jsx2(PauseIcon, {}) })
743
+ }
744
+ );
745
+ }
746
+ function MediaMuteButton({ onClick, ...props }) {
747
+ const store = useMediaStore();
748
+ const muted2 = useMedia((s) => s.muted || s.volume === 0);
749
+ return /* @__PURE__ */ jsx2(
750
+ MediaButton,
751
+ {
752
+ "aria-label": muted2 ? "Unmute" : "Mute",
753
+ tooltip: muted2 ? "Unmute" : "Mute",
754
+ shortcut: "M",
755
+ onClick: (event) => {
756
+ onClick?.(event);
757
+ store.toggleMute();
758
+ },
759
+ ...props,
760
+ children: /* @__PURE__ */ jsx2(VolumeIcon, { muted: muted2 })
761
+ }
762
+ );
763
+ }
764
+ function MediaFullscreenButton({ onClick, ...props }) {
765
+ const store = useMediaStore();
766
+ const fullscreen = useMedia((s) => s.fullscreen);
767
+ return /* @__PURE__ */ jsx2(
768
+ MediaButton,
769
+ {
770
+ "aria-label": fullscreen ? "Exit fullscreen" : "Enter fullscreen",
771
+ tooltip: fullscreen ? "Exit fullscreen" : "Fullscreen",
772
+ shortcut: "F",
773
+ onClick: (event) => {
774
+ onClick?.(event);
775
+ store.toggleFullscreen();
776
+ },
777
+ ...props,
778
+ children: /* @__PURE__ */ jsx2(IconSwap, { show: fullscreen ? "b" : "a", a: /* @__PURE__ */ jsx2(EnterFullscreenIcon, {}), b: /* @__PURE__ */ jsx2(ExitFullscreenIcon, {}) })
779
+ }
780
+ );
781
+ }
782
+ var subscribeNoop = () => () => {
783
+ };
784
+ function MediaPictureInPictureButton({ onClick, ...props }) {
785
+ const store = useMediaStore();
786
+ const active = useMedia((s) => s.pictureInPicture);
787
+ const supported = React2.useSyncExternalStore(
788
+ subscribeNoop,
789
+ () => "pictureInPictureEnabled" in document && document.pictureInPictureEnabled,
790
+ () => false
791
+ );
792
+ if (!supported) return null;
793
+ return /* @__PURE__ */ jsx2(
794
+ MediaButton,
795
+ {
796
+ "aria-label": active ? "Exit picture in picture" : "Enter picture in picture",
797
+ "aria-pressed": active,
798
+ tooltip: active ? "Exit picture in picture" : "Picture in picture",
799
+ shortcut: "I",
800
+ onClick: (event) => {
801
+ onClick?.(event);
802
+ store.togglePictureInPicture();
803
+ },
804
+ ...props,
805
+ children: /* @__PURE__ */ jsx2(PictureInPictureIcon, {})
806
+ }
807
+ );
808
+ }
809
+ function MediaPlaybackRateButton({
810
+ rates = [1, 1.25, 1.5, 2, 0.75],
811
+ className,
812
+ style,
813
+ onClick,
814
+ ...props
815
+ }) {
816
+ const store = useMediaStore();
817
+ const rate = useMedia((s) => s.playbackRate);
818
+ const chars = Math.max(...[...rates, rate].map((r) => `${r}\xD7`.length));
819
+ return /* @__PURE__ */ jsx2(
820
+ MediaButton,
821
+ {
822
+ "aria-label": `Playback speed ${rate}x`,
823
+ tooltip: "Playback speed",
824
+ dir: "ltr",
825
+ className: cn("w-(--media-rate-width) min-w-7 px-1.5 font-mono text-xs tabular-nums", className),
826
+ style: { "--media-rate-width": `calc(${chars}ch + 0.75rem)`, ...style },
827
+ onClick: (event) => {
828
+ onClick?.(event);
829
+ const index = rates.indexOf(rate);
830
+ store.setPlaybackRate(rates[(index + 1) % rates.length]);
831
+ },
832
+ ...props,
833
+ children: `${rate}\xD7`
834
+ }
835
+ );
836
+ }
837
+ function MediaControls({ className, ...props }) {
838
+ return /* @__PURE__ */ jsx2(
839
+ "div",
840
+ {
841
+ "data-slot": "media-controls",
842
+ className: cn(
843
+ "flex items-center transition-[opacity,translate] duration-200 ease-media motion-reduce:transition-none",
844
+ "pointer-events-none translate-y-2 opacity-0",
845
+ "group-data-active/media:pointer-events-auto group-data-active/media:translate-y-0 group-data-active/media:opacity-100",
846
+ "group-has-focus-visible/media:pointer-events-auto group-has-focus-visible/media:translate-y-0 group-has-focus-visible/media:opacity-100",
847
+ "group-data-paused/media:group-data-started/media:pointer-events-auto group-data-paused/media:group-data-started/media:translate-y-0 group-data-paused/media:group-data-started/media:opacity-100",
848
+ className
849
+ ),
850
+ ...props
851
+ }
852
+ );
853
+ }
854
+ var DOUBLE_TAP_MS = 280;
855
+ function MediaSurface({
856
+ className,
857
+ doubleTapSeek = 10,
858
+ doubleClickFullscreen = true,
859
+ indicatorClassName,
860
+ onClick,
861
+ onDoubleClick,
862
+ onPointerDown,
863
+ children,
864
+ ...props
865
+ }) {
866
+ const store = useMediaStore();
867
+ const paused = useMedia((s) => s.paused);
868
+ const pointer = React2.useRef({ type: "mouse", revealOnly: false, lastTap: 0 });
869
+ const tapTimer = React2.useRef(void 0);
870
+ const flashTimer = React2.useRef(void 0);
871
+ const [flash, setFlash] = React2.useState(null);
872
+ React2.useEffect(
873
+ () => () => {
874
+ clearTimeout(tapTimer.current);
875
+ clearTimeout(flashTimer.current);
876
+ },
877
+ []
878
+ );
879
+ const showFlash = (side) => {
880
+ setFlash((prev) => ({ side, count: prev?.side === side ? prev.count + 1 : 1 }));
881
+ clearTimeout(flashTimer.current);
882
+ flashTimer.current = setTimeout(() => setFlash(null), 650);
883
+ };
884
+ const handleTouchTap = (event) => {
885
+ const now = performance.now();
886
+ const { revealOnly, lastTap } = pointer.current;
887
+ if (doubleTapSeek > 0 && now - lastTap < DOUBLE_TAP_MS) {
888
+ clearTimeout(tapTimer.current);
889
+ pointer.current.lastTap = now;
890
+ const rect = event.currentTarget.getBoundingClientRect();
891
+ const side = event.clientX - rect.left < rect.width / 2 ? "back" : "forward";
892
+ store.seekBy(side === "back" ? -doubleTapSeek : doubleTapSeek);
893
+ showFlash(side);
894
+ return;
895
+ }
896
+ pointer.current.lastTap = now;
897
+ if (revealOnly) return;
898
+ clearTimeout(tapTimer.current);
899
+ tapTimer.current = setTimeout(() => store.toggle(), doubleTapSeek > 0 ? DOUBLE_TAP_MS : 0);
900
+ };
901
+ return /* @__PURE__ */ jsxs2(
902
+ "button",
903
+ {
904
+ type: "button",
905
+ "data-slot": "media-surface",
906
+ "aria-label": paused ? "Play" : "Pause",
907
+ className: cn("absolute inset-0 cursor-[inherit] outline-none", className),
908
+ onPointerDown: (event) => {
909
+ onPointerDown?.(event);
910
+ const root = event.currentTarget.closest("[data-slot='media-root']");
911
+ pointer.current.type = event.pointerType;
912
+ pointer.current.revealOnly = event.pointerType !== "mouse" && !store.getState().paused && !root?.hasAttribute("data-active");
913
+ },
914
+ onClick: (event) => {
915
+ onClick?.(event);
916
+ if (event.detail !== 0 && pointer.current.type !== "mouse") {
917
+ handleTouchTap(event);
918
+ return;
919
+ }
920
+ store.toggle();
921
+ },
922
+ onDoubleClick: (event) => {
923
+ onDoubleClick?.(event);
924
+ if (doubleClickFullscreen && pointer.current.type === "mouse") store.toggleFullscreen();
925
+ },
926
+ ...props,
927
+ children: [
928
+ doubleTapSeek > 0 && /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 flex items-center justify-between px-[8%]", children: ["back", "forward"].map((side) => /* @__PURE__ */ jsxs2(
929
+ "span",
930
+ {
931
+ "data-show": flash?.side === side || void 0,
932
+ className: cn(
933
+ "flex items-center gap-1.5 rounded-full px-3 py-1.5 font-mono text-xs tabular-nums opacity-0 scale-90 transition-[opacity,scale] duration-200 ease-media data-show:scale-100 data-show:opacity-100 motion-reduce:transition-none",
934
+ side === "forward" && "flex-row-reverse",
935
+ indicatorClassName
936
+ ),
937
+ children: [
938
+ /* @__PURE__ */ jsx2(SeekIcon, { direction: side, className: "size-3.5" }),
939
+ `${(flash?.side === side ? flash.count : 1) * doubleTapSeek}s`
940
+ ]
941
+ },
942
+ side
943
+ )) }),
944
+ children
945
+ ]
946
+ }
947
+ );
948
+ }
949
+ function MediaSeek({
950
+ className,
951
+ step = 5,
952
+ preview = true,
953
+ previewClassName,
954
+ ...props
955
+ }) {
956
+ const store = useMediaStore();
957
+ const duration = useMedia((s) => s.duration);
958
+ const bufferedEnd = useMedia((s) => s.bufferedEnd);
959
+ const currentTime = useMedia((s) => Math.floor(s.currentTime));
960
+ const fillRef = React2.useRef(null);
961
+ const scrub = React2.useRef(null);
962
+ const [hover, setHover] = React2.useState(null);
963
+ React2.useEffect(() => {
964
+ let frame = 0;
965
+ const draw = () => {
966
+ cancelAnimationFrame(frame);
967
+ const media = store.getMedia();
968
+ const { duration: duration2, paused } = store.getState();
969
+ if (!scrub.current && fillRef.current) {
970
+ const progress = duration2 ? clamp((media?.currentTime ?? 0) / duration2) : 0;
971
+ fillRef.current.style.transform = `scaleX(${progress})`;
972
+ }
973
+ if (!paused) frame = requestAnimationFrame(draw);
974
+ };
975
+ draw();
976
+ const unsubscribe = store.subscribe(draw);
977
+ return () => {
978
+ unsubscribe();
979
+ cancelAnimationFrame(frame);
980
+ };
981
+ }, [store]);
982
+ const ratioFromEvent = (event) => {
983
+ const rect = event.currentTarget.getBoundingClientRect();
984
+ return clamp((event.clientX - rect.left) / rect.width);
985
+ };
986
+ const seekToRatio = (ratio) => {
987
+ if (fillRef.current) fillRef.current.style.transform = `scaleX(${ratio})`;
988
+ store.seek(ratio * store.getState().duration);
989
+ };
990
+ const endScrub = (event) => {
991
+ if (!scrub.current) return;
992
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
993
+ event.currentTarget.releasePointerCapture(event.pointerId);
994
+ }
995
+ const { resume } = scrub.current;
996
+ scrub.current = null;
997
+ if (resume) store.play();
998
+ };
999
+ return /* @__PURE__ */ jsxs2(
1000
+ "div",
1001
+ {
1002
+ role: "slider",
1003
+ tabIndex: 0,
1004
+ "aria-label": "Seek",
1005
+ "aria-valuemin": 0,
1006
+ "aria-valuemax": Math.floor(duration),
1007
+ "aria-valuenow": currentTime,
1008
+ "aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
1009
+ "data-slot": "media-seek",
1010
+ className: cn(
1011
+ /* on touch, an invisible 44px-tall hit area around the 20px bar */
1012
+ "group/seek relative flex h-5 min-w-0 grow cursor-pointer touch-none items-center rounded-full outline-none select-none pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:-inset-y-3 pointer-coarse:before:content-['']",
1013
+ className
1014
+ ),
1015
+ onPointerDown: (event) => {
1016
+ if (event.button !== 0) return;
1017
+ event.currentTarget.setPointerCapture(event.pointerId);
1018
+ scrub.current = { resume: !store.getState().paused };
1019
+ store.pause();
1020
+ seekToRatio(ratioFromEvent(event));
1021
+ },
1022
+ onPointerMove: (event) => {
1023
+ const ratio = ratioFromEvent(event);
1024
+ if (event.pointerType === "mouse") setHover(ratio);
1025
+ if (scrub.current) seekToRatio(ratio);
1026
+ },
1027
+ onPointerUp: endScrub,
1028
+ onPointerCancel: endScrub,
1029
+ onPointerLeave: () => setHover(null),
1030
+ onKeyDown: (event) => {
1031
+ const { currentTime: currentTime2, duration: duration2 } = store.getState();
1032
+ const moves = {
1033
+ ArrowLeft: currentTime2 - step,
1034
+ ArrowDown: currentTime2 - step,
1035
+ ArrowRight: currentTime2 + step,
1036
+ ArrowUp: currentTime2 + step,
1037
+ PageDown: currentTime2 - duration2 / 10,
1038
+ PageUp: currentTime2 + duration2 / 10,
1039
+ Home: 0,
1040
+ End: duration2
1041
+ };
1042
+ if (!(event.key in moves)) return;
1043
+ event.preventDefault();
1044
+ event.stopPropagation();
1045
+ store.seek(moves[event.key]);
1046
+ },
1047
+ ...props,
1048
+ children: [
1049
+ /* @__PURE__ */ jsxs2("div", { className: "relative h-1 w-full overflow-hidden rounded-full bg-current/25 transition-[height] duration-150 ease-media group-hover/seek:h-1.5 group-focus-visible/seek:h-1.5 group-active/seek:h-1.5 motion-reduce:transition-none", children: [
1050
+ /* @__PURE__ */ jsx2(
1051
+ "div",
1052
+ {
1053
+ className: "absolute inset-0 origin-left bg-current/25 transition-transform duration-300 ease-media",
1054
+ style: { transform: `scaleX(${duration ? clamp(bufferedEnd / duration) : 0})` }
1055
+ }
1056
+ ),
1057
+ /* @__PURE__ */ jsx2("div", { ref: fillRef, className: "absolute inset-0 origin-left bg-current", style: { transform: "scaleX(0)" } })
1058
+ ] }),
1059
+ /* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute inset-0 rounded-full ring-current/60 group-focus-visible/seek:ring-2" }),
1060
+ preview && hover !== null && duration > 0 && /* @__PURE__ */ jsx2(
1061
+ "span",
1062
+ {
1063
+ "aria-hidden": true,
1064
+ className: cn(
1065
+ "pointer-events-none absolute bottom-full mb-1.5 -translate-x-1/2 rounded-md bg-foreground px-1.5 py-0.5 font-mono text-[11px] leading-4 text-background tabular-nums",
1066
+ previewClassName
1067
+ ),
1068
+ style: { left: `${hover * 100}%` },
1069
+ children: formatTime(hover * duration, duration)
1070
+ }
1071
+ )
1072
+ ]
1073
+ }
1074
+ );
1075
+ }
1076
+ function MediaVolume({ orientation = "horizontal", className, ...props }) {
1077
+ const store = useMediaStore();
1078
+ const volume = useMedia((s) => s.muted ? 0 : s.volume);
1079
+ const dragging = React2.useRef(false);
1080
+ const vertical = orientation === "vertical";
1081
+ const setFromEvent = (event) => {
1082
+ const rect = event.currentTarget.getBoundingClientRect();
1083
+ store.setVolume(
1084
+ clamp(vertical ? (rect.bottom - event.clientY) / rect.height : (event.clientX - rect.left) / rect.width)
1085
+ );
1086
+ };
1087
+ return /* @__PURE__ */ jsxs2(
1088
+ "div",
1089
+ {
1090
+ role: "slider",
1091
+ tabIndex: 0,
1092
+ "aria-label": "Volume",
1093
+ "aria-valuemin": 0,
1094
+ "aria-valuemax": 100,
1095
+ "aria-valuenow": Math.round(volume * 100),
1096
+ "aria-valuetext": `${Math.round(volume * 100)}%`,
1097
+ "aria-orientation": orientation,
1098
+ "data-slot": "media-volume",
1099
+ className: cn(
1100
+ "group/volume relative flex shrink-0 cursor-pointer touch-none outline-none select-none",
1101
+ vertical ? "h-16 w-5 justify-center" : "h-5 w-16 items-center",
1102
+ className
1103
+ ),
1104
+ onPointerDown: (event) => {
1105
+ if (event.button !== 0) return;
1106
+ event.currentTarget.setPointerCapture(event.pointerId);
1107
+ dragging.current = true;
1108
+ setFromEvent(event);
1109
+ },
1110
+ onPointerMove: (event) => dragging.current && setFromEvent(event),
1111
+ onPointerUp: (event) => {
1112
+ dragging.current = false;
1113
+ event.currentTarget.releasePointerCapture(event.pointerId);
1114
+ },
1115
+ onPointerCancel: () => {
1116
+ dragging.current = false;
1117
+ },
1118
+ onKeyDown: (event) => {
1119
+ const moves = {
1120
+ ArrowLeft: volume - 0.1,
1121
+ ArrowDown: volume - 0.1,
1122
+ ArrowRight: volume + 0.1,
1123
+ ArrowUp: volume + 0.1,
1124
+ PageDown: volume - 0.25,
1125
+ PageUp: volume + 0.25,
1126
+ Home: 0,
1127
+ End: 1
1128
+ };
1129
+ if (!(event.key in moves)) return;
1130
+ event.preventDefault();
1131
+ event.stopPropagation();
1132
+ store.setVolume(moves[event.key]);
1133
+ },
1134
+ ...props,
1135
+ children: [
1136
+ /* @__PURE__ */ jsx2(
1137
+ "div",
1138
+ {
1139
+ className: cn(
1140
+ "relative overflow-hidden rounded-full bg-current/25 duration-150 ease-media motion-reduce:transition-none",
1141
+ vertical ? "h-full w-1 transition-[width] group-hover/volume:w-1.5 group-focus-visible/volume:w-1.5" : "h-1 w-full transition-[height] group-hover/volume:h-1.5 group-focus-visible/volume:h-1.5"
1142
+ ),
1143
+ children: /* @__PURE__ */ jsx2(
1144
+ "div",
1145
+ {
1146
+ className: cn("absolute inset-0 bg-current", vertical ? "origin-bottom" : "origin-left"),
1147
+ style: { transform: vertical ? `scaleY(${volume})` : `scaleX(${volume})` }
1148
+ }
1149
+ )
1150
+ }
1151
+ ),
1152
+ /* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute inset-0 rounded-full ring-current/60 group-focus-visible/volume:ring-2" })
1153
+ ]
1154
+ }
1155
+ );
1156
+ }
1157
+ var timeNames = { current: "elapsed", duration: "duration", remaining: "remaining" };
1158
+ function MediaTime({ type = "current", toggle = false, className, ...props }) {
1159
+ const [picked, setPicked] = React2.useState(type);
1160
+ const shown = toggle ? picked : type;
1161
+ const duration = useMedia((s) => s.duration);
1162
+ const value = useMedia((s) => {
1163
+ if (shown === "duration") return Math.floor(s.duration) || Math.ceil(s.duration);
1164
+ if (shown === "remaining") return Math.ceil(Math.max(s.duration - s.currentTime, 0));
1165
+ return Math.floor(s.currentTime);
1166
+ });
1167
+ const time = formatTime(value, duration);
1168
+ const text = shown === "remaining" ? `-${time}` : time;
1169
+ const other = type === "remaining" ? "current" : "remaining";
1170
+ const next = shown === type ? other : type;
1171
+ return /* @__PURE__ */ jsx2("span", { "data-slot": "media-time", dir: "ltr", className: cn("font-mono text-xs tabular-nums", className), ...props, children: toggle ? /* @__PURE__ */ jsxs2(
1172
+ "button",
1173
+ {
1174
+ type: "button",
1175
+ "aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${time}`,
1176
+ onClick: () => setPicked(next),
1177
+ className: "group/media-button relative cursor-pointer rounded-sm outline-none transition-colors duration-150 ease-media hover:text-current focus-visible:ring-2 focus-visible:ring-current/60",
1178
+ children: [
1179
+ text,
1180
+ /* @__PURE__ */ jsx2(MediaTooltip, { label: `Show ${timeNames[next]}` })
1181
+ ]
1182
+ }
1183
+ ) : text });
1184
+ }
1185
+ function MediaLoading({ className, ...props }) {
1186
+ const waiting = useMedia((s) => s.waiting && !s.paused);
1187
+ return /* @__PURE__ */ jsx2(
1188
+ "span",
1189
+ {
1190
+ role: "status",
1191
+ "aria-label": waiting ? "Loading" : void 0,
1192
+ "data-slot": "media-loading",
1193
+ className: cn(
1194
+ "pointer-events-none size-6 rounded-full border-2 border-current/25 border-t-current transition-opacity duration-200 ease-media",
1195
+ waiting ? "animate-spin opacity-100 delay-300" : "opacity-0",
1196
+ className
1197
+ ),
1198
+ ...props
1199
+ }
1200
+ );
1201
+ }
1202
+ function MediaCaptions({ className, ...props }) {
1203
+ const cues = useMedia((s) => s.textTrack === -1 ? emptyCues : s.cues);
1204
+ if (!cues.length) return null;
1205
+ return /* @__PURE__ */ jsx2(
1206
+ "div",
1207
+ {
1208
+ "data-slot": "media-captions",
1209
+ className: cn("pointer-events-none flex flex-col items-center gap-1 text-center", className),
1210
+ ...props,
1211
+ children: cues.map((cue, i) => /* @__PURE__ */ jsx2("span", { dir: "auto", className: "whitespace-pre-line text-balance rounded-md px-2 py-0.5 box-decoration-clone", children: cue }, `${i}-${cue}`))
1212
+ }
1213
+ );
1214
+ }
1215
+ var emptyCues = [];
1216
+ function MediaCaptionsButton({ onClick, ...props }) {
1217
+ const store = useMediaStore();
1218
+ const available = useMedia((s) => s.textTracks.length > 0);
1219
+ const on = useMedia((s) => s.textTrack !== -1);
1220
+ if (!available) return null;
1221
+ return /* @__PURE__ */ jsx2(
1222
+ MediaButton,
1223
+ {
1224
+ "aria-label": "Captions",
1225
+ "aria-pressed": on,
1226
+ tooltip: on ? "Captions off" : "Captions",
1227
+ shortcut: "C",
1228
+ onClick: (event) => {
1229
+ onClick?.(event);
1230
+ store.toggleCaptions();
1231
+ },
1232
+ ...props,
1233
+ children: /* @__PURE__ */ jsx2(CaptionsIcon, { active: on })
1234
+ }
1235
+ );
1236
+ }
1237
+ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelClassName, ...props }) {
1238
+ const store = useMediaStore();
1239
+ const rate = useMedia((s) => s.playbackRate);
1240
+ const tracks = useMedia((s) => s.textTracks);
1241
+ const textTrack = useMedia((s) => s.textTrack);
1242
+ const [open, setOpen] = React2.useState(false);
1243
+ const rootEl = React2.useRef(null);
1244
+ const triggerEl = React2.useRef(null);
1245
+ const panelEl = React2.useRef(null);
1246
+ const listEl = React2.useRef(null);
1247
+ const panelId = React2.useId();
1248
+ React2.useLayoutEffect(() => {
1249
+ const panel = panelEl.current;
1250
+ const list = listEl.current;
1251
+ const trigger = triggerEl.current;
1252
+ const player = rootEl.current?.closest("[data-slot='media-root']");
1253
+ if (!open || !panel || !list || !trigger || !player) return;
1254
+ const edge = () => {
1255
+ const left = Math.min(24, list.scrollHeight - list.clientHeight - list.scrollTop);
1256
+ list.style.maskImage = left > 1 ? `linear-gradient(to bottom, black calc(100% - ${left}px), rgb(0 0 0 / 0.3))` : "";
1257
+ };
1258
+ const fit = () => {
1259
+ const space = trigger.getBoundingClientRect().top - player.getBoundingClientRect().top - 16;
1260
+ panel.style.maxHeight = `${Math.max(96, Math.min(288, space))}px`;
1261
+ edge();
1262
+ };
1263
+ fit();
1264
+ const observer = new ResizeObserver(fit);
1265
+ observer.observe(player);
1266
+ list.addEventListener("scroll", edge, { passive: true });
1267
+ return () => {
1268
+ observer.disconnect();
1269
+ list.removeEventListener("scroll", edge);
1270
+ };
1271
+ }, [open]);
1272
+ React2.useEffect(() => {
1273
+ if (!open) return;
1274
+ const onPointerDown = (event) => {
1275
+ if (!rootEl.current?.contains(event.target)) setOpen(false);
1276
+ };
1277
+ document.addEventListener("pointerdown", onPointerDown);
1278
+ rootEl.current?.querySelector("[aria-checked='true']")?.focus();
1279
+ return () => document.removeEventListener("pointerdown", onPointerDown);
1280
+ }, [open]);
1281
+ const close = () => {
1282
+ setOpen(false);
1283
+ triggerEl.current?.focus();
1284
+ };
1285
+ const onPanelKeyDown = (event) => {
1286
+ if (event.key === "Escape") {
1287
+ event.stopPropagation();
1288
+ close();
1289
+ return;
1290
+ }
1291
+ const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
1292
+ if (!step) return;
1293
+ event.preventDefault();
1294
+ event.stopPropagation();
1295
+ const items = Array.from(rootEl.current?.querySelectorAll("[role='menuitemradio']") ?? []);
1296
+ const index = items.indexOf(document.activeElement);
1297
+ const next = items[(index + step + items.length) % items.length];
1298
+ next?.focus();
1299
+ };
1300
+ const option = (label, checked, select) => /* @__PURE__ */ jsxs2(
1301
+ "button",
1302
+ {
1303
+ type: "button",
1304
+ role: "menuitemradio",
1305
+ "aria-checked": checked,
1306
+ tabIndex: checked ? 0 : -1,
1307
+ onClick: () => {
1308
+ select();
1309
+ close();
1310
+ },
1311
+ className: "flex min-h-8 w-full shrink-0 cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm leading-5 outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:bg-current/15",
1312
+ children: [
1313
+ /* @__PURE__ */ jsx2("span", { dir: "auto", className: "min-w-0", children: label }),
1314
+ /* @__PURE__ */ jsx2(CheckIcon, { className: cn("size-3.5 shrink-0 transition-opacity duration-150", checked ? "opacity-100" : "opacity-0") })
1315
+ ]
1316
+ },
1317
+ label
1318
+ );
1319
+ return /* @__PURE__ */ jsxs2(
1320
+ "div",
1321
+ {
1322
+ ref: rootEl,
1323
+ "data-slot": "media-settings",
1324
+ "data-open": open || void 0,
1325
+ className: cn("relative", className),
1326
+ ...props,
1327
+ children: [
1328
+ /* @__PURE__ */ jsx2(
1329
+ MediaButton,
1330
+ {
1331
+ ref: triggerEl,
1332
+ "aria-label": "Settings",
1333
+ tooltip: "Settings",
1334
+ "aria-haspopup": "menu",
1335
+ "aria-expanded": open,
1336
+ "aria-controls": open ? panelId : void 0,
1337
+ onClick: () => setOpen((o) => !o),
1338
+ children: /* @__PURE__ */ jsx2(SettingsIcon, { className: cn("transition-transform duration-300 ease-media motion-reduce:transition-none", open && "rotate-90") })
1339
+ }
1340
+ ),
1341
+ open && /* @__PURE__ */ jsx2(
1342
+ "div",
1343
+ {
1344
+ ref: panelEl,
1345
+ id: panelId,
1346
+ role: "menu",
1347
+ "aria-label": "Playback settings",
1348
+ onKeyDown: onPanelKeyDown,
1349
+ className: cn(
1350
+ "absolute right-0 bottom-full z-10 mb-2 flex max-h-72 w-48 origin-bottom-right flex-col overflow-hidden rounded-xl",
1351
+ "transition-[opacity,translate,scale] duration-150 ease-media starting:translate-y-1 starting:scale-95 starting:opacity-0 motion-reduce:transition-none",
1352
+ panelClassName
1353
+ ),
1354
+ children: /* @__PURE__ */ jsxs2(
1355
+ "div",
1356
+ {
1357
+ ref: listEl,
1358
+ className: "flex min-h-0 flex-col gap-2 overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1359
+ children: [
1360
+ /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Speed", className: "flex flex-col", children: [
1361
+ /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Speed" }),
1362
+ /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-3 gap-1", children: rates.map((r) => /* @__PURE__ */ jsx2(
1363
+ "button",
1364
+ {
1365
+ type: "button",
1366
+ role: "menuitemradio",
1367
+ "aria-checked": rate === r,
1368
+ "aria-label": r === 1 ? "Normal speed" : `${r}\xD7 speed`,
1369
+ tabIndex: rate === r ? 0 : -1,
1370
+ onClick: () => {
1371
+ store.setPlaybackRate(r);
1372
+ close();
1373
+ },
1374
+ className: cn(
1375
+ "h-8 cursor-pointer rounded-md text-sm tabular-nums outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:ring-1 focus-visible:ring-current/40",
1376
+ rate === r && "bg-current/15"
1377
+ ),
1378
+ children: `${r}\xD7`
1379
+ },
1380
+ r
1381
+ )) })
1382
+ ] }),
1383
+ tracks.length > 0 && /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Captions", className: "flex flex-col", children: [
1384
+ /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Captions" }),
1385
+ option("Off", textTrack === -1, () => store.setTextTrack(-1)),
1386
+ tracks.map((track, i) => option(track.label, textTrack === i, () => store.setTextTrack(i)))
1387
+ ] })
1388
+ ]
1389
+ }
1390
+ )
1391
+ }
1392
+ )
1393
+ ]
1394
+ }
1395
+ );
1396
+ }
1397
+ function MediaErrorMessage({ className, message, ...props }) {
1398
+ const store = useMediaStore();
1399
+ const error = useMedia((s) => s.error);
1400
+ if (!error) return null;
1401
+ return /* @__PURE__ */ jsxs2(
1402
+ "div",
1403
+ {
1404
+ role: "alert",
1405
+ "data-slot": "media-error",
1406
+ className: cn("flex flex-col items-center justify-center gap-3 text-center", className),
1407
+ ...props,
1408
+ children: [
1409
+ /* @__PURE__ */ jsx2("p", { className: "text-pretty text-sm", children: message ? message(error) : describeMediaError(error) }),
1410
+ /* @__PURE__ */ jsxs2(
1411
+ "button",
1412
+ {
1413
+ type: "button",
1414
+ onClick: () => store.retry(),
1415
+ className: "inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm whitespace-nowrap outline-none ring-1 ring-current/25 transition-[background-color,scale] duration-150 ease-media hover:bg-current/10 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:active:scale-[0.96]",
1416
+ children: [
1417
+ /* @__PURE__ */ jsx2(RetryIcon, { className: "size-3.5" }),
1418
+ "Try again"
1419
+ ]
1420
+ }
1421
+ )
1422
+ ]
1423
+ }
1424
+ );
1425
+ }
1426
+
1427
+ // ../../components/ui/video-player.tsx
1428
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1429
+ var reveal = "transition-[opacity,scale] duration-200 ease-media motion-reduce:transition-none";
1430
+ var visibleWithControls = "group-data-active/media:opacity-100 group-has-focus-visible/media:opacity-100 group-data-paused/media:group-data-started/media:opacity-100";
1431
+ var glass = "bg-media-scrim/70 text-media-foreground ring-1 ring-media-foreground/10 backdrop-blur-xl";
1432
+ var volumeOpen = "group-hover/volume-menu:pointer-events-auto group-hover/volume-menu:translate-y-0 group-hover/volume-menu:opacity-100 group-has-focus-visible/volume-menu:pointer-events-auto group-has-focus-visible/volume-menu:translate-y-0 group-has-focus-visible/volume-menu:opacity-100";
1433
+ function VideoPlayer({
1434
+ className,
1435
+ frameClassName,
1436
+ tracks,
1437
+ loader,
1438
+ metadata,
1439
+ persistKey,
1440
+ tooltips = true,
1441
+ children,
1442
+ crossOrigin,
1443
+ ...props
1444
+ }) {
1445
+ return /* @__PURE__ */ jsxs3(
1446
+ MediaRoot,
1447
+ {
1448
+ persistKey,
1449
+ metadata,
1450
+ tooltips,
1451
+ className: cn(
1452
+ "@container/player aspect-video w-full overflow-hidden rounded-xl bg-media-scrim text-media-foreground data-fullscreen:rounded-none",
1453
+ frameClassName
1454
+ ),
1455
+ children: [
1456
+ /* @__PURE__ */ jsxs3(
1457
+ MediaVideo,
1458
+ {
1459
+ className: cn("size-full object-contain", className),
1460
+ loader,
1461
+ crossOrigin: crossOrigin ?? (tracks?.length ? "anonymous" : void 0),
1462
+ ...props,
1463
+ children: [
1464
+ tracks?.map((track) => /* @__PURE__ */ jsx3(
1465
+ "track",
1466
+ {
1467
+ src: track.src,
1468
+ label: track.label,
1469
+ srcLang: track.srcLang,
1470
+ kind: track.kind ?? "subtitles",
1471
+ default: track.default
1472
+ },
1473
+ track.src
1474
+ )),
1475
+ children
1476
+ ]
1477
+ }
1478
+ ),
1479
+ /* @__PURE__ */ jsx3(MediaSurface, { indicatorClassName: glass }),
1480
+ /* @__PURE__ */ jsx3(
1481
+ "span",
1482
+ {
1483
+ "aria-hidden": true,
1484
+ className: cn(
1485
+ "pointer-events-none absolute top-1/2 left-1/2 grid size-12 -translate-1/2 place-items-center rounded-full bg-media-scrim/40 backdrop-blur-md",
1486
+ reveal,
1487
+ "group-data-started/media:scale-75 group-data-started/media:opacity-0"
1488
+ ),
1489
+ children: /* @__PURE__ */ jsx3(PlayIcon, { className: "size-5" })
1490
+ }
1491
+ ),
1492
+ /* @__PURE__ */ jsx3("span", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: /* @__PURE__ */ jsx3(MediaLoading, {}) }),
1493
+ /* @__PURE__ */ jsx3(
1494
+ MediaCaptions,
1495
+ {
1496
+ className: cn(
1497
+ /* sized by the player, not the viewport, so a small player keeps its cues */
1498
+ "absolute inset-x-4 bottom-4 text-xs leading-snug transition-transform duration-200 ease-media motion-reduce:transition-none @xs/player:text-sm @2xl/player:text-base",
1499
+ "group-data-active/media:-translate-y-10 group-has-focus-visible/media:-translate-y-10 group-data-paused/media:group-data-started/media:-translate-y-10",
1500
+ "[&>span]:bg-media-scrim/75 [&>span]:text-media-foreground"
1501
+ )
1502
+ }
1503
+ ),
1504
+ /* @__PURE__ */ jsx3(
1505
+ "div",
1506
+ {
1507
+ "aria-hidden": true,
1508
+ className: cn(
1509
+ "pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-linear-to-t from-media-scrim/60 to-transparent opacity-0",
1510
+ reveal,
1511
+ visibleWithControls
1512
+ )
1513
+ }
1514
+ ),
1515
+ /* @__PURE__ */ jsxs3(
1516
+ MediaControls,
1517
+ {
1518
+ dir: "ltr",
1519
+ className: "absolute inset-x-2 bottom-2 h-9 gap-1 px-1 **:data-[slot=media-tooltip]:bg-media-foreground **:data-[slot=media-tooltip]:text-media-scrim",
1520
+ children: [
1521
+ /* @__PURE__ */ jsx3(MediaPlayButton, { tooltipAlign: "start" }),
1522
+ /* @__PURE__ */ jsx3(MediaSeek, { className: "mx-2", previewClassName: "bg-media-foreground text-media-scrim" }),
1523
+ /* @__PURE__ */ jsx3(MediaTime, { type: "remaining", toggle: true, className: "hidden min-w-10 text-right text-media-foreground/80 @xs/player:inline" }),
1524
+ /* @__PURE__ */ jsxs3("div", { className: "group/volume-menu relative", children: [
1525
+ /* @__PURE__ */ jsx3(MediaMuteButton, { tooltip: false }),
1526
+ /* @__PURE__ */ jsx3(
1527
+ "div",
1528
+ {
1529
+ className: cn(
1530
+ "pointer-events-none absolute bottom-full left-1/2 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
1531
+ "transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
1532
+ volumeOpen
1533
+ ),
1534
+ children: /* @__PURE__ */ jsx3("div", { className: cn("flex h-24 w-7 justify-center rounded-full py-3", glass), children: /* @__PURE__ */ jsx3(MediaVolume, { orientation: "vertical", className: "h-full" }) })
1535
+ }
1536
+ )
1537
+ ] }),
1538
+ /* @__PURE__ */ jsx3(MediaCaptionsButton, {}),
1539
+ /* @__PURE__ */ jsx3(MediaSettings, { panelClassName: glass }),
1540
+ /* @__PURE__ */ jsx3(MediaPictureInPictureButton, { tooltipAlign: "end", className: "hidden @sm/player:inline-grid" }),
1541
+ /* @__PURE__ */ jsx3(MediaFullscreenButton, { tooltipAlign: "end" })
1542
+ ]
1543
+ }
1544
+ ),
1545
+ /* @__PURE__ */ jsx3(MediaErrorMessage, { className: "absolute inset-0 bg-media-scrim/85 px-6 text-media-foreground backdrop-blur-sm" })
1546
+ ]
1547
+ }
1548
+ );
1549
+ }
1550
+
1551
+ // ../../components/ui/audio-player.tsx
1552
+ import * as React3 from "react";
1553
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1554
+ var muted = "text-muted-foreground hover:text-foreground";
1555
+ var volumeOpen2 = "group-hover/volume-menu:pointer-events-auto group-hover/volume-menu:translate-y-0 group-hover/volume-menu:opacity-100 group-has-focus-visible/volume-menu:pointer-events-auto group-has-focus-visible/volume-menu:translate-y-0 group-has-focus-visible/volume-menu:opacity-100";
1556
+ function AudioPlayer({
1557
+ title,
1558
+ description,
1559
+ artwork,
1560
+ showSpeed,
1561
+ tooltips = true,
1562
+ className,
1563
+ frameClassName,
1564
+ loader,
1565
+ metadata,
1566
+ persistKey,
1567
+ ...props
1568
+ }) {
1569
+ const sessionInfo = metadata ?? (typeof title === "string" ? {
1570
+ title,
1571
+ artist: typeof description === "string" ? description : void 0,
1572
+ artwork: artwork ? [{ src: artwork }] : void 0
1573
+ } : void 0);
1574
+ const hasInfo = Boolean(title || description);
1575
+ const [failedArtwork, setFailedArtwork] = React3.useState(null);
1576
+ const showArtwork = artwork && failedArtwork !== artwork;
1577
+ return /* @__PURE__ */ jsxs4(
1578
+ MediaRoot,
1579
+ {
1580
+ persistKey,
1581
+ metadata: sessionInfo,
1582
+ tooltips,
1583
+ className: cn(
1584
+ "@container/audio grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 rounded-xl border bg-card p-2.5 pr-3 text-card-foreground",
1585
+ frameClassName
1586
+ ),
1587
+ children: [
1588
+ /* @__PURE__ */ jsx4(MediaAudio, { className, loader, ...props }),
1589
+ /* @__PURE__ */ jsx4("div", { className: cn("flex items-center", hasInfo && "row-span-2 @max-sm/audio:row-span-1"), children: /* @__PURE__ */ jsxs4("span", { className: "relative shrink-0", children: [
1590
+ showArtwork ? (
1591
+ /**
1592
+ * The tile presses as one piece. Scaling the button alone pulled
1593
+ * its scrim in from the edges and bared a band of the image. No
1594
+ * overflow-hidden, so the play tooltip is not clipped.
1595
+ */
1596
+ /* @__PURE__ */ jsxs4("span", { className: "relative block size-11 rounded-lg bg-muted transition-[scale] duration-150 ease-media motion-safe:has-[button:enabled:active]:scale-[0.96] motion-reduce:transition-none", children: [
1597
+ /* @__PURE__ */ jsx4(
1598
+ "img",
1599
+ {
1600
+ src: artwork,
1601
+ alt: "",
1602
+ draggable: false,
1603
+ onError: () => setFailedArtwork(artwork),
1604
+ ref: (img) => {
1605
+ if (img?.complete && img.naturalWidth === 0) setFailedArtwork(artwork);
1606
+ },
1607
+ className: "size-full rounded-lg object-cover outline select-none outline-black/10 -outline-offset-1 dark:outline-white/10"
1608
+ }
1609
+ ),
1610
+ /* @__PURE__ */ jsx4(
1611
+ MediaPlayButton,
1612
+ {
1613
+ tooltipAlign: "start",
1614
+ className: "absolute inset-0 size-full rounded-lg motion-safe:enabled:active:scale-100 bg-media-scrim/15 text-media-foreground hover:bg-media-scrim/35 focus-visible:bg-media-scrim/35 group-data-playing/media:bg-media-scrim/35 [&_svg]:size-4 [&_svg]:drop-shadow-sm"
1615
+ }
1616
+ )
1617
+ ] })
1618
+ ) : /* @__PURE__ */ jsx4(
1619
+ MediaPlayButton,
1620
+ {
1621
+ tooltipAlign: "start",
1622
+ className: "size-10 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 [&_svg]:size-4"
1623
+ }
1624
+ ),
1625
+ /* @__PURE__ */ jsx4(
1626
+ MediaLoading,
1627
+ {
1628
+ className: showArtwork ? "absolute inset-0 m-auto size-5 text-background" : "absolute -inset-1 size-auto border-current/15 border-t-current text-foreground"
1629
+ }
1630
+ )
1631
+ ] }) }),
1632
+ hasInfo && /* @__PURE__ */ jsxs4("div", { className: "col-start-2 flex min-w-0 items-baseline gap-2 self-end text-sm leading-5 @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-2 @max-sm/audio:pt-1", children: [
1633
+ title && /* @__PURE__ */ jsx4("span", { dir: "auto", title: typeof title === "string" ? title : void 0, className: "truncate font-medium", children: title }),
1634
+ description && /* @__PURE__ */ jsx4(
1635
+ "span",
1636
+ {
1637
+ dir: "auto",
1638
+ title: typeof description === "string" ? description : void 0,
1639
+ className: "min-w-12 shrink-[4] truncate text-muted-foreground",
1640
+ children: description
1641
+ }
1642
+ )
1643
+ ] }),
1644
+ /* @__PURE__ */ jsxs4(
1645
+ "div",
1646
+ {
1647
+ dir: "ltr",
1648
+ className: cn(
1649
+ "col-start-2 flex items-center gap-2.5 text-muted-foreground",
1650
+ hasInfo && "row-start-2 self-start @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-3"
1651
+ ),
1652
+ children: [
1653
+ /* @__PURE__ */ jsx4(MediaTime, { className: "min-w-9" }),
1654
+ /* @__PURE__ */ jsx4(MediaSeek, { className: "text-foreground" }),
1655
+ /* @__PURE__ */ jsx4(MediaTime, { type: "duration", toggle: true, className: "min-w-9 text-right" })
1656
+ ]
1657
+ }
1658
+ ),
1659
+ /* @__PURE__ */ jsxs4("div", { className: cn("col-start-3 flex items-center gap-0.5", hasInfo && "row-span-2 row-start-1 @max-sm/audio:row-span-1"), children: [
1660
+ showSpeed && /* @__PURE__ */ jsx4(MediaPlaybackRateButton, { rates: [1, 1.5, 2, 0.75], className: muted }),
1661
+ /* @__PURE__ */ jsxs4("div", { className: "group/volume-menu relative", children: [
1662
+ /* @__PURE__ */ jsx4(MediaMuteButton, { tooltip: false, className: muted }),
1663
+ /* @__PURE__ */ jsx4(
1664
+ "div",
1665
+ {
1666
+ className: cn(
1667
+ "pointer-events-none absolute bottom-full left-1/2 z-10 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
1668
+ "transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
1669
+ volumeOpen2
1670
+ ),
1671
+ children: /* @__PURE__ */ jsx4("div", { className: "flex h-24 w-7 justify-center rounded-full bg-popover py-3 text-popover-foreground shadow-md ring-1 ring-foreground/10", children: /* @__PURE__ */ jsx4(MediaVolume, { orientation: "vertical", className: "h-full" }) })
1672
+ }
1673
+ )
1674
+ ] })
1675
+ ] }),
1676
+ /* @__PURE__ */ jsx4(MediaErrorMessage, { className: "absolute inset-0 flex-row gap-4 rounded-[inherit] bg-card/95 px-4 text-card-foreground" })
1677
+ ]
1678
+ }
1679
+ );
1680
+ }
1681
+ export {
1682
+ AudioPlayer,
1683
+ CaptionsIcon,
1684
+ CheckIcon,
1685
+ EnterFullscreenIcon,
1686
+ ExitFullscreenIcon,
1687
+ MediaAudio,
1688
+ MediaButton,
1689
+ MediaCaptions,
1690
+ MediaCaptionsButton,
1691
+ MediaControls,
1692
+ MediaErrorMessage,
1693
+ MediaFullscreenButton,
1694
+ MediaLoading,
1695
+ MediaMuteButton,
1696
+ MediaPictureInPictureButton,
1697
+ MediaPlayButton,
1698
+ MediaPlaybackRateButton,
1699
+ MediaRoot,
1700
+ MediaSeek,
1701
+ MediaSettings,
1702
+ MediaSurface,
1703
+ MediaTime,
1704
+ MediaVideo,
1705
+ MediaVolume,
1706
+ PauseIcon,
1707
+ PictureInPictureIcon,
1708
+ PlayIcon,
1709
+ RetryIcon,
1710
+ SeekIcon,
1711
+ SettingsIcon,
1712
+ VideoPlayer,
1713
+ VolumeIcon,
1714
+ createMediaStore,
1715
+ describeMediaError,
1716
+ formatTime,
1717
+ useMedia,
1718
+ useMediaStore
1719
+ };
1720
+ //# sourceMappingURL=index.js.map