{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video",
  "title": "Video",
  "author": "vorhdam <https://github.com/vorhdam>",
  "description": "A video playback with extendable control modules already included such as volume, speed and fullscreen.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "slider"
  ],
  "files": [
    {
      "path": "src/components/ui/video.tsx",
      "content": "\"use client\";\r\n\r\nimport * as MenuPrimitive from \"@base-ui/react/menu\";\r\nimport * as SliderPrimitive from \"@base-ui/react/slider\";\r\nimport * as React from \"react\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n  DropdownMenu,\r\n  DropdownMenuItem,\r\n  DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Slider } from \"@/components/ui/slider\";\r\nimport { useIsMobile } from \"@/hooks/use-mobile\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n  Check,\r\n  Gauge,\r\n  Maximize,\r\n  Minimize,\r\n  Pause,\r\n  Play,\r\n  Repeat,\r\n  Repeat1,\r\n  Volume,\r\n  Volume1,\r\n  Volume2,\r\n  VolumeX,\r\n} from \"lucide-react\";\r\n\r\ntype VideoContext = {\r\n  videoRef: React.RefObject<HTMLVideoElement | null>;\r\n  containerRef: React.RefObject<HTMLDivElement | null>;\r\n  paused: boolean;\r\n  setPaused: React.Dispatch<React.SetStateAction<boolean>>;\r\n  progress: number;\r\n  setProgress: React.Dispatch<React.SetStateAction<number>>;\r\n  volume: number;\r\n  setVolume: React.Dispatch<React.SetStateAction<number>>;\r\n  loop: boolean;\r\n  setLoop: React.Dispatch<React.SetStateAction<boolean>>;\r\n  muted: boolean;\r\n  setMuted: React.Dispatch<React.SetStateAction<boolean>>;\r\n  fullscreen: boolean;\r\n  setFullscreen: React.Dispatch<React.SetStateAction<boolean>>;\r\n  playbackRate: number;\r\n  setPlaybackRate: React.Dispatch<React.SetStateAction<number>>;\r\n  duration: number;\r\n  setDuration: React.Dispatch<React.SetStateAction<number>>;\r\n  currentTime: number;\r\n  setCurrentTime: React.Dispatch<React.SetStateAction<number>>;\r\n};\r\n\r\nconst DefaultVideoContext: VideoContext = {\r\n  videoRef: { current: null },\r\n  containerRef: { current: null },\r\n  paused: true,\r\n  setPaused: () => {},\r\n  progress: 0,\r\n  setProgress: () => {},\r\n  volume: 1,\r\n  setVolume: () => {},\r\n  loop: false,\r\n  setLoop: () => {},\r\n  muted: false,\r\n  setMuted: () => {},\r\n  fullscreen: false,\r\n  setFullscreen: () => {},\r\n  playbackRate: 1,\r\n  setPlaybackRate: () => {},\r\n  duration: 0,\r\n  setDuration: () => {},\r\n  currentTime: 0,\r\n  setCurrentTime: () => {},\r\n};\r\n\r\nif (typeof window !== \"undefined\") {\r\n  const _error = console.error.bind(console);\r\n  console.error = (...args) => {\r\n    if (typeof args[0] === \"string\" && args[0].includes(\"script\")) return;\r\n    _error(...args);\r\n  };\r\n}\r\n\r\nconst Context = React.createContext<VideoContext>(DefaultVideoContext);\r\n\r\ntype VideoProviderProps = {\r\n  defaultVolume?: number;\r\n  defaultMuted?: boolean;\r\n  defaultLoop?: boolean;\r\n  defaultRate?: number;\r\n  children: React.ReactNode;\r\n};\r\n\r\nfunction VideoProvider({\r\n  defaultVolume = 0.5,\r\n  defaultMuted = false,\r\n  defaultLoop = false,\r\n  defaultRate = 1,\r\n  children,\r\n}: VideoProviderProps) {\r\n  const videoRef = React.useRef<HTMLVideoElement>(null);\r\n  const containerRef = React.useRef<HTMLDivElement>(null);\r\n\r\n  const [paused, setPaused] = React.useState<boolean>(true);\r\n  const [progress, setProgress] = React.useState<number>(0);\r\n  const [volume, setVolume] = React.useState<number>(defaultVolume);\r\n  const [muted, setMuted] = React.useState<boolean>(defaultMuted);\r\n  const [loop, setLoop] = React.useState<boolean>(defaultLoop);\r\n  const [playbackRate, setPlaybackRate] = React.useState<number>(defaultRate);\r\n  const [fullscreen, setFullscreen] = React.useState<boolean>(false);\r\n  const [duration, setDuration] = React.useState<number>(0);\r\n  const [currentTime, setCurrentTime] = React.useState<number>(0);\r\n\r\n  React.useEffect(() => {\r\n    if (!videoRef.current) return;\r\n    videoRef.current.volume = volume;\r\n    videoRef.current.muted = muted;\r\n    videoRef.current.playbackRate = playbackRate;\r\n    videoRef.current.loop = loop;\r\n\r\n    if (currentTime === duration && !loop) setPaused(true);\r\n    setDuration(videoRef.current.duration);\r\n  }, [volume, muted, playbackRate, loop, currentTime, duration]);\r\n\r\n  const values = {\r\n    videoRef,\r\n    containerRef,\r\n    paused,\r\n    setPaused,\r\n    progress,\r\n    setProgress,\r\n    volume,\r\n    setVolume,\r\n    loop,\r\n    setLoop,\r\n    muted,\r\n    setMuted,\r\n    fullscreen,\r\n    setFullscreen,\r\n    playbackRate,\r\n    setPlaybackRate,\r\n    duration,\r\n    setDuration,\r\n    currentTime,\r\n    setCurrentTime,\r\n  };\r\n\r\n  return <Context.Provider value={values}>{children}</Context.Provider>;\r\n}\r\n\r\nfunction VideoPlayer({\r\n  src,\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"video\">) {\r\n  const mobile = useIsMobile();\r\n\r\n  const {\r\n    videoRef,\r\n    containerRef,\r\n    paused,\r\n    fullscreen,\r\n    setPaused,\r\n    setCurrentTime,\r\n    setProgress,\r\n    setDuration,\r\n  } = React.useContext(Context);\r\n\r\n  const showControlRef = React.useRef<ReturnType<typeof setTimeout> | null>(\r\n    null,\r\n  );\r\n  const [showControl, setShowControl] = React.useState<boolean>(false);\r\n\r\n  const revealControls = React.useCallback(() => {\r\n    setShowControl(true);\r\n    if (showControlRef.current) clearTimeout(showControlRef.current);\r\n    showControlRef.current = setTimeout(() => {\r\n      setShowControl(false);\r\n    }, 3000);\r\n  }, [setShowControl]);\r\n\r\n  const handleTouch = () => {\r\n    handlePlay();\r\n    revealControls();\r\n  };\r\n\r\n  const handlePlay = () => {\r\n    if (!videoRef.current) return;\r\n    if (paused) videoRef.current.play();\r\n    else videoRef.current.pause();\r\n    setPaused(!paused);\r\n  };\r\n\r\n  const handleTimeUpdate = () => {\r\n    if (!videoRef.current) return;\r\n    setCurrentTime(videoRef.current.currentTime);\r\n    setProgress(\r\n      (videoRef.current.currentTime / videoRef.current.duration) * 100,\r\n    );\r\n  };\r\n\r\n  const handleLoadedMetadata = () => {\r\n    setDuration(videoRef.current?.duration || 0);\r\n  };\r\n\r\n  if (mobile)\r\n    return (\r\n      <div className=\"relative group overflow-hidden bg-secondary flex items-center justify-center transition-all rounded-xl aspect-video\">\r\n        <video\r\n          src={src}\r\n          className=\"w-full max-h-full transition-transform\"\r\n          playsInline\r\n          controls\r\n        />\r\n      </div>\r\n    );\r\n\r\n  if (!mobile)\r\n    return (\r\n      <div\r\n        ref={containerRef}\r\n        className={cn(\r\n          \"relative group overflow-hidden bg-secondary flex items-center justify-center transition-all dark\",\r\n          fullscreen ? \"w-screen h-screen\" : \"rounded-xl aspect-video\",\r\n          className,\r\n        )}\r\n      >\r\n        <video\r\n          ref={videoRef}\r\n          src={src}\r\n          className=\"w-full max-h-full transition-transform\"\r\n          onTimeUpdate={handleTimeUpdate}\r\n          onLoadedMetadata={handleLoadedMetadata}\r\n          playsInline\r\n          {...props}\r\n        />\r\n        <div\r\n          className={cn(\r\n            \"absolute inset-0 bg-background/20 transition-opacity flex flex-col justify-end p-2 bg-linear-to-t from-background/80 via-transparent to-transparent\",\r\n            \"opacity-0 group-hover:opacity-100\",\r\n            showControl && \"opacity-100\",\r\n          )}\r\n        >\r\n          <div\r\n            className=\"size-full mb-3\"\r\n            onClick={handlePlay}\r\n            onTouchEnd={handleTouch}\r\n          />\r\n          {children}\r\n        </div>\r\n      </div>\r\n    );\r\n}\r\n\r\nfunction VideoControlSeek({\r\n  className,\r\n  ...props\r\n}: SliderPrimitive.SliderRoot.Props) {\r\n  const { videoRef, progress, setProgress } = React.useContext(Context);\r\n  const value = [progress];\r\n\r\n  const handleSeek = (value: number | readonly number[]) => {\r\n    if (!videoRef.current?.duration) return;\r\n    const nextValue = Array.isArray(value) ? value[0] : value;\r\n    const newTime = (nextValue / 100) * videoRef.current.duration;\r\n    videoRef.current.currentTime = newTime;\r\n    setProgress(nextValue);\r\n  };\r\n\r\n  return (\r\n    <div className=\"px-1 pb-2\">\r\n      <Slider\r\n        value={value}\r\n        max={100}\r\n        step={0.1}\r\n        onValueChange={handleSeek}\r\n        className={cn(\"cursor-pointer\", className)}\r\n        {...props}\r\n      />\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction VideoControlList({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div\r\n      className={cn(\"flex items-center justify-between\", className)}\r\n      {...props}\r\n    >\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction VideoControlGroup({\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div className={cn(\"flex items-center gap-2\", className)} {...props}>\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction VideoControlPlay({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"button\">) {\r\n  const { videoRef, paused, setPaused } = React.useContext(Context);\r\n\r\n  const handlePlay = () => {\r\n    if (!videoRef.current) return;\r\n    if (paused) videoRef.current.play();\r\n    else videoRef.current.pause();\r\n    setPaused(!paused);\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      variant=\"ghost\"\r\n      size=\"icon\"\r\n      onClick={handlePlay}\r\n      className={cn(\"text-white rounded-full aspect-square h-8\", className)}\r\n      {...props}\r\n    >\r\n      {paused ? (\r\n        <Play className=\"fill-current stroke-0\" />\r\n      ) : (\r\n        <Pause className=\"fill-current stroke-0\" />\r\n      )}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction VideoControlTimer({ className }: React.ComponentProps<\"button\">) {\r\n  const { currentTime, duration } = React.useContext(Context);\r\n  const [reverse, setReverse] = React.useState<boolean>(false);\r\n\r\n  const displayTime = reverse\r\n    ? `-${Math.floor((duration - currentTime) / 60)}` +\r\n      \":\" +\r\n      `${Math.floor((duration - currentTime) % 60)\r\n        .toString()\r\n        .padStart(2, \"0\")}`\r\n    : `${Math.floor(currentTime / 60)}` +\r\n      \":\" +\r\n      `${Math.floor(currentTime % 60)\r\n        .toString()\r\n        .padStart(2, \"0\")}`;\r\n\r\n  const handleToggle = () => {\r\n    setReverse(!reverse);\r\n  };\r\n\r\n  return (\r\n    <div\r\n      className={cn(\"text-white text-sm cursor-pointer\", className)}\r\n      onClick={handleToggle}\r\n    >\r\n      {displayTime}\r\n      <span className=\"text-white/50 mx-1\">/</span>\r\n      {Math.floor(duration / 60)}:\r\n      {Math.floor(duration % 60)\r\n        .toString()\r\n        .padStart(2, \"0\")}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction VideoControlPlaybackRate({\r\n  rates,\r\n  className,\r\n  ...props\r\n}: MenuPrimitive.MenuTrigger.Props & { rates: number[] }) {\r\n  const { playbackRate, setPlaybackRate } = React.useContext(Context);\r\n\r\n  const handlePlaybackRate = (rate: number) => {\r\n    setPlaybackRate(rate);\r\n  };\r\n\r\n  return (\r\n    <DropdownMenu>\r\n      <DropdownMenuTrigger\r\n        className={cn(\r\n          \"inline-flex items-center justify-center text-white rounded-full aspect-square h-8 hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground cursor-pointer\",\r\n          className,\r\n        )}\r\n        {...props}\r\n      >\r\n        <Gauge className=\"size-4\" />\r\n      </DropdownMenuTrigger>\r\n      <DropdownMenuContainer\r\n        align=\"end\"\r\n        className=\"bg-zinc-900 text-white border-zinc-800\"\r\n      >\r\n        {rates.map((rate) => (\r\n          <DropdownMenuItem\r\n            key={rate}\r\n            onClick={() => handlePlaybackRate(rate)}\r\n            className=\"flex items-center justify-between focus:bg-zinc-800 focus:text-white\"\r\n          >\r\n            {rate}x{playbackRate === rate && <Check className=\"size-4 ml-2\" />}\r\n          </DropdownMenuItem>\r\n        ))}\r\n      </DropdownMenuContainer>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\nfunction VideoControlLoop({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"button\">) {\r\n  const { videoRef, loop, setLoop } = React.useContext(Context);\r\n\r\n  const handleLoop = () => {\r\n    if (!videoRef.current) return;\r\n    setLoop(!loop);\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      variant=\"ghost\"\r\n      size=\"icon\"\r\n      onClick={handleLoop}\r\n      className={cn(\"text-white rounded-full aspect-square h-8\", className)}\r\n      {...props}\r\n    >\r\n      {loop ? <Repeat1 /> : <Repeat />}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction VideoControlVolume({\r\n  className,\r\n  ...props\r\n}: MenuPrimitive.MenuTrigger.Props) {\r\n  const { muted, volume, setMuted, setVolume } = React.useContext(Context);\r\n\r\n  const value = React.useMemo(\r\n    () => [muted ? 0 : (Number(volume) || 0) * 100],\r\n    [muted, volume],\r\n  );\r\n\r\n  const handleMute = () => {\r\n    setMuted(!muted);\r\n  };\r\n\r\n  const handleVolume = (newVolume: number | readonly number[]) => {\r\n    const nextValue = Array.isArray(newVolume) ? newVolume[0] : newVolume;\r\n    setVolume(nextValue / 100);\r\n    if (nextValue > 0) setMuted(false);\r\n  };\r\n\r\n  return (\r\n    <DropdownMenu>\r\n      <DropdownMenuTrigger\r\n        className={cn(\r\n          \"inline-flex items-center justify-center text-white rounded-full aspect-square h-8 hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground cursor-pointer\",\r\n          className,\r\n        )}\r\n        {...props}\r\n      >\r\n        <VolumeIcon className=\"size-4\" muted={muted} volume={volume} />\r\n      </DropdownMenuTrigger>\r\n      <DropdownMenuContainer align=\"end\" className=\"min-w-40\">\r\n        <div className=\"flex flex-row items-center gap-1\">\r\n          <Button\r\n            variant=\"ghost\"\r\n            size=\"icon\"\r\n            className=\"text-accent-foreground/80 hover:bg-accent/20 rounded-full\"\r\n            onClick={handleMute}\r\n          >\r\n            <VolumeIcon muted={muted} volume={volume} />\r\n          </Button>\r\n          <Slider\r\n            value={value}\r\n            className=\"w-20 group-hover/volume:block transition-all mr-1.5\"\r\n            onValueChange={(v) => handleVolume(v)}\r\n          />\r\n        </div>\r\n      </DropdownMenuContainer>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\nfunction VideoControlFullscreen({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"button\">) {\r\n  const { containerRef, fullscreen, setFullscreen } = React.useContext(Context);\r\n\r\n  const handleFullscreen = () => {\r\n    if (!document.fullscreenElement) {\r\n      containerRef.current?.requestFullscreen();\r\n      setFullscreen(true);\r\n    } else {\r\n      document.exitFullscreen();\r\n      setFullscreen(false);\r\n    }\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      variant=\"ghost\"\r\n      size=\"icon\"\r\n      onClick={handleFullscreen}\r\n      className={cn(\"text-white rounded-full aspect-square h-8\", className)}\r\n      {...props}\r\n    >\r\n      {fullscreen ? <Minimize /> : <Maximize />}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction VolumeIcon({\r\n  className,\r\n  muted,\r\n  volume,\r\n}: {\r\n  className?: string;\r\n  muted: boolean;\r\n  volume: number;\r\n}) {\r\n  if (muted || volume === 0) return <VolumeX className={className} />;\r\n  if (volume > 0.66) return <Volume2 className={className} />;\r\n  if (volume > 0.33) return <Volume1 className={className} />;\r\n  return <Volume className={className} />;\r\n}\r\n\r\nfunction DropdownMenuContainer({\r\n  align = \"start\",\r\n  alignOffset = 0,\r\n  side = \"bottom\",\r\n  sideOffset = 4,\r\n  className,\r\n  children,\r\n}: {\r\n  align?: MenuPrimitive.MenuPositioner.Props[\"align\"];\r\n  alignOffset?: number;\r\n  side?: MenuPrimitive.MenuPositioner.Props[\"side\"];\r\n  sideOffset?: number;\r\n  className?: string;\r\n  children: React.ReactNode;\r\n}) {\r\n  const { fullscreen, containerRef } = React.useContext(Context);\r\n  const container = fullscreen ? containerRef : { current: undefined };\r\n\r\n  return (\r\n    <MenuPrimitive.Menu.Portal container={container.current}>\r\n      <MenuPrimitive.Menu.Positioner\r\n        className=\"isolate z-50 outline-none\"\r\n        align={align}\r\n        alignOffset={alignOffset}\r\n        side={side}\r\n        sideOffset={sideOffset}\r\n      >\r\n        <MenuPrimitive.Menu.Popup\r\n          data-slot=\"dropdown-menu-content\"\r\n          className={cn(\r\n            \"z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95\",\r\n            className,\r\n          )}\r\n        >\r\n          {children}\r\n        </MenuPrimitive.Menu.Popup>\r\n      </MenuPrimitive.Menu.Positioner>\r\n    </MenuPrimitive.Menu.Portal>\r\n  );\r\n}\r\n\r\nexport {\r\n  VideoControlFullscreen,\r\n  VideoControlGroup,\r\n  VideoControlList,\r\n  VideoControlLoop,\r\n  VideoControlPlay,\r\n  VideoControlPlaybackRate,\r\n  VideoControlSeek,\r\n  VideoControlTimer,\r\n  VideoControlVolume,\r\n  VideoPlayer,\r\n  VideoProvider,\r\n};\r\n",
      "type": "registry:ui"
    },
    {
      "path": "src/hooks/use-mobile.ts",
      "content": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n  React.useEffect(() => {\n    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n    const onChange = () => {\n      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n    }\n    mql.addEventListener(\"change\", onChange)\n    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n    return () => mql.removeEventListener(\"change\", onChange)\n  }, [])\n\n  return !!isMobile\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:block"
}