{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-player",
  "title": "Video Player",
  "description": "Video playback with controls.",
  "dependencies": [
    "@radix-ui/react-slot"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/video-player.tsx",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  Maximize,\n  Minimize,\n  Pause,\n  Play,\n  SkipBack,\n  SkipForward,\n  Volume2,\n  VolumeX,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst videoPlayerVariants = cva(\n  \"group relative w-full touch-manipulation overflow-hidden rounded-card bg-black\",\n  {\n    variants: {\n      size: {\n        sm: \"max-w-md\",\n        default: \"max-w-2xl\",\n        lg: \"max-w-4xl\",\n        full: \"w-full\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n);\n\nexport interface VideoPlayerProps\n  extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, \"controls\">,\n    VariantProps<typeof videoPlayerVariants> {\n  src: string;\n  poster?: string;\n  showControls?: boolean;\n  autoHide?: boolean;\n  className?: string;\n}\n\nconst VideoPlayer = React.forwardRef<HTMLVideoElement, VideoPlayerProps>(\n  (\n    {\n      className,\n      size,\n      src,\n      poster,\n      showControls = true,\n      autoHide = true,\n      ...props\n    },\n    ref\n  ) => {\n    const [isPlaying, setIsPlaying] = React.useState(false);\n    const [currentTime, setCurrentTime] = React.useState(0);\n    const [duration, setDuration] = React.useState(0);\n    const [volume, setVolume] = React.useState(1);\n    const [isMuted, setIsMuted] = React.useState(false);\n    const [isFullscreen, setIsFullscreen] = React.useState(false);\n    const [showControlsState, setShowControlsState] = React.useState(true);\n\n    const videoRef = React.useRef<HTMLVideoElement>(null);\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const hideControlsTimeoutRef = React.useRef<number | null>(null);\n    const liveRef = React.useRef<HTMLDivElement>(null);\n\n    React.useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement);\n\n    const formatTime = (time: number) => {\n      const hours = Math.floor(time / 3600);\n      const minutes = Math.floor((time % 3600) / 60);\n      const seconds = Math.floor(time % 60);\n      if (hours > 0) {\n        return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds\n          .toString()\n          .padStart(2, \"0\")}`;\n      }\n      return `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n    };\n\n    const announce = React.useCallback((msg: string) => {\n      if (!liveRef.current) return;\n      liveRef.current.textContent = msg;\n    }, []);\n\n    const resetHideControlsTimeout = React.useCallback(() => {\n      if (hideControlsTimeoutRef.current)\n        window.clearTimeout(hideControlsTimeoutRef.current);\n      if (autoHide && isPlaying) {\n        hideControlsTimeoutRef.current = window.setTimeout(() => {\n          setShowControlsState(false);\n        }, 3000);\n      }\n    }, [autoHide, isPlaying]);\n\n    const togglePlay = React.useCallback(() => {\n      const el = videoRef.current;\n      if (!el) return;\n      if (el.paused) {\n        el.play();\n        announce(\"Playing\");\n      } else {\n        el.pause();\n        announce(\"Paused\");\n      }\n    }, [announce]);\n\n    const toggleMute = React.useCallback(() => {\n      const el = videoRef.current;\n      if (!el) return;\n      el.muted = !el.muted;\n      setIsMuted(el.muted);\n      announce(el.muted ? \"Muted\" : \"Unmuted\");\n    }, [announce]);\n\n    const handleVolumeChange = React.useCallback((newVolume: number) => {\n      const el = videoRef.current;\n      setVolume(newVolume);\n      if (el) {\n        el.volume = newVolume;\n        setIsMuted(newVolume === 0);\n      }\n    }, []);\n\n    const handleSeek = React.useCallback((newTime: number) => {\n      setCurrentTime(newTime);\n      const el = videoRef.current;\n      if (el) el.currentTime = newTime;\n    }, []);\n\n    const toggleFullscreen = React.useCallback(() => {\n      if (document.fullscreenElement) {\n        document.exitFullscreen();\n        setIsFullscreen(false);\n      } else {\n        containerRef.current?.requestFullscreen();\n        setIsFullscreen(true);\n      }\n    }, []);\n\n    const skip = React.useCallback(\n      (seconds: number) => {\n        const el = videoRef.current;\n        if (!el) return;\n        const next = Math.max(\n          0,\n          Math.min(el.duration || 0, (el.currentTime || 0) + seconds)\n        );\n        el.currentTime = next;\n        setCurrentTime(next);\n        announce(\n          `${seconds > 0 ? \"Forward\" : \"Back\"} ${Math.abs(seconds)} seconds`\n        );\n      },\n      [announce]\n    );\n\n    const handleMouseMove = () => {\n      setShowControlsState(true);\n      resetHideControlsTimeout();\n    };\n\n    React.useEffect(() => {\n      const video = videoRef.current;\n      if (!video) return;\n      const onLoadedMetadata = () => setDuration(video.duration || 0);\n      const onTimeUpdate = () => setCurrentTime(video.currentTime || 0);\n      const onPlay = () => {\n        setIsPlaying(true);\n        resetHideControlsTimeout();\n      };\n      const onPause = () => {\n        setIsPlaying(false);\n        setShowControlsState(true);\n        if (hideControlsTimeoutRef.current)\n          window.clearTimeout(hideControlsTimeoutRef.current);\n      };\n      const onVol = () => {\n        setVolume(video.volume);\n        setIsMuted(video.muted);\n      };\n      video.addEventListener(\"loadedmetadata\", onLoadedMetadata);\n      video.addEventListener(\"timeupdate\", onTimeUpdate);\n      video.addEventListener(\"play\", onPlay);\n      video.addEventListener(\"pause\", onPause);\n      video.addEventListener(\"volumechange\", onVol);\n      return () => {\n        video.removeEventListener(\"loadedmetadata\", onLoadedMetadata);\n        video.removeEventListener(\"timeupdate\", onTimeUpdate);\n        video.removeEventListener(\"play\", onPlay);\n        video.removeEventListener(\"pause\", onPause);\n        video.removeEventListener(\"volumechange\", onVol);\n        if (hideControlsTimeoutRef.current)\n          window.clearTimeout(hideControlsTimeoutRef.current);\n      };\n    }, [autoHide, isPlaying, resetHideControlsTimeout]);\n\n    React.useEffect(() => {\n      const onFs = () => setIsFullscreen(!!document.fullscreenElement);\n      document.addEventListener(\"fullscreenchange\", onFs);\n      return () => document.removeEventListener(\"fullscreenchange\", onFs);\n    }, []);\n\n    React.useEffect(() => {\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (\n          !(\n            containerRef.current &&\n            containerRef.current.contains(document.activeElement)\n          )\n        ) {\n          return;\n        }\n\n        switch (e.key) {\n          case \" \":\n          case \"k\":\n            e.preventDefault();\n            togglePlay();\n            break;\n          case \"m\":\n            e.preventDefault();\n            toggleMute();\n            break;\n          case \"f\":\n            e.preventDefault();\n            toggleFullscreen();\n            break;\n          case \"ArrowLeft\":\n            e.preventDefault();\n            skip(-10);\n            break;\n          case \"ArrowRight\":\n            e.preventDefault();\n            skip(10);\n            break;\n          case \"ArrowUp\":\n            e.preventDefault();\n            handleVolumeChange(\n              Math.min(1, Math.round((volume + 0.1) * 100) / 100)\n            );\n            break;\n          case \"ArrowDown\":\n            e.preventDefault();\n            handleVolumeChange(\n              Math.max(0, Math.round((volume - 0.1) * 100) / 100)\n            );\n            break;\n          default:\n            break;\n        }\n      };\n\n      document.addEventListener(\"keydown\", handleKeyDown);\n\n      return () => {\n        document.removeEventListener(\"keydown\", handleKeyDown);\n      };\n    }, [\n      togglePlay,\n      toggleMute,\n      toggleFullscreen,\n      skip,\n      volume,\n      handleVolumeChange,\n    ]);\n\n    const progressPct = duration ? (currentTime / duration) * 100 : 0;\n    const volumePct = (isMuted ? 0 : volume) * 100;\n\n    return (\n      <div\n        aria-label=\"Video player\"\n        className={cn(\n          videoPlayerVariants({ size }),\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-safe:duration-200 motion-reduce:animate-none\",\n          className\n        )}\n        onMouseLeave={() =>\n          autoHide && isPlaying && setShowControlsState(false)\n        }\n        onMouseMove={handleMouseMove}\n        ref={containerRef}\n        role=\"region\"\n        tabIndex={0}\n      >\n        <div\n          aria-atomic=\"true\"\n          aria-live=\"polite\"\n          className=\"sr-only\"\n          ref={liveRef}\n        />\n        <video\n          aria-label=\"Video\"\n          className=\"h-full w-full object-cover\"\n          onClick={togglePlay}\n          poster={poster}\n          ref={videoRef}\n          src={src}\n          {...props}\n        />\n        {showControls && (\n          <>\n            <div\n              className={cn(\n                \"pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity motion-safe:duration-200\",\n                !isPlaying || showControlsState ? \"opacity-100\" : \"opacity-0\"\n              )}\n            >\n              <button\n                aria-label={isPlaying ? \"Pause\" : \"Play\"}\n                className=\"pointer-events-auto flex size-16 items-center justify-center rounded-full border border-white/30 bg-white/20 text-white backdrop-blur-sm transition-colors hover:bg-white/30 motion-safe:duration-200\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  togglePlay();\n                }}\n                type=\"button\"\n              >\n                {isPlaying ? (\n                  <Pause aria-hidden=\"true\" className=\"size-6\" />\n                ) : (\n                  <Play aria-hidden=\"true\" className=\"size-6\" />\n                )}\n              </button>\n            </div>\n\n            <div\n              className={cn(\n                \"pointer-events-none absolute inset-x-0 bottom-0 bg-linear-to-t from-black/80 via-black/40 to-transparent transition-opacity motion-safe:duration-200\",\n                showControlsState ? \"opacity-100\" : \"opacity-0\"\n              )}\n            >\n              <div className=\"pointer-events-auto flex flex-col gap-2 p-4\">\n                <div className=\"flex items-center gap-2 text-sm text-white\">\n                  <span aria-live=\"off\" className=\"min-w-0 font-mono text-xs\">\n                    {formatTime(currentTime)}\n                  </span>\n                  <div className=\"group/progress relative flex-1\">\n                    <label className=\"sr-only\" htmlFor=\"video-progress\">\n                      Seek\n                    </label>\n                    <input\n                      aria-label=\"Seek\"\n                      aria-valuemax={Math.max(0, Math.floor(duration))}\n                      aria-valuemin={0}\n                      aria-valuenow={Math.floor(currentTime)}\n                      className=\"h-1 w-full cursor-pointer appearance-none rounded-full bg-white/30 focus-visible:outline-none motion-safe:duration-200 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white group-hover/progress:[&::-webkit-slider-thumb]:scale-125\"\n                      id=\"video-progress\"\n                      max={duration || 0}\n                      min={0}\n                      onChange={(e) => {\n                        e.stopPropagation();\n                        handleSeek(Number.parseFloat(e.target.value));\n                      }}\n                      role=\"slider\"\n                      style={{\n                        background: `linear-gradient(to right, #ffffff 0%, #ffffff ${progressPct}%, rgba(255,255,255,0.3) ${progressPct}%, rgba(255,255,255,0.3) 100%)`,\n                      }}\n                      type=\"range\"\n                      value={currentTime}\n                    />\n                  </div>\n                  <span className=\"min-w-0 font-mono text-xs\">\n                    {formatTime(duration)}\n                  </span>\n                </div>\n\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"flex items-center gap-2\">\n                    <button\n                      aria-label=\"Skip back 10 seconds\"\n                      className=\"rounded-md p-2 text-white transition-colors hover:bg-white/20\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        skip(-10);\n                      }}\n                      type=\"button\"\n                    >\n                      <SkipBack aria-hidden=\"true\" className=\"size-4\" />\n                    </button>\n                    <button\n                      aria-label={isPlaying ? \"Pause\" : \"Play\"}\n                      className=\"rounded-md p-2 text-white transition-colors hover:bg-white/20\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        togglePlay();\n                      }}\n                      type=\"button\"\n                    >\n                      {isPlaying ? (\n                        <Pause aria-hidden=\"true\" className=\"size-4\" />\n                      ) : (\n                        <Play aria-hidden=\"true\" className=\"size-4\" />\n                      )}\n                    </button>\n                    <button\n                      aria-label=\"Skip forward 10 seconds\"\n                      className=\"rounded-md p-2 text-white transition-colors hover:bg-white/20\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        skip(10);\n                      }}\n                      type=\"button\"\n                    >\n                      <SkipForward aria-hidden=\"true\" className=\"size-4\" />\n                    </button>\n                    <div className=\"group/volume flex items-center gap-2\">\n                      <button\n                        aria-label={isMuted || volume === 0 ? \"Unmute\" : \"Mute\"}\n                        className=\"rounded-md p-2 text-white transition-colors hover:bg-white/20\"\n                        onClick={(e) => {\n                          e.stopPropagation();\n                          toggleMute();\n                        }}\n                        type=\"button\"\n                      >\n                        {isMuted || volume === 0 ? (\n                          <VolumeX aria-hidden=\"true\" className=\"size-4\" />\n                        ) : (\n                          <Volume2 aria-hidden=\"true\" className=\"size-4\" />\n                        )}\n                      </button>\n                      <div className=\"w-0 overflow-hidden transition-all group-hover/volume:w-20 motion-safe:duration-200\">\n                        <label className=\"sr-only\" htmlFor=\"video-volume\">\n                          Volume\n                        </label>\n                        <input\n                          aria-label=\"Volume\"\n                          aria-valuemax={100}\n                          aria-valuemin={0}\n                          aria-valuenow={Math.round(volumePct)}\n                          className=\"h-1 w-full cursor-pointer appearance-none rounded-full bg-white/30 focus-visible:outline-none [&::-webkit-slider-thumb]:size-2 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white\"\n                          id=\"video-volume\"\n                          max={1}\n                          min={0}\n                          onChange={(e) => {\n                            e.stopPropagation();\n                            handleVolumeChange(\n                              Number.parseFloat(e.target.value)\n                            );\n                          }}\n                          role=\"slider\"\n                          step={0.1}\n                          style={{\n                            background: `linear-gradient(to right, #ffffff 0%, #ffffff ${volumePct}%, rgba(255,255,255,0.3) ${volumePct}%, rgba(255,255,255,0.3) 100%)`,\n                          }}\n                          type=\"range\"\n                          value={isMuted ? 0 : volume}\n                        />\n                      </div>\n                    </div>\n                  </div>\n\n                  <div className=\"flex items-center gap-2\">\n                    <button\n                      aria-label={\n                        isFullscreen ? \"Exit fullscreen\" : \"Enter fullscreen\"\n                      }\n                      className=\"rounded-md p-2 text-white transition-colors hover:bg-white/20\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        toggleFullscreen();\n                      }}\n                      type=\"button\"\n                    >\n                      {isFullscreen ? (\n                        <Minimize aria-hidden=\"true\" className=\"size-4\" />\n                      ) : (\n                        <Maximize aria-hidden=\"true\" className=\"size-4\" />\n                      )}\n                    </button>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </>\n        )}\n      </div>\n    );\n  }\n);\n\nVideoPlayer.displayName = \"VideoPlayer\";\n\nexport { VideoPlayer };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "component"
  ],
  "type": "registry:ui"
}