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

# Vertical Video Player

> A portrait 9:16 video player example, built from the same video player block.

export const VerticalVideoPlayerDemo = () => {
  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 playCircleIcon = (color, size) => <svg width={size} height={size} viewBox="0 0 24 24" fill={color} xmlns="http://www.w3.org/2000/svg">
      <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>;
  const pauseCircleIcon = (color, size) => <svg width={size} height={size} viewBox="0 0 24 24" fill={color} xmlns="http://www.w3.org/2000/svg">
      <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>;
  const seekBackIcon = color => <svg width={28} height={28} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M7 9l-3 -3l3 -3" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M15.997 17.918a6.002 6.002 0 0 0 -.997 -11.918h-11" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M6 14v6" stroke={color} 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={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>;
  const seekForwardIcon = color => <svg width={28} height={28} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M17 9l3 -3l-3 -3" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M8 17.918a5.997 5.997 0 0 1 -5 -5.918a6 6 0 0 1 6 -6h11" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M12 14v6" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M15 15.5v3a1.5 1.5 0 0 0 3 0v-3a1.5 1.5 0 0 0 -3 0" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>;
  const [target, setTarget] = useState(null);
  const [player, setPlayer] = useState(null);
  const [playbackState, setPlaybackState] = useState("stopped");
  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 [hovering, setHovering] = useState(false);
  const [rootEl, setRootEl] = useState(null);
  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: 54009,
        contentId: "5ab6a5e1-689c-4103-962d-e7e68a4258ff",
        video: true,
        summary: true,
        videoSizes: ["9:16"],
        widgetStyle: "none",
        showUserInterface: false,
        analyticsConsent: "none"
      });
      const sync = () => {
        setPlaybackState(instance.playbackState);
        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]);
  useEffect(() => {
    if (!rootEl) return;
    const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
      threshold: 0
    });
    observer.observe(rootEl);
    return () => observer.disconnect();
  }, [rootEl]);
  const stopped = playbackState === "stopped";
  const imageUrl = content?.[contentIndex]?.imageUrl;
  const mode = inView || closed || stopped ? "inline" : "widget";
  const controlBarOpacity = stopped ? 1 : hovering ? 1 : 0;
  const seek = (type, description, seconds) => () => {
    player.onEvent(newEvent({
      type,
      description,
      seconds
    }));
  };
  const playPause = () => {
    const name = playbackState === "playing" ? "Pause" : "Play";
    player.onEvent(newEvent({
      type: `Pressed${name}`,
      description: `The ${name.toLowerCase()} button was pressed.`
    }));
  };
  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={setRootEl} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} style={{
    position: "relative",
    aspectRatio: "9 / 16",
    height: "480px",
    width: "auto",
    backgroundColor: "black"
  }}>
        <div ref={setTarget} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%"
  }} />
        {player && <>
          {imageUrl && <img src={imageUrl} alt=" " style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    backgroundColor: "black",
    objectFit: "contain",
    display: stopped ? "block" : "none"
  }} />}
          <div style={{
    position: "absolute",
    inset: 0,
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    gap: "2rem",
    backgroundColor: "rgba(0,0,0,0.3)",
    transition: "opacity 150ms",
    opacity: controlBarOpacity
  }}>
            <button type="button" onClick={seek("PressedSeekBack", "The seek backward button was pressed.", 10)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
              {seekBackIcon("white")}
            </button>
            <button type="button" onClick={playPause} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
              {playbackState === "playing" ? pauseCircleIcon("white", 56) : playCircleIcon("white", 56)}
            </button>
            <button type="button" onClick={seek("PressedSeekAhead", "The seek ahead button was pressed.", 10)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
              {seekForwardIcon("white")}
            </button>
          </div>
          {mode === "widget" && <div style={{
    position: "fixed",
    right: "1rem",
    bottom: "1rem",
    zIndex: 50,
    display: "flex",
    height: "3rem",
    minWidth: "15rem",
    flexDirection: "row",
    alignItems: "center",
    gap: "0.5rem",
    borderRadius: "1.5rem",
    backgroundColor: "#F5F5F5",
    padding: "0.25rem",
    boxShadow: "0 4px 12px rgba(0,0,0,0.15)"
  }}>
              <button type="button" onClick={playPause} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                {playbackState === "playing" ? pauseCircleIcon("#323232", 40) : playCircleIcon("#323232", 40)}
              </button>
              <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={seek("PressedSeekBack", "The seek backward button was pressed.", 10)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                  {seekBackIcon("#323232")}
                </button>
                <button type="button" onClick={seek("PressedSeekAhead", "The seek ahead button was pressed.", 10)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                  {seekForwardIcon("#323232")}
                </button>
                <div style={{
    fontSize: "0.625rem",
    fontWeight: 300,
    color: "#323232",
    fontVariantNumeric: "tabular-nums",
    whiteSpace: "nowrap"
  }}>
                  {formatTime(currentTime)} / {formatTime(duration)}
                </div>
                <div style={{
    position: "relative",
    height: "0.5rem",
    flexGrow: 1,
    overflow: "hidden",
    borderRadius: "0.25rem"
  }}>
                  <div style={{
    position: "absolute",
    inset: 0,
    backgroundColor: "#323232",
    opacity: 0.15
  }} />
                  <div style={{
    pointerEvents: "none",
    height: "0.5rem",
    borderRadius: "0.25rem",
    backgroundColor: "#323232",
    width: `${duration > 0 ? Math.max(0, Math.min(currentTime / duration * 100, 100)) : 0}%`
  }} />
                </div>
                <button type="button" onClick={() => setClosed(true)} style={{
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0
  }}>
                  <svg width={24} height={24} viewBox="-4 -4 32 32" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
                    <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
                  </svg>
                </button>
              </div>
            </div>}
          </>}
      </div>
    </div>;
};

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

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

    import { VideoPlayer } from "@/registry/react/blocks/video/VideoPlayer";

    /** Portrait (9:16) variant of the video player example. */
    export function VerticalVideoPlayer(props) {
      return <VideoPlayer vertical {...props} />;
    }
    ```
  </Tab>
</Tabs>

## Installation

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

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

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

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

## Usage

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

export function Player() {
  return <VerticalVideoPlayer projectId={54009} contentId="5ab6a5e1-689c-4103-962d-e7e68a4258ff" />;
}
```

`VerticalVideoPlayer` is the portrait variant of
[Video Player](/player-ui/react/video-player). Same controls and floating widget
behavior, only the frame is 9:16 (480px tall). Use it for stories, Reels-style
clips, or other vertical video.
