> ## 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.

# Minimal Player

> A transparent, ultra-minimal player that expands from play + call to action into rate, seek-back, and time controls.

export const MinimalPlayerDemo = () => {
  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 RATES = Array.from({
    length: 12
  }, (_, i) => Math.round((0.9 + i * 0.1) * 10) / 10);
  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 [playbackRate, setPlaybackRate] = useState(1);
  useEffect(() => {
    if (!target) return;
    let instance;
    const init = () => {
      instance = new window.BeyondWords.Player({
        target,
        projectId: 9504,
        contentId: "ba00ef51-03ac-4a85-8b98-e45c957e8ef8",
        widgetStyle: "none",
        showUserInterface: false,
        analyticsConsent: "none"
      });
      const sync = () => {
        setPlaybackState(instance.playbackState);
        setCallToAction(instance.callToAction);
        setCurrentTime(instance.currentTime);
        setPlaybackRate(instance.playbackRate);
      };
      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 isPlaying = playbackState === "playing";
  const stopped = playbackState === "stopped";
  const rateMenuId = useId();
  const rateMenuRef = useRef(null);
  const [rateMenuOpen, setRateMenuOpen] = useState(false);
  useEffect(() => {
    if (!rateMenuOpen) return;
    const onPointerDown = event => {
      if (!rateMenuRef.current?.contains(event.target)) setRateMenuOpen(false);
    };
    const onKeyDown = event => {
      if (event.key === "Escape") setRateMenuOpen(false);
    };
    document.addEventListener("pointerdown", onPointerDown);
    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("pointerdown", onPointerDown);
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [rateMenuOpen]);
  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",
    width: "536px",
    alignItems: "center",
    gap: "0.75rem",
    color: "#171717"
  }}>
          <button type="button" aria-label={isPlaying ? "Pause audio" : "Play audio"} onClick={() => {
    const name = isPlaying ? "Pause" : "Play";
    player.onEvent(newEvent({
      type: `Pressed${name}`,
      description: `The ${name.toLowerCase()} button was pressed.`
    }));
  }} style={{
    display: "flex",
    width: "2rem",
    height: "2rem",
    flexShrink: 0,
    cursor: "pointer",
    alignItems: "center",
    justifyContent: "center",
    background: "none",
    border: "none",
    padding: 0,
    color: "inherit"
  }}>
            {isPlaying ? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={32} height={32} fill="currentColor" aria-hidden="true">
                <path 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={32} height={32} fill="currentColor" aria-hidden="true">
                <path 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={{
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap",
    fontSize: "1rem",
    lineHeight: "1.5rem",
    fontWeight: 500
  }}>
              {callToAction ?? "Listen to article"}
            </div> : <>
              <button type="button" aria-label="Skip back 10 seconds" onClick={() => player.onEvent(newEvent({
    type: "PressedSeekBack",
    description: "The seek backward button was pressed.",
    seconds: 10
  }))} style={{
    flexShrink: 0,
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0,
    color: "inherit"
  }}>
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={28} height={28} fill="none" aria-hidden="true">
                  <path d="M7 9l-3 -3l3 -3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M15.997 17.918a6.002 6.002 0 0 0 -.997 -11.918h-11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M6 14v6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M9 15.5v3a1.5 1.5 0 0 0 3 0v-3a1.5 1.5 0 0 0 -3 0" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </button>
              <div ref={rateMenuRef} style={{
    position: "relative",
    flexShrink: 0
  }}>
                <button type="button" aria-haspopup="listbox" aria-expanded={rateMenuOpen} aria-controls={rateMenuId} onClick={() => setRateMenuOpen(value => !value)} style={{
    cursor: "pointer",
    borderRadius: "0.375rem",
    padding: "0.25rem 0.5rem",
    fontSize: "0.875rem",
    fontWeight: 500,
    fontVariantNumeric: "tabular-nums",
    background: "none",
    border: "none",
    color: "inherit"
  }}>
                  {playbackRate}x
                </button>
                {rateMenuOpen && <div id={rateMenuId} role="listbox" aria-label="Playback speed" style={{
    position: "absolute",
    top: "100%",
    left: 0,
    zIndex: 50,
    marginTop: "0.5rem",
    minWidth: "9rem",
    overflow: "hidden",
    borderRadius: "0.75rem",
    background: "#171717",
    padding: "0.25rem 0",
    color: "#fff",
    boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)"
  }}>
                    <ul style={{
    maxHeight: "18rem",
    overflow: "auto",
    margin: 0,
    padding: 0,
    listStyle: "none"
  }}>
                      {RATES.map(rate => {
    const selected = Math.abs(rate - playbackRate) < 0.001;
    return <li key={rate} role="option" aria-selected={selected}>
                            <button type="button" onClick={() => {
      player.playbackRate = rate;
      setRateMenuOpen(false);
    }} style={{
      display: "flex",
      width: "100%",
      cursor: "pointer",
      alignItems: "center",
      justifyContent: "space-between",
      gap: "1.5rem",
      padding: "0.5rem 1rem",
      textAlign: "left",
      fontSize: "0.875rem",
      fontVariantNumeric: "tabular-nums",
      color: "#fff",
      background: selected ? "rgb(255 255 255 / 0.05)" : "transparent",
      border: "none",
      fontWeight: selected ? 500 : 400
    }}>
                              <span>{rate}x</span>
                              {selected && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width={16} height={16} fill="currentColor" aria-hidden="true">
                                  <path fillRule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0l-3.5-3.5a1 1 0 1 1 1.4-1.4L8.5 12l6.8-6.7a1 1 0 0 1 1.4 0Z" />
                                </svg>}
                            </button>
                          </li>;
  })}
                    </ul>
                  </div>}
              </div>
              <div style={{
    flexShrink: 0,
    fontSize: "1rem",
    fontVariantNumeric: "tabular-nums"
  }}>{formatTime(currentTime)}</div>
            </>}
        </div>}
    </div>;
};

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

  <Tab title="Code">
    ```jsx MinimalPlayer.jsx theme={null}
    "use client";

    import { CallToAction } from "@/registry/react/blocks/minimal/CallToAction";
    import { CurrentTime } from "@/registry/react/blocks/minimal/CurrentTime";
    import { PlaybackRateMenu } from "@/registry/react/blocks/minimal/PlaybackRateMenu";
    import { PlayerProvider, usePlayerSelector } from "@/registry/react/blocks/minimal/PlayerProvider";
    import { PlayPauseButton } from "@/registry/react/blocks/minimal/PlayPauseButton";
    import { PrevSecondsButton } from "@/registry/react/blocks/minimal/PrevSecondsButton";

    function Bar() {
      const stopped = usePlayerSelector((p) => p.playbackState) === "stopped";
      return (
        <div className="flex w-[536px] items-center gap-3 text-neutral-900 dark:text-neutral-100">
          <PlayPauseButton />
          {stopped ? (
            <CallToAction />
          ) : (
            <>
              <PrevSecondsButton seconds={10} />
              <PlaybackRateMenu />
              <CurrentTime />
            </>
          )}
        </div>
      );
    }

    export function MinimalPlayer({ projectId, sourceId, ...args }) {
      return (
        <PlayerProvider args={{ projectId, sourceId, showUserInterface: false, ...args }}>
          <Bar />
        </PlayerProvider>
      );
    }
    ```
  </Tab>
</Tabs>

## Installation

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

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

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

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

## Usage

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

export function Player() {
  return <MinimalPlayer projectId={9504} contentId="ba00ef51-03ac-4a85-8b98-e45c957e8ef8" />;
}
```

`MinimalPlayer` is a transparent single-line control strip — no background pill.
While stopped it shows only play and a "Listen to article" call to action. Once
playback starts it swaps to the speed dropdown (0.9x–2x), skip-back 10s, and
current time. It wraps its own [`PlayerProvider`](/player-ui/react/player-provider),
so you pass only `projectId` and `contentId`.
