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

# Progress Bar

> A seekable progress bar that reflects playback and emits scrub events.

export const ProgressBarDemo = () => {
  const MOCK_CONTENT = [{
    id: "demo-1",
    title: "How AI voices are changing publishing",
    imageUrl: "/images/player-ui/demo-cover.svg",
    sourceUrl: "https://beyondwords.io",
    sourceId: "demo",
    adsEnabled: false,
    duration: 124,
    audio: [{
      id: "audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 124
    }],
    video: [],
    summarization: {
      audio: [],
      video: []
    },
    segments: []
  }];
  const newEvent = props => ({
    id: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    status: "pending",
    initiatedBy: "user",
    ...props
  });
  const [target, setTarget] = useState(null);
  const [player, setPlayer] = useState(null);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const barRef = useRef(null);
  const mouseDownRef = useRef(false);
  useEffect(() => {
    if (!target) return;
    let instance;
    const init = () => {
      instance = new window.BeyondWords.Player({
        target,
        content: MOCK_CONTENT,
        widgetStyle: "none",
        showUserInterface: false,
        analyticsConsent: "none"
      });
      const sync = () => {
        setCurrentTime(instance.currentTime);
        setDuration(instance.duration);
      };
      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 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 => {
    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) return;
      if (!barRef.current) {
        handleMouseUp();
        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",
    width: "100%",
    maxWidth: "24rem"
  }}>
          <div ref={barRef} style={{
    position: "relative",
    isolation: "isolate",
    height: "0.5rem",
    flexGrow: 1,
    cursor: "pointer",
    overflow: "hidden",
    borderRadius: "0.25rem"
  }} onMouseDown={handleMouseDown} onTouchStart={handleMouseDown}>
            <div style={{
    position: "absolute",
    inset: 0,
    cursor: "pointer",
    backgroundColor: "rgba(0, 0, 0, 0.8)",
    opacity: 0.15
  }} />
            <div style={{
    pointerEvents: "none",
    height: "0.5rem",
    borderRadius: "0.25rem",
    backgroundColor: "rgba(0, 0, 0, 0.8)",
    width: `${progress * 100}%`
  }} />
          </div>
        </div>}
    </div>;
};

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

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

    import { useEffect, useRef } from "react";

    import { newEvent } from "@/lib/newEvent";
    import { usePlayer, usePlayerSelector } from "@/registry/react/PlayerProvider";

    export function ProgressBar() {
      const player = usePlayer();
      const currentTime = usePlayerSelector((p) => p.currentTime);
      const duration = usePlayerSelector((p) => p.duration);
      const progress = duration > 0 ? Math.max(0, Math.min(currentTime / duration, 1)) : 0;

      const barRef = useRef(null);
      const mouseDownRef = useRef(false);

      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) => {
        mouseDownRef.current = true;
        player.onEvent(
          newEvent({
            type: "PressedProgressBar",
            description: "The progress bar was pressed at some ratio.",
            initiatedBy: "user",
            ratio: getMouseRatio(event),
          }),
        );
      };

      useEffect(() => {
        const handleMouseMove = (event) => {
          if (!mouseDownRef.current) return;
          if (!barRef.current) {
            handleMouseUp();
            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
          ref={barRef}
          className="relative isolate h-2 grow cursor-pointer overflow-hidden rounded"
          onMouseDown={handleMouseDown}
          onTouchStart={handleMouseDown}
        >
          <div className="absolute inset-0 cursor-pointer bg-[rgba(0,0,0,0.8)] opacity-15" />
          <div className="pointer-events-none h-2 rounded bg-[rgba(0,0,0,0.8)]" style={{ width: `${progress * 100}%` }} />
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

## Installation

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

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

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

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

## Usage

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

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

The bar fills to `currentTime / duration`, read from the surrounding
[`PlayerProvider`](/player-ui/react/player-provider). Pressing and dragging emits a
sequence of events built with [`newEvent`](/player-ui/react/new-event). Each one
carries the pressed `ratio` (0–1):

* **`PressedProgressBar`**: on press, to seek to a position.
* **`ScrubbedProgressBar`**: while dragging.
* **`FinishedScrubbingProgressBar`**: on release.

Drag tracking is attached to `window` (mouse and touch), so scrubbing keeps
working even when the pointer leaves the bar. The component grows to fill its
container (`grow`). Place it in a flex row alongside the other controls.

## Reference

|                       |                                                                                                           |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| **Props**             | None                                                                                                      |
| **Player state read** | `currentTime`, `duration`                                                                                 |
| **Events emitted**    | `PressedProgressBar`, `ScrubbedProgressBar`, `FinishedScrubbingProgressBar` (each carries a `ratio`, 0–1) |
| **Requires**          | a [`PlayerProvider`](/player-ui/react/player-provider) ancestor                                           |
