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

# Standard Player

> A full-featured player example with playback controls, a call to action, and duration.

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>;
};

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

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

    import { cn } from "@/lib/utils";
    import { BeyondWordsLogo } from "@/registry/react/blocks/standard/BeyondWordsLogo";
    import { CallToAction } from "@/registry/react/blocks/standard/CallToAction";
    import { CloseWidgetButton } from "@/registry/react/blocks/standard/CloseWidgetButton";
    import { Duration } from "@/registry/react/blocks/standard/Duration";
    import { NextSegmentButton } from "@/registry/react/blocks/standard/NextSegmentButton";
    import { PlaybackControls } from "@/registry/react/blocks/standard/PlaybackControls";
    import { PlaybackRateButton } from "@/registry/react/blocks/standard/PlaybackRateButton";
    import { PlayerInfo } from "@/registry/react/blocks/standard/PlayerInfo";
    import { PlayerProvider } from "@/registry/react/blocks/standard/PlayerProvider";
    import { PlayPauseButton } from "@/registry/react/blocks/standard/PlayPauseButton";
    import { PrevSegmentButton } from "@/registry/react/blocks/standard/PrevSegmentButton";
    import { ProgressBar } from "@/registry/react/blocks/standard/ProgressBar";
    import { TimeIndicator } from "@/registry/react/blocks/standard/TimeIndicator";
    import { WidgetConsumer, WidgetProvider } from "@/registry/react/blocks/standard/WidgetProvider";

    export function StandardPlayer({ projectId, sourceId, ...args }) {
      return (
        <PlayerProvider args={{ projectId, sourceId, showUserInterface: false, ...args }}>
          <WidgetProvider>
            <WidgetConsumer>
              {({ mode }) => (
                <div
                  className={cn(
                    "group/player-root flex h-12 w-[536px] flex-row items-center gap-2 rounded-[25px] bg-[#F5F5F5] p-1 pr-4",
                    {
                      "fixed right-4 bottom-4 z-50 w-auto min-w-60 shadow-md": mode === "widget",
                    },
                  )}
                >
                  <PlayPauseButton />
                  <PlayerInfo>
                    <CallToAction />
                    <Duration />
                  </PlayerInfo>
                  <PlaybackControls>
                    <PlaybackRateButton />
                    <PrevSegmentButton />
                    <NextSegmentButton />
                    <TimeIndicator />
                    {mode === "inline" && <ProgressBar />}
                    {mode === "widget" && <CloseWidgetButton />}
                  </PlaybackControls>
                  {mode === "inline" && <BeyondWordsLogo />}
                </div>
              )}
            </WidgetConsumer>
          </WidgetProvider>
        </PlayerProvider>
      );
    }
    ```
  </Tab>
</Tabs>

## Try it

An editable example running on [StackBlitz](https://stackblitz.com/edit/react-standard-player?file=src%2FApp.tsx):

<iframe className="not-prose my-4 h-[28rem] w-full rounded-lg border" src="https://stackblitz.com/edit/react-standard-player?embed=1&file=src%2FApp.tsx" allow="cross-origin-isolated" loading="lazy" />

## Installation

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

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

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

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

## Usage

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

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

`StandardPlayer` is a complete, drop-in player layout. It wraps its own
[`PlayerProvider`](/player-ui/react/player-provider), so you pass only `projectId` and
`contentId`. No surrounding provider is needed.

It composes the individual components into a horizontal bar: play/pause, a call
to action and duration, playback rate, previous/next segment, a time indicator,
and a progress bar. While playing and scrolled out of view, it detaches into a
floating widget (see [Widget Provider](/player-ui/react/widget-provider)).

You own the source, so treat it as a starting point. Rearrange or drop
controls to suit your design.
