> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beyondwords.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Playlist Player

> A playlist player example with a scrollable track list alongside playback controls.

export const PlaylistPlayerDemo = () => {
  const newEvent = props => ({
    id: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    status: "pending",
    initiatedBy: "user",
    ...props
  });
  const formatTime = time => {
    if (time < 0) time = Math.max(0, time);
    const hours = Math.floor(time / 3600).toString();
    let minutes = Math.floor(time % 3600 / 60).toString();
    let seconds = Math.floor(time % 60).toString();
    if (seconds.length < 2) seconds = `0${seconds}`;
    if (time < 3600) return `${minutes}:${seconds}`;
    if (minutes.length < 2) minutes = `0${minutes}`;
    return `${hours}:${minutes}:${seconds}`;
  };
  const [target, setTarget] = useState(null);
  const [player, setPlayer] = useState(null);
  const [playbackState, setPlaybackState] = useState("stopped");
  const [callToAction, setCallToAction] = useState(null);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [playbackRate, setPlaybackRate] = useState(1);
  const [contentIndex, setContentIndex] = useState(0);
  const [content, setContent] = useState([]);
  const [playlistOpen, setPlaylistOpen] = useState(true);
  const barRef = useRef(null);
  const mouseDownRef = useRef(false);
  useEffect(() => {
    if (!target) return;
    let instance;
    const init = () => {
      instance = new window.BeyondWords.Player({
        target,
        projectId: 9504,
        playlistId: 153102,
        widgetStyle: "none",
        showUserInterface: false,
        analyticsConsent: "none"
      });
      const sync = () => {
        setPlaybackState(instance.playbackState);
        setCallToAction(instance.callToAction);
        setCurrentTime(instance.currentTime);
        setDuration(instance.duration);
        setPlaybackRate(instance.playbackRate);
        setContentIndex(instance.contentIndex);
        setContent(instance.content);
      };
      instance.addEventListener("<any>", sync);
      sync();
      setPlayer(instance);
    };
    if (window.BeyondWords) init(); else window.addEventListener("BeyondWordsReady", init);
    return () => {
      window.removeEventListener("BeyondWordsReady", init);
      if (instance) instance.destroy();
    };
  }, [target]);
  const stopped = playbackState === "stopped";
  const progress = duration > 0 ? Math.max(0, Math.min(currentTime / duration, 1)) : 0;
  const getMouseRatio = event => {
    const clientX = (event.clientX ?? event.touches?.[0]?.clientX) ?? 0;
    const {x, width} = barRef.current.getBoundingClientRect();
    return Math.max(0, Math.min(1, (clientX - x) / width));
  };
  const handleMouseDown = event => {
    if (!player) return;
    mouseDownRef.current = true;
    player.onEvent(newEvent({
      type: "PressedProgressBar",
      description: "The progress bar was pressed at some ratio.",
      initiatedBy: "user",
      ratio: getMouseRatio(event)
    }));
  };
  useEffect(() => {
    if (!player) return;
    const handleMouseMove = event => {
      if (!mouseDownRef.current || !barRef.current) return;
      player.onEvent(newEvent({
        type: "ScrubbedProgressBar",
        description: "The user pressed on the progress bar then dragged.",
        initiatedBy: "user",
        ratio: getMouseRatio(event)
      }));
    };
    const handleMouseUp = () => {
      if (!mouseDownRef.current) return;
      mouseDownRef.current = false;
      player.onEvent(newEvent({
        type: "FinishedScrubbingProgressBar",
        description: "The user let go after scrubbing the progress bar.",
        initiatedBy: "user"
      }));
    };
    window.addEventListener("mouseup", handleMouseUp);
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("touchend", handleMouseUp);
    window.addEventListener("touchmove", handleMouseMove);
    return () => {
      window.removeEventListener("mouseup", handleMouseUp);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("touchend", handleMouseUp);
      window.removeEventListener("touchmove", handleMouseMove);
    };
  }, [player]);
  return <div className="not-prose flex w-full items-center justify-center rounded-lg border p-10" style={{
    minHeight: "16rem",
    fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
  }}>
      <div ref={setTarget} style={{
    display: "none"
  }} />
      {player && <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "1rem",
    width: "536px"
  }}>
          <div style={{
    display: "flex",
    height: "3rem",
    flexDirection: "row",
    alignItems: "center",
    gap: "0.5rem",
    borderRadius: "25px",
    backgroundColor: "#F5F5F5",
    padding: "0.25rem",
    paddingRight: "1rem"
  }}>
            <button type="button" onClick={() => {
    const name = playbackState === "playing" ? "Pause" : "Play";
    player.onEvent(newEvent({
      type: `Pressed${name}`,
      description: `The ${name.toLowerCase()} button was pressed.`
    }));
  }} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
              {playbackState === "playing" ? <svg width={40} height={40} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path fill="#323232" fillRule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20ZM8.7 8.95a1.05 1.05 0 0 1 2.1 0v6.1a1.05 1.05 0 0 1 -2.1 0ZM13.2 8.95a1.05 1.05 0 0 1 2.1 0v6.1a1.05 1.05 0 0 1 -2.1 0Z" />
                </svg> : <svg width={40} height={40} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path fill="#323232" fillRule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20ZM10.45 8.48 Q9.4 7.9 9.4 9.1 L9.4 14.9 Q9.4 16.1 10.45 15.52 L15.4 12.78 Q16.8 12 15.4 11.22Z" />
                </svg>}
            </button>
            {stopped ? <div style={{
    flex: 1
  }}>
                <div style={{
    fontSize: "0.75rem",
    lineHeight: "1rem",
    fontWeight: 500,
    color: "#111"
  }}>
                  {callToAction ?? "Listen to this article"}
                </div>
                <div style={{
    fontSize: "0.625rem",
    lineHeight: "normal",
    color: "#111"
  }}>
                  {Math.ceil(duration / 60)} min
                </div>
              </div> : <div style={{
    display: "flex",
    flexGrow: 1,
    flexDirection: "row",
    alignItems: "center",
    gap: "0.5rem"
  }}>
                <button type="button" onClick={() => player.onEvent(newEvent({
    type: "PressedChangeRate",
    description: "The change playback rate button was pressed."
  }))} style={{
    width: "2.5rem",
    height: "2.5rem",
    cursor: "pointer",
    fontSize: "0.75rem",
    fontWeight: 500,
    background: "none",
    border: "none",
    color: "#323232"
  }}>
                  {playbackRate}x
                </button>
                <button type="button" onClick={() => player.onEvent(newEvent({
    type: "PressedPrevTrack",
    description: "The previous track button was pressed."
  }))} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                  <svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill="#323232" fillRule="evenodd" d="M5.4 8.35a1.05 1.05 0 0 1 2.1 0v7.3a1.05 1.05 0 0 1 -2.1 0ZM17.5 8.08 Q18.55 7.5 18.55 8.7 L18.55 15.3 Q18.55 16.5 17.5 15.92 L11.71 12.73 Q10.4 12 11.71 11.27Z" />
                  </svg>
                </button>
                <button type="button" onClick={() => player.onEvent(newEvent({
    type: "PressedNextTrack",
    description: "The next track button was pressed."
  }))} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                  <svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path fill="#323232" fillRule="evenodd" d="M6.5 8.08 Q5.45 7.5 5.45 8.7 L5.45 15.3 Q5.45 16.5 6.5 15.92 L12.29 12.73 Q13.6 12 12.29 11.27ZM16.5 8.35a1.05 1.05 0 0 1 2.1 0v7.3a1.05 1.05 0 0 1 -2.1 0Z" />
                  </svg>
                </button>
                <div style={{
    fontSize: "0.625rem",
    fontWeight: 300,
    color: "#323232",
    fontVariantNumeric: "tabular-nums",
    whiteSpace: "nowrap"
  }}>
                  {formatTime(currentTime)} / {formatTime(duration)}
                </div>
                <div ref={barRef} onMouseDown={handleMouseDown} onTouchStart={handleMouseDown} style={{
    position: "relative",
    height: "0.5rem",
    flexGrow: 1,
    overflow: "hidden",
    borderRadius: "0.25rem",
    cursor: "pointer"
  }}>
                  <div style={{
    position: "absolute",
    inset: 0,
    backgroundColor: "#323232",
    opacity: 0.15
  }} />
                  <div style={{
    pointerEvents: "none",
    height: "0.5rem",
    borderRadius: "0.25rem",
    backgroundColor: "#323232",
    width: `${progress * 100}%`
  }} />
                </div>
              </div>}
            <button type="button" aria-label={playlistOpen ? "Hide playlist" : "Show playlist"} aria-expanded={playlistOpen} onClick={() => setPlaylistOpen(open => !open)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0,
    color: "#323232"
  }}>
              <svg width={20} height={20} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                <path fillRule="evenodd" d="M4.8 5.35h14.4a1.05 1.05 0 0 1 0 2.1h-14.4a1.05 1.05 0 0 1 0 -2.1ZM4.8 10.95h14.4a1.05 1.05 0 0 1 0 2.1h-14.4a1.05 1.05 0 0 1 0 -2.1ZM4.8 16.55h6.65a1.05 1.05 0 0 1 0 2.1h-6.65a1.05 1.05 0 0 1 0 -2.1ZM16.18 15.33 Q15.3 14.85 15.3 15.85 L15.3 19.35 Q15.3 20.35 16.18 19.87 L19.3 18.17 Q20.35 17.6 19.3 17.03Z" />
              </svg>
            </button>
          </div>
          {playlistOpen && <div style={{
    maxHeight: "15rem",
    overflowX: "hidden",
    overflowY: "scroll",
    borderRadius: "0.375rem",
    backgroundColor: "#F5F5F5",
    padding: "0.2rem 0 0.2rem 0.25rem"
  }}>
              {content.map((item, index) => <button key={index} onClick={() => player.onEvent(newEvent({
    type: "PressedPlaylistItem",
    description: "A playlist item was pressed.",
    index
  }))} style={{
    display: "grid",
    height: "2.5rem",
    width: "100%",
    cursor: "pointer",
    gridTemplateColumns: "auto 1fr auto",
    alignItems: "center",
    gap: "0.5rem",
    paddingRight: "1rem",
    background: "none",
    border: "none"
  }}>
                  <div style={{
    display: "flex",
    width: "2.5rem",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    fontSize: "11px",
    lineHeight: 1,
    color: "#161119"
  }}>
                    {contentIndex === index ? <svg width={16} height={16} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                        <path fillRule="evenodd" d="M5.4 8.8a1.2 1.2 0 0 1 2.4 0v6.4a1.2 1.2 0 0 1 -2.4 0ZM10.8 6a1.2 1.2 0 0 1 2.4 0v12a1.2 1.2 0 0 1 -2.4 0ZM16.2 10.4a1.2 1.2 0 0 1 2.4 0v3.2a1.2 1.2 0 0 1 -2.4 0Z" />
                      </svg> : index + 1}
                  </div>
                  <div style={{
    textAlign: "start",
    fontSize: "11px",
    lineHeight: 1,
    color: "#161119",
    fontWeight: contentIndex === index ? 500 : 400,
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap"
  }}>
                    {item.title}
                  </div>
                  <div style={{
    fontSize: "11px",
    lineHeight: 1,
    color: "#161119"
  }}>
                    {Math.ceil(item.duration / 60)} min
                  </div>
                </button>)}
            </div>}
        </div>}
    </div>;
};

