{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-player",
  "title": "Video Player",
  "description": "Video with real captions, a keyboard-operable scrubber, and controls that survive full screen.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/video-player/video-player.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Captions,\n  CaptionsOff,\n  Maximize,\n  Minimize,\n  Pause,\n  Play,\n  Volume2,\n  VolumeX,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type VideoTrack = {\n  src: string\n  /** A BCP 47 language tag, such as \"en\". */\n  srcLang: string\n  label: string\n  kind?: \"subtitles\" | \"captions\"\n  default?: boolean\n}\n\nexport type VideoPlayerProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onError\" | \"children\"\n> & {\n  src: string\n  /** Names the video for assistive technology. */\n  label: string\n  poster?: string\n  tracks?: readonly VideoTrack[]\n  rates?: readonly number[]\n}\n\nfunction formatTime(seconds: number) {\n  if (!Number.isFinite(seconds) || seconds < 0) return \"0:00\"\n\n  const whole = Math.floor(seconds)\n  return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, \"0\")}`\n}\n\n/**\n * Video with controls of its own, captions that can be turned on, and a\n * scrubber that is a real range input rather than a bar to be dragged.\n */\nexport function VideoPlayer({\n  src,\n  label,\n  poster,\n  tracks = [],\n  rates = [1, 1.5, 2],\n  className,\n  ...rootProps\n}: VideoPlayerProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const videoRef = React.useRef<HTMLVideoElement>(null)\n\n  const [playing, setPlaying] = React.useState(false)\n  const [time, setTime] = React.useState(0)\n  const [duration, setDuration] = React.useState(0)\n  const [muted, setMuted] = React.useState(false)\n  const [rate, setRate] = React.useState(rates[0] ?? 1)\n  const [captions, setCaptions] = React.useState(\n    tracks.some((track) => track.default)\n  )\n  const [full, setFull] = React.useState(false)\n  const touched = React.useRef(false)\n\n  const wanted = tracks.some((track) => track.default)\n\n  // Tracks often arrive after the video does. Following the default until\n  // someone works the toggle keeps a late arrival from being ignored, and a\n  // deliberate choice from being overruled.\n  React.useEffect(() => {\n    if (!touched.current) setCaptions(wanted)\n  }, [wanted])\n\n  React.useEffect(() => {\n    const element = videoRef.current\n    if (element) element.playbackRate = rate\n  }, [rate])\n\n  // Custom controls mean the browser's caption menu is gone, so the track mode\n  // is set by hand. The cues are still drawn by the browser once it is showing.\n  React.useEffect(() => {\n    const element = videoRef.current\n    if (!element) return\n\n    for (const track of Array.from(element.textTracks)) {\n      if (track.kind === \"subtitles\" || track.kind === \"captions\") {\n        track.mode = captions ? \"showing\" : \"hidden\"\n      }\n    }\n  }, [captions, tracks])\n\n  React.useEffect(() => {\n    const sync = () => setFull(document.fullscreenElement === rootRef.current)\n\n    document.addEventListener(\"fullscreenchange\", sync)\n    return () => document.removeEventListener(\"fullscreenchange\", sync)\n  }, [])\n\n  const seek = (to: number) => {\n    const element = videoRef.current\n    if (!element) return\n\n    element.currentTime = to\n    setTime(to)\n  }\n\n  const position = `${formatTime(time)} of ${formatTime(duration)}`\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"video-player\"\n      data-playing={playing ? \"\" : undefined}\n      className={cn(\n        \"border-border bg-background overflow-hidden rounded-xl border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <video\n        ref={videoRef}\n        src={src}\n        poster={poster}\n        preload=\"metadata\"\n        playsInline\n        aria-label={label}\n        onLoadedMetadata={(event) =>\n          setDuration(event.currentTarget.duration || 0)\n        }\n        onTimeUpdate={(event) => setTime(event.currentTarget.currentTime)}\n        onPlay={() => setPlaying(true)}\n        onPause={() => setPlaying(false)}\n        onEnded={() => setPlaying(false)}\n        onClick={() => {\n          const element = videoRef.current\n          if (!element) return\n          if (playing) element.pause()\n          else void element.play().catch(() => {})\n        }}\n        className=\"block w-full cursor-pointer bg-black\"\n      >\n        {tracks.map((track) => (\n          <track\n            key={track.src}\n            src={track.src}\n            kind={track.kind ?? \"captions\"}\n            srcLang={track.srcLang}\n            label={track.label}\n            default={track.default}\n          />\n        ))}\n      </video>\n\n      <div className=\"flex items-center gap-2 p-2\">\n        <button\n          type=\"button\"\n          aria-label={playing ? `Pause ${label}` : `Play ${label}`}\n          onClick={() => {\n            const element = videoRef.current\n            if (!element) return\n            if (playing) element.pause()\n            else void element.play().catch(() => {})\n          }}\n          className=\"bg-foreground text-background focus-visible:ring-ring inline-flex size-9 shrink-0 items-center justify-center rounded-full transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        >\n          {playing ? (\n            <Pause aria-hidden=\"true\" size={15} />\n          ) : (\n            <Play aria-hidden=\"true\" size={15} />\n          )}\n        </button>\n\n        <input\n          type=\"range\"\n          min={0}\n          max={duration || 0}\n          step={0.01}\n          value={Math.min(time, duration || 0)}\n          disabled={duration === 0}\n          aria-label={`Seek ${label}`}\n          aria-valuetext={position}\n          onChange={(event) => seek(Number(event.target.value))}\n          className=\"accent-primary focus-visible:ring-ring min-w-0 flex-1 cursor-pointer rounded-full focus-visible:ring-2 focus-visible:outline-none disabled:cursor-default\"\n        />\n\n        <span\n          data-slot=\"video-player-time\"\n          className=\"text-muted-foreground shrink-0 font-mono text-xs tabular-nums\"\n        >\n          {formatTime(time)}\n          <span className=\"opacity-60\"> / {formatTime(duration)}</span>\n        </span>\n\n        <button\n          type=\"button\"\n          aria-label={muted ? `Unmute ${label}` : `Mute ${label}`}\n          aria-pressed={muted}\n          onClick={() => {\n            const element = videoRef.current\n            if (!element) return\n            element.muted = !muted\n            setMuted(!muted)\n          }}\n          className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        >\n          {muted ? (\n            <VolumeX aria-hidden=\"true\" size={15} />\n          ) : (\n            <Volume2 aria-hidden=\"true\" size={15} />\n          )}\n        </button>\n\n        {tracks.length > 0 ? (\n          <button\n            type=\"button\"\n            aria-label={captions ? \"Turn captions off\" : \"Turn captions on\"}\n            aria-pressed={captions}\n            onClick={() => {\n              touched.current = true\n              setCaptions((on) => !on)\n            }}\n            className=\"text-muted-foreground aria-pressed:text-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n          >\n            {captions ? (\n              <Captions aria-hidden=\"true\" size={15} />\n            ) : (\n              <CaptionsOff aria-hidden=\"true\" size={15} />\n            )}\n          </button>\n        ) : null}\n\n        {rates.length > 1 ? (\n          <button\n            type=\"button\"\n            aria-label={`Playback speed, ${rate} times. Change.`}\n            onClick={() =>\n              setRate(rates[(rates.indexOf(rate) + 1) % rates.length] ?? 1)\n            }\n            className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex h-8 shrink-0 items-center rounded-md px-1.5 font-mono text-xs tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n          >\n            {rate}&times;\n          </button>\n        ) : null}\n\n        <button\n          type=\"button\"\n          aria-label={full ? \"Leave full screen\" : \"Go full screen\"}\n          aria-pressed={full}\n          onClick={() => {\n            if (document.fullscreenElement) void document.exitFullscreen()\n            else void rootRef.current?.requestFullscreen().catch(() => {})\n          }}\n          className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        >\n          {full ? (\n            <Minimize aria-hidden=\"true\" size={15} />\n          ) : (\n            <Maximize aria-hidden=\"true\" size={15} />\n          )}\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "media",
    "video",
    "player"
  ],
  "type": "registry:ui"
}