{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio",
  "title": "Audio",
  "author": "vorhdam <https://github.com/vorhdam>",
  "description": "An audio playback with extendable control modules already included such as volume and speed.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "slider"
  ],
  "files": [
    {
      "path": "src/components/ui/audio.tsx",
      "content": "\"use client\";\r\n\r\nimport { MenuTrigger } from \"@base-ui/react\";\r\nimport * as React from \"react\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n  DropdownMenu,\r\n  DropdownMenuContent,\r\n  DropdownMenuItem,\r\n  DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Slider } from \"@/components/ui/slider\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n  Check,\r\n  Gauge,\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\ndeclare global {\r\n  interface Window {\r\n    webkitAudioContext: typeof AudioContext;\r\n  }\r\n}\r\n\r\ntype AudioContext = {\r\n  audioRef: React.RefObject<HTMLAudioElement | null>;\r\n  containerRef: React.RefObject<HTMLDivElement | null>;\r\n  analyserRef: React.RefObject<AnalyserNode | null>;\r\n  graphRef: React.RefObject<globalThis.AudioContext | 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  muted: boolean;\r\n  setMuted: React.Dispatch<React.SetStateAction<boolean>>;\r\n  loop: boolean;\r\n  setLoop: 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 DefaultAudioContext: AudioContext = {\r\n  audioRef: { current: null },\r\n  containerRef: { current: null },\r\n  analyserRef: { current: null },\r\n  graphRef: { 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  muted: false,\r\n  setMuted: () => {},\r\n  loop: false,\r\n  setLoop: () => {},\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 WaveformCache = new WeakMap<\r\n  HTMLAudioElement,\r\n  { ctx: globalThis.AudioContext; analyser: AnalyserNode }\r\n>();\r\n\r\nconst Context = React.createContext<AudioContext>(DefaultAudioContext);\r\n\r\ntype AudioProviderProps = {\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 AudioProvider({\r\n  defaultVolume = 0.5,\r\n  defaultMuted = false,\r\n  defaultLoop = false,\r\n  defaultRate = 1,\r\n  children,\r\n}: AudioProviderProps) {\r\n  const audioRef = React.useRef<HTMLAudioElement>(null);\r\n  const containerRef = React.useRef<HTMLDivElement>(null);\r\n  const analyserRef = React.useRef<AnalyserNode>(null);\r\n  const graphRef = React.useRef<globalThis.AudioContext>(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 [duration, setDuration] = React.useState<number>(0);\r\n  const [currentTime, setCurrentTime] = React.useState<number>(0);\r\n\r\n  React.useEffect(() => {\r\n    if (!audioRef.current) return;\r\n    audioRef.current.volume = volume;\r\n    audioRef.current.muted = muted;\r\n    audioRef.current.playbackRate = playbackRate;\r\n    audioRef.current.loop = loop;\r\n    setDuration(audioRef.current.duration);\r\n\r\n    if (WaveformCache.has(audioRef.current)) {\r\n      const cached = WaveformCache.get(audioRef.current)!;\r\n      analyserRef.current = cached.analyser;\r\n      graphRef.current = cached.ctx;\r\n      return;\r\n    }\r\n\r\n    const ctx = new (window.AudioContext || window.webkitAudioContext)();\r\n    const source = ctx.createMediaElementSource(audioRef.current);\r\n    const analyser = ctx.createAnalyser();\r\n    analyser.fftSize = 256;\r\n    source.connect(analyser);\r\n    analyser.connect(ctx.destination);\r\n\r\n    analyserRef.current = analyser;\r\n    graphRef.current = ctx;\r\n    WaveformCache.set(audioRef.current, { ctx, analyser });\r\n  }, [volume, muted, playbackRate, loop]);\r\n\r\n  const values = {\r\n    audioRef,\r\n    containerRef,\r\n    analyserRef,\r\n    graphRef,\r\n    paused,\r\n    setPaused,\r\n    progress,\r\n    setProgress,\r\n    volume,\r\n    setVolume,\r\n    muted,\r\n    setMuted,\r\n    loop,\r\n    setLoop,\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 AudioPlayer({\r\n  src,\r\n  className,\r\n  children,\r\n  ...props\r\n}: React.ComponentProps<\"audio\">) {\r\n  const {\r\n    audioRef,\r\n    containerRef,\r\n    setPaused,\r\n    setCurrentTime,\r\n    setProgress,\r\n    setDuration,\r\n  } = React.useContext(Context);\r\n\r\n  const handleTimeUpdate = () => {\r\n    if (!audioRef.current) return;\r\n    setCurrentTime(audioRef.current.currentTime);\r\n    setProgress(\r\n      (audioRef.current.currentTime / audioRef.current.duration) * 100,\r\n    );\r\n  };\r\n\r\n  const handleLoadedMetadata = () => {\r\n    setDuration(audioRef.current?.duration || 0);\r\n  };\r\n\r\n  const handleEnded = () => {\r\n    setPaused(true);\r\n  };\r\n\r\n  return (\r\n    <div\r\n      ref={containerRef}\r\n      className={cn(\r\n        \"relative flex items-center gap-3 bg-secondary rounded-xl px-3 py-2 min-h-14 dark:bg-secondary/50\",\r\n        className,\r\n      )}\r\n    >\r\n      <audio\r\n        ref={audioRef}\r\n        src={src}\r\n        onTimeUpdate={handleTimeUpdate}\r\n        onLoadedMetadata={handleLoadedMetadata}\r\n        onEnded={handleEnded}\r\n        {...props}\r\n      />\r\n      <div className=\"flex items-center justify-between w-full gap-2\">\r\n        {children}\r\n      </div>\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction AudioControlSeek({ className }: { className?: string }) {\r\n  const { audioRef, setProgress, setCurrentTime } = React.useContext(Context);\r\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\r\n\r\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\r\n    if (!audioRef.current || !wrapperRef.current) return;\r\n    const rect = wrapperRef.current.getBoundingClientRect();\r\n    const ratio = (e.clientX - rect.left) / rect.width;\r\n    const newTime = ratio * audioRef.current.duration;\r\n    audioRef.current.currentTime = newTime;\r\n    setCurrentTime(newTime);\r\n    setProgress(ratio * 100);\r\n  };\r\n\r\n  return (\r\n    <div\r\n      ref={wrapperRef}\r\n      className={cn(\"flex-1 flex items-center\", className)}\r\n      onClick={handleClick}\r\n    >\r\n      <Waveform className=\"w-full\" />\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction AudioControlGroup({\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-1\", className)} {...props}>\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction AudioControlPlay({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"button\">) {\r\n  const { audioRef, graphRef, paused, setPaused } = React.useContext(Context);\r\n\r\n  const handlePlay = async () => {\r\n    if (!audioRef.current) return;\r\n    if (paused) {\r\n      await graphRef.current?.resume();\r\n      audioRef.current.play();\r\n    } else {\r\n      audioRef.current.pause();\r\n    }\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(\r\n        \"text-card-foreground rounded-full aspect-square h-8\",\r\n        className,\r\n      )}\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 AudioControlTimer({ 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(\r\n        \"text-card-foreground text-sm cursor-pointer mx-1.5\",\r\n        className,\r\n      )}\r\n      onClick={handleToggle}\r\n    >\r\n      {displayTime}\r\n      <span className=\"text-card-foreground/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 AudioControlPlaybackRate({\r\n  rates,\r\n  className,\r\n  ...props\r\n}: 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-card-foreground 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      <DropdownMenuContent\r\n        align=\"end\"\r\n        className=\"bg-card text-card-foreground border-border\"\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-accent focus:text-card-foreground\"\r\n          >\r\n            {rate}x{playbackRate === rate && <Check className=\"size-4 ml-2\" />}\r\n          </DropdownMenuItem>\r\n        ))}\r\n      </DropdownMenuContent>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\nfunction AudioControlLoop({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<\"button\">) {\r\n  const { audioRef, loop, setLoop } = React.useContext(Context);\r\n\r\n  const handleLoop = () => {\r\n    if (!audioRef.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(\r\n        \"text-card-foreground rounded-full aspect-square h-8\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      {loop ? <Repeat1 /> : <Repeat />}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction AudioControlVolume({ className, ...props }: 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-card-foreground 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      <DropdownMenuContent 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      </DropdownMenuContent>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\nfunction Waveform({\r\n  className,\r\n  playedClassName = \"bg-primary\",\r\n  unplayedClassName = \"bg-primary/20\",\r\n}: {\r\n  className?: string;\r\n  playedClassName?: string;\r\n  unplayedClassName?: string;\r\n}) {\r\n  const BAR_W = 3;\r\n  const BAR_GAP = 2;\r\n  const TRANSITION_SPEED = 0.08;\r\n\r\n  const { analyserRef, paused, progress } = React.useContext(Context);\r\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\r\n  const barsRef = React.useRef<(HTMLDivElement | null)[]>([]);\r\n  const playedStateRef = React.useRef<boolean[]>([]);\r\n  const rafRef = React.useRef<number>(0);\r\n  const blendRef = React.useRef<number>(paused ? 1 : 0);\r\n  const [barCount, setBarCount] = React.useState(0);\r\n\r\n  React.useEffect(() => {\r\n    const wrapper = wrapperRef.current;\r\n    if (!wrapper) return;\r\n    const ro = new ResizeObserver(() => {\r\n      setBarCount(Math.floor(wrapper.clientWidth / (BAR_W + BAR_GAP)));\r\n    });\r\n    ro.observe(wrapper);\r\n    return () => ro.disconnect();\r\n  }, []);\r\n\r\n  const staticHeights = React.useMemo(\r\n    () =>\r\n      Array.from({ length: barCount }, (_, i) => {\r\n        const t = i / barCount;\r\n        return (\r\n          0.15 +\r\n          0.35 * Math.abs(Math.sin(t * Math.PI * 6)) +\r\n          0.2 * Math.abs(Math.sin(t * Math.PI * 13.7 + 1))\r\n        );\r\n      }),\r\n    [barCount],\r\n  );\r\n\r\n  React.useEffect(() => {\r\n    if (barCount === 0) return;\r\n\r\n    const target = paused ? 1 : 0;\r\n\r\n    const animate = () => {\r\n      rafRef.current = requestAnimationFrame(animate);\r\n\r\n      blendRef.current += (target - blendRef.current) * TRANSITION_SPEED;\r\n      if (Math.abs(blendRef.current - target) < 0.001)\r\n        blendRef.current = target;\r\n      const blend = blendRef.current;\r\n\r\n      const analyser = analyserRef.current;\r\n      let freqData: Uint8Array<ArrayBuffer> | null = null;\r\n      if (analyser) {\r\n        freqData = new Uint8Array(\r\n          analyser.frequencyBinCount,\r\n        ) as Uint8Array<ArrayBuffer>;\r\n        analyser.getByteFrequencyData(freqData);\r\n      }\r\n      const step = freqData ? Math.floor(freqData.length / barCount) : 1;\r\n\r\n      for (let i = 0; i < barCount; i++) {\r\n        const bar = barsRef.current[i];\r\n        if (!bar) continue;\r\n\r\n        const t = i / barCount;\r\n        const liveValue = freqData ? freqData[i * step] / 255 : 0;\r\n        const value = liveValue * (1 - blend) + staticHeights[i] * blend;\r\n        bar.style.height = `${Math.max(10, value * 100)}%`;\r\n\r\n        const played = t * 100 < progress;\r\n        if (played !== playedStateRef.current[i]) {\r\n          playedStateRef.current[i] = played;\r\n          bar.className = cn(\r\n            \"shrink-0 rounded-full\",\r\n            played ? playedClassName : unplayedClassName,\r\n          );\r\n          bar.style.width = `${BAR_W}px`;\r\n        }\r\n      }\r\n    };\r\n\r\n    animate();\r\n    return () => cancelAnimationFrame(rafRef.current);\r\n  }, [\r\n    analyserRef,\r\n    paused,\r\n    progress,\r\n    barCount,\r\n    staticHeights,\r\n    playedClassName,\r\n    unplayedClassName,\r\n  ]);\r\n\r\n  return (\r\n    <div\r\n      ref={wrapperRef}\r\n      className={cn(\r\n        \"flex-1 h-8 flex items-center overflow-hidden cursor-pointer\",\r\n        className,\r\n      )}\r\n      style={{ gap: `${BAR_GAP}px` }}\r\n    >\r\n      {Array.from({ length: barCount }, (_, index) => (\r\n        <div\r\n          key={index}\r\n          ref={(element) => {\r\n            barsRef.current[index] = element;\r\n          }}\r\n          className={cn(\"shrink-0 rounded-full\", unplayedClassName)}\r\n          style={{ width: BAR_W, height: \"20%px\" }}\r\n        />\r\n      ))}\r\n    </div>\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\nexport {\r\n  AudioControlGroup,\r\n  AudioControlLoop,\r\n  AudioControlPlay,\r\n  AudioControlPlaybackRate,\r\n  AudioControlSeek,\r\n  AudioControlTimer,\r\n  AudioControlVolume,\r\n  AudioPlayer,\r\n  AudioProvider,\r\n};\r\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:block"
}