{ "$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 {\n id: string | number\n src: string\n data?: TData\n}\n\ninterface AudioPlayerApi {\n ref: RefObject\n activeItem: AudioPlayerItem | 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 | null) => Promise\n play: (item?: AudioPlayerItem | null) => Promise\n pause: () => void\n seek: (time: number) => void\n setPlaybackRate: (rate: number) => void\n}\n\nconst AudioPlayerContext = createContext | null>(null)\n\nexport function useAudioPlayer(): AudioPlayerApi {\n const api = useContext(AudioPlayerContext) as AudioPlayerApi | 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(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({\n children,\n}: {\n children: ReactNode\n}) {\n const audioRef = useRef(null)\n const itemRef = useRef | null>(null)\n const playPromiseRef = useRef | null>(null)\n const [readyState, setReadyState] = useState(0)\n const [networkState, setNetworkState] = useState(0)\n const [time, setTime] = useState(0)\n const [duration, setDuration] = useState(undefined)\n const [error, setError] = useState(null)\n const [activeItem, _setActiveItem] = useState | null>(\n null\n )\n const [paused, setPaused] = useState(true)\n const [playbackRate, setPlaybackRateState] = useState(1)\n\n const setActiveItem = useCallback(\n async (item: AudioPlayerItem | 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 | 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>(\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 }>\n \n \n \n )\n}\n\nexport const AudioPlayerProgress = ({\n ...otherProps\n}: Omit<\n ComponentProps,\n \"min\" | \"max\" | \"value\"\n>) => {\n const player = useAudioPlayer()\n const time = useAudioPlayerTime()\n const wasPlayingRef = useRef(false)\n\n return (\n {\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 \n \n \n \n
\n \n \n )\n}\n\nexport const AudioPlayerTime = ({\n className,\n ...otherProps\n}: HTMLProps) => {\n const time = useAudioPlayerTime()\n return (\n \n {formatTime(time)}\n \n )\n}\n\nexport const AudioPlayerDuration = ({\n className,\n ...otherProps\n}: HTMLProps) => {\n const player = useAudioPlayer()\n return (\n \n {player.duration !== null &&\n player.duration !== undefined &&\n !Number.isNaN(player.duration)\n ? formatTime(player.duration)\n : \"--:--\"}\n \n )\n}\n\ninterface SpinnerProps {\n className?: string\n}\n\nfunction Spinner({ className }: SpinnerProps) {\n return (\n \n Loading...\n
\n )\n}\n\ninterface PlayButtonProps extends React.ComponentProps {\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 {\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 \n ) : (\n \n )}\n {loading && (\n
\n \n
\n )}\n \n )\n}\n\nexport interface AudioPlayerButtonProps\n extends React.ComponentProps {\n item?: AudioPlayerItem\n}\n\nexport function AudioPlayerButton({\n item,\n ...otherProps\n}: AudioPlayerButtonProps) {\n const player = useAudioPlayer()\n\n if (!item) {\n return (\n {\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 {\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(null)\n const previousTimeRef = useRef(null)\n const callbackRef = useRef(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 {\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 \n \n \n \n \n \n \n {speeds.map((speed) => (\n player.setPlaybackRate(speed)}\n className=\"flex items-center justify-between\"\n >\n \n {speed === 1 ? \"Normal\" : `${speed}x`}\n \n {currentSpeed === speed && }\n \n ))}\n \n \n )\n}\n\nexport interface AudioPlayerSpeedButtonGroupProps\n extends Omit, \"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 \n {speeds.map((speed) => (\n player.setPlaybackRate(speed)}\n className=\"min-w-[50px] font-mono text-xs\"\n >\n {speed}x\n \n ))}\n \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" }