{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "video",
  "title": "Video",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/video/video.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  SimpleChanges,\n  ViewChild,\n  booleanAttribute,\n  inject,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n/**\n * Angular port of UIPKGE Video — native video element with a custom controls\n * overlay (play/pause, skip, volume, rate, fullscreen, progress).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-video, [ui-video]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"video\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-playing]': 'playing',\n    role: 'region',\n    'aria-label': 'Video player',\n    tabindex: '0',\n    '[class]': 'hostClass',\n    '[style.aspectRatio]': 'aspectRatio',\n    '(mousemove)': 'onMouseMove()',\n    '(mouseleave)': 'onMouseLeave()',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `\n    <video\n      #videoEl\n      data-slot=\"video-element\"\n      class=\"size-full object-contain\"\n      [src]=\"src\"\n      [attr.poster]=\"poster ? poster : null\"\n      [autoplay]=\"autoplay\"\n      [loop]=\"loop\"\n      [muted]=\"muted\"\n      [controls]=\"nativeControls\"\n      playsinline\n      (play)=\"onPlay()\"\n      (pause)=\"onPause()\"\n      (ended)=\"ended.emit()\"\n      (timeupdate)=\"onTimeUpdate()\"\n      (loadedmetadata)=\"onLoadedMetadata()\"\n      (volumechange)=\"onVolumeChange()\"\n      (click)=\"togglePlay()\"\n      (error)=\"playbackFailed = true\"\n      (loadstart)=\"playbackFailed = false\"\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    }\n\n    @if (!nativeControls) {\n      <div data-slot=\"video-controls\" [class]=\"controlsClass\">\n        <div class=\"flex-1\" (click)=\"togglePlay()\"></div>\n\n        <div class=\"bg-gradient-to-t from-black/80 to-transparent px-3 pt-6 pb-2\">\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              (input)=\"seek($event)\"\n            />\n            <span class=\"text-xs text-white/80 tabular-nums\">{{ formatTime(duration) }}</span>\n          </div>\n\n          <div class=\"flex items-center gap-2\">\n            <button\n              type=\"button\"\n              class=\"text-white/90 transition-colors hover:text-white\"\n              [attr.aria-label]=\"playing ? 'Pause' : 'Play'\"\n              (click)=\"togglePlay()\"\n            >\n              @if (playing) {\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"size-5\"\n                >\n                  <rect x=\"6\" y=\"4\" width=\"4\" height=\"16\" />\n                  <rect x=\"14\" y=\"4\" width=\"4\" height=\"16\" />\n                </svg>\n              } @else {\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"size-5\"\n                >\n                  <polygon points=\"6 3 20 12 6 21 6 3\" />\n                </svg>\n              }\n            </button>\n\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              <svg\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"size-4\"\n              >\n                <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n                <path d=\"M3 3v5h5\" />\n              </svg>\n            </button>\n\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              <svg\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"size-4\"\n              >\n                <path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.85.83 6.72 2.24L21 8\" />\n                <path d=\"M21 3v5h-5\" />\n              </svg>\n            </button>\n\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                [attr.aria-label]=\"isMuted ? 'Unmute' : 'Mute'\"\n                (click)=\"toggleMute()\"\n              >\n                @if (isMuted || volume === 0) {\n                  <svg\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    stroke-width=\"2\"\n                    stroke-linecap=\"round\"\n                    stroke-linejoin=\"round\"\n                    class=\"size-4\"\n                  >\n                    <polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\" />\n                    <line x1=\"22\" x2=\"16\" y1=\"9\" y2=\"15\" />\n                    <line x1=\"16\" x2=\"22\" y1=\"9\" y2=\"15\" />\n                  </svg>\n                } @else {\n                  <svg\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    stroke-width=\"2\"\n                    stroke-linecap=\"round\"\n                    stroke-linejoin=\"round\"\n                    class=\"size-4\"\n                  >\n                    <polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\" />\n                    <path d=\"M15.54 8.46a5 5 0 0 1 0 7.07\" />\n                    <path d=\"M19.07 4.93a10 10 0 0 1 0 14.14\" />\n                  </svg>\n                }\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                (input)=\"onVolumeSlider($event)\"\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              [attr.aria-label]=\"isFullscreen ? 'Exit fullscreen' : 'Fullscreen'\"\n              (click)=\"toggleFullscreen()\"\n            >\n              @if (isFullscreen) {\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"size-4\"\n                >\n                  <path d=\"M8 3v3a2 2 0 0 1-2 2H3\" />\n                  <path d=\"M21 8h-3a2 2 0 0 1-2-2V3\" />\n                  <path d=\"M3 16h3a2 2 0 0 1 2 2v3\" />\n                  <path d=\"M16 21v-3a2 2 0 0 1 2-2h3\" />\n                </svg>\n              } @else {\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"size-4\"\n                >\n                  <path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n                  <path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n                  <path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n                  <path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n                </svg>\n              }\n            </button>\n          </div>\n        </div>\n      </div>\n    }\n  `,\n})\nexport class UiVideoComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {\n  private hostEl?: HTMLElement\n\n  constructor() {\n    try {\n      this.hostEl = inject(ElementRef, { optional: true })?.nativeElement\n    } catch {\n      // Instantiated outside injection context (e.g. unit tests)\n    }\n  }\n\n  @Input({ required: true }) src = ''\n  @Input() poster?: string\n  @Input({ transform: booleanAttribute }) autoplay = false\n  @Input({ transform: booleanAttribute }) loop = false\n  @Input({ transform: booleanAttribute }) muted = false\n  @Input({ transform: booleanAttribute }) nativeControls = false\n  @Input() playbackRate = 1\n  @Input() aspectRatio = '16/9'\n  @Input('class') hostClassAttr?: string\n  @Input() className?: string\n\n  @Output() play = new EventEmitter<void>()\n  @Output() pause = new EventEmitter<void>()\n  @Output() ended = new EventEmitter<void>()\n  @Output() timeUpdate = new EventEmitter<number>()\n\n  @ViewChild('videoEl') videoElRef?: ElementRef<HTMLVideoElement>\n\n  playing = false\n  playbackFailed = false\n  current = 0\n  duration = 0\n  volume = 1\n  isMuted = false\n  isFullscreen = false\n  showControls = true\n  private hideTimer: ReturnType<typeof setTimeout> | null = null\n\n  get hostClass(): string {\n    return cn(\n      'block w-full group focus-visible:ring-ring relative overflow-hidden rounded-lg bg-black outline-none focus-visible:ring-2',\n      this.hostClassAttr,\n      this.className,\n    )\n  }\n\n  get controlsClass(): string {\n    return cn(\n      'absolute inset-0 flex flex-col justify-between transition-opacity duration-200 motion-reduce:transition-none',\n      this.showControls || !this.playing ? 'opacity-100' : 'pointer-events-none opacity-0',\n    )\n  }\n\n  ngOnInit(): void {\n    if (this.muted) {\n      this.volume = 0\n      this.isMuted = true\n    }\n  }\n\n  ngAfterViewInit(): void {\n    if (typeof document !== 'undefined') {\n      document.addEventListener('fullscreenchange', this.handleFullscreenChange)\n    }\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const v = this.videoElRef?.nativeElement\n    if (v && changes['playbackRate']) v.playbackRate = this.playbackRate\n  }\n\n  ngOnDestroy(): void {\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    if (typeof document !== 'undefined') {\n      document.removeEventListener('fullscreenchange', this.handleFullscreenChange)\n    }\n  }\n\n  private handleFullscreenChange = (): void => {\n    if (typeof document !== 'undefined') {\n      this.isFullscreen = !!document.fullscreenElement\n    }\n  }\n\n  onPlay(): void {\n    this.playbackFailed = false\n    this.playing = true\n    this.play.emit()\n  }\n\n  onPause(): void {\n    this.playing = false\n    this.pause.emit()\n  }\n\n  onTimeUpdate(): void {\n    const v = this.videoElRef?.nativeElement\n    if (!v) return\n    this.current = v.currentTime\n    this.timeUpdate.emit(this.current)\n  }\n\n  onLoadedMetadata(): void {\n    const v = this.videoElRef?.nativeElement\n    if (!v) return\n    this.duration = v.duration || 0\n    v.playbackRate = this.playbackRate\n    v.volume = this.volume\n  }\n\n  onVolumeChange(): void {\n    const v = this.videoElRef?.nativeElement\n    if (!v) return\n    this.volume = v.volume\n    this.isMuted = v.muted\n  }\n\n  togglePlay(): void {\n    const v = this.videoElRef?.nativeElement\n    if (v) {\n      if (v.paused) {\n        v.play().catch(() => {\n          this.playing = false\n          this.playbackFailed = true\n        })\n      } else {\n        v.pause()\n      }\n    } else {\n      this.playing = !this.playing\n      if (this.playing) this.play.emit()\n      else this.pause.emit()\n    }\n  }\n\n  toggleMute(): void {\n    const v = this.videoElRef?.nativeElement\n    if (v) {\n      v.muted = !v.muted\n    } else {\n      this.isMuted = !this.isMuted\n      this.volume = this.isMuted ? 0 : 1\n    }\n  }\n\n  setVolume(v: number): void {\n    this.volume = Math.min(1, Math.max(0, v))\n    this.isMuted = this.volume === 0\n    const el = this.videoElRef?.nativeElement\n    if (el) {\n      el.volume = this.volume\n      el.muted = this.isMuted\n    }\n  }\n\n  onVolumeSlider(e: Event): void {\n    const target = e.target as HTMLInputElement\n    this.setVolume(Number(target.value))\n  }\n\n  seek(e: Event): void {\n    const target = e.target as HTMLInputElement\n    this.seekTo(Number(target.value))\n  }\n\n  seekTo(t: number): void {\n    const max = this.duration > 0 ? this.duration : Number.POSITIVE_INFINITY\n    this.current = Math.min(max, Math.max(0, t))\n    const v = this.videoElRef?.nativeElement\n    if (v) v.currentTime = this.current\n    this.timeUpdate.emit(this.current)\n  }\n\n  skip(seconds: number): void {\n    this.seekTo(this.current + seconds)\n  }\n\n  setRate(r: number): void {\n    if (r > 0) {\n      this.playbackRate = r\n      const v = this.videoElRef?.nativeElement\n      if (v) v.playbackRate = r\n    }\n  }\n\n  toggleFullscreen(): void {\n    const el = this.hostEl\n    if (!el || typeof document === 'undefined') return\n    if (document.fullscreenElement) {\n      document.exitFullscreen()\n    } else {\n      el.requestFullscreen()\n    }\n  }\n\n  progress(): number {\n    if (!this.duration) return 0\n    return Math.min(1, Math.max(0, this.current / this.duration))\n  }\n\n  formatTime(s: number): string {\n    const total = Math.max(0, Math.floor(s))\n    const m = Math.floor(total / 60)\n    const sec = total % 60\n    return `${m}:${String(sec).padStart(2, '0')}`\n  }\n\n  onMouseMove(): void {\n    this.showControls = true\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    if (this.playing) {\n      this.hideTimer = setTimeout(() => {\n        this.showControls = false\n      }, 2500)\n    }\n  }\n\n  onMouseLeave(): void {\n    if (this.playing) this.showControls = false\n  }\n\n  onKeydown(e: KeyboardEvent): void {\n    if (this.nativeControls) return\n    const target = e.target as HTMLElement | null\n    if ((e.key === ' ' || e.key === 'Enter') && target?.closest('button')) return\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      this.togglePlay()\n    } else if (key === 'm' || key === 'M') {\n      e.preventDefault()\n      this.toggleMute()\n    } else if (key === 'f' || key === 'F') {\n      e.preventDefault()\n      this.toggleFullscreen()\n    } else if (key === 'ArrowLeft') {\n      e.preventDefault()\n      this.skip(-5)\n    } else if (key === 'ArrowRight') {\n      e.preventDefault()\n      this.skip(5)\n    } else if (key === 'ArrowUp') {\n      e.preventDefault()\n      this.setVolume(this.volume + 0.05)\n    } else if (key === 'ArrowDown') {\n      e.preventDefault()\n      this.setVolume(this.volume - 0.05)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/video/video.component.ts"
    },
    {
      "path": "packages/registry-angular/components/video/index.ts",
      "content": "export { UiVideoComponent } from './video.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/video/index.ts"
    }
  ],
  "dependencies": [],
  "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, progress bar, play/pause, skip, and volume.",
  "categories": [
    "media",
    "display"
  ]
}