wavesurf 1.0.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,707 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var WaveSurfer = require('wavesurfer.js');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var WaveSurfer__default = /*#__PURE__*/_interopDefault(WaveSurfer);
10
+
11
+ // src/context/AudioPlayerContext.tsx
12
+ var MINI_PLAYER_PLAY_EVENT = "wavesurfer-player-mini-play";
13
+ var AudioPlayerContext = react.createContext(null);
14
+ var DEFAULT_CONFIG = {
15
+ fadeInEnabled: true,
16
+ fadeInDuration: 3e3,
17
+ persistVolume: true,
18
+ storageKey: "audioPlayerVolume",
19
+ defaultVolume: 1,
20
+ onPlay: () => {
21
+ },
22
+ onPause: () => {
23
+ },
24
+ onEnd: () => {
25
+ },
26
+ onTimeUpdate: () => {
27
+ }
28
+ };
29
+ var FADE_STEPS = 30;
30
+ var MIN_FADE_IN_VOLUME = 0.1;
31
+ function AudioPlayerProvider({
32
+ children,
33
+ config: userConfig
34
+ }) {
35
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
36
+ const audioRef = react.useRef(null);
37
+ const fadeIntervalRef = react.useRef(null);
38
+ const configRef = react.useRef(config);
39
+ react.useEffect(() => {
40
+ configRef.current = config;
41
+ }, [config]);
42
+ const [state, setState] = react.useState({
43
+ currentSong: null,
44
+ isPlaying: false,
45
+ currentTime: 0,
46
+ duration: 0,
47
+ volume: config.defaultVolume,
48
+ displayVolume: config.defaultVolume,
49
+ isFadingIn: false
50
+ });
51
+ react.useEffect(() => {
52
+ const audio = new Audio();
53
+ audio.preload = "metadata";
54
+ audioRef.current = audio;
55
+ if (config.persistVolume && typeof window !== "undefined") {
56
+ const savedVolume = localStorage.getItem(config.storageKey);
57
+ if (savedVolume) {
58
+ const vol = parseFloat(savedVolume);
59
+ if (!isNaN(vol) && vol >= 0 && vol <= 1) {
60
+ setState((s) => ({ ...s, volume: vol, displayVolume: vol }));
61
+ audio.volume = vol;
62
+ }
63
+ }
64
+ }
65
+ const handleTimeUpdate = () => {
66
+ setState((s) => ({ ...s, currentTime: audio.currentTime }));
67
+ configRef.current.onTimeUpdate?.(audio.currentTime);
68
+ };
69
+ const handleLoadedMetadata = () => {
70
+ setState((s) => ({ ...s, duration: audio.duration }));
71
+ };
72
+ const handleEnded = () => {
73
+ setState((s) => ({ ...s, isPlaying: false, currentTime: 0 }));
74
+ configRef.current.onEnd?.();
75
+ };
76
+ const handlePlay = () => {
77
+ setState((s) => ({ ...s, isPlaying: true }));
78
+ };
79
+ const handlePause = () => {
80
+ setState((s) => ({ ...s, isPlaying: false }));
81
+ };
82
+ audio.addEventListener("timeupdate", handleTimeUpdate);
83
+ audio.addEventListener("loadedmetadata", handleLoadedMetadata);
84
+ audio.addEventListener("ended", handleEnded);
85
+ audio.addEventListener("play", handlePlay);
86
+ audio.addEventListener("pause", handlePause);
87
+ return () => {
88
+ if (fadeIntervalRef.current) {
89
+ clearInterval(fadeIntervalRef.current);
90
+ }
91
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
92
+ audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
93
+ audio.removeEventListener("ended", handleEnded);
94
+ audio.removeEventListener("play", handlePlay);
95
+ audio.removeEventListener("pause", handlePause);
96
+ audio.pause();
97
+ audio.src = "";
98
+ };
99
+ }, []);
100
+ const clearFade = react.useCallback(() => {
101
+ if (fadeIntervalRef.current) {
102
+ clearInterval(fadeIntervalRef.current);
103
+ fadeIntervalRef.current = null;
104
+ }
105
+ }, []);
106
+ const fadeInVolume = react.useCallback(
107
+ (targetVolume) => {
108
+ if (!audioRef.current) return;
109
+ clearFade();
110
+ setState((s) => ({ ...s, isFadingIn: true, displayVolume: 0 }));
111
+ const fadeStepDuration = configRef.current.fadeInDuration / FADE_STEPS;
112
+ const volumeStep = targetVolume / FADE_STEPS;
113
+ let currentStep = 0;
114
+ fadeIntervalRef.current = setInterval(() => {
115
+ currentStep++;
116
+ const newVolume = Math.min(volumeStep * currentStep, targetVolume);
117
+ if (audioRef.current) {
118
+ audioRef.current.volume = newVolume;
119
+ }
120
+ setState((s) => ({ ...s, displayVolume: newVolume }));
121
+ if (currentStep >= FADE_STEPS) {
122
+ clearFade();
123
+ setState((s) => ({
124
+ ...s,
125
+ isFadingIn: false,
126
+ displayVolume: targetVolume
127
+ }));
128
+ }
129
+ }, fadeStepDuration);
130
+ },
131
+ [clearFade]
132
+ );
133
+ const play = react.useCallback(
134
+ async (song) => {
135
+ if (!audioRef.current) return;
136
+ clearFade();
137
+ if (state.currentSong?.id !== song.id) {
138
+ audioRef.current.src = song.audioUrl;
139
+ setState((s) => ({
140
+ ...s,
141
+ currentSong: song,
142
+ currentTime: 0,
143
+ duration: song.duration || 0
144
+ }));
145
+ }
146
+ const targetVolume = Math.max(state.volume, MIN_FADE_IN_VOLUME);
147
+ if (configRef.current.fadeInEnabled) {
148
+ audioRef.current.volume = 0;
149
+ } else {
150
+ audioRef.current.volume = targetVolume;
151
+ }
152
+ try {
153
+ await audioRef.current.play();
154
+ if (typeof window !== "undefined") {
155
+ window.dispatchEvent(
156
+ new CustomEvent(MINI_PLAYER_PLAY_EVENT, { detail: song.id })
157
+ );
158
+ }
159
+ if (configRef.current.fadeInEnabled) {
160
+ fadeInVolume(targetVolume);
161
+ }
162
+ configRef.current.onPlay?.(song);
163
+ } catch {
164
+ }
165
+ },
166
+ [state.currentSong?.id, state.volume, clearFade, fadeInVolume]
167
+ );
168
+ const pause = react.useCallback(() => {
169
+ if (!audioRef.current) return;
170
+ clearFade();
171
+ audioRef.current.pause();
172
+ configRef.current.onPause?.();
173
+ }, [clearFade]);
174
+ const togglePlay = react.useCallback(() => {
175
+ if (!audioRef.current || !state.currentSong) return;
176
+ if (state.isPlaying) {
177
+ pause();
178
+ } else {
179
+ const targetVolume = Math.max(state.volume, MIN_FADE_IN_VOLUME);
180
+ if (configRef.current.fadeInEnabled) {
181
+ audioRef.current.volume = 0;
182
+ }
183
+ audioRef.current.play().then(() => {
184
+ if (typeof window !== "undefined") {
185
+ window.dispatchEvent(
186
+ new CustomEvent(MINI_PLAYER_PLAY_EVENT, {
187
+ detail: state.currentSong?.id
188
+ })
189
+ );
190
+ }
191
+ if (configRef.current.fadeInEnabled) {
192
+ fadeInVolume(targetVolume);
193
+ }
194
+ }).catch(() => {
195
+ });
196
+ }
197
+ }, [state.isPlaying, state.currentSong, state.volume, pause, fadeInVolume]);
198
+ const seek = react.useCallback((time) => {
199
+ if (!audioRef.current) return;
200
+ audioRef.current.currentTime = time;
201
+ setState((s) => ({ ...s, currentTime: time }));
202
+ }, []);
203
+ const setVolume = react.useCallback(
204
+ (volume) => {
205
+ if (!audioRef.current) return;
206
+ const clampedVolume = Math.max(0, Math.min(1, volume));
207
+ if (fadeIntervalRef.current) {
208
+ clearFade();
209
+ setState((s) => ({ ...s, isFadingIn: false }));
210
+ }
211
+ audioRef.current.volume = clampedVolume;
212
+ if (configRef.current.persistVolume && typeof window !== "undefined") {
213
+ localStorage.setItem(
214
+ configRef.current.storageKey,
215
+ clampedVolume.toString()
216
+ );
217
+ }
218
+ setState((s) => ({
219
+ ...s,
220
+ volume: clampedVolume,
221
+ displayVolume: clampedVolume
222
+ }));
223
+ },
224
+ [clearFade]
225
+ );
226
+ const stop = react.useCallback(() => {
227
+ if (!audioRef.current) return;
228
+ clearFade();
229
+ audioRef.current.pause();
230
+ audioRef.current.currentTime = 0;
231
+ audioRef.current.src = "";
232
+ setState((s) => ({
233
+ ...s,
234
+ currentSong: null,
235
+ isPlaying: false,
236
+ currentTime: 0,
237
+ duration: 0,
238
+ isFadingIn: false
239
+ }));
240
+ }, [clearFade]);
241
+ const value = {
242
+ ...state,
243
+ play,
244
+ pause,
245
+ togglePlay,
246
+ seek,
247
+ setVolume,
248
+ stop
249
+ };
250
+ return /* @__PURE__ */ jsxRuntime.jsx(AudioPlayerContext.Provider, { value, children });
251
+ }
252
+ function useAudioPlayer() {
253
+ const context = react.useContext(AudioPlayerContext);
254
+ if (!context) {
255
+ throw new Error(
256
+ "useAudioPlayer must be used within an AudioPlayerProvider"
257
+ );
258
+ }
259
+ return context;
260
+ }
261
+ function useLazyLoad(options) {
262
+ const {
263
+ rootMargin = "100px",
264
+ threshold = 0,
265
+ forceVisible = false
266
+ } = options || {};
267
+ const ref = react.useRef(null);
268
+ const [isVisible, setIsVisible] = react.useState(forceVisible);
269
+ react.useEffect(() => {
270
+ if (forceVisible) {
271
+ setIsVisible(true);
272
+ return;
273
+ }
274
+ if (!ref.current) return;
275
+ const observer = new IntersectionObserver(
276
+ ([entry]) => {
277
+ if (entry.isIntersecting) {
278
+ setIsVisible(true);
279
+ observer.disconnect();
280
+ }
281
+ },
282
+ { rootMargin, threshold }
283
+ );
284
+ observer.observe(ref.current);
285
+ return () => observer.disconnect();
286
+ }, [rootMargin, threshold, forceVisible]);
287
+ return { ref, isVisible };
288
+ }
289
+
290
+ // src/utils/formatTime.ts
291
+ function formatTime(seconds) {
292
+ if (!seconds || isNaN(seconds) || seconds < 0) return "0:00";
293
+ const mins = Math.floor(seconds / 60);
294
+ const secs = Math.floor(seconds % 60);
295
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
296
+ }
297
+ var DEFAULT_WAVEFORM_CONFIG = {
298
+ waveColor: "#666666",
299
+ progressColor: "#D4AF37",
300
+ cursorColor: "#D4AF37",
301
+ barWidth: 2,
302
+ barGap: 1,
303
+ barRadius: 2,
304
+ height: 60,
305
+ normalize: true
306
+ };
307
+ function WaveformPlayer({
308
+ song,
309
+ waveformConfig: userWaveformConfig,
310
+ lazyLoad = true,
311
+ showTime = true,
312
+ className = "",
313
+ renderHeader,
314
+ renderControls
315
+ }) {
316
+ const waveformConfig = { ...DEFAULT_WAVEFORM_CONFIG, ...userWaveformConfig };
317
+ const containerRef = react.useRef(null);
318
+ const wavesurferRef = react.useRef(null);
319
+ const [isReady, setIsReady] = react.useState(false);
320
+ const [totalDuration, setTotalDuration] = react.useState(song.duration || 0);
321
+ const { ref: wrapperRef, isVisible } = useLazyLoad({
322
+ forceVisible: !lazyLoad
323
+ });
324
+ const {
325
+ play: contextPlay,
326
+ togglePlay: contextTogglePlay,
327
+ seek: contextSeek,
328
+ currentSong,
329
+ isPlaying: contextIsPlaying,
330
+ currentTime: contextCurrentTime
331
+ } = useAudioPlayer();
332
+ const isThisSongPlaying = currentSong?.id === song.id;
333
+ const isPlaying = isThisSongPlaying && contextIsPlaying;
334
+ const currentTime = isThisSongPlaying ? contextCurrentTime : 0;
335
+ react.useEffect(() => {
336
+ if (!wavesurferRef.current || !isThisSongPlaying) return;
337
+ const waveDuration = wavesurferRef.current.getDuration();
338
+ if (waveDuration > 0 && contextCurrentTime >= 0) {
339
+ const progress = contextCurrentTime / waveDuration;
340
+ wavesurferRef.current.seekTo(Math.min(progress, 1));
341
+ }
342
+ }, [contextCurrentTime, isThisSongPlaying]);
343
+ react.useEffect(() => {
344
+ if (!containerRef.current || !isVisible) return;
345
+ const hasPeaks = song.peaks && song.peaks.length > 0;
346
+ const wavesurfer = WaveSurfer__default.default.create({
347
+ container: containerRef.current,
348
+ waveColor: waveformConfig.waveColor,
349
+ progressColor: waveformConfig.progressColor,
350
+ cursorColor: waveformConfig.cursorColor,
351
+ barWidth: waveformConfig.barWidth,
352
+ barGap: waveformConfig.barGap,
353
+ barRadius: waveformConfig.barRadius,
354
+ height: waveformConfig.height,
355
+ normalize: waveformConfig.normalize,
356
+ interact: true,
357
+ // Allow clicking on waveform to seek
358
+ // Only load audio URL if we don't have peaks (needed to generate waveform)
359
+ url: hasPeaks ? void 0 : song.audioUrl,
360
+ peaks: hasPeaks ? [song.peaks] : void 0,
361
+ duration: hasPeaks ? song.duration || 0 : void 0
362
+ });
363
+ wavesurfer.on("ready", () => {
364
+ setIsReady(true);
365
+ setTotalDuration(wavesurfer.getDuration() || song.duration || 0);
366
+ });
367
+ wavesurfer.on("interaction", (newTime) => {
368
+ if (isThisSongPlaying) {
369
+ contextSeek(newTime);
370
+ }
371
+ });
372
+ wavesurfer.on("error", () => {
373
+ });
374
+ wavesurferRef.current = wavesurfer;
375
+ if (hasPeaks) {
376
+ setIsReady(true);
377
+ setTotalDuration(song.duration || 0);
378
+ }
379
+ return () => {
380
+ try {
381
+ wavesurfer.destroy();
382
+ } catch {
383
+ }
384
+ };
385
+ }, [
386
+ song.audioUrl,
387
+ song.peaks,
388
+ song.duration,
389
+ isVisible,
390
+ isThisSongPlaying,
391
+ contextSeek,
392
+ waveformConfig.waveColor,
393
+ waveformConfig.progressColor,
394
+ waveformConfig.cursorColor,
395
+ waveformConfig.barWidth,
396
+ waveformConfig.barGap,
397
+ waveformConfig.barRadius,
398
+ waveformConfig.height,
399
+ waveformConfig.normalize
400
+ ]);
401
+ const handlePlayClick = react.useCallback(() => {
402
+ if (!song.id || !song.audioUrl) return;
403
+ if (isThisSongPlaying) {
404
+ contextTogglePlay();
405
+ } else {
406
+ contextPlay(song);
407
+ }
408
+ }, [song, isThisSongPlaying, contextPlay, contextTogglePlay]);
409
+ return /* @__PURE__ */ jsxRuntime.jsxs(
410
+ "div",
411
+ {
412
+ ref: wrapperRef,
413
+ className: `wsp-player ${className}`,
414
+ "data-song-id": song.id,
415
+ children: [
416
+ renderHeader ? renderHeader(song, isPlaying) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-player-header", children: [
417
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "wsp-player-title", children: song.title }),
418
+ song.artist && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "wsp-player-artist", children: song.artist })
419
+ ] }),
420
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-player-controls", children: [
421
+ /* @__PURE__ */ jsxRuntime.jsx(
422
+ "button",
423
+ {
424
+ onClick: handlePlayClick,
425
+ disabled: !isReady,
426
+ className: `wsp-play-button ${isReady ? "wsp-play-button--ready" : ""}`,
427
+ "aria-label": isPlaying ? "Pause" : "Play",
428
+ children: !isReady ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "wsp-icon wsp-icon--spinner", fill: "none", viewBox: "0 0 24 24", children: [
429
+ /* @__PURE__ */ jsxRuntime.jsx(
430
+ "circle",
431
+ {
432
+ className: "wsp-spinner-track",
433
+ cx: "12",
434
+ cy: "12",
435
+ r: "10",
436
+ stroke: "currentColor",
437
+ strokeWidth: "4"
438
+ }
439
+ ),
440
+ /* @__PURE__ */ jsxRuntime.jsx(
441
+ "path",
442
+ {
443
+ className: "wsp-spinner-head",
444
+ fill: "currentColor",
445
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
446
+ }
447
+ )
448
+ ] }) : isPlaying ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "wsp-icon", fill: "currentColor", viewBox: "0 0 24 24", children: [
449
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" }),
450
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" })
451
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "wsp-icon wsp-icon--play", fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" }) })
452
+ }
453
+ ),
454
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-waveform-wrapper", children: [
455
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "wsp-waveform" }),
456
+ showTime && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-time-display", children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "wsp-time", children: formatTime(currentTime) }),
458
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "wsp-time", children: formatTime(totalDuration) })
459
+ ] })
460
+ ] }),
461
+ renderControls && renderControls(song, isPlaying)
462
+ ] })
463
+ ]
464
+ }
465
+ );
466
+ }
467
+ var DEFAULT_WAVEFORM_CONFIG2 = {
468
+ waveColor: "#666666",
469
+ progressColor: "#D4AF37",
470
+ cursorColor: "transparent",
471
+ barWidth: 2,
472
+ barGap: 1,
473
+ barRadius: 2,
474
+ height: 40,
475
+ normalize: true
476
+ };
477
+ function MiniPlayer({
478
+ position = "bottom",
479
+ showVolume = true,
480
+ showClose = true,
481
+ onClose,
482
+ className = "",
483
+ waveformConfig: userWaveformConfig
484
+ }) {
485
+ const waveformConfig = { ...DEFAULT_WAVEFORM_CONFIG2, ...userWaveformConfig };
486
+ const {
487
+ currentSong,
488
+ isPlaying,
489
+ currentTime,
490
+ duration,
491
+ displayVolume,
492
+ togglePlay,
493
+ seek,
494
+ setVolume,
495
+ stop
496
+ } = useAudioPlayer();
497
+ const waveformRef = react.useRef(null);
498
+ const wavesurferRef = react.useRef(null);
499
+ const [isMobile, setIsMobile] = react.useState(false);
500
+ const [waveformReady, setWaveformReady] = react.useState(false);
501
+ const lastSongIdRef = react.useRef(null);
502
+ react.useEffect(() => {
503
+ const checkMobile = () => setIsMobile(window.innerWidth < 640);
504
+ checkMobile();
505
+ window.addEventListener("resize", checkMobile);
506
+ return () => window.removeEventListener("resize", checkMobile);
507
+ }, []);
508
+ react.useEffect(() => {
509
+ if (!waveformRef.current || !currentSong) return;
510
+ if (lastSongIdRef.current === currentSong.id && wavesurferRef.current) {
511
+ return;
512
+ }
513
+ if (wavesurferRef.current) {
514
+ wavesurferRef.current.destroy();
515
+ wavesurferRef.current = null;
516
+ }
517
+ setWaveformReady(false);
518
+ lastSongIdRef.current = currentSong.id;
519
+ const hasPeaks = currentSong.peaks && currentSong.peaks.length > 0;
520
+ const wavesurfer = WaveSurfer__default.default.create({
521
+ container: waveformRef.current,
522
+ waveColor: waveformConfig.waveColor,
523
+ progressColor: waveformConfig.progressColor,
524
+ cursorColor: waveformConfig.cursorColor,
525
+ barWidth: waveformConfig.barWidth,
526
+ barGap: waveformConfig.barGap,
527
+ barRadius: waveformConfig.barRadius,
528
+ height: waveformConfig.height,
529
+ normalize: waveformConfig.normalize,
530
+ interact: true,
531
+ // Use peaks if available, otherwise need audio URL to generate waveform
532
+ url: hasPeaks ? void 0 : currentSong.audioUrl,
533
+ peaks: hasPeaks && currentSong.peaks ? [currentSong.peaks] : void 0,
534
+ duration: hasPeaks ? currentSong.duration || duration || 0 : void 0
535
+ });
536
+ wavesurfer.on("ready", () => {
537
+ setWaveformReady(true);
538
+ });
539
+ wavesurfer.on("interaction", (newTime) => {
540
+ seek(newTime);
541
+ });
542
+ wavesurferRef.current = wavesurfer;
543
+ if (hasPeaks) {
544
+ setWaveformReady(true);
545
+ }
546
+ return () => {
547
+ };
548
+ }, [
549
+ currentSong?.id,
550
+ currentSong?.audioUrl,
551
+ currentSong?.peaks,
552
+ currentSong?.duration,
553
+ duration,
554
+ seek,
555
+ waveformConfig.waveColor,
556
+ waveformConfig.progressColor,
557
+ waveformConfig.cursorColor,
558
+ waveformConfig.barWidth,
559
+ waveformConfig.barGap,
560
+ waveformConfig.barRadius,
561
+ waveformConfig.height,
562
+ waveformConfig.normalize
563
+ ]);
564
+ react.useEffect(() => {
565
+ return () => {
566
+ if (wavesurferRef.current) {
567
+ wavesurferRef.current.destroy();
568
+ wavesurferRef.current = null;
569
+ }
570
+ };
571
+ }, []);
572
+ react.useEffect(() => {
573
+ if (!wavesurferRef.current || !waveformReady) return;
574
+ const waveDuration = wavesurferRef.current.getDuration();
575
+ if (waveDuration > 0 && currentTime >= 0) {
576
+ const progress = currentTime / waveDuration;
577
+ wavesurferRef.current.seekTo(Math.min(progress, 1));
578
+ }
579
+ }, [currentTime, waveformReady]);
580
+ const handleVolumeChange = react.useCallback(
581
+ (e) => {
582
+ setVolume(parseFloat(e.target.value));
583
+ },
584
+ [setVolume]
585
+ );
586
+ const handleClose = react.useCallback(() => {
587
+ stop();
588
+ onClose?.();
589
+ }, [stop, onClose]);
590
+ if (!currentSong) return null;
591
+ const shouldShowVolume = showVolume && !isMobile;
592
+ const positionClass = position === "top" ? "wsp-mini-player--top" : "wsp-mini-player--bottom";
593
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `wsp-mini-player ${positionClass} ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-player-inner", children: [
594
+ /* @__PURE__ */ jsxRuntime.jsx(
595
+ "button",
596
+ {
597
+ onClick: togglePlay,
598
+ className: "wsp-mini-play-button",
599
+ "aria-label": isPlaying ? "Pause" : "Play",
600
+ children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "wsp-mini-icon", fill: "currentColor", viewBox: "0 0 24 24", children: [
601
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" }),
602
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" })
603
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "wsp-mini-icon wsp-mini-icon--play", fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" }) })
604
+ }
605
+ ),
606
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-content", children: [
607
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-info", children: [
608
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "wsp-mini-title", children: currentSong.title }),
609
+ currentSong.album && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-album", children: [
610
+ "\u2022 ",
611
+ currentSong.album
612
+ ] })
613
+ ] }),
614
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-waveform-container", children: [
615
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "wsp-mini-time", children: formatTime(currentTime) }),
616
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: waveformRef, className: "wsp-mini-waveform" }),
617
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "wsp-mini-time", children: formatTime(duration) })
618
+ ] })
619
+ ] }),
620
+ shouldShowVolume && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "wsp-mini-volume", children: [
621
+ /* @__PURE__ */ jsxRuntime.jsx(
622
+ "button",
623
+ {
624
+ onClick: () => setVolume(displayVolume > 0 ? 0 : 1),
625
+ className: "wsp-mini-volume-button",
626
+ "aria-label": displayVolume > 0 ? "Mute" : "Unmute",
627
+ children: displayVolume === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "wsp-mini-volume-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
628
+ /* @__PURE__ */ jsxRuntime.jsx(
629
+ "path",
630
+ {
631
+ strokeLinecap: "round",
632
+ strokeLinejoin: "round",
633
+ strokeWidth: 2,
634
+ d: "M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsxRuntime.jsx(
638
+ "path",
639
+ {
640
+ strokeLinecap: "round",
641
+ strokeLinejoin: "round",
642
+ strokeWidth: 2,
643
+ d: "M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"
644
+ }
645
+ )
646
+ ] }) : displayVolume < 0.5 ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "wsp-mini-volume-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
647
+ "path",
648
+ {
649
+ strokeLinecap: "round",
650
+ strokeLinejoin: "round",
651
+ strokeWidth: 2,
652
+ d: "M15.536 8.464a5 5 0 010 7.072M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
653
+ }
654
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "wsp-mini-volume-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
655
+ "path",
656
+ {
657
+ strokeLinecap: "round",
658
+ strokeLinejoin: "round",
659
+ strokeWidth: 2,
660
+ d: "M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
661
+ }
662
+ ) })
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsxRuntime.jsx(
666
+ "input",
667
+ {
668
+ type: "range",
669
+ min: "0",
670
+ max: "1",
671
+ step: "0.01",
672
+ value: displayVolume,
673
+ onChange: handleVolumeChange,
674
+ className: "wsp-mini-volume-slider",
675
+ "aria-label": "Volume"
676
+ }
677
+ )
678
+ ] }),
679
+ showClose && /* @__PURE__ */ jsxRuntime.jsx(
680
+ "button",
681
+ {
682
+ onClick: handleClose,
683
+ className: "wsp-mini-close-button",
684
+ "aria-label": "Close player",
685
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "wsp-mini-close-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
686
+ "path",
687
+ {
688
+ strokeLinecap: "round",
689
+ strokeLinejoin: "round",
690
+ strokeWidth: 2,
691
+ d: "M6 18L18 6M6 6l12 12"
692
+ }
693
+ ) })
694
+ }
695
+ )
696
+ ] }) });
697
+ }
698
+
699
+ exports.AudioPlayerProvider = AudioPlayerProvider;
700
+ exports.MINI_PLAYER_PLAY_EVENT = MINI_PLAYER_PLAY_EVENT;
701
+ exports.MiniPlayer = MiniPlayer;
702
+ exports.WaveformPlayer = WaveformPlayer;
703
+ exports.formatTime = formatTime;
704
+ exports.useAudioPlayer = useAudioPlayer;
705
+ exports.useLazyLoad = useLazyLoad;
706
+ //# sourceMappingURL=index.js.map
707
+ //# sourceMappingURL=index.js.map