@djangocfg/ui-tools 2.1.310 → 2.1.313

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 (161) hide show
  1. package/README.md +38 -22
  2. package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
  3. package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
  4. package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
  5. package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
  6. package/dist/Player-M3GC3VPE.mjs +4 -0
  7. package/dist/Player-M3GC3VPE.mjs.map +1 -0
  8. package/dist/Player-ZGQKKOWI.css +65 -0
  9. package/dist/Player-ZGQKKOWI.css.map +1 -0
  10. package/dist/Player-ZL2X5LGG.cjs +13 -0
  11. package/dist/Player-ZL2X5LGG.cjs.map +1 -0
  12. package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
  13. package/dist/chunk-DFTVB66S.cjs.map +1 -0
  14. package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
  15. package/dist/chunk-EUADAUBQ.mjs.map +1 -0
  16. package/dist/chunk-FX2QFYWF.mjs +2059 -0
  17. package/dist/chunk-FX2QFYWF.mjs.map +1 -0
  18. package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
  19. package/dist/chunk-GBLQTHWT.mjs.map +1 -0
  20. package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
  21. package/dist/chunk-S44PW6NK.cjs.map +1 -0
  22. package/dist/chunk-ZLQHUZDU.cjs +2061 -0
  23. package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
  24. package/dist/components-WYEZL5TE.cjs +26 -0
  25. package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
  26. package/dist/components-ZAGG2PBO.mjs +5 -0
  27. package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
  28. package/dist/index.cjs +36 -220
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.css +65 -0
  31. package/dist/index.css.map +1 -1
  32. package/dist/index.d.cts +44 -500
  33. package/dist/index.d.ts +44 -500
  34. package/dist/index.mjs +16 -62
  35. package/dist/index.mjs.map +1 -1
  36. package/package.json +6 -6
  37. package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
  38. package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
  39. package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
  40. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
  41. package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
  42. package/src/components/markdown/MarkdownMessage/README.md +72 -0
  43. package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
  44. package/src/components/markdown/MarkdownMessage/index.ts +6 -0
  45. package/src/index.ts +2 -11
  46. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
  47. package/src/tools/AudioPlayer/Player.tsx +80 -0
  48. package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
  49. package/src/tools/AudioPlayer/README.md +139 -204
  50. package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
  51. package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
  52. package/src/tools/AudioPlayer/audio/index.ts +4 -0
  53. package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
  54. package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
  55. package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
  56. package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
  57. package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
  58. package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
  59. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
  60. package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
  61. package/src/tools/AudioPlayer/context/index.ts +16 -15
  62. package/src/tools/AudioPlayer/context/selectors.ts +36 -0
  63. package/src/tools/AudioPlayer/hooks/index.ts +12 -39
  64. package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
  65. package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
  66. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
  67. package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
  68. package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
  69. package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
  70. package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
  71. package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
  72. package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
  73. package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
  74. package/src/tools/AudioPlayer/index.ts +63 -134
  75. package/src/tools/AudioPlayer/lazy.tsx +8 -97
  76. package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
  77. package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
  78. package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
  79. package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
  80. package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
  81. package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
  82. package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
  83. package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
  84. package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
  85. package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
  86. package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
  87. package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
  88. package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
  89. package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
  90. package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
  91. package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
  92. package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
  93. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
  94. package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
  95. package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
  96. package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
  97. package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
  98. package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
  99. package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
  100. package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
  101. package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
  102. package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
  103. package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
  104. package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
  105. package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
  106. package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
  107. package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
  108. package/src/tools/AudioPlayer/parts/index.ts +1 -0
  109. package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
  110. package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
  111. package/src/tools/AudioPlayer/store/index.ts +16 -0
  112. package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
  113. package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
  114. package/src/tools/AudioPlayer/types.ts +95 -0
  115. package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
  116. package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
  117. package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
  118. package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
  119. package/src/tools/AudioPlayer/utils/index.ts +4 -5
  120. package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
  121. package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
  122. package/src/tools/index.ts +5 -75
  123. package/dist/chunk-2QY3LJR6.mjs.map +0 -1
  124. package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
  125. package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
  126. package/dist/chunk-F2N7P5XU.cjs +0 -30
  127. package/dist/chunk-F2N7P5XU.cjs.map +0 -1
  128. package/dist/chunk-FYLR232K.cjs.map +0 -1
  129. package/dist/chunk-HMHIVEMS.mjs +0 -1619
  130. package/dist/chunk-HMHIVEMS.mjs.map +0 -1
  131. package/dist/chunk-JWB2EWQO.mjs +0 -5
  132. package/dist/chunk-JWB2EWQO.mjs.map +0 -1
  133. package/dist/chunk-YZX6FH3H.cjs +0 -1656
  134. package/dist/chunk-YZX6FH3H.cjs.map +0 -1
  135. package/dist/components-3RTH76CV.cjs +0 -27
  136. package/dist/components-5GVVL2Q6.mjs +0 -5
  137. package/dist/components-CPHOUQ5F.cjs +0 -46
  138. package/dist/components-CPHOUQ5F.cjs.map +0 -1
  139. package/dist/components-OTK43IMD.mjs +0 -6
  140. package/dist/components-OTK43IMD.mjs.map +0 -1
  141. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
  142. package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
  143. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
  144. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
  145. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
  146. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
  147. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
  148. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
  149. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
  150. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
  151. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
  152. package/src/tools/AudioPlayer/components/index.ts +0 -23
  153. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
  154. package/src/tools/AudioPlayer/effects/index.ts +0 -412
  155. package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
  156. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
  157. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
  158. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
  159. package/src/tools/AudioPlayer/types/effects.ts +0 -73
  160. package/src/tools/AudioPlayer/types/index.ts +0 -27
  161. package/src/tools/AudioPlayer/utils/debug.ts +0 -14
