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