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

# Setup

> Install the components with the BeyondWords CLI and build your first player.

export const StandardPlayerDemo = () => {
  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}`;
    } else {
      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 [duration, setDuration] = useState(0);
  const [playbackRate, setPlaybackRate] = useState(1);
  const [currentTime, setCurrentTime] = useState(0);
  const [content, setContent] = useState([]);
  const [contentIndex, setContentIndex] = useState(0);
  const [analyticsId, setAnalyticsId] = useState(null);
  const [isAdvert, setIsAdvert] = useState(false);
  const [isIntroOutro, setIsIntroOutro] = useState(false);
  const [hovered, setHovered] = useState(false);
  const [inView, setInView] = useState(true);
  const [closed, setClosed] = useState(false);
  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);
        setDuration(instance.duration);
        setPlaybackRate(instance.playbackRate);
        setCurrentTime(instance.currentTime);
        setContent(instance.content);
        setContentIndex(instance.contentIndex);
        setAnalyticsId(instance.analyticsId);
        setIsAdvert(!!instance.adverts[instance.advertIndex] && instance.playbackState !== "stopped");
        setIsIntroOutro(!!instance.introsOutros[instance.introsOutrosIndex] && instance.playbackState !== "stopped");
      };
      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 widgetRef = useRef(null);
  const barRef = useRef(null);
  const mouseDownRef = useRef(false);
  const readonly = isAdvert || isIntroOutro;
  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 (readonly || !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 || readonly) return;
    const handleMouseMove = event => {
      if (!mouseDownRef.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, readonly]);
  useEffect(() => {
    const el = widgetRef.current;
    if (!el) return;
    const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
      threshold: 0
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, [player]);
  const isPlaying = playbackState === "playing";
  const isStopped = playbackState === "stopped";
  const mode = inView || closed || isStopped ? "inline" : "widget";
  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 ref={widgetRef} style={{
    height: "3rem"
  }}>
        <div onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    gap: "0.5rem",
    height: "3rem",
    width: "536px",
    borderRadius: "25px",
    backgroundColor: "#F5F5F5",
    padding: "0.25rem",
    paddingRight: "1rem",
    ...mode === "widget" && ({
      position: "fixed",
      right: "1rem",
      bottom: "1rem",
      zIndex: 50,
      width: "auto",
      minWidth: "15rem",
      boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
    })
  }}>
          <button type="button" style={{
    cursor: "pointer",
    background: "none",
    border: 0,
    padding: 0,
    display: "flex"
  }} onClick={() => {
    const name = isPlaying ? "Pause" : "Play";
    player.onEvent(newEvent({
      type: `Pressed${name}`,
      description: `The ${name.toLowerCase()} button was pressed.`
    }));
  }}>
            {isPlaying ? <svg width={40} height={40} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path fill="rgba(0, 0, 0, 0.8)" 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="rgba(0, 0, 0, 0.8)" 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>

          {isStopped && <div style={{
    flex: 1
  }}>
              <div style={{
    fontSize: "0.75rem",
    lineHeight: "1rem",
    fontWeight: 500,
    color: "#111"
  }}>
                {callToAction ?? "Listen to this article"}
              </div>
              <div style={{
    fontSize: "10px",
    lineHeight: "normal",
    color: "#111"
  }}>
                {Math.ceil(duration / 60)} min
              </div>
            </div>}

          {!isStopped && <div style={{
    display: "flex",
    flexGrow: 1,
    flexDirection: "row",
    alignItems: "center",
    gap: "0.5rem"
  }}>
              <button type="button" style={{
    width: "2.5rem",
    height: "2.5rem",
    cursor: "pointer",
    fontSize: "0.75rem",
    fontWeight: 500,
    background: "none",
    border: 0
  }} onClick={() => {
    player.onEvent(newEvent({
      type: "PressedChangeRate",
      description: "The change playback rate button was pressed."
    }));
  }}>
                {playbackRate}x
              </button>

              <button type="button" style={{
    cursor: "pointer",
    background: "none",
    border: 0,
    padding: 0,
    display: "flex"
  }} onClick={() => {
    player.onEvent(newEvent({
      type: "PressedPrevSegment",
      description: "The previous segment button was pressed."
    }));
  }}>
                <svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path fill="rgba(0, 0, 0, 0.8)" fillRule="evenodd" d="M19.4 8.08 Q20.45 7.5 20.45 8.7 L20.45 15.3 Q20.45 16.5 19.4 15.92 L13.61 12.73 Q12.3 12 13.61 11.27ZM4.6 7.3h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1ZM4.6 10.95h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1ZM4.6 14.6h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1Z" />
                </svg>
              </button>

              <button type="button" style={{
    cursor: "pointer",
    background: "none",
    border: 0,
    padding: 0,
    display: "flex"
  }} onClick={() => {
    player.onEvent(newEvent({
      type: "PressedNextSegment",
      description: "The next segment button was pressed."
    }));
  }}>
                <svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path fill="rgba(0, 0, 0, 0.8)" fillRule="evenodd" d="M4.6 8.08 Q3.55 7.5 3.55 8.7 L3.55 15.3 Q3.55 16.5 4.6 15.92 L10.39 12.73 Q11.7 12 10.39 11.27ZM15.15 7.3h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1ZM15.15 10.95h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1ZM15.15 14.6h4.25a1.05 1.05 0 0 1 0 2.1h-4.25a1.05 1.05 0 0 1 0 -2.1Z" />
                </svg>
              </button>

              <div style={{
    fontSize: "10px",
    fontWeight: 300,
    color: "#111",
    fontVariantNumeric: "tabular-nums"
  }}>
                {formatTime(currentTime)} / {formatTime(duration)}
              </div>

              {mode === "inline" && <div ref={barRef} style={{
    position: "relative",
    height: "0.5rem",
    flexGrow: 1,
    overflow: "hidden",
    borderRadius: "0.25rem",
    cursor: readonly ? "auto" : "pointer"
  }} onMouseDown={handleMouseDown} onTouchStart={handleMouseDown}>
                  <div style={{
    position: "absolute",
    inset: 0,
    backgroundColor: "rgba(0,0,0,0.8)",
    opacity: 0.15
  }} />
                  <div style={{
    height: "0.5rem",
    borderRadius: "0.25rem",
    backgroundColor: "rgba(0,0,0,0.8)",
    width: `${progress * 100}%`,
    pointerEvents: "none"
  }} />
                </div>}

              {mode === "widget" && <button type="button" aria-label="Close widget" style={{
    cursor: "pointer",
    background: "none",
    border: 0,
    padding: 0,
    display: "flex"
  }} onClick={() => setClosed(true)}>
                  <svg width={16} height={16} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path stroke="rgba(0, 0, 0, 0.8)" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" d="M7 7 17 17M17 7 7 17" />
                  </svg>
                </button>}
            </div>}

          {mode === "inline" && <a href={`https://beyondwords.io/?utm_source=${encodeURIComponent(window.location.origin)}&utm_medium=player&utm_campaign=${analyticsId}`} target="_blank" style={{
    cursor: "pointer",
    opacity: isPlaying || hovered ? 1 : 0,
    transition: "opacity 0.5s",
    position: "relative",
    zIndex: 50
  }}>
            <svg width={16} height={16} viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path fill="rgba(0, 0, 0, 0.8)" d="M29.4,18.59A20.61,20.61,0,0,0,28,15c2.62-5.17,2.8-10.15,0-13a7.49,7.49,0,0,0-5.47-2,14.14,14.14,0,0,0-3.9.6A21,21,0,0,0,15,2,17,17,0,0,0,7.59,0H7.53A7.57,7.57,0,0,0,2.05,2.05,7.59,7.59,0,0,0,0,7.54v0A17,17,0,0,0,2,15C-.58,20.17-.76,25.14,2.05,28A7.54,7.54,0,0,0,7.56,30,16.8,16.8,0,0,0,15,28a20.06,20.06,0,0,0,3.57,1.44,14.08,14.08,0,0,0,3.89.6A7.53,7.53,0,0,0,28,28C30,25.88,30.55,22.55,29.4,18.59ZM19.18,2.67a11.78,11.78,0,0,1,3.29-.52,5.46,5.46,0,0,1,4,1.41c1.5,1.5,1.81,4.08.89,7.25a18.56,18.56,0,0,1-.68,1.88,31.07,31.07,0,0,0-4.17-5.15A31.16,31.16,0,0,0,17.3,3.35,18.56,18.56,0,0,1,19.18,2.67ZM3.56,3.56a5.43,5.43,0,0,1,4-1.41,13.77,13.77,0,0,1,5.19,1.2A31.27,31.27,0,0,0,7.54,7.53a30.81,30.81,0,0,0-4.19,5.18C1.76,8.91,1.69,5.44,3.56,3.56Zm4,24.27a5.44,5.44,0,0,1-3.95-1.4c-1.5-1.5-1.82-4.08-.89-7.25a18.29,18.29,0,0,1,.67-1.87,31.1,31.1,0,0,0,4.19,5.16,31.07,31.07,0,0,0,5.15,4.17A13.85,13.85,0,0,1,7.52,27.83ZM15,25.52A27.82,27.82,0,0,1,9.05,21a28,28,0,0,1-4.57-6,28.51,28.51,0,0,1,4.58-6A27.82,27.82,0,0,1,15,4.48a27.68,27.68,0,0,1,6,4.57A27.86,27.86,0,0,1,25.52,15a27.9,27.9,0,0,1-4.58,6A28,28,0,0,1,15,25.52Zm11.49.93a5.47,5.47,0,0,1-4,1.42,11.78,11.78,0,0,1-3.29-.52,16.79,16.79,0,0,1-1.9-.69,31.29,31.29,0,0,0,5.18-4.2,31.35,31.35,0,0,0,4.2-5.19,16.93,16.93,0,0,1,.69,1.91C28.27,22.36,28,24.94,26.45,26.45Z" />
            </svg>
          </a>}
        </div>
        </div>}
    </div>;
};