@@ -0,0 +1,2059 @@
1
+ import { __name } from './chunk-CGILA3WO.mjs';
2
+ import { createContext, forwardRef, useSyncExternalStore, useId, useState, useEffect, useRef, useMemo, useContext, useCallback, useImperativeHandle } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@djangocfg/ui-core/components';
5
+ import { useHotkey, useMediaQuery, useIsPhone } from '@djangocfg/ui-core/hooks';
6
+ import { Play, SkipBack, SkipForward, Repeat, VolumeX, Volume2, Music, AlertTriangle, RotateCcw, Pause, LoaderCircle } from 'lucide-react';
7
+ import { useThemeColor, alpha } from '@djangocfg/ui-core/styles/palette';
8
+
9
+ // src/tools/AudioPlayer/audio/audioContext.ts
10
+ var _ctx = null;
11
+ function getAudioContext() {
12
+ if (_ctx) return _ctx;
13
+ const w = window;
14
+ const Ctor = w.AudioContext ?? w.webkitAudioContext;
15
+ if (!Ctor) {
16
+ throw new Error("Web Audio API is not supported in this environment");
17
+ }
18
+ _ctx = new Ctor({ latencyHint: "interactive" });
19
+ return _ctx;
20
+ }
21
+ __name(getAudioContext, "getAudioContext");
22
+ async function unlockAudioContext() {
23
+ const ctx = getAudioContext();
24
+ if (ctx.state === "suspended") {
25
+ try {
26
+ await ctx.resume();
27
+ } catch {
28
+ }
29
+ }
30
+ }
31
+ __name(unlockAudioContext, "unlockAudioContext");
32
+
33
+ // src/tools/AudioPlayer/utils/bucketize.ts
34
+ function bucketize(channel2, buckets) {
35
+ const out = new Float32Array(buckets);
36
+ if (buckets <= 0 || channel2.length === 0) return out;
37
+ const samplesPerBucket = Math.max(1, Math.floor(channel2.length / buckets));
38
+ let peakMax = 0;
39
+ for (let b = 0; b < buckets; b++) {
40
+ const start = b * samplesPerBucket;
41
+ const end = Math.min(start + samplesPerBucket, channel2.length);
42
+ let max = 0;
43
+ for (let i = start; i < end; i++) {
44
+ const v = channel2[i];
45
+ const abs = v < 0 ? -v : v;
46
+ if (abs > max) max = abs;
47
+ }
48
+ out[b] = max;
49
+ if (max > peakMax) peakMax = max;
50
+ }
51
+ if (peakMax > 0 && peakMax < 1) {
52
+ const scale = 1 / peakMax;
53
+ for (let i = 0; i < out.length; i++) out[i] *= scale;
54
+ }
55
+ return out;
56
+ }
57
+ __name(bucketize, "bucketize");
58
+
59
+ // src/tools/AudioPlayer/audio/decodePeaks.ts
60
+ var SAMPLE_RATE = 22050;
61
+ var DEFAULT_BUCKETS = 1800;
62
+ function getOfflineCtor() {
63
+ const w = window;
64
+ const Ctor = w.OfflineAudioContext ?? w.webkitOfflineAudioContext;
65
+ if (!Ctor) throw new Error("OfflineAudioContext is not supported");
66
+ return Ctor;
67
+ }
68
+ __name(getOfflineCtor, "getOfflineCtor");
69
+ async function decodePeaks(src, buckets = DEFAULT_BUCKETS, signal) {
70
+ const response = await fetch(src, { signal, credentials: "same-origin" });
71
+ if (!response.ok) {
72
+ throw new Error(`Failed to fetch audio for peaks (${response.status})`);
73
+ }
74
+ const arr = await response.arrayBuffer();
75
+ const Ctor = getOfflineCtor();
76
+ const ctx = new Ctor(1, 1, SAMPLE_RATE);
77
+ const audio = await ctx.decodeAudioData(arr);
78
+ const channel2 = audio.getChannelData(0);
79
+ return bucketize(channel2, buckets);
80
+ }
81
+ __name(decodePeaks, "decodePeaks");
82
+
83
+ // src/tools/AudioPlayer/audio/peaksCache.ts
84
+ var cache = /* @__PURE__ */ new Map();
85
+ var inflight = /* @__PURE__ */ new Map();
86
+ async function getPeaks(src, buckets) {
87
+ const hit = cache.get(src);
88
+ if (hit) return hit;
89
+ const flying = inflight.get(src);
90
+ if (flying) return flying;
91
+ const promise = decodePeaks(src, buckets).then((peaks) => {
92
+ cache.set(src, peaks);
93
+ return peaks;
94
+ });
95
+ inflight.set(src, promise);
96
+ try {
97
+ return await promise;
98
+ } finally {
99
+ inflight.delete(src);
100
+ }
101
+ }
102
+ __name(getPeaks, "getPeaks");
103
+ function setPeaks(src, peaks) {
104
+ cache.set(src, peaks);
105
+ }
106
+ __name(setPeaks, "setPeaks");
107
+ function getPeaksFromCache(src) {
108
+ return cache.get(src);
109
+ }
110
+ __name(getPeaksFromCache, "getPeaksFromCache");
111
+
112
+ // src/tools/AudioPlayer/store/activePlayerBus.ts
113
+ var CHANNEL = "djangocfg-audioplayer:active";
114
+ var activeId = null;
115
+ var listeners = /* @__PURE__ */ new Set();
116
+ var pausers = /* @__PURE__ */ new Map();
117
+ var channel = null;
118
+ function getChannel() {
119
+ if (typeof BroadcastChannel === "undefined") return null;
120
+ if (channel) return channel;
121
+ channel = new BroadcastChannel(CHANNEL);
122
+ channel.addEventListener("message", (e) => {
123
+ const next = typeof e.data === "string" ? e.data : null;
124
+ if (next === activeId) return;
125
+ activeId = next;
126
+ for (const [id, pause] of pausers) {
127
+ if (id !== next) pause();
128
+ }
129
+ for (const l of listeners) l(next);
130
+ });
131
+ return channel;
132
+ }
133
+ __name(getChannel, "getChannel");
134
+ function registerPlayer(id, pause) {
135
+ pausers.set(id, pause);
136
+ return () => {
137
+ pausers.delete(id);
138
+ if (activeId === id) activeId = null;
139
+ };
140
+ }
141
+ __name(registerPlayer, "registerPlayer");
142
+ function setActivePlayer(id) {
143
+ if (activeId === id) return;
144
+ activeId = id;
145
+ for (const [pid, pause] of pausers) {
146
+ if (pid !== id) pause();
147
+ }
148
+ for (const l of listeners) l(id);
149
+ getChannel()?.postMessage(id);
150
+ }
151
+ __name(setActivePlayer, "setActivePlayer");
152
+
153
+ // src/tools/AudioPlayer/store/createLevelsStore.ts
154
+ function createLevelsStore(initial) {
155
+ let current = initial ?? new Float32Array(0);
156
+ let active = false;
157
+ const listeners3 = /* @__PURE__ */ new Set();
158
+ return {
159
+ subscribe(cb) {
160
+ listeners3.add(cb);
161
+ return () => listeners3.delete(cb);
162
+ },
163
+ getCurrent() {
164
+ return current;
165
+ },
166
+ set(next) {
167
+ current = next;
168
+ for (const cb of listeners3) cb();
169
+ },
170
+ setActive(value) {
171
+ active = value;
172
+ },
173
+ isActive() {
174
+ return active;
175
+ }
176
+ };
177
+ }
178
+ __name(createLevelsStore, "createLevelsStore");
179
+
180
+ // src/tools/AudioPlayer/store/preferencesStore.ts
181
+ var STORAGE_KEY = "djangocfg-audioplayer:prefs";
182
+ var DEFAULT_VOLUME = 1;
183
+ var DEFAULT_MUTED = false;
184
+ var cached = null;
185
+ var listeners2 = /* @__PURE__ */ new Set();
186
+ var storageBound = false;
187
+ function clamp01(v) {
188
+ if (!Number.isFinite(v)) return DEFAULT_VOLUME;
189
+ return v < 0 ? 0 : v > 1 ? 1 : v;
190
+ }
191
+ __name(clamp01, "clamp01");
192
+ function readFromStorage() {
193
+ if (typeof window === "undefined") {
194
+ return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED };
195
+ }
196
+ try {
197
+ const raw = window.localStorage.getItem(STORAGE_KEY);
198
+ if (!raw) return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED };
199
+ const parsed = JSON.parse(raw);
200
+ return {
201
+ volume: typeof parsed?.volume === "number" ? clamp01(parsed.volume) : DEFAULT_VOLUME,
202
+ muted: typeof parsed?.muted === "boolean" ? parsed.muted : DEFAULT_MUTED
203
+ };
204
+ } catch {
205
+ return { volume: DEFAULT_VOLUME, muted: DEFAULT_MUTED };
206
+ }
207
+ }
208
+ __name(readFromStorage, "readFromStorage");
209
+ function writeToStorage(prefs) {
210
+ if (typeof window === "undefined") return;
211
+ try {
212
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
213
+ } catch {
214
+ }
215
+ }
216
+ __name(writeToStorage, "writeToStorage");
217
+ function bindStorage() {
218
+ if (storageBound || typeof window === "undefined") return;
219
+ storageBound = true;
220
+ window.addEventListener("storage", (e) => {
221
+ if (e.key !== STORAGE_KEY) return;
222
+ cached = readFromStorage();
223
+ for (const cb of listeners2) cb(cached);
224
+ });
225
+ }
226
+ __name(bindStorage, "bindStorage");
227
+ function getPreferences() {
228
+ if (!cached) cached = readFromStorage();
229
+ bindStorage();
230
+ return cached;
231
+ }
232
+ __name(getPreferences, "getPreferences");
233
+ function update(next, persist = true) {
234
+ cached = next;
235
+ if (persist) writeToStorage(next);
236
+ for (const cb of listeners2) cb(next);
237
+ }
238
+ __name(update, "update");
239
+ function setStoredVolume(volume) {
240
+ const current = getPreferences();
241
+ const v = clamp01(volume);
242
+ if (v === current.volume) return;
243
+ update({ ...current, volume: v });
244
+ }
245
+ __name(setStoredVolume, "setStoredVolume");
246
+ function setStoredMuted(muted) {
247
+ const current = getPreferences();
248
+ if (muted === current.muted) return;
249
+ update({ ...current, muted });
250
+ }
251
+ __name(setStoredMuted, "setStoredMuted");
252
+ function subscribePreferences(cb) {
253
+ listeners2.add(cb);
254
+ bindStorage();
255
+ return () => listeners2.delete(cb);
256
+ }
257
+ __name(subscribePreferences, "subscribePreferences");
258
+
259
+ // src/tools/AudioPlayer/utils/clamp.ts
260
+ function clamp(value, min, max) {
261
+ if (value < min) return min;
262
+ if (value > max) return max;
263
+ return value;
264
+ }
265
+ __name(clamp, "clamp");
266
+ var AudioRefCtx = createContext(null);
267
+ AudioRefCtx.displayName = "AudioPlayerAudioRefCtx";
268
+ var ControlsCtx = createContext(null);
269
+ ControlsCtx.displayName = "AudioPlayerControlsCtx";
270
+ var LevelsCtx = createContext(null);
271
+ LevelsCtx.displayName = "AudioPlayerLevelsCtx";
272
+ var MetaCtx = createContext(null);
273
+ MetaCtx.displayName = "AudioPlayerMetaCtx";
274
+ var StateCtx = createContext(null);
275
+ StateCtx.displayName = "AudioPlayerStateCtx";
276
+ var TRACKED = [
277
+ "play",
278
+ "pause",
279
+ "ended",
280
+ "loadedmetadata",
281
+ "durationchange",
282
+ "emptied",
283
+ "waiting",
284
+ "canplay",
285
+ "error"
286
+ ];
287
+ function readSnapshot(audio) {
288
+ return {
289
+ paused: audio.paused,
290
+ ended: audio.ended,
291
+ duration: Number.isFinite(audio.duration) ? audio.duration : 0,
292
+ ready: audio.readyState >= 2,
293
+ errorCode: audio.error?.code ?? null
294
+ };
295
+ }
296
+ __name(readSnapshot, "readSnapshot");
297
+ function snapshotsEqual(a, b) {
298
+ return a.paused === b.paused && a.ended === b.ended && a.duration === b.duration && a.ready === b.ready && a.errorCode === b.errorCode;
299
+ }
300
+ __name(snapshotsEqual, "snapshotsEqual");
301
+ function createAudioSnapshotSource(audio) {
302
+ let cached2 = readSnapshot(audio);
303
+ return {
304
+ subscribe(cb) {
305
+ const refresh = /* @__PURE__ */ __name(() => {
306
+ const next = readSnapshot(audio);
307
+ if (!snapshotsEqual(cached2, next)) {
308
+ cached2 = next;
309
+ cb();
310
+ }
311
+ }, "refresh");
312
+ for (const e of TRACKED) audio.addEventListener(e, refresh);
313
+ return () => {
314
+ for (const e of TRACKED) audio.removeEventListener(e, refresh);
315
+ };
316
+ },
317
+ getSnapshot() {
318
+ return cached2;
319
+ },
320
+ getServerSnapshot() {
321
+ return cached2;
322
+ }
323
+ };
324
+ }
325
+ __name(createAudioSnapshotSource, "createAudioSnapshotSource");
326
+ function errorCodeToReason(code) {
327
+ if (code === 2) return "network";
328
+ if (code === 3) return "decode";
329
+ if (code === 4) return "unsupported";
330
+ return "unknown";
331
+ }
332
+ __name(errorCodeToReason, "errorCodeToReason");
333
+ function snapshotToState(snap, hasSrc, peaks) {
334
+ if (snap.errorCode !== null) {
335
+ return { kind: "error", reason: errorCodeToReason(snap.errorCode), duration: snap.duration };
336
+ }
337
+ if (!hasSrc) return { kind: "idle" };
338
+ if (!snap.ready) return { kind: "loading" };
339
+ const kind = snap.ended ? "ended" : snap.paused ? "paused" : "playing";
340
+ return { kind, duration: snap.duration, peaks };
341
+ }
342
+ __name(snapshotToState, "snapshotToState");
343
+ function useAudioElementState(source, hasSrc, peaks) {
344
+ const snap = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getServerSnapshot);
345
+ return snapshotToState(snap, hasSrc, peaks);
346
+ }
347
+ __name(useAudioElementState, "useAudioElementState");
348
+ function createAudioElement() {
349
+ const a = new Audio();
350
+ a.crossOrigin = "anonymous";
351
+ a.preload = "metadata";
352
+ return a;
353
+ }
354
+ __name(createAudioElement, "createAudioElement");
355
+ function PlayerProvider(props) {
356
+ const {
357
+ src,
358
+ peaks: peaksProp,
359
+ title,
360
+ artist,
361
+ album,
362
+ cover,
363
+ autoplay = false,
364
+ loop = false,
365
+ initialVolume,
366
+ muted: mutedProp,
367
+ preload = "metadata",
368
+ exclusive = true,
369
+ onPrev,
370
+ onNext,
371
+ onPlay,
372
+ onPause,
373
+ onEnded,
374
+ onError,
375
+ children
376
+ } = props;
377
+ const volumeIsControlled = initialVolume !== void 0;
378
+ const mutedIsControlled = mutedProp !== void 0;
379
+ const playerId = useId();
380
+ const [audio] = useState(createAudioElement);
381
+ const [source] = useState(() => createAudioSnapshotSource(audio));
382
+ const [levelsStore] = useState(() => createLevelsStore());
383
+ if (peaksProp && getPeaksFromCache(src) === void 0) {
384
+ setPeaks(src, peaksProp);
385
+ }
386
+ const initialPeaks = peaksProp ?? getPeaksFromCache(src);
387
+ const [activePeaks, setActivePeaks] = useState(initialPeaks);
388
+ useEffect(() => {
389
+ if (audio.src !== src) {
390
+ audio.src = src;
391
+ audio.load();
392
+ }
393
+ }, [audio, src]);
394
+ useEffect(() => {
395
+ audio.preload = preload;
396
+ }, [audio, preload]);
397
+ useEffect(() => {
398
+ audio.loop = loop;
399
+ }, [audio, loop]);
400
+ useEffect(() => {
401
+ const stored = getPreferences();
402
+ audio.volume = clamp(volumeIsControlled ? initialVolume : stored.volume, 0, 1);
403
+ audio.muted = mutedIsControlled ? Boolean(mutedProp) : stored.muted;
404
+ }, [audio]);
405
+ useEffect(() => {
406
+ if (!volumeIsControlled || initialVolume === void 0) return;
407
+ audio.volume = clamp(initialVolume, 0, 1);
408
+ }, [audio, volumeIsControlled, initialVolume]);
409
+ useEffect(() => {
410
+ if (!mutedIsControlled || mutedProp === void 0) return;
411
+ audio.muted = mutedProp;
412
+ }, [audio, mutedIsControlled, mutedProp]);
413
+ useEffect(() => {
414
+ if (volumeIsControlled && mutedIsControlled) return;
415
+ return subscribePreferences((prefs) => {
416
+ if (!volumeIsControlled && audio.volume !== prefs.volume) {
417
+ audio.volume = prefs.volume;
418
+ }
419
+ if (!mutedIsControlled && audio.muted !== prefs.muted) {
420
+ audio.muted = prefs.muted;
421
+ }
422
+ });
423
+ }, [audio, volumeIsControlled, mutedIsControlled]);
424
+ useEffect(() => {
425
+ setActivePeaks(getPeaksFromCache(src));
426
+ }, [src]);
427
+ useEffect(() => {
428
+ const handlePlay = /* @__PURE__ */ __name(() => onPlay?.(), "handlePlay");
429
+ const handlePause = /* @__PURE__ */ __name(() => onPause?.(), "handlePause");
430
+ const handleEnded = /* @__PURE__ */ __name(() => onEnded?.(), "handleEnded");
431
+ const handleError = /* @__PURE__ */ __name((e) => {
432
+ const code = audio.error?.code ?? null;
433
+ const reason = code === 2 ? "network" : code === 3 ? "decode" : code === 4 ? "unsupported" : "unknown";
434
+ onError?.(reason, e);
435
+ }, "handleError");
436
+ audio.addEventListener("play", handlePlay);
437
+ audio.addEventListener("pause", handlePause);
438
+ audio.addEventListener("ended", handleEnded);
439
+ audio.addEventListener("error", handleError);
440
+ return () => {
441
+ audio.removeEventListener("play", handlePlay);
442
+ audio.removeEventListener("pause", handlePause);
443
+ audio.removeEventListener("ended", handleEnded);
444
+ audio.removeEventListener("error", handleError);
445
+ };
446
+ }, [audio, onEnded, onError, onPause, onPlay]);
447
+ const exclusiveRef = useRef(exclusive);
448
+ exclusiveRef.current = exclusive;
449
+ const playerIdRef = useRef(playerId);
450
+ playerIdRef.current = playerId;
451
+ useEffect(() => {
452
+ if (!exclusive) return;
453
+ const unregister = registerPlayer(playerId, () => {
454
+ audio.pause();
455
+ });
456
+ const onPlayHandler = /* @__PURE__ */ __name(() => setActivePlayer(playerId), "onPlayHandler");
457
+ audio.addEventListener("play", onPlayHandler);
458
+ return () => {
459
+ audio.removeEventListener("play", onPlayHandler);
460
+ unregister();
461
+ };
462
+ }, [audio, exclusive, playerId]);
463
+ useEffect(() => {
464
+ return () => {
465
+ try {
466
+ audio.pause();
467
+ audio.removeAttribute("src");
468
+ audio.load();
469
+ } catch {
470
+ }
471
+ };
472
+ }, [audio]);
473
+ const controls = useMemo(() => {
474
+ const playFn = /* @__PURE__ */ __name(async () => {
475
+ if (exclusiveRef.current) setActivePlayer(playerIdRef.current);
476
+ try {
477
+ await unlockAudioContext();
478
+ } catch {
479
+ }
480
+ try {
481
+ await audio.play();
482
+ } catch (e) {
483
+ const isAbort = e instanceof DOMException && e.name === "AbortError";
484
+ if (!isAbort) onError?.("unknown", e);
485
+ }
486
+ }, "playFn");
487
+ const pauseFn = /* @__PURE__ */ __name(() => {
488
+ audio.pause();
489
+ }, "pauseFn");
490
+ return {
491
+ play: playFn,
492
+ pause: pauseFn,
493
+ toggle: /* @__PURE__ */ __name(async () => {
494
+ if (audio.paused || audio.ended) await playFn();
495
+ else pauseFn();
496
+ }, "toggle"),
497
+ seek: /* @__PURE__ */ __name((seconds) => {
498
+ if (!Number.isFinite(seconds)) return;
499
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
500
+ audio.currentTime = clamp(seconds, 0, dur || seconds);
501
+ }, "seek"),
502
+ seekTo: /* @__PURE__ */ __name((ratio) => {
503
+ const r = clamp(ratio, 0, 1);
504
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
505
+ if (dur > 0) audio.currentTime = r * dur;
506
+ }, "seekTo"),
507
+ setVolume: /* @__PURE__ */ __name((v) => {
508
+ const next = clamp(v, 0, 1);
509
+ audio.volume = next;
510
+ if (next > 0 && audio.muted) audio.muted = false;
511
+ if (!volumeIsControlled) setStoredVolume(next);
512
+ if (!mutedIsControlled && next > 0) setStoredMuted(false);
513
+ }, "setVolume"),
514
+ toggleMute: /* @__PURE__ */ __name(() => {
515
+ const next = !audio.muted;
516
+ audio.muted = next;
517
+ if (!mutedIsControlled) setStoredMuted(next);
518
+ }, "toggleMute"),
519
+ toggleLoop: /* @__PURE__ */ __name(() => {
520
+ audio.loop = !audio.loop;
521
+ }, "toggleLoop")
522
+ };
523
+ }, [audio, volumeIsControlled, mutedIsControlled]);
524
+ useEffect(() => {
525
+ if (!autoplay) return;
526
+ void controls.play();
527
+ }, [autoplay, controls]);
528
+ const state = useAudioElementState(source, Boolean(audio.src), activePeaks);
529
+ const meta = useMemo(
530
+ () => ({
531
+ src,
532
+ title,
533
+ artist,
534
+ album,
535
+ cover,
536
+ hasPrev: Boolean(onPrev),
537
+ hasNext: Boolean(onNext)
538
+ }),
539
+ [src, title, artist, album, cover, onPrev, onNext]
540
+ );
541
+ return /* @__PURE__ */ jsx(AudioRefCtx.Provider, { value: audio, children: /* @__PURE__ */ jsx(MetaCtx.Provider, { value: meta, children: /* @__PURE__ */ jsx(StateCtx.Provider, { value: state, children: /* @__PURE__ */ jsx(ControlsCtx.Provider, { value: controls, children: /* @__PURE__ */ jsx(LevelsCtx.Provider, { value: levelsStore, children }) }) }) }) });
542
+ }
543
+ __name(PlayerProvider, "PlayerProvider");
544
+ function useCtxOrThrow(ctx, name) {
545
+ const value = useContext(ctx);
546
+ if (value === null) {
547
+ throw new Error(`${name} must be used inside <PlayerProvider>`);
548
+ }
549
+ return value;
550
+ }
551
+ __name(useCtxOrThrow, "useCtxOrThrow");
552
+ var usePlayerState = /* @__PURE__ */ __name(() => useCtxOrThrow(StateCtx, "usePlayerState"), "usePlayerState");
553
+ var usePlayerControls = /* @__PURE__ */ __name(() => useCtxOrThrow(ControlsCtx, "usePlayerControls"), "usePlayerControls");
554
+ var usePlayerLevels = /* @__PURE__ */ __name(() => useCtxOrThrow(LevelsCtx, "usePlayerLevels"), "usePlayerLevels");
555
+ var usePlayerMeta = /* @__PURE__ */ __name(() => useCtxOrThrow(MetaCtx, "usePlayerMeta"), "usePlayerMeta");
556
+ var usePlayerAudio = /* @__PURE__ */ __name(() => useCtxOrThrow(AudioRefCtx, "usePlayerAudio"), "usePlayerAudio");
557
+ var usePlayerPaused = /* @__PURE__ */ __name(() => {
558
+ const s = usePlayerState();
559
+ return s.kind !== "playing";
560
+ }, "usePlayerPaused");
561
+ var usePlayerDuration = /* @__PURE__ */ __name(() => {
562
+ const s = usePlayerState();
563
+ return "duration" in s ? s.duration : 0;
564
+ }, "usePlayerDuration");
565
+ function useElementWidth(el) {
566
+ const [w, setW] = useState(0);
567
+ useEffect(() => {
568
+ if (!el || typeof ResizeObserver === "undefined") return;
569
+ setW(el.clientWidth);
570
+ const obs = new ResizeObserver((entries) => {
571
+ for (const entry of entries) {
572
+ const next = entry.contentRect.width;
573
+ setW((prev) => Math.abs(prev - next) < 0.5 ? prev : next);
574
+ }
575
+ });
576
+ obs.observe(el);
577
+ return () => obs.disconnect();
578
+ }, [el]);
579
+ return w;
580
+ }
581
+ __name(useElementWidth, "useElementWidth");
582
+ var OPTS = { preventDefault: true };
583
+ var BINDINGS = [
584
+ { label: "Play / pause", hint: "Space" },
585
+ { label: "Seek +5s", hint: "\u2192" },
586
+ { label: "Seek \u22125s", hint: "\u2190" },
587
+ { label: "Volume up", hint: "\u2191" },
588
+ { label: "Volume down", hint: "\u2193" },
589
+ { label: "Toggle mute", hint: "M" },
590
+ { label: "Toggle loop", hint: "L" }
591
+ ];
592
+ function useKeyboardShortcuts({
593
+ audio,
594
+ controls,
595
+ enabled = true
596
+ }) {
597
+ const refToggle = useHotkey(["space", "k"], () => void controls.toggle(), {
598
+ ...OPTS,
599
+ enabled,
600
+ description: "Play / pause"
601
+ });
602
+ const refForward = useHotkey("right", () => controls.seek(audio.currentTime + 5), {
603
+ ...OPTS,
604
+ enabled,
605
+ description: "Seek +5s"
606
+ });
607
+ const refBackward = useHotkey("left", () => controls.seek(audio.currentTime - 5), {
608
+ ...OPTS,
609
+ enabled,
610
+ description: "Seek \u22125s"
611
+ });
612
+ const refVolUp = useHotkey("up", () => controls.setVolume(clamp(audio.volume + 0.05, 0, 1)), {
613
+ ...OPTS,
614
+ enabled,
615
+ description: "Volume up"
616
+ });
617
+ const refVolDown = useHotkey("down", () => controls.setVolume(clamp(audio.volume - 0.05, 0, 1)), {
618
+ ...OPTS,
619
+ enabled,
620
+ description: "Volume down"
621
+ });
622
+ const refMute = useHotkey("m", () => controls.toggleMute(), {
623
+ ...OPTS,
624
+ enabled,
625
+ description: "Toggle mute"
626
+ });
627
+ const refLoop = useHotkey("l", () => controls.toggleLoop(), {
628
+ ...OPTS,
629
+ enabled,
630
+ description: "Toggle loop"
631
+ });
632
+ const ref = useCallback(
633
+ (instance) => {
634
+ refToggle(instance);
635
+ refForward(instance);
636
+ refBackward(instance);
637
+ refVolUp(instance);
638
+ refVolDown(instance);
639
+ refMute(instance);
640
+ refLoop(instance);
641
+ },
642
+ [refToggle, refForward, refBackward, refVolUp, refVolDown, refMute, refLoop]
643
+ );
644
+ return { ref, bindings: BINDINGS };
645
+ }
646
+ __name(useKeyboardShortcuts, "useKeyboardShortcuts");
647
+ var ACTIONS = [
648
+ "play",
649
+ "pause",
650
+ "previoustrack",
651
+ "nexttrack",
652
+ "seekbackward",
653
+ "seekforward"
654
+ ];
655
+ function useMediaSession(audio, meta, controls, onPrev, onNext) {
656
+ useEffect(() => {
657
+ if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
658
+ const ms = navigator.mediaSession;
659
+ try {
660
+ ms.metadata = new MediaMetadata({
661
+ title: meta.title ?? "",
662
+ artist: meta.artist ?? "",
663
+ album: meta.album ?? "",
664
+ artwork: meta.cover ? [{ src: meta.cover, sizes: "512x512", type: "image/jpeg" }] : []
665
+ });
666
+ } catch {
667
+ }
668
+ const handlers = {
669
+ play: /* @__PURE__ */ __name(() => {
670
+ void controls.play();
671
+ }, "play"),
672
+ pause: /* @__PURE__ */ __name(() => controls.pause(), "pause"),
673
+ previoustrack: onPrev ?? null,
674
+ nexttrack: onNext ?? null,
675
+ seekbackward: /* @__PURE__ */ __name((details) => {
676
+ const offset = details.seekOffset ?? 10;
677
+ controls.seek(audio.currentTime - offset);
678
+ }, "seekbackward"),
679
+ seekforward: /* @__PURE__ */ __name((details) => {
680
+ const offset = details.seekOffset ?? 10;
681
+ controls.seek(audio.currentTime + offset);
682
+ }, "seekforward")
683
+ };
684
+ for (const a of ACTIONS) {
685
+ try {
686
+ ms.setActionHandler(a, handlers[a] ?? null);
687
+ } catch {
688
+ }
689
+ }
690
+ return () => {
691
+ for (const a of ACTIONS) {
692
+ try {
693
+ ms.setActionHandler(a, null);
694
+ } catch {
695
+ }
696
+ }
697
+ };
698
+ }, [audio, controls, meta.title, meta.artist, meta.album, meta.cover, onPrev, onNext]);
699
+ }
700
+ __name(useMediaSession, "useMediaSession");
701
+ function PlayButton({ size = "default" }) {
702
+ const state = usePlayerState();
703
+ const { toggle, play } = usePlayerControls();
704
+ const dim = size === "compact" ? 28 : 36;
705
+ const icon = size === "compact" ? 14 : 16;
706
+ let label = "Play";
707
+ let Icon = Play;
708
+ let onClick = /* @__PURE__ */ __name(() => void toggle(), "onClick");
709
+ let disabled = false;
710
+ switch (state.kind) {
711
+ case "idle":
712
+ Icon = Play;
713
+ disabled = true;
714
+ break;
715
+ case "loading":
716
+ case "decoding":
717
+ Icon = LoaderCircle;
718
+ label = "Loading";
719
+ disabled = true;
720
+ break;
721
+ case "playing":
722
+ Icon = Pause;
723
+ label = "Pause";
724
+ break;
725
+ case "paused":
726
+ Icon = Play;
727
+ label = "Play";
728
+ break;
729
+ case "ended":
730
+ Icon = RotateCcw;
731
+ label = "Replay";
732
+ onClick = /* @__PURE__ */ __name(() => void play(), "onClick");
733
+ break;
734
+ case "error":
735
+ Icon = Play;
736
+ label = "Retry";
737
+ onClick = /* @__PURE__ */ __name(() => void play(), "onClick");
738
+ break;
739
+ }
740
+ const button = /* @__PURE__ */ jsx(
741
+ "button",
742
+ {
743
+ type: "button",
744
+ onClick,
745
+ disabled,
746
+ "aria-label": label,
747
+ className: "audioplayer-press grid place-items-center rounded-full bg-foreground text-background transition-colors hover:bg-foreground/90 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
748
+ style: { width: dim, height: dim },
749
+ children: /* @__PURE__ */ jsx(
750
+ Icon,
751
+ {
752
+ size: icon,
753
+ strokeWidth: 2,
754
+ className: state.kind === "loading" || state.kind === "decoding" ? "animate-spin" : ""
755
+ }
756
+ )
757
+ }
758
+ );
759
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
760
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: button }),
761
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
762
+ label,
763
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border border-border/40 bg-muted px-1 font-mono text-[10px] text-muted-foreground", children: "Space" })
764
+ ] }) })
765
+ ] });
766
+ }
767
+ __name(PlayButton, "PlayButton");
768
+ function IconButton({
769
+ label,
770
+ shortcut,
771
+ active,
772
+ children,
773
+ noTooltip,
774
+ className = "",
775
+ ...rest
776
+ }) {
777
+ const stateClasses = active ? "bg-primary/10 text-primary hover:bg-primary/15" : "text-muted-foreground hover:bg-accent hover:text-foreground";
778
+ const button = /* @__PURE__ */ jsx(
779
+ "button",
780
+ {
781
+ type: "button",
782
+ "aria-label": label,
783
+ "aria-pressed": active,
784
+ className: `audioplayer-press grid h-8 w-8 place-items-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-50 disabled:pointer-events-none ${stateClasses} ${className}`,
785
+ ...rest,
786
+ children
787
+ }
788
+ );
789
+ if (noTooltip) return button;
790
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
791
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: button }),
792
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
793
+ label,
794
+ shortcut && /* @__PURE__ */ jsx("kbd", { className: "rounded border border-border/40 bg-muted px-1 font-mono text-[10px] text-muted-foreground", children: shortcut })
795
+ ] }) })
796
+ ] });
797
+ }
798
+ __name(IconButton, "IconButton");
799
+ function SkipButton({ direction, onClick }) {
800
+ if (!onClick) return null;
801
+ const Icon = direction === "prev" ? SkipBack : SkipForward;
802
+ const label = direction === "prev" ? "Previous track" : "Next track";
803
+ return /* @__PURE__ */ jsx(IconButton, { label, shortcut: direction === "prev" ? "\u2190" : "\u2192", onClick, children: /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 1.75 }) });
804
+ }
805
+ __name(SkipButton, "SkipButton");
806
+ function LoopButton() {
807
+ const audio = usePlayerAudio();
808
+ const { toggleLoop } = usePlayerControls();
809
+ const [loop, setLoop] = useState(audio.loop);
810
+ useEffect(() => {
811
+ setLoop(audio.loop);
812
+ }, [audio]);
813
+ return /* @__PURE__ */ jsx(
814
+ IconButton,
815
+ {
816
+ label: loop ? "Disable loop" : "Enable loop",
817
+ shortcut: "L",
818
+ active: loop,
819
+ onClick: () => {
820
+ toggleLoop();
821
+ setLoop(audio.loop);
822
+ },
823
+ children: /* @__PURE__ */ jsx(Repeat, { size: 16, strokeWidth: 1.75 })
824
+ }
825
+ );
826
+ }
827
+ __name(LoopButton, "LoopButton");
828
+ var CLOSE_DELAY_MS = 120;
829
+ function isIosSafari() {
830
+ if (typeof navigator === "undefined") return false;
831
+ const ua = navigator.userAgent;
832
+ const iOS = /iPad|iPhone|iPod/.test(ua) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
833
+ return iOS;
834
+ }
835
+ __name(isIosSafari, "isIosSafari");
836
+ var HIDE_VOLUME = isIosSafari();
837
+ function VolumeControl() {
838
+ const audio = usePlayerAudio();
839
+ const { setVolume, toggleMute } = usePlayerControls();
840
+ const [volume, setVol] = useState(audio.volume);
841
+ const [muted, setMuted] = useState(audio.muted);
842
+ const [isOpen, setOpen] = useState(false);
843
+ const closeTimer = useRef(null);
844
+ const isTouch = useMediaQuery("(hover: none), (pointer: coarse)");
845
+ useEffect(() => {
846
+ const sync = /* @__PURE__ */ __name(() => {
847
+ setVol(audio.volume);
848
+ setMuted(audio.muted);
849
+ }, "sync");
850
+ audio.addEventListener("volumechange", sync);
851
+ return () => audio.removeEventListener("volumechange", sync);
852
+ }, [audio]);
853
+ useEffect(() => {
854
+ return () => {
855
+ if (closeTimer.current) clearTimeout(closeTimer.current);
856
+ };
857
+ }, []);
858
+ const containerRef = useRef(null);
859
+ useEffect(() => {
860
+ if (!isOpen || !isTouch) return;
861
+ const onDown = /* @__PURE__ */ __name((e) => {
862
+ if (!containerRef.current) return;
863
+ if (!containerRef.current.contains(e.target)) setOpen(false);
864
+ }, "onDown");
865
+ document.addEventListener("pointerdown", onDown);
866
+ return () => document.removeEventListener("pointerdown", onDown);
867
+ }, [isOpen, isTouch]);
868
+ if (HIDE_VOLUME) {
869
+ return /* @__PURE__ */ jsx(
870
+ IconButton,
871
+ {
872
+ label: muted ? "Unmute" : "Mute",
873
+ shortcut: "M",
874
+ active: muted,
875
+ onClick: () => {
876
+ toggleMute();
877
+ setMuted(audio.muted);
878
+ },
879
+ children: muted || volume === 0 ? /* @__PURE__ */ jsx(VolumeX, { size: 16, strokeWidth: 1.75 }) : /* @__PURE__ */ jsx(Volume2, { size: 16, strokeWidth: 1.75 })
880
+ }
881
+ );
882
+ }
883
+ const cancelClose = /* @__PURE__ */ __name(() => {
884
+ if (closeTimer.current) {
885
+ clearTimeout(closeTimer.current);
886
+ closeTimer.current = null;
887
+ }
888
+ }, "cancelClose");
889
+ const scheduleClose = /* @__PURE__ */ __name(() => {
890
+ cancelClose();
891
+ closeTimer.current = setTimeout(() => setOpen(false), CLOSE_DELAY_MS);
892
+ }, "scheduleClose");
893
+ const open = /* @__PURE__ */ __name(() => {
894
+ cancelClose();
895
+ setOpen(true);
896
+ }, "open");
897
+ const Icon = muted || volume === 0 ? VolumeX : Volume2;
898
+ const hoverHandlers = isTouch ? {} : {
899
+ onPointerEnter: open,
900
+ onPointerLeave: scheduleClose,
901
+ onFocusCapture: open,
902
+ onBlurCapture: /* @__PURE__ */ __name((e) => {
903
+ if (!e.currentTarget.contains(e.relatedTarget)) scheduleClose();
904
+ }, "onBlurCapture")
905
+ };
906
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative", ...hoverHandlers, children: [
907
+ /* @__PURE__ */ jsx(
908
+ IconButton,
909
+ {
910
+ label: isOpen ? "Close volume" : muted ? "Unmute" : "Volume",
911
+ shortcut: isTouch ? void 0 : "M",
912
+ active: muted,
913
+ noTooltip: isOpen,
914
+ onClick: () => {
915
+ if (isTouch) {
916
+ setOpen((v) => !v);
917
+ return;
918
+ }
919
+ toggleMute();
920
+ setMuted(audio.muted);
921
+ },
922
+ children: /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 1.75 })
923
+ }
924
+ ),
925
+ isOpen && /* @__PURE__ */ jsx(
926
+ "div",
927
+ {
928
+ className: "absolute bottom-full left-1/2 z-20 -translate-x-1/2 pb-2",
929
+ onPointerEnter: isTouch ? void 0 : open,
930
+ onPointerLeave: isTouch ? void 0 : scheduleClose,
931
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md border border-border/60 bg-card px-3 py-2 shadow-sm", children: [
932
+ /* @__PURE__ */ jsx(
933
+ "input",
934
+ {
935
+ type: "range",
936
+ min: 0,
937
+ max: 1,
938
+ step: 0.01,
939
+ value: muted ? 0 : volume,
940
+ onChange: (e) => {
941
+ const v = parseFloat(e.target.value);
942
+ setVolume(v);
943
+ setVol(v);
944
+ if (v > 0) setMuted(false);
945
+ },
946
+ className: "h-1 w-32 appearance-none rounded-full bg-muted accent-foreground",
947
+ "aria-label": "Volume"
948
+ }
949
+ ),
950
+ /* @__PURE__ */ jsx("span", { className: "w-8 tabular-nums text-right text-[10px] text-muted-foreground", children: Math.round((muted ? 0 : volume) * 100) }),
951
+ isTouch && /* @__PURE__ */ jsx(
952
+ "button",
953
+ {
954
+ type: "button",
955
+ "aria-label": muted ? "Unmute" : "Mute",
956
+ className: "audioplayer-press grid h-7 w-7 place-items-center rounded text-muted-foreground hover:bg-accent",
957
+ onClick: () => {
958
+ toggleMute();
959
+ setMuted(audio.muted);
960
+ },
961
+ children: muted ? /* @__PURE__ */ jsx(VolumeX, { size: 14, strokeWidth: 1.75 }) : /* @__PURE__ */ jsx(Volume2, { size: 14, strokeWidth: 1.75 })
962
+ }
963
+ )
964
+ ] })
965
+ }
966
+ )
967
+ ] });
968
+ }
969
+ __name(VolumeControl, "VolumeControl");
970
+
971
+ // src/tools/AudioPlayer/utils/formatTime.ts
972
+ function formatTime(seconds) {
973
+ if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
974
+ const total = Math.floor(seconds);
975
+ const s = total % 60;
976
+ const m = Math.floor(total / 60) % 60;
977
+ const h = Math.floor(total / 3600);
978
+ const ss = s.toString().padStart(2, "0");
979
+ if (h > 0) {
980
+ const mm = m.toString().padStart(2, "0");
981
+ return `${h}:${mm}:${ss}`;
982
+ }
983
+ return `${m}:${ss}`;
984
+ }
985
+ __name(formatTime, "formatTime");
986
+ var READ_INTERVAL_MS = 200;
987
+ function TimeDisplay() {
988
+ const audio = usePlayerAudio();
989
+ const duration = usePlayerDuration();
990
+ const currentRef = useRef(null);
991
+ useEffect(() => {
992
+ const el = currentRef.current;
993
+ if (!el) return;
994
+ let raf = 0;
995
+ let last = -1;
996
+ let timer = null;
997
+ const write = /* @__PURE__ */ __name(() => {
998
+ const t = audio.currentTime;
999
+ if (Math.abs(t - last) < 0.5) return;
1000
+ last = t;
1001
+ el.textContent = formatTime(t);
1002
+ }, "write");
1003
+ write();
1004
+ const onSeek = /* @__PURE__ */ __name(() => write(), "onSeek");
1005
+ audio.addEventListener("seeked", onSeek);
1006
+ audio.addEventListener("timeupdate", onSeek);
1007
+ timer = setInterval(() => {
1008
+ if (!audio.paused) write();
1009
+ }, READ_INTERVAL_MS);
1010
+ return () => {
1011
+ cancelAnimationFrame(raf);
1012
+ audio.removeEventListener("seeked", onSeek);
1013
+ audio.removeEventListener("timeupdate", onSeek);
1014
+ if (timer) clearInterval(timer);
1015
+ };
1016
+ }, [audio]);
1017
+ return /* @__PURE__ */ jsxs("span", { className: "tabular-nums text-xs text-muted-foreground", children: [
1018
+ /* @__PURE__ */ jsx("span", { ref: currentRef, children: formatTime(audio.currentTime) }),
1019
+ " / ",
1020
+ formatTime(duration)
1021
+ ] });
1022
+ }
1023
+ __name(TimeDisplay, "TimeDisplay");
1024
+ function ControlsRow({ onPrev, onNext, showTime = false }) {
1025
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
1026
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1027
+ /* @__PURE__ */ jsx(SkipButton, { direction: "prev", onClick: onPrev }),
1028
+ /* @__PURE__ */ jsx(PlayButton, {}),
1029
+ /* @__PURE__ */ jsx(SkipButton, { direction: "next", onClick: onNext })
1030
+ ] }),
1031
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1032
+ showTime && /* @__PURE__ */ jsx(TimeDisplay, {}),
1033
+ /* @__PURE__ */ jsx(VolumeControl, {}),
1034
+ /* @__PURE__ */ jsx(LoopButton, {})
1035
+ ] })
1036
+ ] });
1037
+ }
1038
+ __name(ControlsRow, "ControlsRow");
1039
+ function CoverPlaceholder({ size }) {
1040
+ const inset = Math.max(4, Math.round(size * 0.14));
1041
+ const iconSize = Math.round(size * 0.4);
1042
+ return /* @__PURE__ */ jsxs(
1043
+ "div",
1044
+ {
1045
+ className: "relative grid place-items-center rounded-md bg-muted",
1046
+ style: { width: size, height: size },
1047
+ "aria-hidden": "true",
1048
+ children: [
1049
+ /* @__PURE__ */ jsx(
1050
+ "span",
1051
+ {
1052
+ className: "absolute rounded-sm bg-muted-foreground/10",
1053
+ style: { inset }
1054
+ }
1055
+ ),
1056
+ /* @__PURE__ */ jsx(
1057
+ Music,
1058
+ {
1059
+ className: "relative text-muted-foreground",
1060
+ style: { width: iconSize, height: iconSize },
1061
+ strokeWidth: 1.5
1062
+ }
1063
+ )
1064
+ ]
1065
+ }
1066
+ );
1067
+ }
1068
+ __name(CoverPlaceholder, "CoverPlaceholder");
1069
+ function Cover({ src, alt, size }) {
1070
+ const [errored, setErrored] = useState(false);
1071
+ if (!src || errored) return /* @__PURE__ */ jsx(CoverPlaceholder, { size });
1072
+ return /* @__PURE__ */ jsx(
1073
+ "img",
1074
+ {
1075
+ src,
1076
+ alt: alt ?? "",
1077
+ width: size,
1078
+ height: size,
1079
+ loading: "lazy",
1080
+ decoding: "async",
1081
+ onError: () => setErrored(true),
1082
+ className: "block rounded-md object-cover",
1083
+ style: { width: size, height: size }
1084
+ }
1085
+ );
1086
+ }
1087
+ __name(Cover, "Cover");
1088
+ var VAR = "--audioplayer-pulse";
1089
+ var MAX_SCALE = 0.03;
1090
+ var SMOOTH = 0.18;
1091
+ function ReactivePulse({ enabled, children }) {
1092
+ const ref = useRef(null);
1093
+ const store = usePlayerLevels();
1094
+ useEffect(() => {
1095
+ if (!enabled) {
1096
+ ref.current?.style.setProperty(VAR, "1");
1097
+ return;
1098
+ }
1099
+ const el = ref.current;
1100
+ if (!el) return;
1101
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
1102
+ el.style.setProperty(VAR, "1");
1103
+ return;
1104
+ }
1105
+ let raf = 0;
1106
+ let env = 0;
1107
+ const tick = /* @__PURE__ */ __name(() => {
1108
+ const buf = store.getCurrent();
1109
+ let energy = 0;
1110
+ const usable = Math.min(buf.length, 32);
1111
+ if (usable > 0) {
1112
+ for (let i = 0; i < usable; i++) energy += buf[i];
1113
+ energy /= usable;
1114
+ }
1115
+ env = env + (energy - env) * SMOOTH;
1116
+ const scale = 1 + Math.min(MAX_SCALE, env * MAX_SCALE * 1.5);
1117
+ el.style.setProperty(VAR, scale.toFixed(4));
1118
+ raf = requestAnimationFrame(tick);
1119
+ }, "tick");
1120
+ raf = requestAnimationFrame(tick);
1121
+ return () => cancelAnimationFrame(raf);
1122
+ }, [enabled, store]);
1123
+ return /* @__PURE__ */ jsx(
1124
+ "div",
1125
+ {
1126
+ ref,
1127
+ className: "audioplayer-pulse",
1128
+ style: {
1129
+ transform: "scale(var(--audioplayer-pulse, 1))",
1130
+ transformOrigin: "center",
1131
+ willChange: enabled ? "transform" : void 0
1132
+ },
1133
+ children
1134
+ }
1135
+ );
1136
+ }
1137
+ __name(ReactivePulse, "ReactivePulse");
1138
+ var REASONS = {
1139
+ network: "Network error while loading audio.",
1140
+ decode: "We can't decode this audio.",
1141
+ unsupported: "This audio format is not supported.",
1142
+ unknown: "Audio playback failed."
1143
+ };
1144
+ function ErrorState() {
1145
+ const state = usePlayerState();
1146
+ const { play } = usePlayerControls();
1147
+ if (state.kind !== "error") return null;
1148
+ const message = REASONS[state.reason] ?? REASONS.unknown;
1149
+ return /* @__PURE__ */ jsxs(
1150
+ "div",
1151
+ {
1152
+ role: "alert",
1153
+ className: "flex items-center gap-3 rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive",
1154
+ children: [
1155
+ /* @__PURE__ */ jsx(AlertTriangle, { size: 14, strokeWidth: 1.75 }),
1156
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: message }),
1157
+ /* @__PURE__ */ jsxs(
1158
+ "button",
1159
+ {
1160
+ type: "button",
1161
+ onClick: () => void play(),
1162
+ className: "audioplayer-press inline-flex items-center gap-1 rounded-md px-2 py-1 text-destructive hover:bg-destructive/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40",
1163
+ children: [
1164
+ /* @__PURE__ */ jsx(RotateCcw, { size: 12, strokeWidth: 2 }),
1165
+ "Retry"
1166
+ ]
1167
+ }
1168
+ )
1169
+ ]
1170
+ }
1171
+ );
1172
+ }
1173
+ __name(ErrorState, "ErrorState");
1174
+ function Title() {
1175
+ const { title } = usePlayerMeta();
1176
+ if (!title) return null;
1177
+ return /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", title, children: title });
1178
+ }
1179
+ __name(Title, "Title");
1180
+ function Artist() {
1181
+ const { artist, album } = usePlayerMeta();
1182
+ if (!artist && !album) return null;
1183
+ const text = [artist, album].filter(Boolean).join(" \xB7 ");
1184
+ return /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", title: text, children: text });
1185
+ }
1186
+ __name(Artist, "Artist");
1187
+ function usePeaks(opts) {
1188
+ const { src, enabled = true, triggerRef, decodeOnMount = false } = opts;
1189
+ const cached2 = getPeaksFromCache(src) ?? null;
1190
+ const [peaks, setLocal] = useState(cached2);
1191
+ const [loading, setLoading] = useState(!cached2 && enabled);
1192
+ const [error, setError] = useState(null);
1193
+ useEffect(() => {
1194
+ if (!enabled) return;
1195
+ const hit = getPeaksFromCache(src);
1196
+ if (hit) {
1197
+ setLocal(hit);
1198
+ setLoading(false);
1199
+ return;
1200
+ }
1201
+ setLocal(null);
1202
+ setLoading(true);
1203
+ let cancelled = false;
1204
+ let started = false;
1205
+ const startDecode = /* @__PURE__ */ __name(() => {
1206
+ if (started) return;
1207
+ started = true;
1208
+ getPeaks(src).then((p) => {
1209
+ if (cancelled) return;
1210
+ setLocal(p);
1211
+ setLoading(false);
1212
+ }).catch((e) => {
1213
+ if (cancelled) return;
1214
+ setError(e);
1215
+ setLoading(false);
1216
+ });
1217
+ }, "startDecode");
1218
+ if (decodeOnMount || !triggerRef?.current || typeof IntersectionObserver === "undefined") {
1219
+ startDecode();
1220
+ return () => {
1221
+ cancelled = true;
1222
+ };
1223
+ }
1224
+ const obs = new IntersectionObserver(
1225
+ (entries) => {
1226
+ for (const entry of entries) {
1227
+ if (entry.isIntersecting) {
1228
+ startDecode();
1229
+ obs.disconnect();
1230
+ break;
1231
+ }
1232
+ }
1233
+ },
1234
+ { rootMargin: "200px" }
1235
+ );
1236
+ obs.observe(triggerRef.current);
1237
+ return () => {
1238
+ cancelled = true;
1239
+ obs.disconnect();
1240
+ };
1241
+ }, [src, enabled, decodeOnMount, triggerRef]);
1242
+ return { peaks, loading, error };
1243
+ }
1244
+ __name(usePeaks, "usePeaks");
1245
+ var BAR_COUNT = 28;
1246
+ function BarsWaveform({ height, barWidth, barGap, bars = BAR_COUNT }) {
1247
+ const paused = usePlayerPaused();
1248
+ const items = useMemo(() => {
1249
+ return Array.from({ length: bars }, (_, i) => ({
1250
+ delay: `${i % 7 * 90}ms`,
1251
+ duration: `${800 + i % 5 * 120}ms`
1252
+ }));
1253
+ }, [bars]);
1254
+ return /* @__PURE__ */ jsx(
1255
+ "div",
1256
+ {
1257
+ className: "audioplayer-bars flex w-full items-center justify-between",
1258
+ style: { height, gap: barGap },
1259
+ "data-mode": "bars",
1260
+ "aria-hidden": "true",
1261
+ children: items.map((it, i) => /* @__PURE__ */ jsx(
1262
+ "span",
1263
+ {
1264
+ className: "rounded-sm bg-primary",
1265
+ style: {
1266
+ width: barWidth,
1267
+ height: "40%",
1268
+ animation: paused ? void 0 : `audioplayer-bar ${it.duration} ${it.delay} ease-in-out infinite`,
1269
+ transformOrigin: "center"
1270
+ }
1271
+ },
1272
+ i
1273
+ ))
1274
+ }
1275
+ );
1276
+ }
1277
+ __name(BarsWaveform, "BarsWaveform");
1278
+
1279
+ // src/tools/AudioPlayer/audio/mediaElementSourceCache.ts
1280
+ var cache2 = /* @__PURE__ */ new WeakMap();
1281
+ function getMediaElementSource(el) {
1282
+ const hit = cache2.get(el);
1283
+ if (hit) return hit;
1284
+ const ctx = getAudioContext();
1285
+ const node = ctx.createMediaElementSource(el);
1286
+ node.connect(ctx.destination);
1287
+ cache2.set(el, node);
1288
+ return node;
1289
+ }
1290
+ __name(getMediaElementSource, "getMediaElementSource");
1291
+
1292
+ // src/tools/AudioPlayer/hooks/useAnalyser.ts
1293
+ var FFT_SIZE = 1024;
1294
+ var READ_INTERVAL_MS2 = 33;
1295
+ var SMOOTHING = 0.8;
1296
+ function useAnalyser(audio, store, enabled) {
1297
+ useEffect(() => {
1298
+ if (!enabled) return;
1299
+ let analyser = null;
1300
+ let interval = null;
1301
+ let buffer = new Uint8Array(0);
1302
+ let normalized = new Float32Array(0);
1303
+ let cancelled = false;
1304
+ try {
1305
+ const ctx = getAudioContext();
1306
+ const source = getMediaElementSource(audio);
1307
+ analyser = ctx.createAnalyser();
1308
+ analyser.fftSize = FFT_SIZE;
1309
+ analyser.smoothingTimeConstant = SMOOTHING;
1310
+ source.connect(analyser);
1311
+ buffer = new Uint8Array(analyser.frequencyBinCount);
1312
+ normalized = new Float32Array(analyser.frequencyBinCount);
1313
+ store.setActive(true);
1314
+ const tick = /* @__PURE__ */ __name(() => {
1315
+ if (cancelled || !analyser) return;
1316
+ if (typeof document !== "undefined" && document.hidden) return;
1317
+ analyser.getByteFrequencyData(buffer);
1318
+ for (let i = 0; i < buffer.length; i++) normalized[i] = buffer[i] / 255;
1319
+ store.set(normalized);
1320
+ }, "tick");
1321
+ interval = setInterval(tick, READ_INTERVAL_MS2);
1322
+ } catch {
1323
+ store.setActive(false);
1324
+ }
1325
+ return () => {
1326
+ cancelled = true;
1327
+ if (interval) clearInterval(interval);
1328
+ if (analyser) {
1329
+ try {
1330
+ analyser.disconnect();
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ store.setActive(false);
1335
+ };
1336
+ }, [audio, store, enabled]);
1337
+ }
1338
+ __name(useAnalyser, "useAnalyser");
1339
+
1340
+ // src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts
1341
+ var HOVER_X = "--hp";
1342
+ var HOVER_OPACITY = "--ho";
1343
+ var TOOLTIP_LABEL = "--ht";
1344
+ function attachSeek(container, audio, options = {}) {
1345
+ let dragging = false;
1346
+ let movedDuringDrag = false;
1347
+ const { startsPlayback = true, onPlayRequest } = options;
1348
+ const ratioFor = /* @__PURE__ */ __name((clientX) => {
1349
+ const rect = container.getBoundingClientRect();
1350
+ if (rect.width === 0) return 0;
1351
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1352
+ }, "ratioFor");
1353
+ const seekTo = /* @__PURE__ */ __name((clientX) => {
1354
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
1355
+ if (dur > 0) audio.currentTime = ratioFor(clientX) * dur;
1356
+ }, "seekTo");
1357
+ const onPointerDown = /* @__PURE__ */ __name((e) => {
1358
+ if (e.button !== 0 && e.pointerType === "mouse") return;
1359
+ dragging = true;
1360
+ movedDuringDrag = false;
1361
+ container.setPointerCapture?.(e.pointerId);
1362
+ seekTo(e.clientX);
1363
+ }, "onPointerDown");
1364
+ const onPointerMove = /* @__PURE__ */ __name((e) => {
1365
+ if (!dragging) return;
1366
+ movedDuringDrag = true;
1367
+ seekTo(e.clientX);
1368
+ }, "onPointerMove");
1369
+ const onPointerEnd = /* @__PURE__ */ __name((e) => {
1370
+ if (!dragging) return;
1371
+ const wasDrag = movedDuringDrag;
1372
+ dragging = false;
1373
+ movedDuringDrag = false;
1374
+ try {
1375
+ container.releasePointerCapture?.(e.pointerId);
1376
+ } catch {
1377
+ }
1378
+ if (!wasDrag && startsPlayback && (audio.paused || audio.ended)) {
1379
+ onPlayRequest?.();
1380
+ }
1381
+ }, "onPointerEnd");
1382
+ container.addEventListener("pointerdown", onPointerDown);
1383
+ container.addEventListener("pointermove", onPointerMove);
1384
+ container.addEventListener("pointerup", onPointerEnd);
1385
+ container.addEventListener("pointercancel", onPointerEnd);
1386
+ return () => {
1387
+ container.removeEventListener("pointerdown", onPointerDown);
1388
+ container.removeEventListener("pointermove", onPointerMove);
1389
+ container.removeEventListener("pointerup", onPointerEnd);
1390
+ container.removeEventListener("pointercancel", onPointerEnd);
1391
+ };
1392
+ }
1393
+ __name(attachSeek, "attachSeek");
1394
+ function attachHover(container, audio) {
1395
+ const tooltip = container.querySelector("[data-audioplayer-time-tip]");
1396
+ const onMove = /* @__PURE__ */ __name((e) => {
1397
+ const rect = container.getBoundingClientRect();
1398
+ if (rect.width === 0) return;
1399
+ const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
1400
+ container.style.setProperty(HOVER_X, `${x}px`);
1401
+ container.style.setProperty(HOVER_OPACITY, "1");
1402
+ if (tooltip) {
1403
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
1404
+ const t = x / rect.width * dur;
1405
+ tooltip.textContent = formatTime(t);
1406
+ tooltip.style.setProperty(TOOLTIP_LABEL, "1");
1407
+ }
1408
+ }, "onMove");
1409
+ const onLeave = /* @__PURE__ */ __name(() => {
1410
+ container.style.setProperty(HOVER_OPACITY, "0");
1411
+ if (tooltip) tooltip.style.setProperty(TOOLTIP_LABEL, "0");
1412
+ }, "onLeave");
1413
+ container.addEventListener("pointermove", onMove);
1414
+ container.addEventListener("pointerleave", onLeave);
1415
+ return () => {
1416
+ container.removeEventListener("pointermove", onMove);
1417
+ container.removeEventListener("pointerleave", onLeave);
1418
+ };
1419
+ }
1420
+ __name(attachHover, "attachHover");
1421
+
1422
+ // src/tools/AudioPlayer/utils/dpr.ts
1423
+ var MAX_DPR = 2;
1424
+ var MAX_BACKING_WIDTH = 2048;
1425
+ function getDpr() {
1426
+ if (typeof window === "undefined") return 1;
1427
+ const dpr = window.devicePixelRatio ?? 1;
1428
+ return Math.min(Math.max(dpr, 1), MAX_DPR);
1429
+ }
1430
+ __name(getDpr, "getDpr");
1431
+ function backingWidth(cssWidth, dpr = getDpr()) {
1432
+ return Math.min(Math.round(cssWidth * dpr), MAX_BACKING_WIDTH);
1433
+ }
1434
+ __name(backingWidth, "backingWidth");
1435
+ function backingHeight(cssHeight, dpr = getDpr()) {
1436
+ return Math.round(cssHeight * dpr);
1437
+ }
1438
+ __name(backingHeight, "backingHeight");
1439
+
1440
+ // src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts
1441
+ function resizeCanvas(canvas) {
1442
+ const cssW = canvas.clientWidth;
1443
+ const cssH = canvas.clientHeight;
1444
+ if (cssW === 0 || cssH === 0) return null;
1445
+ const dpr = getDpr();
1446
+ const w = backingWidth(cssW, dpr);
1447
+ const h = backingHeight(cssH, dpr);
1448
+ if (canvas.width !== w) canvas.width = w;
1449
+ if (canvas.height !== h) canvas.height = h;
1450
+ const ctx = canvas.getContext("2d", { alpha: true, desynchronized: true });
1451
+ if (!ctx) return null;
1452
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1453
+ return { ctx, cssW, cssH };
1454
+ }
1455
+ __name(resizeCanvas, "resizeCanvas");
1456
+ function paintPeaks(canvas, peaks, opts) {
1457
+ const sized = resizeCanvas(canvas);
1458
+ if (!sized) return;
1459
+ const { ctx, cssW, cssH } = sized;
1460
+ ctx.clearRect(0, 0, cssW, cssH);
1461
+ if (opts.background) {
1462
+ ctx.fillStyle = opts.background;
1463
+ ctx.fillRect(0, 0, cssW, cssH);
1464
+ }
1465
+ if (peaks.length === 0) return;
1466
+ const step = Math.max(1, opts.barWidth + opts.barGap);
1467
+ const numBars = Math.max(1, Math.floor(cssW / step));
1468
+ const mid = cssH / 2;
1469
+ const minH = opts.minBarHeight ?? 1;
1470
+ ctx.fillStyle = opts.color;
1471
+ for (let i = 0; i < numBars; i++) {
1472
+ const peakIdx = Math.min(peaks.length - 1, Math.floor(i / numBars * peaks.length));
1473
+ const amp = peaks[peakIdx];
1474
+ const h = Math.max(minH, amp * cssH);
1475
+ const x = i * step;
1476
+ ctx.fillRect(x, mid - h / 2, opts.barWidth, h);
1477
+ }
1478
+ }
1479
+ __name(paintPeaks, "paintPeaks");
1480
+ function paintLive(canvas, levels, opts) {
1481
+ const sized = resizeCanvas(canvas);
1482
+ if (!sized) return;
1483
+ const { ctx, cssW, cssH } = sized;
1484
+ ctx.clearRect(0, 0, cssW, cssH);
1485
+ if (levels.length === 0) return;
1486
+ const step = Math.max(1, opts.barWidth + opts.barGap);
1487
+ const numBars = Math.max(1, Math.floor(cssW / step));
1488
+ const mid = cssH / 2;
1489
+ const minH = opts.minBarHeight ?? 1;
1490
+ const usable = Math.floor(levels.length * 0.7);
1491
+ ctx.fillStyle = opts.color;
1492
+ for (let i = 0; i < numBars; i++) {
1493
+ const idx = Math.min(usable - 1, Math.floor(i / numBars * usable));
1494
+ const v = levels[idx] ?? 0;
1495
+ const h = Math.max(minH, v * cssH);
1496
+ ctx.fillRect(i * step, mid - h / 2, opts.barWidth, h);
1497
+ }
1498
+ }
1499
+ __name(paintLive, "paintLive");
1500
+ function LiveWaveform({ height, barWidth, barGap, seekStartsPlayback }) {
1501
+ const audio = usePlayerAudio();
1502
+ const controls = usePlayerControls();
1503
+ const store = usePlayerLevels();
1504
+ const [container, setContainer] = useState(null);
1505
+ const canvasRef = useRef(null);
1506
+ const fgHex = useThemeColor("primary");
1507
+ const mutedHex = useThemeColor("muted-foreground");
1508
+ const colorRef = useRef(fgHex);
1509
+ colorRef.current = fgHex;
1510
+ useElementWidth(container);
1511
+ useAnalyser(audio, store, true);
1512
+ useEffect(() => {
1513
+ const canvas = canvasRef.current;
1514
+ if (!canvas) return;
1515
+ let raf = 0;
1516
+ const tick = /* @__PURE__ */ __name(() => {
1517
+ if (typeof document !== "undefined" && document.hidden) {
1518
+ raf = requestAnimationFrame(tick);
1519
+ return;
1520
+ }
1521
+ paintLive(canvas, store.getCurrent(), {
1522
+ color: colorRef.current,
1523
+ barWidth,
1524
+ barGap,
1525
+ minBarHeight: 1
1526
+ });
1527
+ raf = requestAnimationFrame(tick);
1528
+ }, "tick");
1529
+ raf = requestAnimationFrame(tick);
1530
+ return () => cancelAnimationFrame(raf);
1531
+ }, [store, barWidth, barGap]);
1532
+ useEffect(() => {
1533
+ if (!container) return;
1534
+ const detachSeek = attachSeek(container, audio, {
1535
+ startsPlayback: seekStartsPlayback,
1536
+ onPlayRequest: /* @__PURE__ */ __name(() => void controls.play(), "onPlayRequest")
1537
+ });
1538
+ const detachHover = attachHover(container, audio);
1539
+ return () => {
1540
+ detachSeek();
1541
+ detachHover();
1542
+ };
1543
+ }, [audio, container, controls, seekStartsPlayback]);
1544
+ return /* @__PURE__ */ jsxs(
1545
+ "div",
1546
+ {
1547
+ ref: setContainer,
1548
+ className: "audioplayer-waveform relative w-full select-none cursor-pointer",
1549
+ style: { height },
1550
+ "data-mode": "live",
1551
+ children: [
1552
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "absolute inset-0 h-full w-full", "aria-hidden": "true" }),
1553
+ /* @__PURE__ */ jsx(
1554
+ "div",
1555
+ {
1556
+ className: "audioplayer-hover pointer-events-none absolute top-0 bottom-0 w-px transition-opacity",
1557
+ style: {
1558
+ left: "var(--hp, -10px)",
1559
+ opacity: "var(--ho, 0)",
1560
+ backgroundColor: alpha(mutedHex, 0.5)
1561
+ },
1562
+ "aria-hidden": "true"
1563
+ }
1564
+ ),
1565
+ /* @__PURE__ */ jsx(
1566
+ "div",
1567
+ {
1568
+ "data-audioplayer-time-tip": true,
1569
+ className: "audioplayer-tip pointer-events-none absolute -top-7 -translate-x-1/2 rounded bg-foreground px-1.5 py-0.5 text-[10px] tabular-nums text-background transition-opacity",
1570
+ style: {
1571
+ left: "var(--hp, -100px)",
1572
+ opacity: "var(--ht, 0)"
1573
+ },
1574
+ "aria-hidden": "true"
1575
+ }
1576
+ )
1577
+ ]
1578
+ }
1579
+ );
1580
+ }
1581
+ __name(LiveWaveform, "LiveWaveform");
1582
+ var VAR2 = "--p";
1583
+ function usePlayheadLoop(audio, el, enabled = true) {
1584
+ useEffect(() => {
1585
+ if (!enabled || !el) return;
1586
+ let raf = 0;
1587
+ let lastPct = -1;
1588
+ const writePct = /* @__PURE__ */ __name(() => {
1589
+ const dur = audio.duration;
1590
+ if (!Number.isFinite(dur) || dur <= 0) return;
1591
+ const pct = Math.max(0, Math.min(100, audio.currentTime / dur * 100));
1592
+ if (Math.abs(pct - lastPct) < 0.01) return;
1593
+ lastPct = pct;
1594
+ el.style.setProperty(VAR2, `${pct.toFixed(2)}%`);
1595
+ }, "writePct");
1596
+ const tick = /* @__PURE__ */ __name(() => {
1597
+ if (typeof document !== "undefined" && document.hidden) {
1598
+ raf = 0;
1599
+ return;
1600
+ }
1601
+ writePct();
1602
+ raf = requestAnimationFrame(tick);
1603
+ }, "tick");
1604
+ const start = /* @__PURE__ */ __name(() => {
1605
+ if (raf) return;
1606
+ raf = requestAnimationFrame(tick);
1607
+ }, "start");
1608
+ const stop = /* @__PURE__ */ __name(() => {
1609
+ if (!raf) return;
1610
+ cancelAnimationFrame(raf);
1611
+ raf = 0;
1612
+ writePct();
1613
+ }, "stop");
1614
+ if (!audio.paused) start();
1615
+ const onPlay = /* @__PURE__ */ __name(() => start(), "onPlay");
1616
+ const onPauseOrEnd = /* @__PURE__ */ __name(() => stop(), "onPauseOrEnd");
1617
+ const onSeek = /* @__PURE__ */ __name(() => writePct(), "onSeek");
1618
+ const onVisibility = /* @__PURE__ */ __name(() => {
1619
+ if (document.hidden) stop();
1620
+ else if (!audio.paused) start();
1621
+ }, "onVisibility");
1622
+ audio.addEventListener("play", onPlay);
1623
+ audio.addEventListener("pause", onPauseOrEnd);
1624
+ audio.addEventListener("ended", onPauseOrEnd);
1625
+ audio.addEventListener("seeked", onSeek);
1626
+ audio.addEventListener("timeupdate", writePct);
1627
+ document.addEventListener("visibilitychange", onVisibility);
1628
+ return () => {
1629
+ stop();
1630
+ audio.removeEventListener("play", onPlay);
1631
+ audio.removeEventListener("pause", onPauseOrEnd);
1632
+ audio.removeEventListener("ended", onPauseOrEnd);
1633
+ audio.removeEventListener("seeked", onSeek);
1634
+ audio.removeEventListener("timeupdate", writePct);
1635
+ document.removeEventListener("visibilitychange", onVisibility);
1636
+ };
1637
+ }, [audio, el, enabled]);
1638
+ }
1639
+ __name(usePlayheadLoop, "usePlayheadLoop");
1640
+ function useThemeWatcher(cb) {
1641
+ useEffect(() => {
1642
+ const root = document.documentElement;
1643
+ const obs = new MutationObserver(cb);
1644
+ obs.observe(root, { attributes: true, attributeFilter: ["class", "data-theme"] });
1645
+ const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
1646
+ const onMq = /* @__PURE__ */ __name(() => cb(), "onMq");
1647
+ mq?.addEventListener?.("change", onMq);
1648
+ return () => {
1649
+ obs.disconnect();
1650
+ mq?.removeEventListener?.("change", onMq);
1651
+ };
1652
+ }, [cb]);
1653
+ }
1654
+ __name(useThemeWatcher, "useThemeWatcher");
1655
+ function PeaksWaveform({ peaks, height, barWidth, barGap, seekStartsPlayback }) {
1656
+ const audio = usePlayerAudio();
1657
+ const controls = usePlayerControls();
1658
+ const [container, setContainer] = useState(null);
1659
+ const bgCanvasRef = useRef(null);
1660
+ const fgCanvasRef = useRef(null);
1661
+ const width = useElementWidth(container);
1662
+ const fgHex = useThemeColor("primary");
1663
+ const mutedHex = useThemeColor("muted-foreground");
1664
+ const repaint = useCallback(() => {
1665
+ const bg = bgCanvasRef.current;
1666
+ const fg = fgCanvasRef.current;
1667
+ if (!bg || !fg) return;
1668
+ paintPeaks(bg, peaks, { color: alpha(mutedHex, 0.4), barWidth, barGap, minBarHeight: 1 });
1669
+ paintPeaks(fg, peaks, { color: fgHex, barWidth, barGap, minBarHeight: 1 });
1670
+ }, [peaks, barWidth, barGap, fgHex, mutedHex]);
1671
+ useEffect(repaint, [repaint, width]);
1672
+ useThemeWatcher(repaint);
1673
+ usePlayheadLoop(audio, container, true);
1674
+ useEffect(() => {
1675
+ if (!container) return;
1676
+ const detachSeek = attachSeek(container, audio, {
1677
+ startsPlayback: seekStartsPlayback,
1678
+ onPlayRequest: /* @__PURE__ */ __name(() => void controls.play(), "onPlayRequest")
1679
+ });
1680
+ const detachHover = attachHover(container, audio);
1681
+ return () => {
1682
+ detachSeek();
1683
+ detachHover();
1684
+ };
1685
+ }, [audio, container, controls, seekStartsPlayback]);
1686
+ return /* @__PURE__ */ jsxs(
1687
+ "div",
1688
+ {
1689
+ ref: setContainer,
1690
+ className: "audioplayer-waveform relative w-full select-none cursor-pointer",
1691
+ style: { height, ["--p"]: "0%" },
1692
+ "data-mode": "peaks",
1693
+ children: [
1694
+ /* @__PURE__ */ jsx("canvas", { ref: bgCanvasRef, className: "absolute inset-0 h-full w-full", "aria-hidden": "true" }),
1695
+ /* @__PURE__ */ jsx(
1696
+ "div",
1697
+ {
1698
+ className: "audioplayer-fg-clip absolute inset-0",
1699
+ style: {
1700
+ clipPath: "polygon(0 0, var(--p) 0, var(--p) 100%, 0 100%)",
1701
+ willChange: "clip-path"
1702
+ },
1703
+ children: /* @__PURE__ */ jsx("canvas", { ref: fgCanvasRef, className: "absolute inset-0 h-full w-full", "aria-hidden": "true" })
1704
+ }
1705
+ ),
1706
+ /* @__PURE__ */ jsx(
1707
+ "div",
1708
+ {
1709
+ className: "audioplayer-cursor pointer-events-none absolute top-0 bottom-0 w-px",
1710
+ style: { left: "var(--p)", backgroundColor: alpha(fgHex, 0.7) },
1711
+ "aria-hidden": "true"
1712
+ }
1713
+ ),
1714
+ /* @__PURE__ */ jsx(
1715
+ "div",
1716
+ {
1717
+ className: "audioplayer-hover pointer-events-none absolute top-0 bottom-0 w-px transition-opacity",
1718
+ style: {
1719
+ left: "var(--hp, -10px)",
1720
+ opacity: "var(--ho, 0)",
1721
+ backgroundColor: alpha(mutedHex, 0.5)
1722
+ },
1723
+ "aria-hidden": "true"
1724
+ }
1725
+ ),
1726
+ /* @__PURE__ */ jsx(
1727
+ "div",
1728
+ {
1729
+ "data-audioplayer-time-tip": true,
1730
+ className: "audioplayer-tip pointer-events-none absolute -top-7 -translate-x-1/2 rounded bg-foreground px-1.5 py-0.5 text-[10px] tabular-nums text-background transition-opacity",
1731
+ style: {
1732
+ left: "var(--hp, -100px)",
1733
+ opacity: "var(--ht, 0)"
1734
+ },
1735
+ "aria-hidden": "true"
1736
+ }
1737
+ )
1738
+ ]
1739
+ }
1740
+ );
1741
+ }
1742
+ __name(PeaksWaveform, "PeaksWaveform");
1743
+ function ProgressBar({ height = 4, seekStartsPlayback }) {
1744
+ const audio = usePlayerAudio();
1745
+ const controls = usePlayerControls();
1746
+ const [container, setContainer] = useState(null);
1747
+ const fgHex = useThemeColor("primary");
1748
+ const mutedHex = useThemeColor("muted-foreground");
1749
+ usePlayheadLoop(audio, container, true);
1750
+ useEffect(() => {
1751
+ if (!container) return;
1752
+ const detachSeek = attachSeek(container, audio, {
1753
+ startsPlayback: seekStartsPlayback,
1754
+ onPlayRequest: /* @__PURE__ */ __name(() => void controls.play(), "onPlayRequest")
1755
+ });
1756
+ const detachHover = attachHover(container, audio);
1757
+ return () => {
1758
+ detachSeek();
1759
+ detachHover();
1760
+ };
1761
+ }, [audio, container, controls, seekStartsPlayback]);
1762
+ return /* @__PURE__ */ jsxs(
1763
+ "div",
1764
+ {
1765
+ ref: setContainer,
1766
+ className: "audioplayer-waveform relative w-full select-none cursor-pointer",
1767
+ style: { ["--p"]: "0%" },
1768
+ "data-mode": "progress",
1769
+ children: [
1770
+ /* @__PURE__ */ jsx(
1771
+ "div",
1772
+ {
1773
+ className: "rounded-full",
1774
+ style: { height, backgroundColor: alpha(mutedHex, 0.25) }
1775
+ }
1776
+ ),
1777
+ /* @__PURE__ */ jsx(
1778
+ "div",
1779
+ {
1780
+ className: "absolute inset-y-0 left-0 rounded-full",
1781
+ style: {
1782
+ width: "var(--p)",
1783
+ backgroundColor: fgHex
1784
+ },
1785
+ "aria-hidden": "true"
1786
+ }
1787
+ ),
1788
+ /* @__PURE__ */ jsx(
1789
+ "div",
1790
+ {
1791
+ className: "audioplayer-hover pointer-events-none absolute -top-1 -bottom-1 w-px transition-opacity",
1792
+ style: {
1793
+ left: "var(--hp, -10px)",
1794
+ opacity: "var(--ho, 0)",
1795
+ backgroundColor: alpha(mutedHex, 0.5)
1796
+ },
1797
+ "aria-hidden": "true"
1798
+ }
1799
+ ),
1800
+ /* @__PURE__ */ jsx(
1801
+ "div",
1802
+ {
1803
+ "data-audioplayer-time-tip": true,
1804
+ className: "audioplayer-tip pointer-events-none absolute -top-7 -translate-x-1/2 rounded bg-foreground px-1.5 py-0.5 text-[10px] tabular-nums text-background transition-opacity",
1805
+ style: {
1806
+ left: "var(--hp, -100px)",
1807
+ opacity: "var(--ht, 0)"
1808
+ },
1809
+ "aria-hidden": "true"
1810
+ }
1811
+ )
1812
+ ]
1813
+ }
1814
+ );
1815
+ }
1816
+ __name(ProgressBar, "ProgressBar");
1817
+ function WaveformSkeleton({ height }) {
1818
+ return /* @__PURE__ */ jsxs(
1819
+ "div",
1820
+ {
1821
+ className: "audioplayer-skeleton relative w-full overflow-hidden",
1822
+ style: { height },
1823
+ "aria-hidden": "true",
1824
+ children: [
1825
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-muted-foreground/30" }),
1826
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 audioplayer-shimmer" })
1827
+ ]
1828
+ }
1829
+ );
1830
+ }
1831
+ __name(WaveformSkeleton, "WaveformSkeleton");
1832
+ function Waveform({ config, height, seekStartsPlayback }) {
1833
+ const meta = usePlayerMeta();
1834
+ const triggerRef = useRef(null);
1835
+ const mode = config?.mode ?? "peaks";
1836
+ const barWidth = config?.barWidth ?? 2;
1837
+ const barGap = config?.barGap ?? 1;
1838
+ const peaksEnabled = mode === "peaks";
1839
+ const { peaks, loading, error } = usePeaks({
1840
+ src: meta.src,
1841
+ enabled: peaksEnabled,
1842
+ triggerRef,
1843
+ decodeOnMount: config?.decodeOnMount
1844
+ });
1845
+ if (mode === "none") return null;
1846
+ if (mode === "progress")
1847
+ return /* @__PURE__ */ jsx(ProgressBar, { height: Math.min(height, 6), seekStartsPlayback });
1848
+ if (mode === "bars") return /* @__PURE__ */ jsx(BarsWaveform, { height, barWidth, barGap });
1849
+ if (mode === "live")
1850
+ return /* @__PURE__ */ jsx(
1851
+ LiveWaveform,
1852
+ {
1853
+ height,
1854
+ barWidth,
1855
+ barGap,
1856
+ seekStartsPlayback
1857
+ }
1858
+ );
1859
+ if (loading || !peaks && !error) {
1860
+ return /* @__PURE__ */ jsx("div", { ref: triggerRef, children: /* @__PURE__ */ jsx(WaveformSkeleton, { height }) });
1861
+ }
1862
+ if (!peaks) {
1863
+ return /* @__PURE__ */ jsx(ProgressBar, { height: Math.min(height, 6), seekStartsPlayback });
1864
+ }
1865
+ return /* @__PURE__ */ jsx("div", { ref: triggerRef, children: /* @__PURE__ */ jsx(
1866
+ PeaksWaveform,
1867
+ {
1868
+ peaks,
1869
+ height,
1870
+ barWidth,
1871
+ barGap,
1872
+ seekStartsPlayback
1873
+ }
1874
+ ) });
1875
+ }
1876
+ __name(Waveform, "Waveform");
1877
+ function DefaultLayout({ waveform, reactiveCover, onPrev, onNext, seekStartsPlayback }) {
1878
+ const meta = usePlayerMeta();
1879
+ const cover = /* @__PURE__ */ jsx(Cover, { src: meta.cover, alt: meta.title ? `${meta.title} cover` : "", size: 56 });
1880
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3.5 p-4", children: [
1881
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1882
+ reactiveCover === "subtle" ? /* @__PURE__ */ jsx(ReactivePulse, { enabled: true, children: cover }) : cover,
1883
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1884
+ /* @__PURE__ */ jsx(Title, {}),
1885
+ /* @__PURE__ */ jsx(Artist, {})
1886
+ ] })
1887
+ ] }),
1888
+ /* @__PURE__ */ jsx(
1889
+ Waveform,
1890
+ {
1891
+ config: waveform,
1892
+ height: waveform?.height ?? 40,
1893
+ seekStartsPlayback
1894
+ }
1895
+ ),
1896
+ /* @__PURE__ */ jsx(ErrorState, {}),
1897
+ /* @__PURE__ */ jsx(ControlsRow, { onPrev, onNext, showTime: true })
1898
+ ] });
1899
+ }
1900
+ __name(DefaultLayout, "DefaultLayout");
1901
+ function CompactLayout({ waveform, seekStartsPlayback }) {
1902
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-2", children: [
1903
+ /* @__PURE__ */ jsx(PlayButton, { size: "compact" }),
1904
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
1905
+ Waveform,
1906
+ {
1907
+ config: waveform,
1908
+ height: waveform?.height ?? 24,
1909
+ seekStartsPlayback
1910
+ }
1911
+ ) }),
1912
+ /* @__PURE__ */ jsx(TimeDisplay, {}),
1913
+ /* @__PURE__ */ jsx(VolumeControl, {})
1914
+ ] });
1915
+ }
1916
+ __name(CompactLayout, "CompactLayout");
1917
+ var COMPACT_BREAKPOINT = 480;
1918
+ function PlayerShell({
1919
+ className = "",
1920
+ variant = "auto",
1921
+ waveform,
1922
+ reactiveCover = false,
1923
+ onPrev,
1924
+ onNext,
1925
+ enableKeyboardShortcuts = true,
1926
+ ariaLabel,
1927
+ seekStartsPlayback = true,
1928
+ handleRef
1929
+ }) {
1930
+ const [container, setContainer] = useState(null);
1931
+ const audio = usePlayerAudio();
1932
+ const controls = usePlayerControls();
1933
+ const meta = usePlayerMeta();
1934
+ const width = useElementWidth(container);
1935
+ const isPhone = useIsPhone();
1936
+ const resolvedVariant = variant === "auto" ? isPhone || width > 0 && width < COMPACT_BREAKPOINT ? "compact" : "default" : variant;
1937
+ useMediaSession(audio, meta, controls, onPrev, onNext);
1938
+ const hotkeys = useKeyboardShortcuts({
1939
+ audio,
1940
+ controls,
1941
+ enabled: enableKeyboardShortcuts
1942
+ });
1943
+ const setRootRef = useCallback(
1944
+ (node) => {
1945
+ setContainer(node);
1946
+ hotkeys.ref(node);
1947
+ },
1948
+ [hotkeys]
1949
+ );
1950
+ useImperativeHandle(
1951
+ handleRef,
1952
+ () => ({
1953
+ audio,
1954
+ play: /* @__PURE__ */ __name(() => controls.play(), "play"),
1955
+ pause: /* @__PURE__ */ __name(() => controls.pause(), "pause"),
1956
+ seek: /* @__PURE__ */ __name((s) => controls.seek(s), "seek"),
1957
+ getCurrentTime: /* @__PURE__ */ __name(() => audio.currentTime, "getCurrentTime"),
1958
+ getDuration: /* @__PURE__ */ __name(() => Number.isFinite(audio.duration) ? audio.duration : 0, "getDuration")
1959
+ }),
1960
+ [audio, controls]
1961
+ );
1962
+ useEffect(() => {
1963
+ if (!container || container.hasAttribute("tabindex")) return;
1964
+ container.setAttribute("tabindex", "0");
1965
+ }, [container]);
1966
+ return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 400, children: /* @__PURE__ */ jsx(
1967
+ "div",
1968
+ {
1969
+ ref: setRootRef,
1970
+ role: "group",
1971
+ "aria-label": ariaLabel ?? "Audio player",
1972
+ className: `audioplayer @container/player rounded-lg border border-border/60 bg-card text-foreground shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ${className}`,
1973
+ children: resolvedVariant === "compact" ? /* @__PURE__ */ jsx(CompactLayout, { waveform, seekStartsPlayback }) : /* @__PURE__ */ jsx(
1974
+ DefaultLayout,
1975
+ {
1976
+ waveform,
1977
+ reactiveCover,
1978
+ onPrev,
1979
+ onNext,
1980
+ seekStartsPlayback
1981
+ }
1982
+ )
1983
+ }
1984
+ ) });
1985
+ }
1986
+ __name(PlayerShell, "PlayerShell");
1987
+ var Player = forwardRef(/* @__PURE__ */ __name(function Player2(props, ref) {
1988
+ const {
1989
+ src,
1990
+ peaks,
1991
+ title,
1992
+ artist,
1993
+ album,
1994
+ cover,
1995
+ autoplay,
1996
+ loop,
1997
+ initialVolume,
1998
+ muted,
1999
+ preload,
2000
+ exclusive,
2001
+ onPrev,
2002
+ onNext,
2003
+ onPlay,
2004
+ onPause,
2005
+ onEnded,
2006
+ onError,
2007
+ onTimeUpdate,
2008
+ variant,
2009
+ waveform,
2010
+ reactiveCover,
2011
+ className,
2012
+ ariaLabel,
2013
+ enableKeyboardShortcuts,
2014
+ seekStartsPlayback
2015
+ } = props;
2016
+ return /* @__PURE__ */ jsx(
2017
+ PlayerProvider,
2018
+ {
2019
+ src,
2020
+ peaks,
2021
+ title,
2022
+ artist,
2023
+ album,
2024
+ cover,
2025
+ autoplay,
2026
+ loop,
2027
+ initialVolume,
2028
+ muted,
2029
+ preload,
2030
+ exclusive,
2031
+ onPrev,
2032
+ onNext,
2033
+ onPlay,
2034
+ onPause,
2035
+ onEnded,
2036
+ onError,
2037
+ children: /* @__PURE__ */ jsx(
2038
+ PlayerShell,
2039
+ {
2040
+ className,
2041
+ variant,
2042
+ waveform,
2043
+ reactiveCover,
2044
+ onPrev,
2045
+ onNext,
2046
+ enableKeyboardShortcuts,
2047
+ ariaLabel,
2048
+ seekStartsPlayback,
2049
+ handleRef: ref
2050
+ }
2051
+ )
2052
+ }
2053
+ );
2054
+ }, "Player"));
2055
+ Player.displayName = "AudioPlayer";
2056
+
2057
+ export { Player };
2058
+ //# sourceMappingURL=chunk-FX2QFYWF.mjs.map
2059
+ //# sourceMappingURL=chunk-FX2QFYWF.mjs.map