<Tabs>
  <Tab title="Preview">
    <PlaylistPlayerDemo />
  </Tab>

  <Tab title="Code">
    ```jsx PlaylistPlayer.jsx theme={null}
    import { CallToAction } from "@/registry/react/blocks/playlist/CallToAction";
    import { Duration } from "@/registry/react/blocks/playlist/Duration";
    import { NextTrackButton } from "@/registry/react/blocks/playlist/NextTrackButton";
    import { PlaybackControls } from "@/registry/react/blocks/playlist/PlaybackControls";
    import { PlaybackRateButton } from "@/registry/react/blocks/playlist/PlaybackRateButton";
    import { PlayerInfo } from "@/registry/react/blocks/playlist/PlayerInfo";
    import { PlayerProvider } from "@/registry/react/blocks/playlist/PlayerProvider";
    import { Playlist, PlaylistProvider, PlaylistToggleButton } from "@/registry/react/blocks/playlist/Playlist";
    import { PlayPauseButton } from "@/registry/react/blocks/playlist/PlayPauseButton";
    import { PrevTrackButton } from "@/registry/react/blocks/playlist/PrevTrackButton";
    import { ProgressBar } from "@/registry/react/blocks/playlist/ProgressBar";
    import { TimeIndicator } from "@/registry/react/blocks/playlist/TimeIndicator";

    export function PlaylistPlayer({ projectId, playlistId, ...args }) {
      return (
        <PlayerProvider args={{ projectId, playlistId, showUserInterface: false, ...args }}>
          <PlaylistProvider>
            <div className="w-[536px] space-y-4">
              <div className="flex h-12 flex-row items-center gap-2 rounded-[25px] bg-[#F5F5F5] p-1 pr-4">
                <PlayPauseButton />
                <PlayerInfo>
                  <CallToAction />
                  <Duration />
                </PlayerInfo>
                <PlaybackControls>
                  <PlaybackRateButton />
                  <PrevTrackButton />
                  <NextTrackButton />
                  <TimeIndicator />
                  <ProgressBar />
                </PlaybackControls>
                <PlaylistToggleButton />
              </div>
              <Playlist />
            </div>
          </PlaylistProvider>
        </PlayerProvider>
      );
    }
    ```
  </Tab>
</Tabs>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npx @beyondwords/cli components add playlist-player
  ```

  ```bash pnpm theme={null}
  pnpm dlx @beyondwords/cli components add playlist-player
  ```

  ```bash yarn theme={null}
  yarn dlx @beyondwords/cli components add playlist-player
  ```

  ```bash bun theme={null}
  bunx --bun @beyondwords/cli components add playlist-player
  ```
</CodeGroup>

## Usage

```tsx theme={null}
import { PlaylistPlayer } from '@/registry/react/blocks/playlist/PlaylistPlayer';

export function Player() {
  return <PlaylistPlayer projectId={9504} playlistId={153102} />;
}
```

`PlaylistPlayer` is a drop-in layout for a playlist. It wraps its own
[`PlayerProvider`](/player-ui/react/player-provider), so you pass `projectId` and a
`playlistId` instead of a single `sourceId`.

It pairs a control bar with a toggleable [`Playlist`](/player-ui/react/playlist) of the playlist's items. The control bar includes play/pause, a call to action and duration, playback rate, previous/next track, a time indicator, a progress bar, and a button that shows or hides the playlist.
