react-modern-audio-player 2.1.0 → 2.3.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.
Files changed (69) hide show
  1. package/README.md +196 -43
  2. package/dist/index.css +1 -1
  3. package/dist/index.es.js +1500 -1217
  4. package/dist/types/api/useAudioPlayer.d.ts +2 -0
  5. package/dist/types/api/useAudioPlayerPlayback.d.ts +2 -0
  6. package/dist/types/components/AudioPlayer/Context/PlaybackContext.d.ts +1 -0
  7. package/dist/types/components/AudioPlayer/Context/StateContext/audio.d.ts +2 -0
  8. package/dist/types/components/AudioPlayer/Context/StateContext/element.d.ts +1 -0
  9. package/dist/types/components/AudioPlayer/Context/StateContext/index.d.ts +3 -1
  10. package/dist/types/components/AudioPlayer/Context/StateContext/placement.d.ts +7 -5
  11. package/dist/types/components/AudioPlayer/Context/UIContext.d.ts +3 -1
  12. package/dist/types/components/AudioPlayer/Context/dispatchContext.d.ts +5 -2
  13. package/dist/types/{hooks/context → components/AudioPlayer/Context/hooks}/index.d.ts +0 -1
  14. package/dist/types/components/AudioPlayer/Context/hooks/useAudioAttrsContext.d.ts +2 -0
  15. package/dist/types/components/AudioPlayer/Context/hooks/usePlaybackContext.d.ts +2 -0
  16. package/dist/types/components/AudioPlayer/Context/hooks/useResourceContext.d.ts +2 -0
  17. package/dist/types/components/AudioPlayer/Context/hooks/useTimeContext.d.ts +2 -0
  18. package/dist/types/components/AudioPlayer/Context/hooks/useTrackContext.d.ts +2 -0
  19. package/dist/types/components/AudioPlayer/Context/hooks/useUIContext.d.ts +1 -0
  20. package/dist/types/components/AudioPlayer/Context/index.d.ts +1 -0
  21. package/dist/types/components/AudioPlayer/Context/providerProps.d.ts +2 -1
  22. package/dist/types/components/AudioPlayer/Interface/Controller/Button/RepeatTypeBtn.d.ts +3 -1
  23. package/dist/types/components/AudioPlayer/Interface/Controller/Button/TransportControls/index.d.ts +4 -0
  24. package/dist/types/components/AudioPlayer/Interface/Controller/Button/index.d.ts +4 -3
  25. package/dist/types/components/AudioPlayer/Interface/Controller/Drawer/SortablePlayList/index.d.ts +5 -1
  26. package/dist/types/components/AudioPlayer/Interface/Controller/Input/Progress/index.d.ts +12 -1
  27. package/dist/types/components/AudioPlayer/Interface/Controller/SpeedSelector/SpeedSelector.d.ts +10 -0
  28. package/dist/types/components/AudioPlayer/Interface/Controller/SpeedSelector/index.d.ts +2 -0
  29. package/dist/types/components/AudioPlayer/Interface/Controller/Tooltip/Volume/index.d.ts +7 -1
  30. package/dist/types/components/AudioPlayer/Interface/Information/Artwork.d.ts +3 -1
  31. package/dist/types/components/AudioPlayer/Interface/Information/TrackInfo.d.ts +3 -1
  32. package/dist/types/components/AudioPlayer/Interface/Information/TrackTime/index.d.ts +3 -1
  33. package/dist/types/components/AudioPlayer/Interface/PlayListEmpty/index.d.ts +6 -0
  34. package/dist/types/components/AudioPlayer/Interface/compound/index.d.ts +3 -0
  35. package/dist/types/components/AudioPlayer/Interface/compound/slotMetaMap.d.ts +10 -0
  36. package/dist/types/components/AudioPlayer/Interface/compound/useCompoundSlots.d.ts +6 -0
  37. package/dist/types/components/AudioPlayer/Interface/compound/useDuplicateSlotWarning.d.ts +7 -0
  38. package/dist/types/components/AudioPlayer/Interface/contexts/index.d.ts +2 -0
  39. package/dist/types/components/AudioPlayer/Interface/contexts/playListEmptyContext.d.ts +2 -0
  40. package/dist/types/components/AudioPlayer/Interface/contexts/playListPortalContext.d.ts +1 -0
  41. package/dist/types/components/AudioPlayer/Interface/hooks/index.d.ts +4 -0
  42. package/dist/types/components/AudioPlayer/Interface/hooks/useDropdownAutoPlacement.d.ts +5 -0
  43. package/dist/types/components/AudioPlayer/Interface/hooks/useGridTemplate.d.ts +3 -0
  44. package/dist/types/components/AudioPlayer/Interface/hooks/useResolvedDropdownProps.d.ts +18 -0
  45. package/dist/types/components/AudioPlayer/Interface/hooks/useResolvedGridArea.d.ts +2 -0
  46. package/dist/types/components/AudioPlayer/Player/index.d.ts +2 -2
  47. package/dist/types/components/AudioPlayer/Provider/AudioPlayerStateProvider.d.ts +2 -2
  48. package/dist/types/components/AudioPlayer/index.d.ts +28 -5
  49. package/dist/types/components/Drawer/Drawer.d.ts +1 -0
  50. package/dist/types/components/Grid/Item.d.ts +3 -2
  51. package/dist/types/components/Grid/index.d.ts +1 -0
  52. package/dist/types/hooks/index.d.ts +1 -2
  53. package/dist/types/index.d.ts +2 -2
  54. package/dist/types/utils/ssr.d.ts +0 -2
  55. package/llms.txt +61 -0
  56. package/package.json +4 -2
  57. package/dist/types/components/AudioPlayer/Interface/Controller/Tooltip/Volume/useVolume.d.ts +0 -5
  58. package/dist/types/hooks/context/useAudioAttrsContext.d.ts +0 -2
  59. package/dist/types/hooks/context/usePlaybackContext.d.ts +0 -2
  60. package/dist/types/hooks/context/useResourceContext.d.ts +0 -2
  61. package/dist/types/hooks/context/useTimeContext.d.ts +0 -2
  62. package/dist/types/hooks/context/useTrackContext.d.ts +0 -2
  63. package/dist/types/hooks/context/useUIContext.d.ts +0 -1
  64. package/dist/types/hooks/useGridTemplate.d.ts +0 -2
  65. /package/dist/types/components/AudioPlayer/Interface/Controller/Button/{NextBtn.d.ts → TransportControls/NextBtn.d.ts} +0 -0
  66. /package/dist/types/components/AudioPlayer/Interface/Controller/Button/{PlayBtn.d.ts → TransportControls/PlayBtn.d.ts} +0 -0
  67. /package/dist/types/components/AudioPlayer/Interface/Controller/Button/{PrevBtn.d.ts → TransportControls/PrevBtn.d.ts} +0 -0
  68. /package/dist/types/{utils → components/AudioPlayer/utils}/clampVolume.d.ts +0 -0
  69. /package/dist/types/hooks/{context/useNonNullableContext.d.ts → useNonNullableContext.d.ts} +0 -0
package/README.md CHANGED
@@ -15,11 +15,11 @@
15
15
  <a href="https://www.npmjs.com/package/react-modern-audio-player">
16
16
  <img src="https://img.shields.io/npm/dt/react-modern-audio-player" alt="Download">
17
17
  </a>
