@stargate91/pill-player 0.1.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,1751 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AudioPlaylist: () => AudioPlaylist,
24
+ AudioSeekbar: () => AudioSeekbar,
25
+ AudioTriggerPill: () => AudioTriggerPill,
26
+ CloseIcon: () => CloseIcon,
27
+ DEFAULT_PILL_LABELS: () => DEFAULT_PILL_LABELS,
28
+ ExternalLinkIcon: () => ExternalLinkIcon,
29
+ PauseIcon: () => PauseIcon,
30
+ PillPlayer: () => PillPlayer,
31
+ PlayIcon: () => PlayIcon,
32
+ PlayerControls: () => PlayerControls,
33
+ PlayerHeader: () => PlayerHeader,
34
+ ProviderEmbeds: () => ProviderEmbeds,
35
+ SkipBackIcon: () => SkipBackIcon,
36
+ SkipForwardIcon: () => SkipForwardIcon,
37
+ SpectrumVisualizer: () => SpectrumVisualizer,
38
+ TrackInfo: () => TrackInfo,
39
+ VolumeMuteIcon: () => VolumeMuteIcon,
40
+ VolumeUpIcon: () => VolumeUpIcon,
41
+ calculateBpmTiming: () => calculateBpmTiming,
42
+ extractYouTubeId: () => extractYouTubeId,
43
+ formatTime: () => formatTime,
44
+ parseDurationToSeconds: () => parseDurationToSeconds,
45
+ useAudioPlayer: () => useAudioPlayer
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/components/PillPlayer.tsx
50
+ var import_react7 = require("react");
51
+
52
+ // src/types/index.ts
53
+ var DEFAULT_PILL_LABELS = {
54
+ badge: "AUDIO PLAYER",
55
+ openPlayer: "Open player",
56
+ closePlayer: "Close player",
57
+ play: "Play",
58
+ pause: "Pause",
59
+ prevTrack: "Previous",
60
+ nextTrack: "Next",
61
+ mute: "Mute",
62
+ unmute: "Unmute",
63
+ directPlayHint: "Stream loaded. Use controls to play or pause.",
64
+ listenExternal: "Open stream",
65
+ selectTrack: "PLAYLIST",
66
+ frequenciesActive: "PLAYING",
67
+ frequenciesStandby: "PAUSED",
68
+ spectrumHeader: "EQUALIZER",
69
+ spectrumDsp: "",
70
+ subtitle: "Audio"
71
+ };
72
+
73
+ // src/hooks/useAudioPlayer.ts
74
+ var import_react5 = require("react");
75
+
76
+ // src/utils/youtube.ts
77
+ function extractYouTubeId(urlOrId) {
78
+ if (!urlOrId) return null;
79
+ const trimmed = urlOrId.trim();
80
+ if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed)) {
81
+ return trimmed;
82
+ }
83
+ const match = trimmed.match(
84
+ /(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/))([a-zA-Z0-9_-]{11})/
85
+ );
86
+ return match ? match[1] : null;
87
+ }
88
+
89
+ // src/utils/time.ts
90
+ function formatTime(totalSeconds) {
91
+ if (isNaN(totalSeconds) || totalSeconds <= 0) return "00:00";
92
+ const h = Math.floor(totalSeconds / 3600);
93
+ const m = Math.floor(totalSeconds % 3600 / 60);
94
+ const s = Math.floor(totalSeconds % 60);
95
+ if (h > 0) {
96
+ return h.toString().padStart(2, "0") + ":" + m.toString().padStart(2, "0") + ":" + s.toString().padStart(2, "0");
97
+ }
98
+ return m.toString().padStart(2, "0") + ":" + s.toString().padStart(2, "0");
99
+ }
100
+ function parseDurationToSeconds(duration) {
101
+ if (!duration) return 0;
102
+ const parts = duration.split(":");
103
+ if (parts.length === 2) {
104
+ const min = parseInt(parts[0], 10);
105
+ const sec = parseInt(parts[1], 10);
106
+ if (!isNaN(min) && !isNaN(sec)) return min * 60 + sec;
107
+ }
108
+ if (duration.includes("min")) {
109
+ const parsed = parseInt(duration, 10);
110
+ if (!isNaN(parsed)) return parsed * 60;
111
+ }
112
+ const directNum = parseFloat(duration);
113
+ return !isNaN(directNum) ? directNum : 0;
114
+ }
115
+ function calculateBpmTiming(bpm) {
116
+ const numericBpm = typeof bpm === "number" ? bpm : parseInt(String(bpm || "174").replace(/[^0-9]/g, ""), 10) || 174;
117
+ const beatMs = Math.round(60 / numericBpm * 1e3);
118
+ const halfBeatMs = Math.round(beatMs / 2);
119
+ const twoBeatsMs = Math.round(beatMs * 2);
120
+ const barMs = Math.round(beatMs * 4);
121
+ return { numericBpm, beatMs, halfBeatMs, twoBeatsMs, barMs };
122
+ }
123
+
124
+ // src/engines/html5Engine.ts
125
+ var import_react = require("react");
126
+ function useHtml5Engine(callbacks) {
127
+ const [frequencyData, setFrequencyData] = (0, import_react.useState)(new Array(16).fill(0));
128
+ const audioRef = (0, import_react.useRef)(null);
129
+ const audioContextRef = (0, import_react.useRef)(null);
130
+ const analyserRef = (0, import_react.useRef)(null);
131
+ const animFrameRef = (0, import_react.useRef)(null);
132
+ (0, import_react.useEffect)(() => {
133
+ if (typeof window === "undefined") return;
134
+ const audio = new Audio();
135
+ audio.preload = "metadata";
136
+ audioRef.current = audio;
137
+ audio.ontimeupdate = () => {
138
+ callbacks.onTimeUpdate(audio.currentTime);
139
+ };
140
+ audio.onloadedmetadata = () => {
141
+ callbacks.onDurationChange(audio.duration || 0);
142
+ };
143
+ audio.ondurationchange = () => {
144
+ callbacks.onDurationChange(audio.duration || 0);
145
+ };
146
+ audio.onplay = () => {
147
+ callbacks.onPlay();
148
+ if (audioContextRef.current?.state === "suspended") {
149
+ audioContextRef.current.resume().catch(() => {
150
+ });
151
+ }
152
+ };
153
+ audio.onpause = () => {
154
+ callbacks.onPause();
155
+ setFrequencyData(new Array(16).fill(0));
156
+ };
157
+ audio.onended = () => {
158
+ callbacks.onEnded();
159
+ };
160
+ const updateFrequencies = () => {
161
+ if (analyserRef.current) {
162
+ const bufferLength = analyserRef.current.frequencyBinCount;
163
+ const dataArray = new Uint8Array(bufferLength);
164
+ analyserRef.current.getByteFrequencyData(dataArray);
165
+ const bands = 16;
166
+ const step = Math.max(1, Math.floor(bufferLength / bands));
167
+ const normalized = new Array(bands).fill(0).map((_, i) => {
168
+ const val = dataArray[i * step] || 0;
169
+ return val / 255;
170
+ });
171
+ setFrequencyData(normalized);
172
+ }
173
+ animFrameRef.current = requestAnimationFrame(updateFrequencies);
174
+ };
175
+ animFrameRef.current = requestAnimationFrame(updateFrequencies);
176
+ return () => {
177
+ if (animFrameRef.current) {
178
+ cancelAnimationFrame(animFrameRef.current);
179
+ }
180
+ audio.pause();
181
+ audio.src = "";
182
+ if (audioContextRef.current && audioContextRef.current.state !== "closed") {
183
+ audioContextRef.current.close().catch(() => {
184
+ });
185
+ }
186
+ };
187
+ }, [callbacks]);
188
+ const initAudioContext = (0, import_react.useCallback)(() => {
189
+ if (!audioContextRef.current && audioRef.current && typeof window !== "undefined") {
190
+ try {
191
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
192
+ if (AudioCtx) {
193
+ const ctx = new AudioCtx();
194
+ const source = ctx.createMediaElementSource(audioRef.current);
195
+ const analyser = ctx.createAnalyser();
196
+ analyser.fftSize = 64;
197
+ source.connect(analyser);
198
+ analyser.connect(ctx.destination);
199
+ audioContextRef.current = ctx;
200
+ analyserRef.current = analyser;
201
+ }
202
+ } catch {
203
+ }
204
+ }
205
+ }, []);
206
+ const loadTrack = (0, import_react.useCallback)((src, autoPlay = true) => {
207
+ if (!audioRef.current) return;
208
+ audioRef.current.src = src;
209
+ if (autoPlay) {
210
+ initAudioContext();
211
+ audioRef.current.play().catch(() => {
212
+ });
213
+ }
214
+ }, [initAudioContext]);
215
+ const play = (0, import_react.useCallback)(() => {
216
+ initAudioContext();
217
+ audioRef.current?.play().catch(() => {
218
+ });
219
+ }, [initAudioContext]);
220
+ const pause = (0, import_react.useCallback)(() => {
221
+ audioRef.current?.pause();
222
+ setFrequencyData(new Array(16).fill(0));
223
+ }, []);
224
+ const seek = (0, import_react.useCallback)((seconds) => {
225
+ if (audioRef.current) {
226
+ audioRef.current.currentTime = seconds;
227
+ }
228
+ }, []);
229
+ const setMute = (0, import_react.useCallback)((muted) => {
230
+ if (audioRef.current) {
231
+ audioRef.current.muted = muted;
232
+ }
233
+ }, []);
234
+ return {
235
+ audioRef,
236
+ frequencyData,
237
+ hasLiveFrequency: frequencyData.some((v) => v > 0.05),
238
+ loadTrack,
239
+ play,
240
+ pause,
241
+ seek,
242
+ setMute,
243
+ initAudioContext
244
+ };
245
+ }
246
+
247
+ // src/engines/mixcloudEngine.ts
248
+ var import_react2 = require("react");
249
+ var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
250
+ "https://player-widget.mixcloud.com",
251
+ "https://www.mixcloud.com",
252
+ "https://widget.mixcloud.com"
253
+ ]);
254
+ function useMixcloudEngine(isLoaded, isMixcloud, callbacks) {
255
+ const iframeRef = (0, import_react2.useRef)(null);
256
+ const widgetRef = (0, import_react2.useRef)(null);
257
+ const isWidgetReadyRef = (0, import_react2.useRef)(false);
258
+ const pendingPlayRef = (0, import_react2.useRef)(false);
259
+ const setupMixcloudWidget = (0, import_react2.useCallback)(() => {
260
+ if (!iframeRef.current || typeof window === "undefined" || !window.Mixcloud) return;
261
+ try {
262
+ const widget = window.Mixcloud.PlayerWidget(iframeRef.current);
263
+ widgetRef.current = widget;
264
+ widget.ready.then((resolved) => {
265
+ const active = resolved || widget;
266
+ widgetRef.current = active;
267
+ isWidgetReadyRef.current = true;
268
+ active.events?.play?.on?.(() => {
269
+ callbacks.onPlay();
270
+ active.getDuration?.().then((dur) => {
271
+ if (typeof dur === "number" && dur > 0) callbacks.onDurationChange(dur);
272
+ }).catch(() => {
273
+ });
274
+ });
275
+ active.events?.pause?.on?.(() => callbacks.onPause());
276
+ active.events?.progress?.on?.((pos, dur) => {
277
+ callbacks.onPlay();
278
+ if (typeof pos === "number") callbacks.onTimeUpdate(pos);
279
+ if (typeof dur === "number" && dur > 0) callbacks.onDurationChange(dur);
280
+ });
281
+ active.events?.ended?.on?.(() => callbacks.onEnded());
282
+ if (pendingPlayRef.current) {
283
+ pendingPlayRef.current = false;
284
+ active.play?.().catch(() => {
285
+ });
286
+ }
287
+ }).catch(() => {
288
+ });
289
+ } catch {
290
+ }
291
+ }, [callbacks]);
292
+ (0, import_react2.useEffect)(() => {
293
+ if (typeof window === "undefined" || !isLoaded || !isMixcloud) return;
294
+ if (window.Mixcloud) {
295
+ setupMixcloudWidget();
296
+ return;
297
+ }
298
+ let script = document.getElementById("mixcloud-widget-api");
299
+ if (!script) {
300
+ script = document.createElement("script");
301
+ script.id = "mixcloud-widget-api";
302
+ script.src = "https://widget.mixcloud.com/media/js/widgetApi.js";
303
+ script.async = true;
304
+ document.body.appendChild(script);
305
+ }
306
+ const onLoad = () => setupMixcloudWidget();
307
+ script.addEventListener("load", onLoad);
308
+ return () => script?.removeEventListener("load", onLoad);
309
+ }, [isLoaded, isMixcloud, setupMixcloudWidget]);
310
+ (0, import_react2.useEffect)(() => {
311
+ if (typeof window === "undefined") return;
312
+ const handleMessage = (event) => {
313
+ if (!ALLOWED_ORIGINS.has(event.origin)) return;
314
+ try {
315
+ let data = event.data;
316
+ if (typeof data === "string" && (data.includes("play") || data.includes("pause") || data.includes("ended"))) {
317
+ data = JSON.parse(data);
318
+ }
319
+ if (typeof data === "object" && data !== null) {
320
+ const eventName = data.widgetEvent || data.event || data.type;
321
+ if (eventName === "play") callbacks.onPlay();
322
+ else if (eventName === "pause") callbacks.onPause();
323
+ else if (eventName === "ended") callbacks.onEnded();
324
+ }
325
+ } catch {
326
+ }
327
+ };
328
+ window.addEventListener("message", handleMessage);
329
+ return () => window.removeEventListener("message", handleMessage);
330
+ }, [callbacks]);
331
+ (0, import_react2.useEffect)(() => {
332
+ if (!isLoaded || !isMixcloud) return;
333
+ const interval = setInterval(() => {
334
+ const widget = widgetRef.current;
335
+ if (widget && typeof widget.getIsPaused === "function") {
336
+ widget.getIsPaused().then((isPaused) => {
337
+ if (isPaused) callbacks.onPause();
338
+ else callbacks.onPlay();
339
+ }).catch(() => {
340
+ });
341
+ if (typeof widget.getPosition === "function") {
342
+ widget.getPosition().then((pos) => {
343
+ if (typeof pos === "number") callbacks.onTimeUpdate(pos);
344
+ }).catch(() => {
345
+ });
346
+ }
347
+ }
348
+ }, 800);
349
+ return () => clearInterval(interval);
350
+ }, [isLoaded, isMixcloud, callbacks]);
351
+ const loadTrack = (0, import_react2.useCallback)((feed, autoPlay = true) => {
352
+ const widget = widgetRef.current;
353
+ if (widget?.load) {
354
+ widget.load(feed, autoPlay).catch(() => {
355
+ });
356
+ if (autoPlay) callbacks.onPlay();
357
+ } else {
358
+ pendingPlayRef.current = autoPlay;
359
+ if (autoPlay) callbacks.onPlay();
360
+ }
361
+ }, [callbacks]);
362
+ const play = (0, import_react2.useCallback)(() => {
363
+ const widget = widgetRef.current;
364
+ if (widget?.play) {
365
+ widget.play().catch(() => {
366
+ });
367
+ callbacks.onPlay();
368
+ } else {
369
+ pendingPlayRef.current = true;
370
+ callbacks.onPlay();
371
+ }
372
+ }, [callbacks]);
373
+ const pause = (0, import_react2.useCallback)(() => {
374
+ const widget = widgetRef.current;
375
+ if (widget?.pause) {
376
+ widget.pause().catch(() => {
377
+ });
378
+ callbacks.onPause();
379
+ }
380
+ }, [callbacks]);
381
+ const seek = (0, import_react2.useCallback)((seconds) => {
382
+ const widget = widgetRef.current;
383
+ if (widget && typeof widget.seek === "function") {
384
+ widget.seek(seconds).catch(() => {
385
+ });
386
+ }
387
+ }, []);
388
+ const setMute = (0, import_react2.useCallback)((muted) => {
389
+ const widget = widgetRef.current;
390
+ if (widget?.setVolume) {
391
+ widget.setVolume(muted ? 0 : 1).catch(() => {
392
+ });
393
+ }
394
+ }, []);
395
+ return {
396
+ iframeRef,
397
+ widgetRef,
398
+ setupMixcloudWidget,
399
+ loadTrack,
400
+ play,
401
+ pause,
402
+ seek,
403
+ setMute
404
+ };
405
+ }
406
+
407
+ // src/engines/soundCloudEngine.ts
408
+ var import_react3 = require("react");
409
+ function useSoundCloudEngine(isLoaded, isSoundCloud, callbacks) {
410
+ const scIframeRef = (0, import_react3.useRef)(null);
411
+ const scWidgetRef = (0, import_react3.useRef)(null);
412
+ const pendingPlayRef = (0, import_react3.useRef)(false);
413
+ const setupSoundCloudWidget = (0, import_react3.useCallback)(() => {
414
+ if (!scIframeRef.current || typeof window === "undefined" || !window.SC?.Widget) return;
415
+ try {
416
+ const widget = window.SC.Widget(scIframeRef.current);
417
+ scWidgetRef.current = widget;
418
+ const events = window.SC?.Widget?.Events;
419
+ if (!events) return;
420
+ widget.bind(events.READY, () => {
421
+ widget.getDuration?.((ms) => {
422
+ if (ms) callbacks.onDurationChange(ms / 1e3);
423
+ });
424
+ widget.bind(events.PLAY, () => {
425
+ callbacks.onPlay();
426
+ widget.getDuration?.((ms) => {
427
+ if (ms) callbacks.onDurationChange(ms / 1e3);
428
+ });
429
+ });
430
+ widget.bind(events.PAUSE, () => callbacks.onPause());
431
+ widget.bind(events.FINISH, () => callbacks.onEnded());
432
+ widget.bind(events.PLAY_PROGRESS, (data) => {
433
+ if (data && typeof data.currentPosition === "number") {
434
+ callbacks.onTimeUpdate(data.currentPosition / 1e3);
435
+ }
436
+ if (data && typeof data.relativePosition === "number" && data.relativePosition > 0 && typeof data.currentPosition === "number") {
437
+ const calculatedDuration = data.currentPosition / data.relativePosition / 1e3;
438
+ if (!isNaN(calculatedDuration) && isFinite(calculatedDuration) && calculatedDuration > 0) {
439
+ callbacks.onDurationChange(calculatedDuration);
440
+ }
441
+ }
442
+ });
443
+ if (pendingPlayRef.current) {
444
+ pendingPlayRef.current = false;
445
+ widget.play();
446
+ }
447
+ });
448
+ } catch {
449
+ }
450
+ }, [callbacks]);
451
+ (0, import_react3.useEffect)(() => {
452
+ if (typeof window === "undefined" || !isLoaded || !isSoundCloud) return;
453
+ if (window.SC?.Widget) {
454
+ setupSoundCloudWidget();
455
+ return;
456
+ }
457
+ let script = document.getElementById("soundcloud-widget-api");
458
+ if (!script) {
459
+ script = document.createElement("script");
460
+ script.id = "soundcloud-widget-api";
461
+ script.src = "https://w.soundcloud.com/player/api.js";
462
+ script.async = true;
463
+ document.body.appendChild(script);
464
+ }
465
+ const onLoad = () => setupSoundCloudWidget();
466
+ script.addEventListener("load", onLoad);
467
+ return () => script?.removeEventListener("load", onLoad);
468
+ }, [isLoaded, isSoundCloud, setupSoundCloudWidget]);
469
+ const loadTrack = (0, import_react3.useCallback)((url, autoPlay = true) => {
470
+ const widget = scWidgetRef.current;
471
+ if (widget?.load) {
472
+ widget.load(url, { auto_play: autoPlay });
473
+ if (autoPlay) callbacks.onPlay();
474
+ } else {
475
+ pendingPlayRef.current = autoPlay;
476
+ if (autoPlay) callbacks.onPlay();
477
+ }
478
+ }, [callbacks]);
479
+ const play = (0, import_react3.useCallback)(() => {
480
+ const widget = scWidgetRef.current;
481
+ if (widget?.play) {
482
+ widget.play();
483
+ callbacks.onPlay();
484
+ } else {
485
+ pendingPlayRef.current = true;
486
+ callbacks.onPlay();
487
+ }
488
+ }, [callbacks]);
489
+ const pause = (0, import_react3.useCallback)(() => {
490
+ const widget = scWidgetRef.current;
491
+ if (widget?.pause) {
492
+ widget.pause();
493
+ callbacks.onPause();
494
+ }
495
+ }, [callbacks]);
496
+ const seek = (0, import_react3.useCallback)((seconds) => {
497
+ const widget = scWidgetRef.current;
498
+ if (widget?.seekTo) {
499
+ widget.seekTo(seconds * 1e3);
500
+ }
501
+ }, []);
502
+ const setMute = (0, import_react3.useCallback)((muted) => {
503
+ const widget = scWidgetRef.current;
504
+ if (widget?.setVolume) {
505
+ widget.setVolume(muted ? 0 : 100);
506
+ }
507
+ }, []);
508
+ return {
509
+ scIframeRef,
510
+ scWidgetRef,
511
+ setupSoundCloudWidget,
512
+ loadTrack,
513
+ play,
514
+ pause,
515
+ seek,
516
+ setMute
517
+ };
518
+ }
519
+
520
+ // src/engines/youTubeEngine.ts
521
+ var import_react4 = require("react");
522
+ function useYouTubeEngine(isLoaded, isYouTube, isPlaying, isMuted, callbacks) {
523
+ const ytIframeRef = (0, import_react4.useRef)(null);
524
+ const ytPlayerRef = (0, import_react4.useRef)(null);
525
+ const pendingPlayRef = (0, import_react4.useRef)(false);
526
+ const setupYouTubePlayer = (0, import_react4.useCallback)(() => {
527
+ if (!ytIframeRef.current || typeof window === "undefined" || !window.YT?.Player) return;
528
+ try {
529
+ if (ytPlayerRef.current) return;
530
+ new window.YT.Player(ytIframeRef.current, {
531
+ events: {
532
+ onReady: (event) => {
533
+ ytPlayerRef.current = event.target;
534
+ const dur = event.target.getDuration?.();
535
+ if (dur && typeof dur === "number" && dur > 0) {
536
+ callbacks.onDurationChange(dur);
537
+ }
538
+ if (isMuted) {
539
+ event.target.mute?.();
540
+ }
541
+ if (pendingPlayRef.current) {
542
+ pendingPlayRef.current = false;
543
+ event.target.playVideo?.();
544
+ }
545
+ },
546
+ onStateChange: (event) => {
547
+ if (event.data === 1) {
548
+ callbacks.onPlay();
549
+ const dur = event.target.getDuration?.();
550
+ if (dur && typeof dur === "number" && dur > 0) {
551
+ callbacks.onDurationChange(dur);
552
+ }
553
+ } else if (event.data === 2) {
554
+ callbacks.onPause();
555
+ } else if (event.data === 0) {
556
+ callbacks.onEnded();
557
+ }
558
+ }
559
+ }
560
+ });
561
+ } catch {
562
+ }
563
+ }, [callbacks, isMuted]);
564
+ (0, import_react4.useEffect)(() => {
565
+ if (typeof window === "undefined" || !isLoaded || !isYouTube) return;
566
+ if (window.YT && window.YT.Player) {
567
+ setupYouTubePlayer();
568
+ return;
569
+ }
570
+ let script = document.getElementById("youtube-iframe-api");
571
+ if (!script) {
572
+ script = document.createElement("script");
573
+ script.id = "youtube-iframe-api";
574
+ script.src = "https://www.youtube.com/iframe_api";
575
+ script.async = true;
576
+ document.body.appendChild(script);
577
+ }
578
+ const prevOnReady = window.onYouTubeIframeAPIReady;
579
+ window.onYouTubeIframeAPIReady = () => {
580
+ prevOnReady?.();
581
+ setupYouTubePlayer();
582
+ };
583
+ }, [isLoaded, isYouTube, setupYouTubePlayer]);
584
+ (0, import_react4.useEffect)(() => {
585
+ if (!isYouTube || !isPlaying || !ytPlayerRef.current) return;
586
+ const interval = setInterval(() => {
587
+ const player = ytPlayerRef.current;
588
+ if (player && typeof player.getCurrentTime === "function") {
589
+ try {
590
+ const time = player.getCurrentTime();
591
+ if (typeof time === "number") callbacks.onTimeUpdate(time);
592
+ const dur = player.getDuration?.();
593
+ if (typeof dur === "number" && dur > 0) callbacks.onDurationChange(dur);
594
+ } catch {
595
+ }
596
+ }
597
+ }, 400);
598
+ return () => clearInterval(interval);
599
+ }, [isYouTube, isPlaying, callbacks]);
600
+ const loadTrack = (0, import_react4.useCallback)((ytId, autoPlay = true) => {
601
+ const player = ytPlayerRef.current;
602
+ if (player && typeof player.loadVideoById === "function") {
603
+ player.loadVideoById(ytId);
604
+ if (autoPlay) callbacks.onPlay();
605
+ } else {
606
+ pendingPlayRef.current = autoPlay;
607
+ if (autoPlay) callbacks.onPlay();
608
+ }
609
+ }, [callbacks]);
610
+ const play = (0, import_react4.useCallback)(() => {
611
+ const player = ytPlayerRef.current;
612
+ if (player?.playVideo) {
613
+ player.playVideo();
614
+ callbacks.onPlay();
615
+ } else {
616
+ pendingPlayRef.current = true;
617
+ callbacks.onPlay();
618
+ }
619
+ }, [callbacks]);
620
+ const pause = (0, import_react4.useCallback)(() => {
621
+ const player = ytPlayerRef.current;
622
+ if (player?.pauseVideo) {
623
+ player.pauseVideo();
624
+ callbacks.onPause();
625
+ }
626
+ }, [callbacks]);
627
+ const seek = (0, import_react4.useCallback)((seconds) => {
628
+ ytPlayerRef.current?.seekTo?.(seconds, true);
629
+ }, []);
630
+ const setMute = (0, import_react4.useCallback)((muted) => {
631
+ if (ytPlayerRef.current) {
632
+ if (muted) ytPlayerRef.current.mute?.();
633
+ else ytPlayerRef.current.unMute?.();
634
+ }
635
+ }, []);
636
+ return {
637
+ ytIframeRef,
638
+ ytPlayerRef,
639
+ setupYouTubePlayer,
640
+ loadTrack,
641
+ play,
642
+ pause,
643
+ seek,
644
+ setMute
645
+ };
646
+ }
647
+
648
+ // src/hooks/useAudioPlayer.ts
649
+ var STORAGE_TRACK_KEY = "pill_player_track_index";
650
+ function useAudioPlayer({
651
+ tracks,
652
+ persistState = true,
653
+ onTrackChange,
654
+ onPlayStateChange
655
+ }) {
656
+ const [currentTrackIndex, setCurrentTrackIndex] = (0, import_react5.useState)(0);
657
+ const [isPlaying, setIsPlaying] = (0, import_react5.useState)(false);
658
+ const [isMuted, setIsMuted] = (0, import_react5.useState)(false);
659
+ const [isLoaded, setIsLoaded] = (0, import_react5.useState)(false);
660
+ const [currentTime, setCurTime] = (0, import_react5.useState)(0);
661
+ const [duration, setDur] = (0, import_react5.useState)(0);
662
+ const currentTrack = tracks[currentTrackIndex] || tracks[0];
663
+ const youTubeId = extractYouTubeId(
664
+ currentTrack?.youtube || (currentTrack?.src && currentTrack.src.includes("youtube.com") ? currentTrack.src : void 0)
665
+ );
666
+ const isYouTube = Boolean(youTubeId);
667
+ const soundCloudUrl = currentTrack?.soundcloud || (currentTrack?.src && currentTrack.src.includes("soundcloud.com") ? currentTrack.src : "");
668
+ const isSoundCloud = Boolean(soundCloudUrl);
669
+ const isMixcloud = Boolean(currentTrack?.feed);
670
+ const isHtmlAudio = Boolean(currentTrack?.src && !currentTrack.src.includes("soundcloud.com") && !currentTrack.src.includes("youtube.com"));
671
+ (0, import_react5.useEffect)(() => {
672
+ if (!persistState || typeof window === "undefined") return;
673
+ try {
674
+ const stored = localStorage.getItem(STORAGE_TRACK_KEY);
675
+ if (stored !== null) {
676
+ const parsed = parseInt(stored, 10);
677
+ if (!isNaN(parsed) && parsed >= 0 && parsed < tracks.length) {
678
+ setCurrentTrackIndex(parsed);
679
+ }
680
+ }
681
+ } catch {
682
+ }
683
+ }, [persistState, tracks.length]);
684
+ const nextTrackRef = (0, import_react5.useRef)(() => {
685
+ });
686
+ const callbacks = (0, import_react5.useMemo)(() => ({
687
+ onPlay: () => {
688
+ setIsPlaying(true);
689
+ onPlayStateChange?.(true);
690
+ },
691
+ onPause: () => {
692
+ setIsPlaying(false);
693
+ onPlayStateChange?.(false);
694
+ },
695
+ onTimeUpdate: (time) => {
696
+ setCurTime(time);
697
+ },
698
+ onDurationChange: (dur) => {
699
+ setDur(dur);
700
+ },
701
+ onEnded: () => {
702
+ nextTrackRef.current();
703
+ }
704
+ }), [onPlayStateChange]);
705
+ const html5 = useHtml5Engine(callbacks);
706
+ const mixcloud = useMixcloudEngine(isLoaded, isMixcloud, callbacks);
707
+ const soundCloud = useSoundCloudEngine(isLoaded, isSoundCloud, callbacks);
708
+ const youTube = useYouTubeEngine(isLoaded, isYouTube, isPlaying, isMuted, callbacks);
709
+ const handleSelectTrack = (0, import_react5.useCallback)((index) => {
710
+ if (index < 0 || index >= tracks.length) return;
711
+ setCurrentTrackIndex(index);
712
+ if (persistState && typeof window !== "undefined") {
713
+ try {
714
+ localStorage.setItem(STORAGE_TRACK_KEY, String(index));
715
+ } catch {
716
+ }
717
+ }
718
+ if (!isLoaded) {
719
+ setIsLoaded(true);
720
+ }
721
+ const target = tracks[index];
722
+ if (target) {
723
+ setCurTime(0);
724
+ setDur(parseDurationToSeconds(target.duration));
725
+ onTrackChange?.(target, index);
726
+ const targetYtId = extractYouTubeId(target.youtube);
727
+ const targetScUrl = target.soundcloud || (target.src && target.src.includes("soundcloud.com") ? target.src : "");
728
+ const targetFeed = target.feed;
729
+ const targetSrc = target.src && !target.src.includes("soundcloud.com") && !target.src.includes("youtube.com") ? target.src : "";
730
+ if (!targetSrc) html5.pause();
731
+ if (!targetScUrl) soundCloud.pause();
732
+ if (!targetFeed) mixcloud.pause();
733
+ if (!targetYtId) youTube.pause();
734
+ if (targetYtId) {
735
+ youTube.loadTrack(targetYtId, true);
736
+ } else if (targetScUrl) {
737
+ soundCloud.loadTrack(targetScUrl, true);
738
+ } else if (targetFeed) {
739
+ mixcloud.loadTrack(targetFeed, true);
740
+ } else if (targetSrc) {
741
+ html5.loadTrack(targetSrc, true);
742
+ }
743
+ }
744
+ }, [tracks, isLoaded, persistState, onTrackChange, html5, soundCloud, mixcloud, youTube]);
745
+ const handleNextTrack = (0, import_react5.useCallback)(() => {
746
+ const nextIdx = currentTrackIndex < tracks.length - 1 ? currentTrackIndex + 1 : 0;
747
+ handleSelectTrack(nextIdx);
748
+ }, [currentTrackIndex, tracks.length, handleSelectTrack]);
749
+ const handlePrevTrack = (0, import_react5.useCallback)(() => {
750
+ const prevIdx = currentTrackIndex > 0 ? currentTrackIndex - 1 : tracks.length - 1;
751
+ handleSelectTrack(prevIdx);
752
+ }, [currentTrackIndex, tracks.length, handleSelectTrack]);
753
+ nextTrackRef.current = handleNextTrack;
754
+ const handleTogglePlay = (0, import_react5.useCallback)(() => {
755
+ if (!isLoaded) {
756
+ setIsLoaded(true);
757
+ }
758
+ if (isYouTube) {
759
+ if (isPlaying) youTube.pause();
760
+ else youTube.play();
761
+ return;
762
+ }
763
+ if (isSoundCloud) {
764
+ if (isPlaying) soundCloud.pause();
765
+ else soundCloud.play();
766
+ return;
767
+ }
768
+ if (isMixcloud) {
769
+ if (isPlaying) mixcloud.pause();
770
+ else mixcloud.play();
771
+ return;
772
+ }
773
+ if (isHtmlAudio) {
774
+ if (isPlaying) html5.pause();
775
+ else html5.play();
776
+ }
777
+ }, [isLoaded, isYouTube, isSoundCloud, isMixcloud, isHtmlAudio, isPlaying, youTube, soundCloud, mixcloud, html5]);
778
+ const handleSeek = (0, import_react5.useCallback)((seconds) => {
779
+ setCurTime(seconds);
780
+ if (isYouTube) {
781
+ youTube.seek(seconds);
782
+ return;
783
+ }
784
+ if (isSoundCloud) {
785
+ soundCloud.seek(seconds);
786
+ return;
787
+ }
788
+ if (isMixcloud) {
789
+ mixcloud.seek(seconds);
790
+ return;
791
+ }
792
+ if (isHtmlAudio) {
793
+ html5.seek(seconds);
794
+ }
795
+ }, [isYouTube, isSoundCloud, isMixcloud, isHtmlAudio, youTube, soundCloud, mixcloud, html5]);
796
+ const handleToggleMute = (0, import_react5.useCallback)(() => {
797
+ setIsMuted((prev) => {
798
+ const nextMuted = !prev;
799
+ html5.setMute(nextMuted);
800
+ soundCloud.setMute(nextMuted);
801
+ mixcloud.setMute(nextMuted);
802
+ youTube.setMute(nextMuted);
803
+ return nextMuted;
804
+ });
805
+ }, [html5, soundCloud, mixcloud, youTube]);
806
+ const timing = (0, import_react5.useMemo)(() => calculateBpmTiming(currentTrack?.bpm), [currentTrack?.bpm]);
807
+ const origin = typeof window !== "undefined" && window.location.origin ? encodeURIComponent(window.location.origin) : "";
808
+ const youTubeIframeSrc = youTubeId ? "https://www.youtube-nocookie.com/embed/" + youTubeId + "?enablejsapi=1&origin=" + origin + "&autoplay=0&controls=0&disablekb=1&fs=0&rel=0&playsinline=1" : "";
809
+ const soundCloudIframeSrc = soundCloudUrl ? "https://w.soundcloud.com/player/?url=" + encodeURIComponent(soundCloudUrl) + "&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false&visual=false" : "";
810
+ const iframeSrc = currentTrack?.feed ? "https://player-widget.mixcloud.com/widget/iframe/?feed=" + encodeURIComponent(currentTrack.feed) + "&hide_cover=1&light=0" : "";
811
+ return {
812
+ tracks,
813
+ currentTrack,
814
+ currentTrackIndex,
815
+ isPlaying,
816
+ isMuted,
817
+ isLoaded,
818
+ currentTime,
819
+ duration,
820
+ handleSeek,
821
+ iframeRef: mixcloud.iframeRef,
822
+ scIframeRef: soundCloud.scIframeRef,
823
+ soundCloudIframeSrc,
824
+ isSoundCloud,
825
+ isYouTube,
826
+ ytIframeRef: youTube.ytIframeRef,
827
+ youTubeIframeSrc,
828
+ setupYouTubePlayer: youTube.setupYouTubePlayer,
829
+ isHtmlAudio,
830
+ isMixcloud,
831
+ setupSoundCloudWidget: soundCloud.setupSoundCloudWidget,
832
+ iframeSrc,
833
+ frequencyData: html5.frequencyData,
834
+ hasLiveFrequency: html5.hasLiveFrequency,
835
+ timing,
836
+ setIsLoaded,
837
+ handleTogglePlay,
838
+ handleToggleMute,
839
+ handleNextTrack,
840
+ handlePrevTrack,
841
+ handleSelectTrack,
842
+ setupMixcloudWidget: mixcloud.setupMixcloudWidget
843
+ };
844
+ }
845
+
846
+ // src/components/Icons.tsx
847
+ var import_jsx_runtime = require("react/jsx-runtime");
848
+ function PlayIcon({ size = 16, ...props }) {
849
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
850
+ "svg",
851
+ {
852
+ width: size,
853
+ height: size,
854
+ viewBox: "0 0 24 24",
855
+ fill: "currentColor",
856
+ stroke: "none",
857
+ "aria-hidden": "true",
858
+ ...props,
859
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "6 4 20 12 6 20 6 4" })
860
+ }
861
+ );
862
+ }
863
+ function PauseIcon({ size = 16, ...props }) {
864
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
865
+ "svg",
866
+ {
867
+ width: size,
868
+ height: size,
869
+ viewBox: "0 0 24 24",
870
+ fill: "currentColor",
871
+ stroke: "none",
872
+ "aria-hidden": "true",
873
+ ...props,
874
+ children: [
875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "5", y: "4", width: "4", height: "16", rx: "1" }),
876
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "15", y: "4", width: "4", height: "16", rx: "1" })
877
+ ]
878
+ }
879
+ );
880
+ }
881
+ function SkipBackIcon({ size = 16, ...props }) {
882
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
883
+ "svg",
884
+ {
885
+ width: size,
886
+ height: size,
887
+ viewBox: "0 0 24 24",
888
+ fill: "currentColor",
889
+ stroke: "none",
890
+ "aria-hidden": "true",
891
+ ...props,
892
+ children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "19 20 9 12 19 4 19 20" }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "5", y1: "4", x2: "5", y2: "20", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })
895
+ ]
896
+ }
897
+ );
898
+ }
899
+ function SkipForwardIcon({ size = 16, ...props }) {
900
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
901
+ "svg",
902
+ {
903
+ width: size,
904
+ height: size,
905
+ viewBox: "0 0 24 24",
906
+ fill: "currentColor",
907
+ stroke: "none",
908
+ "aria-hidden": "true",
909
+ ...props,
910
+ children: [
911
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "5 4 15 12 5 20 5 4" }),
912
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "19", y1: "4", x2: "19", y2: "20", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })
913
+ ]
914
+ }
915
+ );
916
+ }
917
+ function VolumeMuteIcon({ size = 16, ...props }) {
918
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
919
+ "svg",
920
+ {
921
+ width: size,
922
+ height: size,
923
+ viewBox: "0 0 24 24",
924
+ fill: "none",
925
+ stroke: "currentColor",
926
+ strokeWidth: "2",
927
+ strokeLinecap: "round",
928
+ strokeLinejoin: "round",
929
+ "aria-hidden": "true",
930
+ ...props,
931
+ children: [
932
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5", fill: "currentColor", stroke: "none" }),
933
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "23", y1: "9", x2: "17", y2: "15" }),
934
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "9", x2: "23", y2: "15" })
935
+ ]
936
+ }
937
+ );
938
+ }
939
+ function VolumeUpIcon({ size = 16, ...props }) {
940
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
941
+ "svg",
942
+ {
943
+ width: size,
944
+ height: size,
945
+ viewBox: "0 0 24 24",
946
+ fill: "none",
947
+ stroke: "currentColor",
948
+ strokeWidth: "2",
949
+ strokeLinecap: "round",
950
+ strokeLinejoin: "round",
951
+ "aria-hidden": "true",
952
+ ...props,
953
+ children: [
954
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 5", fill: "currentColor", stroke: "none" }),
955
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
956
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
957
+ ]
958
+ }
959
+ );
960
+ }
961
+ function CloseIcon({ size = 16, ...props }) {
962
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
963
+ "svg",
964
+ {
965
+ width: size,
966
+ height: size,
967
+ viewBox: "0 0 24 24",
968
+ fill: "none",
969
+ stroke: "currentColor",
970
+ strokeWidth: "2.5",
971
+ strokeLinecap: "round",
972
+ strokeLinejoin: "round",
973
+ "aria-hidden": "true",
974
+ ...props,
975
+ children: [
976
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
978
+ ]
979
+ }
980
+ );
981
+ }
982
+ function ExternalLinkIcon({ size = 16, ...props }) {
983
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
984
+ "svg",
985
+ {
986
+ width: size,
987
+ height: size,
988
+ viewBox: "0 0 24 24",
989
+ fill: "none",
990
+ stroke: "currentColor",
991
+ strokeWidth: "2",
992
+ strokeLinecap: "round",
993
+ strokeLinejoin: "round",
994
+ "aria-hidden": "true",
995
+ ...props,
996
+ children: [
997
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
999
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
1000
+ ]
1001
+ }
1002
+ );
1003
+ }
1004
+
1005
+ // src/components/AudioTriggerPill.tsx
1006
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1007
+ function AudioTriggerPill({
1008
+ isOpen,
1009
+ isPlaying,
1010
+ currentTrack,
1011
+ labels,
1012
+ icons,
1013
+ onToggleOpen
1014
+ }) {
1015
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1016
+ "button",
1017
+ {
1018
+ type: "button",
1019
+ className: "pp-pill-btn",
1020
+ onClick: onToggleOpen,
1021
+ "aria-expanded": isOpen,
1022
+ "aria-label": isOpen ? labels.closePlayer : labels.openPlayer,
1023
+ children: [
1024
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "pp-pill-equalizer", "aria-hidden": "true", children: [
1025
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-bar" }),
1026
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-bar" }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-bar" }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-bar" })
1029
+ ] }),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "pp-pill-meta", children: [
1031
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-code", children: currentTrack?.code || currentTrack?.title || "AUDIO" }),
1032
+ currentTrack?.bpm && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "pp-pill-bpm", children: [
1033
+ "\u2022 ",
1034
+ currentTrack.bpm
1035
+ ] })
1036
+ ] }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "pp-pill-icon", "aria-hidden": "true", children: isPlaying ? icons?.pause ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PauseIcon, { size: 12 }) : icons?.play ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PlayIcon, { size: 12 }) })
1038
+ ]
1039
+ }
1040
+ );
1041
+ }
1042
+
1043
+ // src/components/SpectrumVisualizer.tsx
1044
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1045
+ function SpectrumVisualizer({
1046
+ isPlaying,
1047
+ labels,
1048
+ frequencyData = [],
1049
+ hasLiveFrequency = false
1050
+ }) {
1051
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-visualizer", "aria-hidden": "true", children: [
1052
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-vis-header", children: [
1053
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "pp-vis-title", children: [
1054
+ labels.spectrumHeader,
1055
+ labels.spectrumDsp ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "pp-vis-dsp", children: [
1056
+ " // ",
1057
+ labels.spectrumDsp
1058
+ ] }) : null
1059
+ ] }),
1060
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "pp-vis-status", children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pp-status-led " + (isPlaying ? "pp-status-led-active" : "") }),
1062
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isPlaying ? labels.frequenciesActive : labels.frequenciesStandby })
1063
+ ] })
1064
+ ] }),
1065
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-vis-stage", children: [
1066
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-vis-grid", children: [
1067
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pp-grid-line" }),
1068
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pp-grid-line" }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pp-grid-line" })
1070
+ ] }),
1071
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-vis-bars", children: Array.from({ length: 16 }, (_, i) => {
1072
+ let barStyle;
1073
+ if (!isPlaying) {
1074
+ barStyle = {
1075
+ height: "3px",
1076
+ opacity: 0.25,
1077
+ animation: "none"
1078
+ };
1079
+ } else if (hasLiveFrequency) {
1080
+ const liveVal = frequencyData[i] || 0;
1081
+ barStyle = {
1082
+ height: Math.max(3, Math.round(liveVal * 38)) + "px",
1083
+ opacity: Math.max(0.45, liveVal),
1084
+ animation: "none"
1085
+ };
1086
+ } else {
1087
+ barStyle = void 0;
1088
+ }
1089
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-bar-col", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pp-spectrum-bar", style: barStyle }) }, "bar-" + i);
1090
+ }) })
1091
+ ] }),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-freq-scale", children: [
1093
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "32Hz" }),
1094
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "125Hz" }),
1095
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "500Hz" }),
1096
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "2kHz" }),
1097
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "8kHz" }),
1098
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "16kHz" })
1099
+ ] })
1100
+ ] });
1101
+ }
1102
+
1103
+ // src/components/AudioPlaylist.tsx
1104
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1105
+ function AudioPlaylist({
1106
+ tracks,
1107
+ currentTrackIndex,
1108
+ labels,
1109
+ onSelectTrack
1110
+ }) {
1111
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pp-playlist", children: [
1112
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pp-playlist-header", children: [
1113
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: labels.selectTrack }),
1114
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
1115
+ currentTrackIndex + 1,
1116
+ " / ",
1117
+ tracks.length
1118
+ ] })
1119
+ ] }),
1120
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pp-playlist-list", role: "listbox", "aria-label": labels.selectTrack, children: tracks.map((track, idx) => {
1121
+ const isSelected = idx === currentTrackIndex;
1122
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1123
+ "button",
1124
+ {
1125
+ type: "button",
1126
+ role: "option",
1127
+ "aria-selected": isSelected,
1128
+ className: "pp-track-item " + (isSelected ? "pp-track-item-active" : ""),
1129
+ onClick: () => onSelectTrack(idx),
1130
+ children: [
1131
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pp-track-item-left", children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "pp-track-item-code", children: track.code || "#" + (idx + 1) }),
1133
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "pp-track-item-title", children: track.title })
1134
+ ] }),
1135
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pp-track-item-right", children: [
1136
+ track.bpm && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: track.bpm }),
1137
+ track.duration && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: track.duration })
1138
+ ] })
1139
+ ]
1140
+ },
1141
+ track.id || "track-" + idx
1142
+ );
1143
+ }) })
1144
+ ] });
1145
+ }
1146
+
1147
+ // src/components/AudioSeekbar.tsx
1148
+ var import_react6 = require("react");
1149
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1150
+ function AudioSeekbar({
1151
+ currentTime,
1152
+ duration,
1153
+ onSeek
1154
+ }) {
1155
+ const [isDragging, setIsDragging] = (0, import_react6.useState)(false);
1156
+ const [dragProgress, setDragProgress] = (0, import_react6.useState)(0);
1157
+ const trackRef = (0, import_react6.useRef)(null);
1158
+ const progress = duration > 0 ? Math.min(1, Math.max(0, isDragging ? dragProgress : currentTime / duration)) : 0;
1159
+ const calculateRatio = (0, import_react6.useCallback)((clientX) => {
1160
+ if (!trackRef.current) return 0;
1161
+ const rect = trackRef.current.getBoundingClientRect();
1162
+ const pos = clientX - rect.left;
1163
+ return Math.min(1, Math.max(0, pos / rect.width));
1164
+ }, []);
1165
+ const handlePointerDown = (e) => {
1166
+ e.preventDefault();
1167
+ setIsDragging(true);
1168
+ const ratio = calculateRatio(e.clientX);
1169
+ setDragProgress(ratio);
1170
+ };
1171
+ const handleTouchStart = (e) => {
1172
+ if (e.touches[0]) {
1173
+ setIsDragging(true);
1174
+ const ratio = calculateRatio(e.touches[0].clientX);
1175
+ setDragProgress(ratio);
1176
+ }
1177
+ };
1178
+ const handleKeyDown = (e) => {
1179
+ if (duration <= 0) return;
1180
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
1181
+ e.preventDefault();
1182
+ onSeek(Math.min(duration, currentTime + 5));
1183
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
1184
+ e.preventDefault();
1185
+ onSeek(Math.max(0, currentTime - 5));
1186
+ } else if (e.key === "PageUp") {
1187
+ e.preventDefault();
1188
+ onSeek(Math.min(duration, currentTime + 30));
1189
+ } else if (e.key === "PageDown") {
1190
+ e.preventDefault();
1191
+ onSeek(Math.max(0, currentTime - 30));
1192
+ }
1193
+ };
1194
+ (0, import_react6.useEffect)(() => {
1195
+ if (!isDragging) return;
1196
+ const handleMouseMove = (e) => {
1197
+ const ratio = calculateRatio(e.clientX);
1198
+ setDragProgress(ratio);
1199
+ };
1200
+ const handleTouchMove = (e) => {
1201
+ if (e.touches[0]) {
1202
+ const ratio = calculateRatio(e.touches[0].clientX);
1203
+ setDragProgress(ratio);
1204
+ }
1205
+ };
1206
+ const handleRelease = (e) => {
1207
+ let clientX = 0;
1208
+ if ("clientX" in e) {
1209
+ clientX = e.clientX;
1210
+ } else if (e.changedTouches && e.changedTouches[0]) {
1211
+ clientX = e.changedTouches[0].clientX;
1212
+ }
1213
+ const ratio = calculateRatio(clientX);
1214
+ setIsDragging(false);
1215
+ if (duration > 0) {
1216
+ onSeek(ratio * duration);
1217
+ }
1218
+ };
1219
+ window.addEventListener("mousemove", handleMouseMove);
1220
+ window.addEventListener("mouseup", handleRelease);
1221
+ window.addEventListener("touchmove", handleTouchMove);
1222
+ window.addEventListener("touchend", handleRelease);
1223
+ return () => {
1224
+ window.removeEventListener("mousemove", handleMouseMove);
1225
+ window.removeEventListener("mouseup", handleRelease);
1226
+ window.removeEventListener("touchmove", handleTouchMove);
1227
+ window.removeEventListener("touchend", handleRelease);
1228
+ };
1229
+ }, [isDragging, calculateRatio, duration, onSeek]);
1230
+ const displayTime = isDragging ? dragProgress * duration : currentTime;
1231
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "pp-seekbar-wrapper", "aria-label": "Audio progress", children: [
1232
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pp-time pp-time-current", children: formatTime(displayTime) }),
1233
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1234
+ "div",
1235
+ {
1236
+ ref: trackRef,
1237
+ className: "pp-seekbar-track " + (isDragging ? "pp-seekbar-dragging" : ""),
1238
+ onMouseDown: handlePointerDown,
1239
+ onTouchStart: handleTouchStart,
1240
+ onKeyDown: handleKeyDown,
1241
+ role: "slider",
1242
+ tabIndex: 0,
1243
+ "aria-valuemin": 0,
1244
+ "aria-valuemax": duration || 100,
1245
+ "aria-valuenow": Math.round(displayTime),
1246
+ "aria-valuetext": formatTime(displayTime),
1247
+ children: [
1248
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1249
+ "div",
1250
+ {
1251
+ className: "pp-seekbar-fill",
1252
+ style: { width: progress * 100 + "%" }
1253
+ }
1254
+ ),
1255
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1256
+ "div",
1257
+ {
1258
+ className: "pp-seekbar-thumb",
1259
+ style: { left: progress * 100 + "%" }
1260
+ }
1261
+ )
1262
+ ]
1263
+ }
1264
+ ),
1265
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pp-time pp-time-total", children: formatTime(duration) })
1266
+ ] });
1267
+ }
1268
+
1269
+ // src/components/PlayerHeader.tsx
1270
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1271
+ function PlayerHeader({ labels, icons, onClose }) {
1272
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "pp-card-header", children: [
1273
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "pp-telemetry-badge", children: labels.badge }),
1274
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1275
+ "button",
1276
+ {
1277
+ type: "button",
1278
+ className: "pp-icon-btn",
1279
+ onClick: onClose,
1280
+ "aria-label": labels.closePlayer,
1281
+ title: labels.closePlayer,
1282
+ children: icons?.close ?? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { size: 14 })
1283
+ }
1284
+ )
1285
+ ] });
1286
+ }
1287
+
1288
+ // src/components/TrackInfo.tsx
1289
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1290
+ function TrackInfo({ currentTrack, labels }) {
1291
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "pp-track-info", children: [
1292
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "pp-track-meta", children: [
1293
+ currentTrack?.genre && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "pp-genre-badge", children: currentTrack.genre }),
1294
+ currentTrack?.bpm && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "pp-bpm-badge", children: [
1295
+ "\u2022 ",
1296
+ currentTrack.bpm
1297
+ ] })
1298
+ ] }),
1299
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "pp-track-title", title: currentTrack?.title, children: currentTrack?.title || "No track selected" }),
1300
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "pp-track-subtitle", children: currentTrack?.artist ? `${currentTrack.artist} \u2022 ${labels.subtitle}` : labels.subtitle })
1301
+ ] });
1302
+ }
1303
+
1304
+ // src/components/PlayerControls.tsx
1305
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1306
+ function PlayerControls({
1307
+ isPlaying,
1308
+ isMuted,
1309
+ externalLinkUrl,
1310
+ labels,
1311
+ icons,
1312
+ onPrev,
1313
+ onTogglePlay,
1314
+ onNext,
1315
+ onToggleMute
1316
+ }) {
1317
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "pp-controls-row", children: [
1318
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1319
+ "button",
1320
+ {
1321
+ type: "button",
1322
+ className: "pp-ctrl-btn",
1323
+ onClick: onPrev,
1324
+ "aria-label": labels.prevTrack,
1325
+ title: labels.prevTrack,
1326
+ children: icons?.prev ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SkipBackIcon, { size: 16 })
1327
+ }
1328
+ ),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1330
+ "button",
1331
+ {
1332
+ type: "button",
1333
+ className: "pp-ctrl-btn-main",
1334
+ onClick: onTogglePlay,
1335
+ "aria-label": isPlaying ? labels.pause : labels.play,
1336
+ children: [
1337
+ isPlaying ? icons?.pause ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PauseIcon, { size: 14 }) : icons?.play ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlayIcon, { size: 14 }),
1338
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: isPlaying ? labels.pause : labels.play })
1339
+ ]
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1343
+ "button",
1344
+ {
1345
+ type: "button",
1346
+ className: "pp-ctrl-btn",
1347
+ onClick: onNext,
1348
+ "aria-label": labels.nextTrack,
1349
+ title: labels.nextTrack,
1350
+ children: icons?.next ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SkipForwardIcon, { size: 16 })
1351
+ }
1352
+ ),
1353
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1354
+ "button",
1355
+ {
1356
+ type: "button",
1357
+ className: "pp-ctrl-btn",
1358
+ onClick: onToggleMute,
1359
+ "aria-label": isMuted ? labels.unmute : labels.mute,
1360
+ title: isMuted ? labels.unmute : labels.mute,
1361
+ children: isMuted ? icons?.unmute ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VolumeMuteIcon, { size: 16 }) : icons?.mute ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VolumeUpIcon, { size: 16 })
1362
+ }
1363
+ ),
1364
+ externalLinkUrl && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1365
+ "a",
1366
+ {
1367
+ href: externalLinkUrl,
1368
+ target: "_blank",
1369
+ rel: "noopener noreferrer",
1370
+ className: "pp-ctrl-btn",
1371
+ "aria-label": labels.listenExternal,
1372
+ title: labels.listenExternal,
1373
+ children: icons?.external ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ExternalLinkIcon, { size: 15 })
1374
+ }
1375
+ )
1376
+ ] });
1377
+ }
1378
+
1379
+ // src/components/ProviderEmbeds.tsx
1380
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1381
+ function ProviderEmbeds({
1382
+ isLoaded,
1383
+ embedMode,
1384
+ currentTrack,
1385
+ labels,
1386
+ isSoundCloud,
1387
+ soundCloudIframeSrc,
1388
+ scIframeRef,
1389
+ onSoundCloudLoad,
1390
+ isYouTube,
1391
+ youTubeIframeSrc,
1392
+ ytIframeRef,
1393
+ onYouTubeLoad,
1394
+ iframeSrc,
1395
+ iframeRef,
1396
+ onMixcloudLoad
1397
+ }) {
1398
+ if (!isLoaded || !currentTrack) return null;
1399
+ const title = (currentTrack.code || "") + " " + currentTrack.title;
1400
+ const isSeamless = embedMode === "seamless";
1401
+ const isThemed = embedMode === "themed";
1402
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1403
+ isSoundCloud && soundCloudIframeSrc && (isSeamless ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-seamless", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1404
+ "iframe",
1405
+ {
1406
+ ref: scIframeRef,
1407
+ tabIndex: -1,
1408
+ title,
1409
+ src: soundCloudIframeSrc,
1410
+ allow: "autoplay",
1411
+ onLoad: onSoundCloudLoad
1412
+ }
1413
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1414
+ "iframe",
1415
+ {
1416
+ ref: scIframeRef,
1417
+ title,
1418
+ className: "pp-mixcloud-iframe",
1419
+ src: soundCloudIframeSrc,
1420
+ allow: "autoplay",
1421
+ onLoad: onSoundCloudLoad
1422
+ }
1423
+ ) })),
1424
+ isYouTube && youTubeIframeSrc && (isSeamless ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-seamless", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1425
+ "iframe",
1426
+ {
1427
+ ref: ytIframeRef,
1428
+ tabIndex: -1,
1429
+ title,
1430
+ src: youTubeIframeSrc,
1431
+ allow: "autoplay; encrypted-media",
1432
+ onLoad: onYouTubeLoad
1433
+ }
1434
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-wrapper " + (isThemed ? "pp-iframe-themed" : ""), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1435
+ "iframe",
1436
+ {
1437
+ ref: ytIframeRef,
1438
+ title,
1439
+ className: "pp-mixcloud-iframe",
1440
+ src: youTubeIframeSrc,
1441
+ allow: "autoplay; encrypted-media",
1442
+ onLoad: onYouTubeLoad
1443
+ }
1444
+ ) })),
1445
+ Boolean(currentTrack.feed) && (isSeamless ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-seamless", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1446
+ "iframe",
1447
+ {
1448
+ ref: iframeRef,
1449
+ tabIndex: -1,
1450
+ title,
1451
+ className: "pp-mixcloud-iframe",
1452
+ src: iframeSrc,
1453
+ allow: "autoplay; encrypted-media; fullscreen; picture-in-picture",
1454
+ onLoad: onMixcloudLoad
1455
+ }
1456
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1457
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "pp-iframe-wrapper " + (isThemed ? "pp-iframe-themed" : ""), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1458
+ "iframe",
1459
+ {
1460
+ ref: iframeRef,
1461
+ title,
1462
+ className: "pp-mixcloud-iframe",
1463
+ src: iframeSrc,
1464
+ allow: "autoplay; encrypted-media; fullscreen; picture-in-picture",
1465
+ onLoad: onMixcloudLoad
1466
+ }
1467
+ ) }),
1468
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "pp-iframe-hint", children: labels.directPlayHint })
1469
+ ] }))
1470
+ ] });
1471
+ }
1472
+
1473
+ // src/components/PillPlayer.tsx
1474
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1475
+ function PillPlayer({
1476
+ tracks,
1477
+ theme = "orbital",
1478
+ colors,
1479
+ position = "bottom-right",
1480
+ icons,
1481
+ labels: customLabels,
1482
+ features = {},
1483
+ initialOpen = false,
1484
+ className = "",
1485
+ style,
1486
+ onTrackChange,
1487
+ onPlayStateChange
1488
+ }) {
1489
+ const [isOpen, setIsOpen] = (0, import_react7.useState)(initialOpen);
1490
+ const [asyncTracks, setAsyncTracks] = (0, import_react7.useState)([]);
1491
+ (0, import_react7.useEffect)(() => {
1492
+ if (typeof tracks !== "string") return;
1493
+ let isMounted = true;
1494
+ fetch(tracks).then((res) => {
1495
+ if (!res.ok) throw new Error(`Failed to fetch tracks: ${res.status}`);
1496
+ return res.json();
1497
+ }).then((data) => {
1498
+ if (isMounted && Array.isArray(data)) {
1499
+ setAsyncTracks(data);
1500
+ }
1501
+ }).catch((err) => {
1502
+ console.error("[PillPlayer] Error loading tracks JSON:", err);
1503
+ });
1504
+ return () => {
1505
+ isMounted = false;
1506
+ };
1507
+ }, [tracks]);
1508
+ const resolvedTracks = typeof tracks === "string" ? asyncTracks : tracks;
1509
+ const containerRef = (0, import_react7.useRef)(null);
1510
+ const {
1511
+ showSpectrum = true,
1512
+ showPlaylist = true,
1513
+ showSeekbar = true,
1514
+ persistState = true,
1515
+ enableKeyboardShortcuts = true,
1516
+ embedMode: propEmbedMode,
1517
+ mixcloudEmbedMode: propMixcloudEmbedMode,
1518
+ beaconGlow = false
1519
+ } = features;
1520
+ const embedMode = propEmbedMode ?? propMixcloudEmbedMode ?? "seamless";
1521
+ const labels = {
1522
+ ...DEFAULT_PILL_LABELS,
1523
+ ...customLabels
1524
+ };
1525
+ const {
1526
+ currentTrack,
1527
+ currentTrackIndex,
1528
+ isPlaying,
1529
+ isMuted,
1530
+ isLoaded,
1531
+ currentTime,
1532
+ duration,
1533
+ handleSeek,
1534
+ iframeRef,
1535
+ iframeSrc,
1536
+ scIframeRef,
1537
+ soundCloudIframeSrc,
1538
+ isSoundCloud,
1539
+ isYouTube,
1540
+ ytIframeRef,
1541
+ youTubeIframeSrc,
1542
+ setupYouTubePlayer,
1543
+ isHtmlAudio,
1544
+ setupSoundCloudWidget,
1545
+ frequencyData,
1546
+ hasLiveFrequency,
1547
+ timing,
1548
+ setIsLoaded,
1549
+ handleTogglePlay,
1550
+ handleToggleMute,
1551
+ handleNextTrack,
1552
+ handlePrevTrack,
1553
+ handleSelectTrack,
1554
+ setupMixcloudWidget
1555
+ } = useAudioPlayer({
1556
+ tracks: resolvedTracks,
1557
+ persistState,
1558
+ onTrackChange,
1559
+ onPlayStateChange
1560
+ });
1561
+ const handleToggleOpen = (0, import_react7.useCallback)(() => {
1562
+ setIsOpen((prev) => {
1563
+ const next = !prev;
1564
+ if (next && !isLoaded) {
1565
+ setIsLoaded(true);
1566
+ }
1567
+ return next;
1568
+ });
1569
+ }, [isLoaded, setIsLoaded]);
1570
+ const handleClose = (0, import_react7.useCallback)(() => {
1571
+ setIsOpen(false);
1572
+ }, []);
1573
+ (0, import_react7.useEffect)(() => {
1574
+ if (!enableKeyboardShortcuts || !isOpen) return;
1575
+ const handleKeyDown = (e) => {
1576
+ if (e.key === "Escape") {
1577
+ handleClose();
1578
+ }
1579
+ };
1580
+ window.addEventListener("keydown", handleKeyDown);
1581
+ return () => window.removeEventListener("keydown", handleKeyDown);
1582
+ }, [enableKeyboardShortcuts, isOpen, handleClose]);
1583
+ (0, import_react7.useEffect)(() => {
1584
+ if (!isOpen) return;
1585
+ const handlePointerDownOutside = (e) => {
1586
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
1587
+ handleClose();
1588
+ }
1589
+ };
1590
+ document.addEventListener("pointerdown", handlePointerDownOutside);
1591
+ return () => document.removeEventListener("pointerdown", handlePointerDownOutside);
1592
+ }, [isOpen, handleClose]);
1593
+ const customColorStyles = colors ? {
1594
+ ...colors.accent ? { "--pp-accent": colors.accent } : {},
1595
+ ...colors.secondary ? { "--pp-accent-secondary": colors.secondary } : {},
1596
+ ...colors.bg ? { "--pp-bg": colors.bg } : {},
1597
+ ...colors.surface ? { "--pp-surface": colors.surface } : {},
1598
+ ...colors.border ? { "--pp-border": colors.border } : {},
1599
+ ...colors.text ? { "--pp-text": colors.text } : {},
1600
+ ...colors.textDim ? { "--pp-text-dim": colors.textDim } : {},
1601
+ ...colors.glow ? { "--pp-accent-glow": colors.glow } : {}
1602
+ } : {};
1603
+ const externalLinkUrl = currentTrack?.externalUrl || currentTrack?.soundcloud || (currentTrack?.youtube ? currentTrack.youtube.startsWith("http") ? currentTrack.youtube : "https://www.youtube.com/watch?v=" + currentTrack.youtube : void 0) || (currentTrack?.feed ? "https://www.mixcloud.com" + currentTrack.feed : void 0);
1604
+ const shouldShowSeekbar = showSeekbar && (isHtmlAudio || embedMode === "seamless");
1605
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1606
+ "div",
1607
+ {
1608
+ ref: containerRef,
1609
+ "data-pill-theme": theme,
1610
+ className: "pp-anchor pp-position-" + position + " " + (isPlaying ? "pp-is-playing " : "") + className,
1611
+ style: {
1612
+ ...customColorStyles,
1613
+ ...style,
1614
+ "--bpm-beat": timing.beatMs + "ms",
1615
+ "--bpm-half-beat": timing.halfBeatMs + "ms",
1616
+ "--bpm-two-beats": timing.twoBeatsMs + "ms",
1617
+ "--bpm-bar": timing.barMs + "ms"
1618
+ },
1619
+ "aria-label": labels.badge,
1620
+ children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1622
+ "section",
1623
+ {
1624
+ role: isOpen ? "dialog" : void 0,
1625
+ "aria-modal": isOpen ? "false" : void 0,
1626
+ "aria-hidden": !isOpen,
1627
+ "aria-label": currentTrack ? (currentTrack.code || "") + " - " + currentTrack.title : labels.badge,
1628
+ className: "pp-hud-card " + (isOpen ? "pp-hud-card-open" : "pp-hud-card-closed") + (beaconGlow && isOpen ? " pp-hud-beacon" : ""),
1629
+ children: [
1630
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1631
+ PlayerHeader,
1632
+ {
1633
+ labels,
1634
+ icons,
1635
+ onClose: handleClose
1636
+ }
1637
+ ),
1638
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "pp-card-body", children: [
1639
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1640
+ TrackInfo,
1641
+ {
1642
+ currentTrack,
1643
+ labels
1644
+ }
1645
+ ),
1646
+ showSpectrum && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1647
+ SpectrumVisualizer,
1648
+ {
1649
+ isPlaying,
1650
+ labels,
1651
+ frequencyData,
1652
+ hasLiveFrequency
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1656
+ ProviderEmbeds,
1657
+ {
1658
+ isLoaded,
1659
+ embedMode,
1660
+ currentTrack,
1661
+ labels,
1662
+ isSoundCloud,
1663
+ soundCloudIframeSrc,
1664
+ scIframeRef,
1665
+ onSoundCloudLoad: setupSoundCloudWidget,
1666
+ isYouTube,
1667
+ youTubeIframeSrc,
1668
+ ytIframeRef,
1669
+ onYouTubeLoad: setupYouTubePlayer,
1670
+ iframeSrc,
1671
+ iframeRef,
1672
+ onMixcloudLoad: setupMixcloudWidget
1673
+ }
1674
+ ),
1675
+ shouldShowSeekbar && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1676
+ AudioSeekbar,
1677
+ {
1678
+ currentTime,
1679
+ duration,
1680
+ onSeek: handleSeek
1681
+ }
1682
+ ),
1683
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1684
+ PlayerControls,
1685
+ {
1686
+ isPlaying,
1687
+ isMuted,
1688
+ externalLinkUrl,
1689
+ labels,
1690
+ icons,
1691
+ onPrev: handlePrevTrack,
1692
+ onTogglePlay: handleTogglePlay,
1693
+ onNext: handleNextTrack,
1694
+ onToggleMute: handleToggleMute
1695
+ }
1696
+ ),
1697
+ showPlaylist && resolvedTracks.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1698
+ AudioPlaylist,
1699
+ {
1700
+ tracks: resolvedTracks,
1701
+ currentTrackIndex,
1702
+ labels,
1703
+ onSelectTrack: handleSelectTrack
1704
+ }
1705
+ )
1706
+ ] })
1707
+ ]
1708
+ }
1709
+ ),
1710
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1711
+ AudioTriggerPill,
1712
+ {
1713
+ isOpen,
1714
+ isPlaying,
1715
+ currentTrack,
1716
+ labels,
1717
+ icons,
1718
+ onToggleOpen: handleToggleOpen
1719
+ }
1720
+ )
1721
+ ]
1722
+ }
1723
+ );
1724
+ }
1725
+ // Annotate the CommonJS export names for ESM import in node:
1726
+ 0 && (module.exports = {
1727
+ AudioPlaylist,
1728
+ AudioSeekbar,
1729
+ AudioTriggerPill,
1730
+ CloseIcon,
1731
+ DEFAULT_PILL_LABELS,
1732
+ ExternalLinkIcon,
1733
+ PauseIcon,
1734
+ PillPlayer,
1735
+ PlayIcon,
1736
+ PlayerControls,
1737
+ PlayerHeader,
1738
+ ProviderEmbeds,
1739
+ SkipBackIcon,
1740
+ SkipForwardIcon,
1741
+ SpectrumVisualizer,
1742
+ TrackInfo,
1743
+ VolumeMuteIcon,
1744
+ VolumeUpIcon,
1745
+ calculateBpmTiming,
1746
+ extractYouTubeId,
1747
+ formatTime,
1748
+ parseDurationToSeconds,
1749
+ useAudioPlayer
1750
+ });
1751
+ //# sourceMappingURL=index.js.map