{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "video",
  "title": "Video",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/video/Video.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface VideoProps extends HTMLAttributes<HTMLDivElement> {\n    /** Video source URL. */\n    src: string\n    /** Poster image shown before playback. */\n    poster?: string\n    /** Autoplay on mount. Note: browsers may block autoplay with sound. */\n    autoplay?: boolean\n    /** Loop playback. */\n    loop?: boolean\n    /** Muted audio. */\n    muted?: boolean\n    /** Use native browser controls instead of the custom overlay. Default false. */\n    nativeControls?: boolean\n    /** Initial playback rate. Default 1. */\n    playbackRate?: number\n    /** Aspect ratio class or inline style value. Default '16/9'. */\n    aspectRatio?: string\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Maximize, Minimize, Pause, Play, RotateCcw, RotateCw, Volume2, VolumeX } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    src,\n    poster,\n    autoplay = false,\n    loop = false,\n    muted = false,\n    nativeControls = false,\n    playbackRate = 1,\n    aspectRatio = '16/9',\n    class: className,\n    ...restProps\n  }: VideoProps = $props()\n\n  let videoRef: HTMLVideoElement | null = $state(null)\n  let containerRef: HTMLDivElement | null = $state(null)\n\n  let playing = $state(false)\n  let playbackFailed = $state(false)\n  let current = $state(0)\n  let duration = $state(0)\n  let volume = $state(muted ? 0 : 1)\n  let isMuted = $state(muted)\n  let isFullscreen = $state(false)\n  let showControls = $state(true)\n  let hideTimer: ReturnType<typeof setTimeout> | null = null\n\n  function togglePlay() {\n    const v = videoRef\n    if (!v) return\n    if (v.paused) {\n      void v.play().catch(() => {\n        playing = false\n        playbackFailed = true\n      })\n    } else v.pause()\n  }\n\n  function adjustVolume(delta: number) {\n    const v = videoRef\n    if (!v) return\n    const next = Math.min(1, Math.max(0, (v.muted ? 0 : v.volume) + delta))\n    v.volume = next\n    v.muted = next === 0\n  }\n\n  /** Keyboard shortcuts when the player (or its controls) is focused. */\n  function onKeydown(e: KeyboardEvent) {\n    if (nativeControls) return\n    const target = e.target as HTMLElement | null\n    // Don't steal Space/Enter from native button activation.\n    if ((e.key === ' ' || e.key === 'Enter') && target?.closest('button')) return\n    // Let range inputs handle their own arrow keys.\n    if (target instanceof HTMLInputElement && target.type === 'range' && e.key.startsWith('Arrow')) return\n\n    const key = e.key\n    if (key === ' ' || key === 'k' || key === 'K') {\n      e.preventDefault()\n      togglePlay()\n      return\n    }\n    if (key === 'm' || key === 'M') {\n      e.preventDefault()\n      toggleMute()\n      return\n    }\n    if (key === 'f' || key === 'F') {\n      e.preventDefault()\n      toggleFullscreen()\n      return\n    }\n    if (key === 'ArrowLeft') {\n      e.preventDefault()\n      skip(-5)\n      return\n    }\n    if (key === 'ArrowRight') {\n      e.preventDefault()\n      skip(5)\n      return\n    }\n    if (key === 'ArrowUp') {\n      e.preventDefault()\n      adjustVolume(0.05)\n      return\n    }\n    if (key === 'ArrowDown') {\n      e.preventDefault()\n      adjustVolume(-0.05)\n    }\n  }\n\n  function onPlay() {\n    playbackFailed = false\n    playing = true\n  }\n  function onPause() {\n    playing = false\n  }\n  function onTimeUpdate() {\n    const v = videoRef\n    if (!v) return\n    current = v.currentTime\n  }\n  function onLoadedMetadata() {\n    const v = videoRef\n    if (!v) return\n    duration = v.duration || 0\n    v.playbackRate = playbackRate\n    v.volume = volume\n  }\n  function onVolumeChange() {\n    const v = videoRef\n    if (!v) return\n    volume = v.volume\n    isMuted = v.muted\n  }\n\n  function seek(e: Event) {\n    const v = videoRef\n    if (!v) return\n    const target = e.target as HTMLInputElement\n    v.currentTime = Number(target.value)\n  }\n\n  function setVolume(e: Event) {\n    const v = videoRef\n    if (!v) return\n    const target = e.target as HTMLInputElement\n    v.volume = Number(target.value)\n    v.muted = Number(target.value) === 0\n  }\n\n  function toggleMute() {\n    const v = videoRef\n    if (!v) return\n    v.muted = !v.muted\n  }\n\n  function skip(seconds: number) {\n    const v = videoRef\n    if (!v) return\n    v.currentTime = Math.min(Math.max(v.currentTime + seconds, 0), v.duration || 0)\n  }\n\n  function toggleFullscreen() {\n    const el = containerRef\n    if (!el) return\n    if (document.fullscreenElement) {\n      document.exitFullscreen()\n    } else {\n      el.requestFullscreen()\n    }\n  }\n\n  function onFullscreenChange() {\n    isFullscreen = !!document.fullscreenElement\n  }\n\n  function onMouseMove() {\n    showControls = true\n    if (hideTimer) clearTimeout(hideTimer)\n    hideTimer = setTimeout(() => {\n      if (playing) showControls = false\n    }, 2500)\n  }\n\n  function onMouseLeave() {\n    if (hideTimer) clearTimeout(hideTimer)\n    if (playing) showControls = false\n  }\n\n  function formatTime(s: number): string {\n    if (!s || !isFinite(s)) return '0:00'\n    const m = Math.floor(s / 60)\n    const sec = Math.floor(s % 60)\n    return `${m}:${sec.toString().padStart(2, '0')}`\n  }\n\n  $effect(() => {\n    if (videoRef) videoRef.playbackRate = playbackRate\n  })\n\n  $effect(() => {\n    if (videoRef) videoRef.muted = muted\n  })\n\n  $effect(() => {\n    document.addEventListener('fullscreenchange', onFullscreenChange)\n    return () => {\n      document.removeEventListener('fullscreenchange', onFullscreenChange)\n      if (hideTimer) clearTimeout(hideTimer)\n    }\n  })\n</script>\n\n<div\n  bind:this={containerRef}\n  data-uipkge\n  data-slot=\"video\"\n  role=\"region\"\n  aria-label=\"Video player\"\n  tabindex=\"0\"\n  class={cn(\n    'group focus-visible:ring-ring relative overflow-hidden rounded-lg bg-black outline-none focus-visible:ring-2',\n    className,\n  )}\n  style:aspect-ratio={aspectRatio}\n  onmousemove={onMouseMove}\n  onmouseleave={onMouseLeave}\n  onkeydown={onKeydown}\n  {...restProps}\n>\n  <!-- svelte-ignore a11y_media_has_caption -->\n  <video\n    bind:this={videoRef}\n    data-slot=\"video-element\"\n    class=\"size-full object-contain\"\n    {src}\n    {poster}\n    {autoplay}\n    {loop}\n    {muted}\n    controls={nativeControls}\n    playsinline\n    onplay={onPlay}\n    onerror={() => (playbackFailed = true)}\n    onloadstart={() => (playbackFailed = false)}\n    onpause={onPause}\n    ontimeupdate={onTimeUpdate}\n    onloadedmetadata={onLoadedMetadata}\n    onvolumechange={onVolumeChange}\n    onclick={togglePlay}\n  ></video>\n\n  {#if playbackFailed}\n    <p role=\"alert\" class=\"bg-background text-foreground absolute inset-x-3 top-3 z-10 rounded-md p-3 text-sm\">\n      Unable to play this video. Check the source or try again.\n    </p>\n  {/if}\n\n  <!-- Custom controls overlay -->\n  {#if !nativeControls}\n    <div\n      data-slot=\"video-controls\"\n      class={cn(\n        'absolute inset-0 flex flex-col justify-between transition-opacity duration-200 motion-reduce:transition-none',\n        showControls || !playing ? 'opacity-100' : 'pointer-events-none opacity-0',\n      )}\n    >\n      <!-- Top spacer / click target (keeps play toggle when controls visible).\n        Keyboard users drive the player through the focused container's shortcuts. -->\n      <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->\n      <div class=\"flex-1\" onclick={togglePlay}></div>\n\n      <!-- Bottom controls bar -->\n      <div class=\"bg-gradient-to-t from-black/80 to-transparent px-3 pt-6 pb-2\">\n        <!-- Progress bar -->\n        <div class=\"mb-2 flex items-center gap-2\">\n          <span class=\"text-xs text-white/80 tabular-nums\">{formatTime(current)}</span>\n          <input\n            type=\"range\"\n            min=\"0\"\n            max={duration || 0}\n            step=\"0.1\"\n            value={current}\n            aria-label=\"Seek\"\n            class=\"accent-primary [&::-webkit-slider-thumb]:bg-primary h-1 flex-1 cursor-pointer appearance-none rounded-full bg-white/30 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full\"\n            oninput={seek}\n          />\n          <span class=\"text-xs text-white/80 tabular-nums\">{formatTime(duration)}</span>\n        </div>\n\n        <!-- Buttons row -->\n        <div class=\"flex items-center gap-2\">\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            aria-label={playing ? 'Pause' : 'Play'}\n            onclick={togglePlay}\n          >\n            {#if playing}\n              <Pause class=\"size-5\" />\n            {:else}\n              <Play class=\"size-5\" />\n            {/if}\n          </button>\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            aria-label=\"Rewind 10s\"\n            onclick={() => skip(-10)}\n          >\n            <RotateCcw class=\"size-4\" />\n          </button>\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            aria-label=\"Forward 10s\"\n            onclick={() => skip(10)}\n          >\n            <RotateCw class=\"size-4\" />\n          </button>\n\n          <!-- Volume -->\n          <div class=\"group/volume flex items-center gap-1.5\">\n            <button\n              type=\"button\"\n              class=\"text-white/90 transition-colors hover:text-white\"\n              aria-label={isMuted ? 'Unmute' : 'Mute'}\n              onclick={toggleMute}\n            >\n              {#if isMuted || volume === 0}\n                <VolumeX class=\"size-4\" />\n              {:else}\n                <Volume2 class=\"size-4\" />\n              {/if}\n            </button>\n            <input\n              type=\"range\"\n              min=\"0\"\n              max=\"1\"\n              step=\"0.05\"\n              value={isMuted ? 0 : volume}\n              aria-label=\"Volume\"\n              class=\"accent-primary [&::-webkit-slider-thumb]:bg-primary h-1 w-0 cursor-pointer appearance-none rounded-full bg-white/30 transition-[width] duration-200 group-hover/volume:w-16 motion-reduce:transition-none [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full\"\n              oninput={setVolume}\n            />\n          </div>\n\n          <div class=\"flex-1\"></div>\n\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n            onclick={toggleFullscreen}\n          >\n            {#if isFullscreen}\n              <Minimize class=\"size-4\" />\n            {:else}\n              <Maximize class=\"size-4\" />\n            {/if}\n          </button>\n        </div>\n      </div>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/video/Video.svelte"
    },
    {
      "path": "packages/registry-svelte/components/video/index.ts",
      "content": "export { default as Video, type VideoProps } from './Video.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/video/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Video player wrapper with a custom controls overlay on a native video element. Supports src, poster, autoplay, loop, muted, custom or native controls, playback rate, fullscreen, time display, a progress bar, play/pause, skip, and volume control.",
  "categories": [
    "media",
    "display"
  ]
}