18
- <a href="https://bundlephobia.com/package/react-modern-audio-player@2.0.0">
18
+ <a href="https://bundlephobia.com/package/react-modern-audio-player">
19
19
  <img src="https://img.shields.io/bundlephobia/minzip/react-modern-audio-player" alt="BundleSize">
20
20
  </a>
21
21
  <a href="https://github.com/slash9494/react-modern-audio-player/actions/workflows/integration.yml">
22
- <img src="https://github.com/slash9494/react-modern-audio-player/actions/workflows/integration.yml/badge.svg?branch=main" alt="CI">
22
+ <img src="https://img.shields.io/github/actions/workflow/status/slash9494/react-modern-audio-player/integration.yml?branch=main&label=CI" alt="CI">
23
23
  </a>
24
24
  <img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript">
25
25
  </p>
@@ -29,6 +29,8 @@
29
29
  - **Waveform** progress bar powered by `wavesurfer.js`
30
30
  - **Playlist** with drag-and-drop reorder, repeat, shuffle
31
31
  - **Fully customizable** — swap any sub-component, CSS variable theming, light & dark themes
32
+ - **Compound slots** — `AudioPlayer.Volume`, `AudioPlayer.Progress`, `AudioPlayer.PlayList`, etc. for partial customization without losing the preset
33
+ - **Multi-instance playlist** — render multiple players on the same page with isolated playlist drawers and fully independent audio state
32
34
  - **Accessible** — WAI-ARIA patterns, full keyboard navigation, axe-tested
33
35
  - **TypeScript-first** — typed props and hooks (`useAudioPlayer`, sub-hooks)
34
36
  - **SSR-friendly** — works with Next.js App Router / Server Components
