{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-player",
  "dependencies": [
    "@radix-ui/react-slider",
    "@radix-ui/react-dropdown-menu"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu"
  ],
  "files": [
    {
      "path": "components/ui/audio-player.tsx",
      "content": "\"use client\"\n\nimport {\n  ComponentProps,\n  createContext,\n  HTMLProps,\n  ReactNode,\n  RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\nimport { Check, PauseIcon, PlayIcon, Settings } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\n\nenum ReadyState {\n  HAVE_NOTHING = 0,\n  HAVE_METADATA = 1,\n  HAVE_CURRENT_DATA = 2,\n  HAVE_FUTURE_DATA = 3,\n  HAVE_ENOUGH_DATA = 4,\n}\n\nenum NetworkState {\n  NETWORK_EMPTY = 0,\n  NETWORK_IDLE = 1,\n  NETWORK_LOADING = 2,\n  NETWORK_NO_SOURCE = 3,\n}\n\nfunction formatTime(seconds: number) {\n  const hrs = Math.floor(seconds / 3600)\n  const mins = Math.floor((seconds % 3600) / 60)\n  const secs = Math.floor(seconds % 60)\n\n  const formattedMins = mins < 10 ? `0${mins}` : mins\n  const formattedSecs = secs < 10 ? `0${secs}` : secs\n\n  return hrs > 0\n    ? `${hrs}:${formattedMins}:${formattedSecs}`\n    : `${mins}:${formattedSecs}`\n}\n\ninterface AudioPlayerItem<TData = unknown> {\n  id: string | number\n  src: string\n  data?: TData\n}\n\ninterface AudioPlayerApi<TData = unknown> {\n  ref: RefObject<HTMLAudioElement | null>\n  activeItem: AudioPlayerItem<TData> | null\n  duration: number | undefined\n  error: MediaError | null\n  isPlaying: boolean\n  isBuffering: boolean\n  playbackRate: number\n  isItemActive: (id: string | number | null) => boolean\n  setActiveItem: (item: AudioPlayerItem<TData> | null) => Promise<void>\n  play: (item?: AudioPlayerItem<TData> | null) => Promise<void>\n  pause: () => void\n  seek: (time: number) => void\n  setPlaybackRate: (rate: number) => void\n}\n\nconst AudioPlayerContext = createContext<AudioPlayerApi<unknown> | null>(null)\n\nexport function useAudioPlayer<TData = unknown>(): AudioPlayerApi<TData> {\n  const api = useContext(AudioPlayerContext) as AudioPlayerApi<TData> | null\n  if (!api) {\n    throw new Error(\n      \"useAudioPlayer cannot be called outside of AudioPlayerProvider\"\n    )\n  }\n  return api\n}\n\nconst AudioPlayerTimeContext = createContext<number | null>(null)\n\nexport const useAudioPlayerTime = () => {\n  const time = useContext(AudioPlayerTimeContext)\n  if (time === null) {\n    throw new Error(\n      \"useAudioPlayerTime cannot be called outside of AudioPlayerProvider\"\n    )\n  }\n  return time\n}\n\nexport function AudioPlayerProvider<TData = unknown>({\n  children,\n}: {\n  children: ReactNode\n}) {\n  const audioRef = useRef<HTMLAudioElement>(null)\n  const itemRef = useRef<AudioPlayerItem<TData> | null>(null)\n  const playPromiseRef = useRef<Promise<void> | null>(null)\n  const [readyState, setReadyState] = useState<number>(0)\n  const [networkState, setNetworkState] = useState<number>(0)\n  const [time, setTime] = useState<number>(0)\n  const [duration, setDuration] = useState<number | undefined>(undefined)\n  const [error, setError] = useState<MediaError | null>(null)\n  const [activeItem, _setActiveItem] = useState<AudioPlayerItem<TData> | null>(\n    null\n  )\n  const [paused, setPaused] = useState(true)\n  const [playbackRate, setPlaybackRateState] = useState<number>(1)\n\n  const setActiveItem = useCallback(\n    async (item: AudioPlayerItem<TData> | null) => {\n      if (!audioRef.current) return\n\n      if (item?.id === itemRef.current?.id) {\n        return\n      }\n      itemRef.current = item\n      const currentRate = audioRef.current.playbackRate\n      audioRef.current.pause()\n      audioRef.current.currentTime = 0\n      if (item === null) {\n        audioRef.current.removeAttribute(\"src\")\n      } else {\n        audioRef.current.src = item.src\n      }\n      audioRef.current.load()\n      audioRef.current.playbackRate = currentRate\n    },\n    []\n  )\n\n  const play = useCallback(\n    async (item?: AudioPlayerItem<TData> | null) => {\n      if (!audioRef.current) return\n\n      if (playPromiseRef.current) {\n        try {\n          await playPromiseRef.current\n        } catch (error) {\n          console.error(\"Play promise error:\", error)\n        }\n      }\n\n      if (item === undefined) {\n        const playPromise = audioRef.current.play()\n        playPromiseRef.current = playPromise\n        return playPromise\n      }\n      if (item?.id === activeItem?.id) {\n        const playPromise = audioRef.current.play()\n        playPromiseRef.current = playPromise\n        return playPromise\n      }\n\n      itemRef.current = item\n      const currentRate = audioRef.current.playbackRate\n      if (!audioRef.current.paused) {\n        audioRef.current.pause()\n      }\n      audioRef.current.currentTime = 0\n      if (item === null) {\n        audioRef.current.removeAttribute(\"src\")\n      } else {\n        audioRef.current.src = item.src\n      }\n      audioRef.current.load()\n      audioRef.current.playbackRate = currentRate\n      const playPromise = audioRef.current.play()\n      playPromiseRef.current = playPromise\n      return playPromise\n    },\n    [activeItem]\n  )\n\n  const pause = useCallback(async () => {\n    if (!audioRef.current) return\n\n    if (playPromiseRef.current) {\n      try {\n        await playPromiseRef.current\n      } catch (e) {\n        console.error(e)\n      }\n    }\n\n    audioRef.current.pause()\n    playPromiseRef.current = null\n  }, [])\n\n  const seek = useCallback((time: number) => {\n    if (!audioRef.current) return\n    audioRef.current.currentTime = time\n  }, [])\n\n  const setPlaybackRate = useCallback((rate: number) => {\n    if (!audioRef.current) return\n    audioRef.current.playbackRate = rate\n    setPlaybackRateState(rate)\n  }, [])\n\n  const isItemActive = useCallback(\n    (id: string | number | null) => {\n      return activeItem?.id === id\n    },\n    [activeItem]\n  )\n\n  useAnimationFrame(() => {\n    if (audioRef.current) {\n      _setActiveItem(itemRef.current)\n      setReadyState(audioRef.current.readyState)\n      setNetworkState(audioRef.current.networkState)\n      setTime(audioRef.current.currentTime)\n      setDuration(audioRef.current.duration)\n      setPaused(audioRef.current.paused)\n      setError(audioRef.current.error)\n      setPlaybackRateState(audioRef.current.playbackRate)\n    }\n  })\n\n  const isPlaying = !paused\n  const isBuffering =\n    readyState < ReadyState.HAVE_FUTURE_DATA &&\n    networkState === NetworkState.NETWORK_LOADING\n\n  const api = useMemo<AudioPlayerApi<TData>>(\n    () => ({\n      ref: audioRef,\n      duration,\n      error,\n      isPlaying,\n      isBuffering,\n      activeItem,\n      playbackRate,\n      isItemActive,\n      setActiveItem,\n      play,\n      pause,\n      seek,\n      setPlaybackRate,\n    }),\n    [\n      audioRef,\n      duration,\n      error,\n      isPlaying,\n      isBuffering,\n      activeItem,\n      playbackRate,\n      isItemActive,\n      setActiveItem,\n      play,\n      pause,\n      seek,\n      setPlaybackRate,\n    ]\n  )\n\n  return (\n    <AudioPlayerContext.Provider value={api as AudioPlayerApi<unknown>}>\n      <AudioPlayerTimeContext.Provider value={time}>\n        <audio ref={audioRef} className=\"hidden\" crossOrigin=\"anonymous\" />\n        {children}\n      </AudioPlayerTimeContext.Provider>\n    </AudioPlayerContext.Provider>\n  )\n}\n\nexport const AudioPlayerProgress = ({\n  ...otherProps\n}: Omit<\n  ComponentProps<typeof SliderPrimitive.Root>,\n  \"min\" | \"max\" | \"value\"\n>) => {\n  const player = useAudioPlayer()\n  const time = useAudioPlayerTime()\n  const wasPlayingRef = useRef(false)\n\n  return (\n    <SliderPrimitive.Root\n      {...otherProps}\n      value={[time]}\n      onValueChange={(vals) => {\n        player.seek(vals[0])\n        otherProps.onValueChange?.(vals)\n      }}\n      min={0}\n      max={player.duration ?? 0}\n      step={otherProps.step || 0.25}\n      onPointerDown={(e) => {\n        wasPlayingRef.current = player.isPlaying\n        player.pause()\n        otherProps.onPointerDown?.(e)\n      }}\n      onPointerUp={(e) => {\n        if (wasPlayingRef.current) {\n          player.play()\n        }\n        otherProps.onPointerUp?.(e)\n      }}\n      className={cn(\n        \"group/player relative flex h-4 touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n        otherProps.className\n      )}\n      onKeyDown={(e) => {\n        if (e.key === \" \") {\n          e.preventDefault()\n          if (!player.isPlaying) {\n            player.play()\n          } else {\n            player.pause()\n          }\n        }\n        otherProps.onKeyDown?.(e)\n      }}\n      disabled={\n        player.duration === undefined ||\n        !Number.isFinite(player.duration) ||\n        Number.isNaN(player.duration)\n      }\n    >\n      <SliderPrimitive.Track className=\"bg-muted relative h-[4px] w-full grow overflow-hidden rounded-full\">\n        <SliderPrimitive.Range className=\"bg-primary absolute h-full\" />\n      </SliderPrimitive.Track>\n      <SliderPrimitive.Thumb\n        className=\"relative flex h-0 w-0 items-center justify-center opacity-0 group-hover/player:opacity-100 focus-visible:opacity-100 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\"\n        data-slot=\"slider-thumb\"\n      >\n        <div className=\"bg-foreground absolute size-3 rounded-full\" />\n      </SliderPrimitive.Thumb>\n    </SliderPrimitive.Root>\n  )\n}\n\nexport const AudioPlayerTime = ({\n  className,\n  ...otherProps\n}: HTMLProps<HTMLSpanElement>) => {\n  const time = useAudioPlayerTime()\n  return (\n    <span\n      {...otherProps}\n      className={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n    >\n      {formatTime(time)}\n    </span>\n  )\n}\n\nexport const AudioPlayerDuration = ({\n  className,\n  ...otherProps\n}: HTMLProps<HTMLSpanElement>) => {\n  const player = useAudioPlayer()\n  return (\n    <span\n      {...otherProps}\n      className={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n    >\n      {player.duration !== null &&\n      player.duration !== undefined &&\n      !Number.isNaN(player.duration)\n        ? formatTime(player.duration)\n        : \"--:--\"}\n    </span>\n  )\n}\n\ninterface SpinnerProps {\n  className?: string\n}\n\nfunction Spinner({ className }: SpinnerProps) {\n  return (\n    <div\n      className={cn(\n        \"border-muted border-t-foreground size-3.5 animate-spin rounded-full border-2\",\n        className\n      )}\n      role=\"status\"\n      aria-label=\"Loading\"\n    >\n      <span className=\"sr-only\">Loading...</span>\n    </div>\n  )\n}\n\ninterface PlayButtonProps extends React.ComponentProps<typeof Button> {\n  playing: boolean\n  onPlayingChange: (playing: boolean) => void\n  loading?: boolean\n}\n\nconst PlayButton = ({\n  playing,\n  onPlayingChange,\n  className,\n  onClick,\n  loading,\n  ...otherProps\n}: PlayButtonProps) => {\n  return (\n    <Button\n      {...otherProps}\n      onClick={(e) => {\n        onPlayingChange(!playing)\n        onClick?.(e)\n      }}\n      className={cn(\"relative\", className)}\n      aria-label={playing ? \"Pause\" : \"Play\"}\n      type=\"button\"\n    >\n      {playing ? (\n        <PauseIcon\n          className={cn(\"size-4\", loading && \"opacity-0\")}\n          aria-hidden=\"true\"\n        />\n      ) : (\n        <PlayIcon\n          className={cn(\"size-4\", loading && \"opacity-0\")}\n          aria-hidden=\"true\"\n        />\n      )}\n      {loading && (\n        <div className=\"absolute inset-0 flex items-center justify-center rounded-[inherit] backdrop-blur-xs\">\n          <Spinner />\n        </div>\n      )}\n    </Button>\n  )\n}\n\nexport interface AudioPlayerButtonProps<TData = unknown>\n  extends React.ComponentProps<typeof Button> {\n  item?: AudioPlayerItem<TData>\n}\n\nexport function AudioPlayerButton<TData = unknown>({\n  item,\n  ...otherProps\n}: AudioPlayerButtonProps<TData>) {\n  const player = useAudioPlayer<TData>()\n\n  if (!item) {\n    return (\n      <PlayButton\n        {...otherProps}\n        playing={player.isPlaying}\n        onPlayingChange={(shouldPlay) => {\n          if (shouldPlay) {\n            player.play()\n          } else {\n            player.pause()\n          }\n        }}\n        loading={player.isBuffering && player.isPlaying}\n      />\n    )\n  }\n\n  return (\n    <PlayButton\n      {...otherProps}\n      playing={player.isItemActive(item.id) && player.isPlaying}\n      onPlayingChange={(shouldPlay) => {\n        if (shouldPlay) {\n          player.play(item)\n        } else {\n          player.pause()\n        }\n      }}\n      loading={\n        player.isItemActive(item.id) && player.isBuffering && player.isPlaying\n      }\n    />\n  )\n}\n\ntype Callback = (delta: number) => void\n\nfunction useAnimationFrame(callback: Callback) {\n  const requestRef = useRef<number | null>(null)\n  const previousTimeRef = useRef<number | null>(null)\n  const callbackRef = useRef<Callback>(callback)\n\n  useEffect(() => {\n    callbackRef.current = callback\n  }, [callback])\n\n  useEffect(() => {\n    const animate = (time: number) => {\n      if (previousTimeRef.current !== null) {\n        const delta = time - previousTimeRef.current\n        callbackRef.current(delta)\n      }\n      previousTimeRef.current = time\n      requestRef.current = requestAnimationFrame(animate)\n    }\n\n    requestRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (requestRef.current) cancelAnimationFrame(requestRef.current)\n      previousTimeRef.current = null\n    }\n  }, [])\n}\n\nconst PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] as const\n\nexport interface AudioPlayerSpeedProps\n  extends React.ComponentProps<typeof Button> {\n  speeds?: readonly number[]\n}\n\nexport function AudioPlayerSpeed({\n  speeds = PLAYBACK_SPEEDS,\n  className,\n  variant = \"ghost\",\n  size = \"icon\",\n  ...props\n}: AudioPlayerSpeedProps) {\n  const player = useAudioPlayer()\n  const currentSpeed = player.playbackRate\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant={variant}\n          size={size}\n          className={cn(className)}\n          aria-label=\"Playback speed\"\n          {...props}\n        >\n          <Settings className=\"size-4\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"min-w-[120px]\">\n        {speeds.map((speed) => (\n          <DropdownMenuItem\n            key={speed}\n            onClick={() => player.setPlaybackRate(speed)}\n            className=\"flex items-center justify-between\"\n          >\n            <span className={speed === 1 ? \"\" : \"font-mono\"}>\n              {speed === 1 ? \"Normal\" : `${speed}x`}\n            </span>\n            {currentSpeed === speed && <Check className=\"size-4\" />}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nexport interface AudioPlayerSpeedButtonGroupProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  speeds?: readonly number[]\n}\n\nexport function AudioPlayerSpeedButtonGroup({\n  speeds = [0.5, 1, 1.5, 2],\n  className,\n  ...props\n}: AudioPlayerSpeedButtonGroupProps) {\n  const player = useAudioPlayer()\n  const currentSpeed = player.playbackRate\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-1\", className)}\n      role=\"group\"\n      aria-label=\"Playback speed controls\"\n      {...props}\n    >\n      {speeds.map((speed) => (\n        <Button\n          key={speed}\n          variant={currentSpeed === speed ? \"default\" : \"outline\"}\n          size=\"sm\"\n          onClick={() => player.setPlaybackRate(speed)}\n          className=\"min-w-[50px] font-mono text-xs\"\n        >\n          {speed}x\n        </Button>\n      ))}\n    </div>\n  )\n}\n\nexport const exampleTracks = [\n  {\n    id: \"0\",\n    name: \"II - 00\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/00.mp3\",\n  },\n  {\n    id: \"1\",\n    name: \"II - 01\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/01.mp3\",\n  },\n  {\n    id: \"2\",\n    name: \"II - 02\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/02.mp3\",\n  },\n  {\n    id: \"3\",\n    name: \"II - 03\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/03.mp3\",\n  },\n  {\n    id: \"4\",\n    name: \"II - 04\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/04.mp3\",\n  },\n  {\n    id: \"5\",\n    name: \"II - 05\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/05.mp3\",\n  },\n  {\n    id: \"6\",\n    name: \"II - 06\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/06.mp3\",\n  },\n  {\n    id: \"7\",\n    name: \"II - 07\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/07.mp3\",\n  },\n  {\n    id: \"8\",\n    name: \"II - 08\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/08.mp3\",\n  },\n  {\n    id: \"9\",\n    name: \"II - 09\",\n    url: \"https://storage.googleapis.com/eleven-public-cdn/audio/ui-elevenlabs-io/09.mp3\",\n  },\n]\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}