{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "video",
  "title": "Video",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/video/Video.vue",
      "content": "<script setup lang=\"ts\">\nimport { Maximize, Minimize, Pause, Play, RotateCcw, RotateCw, Volume2, VolumeX } from 'lucide-vue-next'\nimport { computed, onBeforeUnmount, onMounted, ref, watch, type HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\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  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  autoplay: false,\n  loop: false,\n  muted: false,\n  nativeControls: false,\n  playbackRate: 1,\n  aspectRatio: '16/9',\n})\n\nconst videoRef = ref<HTMLVideoElement | null>(null)\nconst containerRef = ref<HTMLElement | null>(null)\n\nconst playing = ref(false)\nconst current = ref(0)\nconst duration = ref(0)\nconst volume = ref(props.muted ? 0 : 1)\nconst isMuted = ref(props.muted)\nconst isFullscreen = ref(false)\nconst showControls = ref(true)\nlet hideTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction togglePlay() {\n  const v = videoRef.value\n  if (!v) return\n  if (v.paused) v.play()\n  else v.pause()\n}\n\nfunction onPlay() {\n  playing.value = true\n}\nfunction onPause() {\n  playing.value = false\n}\nfunction onTimeUpdate() {\n  const v = videoRef.value\n  if (!v) return\n  current.value = v.currentTime\n}\nfunction onLoadedMetadata() {\n  const v = videoRef.value\n  if (!v) return\n  duration.value = v.duration || 0\n  v.playbackRate = props.playbackRate\n  v.volume = volume.value\n}\nfunction onVolumeChange() {\n  const v = videoRef.value\n  if (!v) return\n  volume.value = v.volume\n  isMuted.value = v.muted\n}\n\nfunction seek(e: Event) {\n  const v = videoRef.value\n  if (!v) return\n  const target = e.target as HTMLInputElement\n  v.currentTime = Number(target.value)\n}\n\nfunction setVolume(e: Event) {\n  const v = videoRef.value\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\nfunction toggleMute() {\n  const v = videoRef.value\n  if (!v) return\n  v.muted = !v.muted\n}\n\nfunction skip(seconds: number) {\n  const v = videoRef.value\n  if (!v) return\n  v.currentTime = Math.min(Math.max(v.currentTime + seconds, 0), v.duration || 0)\n}\n\nfunction toggleFullscreen() {\n  const el = containerRef.value\n  if (!el) return\n  if (document.fullscreenElement) {\n    document.exitFullscreen()\n  } else {\n    el.requestFullscreen()\n  }\n}\n\nfunction onFullscreenChange() {\n  isFullscreen.value = !!document.fullscreenElement\n}\n\nfunction onMouseMove() {\n  showControls.value = true\n  if (hideTimer) clearTimeout(hideTimer)\n  hideTimer = setTimeout(() => {\n    if (playing.value) showControls.value = false\n  }, 2500)\n}\n\nfunction onMouseLeave() {\n  if (hideTimer) clearTimeout(hideTimer)\n  if (playing.value) showControls.value = false\n}\n\nfunction 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\nconst progress = computed(() => (duration.value > 0 ? (current.value / duration.value) * 100 : 0))\n\nwatch(\n  () => props.playbackRate,\n  (rate) => {\n    if (videoRef.value) videoRef.value.playbackRate = rate\n  },\n)\n\nwatch(\n  () => props.muted,\n  (m) => {\n    if (videoRef.value) videoRef.value.muted = m\n  },\n)\n\nonMounted(() => {\n  document.addEventListener('fullscreenchange', onFullscreenChange)\n})\n\nonBeforeUnmount(() => {\n  document.removeEventListener('fullscreenchange', onFullscreenChange)\n  if (hideTimer) clearTimeout(hideTimer)\n})\n</script>\n\n<template>\n  <div\n    ref=\"containerRef\"\n    data-uipkge\n    data-slot=\"video\"\n    :class=\"cn('group relative overflow-hidden rounded-lg bg-black', props.class)\"\n    :style=\"{ aspectRatio: aspectRatio }\"\n    @mousemove=\"onMouseMove\"\n    @mouseleave=\"onMouseLeave\"\n  >\n    <video\n      ref=\"videoRef\"\n      data-slot=\"video-element\"\n      class=\"size-full object-contain\"\n      :src=\"src\"\n      :poster=\"poster\"\n      :autoplay=\"autoplay\"\n      :loop=\"loop\"\n      :muted=\"muted\"\n      :controls=\"nativeControls\"\n      playsinline\n      @play=\"onPlay\"\n      @pause=\"onPause\"\n      @timeupdate=\"onTimeUpdate\"\n      @loadedmetadata=\"onLoadedMetadata\"\n      @volumechange=\"onVolumeChange\"\n      @click=\"togglePlay\"\n    />\n\n    <!-- Custom controls overlay -->\n    <div\n      v-if=\"!nativeControls\"\n      data-slot=\"video-controls\"\n      :class=\"\n        cn(\n          'absolute inset-0 flex flex-col justify-between transition-opacity duration-200',\n          showControls || !playing ? 'opacity-100' : 'opacity-0',\n        )\n      \"\n    >\n      <!-- Top spacer / click target -->\n      <div class=\"flex-1\" @click=\"togglePlay\" />\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            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            @input=\"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            @click=\"togglePlay\"\n          >\n            <component :is=\"playing ? Pause : Play\" class=\"size-5\" />\n          </button>\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            aria-label=\"Rewind 10s\"\n            @click=\"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            @click=\"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              @click=\"toggleMute\"\n            >\n              <component :is=\"isMuted || volume === 0 ? VolumeX : Volume2\" class=\"size-4\" />\n            </button>\n            <input\n              type=\"range\"\n              min=\"0\"\n              max=\"1\"\n              step=\"0.05\"\n              :value=\"isMuted ? 0 : volume\"\n              class=\"accent-primary [&::-webkit-slider-thumb]:bg-primary h-1 w-0 cursor-pointer appearance-none rounded-full bg-white/30 transition-all duration-200 group-hover/volume:w-16 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full\"\n              @input=\"setVolume\"\n            />\n          </div>\n\n          <div class=\"flex-1\" />\n\n          <button\n            type=\"button\"\n            class=\"text-white/90 transition-colors hover:text-white\"\n            :aria-label=\"isFullscreen ? 'Exit fullscreen' : 'Fullscreen'\"\n            @click=\"toggleFullscreen\"\n          >\n            <component :is=\"isFullscreen ? Minimize : Maximize\" class=\"size-4\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/video/Video.vue"
    },
    {
      "path": "packages/registry-vue/components/video/index.ts",
      "content": "export { default as Video } from './Video.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/video/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "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"
  ]
}