@@ -154,8 +156,9 @@ export default function PlayerPage() {
154
156
  | **Props** | [PlayList](#playlist) · [InitialStates](#initialstates) · [ActiveUI](#activeui) · [Placement](#placement) · [RootContainerProps](#rootcontainerprops) |
155
157
  | **Override & Style** | [CustomIcons](#customicons) · [CoverImgsCss](#coverimgscss) · [Theme mode](#theme-mode-dark-mode) · [ID & Classnames](#id--classnames) |
156
158
  | **Player Hook API** | [useAudioPlayer](#useaudioplayer) · [AudioPlayerControls](#audioplayercontrols) · [Sub-Hooks](#sub-hooks) |
157
- | **Custom Component** | [Custom Component](#custom-component) |
159
+ | **Custom Component** | [Custom Component](#custom-component) · [Compound Slots](#compound-slots) |
158
160
  | **Accessibility** | [Keyboard support](#keyboard-support) |
161
+ | **Gotchas** | [Gotchas](#gotchas) |
159
162
  | **Example** | [Example](#example) |
160
163
 
161
164
  # Props
@@ -173,6 +176,7 @@ interface AudioPlayerProps {
173
176
  playList?: PlayListPlacement;
174
177
  interface?: InterfacePlacement;
175
178
  volumeSlider?: VolumeSliderPlacement;
179
+ speedSelector?: SpeedSelectorPlacement;
176
180
  };
177
181
  rootContainerProps?: RootContainerProps;
178
182
  colorScheme?: "light" | "dark";
@@ -238,9 +242,12 @@ type InitialStates = Omit<
238
242
  currentTime?: number;
239
243
  duration?: number;
240
244
  curPlayId: number;
245
+ playListExpanded?: boolean;
241
246
  };
242
247
  ```
243
248
 
249
+ > `playListExpanded: true` opens the playlist drawer on mount. Consistent with the other fields on `audioInitialState`, this is read once at mount and is not tracked in reducer state.
250
+
244
251
  ## ActiveUI
245
252
 
246
253
  ```tsx
@@ -256,6 +263,7 @@ type ActiveUI = {
256
263
  trackInfo: boolean;
257
264
  artwork: boolean;
258
265
  progress: ProgressUI;
266
+ playbackRate: boolean;
259
267
  };
260
268
  type ProgressUI = "waveform" | "bar" | false;
261
269
  type PlayListUI = "sortable" | "unSortable" | false;
@@ -300,7 +308,9 @@ type PlayerPlacement =
300
308
  | "top-left"
301
309
  | "top-right";
302
310
 
303
- type VolumeSliderPlacement = "bottom" | "top" | 'left' | 'right';
311
+ type VolumeSliderPlacement = "bottom" | "top" | "left" | "right";
312
+
313
+ type SpeedSelectorPlacement = "bottom" | "top" | "left" | "right";
304
314
 
305
315
  type PlayListPlacement = "bottom" | "top";
306
316
 
@@ -315,7 +325,7 @@ type InterfacePlacementKey =
315
325
  | "trackTimeCurrent"
316
326
  | "trackTimeDuration";
317
327
 
318
- type InterfacePlacementValue = "row1-1" | "row1-2" | "row1-3" | "row1-4" | ... more ... | "row9-9"
328
+ type InterfacePlacementValue = "row1-1" | "row1-2" | "row1-3" | "row1-4" | ... more ... | "row10-10"
319
329
  /** if you apply custom components, values must be "row1-1" ~ any more */
320
330
 
321
331
  type InterfaceGridTemplateArea = Record<InterfacePlacementKey,InterfacePlacementValue>;
@@ -346,6 +356,7 @@ const defaultInterfacePlacement = {
346
356
  volume: "row1-7",
347
357
  playButton: "row1-8",
348
358
  playList: "row1-9",
359
+ playbackRate: "row1-10",
349
360
  },
350
361
  };
351
362
  ```
@@ -379,6 +390,12 @@ const defaultInterfacePlacement = {
379
390
 
380
391
  - rm-audio-player
381
392
 
393
+ > **Multi-instance note**: when multiple `<AudioPlayer>` instances share a
394
+ > page the root `id` is duplicated across them. Playlist and audio state are
395
+ > still isolated per instance (each player has its own React provider tree
396
+ > and its own `<audio>` DOM node). If you need per-instance selectors, target
397
+ > via the class names below rather than the id.
398
+
382
399
  ### root ClassName
383
400
 
384
401
  - rmap-player-provider
@@ -459,26 +476,28 @@ function App() {
459
476
 
460
477
  ## AudioPlayerControls
461
478
 
462
- | Property | Type | Description |
463
- | ----------------- | -------------------------- | ------------------------- |
464
- | `isPlaying` | `boolean` | Current playback state |
465
- | `volume` | `number` | Current volume (0–1) |
466
- | `currentTime` | `number` | Elapsed time in seconds |
467
- | `duration` | `number` | Track duration in seconds |
468
- | `repeatType` | `RepeatType` | Current repeat mode |
469
- | `muted` | `boolean` | Whether audio is muted |
470
- | `currentTrack` | `AudioData \| null` | Currently playing track |
471
- | `currentIndex` | `number` | Index in playlist |
472
- | `playList` | `PlayList` | Full playlist |
473
- | `play()` | `() => void` | Start playback |
474
- | `pause()` | `() => void` | Pause playback |
475
- | `togglePlay()` | `() => void` | Toggle play/pause |
476
- | `next()` | `() => void` | Skip to next track |
477
- | `prev()` | `() => void` | Skip to previous track |
478
- | `seek(time)` | `(time: number) => void` | Seek to time in seconds |
479
- | `setVolume(vol)` | `(volume: number) => void` | Set volume (0–1, clamped) |
480
- | `toggleMute()` | `() => void` | Toggle mute on/off |
481
- | `setTrack(index)` | `(index: number) => void` | Jump to playlist index |
479
+ | Property | Type | Description |
480
+ | ----------------------- | -------------------------- | -------------------------------------------------------------- |
481
+ | `isPlaying` | `boolean` | Current playback state |
482
+ | `volume` | `number` | Current volume (0–1) |
483
+ | `currentTime` | `number` | Elapsed time in seconds |
484
+ | `duration` | `number` | Track duration in seconds |
485
+ | `repeatType` | `RepeatType` | Current repeat mode |
486
+ | `muted` | `boolean` | Whether audio is muted |
487
+ | `currentTrack` | `AudioData \| null` | Currently playing track |
488
+ | `currentIndex` | `number` | Index in playlist |
489
+ | `playList` | `PlayList` | Full playlist |
490
+ | `play()` | `() => void` | Start playback |
491
+ | `pause()` | `() => void` | Pause playback |
492
+ | `togglePlay()` | `() => void` | Toggle play/pause |
493
+ | `next()` | `() => void` | Skip to next track |
494
+ | `prev()` | `() => void` | Skip to previous track |
495
+ | `seek(time)` | `(time: number) => void` | Seek to time in seconds |
496
+ | `setVolume(vol)` | `(volume: number) => void` | Set volume (0–1, clamped) |
497
+ | `toggleMute()` | `() => void` | Toggle mute on/off |
498
+ | `setTrack(index)` | `(index: number) => void` | Jump to playlist index |
499
+ | `playbackRate` | `number` | Current playback rate (`1` = normal). Default `1`. |
500
+ | `setPlaybackRate(rate)` | `(rate: number) => void` | Set playback rate. No clamping; browser enforces HTML5 bounds. |
482
501
 
483
502
  ## Sub-Hooks
484
503
 
@@ -510,13 +529,13 @@ function TimeDisplay() {
510
529
  }
511
530
  ```
512
531
 
513
- | Hook | Returns |
514
- | ------------------------ | ------------------------------------------------------------------------------- |
515
- | `useAudioPlayerPlayback` | `{ isPlaying, repeatType, play, pause, togglePlay }` |
516
- | `useAudioPlayerTrack` | `{ currentPlayId, currentIndex, playList, currentTrack, setTrack, next, prev }` |
517
- | `useAudioPlayerVolume` | `{ volume, muted, setVolume, toggleMute }` |
518
- | `useAudioPlayerTime` | `{ currentTime, duration, seek }` |
519
- | `useAudioPlayerElement` | `{ audioEl, waveformInst }` (advanced) |
532
+ | Hook | Returns |
533
+ | ------------------------ | ----------------------------------------------------------------------------------- |
534
+ | `useAudioPlayerPlayback` | `{ isPlaying, repeatType, playbackRate, play, pause, togglePlay, setPlaybackRate }` |
535
+ | `useAudioPlayerTrack` | `{ currentPlayId, currentIndex, playList, currentTrack, setTrack, next, prev }` |
536
+ | `useAudioPlayerVolume` | `{ volume, muted, setVolume, toggleMute }` |
537
+ | `useAudioPlayerTime` | `{ currentTime, duration, seek }` |
538
+ | `useAudioPlayerElement` | `{ audioEl, waveformInst }` (advanced) |
520
539
 
521
540
  # Context Hooks
522
541
 
@@ -551,21 +570,34 @@ You can place a custom component anywhere in the player interface using `AudioPl
551
570
  import AudioPlayer, {
552
571
  useAudioPlayer,
553
572
  InterfacePlacement,
573
+ DEFAULT_INTERFACE_GRID_BOUND,
554
574
  } from "react-modern-audio-player";
555
575
 
556
576
  const activeUI: ActiveUI = {
557
577
  all: true,
558
578
  };
559
579
 
580
+ // `as 12` pins the literal type — TS arithmetic widens
581
+ // `DEFAULT_INTERFACE_GRID_BOUND + 1` to `number`, which would erase grid-coord
582
+ const TOTAL_INTERFACE_GRID_BOUND = (DEFAULT_INTERFACE_GRID_BOUND + 1) as 12;
583
+
584
+ // `row1-${DEFAULT_INTERFACE_GRID_BOUND}` resolves to `"row1-11"` —
585
+ // the first cell beyond the default template area (which fills 1..10).
586
+ const customComponentGridArea = `row1-${DEFAULT_INTERFACE_GRID_BOUND}`;
587
+
560
588
  const placement = {
561
589
  interface: {
562
590
  customComponentsArea: {
563
- playerCustomComponent: "row1-10",
591
+ playerCustomComponent: customComponentGridArea,
564
592
  },
565
- } as InterfacePlacement<11>,
593
+ } as InterfacePlacement<typeof TOTAL_INTERFACE_GRID_BOUND>,
566
594
  /**
567
- * set the generic value of InterfacePlacement to the max interface length + 1
568
- * for correct "row1-10" autocompletion
595
+ * The generic on `InterfacePlacement<N>` is an exclusive upper bound on grid
596
+ * indices usable cells are `1..(N - 1)`. Default is
597
+ * `DEFAULT_INTERFACE_GRID_BOUND` (= 11), giving cells `1..10` which the
598
+ * built-in template area already occupies (e.g. `playbackRate` at `row1-10`).
599
+ * To reserve an additional cell for the custom component, pass `<N + 1>` —
600
+ * here `<12>` makes `row1-11` a valid coordinate.
569
601
  */
570
602
  };
571
603
 
@@ -590,6 +622,114 @@ const CustomComponent = () => {
590
622
  </AudioPlayer>;
591
623
  ```
592
624
 
625
+ # Compound Slots
626
+
627
+ `AudioPlayer` exposes its built-in controls as static members so you can re-place or augment individual pieces without rebuilding the whole layout.
628
+
629
+ | Member | Renders |
630
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
631
+ | `AudioPlayer.Progress` | progress bar / waveform |
632
+ | `AudioPlayer.Volume` | volume trigger + slider (accepts `triggerType?: "click" \| "hover"`, `placement?: VolumeSliderPlacement`) |
633
+ | `AudioPlayer.PlayList` | sortable playlist drawer (accepts `initialExpanded?`) |
634
+ | `AudioPlayer.PlayListEmpty` | fallback rendered inside the playlist drawer when `playList` is empty |
635
+ | [`AudioPlayer.PlayButton`](#audioplayerplaybutton-and-activeuiprevnnext) | Play + Prev + Next group (Prev/Next visibility follows `activeUI.prevNnext`) |
636
+ | `AudioPlayer.RepeatButton` | repeat-type button |
637
+ | `AudioPlayer.SpeedSelector` | playback rate dropdown (accepts `options?`, `formatRate?`, `triggerType?: "click" \| "hover"`, `placement?: SpeedSelectorPlacement`) |
638
+ | `AudioPlayer.Artwork` | track artwork |
639
+ | `AudioPlayer.TrackInfo` | track title / writer |
640
+ | `AudioPlayer.TrackTime` | current + duration time |
641
+ | `AudioPlayer.CustomComponent` | user-defined slot |
642
+
643
+ Each slot accepts the full `GridItemLayoutProps` set — `gridArea?`, `visible?`, `width?`, `padding?`, `justifySelf?`, `UNSAFE_className?` — plus its own domain props. `AudioPlayer.TrackTime` is the exception: it only exposes `visible?` because the slot maps to two grid areas internally.
644
+
645
+ Native HTML attributes (`className`, `style`, `onClick`, `data-*`, etc.) are **not** forwarded by compound slots. Compose the underlying primitives (`PlayBtn`, `PrevBtn`, `NextBtn`, etc., still exported) when full DOM control is needed; headless support with native attribute pass-through is planned for v3.
646
+
647
+ ## Mental model — `activeUI` vs compound children
648
+
649
+ - **`activeUI`** governs the **preset** (default layout) — which built-in controls are shown.
650
+ - **Compound children** are **explicit placements** that always render (`visible` defaults to `true`).
651
+
652
+ The two layers are orthogonal. Compound children render **additively** alongside the preset. To truly replace a preset control, disable it in `activeUI` and render the compound counterpart:
653
+
654
+ ```tsx
655
+ // Remove the default volume, re-place it with a custom gridArea
656
+ <AudioPlayer playList={playList} activeUI={{ all: true, volume: false }}>
657
+ <AudioPlayer.Volume gridArea="row1-5" />
658
+ </AudioPlayer>
659
+ ```
660
+
661
+ In development, a `console.warn` is emitted when a compound slot is rendered while its preset counterpart is still active, so silent duplication is easy to catch.
662
+
663
+ ## `AudioPlayer.PlayButton` and `activeUI.prevNnext`
664
+
665
+ `AudioPlayer.PlayButton` is a single slot that renders the **Play + Prev + Next** group as one unit. There is no separate `AudioPlayer.PrevButton` / `AudioPlayer.NextButton` compound slot — Prev / Next are part of the `PlayButton` compound, and there are two ways to render them.
666
+
667
+ ### Method 1 — `AudioPlayer.PlayButton` compound (group)
668
+
669
+ Place the whole group at a custom grid area; Prev / Next visibility inside the group follows `activeUI.prevNnext` (which defaults to `activeUI.all`):
670
+
671
+ ```tsx
672
+ // Re-place the full Play + Prev + Next group at a custom area
673
+ <AudioPlayer
674
+ playList={playList}
675
+ activeUI={{ all: true, playButton: false }} // hide the preset transport
676
+ >
677
+ <AudioPlayer.PlayButton gridArea="row1-3" />
678
+ </AudioPlayer>
679
+
680
+ // Same compound, but render Play only — Prev / Next hidden by activeUI.prevNnext
681
+ <AudioPlayer
682
+ playList={playList}
683
+ activeUI={{ all: true, playButton: false, prevNnext: false }}
684
+ >
685
+ <AudioPlayer.PlayButton gridArea="row1-3" />
686
+ </AudioPlayer>
687
+ ```
688
+
689
+ ### Method 2 — primitives via `AudioPlayer.CustomComponent`
690
+
691
+ When you need finer control — placing Prev / Play / Next in different grid cells, applying custom wrappers, or attaching native HTML attributes — drop down to the `PlayBtn`, `PrevBtn`, and `NextBtn` primitives (still exported) and render them inside `AudioPlayer.CustomComponent`:
692
+
693
+ ```tsx
694
+ import AudioPlayer, {
695
+ PrevBtn,
696
+ PlayBtn,
697
+ NextBtn,
698
+ } from "react-modern-audio-player";
699
+
700
+ <AudioPlayer
701
+ playList={playList}
702
+ activeUI={{ all: true, playButton: false }} // hide the preset transport
703
+ placement={{
704
+ interface: { customComponentsArea: { "custom-transport": "row1-3" } },
705
+ }}
706
+ >
707
+ <AudioPlayer.CustomComponent id="custom-transport">
708
+ <div className="my-transport">
709
+ <PrevBtn isVisible />
710
+ <PlayBtn />
711
+ <NextBtn isVisible />
712
+ </div>
713
+ </AudioPlayer.CustomComponent>
714
+ </AudioPlayer>;
715
+ ```
716
+
717
+ `PrevBtn` / `NextBtn` accept an `isVisible` boolean for symmetry with the compound group's visibility logic; pass `false` to omit them. Use Method 2 when Method 1's group placement and `activeUI.prevNnext` toggle aren't enough.
718
+
719
+ ## Custom empty-playlist UI
720
+
721
+ Pass children to `AudioPlayer.PlayListEmpty` to render a custom node inside the playlist drawer when `playList` is empty. Omit the slot to keep the default (drawer content renders nothing).
722
+
723
+ ```tsx
724
+ <AudioPlayer playList={[]}>
725
+ <AudioPlayer.PlayListEmpty>
726
+ <div className="my-empty">Playlist is empty</div>
727
+ </AudioPlayer.PlayListEmpty>
728
+ </AudioPlayer>
729
+ ```
730
+
731
+ `PlayListEmpty` is a marker slot; its children are consumed by the drawer via context and the component itself does not render in-place.
732
+
593
733
  # **Accessibility**
594
734
 
595
735
  The player follows WAI-ARIA patterns and is fully navigable by keyboard and screen readers.
@@ -598,16 +738,29 @@ The player follows WAI-ARIA patterns and is fully navigable by keyboard and scre
598
738
 
599
739
  All controls are reachable via `Tab` and respond to standard keyboard activation. The playlist uses the WAI-ARIA "Listbox with Rearrangeable Options" pattern:
600
740
 
601
- | Key | Action |
602
- | --- | --- |
603
- | `Tab` / `Shift+Tab` | Move focus between player controls |
604
- | `Space` / `Enter` | Activate the focused button (play/pause, prev/next, repeat, mute, playlist) |
605
- | `ArrowUp` / `ArrowDown` | Move focus between playlist items |
606
- | `Alt+ArrowUp` / `Alt+ArrowDown` | Reorder the focused playlist item |
607
- | `Enter` / `Space` on a playlist item | Select and play that track |
741
+ | Key | Action |
742
+ | ------------------------------------ | --------------------------------------------------------------------------- |
743
+ | `Tab` / `Shift+Tab` | Move focus between player controls |
744
+ | `Space` / `Enter` | Activate the focused button (play/pause, prev/next, repeat, mute, playlist) |
745
+ | `ArrowUp` / `ArrowDown` | Move focus between playlist items |
746
+ | `Alt+ArrowUp` / `Alt+ArrowDown` | Reorder the focused playlist item |
747
+ | `Enter` / `Space` on a playlist item | Select and play that track |
608
748
 
609
749
  Drag-and-drop reordering is preserved as an alternative — keyboard and mouse both call the same `onReorder` handler.
610
750
 
751
+ # **Gotchas**
752
+
753
+ Common integration mistakes to avoid:
754
+
755
+ - **Don't toggle the theme via `rootContainerProps.style.colorScheme`.** The native CSS `color-scheme` property does not switch the player's theme. Use the top-level [`colorScheme`](#theme-mode-dark-mode) prop, which drives the `[data-theme]` attribute and re-initializes the waveform colors.
756
+ - **Set the `InterfacePlacement` generic when placing `customComponentsArea` beyond row 9.** TypeScript rejects values past the default range, so use `InterfacePlacement<N>` where `N` is `(max row length + 1)` — e.g. `InterfacePlacement<11>` for `"row1-10"` (see [Custom Component](#custom-component)).
757
+ - **`AudioPlayer.CustomComponent` accepts a single React element child.** It uses `React.cloneElement` internally, so passing multiple children or a primitive (string, number) will throw.
758
+ - **Volume is `0..1`, not `0..100`.** `setVolume` clamps out-of-range values, so `setVolume(50)` silently becomes `setVolume(1)`.
759
+ - **Compound slots don't forward native HTML attributes.** `<AudioPlayer.Volume className="...">` is rejected by TypeScript — only `GridItemLayoutProps` (layout) pass through. Compose the underlying primitives (`PlayBtn`, `PrevBtn`, `NextBtn`, etc., still exported) when you need `className`, `style`, `onClick`, or `data-*`. Full headless support is planned for v3.
760
+ - **`id: 0` is a valid track id.** The reducer uses nullish checks, so tracks with `id: 0` are handled correctly — don't filter them out of `playList` on the assumption that zero is falsy.
761
+ - **Don't import the CSS manually.** Styles are auto-injected via `sideEffects: ["*.css"]`; `import "react-modern-audio-player/dist/index.css"` will 404 or double-load.
762
+ - **Multiple mounted `<AudioPlayer>` instances don't share React state, but they do share the user's speakers.** Each instance has its own provider and its own `<audio>` element, so the state is isolated — but if two instances both play, the user hears both tracks simultaneously. Coordinate playback yourself (e.g. pause the others when one `play()` fires).
763
+
611
764
  # **Example**
612
765
 
613
766
  ```tsx
package/dist/index.css CHANGED
@@ -1 +1 @@
1
- .rmap-player-provider{--rm-audio-player-text-color: #2c2c2c;--rm-audio-player-shadow: 0 0 0;--rm-audio-player-interface-container: #f5f5f5;--rm-audio-player-volume-background: #ccc;--rm-audio-player-volume-panel-background: #f2f2f2;--rm-audio-player-volume-panel-border: #ccc;--rm-audio-player-volume-thumb: #5c5c5c;--rm-audio-player-volume-fill: rgba(0, 0, 0, .5);--rm-audio-player-volume-track: #ababab;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #8c8c8c;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #393939;--rm-audio-player-waveform-cursor: #4b4b4b;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #eaeaea;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #b3b3b3;color:var(--rm-audio-player-text-color)}@media (prefers-color-scheme: dark){.rmap-player-provider:not([data-theme=light]){--rm-audio-player-text-color: #ededed;--rm-audio-player-shadow: 255 255 255;--rm-audio-player-interface-container: #1e1e1e;--rm-audio-player-volume-background: #5c5c5c;--rm-audio-player-volume-panel-background: #2c2c2c;--rm-audio-player-volume-panel-border: #393939;--rm-audio-player-volume-thumb: #d3d3d3;--rm-audio-player-volume-fill: rgba(255, 255, 255, .5);--rm-audio-player-volume-track: #494949;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #a2a2a2;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #d1d1d1;--rm-audio-player-waveform-cursor: #c8c8c8;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #2c2c2c;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #5c5c5c}}.rmap-player-provider[data-theme=dark]{--rm-audio-player-text-color: #ededed;--rm-audio-player-shadow: 255 255 255;--rm-audio-player-interface-container: #1e1e1e;--rm-audio-player-volume-background: #5c5c5c;--rm-audio-player-volume-panel-background: #2c2c2c;--rm-audio-player-volume-panel-border: #393939;--rm-audio-player-volume-thumb: #d3d3d3;--rm-audio-player-volume-fill: rgba(255, 255, 255, .5);--rm-audio-player-volume-track: #494949;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #a2a2a2;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #d1d1d1;--rm-audio-player-waveform-cursor: #c8c8c8;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #2c2c2c;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #5c5c5c}.rmap-player-provider *{box-sizing:border-box;font:inherit;font-size:100%}.rmap-player-provider ol,.rmap-player-provider ul{list-style:none;margin:0;padding:0}.rmap-player-provider blockquote,.rmap-player-provider q{quotes:none}.rmap-player-provider blockquote:before,.rmap-player-provider blockquote:after,.rmap-player-provider q:before,.rmap-player-provider q:after{content:none}.rmap-player-provider table{border-collapse:collapse;border-spacing:0}.rmap-player-provider button{margin:0;padding:0;background:transparent;color:inherit;cursor:pointer;vertical-align:baseline;border:0}.rmap-ctrl-btn-wrapper{display:flex;align-items:center;gap:10px}.styled-btn.rmap-play-btn{width:35px}.styled-btn{background:none;border:none;padding:0;cursor:pointer;display:flex;width:20px;height:100%;transition:transform .2s ease-out,opacity .2s ease-out}.styled-btn svg{width:100%;height:100%;pointer-events:none}.styled-btn:hover{transform:scale(1.1)}.styled-btn:focus-visible{outline:2px solid var(--rm-audio-player-progress-bar, #0072f5);outline-offset:2px}.styled-btn:active{transform:scale(.8);opacity:.5}.rmap-drawer-container{position:relative;min-width:20px;min-height:20px}.rmap-drawer-container .rmap-drawer-trigger{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer;display:flex}.rmap-drawer-container .rmap-drawer-trigger[aria-expanded=true] svg{color:var(--rm-audio-player-sortable-list-button-active)}.rmap-drawer-container .rmap-drawer-content{transform-origin:center top}.rmap-drawer-container .rmap-drawer-content-enter{animation:rmap-appearance-in .25s ease-out forwards}.rmap-drawer-container .rmap-drawer-content-leave{animation:rmap-appearance-out .1s ease-in forwards}.rmap-sortable-list-container,.rmap-sortable-list-container li{list-style-type:none}.rmap-sortable-list-container{cursor:pointer}.rmap-sortable-item{border-top:2px solid transparent;border-bottom:2px solid transparent;transition:opacity .3s ease-in-out,transform .3s ease-in-out,box-shadow .3s ease-in-out,backdrop-filter .3s ease-in-out}.rmap-sortable-item *{pointer-events:none}.rmap-sortable-item.rmap-drag-start{opacity:.5}.rmap-sortable-item.rmap-drag-over{transform:scale(1.02);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 3.58195px 22.3872px -2.68646px rgb(var( --rm-audio-player-shadow, 0 0 0 ) / 20%)}.rmap-sortable-item-inner{display:flex;align-items:center}.rmap-playlist-item{width:100%;height:100%;display:flex;align-items:center;padding:10px 20px}.rmap-playlist-item.rmap-cur-played{background:var(--rm-audio-player-selected-list-item-background)}.rmap-playlist-item .rmap-playlist-item-contents{width:100%;display:flex;gap:10px}.rmap-playlist-item .rmap-playlist-album-cover{display:flex;align-items:center}.rmap-playlist-item .rmap-playlist-album-cover img{width:35px;height:35px}.rmap-playlist-item .rmap-playlist-album-info{display:grid;min-width:10px;font-size:13px;margin-right:1.5rem;padding:2px 0%}.rmap-playlist-item .rmap-playlist-album-info span{align-self:center;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rmap-playlist-container{transition-property:max-height,opacity;overflow-x:hidden;overflow-y:auto}.rmap-playlist-container.rmap-playlist-content-enter{opacity:0;max-height:0}.rmap-playlist-container.rmap-playlist-content-enter-active{opacity:1;max-height:20vh;transition-duration:.5s;transition-timing-function:cubic-bezier(0,0,0,1.2)}.rmap-playlist-container.rmap-playlist-content-leave{opacity:1;max-height:20vh}.rmap-playlist-container.rmap-playlist-content-leave-active{opacity:0;max-height:0;transition-duration:.25s;transition-timing-function:cubic-bezier(.66,-.41,1,1)}.rmap-bar-progress-wrapper{display:flex;width:100%;height:18px;padding:8px 0;cursor:pointer;position:relative;align-items:center}.rmap-bar-progress-wrapper .rmap-progress-bar{position:relative;width:100%;height:100%;overflow:hidden;background-color:var(--rm-audio-player-progress-bar-background)}.rmap-bar-progress-wrapper .rmap-progress-fill{position:absolute;left:0;width:100%;height:100%;background-color:var(--rm-audio-player-progress-bar);transform-origin:0 0;transform:scaleX(0)}.rmap-bar-progress-wrapper .rmap-progress-handle{position:absolute;left:-4px;background-color:var(--rm-audio-player-progress-bar);border-radius:50%;height:8px;width:8px;opacity:0;transition:opacity .2s ease-in-out}.rmap-bar-progress-wrapper:hover .rmap-progress-handle,.rmap-bar-progress-wrapper:focus-visible .rmap-progress-handle{opacity:1}.rmap-bar-progress-wrapper:focus-visible{outline:2px solid var(--rm-audio-player-progress-bar, #0072f5);outline-offset:2px}.rmap-waveform-wrapper{display:flex;width:100%}.rmap-waveform-wrapper[data-active=false]{height:0;opacity:0;pointer-events:none}.rmap-waveform-wrapper #rm-waveform{width:100%;overflow:hidden}.rmap-waveform-wrapper #rm-waveform wave{cursor:pointer!important;overflow-x:hidden!important}.rmap-progress-container{min-width:100px}.rmap-volume-container{position:relative;height:119px;width:32px;display:flex;align-items:center;justify-content:center}.rmap-volume-container[data-placement=top]{bottom:auto}.rmap-volume-container[data-placement=left]{transform:rotate(-90deg);right:50px}.rmap-volume-container[data-placement=right]{transform:rotate(90deg);left:50px}.rmap-volume-container[data-placement=bottom]{transform:rotateX(180deg);top:5px}.rmap-volume-container .rmap-volume-panel{width:30px;background-color:var(--rm-audio-player-volume-panel-background);border:1px solid var(--rm-audio-player-volume-panel-border);border-radius:5px;height:118px;box-shadow:0 2px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 10%);position:absolute;bottom:5px}.rmap-volume-container .rmap-volume-panel:before{content:"";bottom:-10px;left:7.9px;border-color:transparent transparent var(--rm-audio-player-volume-panel-border) var(--rm-audio-player-volume-panel-border);border-style:solid;border-width:5px;box-shadow:-3px 3px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 10%);position:absolute;width:0;height:0;box-sizing:border-box;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);pointer-events:none;z-index:0}.rmap-volume-container .rmap-volume-panel:after{content:"";bottom:-8px;left:9px;border-color:transparent transparent var(--rm-audio-player-volume-panel-background) var(--rm-audio-player-volume-panel-background);border-style:solid;border-width:4px;z-index:1;position:absolute;width:0;height:0;box-sizing:border-box;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);pointer-events:none}.rmap-volume-container input[type=range]{margin-left:14px;position:absolute;display:block;top:-45px;left:0;height:2px;width:92px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--rm-audio-player-volume-background);transform-origin:75px 75px;transform:rotate(-90deg)}.rmap-volume-container input:focus:not(:focus-visible){outline-color:transparent}.rmap-volume-container input::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:16px;height:16px;border-radius:12px;overflow:visible;background:var(--rm-audio-player-volume-thumb)}.rmap-volume-container input::-moz-range-thumb{width:16px;height:16px;border-radius:12px;overflow:visible;background:var(--rm-audio-player-volume-thumb);border:none}.rmap-volume-container input::-moz-range-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;overflow:visible;color:transparent;cursor:pointer;border-radius:2%/50%;border-color:transparent;background-color:transparent;background-position:center;background-repeat:no-repeat;background-size:100% 3px;background-image:linear-gradient(90deg,var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),var(--rm-audio-player-volume-track) var(--rm-audio-player-volume-value))}.rmap-volume-container input::-webkit-slider-runnable-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;overflow:visible;color:transparent;cursor:pointer;border-radius:2%/50%;border-color:transparent;background-color:transparent;background-position:center;background-repeat:no-repeat;background-size:100% 3px;background-image:linear-gradient(90deg,var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),var(--rm-audio-player-volume-track) var(--rm-audio-player-volume-value))}@keyframes rmap-appearance-in{0%{opacity:0;transform:scale(.95)}60%{opacity:.75;transform:scale(1.05)}to{opacity:1;transform:scale(1)}}@keyframes rmap-appearance-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.5)}}.rmap-dropdown-container{position:relative;display:flex;align-items:center;justify-content:center;min-width:20px;min-height:20px}.rmap-dropdown-container .rmap-dropdown-trigger{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer;display:flex}.rmap-dropdown-container .rmap-dropdown-content{transform-origin:center top}.rmap-dropdown-container .rmap-dropdown-content-enter{animation:rmap-appearance-in .25s ease-out normal forwards}.rmap-dropdown-container .rmap-dropdown-content-leave{animation:rmap-appearance-out .1s ease-in forwards}.rmap-artwork-container{display:flex;align-items:center;width:100%;height:100%}.rmap-artwork-container img,.rmap-artwork-container .rmap-artwork-fallback{width:50px;height:50px}.rmap-artwork-container .rmap-artwork-fallback{display:flex;align-items:center;justify-content:center;overflow:hidden;font-size:13px}.rmap-track-info-container{display:grid;align-items:center;row-gap:5px}.rmap-track-info-container span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rmap-track-info-container .title{font-size:16px}.rmap-track-info-container .writer{font-size:12px}.rmap-track-time-container{display:flex;align-items:center;justify-content:center;min-height:16px;font-family:monospace!important;font-size:16px!important}.rmap-track-time-container[data-position=left] .rmap-track-current-time,.rmap-track-time-container[data-position=left] .rmap-track-duration{margin-right:-10px}.rmap-track-time-container[data-position=right] .rmap-track-current-time:before,.rmap-track-time-container[data-position=right] .rmap-track-duration:before{content:"/";margin:0 .3rem}.rmap-track-time-current .rmap-track-current-time{font-weight:700;letter-spacing:-.1rem;color:var(--rm-audio-player-track-current-time)}.rmap-track-time-duration .rmap-track-duration{display:flex;color:var(--rm-audio-player-track-duration);letter-spacing:-.1rem}.rmap-interface-container .rmap-interface-grid{background:var(--rm-audio-player-interface-container);padding:.5rem 10px}.rmap-interface-container .rmap-sortable-playlist{background:var(--rm-audio-player-sortable-list);box-shadow:-5px 2px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 4%) inset}
1
+ .rmap-player-provider{--rm-audio-player-text-color: #2c2c2c;--rm-audio-player-shadow: 0 0 0;--rm-audio-player-interface-container: #f5f5f5;--rm-audio-player-volume-background: #ccc;--rm-audio-player-volume-panel-background: #f2f2f2;--rm-audio-player-volume-panel-border: #ccc;--rm-audio-player-volume-thumb: #5c5c5c;--rm-audio-player-volume-fill: rgba(0, 0, 0, .5);--rm-audio-player-volume-track: #ababab;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #8c8c8c;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #393939;--rm-audio-player-waveform-cursor: #4b4b4b;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #eaeaea;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #b3b3b3;color:var(--rm-audio-player-text-color)}@media (prefers-color-scheme: dark){.rmap-player-provider:not([data-theme=light]){--rm-audio-player-text-color: #ededed;--rm-audio-player-shadow: 255 255 255;--rm-audio-player-interface-container: #1e1e1e;--rm-audio-player-volume-background: #5c5c5c;--rm-audio-player-volume-panel-background: #2c2c2c;--rm-audio-player-volume-panel-border: #393939;--rm-audio-player-volume-thumb: #d3d3d3;--rm-audio-player-volume-fill: rgba(255, 255, 255, .5);--rm-audio-player-volume-track: #494949;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #a2a2a2;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #d1d1d1;--rm-audio-player-waveform-cursor: #c8c8c8;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #2c2c2c;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #5c5c5c}}.rmap-player-provider[data-theme=dark]{--rm-audio-player-text-color: #ededed;--rm-audio-player-shadow: 255 255 255;--rm-audio-player-interface-container: #1e1e1e;--rm-audio-player-volume-background: #5c5c5c;--rm-audio-player-volume-panel-background: #2c2c2c;--rm-audio-player-volume-panel-border: #393939;--rm-audio-player-volume-thumb: #d3d3d3;--rm-audio-player-volume-fill: rgba(255, 255, 255, .5);--rm-audio-player-volume-track: #494949;--rm-audio-player-track-current-time: #0072f5;--rm-audio-player-track-duration: #a2a2a2;--rm-audio-player-progress-bar: #0072f5;--rm-audio-player-progress-bar-background: #d1d1d1;--rm-audio-player-waveform-cursor: #c8c8c8;--rm-audio-player-waveform-background: var( --rm-audio-player-progress-bar-background );--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);--rm-audio-player-sortable-list: #2c2c2c;--rm-audio-player-sortable-list-button-active: #0072f5;--rm-audio-player-selected-list-item-background: #5c5c5c}.rmap-player-provider *{box-sizing:border-box;font:inherit;font-size:100%}.rmap-player-provider ol,.rmap-player-provider ul{list-style:none;margin:0;padding:0}.rmap-player-provider blockquote,.rmap-player-provider q{quotes:none}.rmap-player-provider blockquote:before,.rmap-player-provider blockquote:after,.rmap-player-provider q:before,.rmap-player-provider q:after{content:none}.rmap-player-provider table{border-collapse:collapse;border-spacing:0}.rmap-player-provider button{margin:0;padding:0;background:transparent;color:inherit;cursor:pointer;vertical-align:baseline;border:0}.rmap-ctrl-btn-wrapper{display:flex;align-items:center;gap:10px}.styled-btn.rmap-play-btn{width:35px}.styled-btn{background:none;border:none;padding:0;cursor:pointer;display:flex;width:20px;height:100%;transition:transform .2s ease-out,opacity .2s ease-out}.styled-btn svg{width:100%;height:100%;pointer-events:none}.styled-btn:hover{transform:scale(1.1)}.styled-btn:focus-visible{outline:2px solid var(--rm-audio-player-progress-bar, #0072f5);outline-offset:2px}.styled-btn:active{transform:scale(.8);opacity:.5}.rmap-drawer-container{position:relative;min-width:20px;min-height:20px}.rmap-drawer-container .rmap-drawer-trigger{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer;display:flex}.rmap-drawer-container .rmap-drawer-trigger[aria-expanded=true] svg{color:var(--rm-audio-player-sortable-list-button-active)}.rmap-drawer-container .rmap-drawer-content{transform-origin:center top}.rmap-drawer-container .rmap-drawer-content-enter{animation:rmap-appearance-in .25s ease-out forwards}.rmap-drawer-container .rmap-drawer-content-leave{animation:rmap-appearance-out .1s ease-in forwards}.rmap-sortable-list-container,.rmap-sortable-list-container li{list-style-type:none}.rmap-sortable-list-container{cursor:pointer}.rmap-sortable-item{border-top:2px solid transparent;border-bottom:2px solid transparent;transition:opacity .3s ease-in-out,transform .3s ease-in-out,box-shadow .3s ease-in-out,backdrop-filter .3s ease-in-out}.rmap-sortable-item *{pointer-events:none}.rmap-sortable-item.rmap-drag-start{opacity:.5}.rmap-sortable-item.rmap-drag-over{transform:scale(1.02);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 3.58195px 22.3872px -2.68646px rgb(var( --rm-audio-player-shadow, 0 0 0 ) / 20%)}.rmap-sortable-item-inner{display:flex;align-items:center}.rmap-playlist-item{width:100%;height:100%;display:flex;align-items:center;padding:10px 20px}.rmap-playlist-item.rmap-cur-played{background:var(--rm-audio-player-selected-list-item-background)}.rmap-playlist-item .rmap-playlist-item-contents{width:100%;display:flex;gap:10px}.rmap-playlist-item .rmap-playlist-album-cover{display:flex;align-items:center}.rmap-playlist-item .rmap-playlist-album-cover img{width:35px;height:35px}.rmap-playlist-item .rmap-playlist-album-info{display:grid;min-width:10px;font-size:13px;margin-right:1.5rem;padding:2px 0%}.rmap-playlist-item .rmap-playlist-album-info span{align-self:center;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rmap-playlist-container{transition-property:max-height,opacity;overflow-x:hidden;overflow-y:auto}.rmap-playlist-container.rmap-playlist-content-enter{opacity:0;max-height:0}.rmap-playlist-container.rmap-playlist-content-enter-active{opacity:1;max-height:20vh;transition-duration:.5s;transition-timing-function:cubic-bezier(0,0,0,1.2)}.rmap-playlist-container.rmap-playlist-content-leave{opacity:1;max-height:20vh}.rmap-playlist-container.rmap-playlist-content-leave-active{opacity:0;max-height:0;transition-duration:.25s;transition-timing-function:cubic-bezier(.66,-.41,1,1)}.rmap-bar-progress-wrapper{display:flex;width:100%;height:18px;padding:8px 0;cursor:pointer;position:relative;align-items:center}.rmap-bar-progress-wrapper .rmap-progress-bar{position:relative;width:100%;height:100%;overflow:hidden;background-color:var(--rm-audio-player-progress-bar-background)}.rmap-bar-progress-wrapper .rmap-progress-fill{position:absolute;left:0;width:100%;height:100%;background-color:var(--rm-audio-player-progress-bar);transform-origin:0 0;transform:scaleX(0)}.rmap-bar-progress-wrapper .rmap-progress-handle{position:absolute;left:-4px;background-color:var(--rm-audio-player-progress-bar);border-radius:50%;height:8px;width:8px;opacity:0;transition:opacity .2s ease-in-out}.rmap-bar-progress-wrapper:hover .rmap-progress-handle,.rmap-bar-progress-wrapper:focus-visible .rmap-progress-handle{opacity:1}.rmap-bar-progress-wrapper:focus-visible{outline:2px solid var(--rm-audio-player-progress-bar, #0072f5);outline-offset:2px}.rmap-waveform-wrapper{display:flex;width:100%}.rmap-waveform-wrapper[data-active=false]{height:0;opacity:0;pointer-events:none}.rmap-waveform-wrapper #rm-waveform{width:100%;overflow:hidden}.rmap-waveform-wrapper #rm-waveform wave{cursor:pointer!important;overflow-x:hidden!important}.rmap-progress-container{min-width:100px}.styled-btn.rmap-speed-selector-trigger{width:auto;min-width:36px;height:100%;align-items:center;justify-content:center;font:inherit;font-variant-numeric:tabular-nums;line-height:1;color:var(--rm-audio-player-text-color);border-radius:4px}.rmap-dropdown-container .rmap-dropdown-trigger.rmap-speed-selector-trigger{position:static;inset:auto}.styled-btn.rmap-speed-selector-trigger:hover{transform:none;color:var(--rm-audio-player-track-current-time)}.styled-btn.rmap-speed-selector-trigger:active{transform:scale(.95)}.rmap-speed-selector-menu{list-style:none;margin:0;padding:4px;min-width:64px;background-color:var(--rm-audio-player-volume-panel-background);border:1px solid var(--rm-audio-player-volume-panel-border);border-radius:5px;box-shadow:0 2px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 10%);display:flex;flex-direction:column}.rmap-speed-selector-option{width:100%;background:none;border:none;cursor:pointer;text-align:center;font:inherit;font-variant-numeric:tabular-nums;line-height:1.2;color:var(--rm-audio-player-text-color);border-radius:3px;transition:background-color .15s ease-out,color .15s ease-out}.rmap-speed-selector-option:hover{background-color:rgb(var(--rm-audio-player-shadow, 0 0 0) / 8%)}.rmap-speed-selector-option:focus-visible{outline:2px solid var(--rm-audio-player-progress-bar, #0072f5);outline-offset:-2px}.rmap-speed-selector-option.rmap-speed-selector-option--active{color:var(--rm-audio-player-track-current-time);font-weight:600}.rmap-speed-selector-menu .rmap-speed-selector-option{padding:6px 12px}@keyframes rmap-appearance-in{0%{opacity:0;transform:scale(.95)}60%{opacity:.75;transform:scale(1.05)}to{opacity:1;transform:scale(1)}}@keyframes rmap-appearance-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.5)}}.rmap-dropdown-container{position:relative;display:flex;align-items:center;justify-content:center;min-width:20px;min-height:20px}.rmap-dropdown-container .rmap-dropdown-trigger{position:absolute;top:0;right:0;bottom:0;left:0;cursor:pointer;display:flex}.rmap-dropdown-container .rmap-dropdown-content{transform-origin:center top}.rmap-dropdown-container .rmap-dropdown-content-enter{animation:rmap-appearance-in .25s ease-out normal forwards}.rmap-dropdown-container .rmap-dropdown-content-leave{animation:rmap-appearance-out .1s ease-in forwards}.rmap-volume-container{position:relative;height:119px;width:32px;display:flex;align-items:center;justify-content:center}.rmap-volume-container[data-placement=top]{bottom:auto}.rmap-volume-container[data-placement=left]{transform:rotate(-90deg);right:50px}.rmap-volume-container[data-placement=right]{transform:rotate(90deg);left:50px}.rmap-volume-container[data-placement=bottom]{transform:rotateX(180deg);top:5px}.rmap-volume-container .rmap-volume-panel{width:30px;background-color:var(--rm-audio-player-volume-panel-background);border:1px solid var(--rm-audio-player-volume-panel-border);border-radius:5px;height:118px;box-shadow:0 2px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 10%);position:absolute;bottom:5px}.rmap-volume-container .rmap-volume-panel:before{content:"";bottom:-10px;left:7.9px;border-color:transparent transparent var(--rm-audio-player-volume-panel-border) var(--rm-audio-player-volume-panel-border);border-style:solid;border-width:5px;box-shadow:-3px 3px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 10%);position:absolute;width:0;height:0;box-sizing:border-box;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);pointer-events:none;z-index:0}.rmap-volume-container .rmap-volume-panel:after{content:"";bottom:-8px;left:9px;border-color:transparent transparent var(--rm-audio-player-volume-panel-background) var(--rm-audio-player-volume-panel-background);border-style:solid;border-width:4px;z-index:1;position:absolute;width:0;height:0;box-sizing:border-box;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);pointer-events:none}.rmap-volume-container input[type=range]{margin-left:14px;position:absolute;display:block;top:-45px;left:0;height:2px;width:92px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--rm-audio-player-volume-background);transform-origin:75px 75px;transform:rotate(-90deg)}.rmap-volume-container input:focus:not(:focus-visible){outline-color:transparent}.rmap-volume-container input::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:16px;height:16px;border-radius:12px;overflow:visible;background:var(--rm-audio-player-volume-thumb)}.rmap-volume-container input::-moz-range-thumb{width:16px;height:16px;border-radius:12px;overflow:visible;background:var(--rm-audio-player-volume-thumb);border:none}.rmap-volume-container input::-moz-range-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;overflow:visible;color:transparent;cursor:pointer;border-radius:2%/50%;border-color:transparent;background-color:transparent;background-position:center;background-repeat:no-repeat;background-size:100% 3px;background-image:linear-gradient(90deg,var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),var(--rm-audio-player-volume-track) var(--rm-audio-player-volume-value))}.rmap-volume-container input::-webkit-slider-runnable-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;overflow:visible;color:transparent;cursor:pointer;border-radius:2%/50%;border-color:transparent;background-color:transparent;background-position:center;background-repeat:no-repeat;background-size:100% 3px;background-image:linear-gradient(90deg,var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),var(--rm-audio-player-volume-track) var(--rm-audio-player-volume-value))}.rmap-artwork-container{display:flex;align-items:center;width:100%;height:100%}.rmap-artwork-container img,.rmap-artwork-container .rmap-artwork-fallback{width:50px;height:50px}.rmap-artwork-container .rmap-artwork-fallback{display:flex;align-items:center;justify-content:center;overflow:hidden;font-size:13px}.rmap-track-info-container{display:grid;align-items:center;row-gap:5px}.rmap-track-info-container span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rmap-track-info-container .title{font-size:16px}.rmap-track-info-container .writer{font-size:12px}.rmap-track-time-container{display:flex;align-items:center;justify-content:center;min-height:16px;font-family:monospace!important;font-size:16px!important}.rmap-track-time-container[data-position=left] .rmap-track-current-time,.rmap-track-time-container[data-position=left] .rmap-track-duration{margin-right:-10px}.rmap-track-time-container[data-position=right] .rmap-track-current-time:before,.rmap-track-time-container[data-position=right] .rmap-track-duration:before{content:"/";margin:0 .3rem}.rmap-track-time-current .rmap-track-current-time{font-weight:700;letter-spacing:-.1rem;color:var(--rm-audio-player-track-current-time)}.rmap-track-time-duration .rmap-track-duration{display:flex;color:var(--rm-audio-player-track-duration);letter-spacing:-.1rem}.rmap-interface-container .rmap-interface-grid{background:var(--rm-audio-player-interface-container);padding:.5rem 10px}.rmap-interface-container .rmap-sortable-playlist{background:var(--rm-audio-player-sortable-list);box-shadow:-5px 2px 4px rgb(var(--rm-audio-player-shadow, 0 0 0) / 4%) inset}