The components are distributed as a shadcn registry. Add them with the CLI, and the source is copied into your project.

## Prerequisites

* A React project with [Tailwind CSS](https://tailwindcss.com). The components
  ship a little Tailwind for layout. Replace it with your own CSS if you prefer.
* A BeyondWords **project ID** and a **source ID** (or content ID) for the audio
  you want to play. The examples below use a public demo project.

<Info>
  You don't need to install `@beyondwords/player` yourself. It's listed as a dependency of the components, so the
  CLI adds it for you.
</Info>

## Add a component

Add the [Play Pause Button](/player-ui/react/play-pause-button). No configuration is
needed. The CLI resolves its dependencies automatically, so this also pulls in
[Player Provider](/player-ui/react/player-provider) and
[New Event](/player-ui/react/new-event), plus the `@beyondwords/player` package.

```bash theme={null}
npx @beyondwords/cli components add play-pause-button
```

If your project has a `components.json`, the CLI delegates the install to
[shadcn](https://ui.shadcn.com), so your aliases and styles config are
respected. Without one, it copies the files to `components/beyondwords/` (it
asks first). Adjust the `@/registry/react/` import paths in these docs to
wherever the files landed.

## Build your first player

Wrap your UI in [`PlayerProvider`](/player-ui/react/player-provider). It mounts
the player and shares the instance with everything inside it. Pass the player
config through `args`, and set `showUserInterface: false` to hide the player's
built-in UI so yours is the only one on the page.

```tsx app.tsx theme={null}
import { PlayerProvider } from '@/registry/react/PlayerProvider';
import { PlayPauseButton } from '@/registry/react/PlayPauseButton';

export function App() {
  return (
    <PlayerProvider args={{ projectId: 9504, contentId: 'ba00ef51-03ac-4a85-8b98-e45c957e8ef8', showUserInterface: false }}>
      <PlayPauseButton />
    </PlayerProvider>
  );
}
```

Every component reads what it needs from the surrounding provider, so adding
controls is just composition. Drop a
[Progress Bar](/player-ui/react/progress-bar) and a
[Time Indicator](/player-ui/react/time-indicator) into a flex row:

```tsx app.tsx theme={null}
import { PlayerProvider } from '@/registry/react/PlayerProvider';
import { PlayPauseButton } from '@/registry/react/PlayPauseButton';
import { ProgressBar } from '@/registry/react/ProgressBar';
import { TimeIndicator } from '@/registry/react/TimeIndicator';

export function App() {
  return (
    <PlayerProvider args={{ projectId: 9504, contentId: 'ba00ef51-03ac-4a85-8b98-e45c957e8ef8', showUserInterface: false }}>
      <div className="flex items-center gap-3">
        <PlayPauseButton />
        <ProgressBar />
        <TimeIndicator />
      </div>
    </PlayerProvider>
  );
}
```

That's a working player. It looks plain because the components are minimal by
design; [Theming](/player-ui/react/theming) shows how to make it yours.

To start from something finished instead, the
[Standard Player](/player-ui/react/standard-player) example assembles all of
these controls into a polished layout. It wraps its own provider, so you only
pass `projectId` and `contentId`:

<StandardPlayerDemo />

## Using the shadcn CLI instead (optional)

The registry also works with the shadcn CLI directly. Add components by full
URL:

```bash theme={null}
npx shadcn add https://ui.beyondwords.io/r/react/play-pause-button.json
```

Or register the registry namespace once in your `components.json`, then use
short names:

```json components.json theme={null}
{
  "registries": {
    "@beyondwords-react": "https://ui.beyondwords.io/r/react/{name}.json"
  }
}
```

```bash theme={null}
npx shadcn add @beyondwords-react/play-pause-button
```

## Next steps

<CardGroup cols={2}>
  <Card title="Theming" href="/player-ui/react/theming">Style the components to match your brand.</Card>
  <Card title="Player Provider" href="/player-ui/react/player-provider">Read player state and drive playback.</Card>
  <Card title="Examples" href="/player-ui/react/standard-player">Start from a complete, pre-built player.</Card>
  <Card title="Troubleshooting" href="/player-ui/react/troubleshooting">Fix common setup issues.</Card>
</CardGroup>
