@react-youtube-jukebox/core 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,649 @@
1
+ import { useState, useSyncExternalStore, useRef, useCallback, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import clsx from 'clsx';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ // src/components/Jukebox.tsx
7
+
8
+ // src/lib/shared.ts
9
+ var DEFAULT_POSITION = "bottom-right";
10
+ var DEFAULT_THEME = "glass";
11
+ var DEFAULT_CHROME = "classic";
12
+ var DEFAULT_OFFSET_PX = 20;
13
+ var DEFAULT_VOLUME = 100;
14
+ var LEVEL_BAR_HEIGHTS = [12, 18, 14];
15
+ var LEVEL_BAR_REST_HEIGHT = 8;
16
+ var LEVEL_BAR_ANIMATION_DELAY_MS = 120;
17
+ function getNextTrackIndex(index, step, totalTracks) {
18
+ if (totalTracks <= 0) {
19
+ return 0;
20
+ }
21
+ return (index + step + totalTracks) % totalTracks;
22
+ }
23
+ function clampVolume(value) {
24
+ return Math.min(Math.max(Math.round(value), 0), 100);
25
+ }
26
+ function normalizeOffset(offset) {
27
+ if (typeof offset === "number") {
28
+ return { x: offset, y: offset };
29
+ }
30
+ if (offset) {
31
+ return { x: offset.x, y: offset.y };
32
+ }
33
+ return { x: DEFAULT_OFFSET_PX, y: DEFAULT_OFFSET_PX };
34
+ }
35
+ function getPositionStyle(position, offset, isPortal) {
36
+ const normalizedOffset = normalizeOffset(offset);
37
+ const style = {
38
+ position: isPortal ? "fixed" : "absolute"
39
+ };
40
+ if (position.includes("top")) {
41
+ style.top = normalizedOffset.y;
42
+ } else {
43
+ style.bottom = normalizedOffset.y;
44
+ }
45
+ if (position.includes("left")) {
46
+ style.left = normalizedOffset.x;
47
+ } else {
48
+ style.right = normalizedOffset.x;
49
+ }
50
+ return style;
51
+ }
52
+
53
+ // src/lib/youtube.ts
54
+ var PLAYER_STATE_ENDED = 0;
55
+ var PLAYER_STATE_PLAYING = 1;
56
+ var PLAYER_STATE_PAUSED = 2;
57
+ var youtubeIframeApiPromise = null;
58
+ function canControlPlayer(player) {
59
+ return player !== null && typeof player.pauseVideo === "function" && typeof player.playVideo === "function";
60
+ }
61
+ function loadYouTubeIframeApi() {
62
+ if (typeof window === "undefined") {
63
+ return Promise.reject(new Error("YouTube iframe API requires a browser."));
64
+ }
65
+ if (window.YT?.Player) {
66
+ return Promise.resolve(window.YT);
67
+ }
68
+ if (youtubeIframeApiPromise) {
69
+ return youtubeIframeApiPromise;
70
+ }
71
+ youtubeIframeApiPromise = new Promise((resolve, reject) => {
72
+ const resetPromise = () => {
73
+ youtubeIframeApiPromise = null;
74
+ };
75
+ const existingScript = document.querySelector(
76
+ 'script[src="https://www.youtube.com/iframe_api"]'
77
+ );
78
+ const script = existingScript ?? document.createElement("script");
79
+ if (!existingScript) {
80
+ script.src = "https://www.youtube.com/iframe_api";
81
+ script.async = true;
82
+ document.head.append(script);
83
+ }
84
+ const handleError = () => {
85
+ resetPromise();
86
+ reject(new Error("Failed to load the YouTube iframe API."));
87
+ };
88
+ script.addEventListener("error", handleError, { once: true });
89
+ const previousReadyHandler = window.onYouTubeIframeAPIReady;
90
+ window.onYouTubeIframeAPIReady = () => {
91
+ previousReadyHandler?.();
92
+ script.removeEventListener("error", handleError);
93
+ if (!window.YT?.Player) {
94
+ resetPromise();
95
+ reject(new Error("YouTube iframe API loaded without a player."));
96
+ return;
97
+ }
98
+ resolve(window.YT);
99
+ };
100
+ });
101
+ return youtubeIframeApiPromise;
102
+ }
103
+
104
+ // src/hooks/useJukeboxPlayer.ts
105
+ function useJukeboxPlayer({
106
+ autoplay,
107
+ tracks
108
+ }) {
109
+ const playerRef = useRef(null);
110
+ const currentIndexRef = useRef(0);
111
+ const isPlayingRef = useRef(false);
112
+ const mutedPreferenceRef = useRef(true);
113
+ const shouldResumePlaybackRef = useRef(autoplay);
114
+ const tracksRef = useRef(tracks);
115
+ const volumeRef = useRef(DEFAULT_VOLUME);
116
+ const [playerMountNode, setPlayerMountNode] = useState(
117
+ null
118
+ );
119
+ const [currentIndex, setCurrentIndex] = useState(0);
120
+ const [isReady, setIsReady] = useState(false);
121
+ const [isPlaying, setIsPlaying] = useState(false);
122
+ const [isMuted, setIsMuted] = useState(true);
123
+ const [volume, setVolumeState] = useState(DEFAULT_VOLUME);
124
+ const trackCount = tracks.length;
125
+ const hasTracks = trackCount > 0;
126
+ const hasMultipleTracks = trackCount > 1;
127
+ const safeCurrentIndex = hasTracks ? Math.min(currentIndex, trackCount - 1) : 0;
128
+ const currentTrack = tracks[safeCurrentIndex];
129
+ const currentVideoId = currentTrack?.videoId;
130
+ const moveTrack = useCallback(
131
+ (step) => {
132
+ if (!hasMultipleTracks) {
133
+ return;
134
+ }
135
+ shouldResumePlaybackRef.current = isPlayingRef.current;
136
+ setCurrentIndex((index) => getNextTrackIndex(index, step, trackCount));
137
+ },
138
+ [hasMultipleTracks, trackCount]
139
+ );
140
+ const pausePlayback = useCallback(() => {
141
+ const player = playerRef.current;
142
+ shouldResumePlaybackRef.current = false;
143
+ if (!canControlPlayer(player)) {
144
+ setIsPlaying(false);
145
+ return;
146
+ }
147
+ player.pauseVideo();
148
+ setIsPlaying(false);
149
+ }, []);
150
+ useEffect(() => {
151
+ currentIndexRef.current = safeCurrentIndex;
152
+ }, [safeCurrentIndex]);
153
+ useEffect(() => {
154
+ tracksRef.current = tracks;
155
+ }, [tracks]);
156
+ useEffect(() => {
157
+ isPlayingRef.current = isPlaying;
158
+ }, [isPlaying]);
159
+ useEffect(() => {
160
+ volumeRef.current = volume;
161
+ }, [volume]);
162
+ useEffect(() => {
163
+ if (!isReady && !isPlayingRef.current) {
164
+ shouldResumePlaybackRef.current = autoplay;
165
+ }
166
+ }, [autoplay, isReady]);
167
+ useEffect(() => {
168
+ if (!playerMountNode || !hasTracks) {
169
+ return;
170
+ }
171
+ let isCancelled = false;
172
+ void loadYouTubeIframeApi().then((YT) => {
173
+ if (isCancelled) {
174
+ return;
175
+ }
176
+ playerRef.current = new YT.Player(playerMountNode, {
177
+ width: "100%",
178
+ height: "100%",
179
+ videoId: tracksRef.current[currentIndexRef.current]?.videoId ?? "",
180
+ playerVars: {
181
+ controls: 1,
182
+ origin: window.location.origin,
183
+ playsinline: 1,
184
+ rel: 0
185
+ },
186
+ events: {
187
+ onReady: () => {
188
+ const player = playerRef.current;
189
+ if (!player) {
190
+ return;
191
+ }
192
+ player.setVolume(volumeRef.current);
193
+ if (mutedPreferenceRef.current) {
194
+ player.mute();
195
+ } else {
196
+ player.unMute();
197
+ }
198
+ setIsMuted(mutedPreferenceRef.current);
199
+ setIsReady(true);
200
+ },
201
+ onStateChange: (event) => {
202
+ if (event.data === PLAYER_STATE_ENDED) {
203
+ const nextTrackCount = tracksRef.current.length;
204
+ if (nextTrackCount <= 1) {
205
+ shouldResumePlaybackRef.current = false;
206
+ setIsPlaying(false);
207
+ return;
208
+ }
209
+ shouldResumePlaybackRef.current = true;
210
+ setCurrentIndex(
211
+ (index) => getNextTrackIndex(index, 1, nextTrackCount)
212
+ );
213
+ return;
214
+ }
215
+ if (event.data === PLAYER_STATE_PLAYING) {
216
+ setIsPlaying(true);
217
+ return;
218
+ }
219
+ if (event.data === PLAYER_STATE_PAUSED) {
220
+ setIsPlaying(false);
221
+ }
222
+ },
223
+ onError: () => {
224
+ shouldResumePlaybackRef.current = false;
225
+ setIsPlaying(false);
226
+ }
227
+ }
228
+ });
229
+ }).catch(() => {
230
+ setIsReady(false);
231
+ setIsPlaying(false);
232
+ });
233
+ return () => {
234
+ isCancelled = true;
235
+ setIsReady(false);
236
+ setIsPlaying(false);
237
+ playerRef.current?.destroy();
238
+ playerRef.current = null;
239
+ };
240
+ }, [hasTracks, playerMountNode]);
241
+ useEffect(() => {
242
+ const player = playerRef.current;
243
+ if (!isReady || !player || !currentVideoId) {
244
+ return;
245
+ }
246
+ if (shouldResumePlaybackRef.current) {
247
+ player.loadVideoById(currentVideoId);
248
+ return;
249
+ }
250
+ player.cueVideoById(currentVideoId);
251
+ }, [currentVideoId, isReady]);
252
+ return {
253
+ playerMountRef: setPlayerMountNode,
254
+ currentIndex: safeCurrentIndex,
255
+ isMuted,
256
+ isPlaying,
257
+ volume,
258
+ setVolume: useCallback((nextVolume) => {
259
+ const clampedVolume = clampVolume(nextVolume);
260
+ const player = playerRef.current;
261
+ setVolumeState(clampedVolume);
262
+ volumeRef.current = clampedVolume;
263
+ if (clampedVolume === 0) {
264
+ mutedPreferenceRef.current = true;
265
+ setIsMuted(true);
266
+ } else {
267
+ mutedPreferenceRef.current = false;
268
+ setIsMuted(false);
269
+ }
270
+ if (!player) {
271
+ return;
272
+ }
273
+ player.setVolume(clampedVolume);
274
+ if (clampedVolume === 0) {
275
+ player.mute();
276
+ return;
277
+ }
278
+ player.unMute();
279
+ }, []),
280
+ toggleMute: useCallback(() => {
281
+ const player = playerRef.current;
282
+ const nextMuted = !mutedPreferenceRef.current;
283
+ mutedPreferenceRef.current = nextMuted;
284
+ setIsMuted(nextMuted);
285
+ if (!player) {
286
+ return;
287
+ }
288
+ if (nextMuted) {
289
+ player.mute();
290
+ return;
291
+ }
292
+ player.unMute();
293
+ }, []),
294
+ togglePlay: useCallback(() => {
295
+ const player = playerRef.current;
296
+ if (!canControlPlayer(player) || !isReady || !hasTracks) {
297
+ return;
298
+ }
299
+ if (isPlayingRef.current) {
300
+ pausePlayback();
301
+ return;
302
+ }
303
+ shouldResumePlaybackRef.current = true;
304
+ player.playVideo();
305
+ }, [hasTracks, isReady, pausePlayback]),
306
+ playNext: useCallback(() => {
307
+ moveTrack(1);
308
+ }, [moveTrack]),
309
+ playPrev: useCallback(() => {
310
+ moveTrack(-1);
311
+ }, [moveTrack])
312
+ };
313
+ }
314
+ function SpeakerIcon({ isMuted }) {
315
+ if (isMuted) {
316
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: [
317
+ /* @__PURE__ */ jsx("path", { d: "M7.06 3.22a.75.75 0 0 1 1.19.61v8.34a.75.75 0 0 1-1.19.61L4.26 10.5H2.75A.75.75 0 0 1 2 9.75v-3.5c0-.41.34-.75.75-.75h1.51l2.8-2.28Z" }),
318
+ /* @__PURE__ */ jsx("path", { d: "M10.28 5.22a.75.75 0 0 1 1.06 0L12 5.88l.66-.66a.75.75 0 1 1 1.06 1.06l-.66.66.66.66a.75.75 0 1 1-1.06 1.06L12 7.94l-.66.66a.75.75 0 0 1-1.06-1.06l.66-.66-.66-.66a.75.75 0 0 1 0-1.06Z" })
319
+ ] });
320
+ }
321
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: [
322
+ /* @__PURE__ */ jsx("path", { d: "M7.06 3.22a.75.75 0 0 1 1.19.61v8.34a.75.75 0 0 1-1.19.61L4.26 10.5H2.75A.75.75 0 0 1 2 9.75v-3.5c0-.41.34-.75.75-.75h1.51l2.8-2.28Z" }),
323
+ /* @__PURE__ */ jsx("path", { d: "M10.5 5.02a.75.75 0 0 1 1.06 0 3.86 3.86 0 0 1 0 5.46.75.75 0 1 1-1.06-1.06 2.36 2.36 0 0 0 0-3.34.75.75 0 0 1 0-1.06Z" })
324
+ ] });
325
+ }
326
+ function JukeboxExpandedPlayer({
327
+ currentIndex,
328
+ currentTrack,
329
+ isMuted,
330
+ isPlaying,
331
+ nextTrack,
332
+ playerMountRef,
333
+ totalTracks,
334
+ volume,
335
+ playNext,
336
+ playPrev,
337
+ togglePlay,
338
+ toggleMute,
339
+ setVolume
340
+ }) {
341
+ const hasMultipleTracks = totalTracks > 1;
342
+ const hasNextTrack = nextTrack !== void 0;
343
+ const handleVolumeInput = (event) => {
344
+ setVolume(Number(event.target.value));
345
+ };
346
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
347
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__shell", children: [
348
+ /* @__PURE__ */ jsx("div", { className: "rj-expanded__screen-frame", children: /* @__PURE__ */ jsx("div", { className: "rj-expanded__screen", children: /* @__PURE__ */ jsx("div", { ref: playerMountRef, className: "rj-expanded__player" }) }) }),
349
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__meta", children: [
350
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__titles", children: [
351
+ /* @__PURE__ */ jsx("div", { className: "rj-expanded__title", children: currentTrack.title }),
352
+ /* @__PURE__ */ jsx("div", { className: "rj-expanded__artist", children: currentTrack.artist ?? "Unknown artist" })
353
+ ] }),
354
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__controls", children: [
355
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__transport", children: [
356
+ /* @__PURE__ */ jsx(
357
+ "button",
358
+ {
359
+ type: "button",
360
+ onClick: playPrev,
361
+ disabled: !hasMultipleTracks,
362
+ className: "rj-chip-button",
363
+ children: "\u25C0"
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsx(
367
+ "button",
368
+ {
369
+ type: "button",
370
+ onClick: togglePlay,
371
+ className: "rj-chip-button rj-chip-button--primary",
372
+ children: isPlaying ? "Pause" : "Play"
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsx(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ onClick: playNext,
380
+ disabled: !hasMultipleTracks,
381
+ className: "rj-chip-button",
382
+ children: "\u25B6"
383
+ }
384
+ )
385
+ ] }),
386
+ /* @__PURE__ */ jsxs("div", { className: "rj-expanded__utility", children: [
387
+ /* @__PURE__ */ jsx(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ onClick: toggleMute,
392
+ "aria-label": isMuted ? "Unmute" : "Mute",
393
+ className: "rj-icon-button",
394
+ children: /* @__PURE__ */ jsx("span", { className: "rj-icon-button__icon", children: /* @__PURE__ */ jsx(SpeakerIcon, { isMuted }) })
395
+ }
396
+ ),
397
+ /* @__PURE__ */ jsx(
398
+ "input",
399
+ {
400
+ type: "range",
401
+ min: 0,
402
+ max: 100,
403
+ step: 1,
404
+ value: volume,
405
+ onChange: handleVolumeInput,
406
+ "aria-label": "Volume",
407
+ className: "rj-volume"
408
+ }
409
+ ),
410
+ hasMultipleTracks ? /* @__PURE__ */ jsxs("span", { className: "rj-expanded__counter", children: [
411
+ currentIndex + 1,
412
+ " / ",
413
+ totalTracks
414
+ ] }) : null
415
+ ] })
416
+ ] })
417
+ ] })
418
+ ] }),
419
+ hasNextTrack ? /* @__PURE__ */ jsxs("div", { className: "rj-next-track", children: [
420
+ /* @__PURE__ */ jsx("span", { className: "rj-next-track__label", children: "Next" }),
421
+ /* @__PURE__ */ jsx(
422
+ "button",
423
+ {
424
+ type: "button",
425
+ onClick: playNext,
426
+ disabled: !hasMultipleTracks,
427
+ className: "rj-next-track__button",
428
+ children: nextTrack.title
429
+ }
430
+ )
431
+ ] }) : null
432
+ ] });
433
+ }
434
+ function subscribeToClientRender() {
435
+ return () => void 0;
436
+ }
437
+ function getClientRenderSnapshot() {
438
+ return true;
439
+ }
440
+ function getServerRenderSnapshot() {
441
+ return false;
442
+ }
443
+ function VolumeIcon({ isMuted }) {
444
+ if (isMuted) {
445
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: [
446
+ /* @__PURE__ */ jsx("path", { d: "M7.06 3.22a.75.75 0 0 1 1.19.61v8.34a.75.75 0 0 1-1.19.61L4.26 10.5H2.75A.75.75 0 0 1 2 9.75v-3.5c0-.41.34-.75.75-.75h1.51l2.8-2.28Z" }),
447
+ /* @__PURE__ */ jsx("path", { d: "M10.28 5.22a.75.75 0 0 1 1.06 0L12 5.88l.66-.66a.75.75 0 1 1 1.06 1.06l-.66.66.66.66a.75.75 0 1 1-1.06 1.06L12 7.94l-.66.66a.75.75 0 0 1-1.06-1.06l.66-.66-.66-.66a.75.75 0 0 1 0-1.06Z" })
448
+ ] });
449
+ }
450
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: [
451
+ /* @__PURE__ */ jsx("path", { d: "M7.06 3.22a.75.75 0 0 1 1.19.61v8.34a.75.75 0 0 1-1.19.61L4.26 10.5H2.75A.75.75 0 0 1 2 9.75v-3.5c0-.41.34-.75.75-.75h1.51l2.8-2.28Z" }),
452
+ /* @__PURE__ */ jsx("path", { d: "M10.5 5.02a.75.75 0 0 1 1.06 0 3.86 3.86 0 0 1 0 5.46.75.75 0 1 1-1.06-1.06 2.36 2.36 0 0 0 0-3.34.75.75 0 0 1 0-1.06Z" }),
453
+ /* @__PURE__ */ jsx("path", { d: "M11.9 3.62a.75.75 0 0 1 1.06 0 5.84 5.84 0 0 1 0 8.76.75.75 0 1 1-1.06-1.06 4.34 4.34 0 0 0 0-6.64.75.75 0 0 1 0-1.06Z" })
454
+ ] });
455
+ }
456
+ function ChevronIcon({ isExpanded }) {
457
+ return /* @__PURE__ */ jsx(
458
+ "svg",
459
+ {
460
+ viewBox: "0 0 16 16",
461
+ fill: "none",
462
+ "aria-hidden": "true",
463
+ className: clsx("rj-chevron__icon", {
464
+ "rj-chevron__icon--expanded": isExpanded
465
+ }),
466
+ children: /* @__PURE__ */ jsx(
467
+ "path",
468
+ {
469
+ d: "M4 6.5 8 10l4-3.5",
470
+ stroke: "currentColor",
471
+ strokeWidth: "1.5",
472
+ strokeLinecap: "round",
473
+ strokeLinejoin: "round"
474
+ }
475
+ )
476
+ }
477
+ );
478
+ }
479
+ function TrackSummary({
480
+ currentTrack,
481
+ isExpanded,
482
+ isPlaying,
483
+ onToggleExpanded
484
+ }) {
485
+ if (!currentTrack) {
486
+ return /* @__PURE__ */ jsx("div", { className: "rj-track-summary rj-track-summary--empty", children: /* @__PURE__ */ jsxs("div", { className: "rj-track-summary__copy", children: [
487
+ /* @__PURE__ */ jsx("div", { className: "rj-track-summary__title", children: "No tracks" }),
488
+ /* @__PURE__ */ jsx("div", { className: "rj-track-summary__artist", children: "Pass at least one YouTube video to start playback." })
489
+ ] }) });
490
+ }
491
+ return /* @__PURE__ */ jsxs(
492
+ "button",
493
+ {
494
+ type: "button",
495
+ onClick: onToggleExpanded,
496
+ "aria-expanded": isExpanded,
497
+ className: "rj-track-summary",
498
+ children: [
499
+ /* @__PURE__ */ jsx("div", { className: "rj-level-meter", children: LEVEL_BAR_HEIGHTS.map((height, index) => {
500
+ const animationDelayMs = index * LEVEL_BAR_ANIMATION_DELAY_MS;
501
+ return /* @__PURE__ */ jsx(
502
+ "div",
503
+ {
504
+ "aria-hidden": "true",
505
+ className: clsx("rj-level-bar", {
506
+ "rj-level-bar--playing": isPlaying
507
+ }),
508
+ style: {
509
+ minHeight: `${LEVEL_BAR_REST_HEIGHT}px`,
510
+ height: `${isPlaying ? height : LEVEL_BAR_REST_HEIGHT}px`,
511
+ animationDelay: `${animationDelayMs}ms`
512
+ }
513
+ },
514
+ `${height}-${index}`
515
+ );
516
+ }) }),
517
+ /* @__PURE__ */ jsxs("div", { className: "rj-track-summary__copy", children: [
518
+ /* @__PURE__ */ jsx("div", { className: "rj-track-summary__title", children: currentTrack.title }),
519
+ /* @__PURE__ */ jsx("div", { className: "rj-track-summary__artist", children: currentTrack.artist ?? "Unknown artist" })
520
+ ] }),
521
+ /* @__PURE__ */ jsx("span", { className: "rj-chevron", children: /* @__PURE__ */ jsx(ChevronIcon, { isExpanded }) })
522
+ ]
523
+ }
524
+ );
525
+ }
526
+ function ExpandedPanel({
527
+ children,
528
+ isExpanded
529
+ }) {
530
+ return /* @__PURE__ */ jsx(
531
+ "div",
532
+ {
533
+ "aria-hidden": !isExpanded,
534
+ className: clsx("rj-expanded", {
535
+ "rj-expanded--hidden": !isExpanded
536
+ }),
537
+ children
538
+ }
539
+ );
540
+ }
541
+ function Jukebox({
542
+ tracks,
543
+ autoplay = true,
544
+ position = DEFAULT_POSITION,
545
+ theme = DEFAULT_THEME,
546
+ chrome = DEFAULT_CHROME,
547
+ offset,
548
+ portal = true,
549
+ className,
550
+ renderExpandedContent
551
+ }) {
552
+ const [isExpanded, setIsExpanded] = useState(false);
553
+ const isMounted = useSyncExternalStore(
554
+ subscribeToClientRender,
555
+ getClientRenderSnapshot,
556
+ getServerRenderSnapshot
557
+ );
558
+ const {
559
+ playerMountRef,
560
+ currentIndex,
561
+ isMuted,
562
+ isPlaying,
563
+ volume,
564
+ setVolume,
565
+ toggleMute,
566
+ togglePlay,
567
+ playNext,
568
+ playPrev
569
+ } = useJukeboxPlayer({ autoplay, tracks });
570
+ const currentTrack = tracks[currentIndex];
571
+ const nextTrack = tracks.length > 1 ? tracks[getNextTrackIndex(currentIndex, 1, tracks.length)] : void 0;
572
+ const effectiveIsExpanded = currentTrack ? isExpanded : false;
573
+ const expandedRenderProps = currentTrack ? {
574
+ currentIndex,
575
+ currentTrack,
576
+ isExpanded: effectiveIsExpanded,
577
+ isMuted,
578
+ isPlaying,
579
+ nextTrack,
580
+ playerMountRef,
581
+ totalTracks: tracks.length,
582
+ volume,
583
+ setVolume,
584
+ toggleMute,
585
+ togglePlay,
586
+ playNext,
587
+ playPrev
588
+ } : void 0;
589
+ const handleToggleExpanded = () => {
590
+ if (!currentTrack) {
591
+ return;
592
+ }
593
+ setIsExpanded((expanded) => !expanded);
594
+ };
595
+ const content = /* @__PURE__ */ jsxs(
596
+ "div",
597
+ {
598
+ className: clsx(
599
+ "rj-root",
600
+ {
601
+ "rj-root--expanded": effectiveIsExpanded,
602
+ "rj-root--portal": portal,
603
+ "rj-root--inline": !portal
604
+ },
605
+ className
606
+ ),
607
+ "data-position": position,
608
+ "data-theme": theme,
609
+ "data-chrome": chrome,
610
+ style: getPositionStyle(position, offset, portal),
611
+ children: [
612
+ expandedRenderProps ? /* @__PURE__ */ jsx(ExpandedPanel, { isExpanded: effectiveIsExpanded, children: renderExpandedContent ? renderExpandedContent(expandedRenderProps) : /* @__PURE__ */ jsx(JukeboxExpandedPlayer, { ...expandedRenderProps }) }) : null,
613
+ /* @__PURE__ */ jsx("div", { className: "rj-dock", children: /* @__PURE__ */ jsxs("div", { className: "rj-dock__inner", children: [
614
+ /* @__PURE__ */ jsx(
615
+ TrackSummary,
616
+ {
617
+ currentTrack,
618
+ isExpanded: effectiveIsExpanded,
619
+ isPlaying,
620
+ onToggleExpanded: handleToggleExpanded
621
+ }
622
+ ),
623
+ /* @__PURE__ */ jsx(
624
+ "button",
625
+ {
626
+ type: "button",
627
+ onClick: toggleMute,
628
+ disabled: !currentTrack,
629
+ "aria-label": isMuted ? "Unmute" : "Mute",
630
+ className: "rj-icon-button",
631
+ children: /* @__PURE__ */ jsx("span", { className: "rj-icon-button__icon", children: /* @__PURE__ */ jsx(VolumeIcon, { isMuted }) })
632
+ }
633
+ )
634
+ ] }) })
635
+ ]
636
+ }
637
+ );
638
+ if (!portal) {
639
+ return content;
640
+ }
641
+ if (!isMounted) {
642
+ return null;
643
+ }
644
+ return createPortal(content, document.body);
645
+ }
646
+
647
+ export { Jukebox };
648
+ //# sourceMappingURL=index.js.map
649
+ //# sourceMappingURL=index.js.map