{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-player",
  "type": "registry:ui",
  "title": "Audio Player",
  "description": "Self-contained audio player UI over the native HTML5 element — play/pause, scrubbing, and volume.",
  "dependencies": [
    "@saasflare/ui"
  ],
  "files": [
    {
      "path": "components/ui/audio-player.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n/**\n * @fileoverview Styled audio player with play/pause, seek bar, and time display.\n * @author Saasflare™\n * Pure UI audio player wrapping the native HTML5 audio element.\n * @module packages/ui/components/ui/audio-player\n * @package ui\n *\n * @component\n * @example\n * import { AudioPlayer } from '@saasflare/ui';\n * <AudioPlayer src=\"/podcast-episode.mp3\" title=\"Episode 12\" />\n */\n\nimport { useRef, useState, useEffect, useCallback } from \"react\"\nimport { PlayIcon, PauseIcon } from \"@saasflare/ui\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\n\n/** Props for the AudioPlayer component. */\nexport interface AudioPlayerProps extends SaasflareComponentProps {\n  /** Audio source URL. */\n  src: string\n  /** Track title. */\n  title?: string\n  /** Additional class names. */\n  className?: string\n}\n\n/** Formats seconds as mm:ss. */\nfunction formatTime(s: number): string {\n  if (!isFinite(s) || s < 0) return \"0:00\"\n  const m = Math.floor(s / 60)\n  const sec = Math.floor(s % 60)\n  return `${m}:${String(sec).padStart(2, \"0\")}`\n}\n\n/**\n * Styled audio player with play/pause, seek, and time display.\n *\n * @component\n * @package ui\n */\nexport function AudioPlayer({\n  src,\n  title,\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: AudioPlayerProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const audioRef = useRef<HTMLAudioElement>(null)\n  const [playing, setPlaying] = useState(false)\n  const [current, setCurrent] = useState(0)\n  const [duration, setDuration] = useState(0)\n\n  const toggle = useCallback(() => {\n    const audio = audioRef.current\n    if (!audio) return\n    if (audio.paused) {\n      // play() returns a Promise that rejects on interrupted/blocked playback;\n      // swallow it so we never leave an unhandled rejection. State is driven by\n      // the element's play/pause events below, not optimistically here.\n      audio.play().catch(() => {})\n    } else {\n      audio.pause()\n    }\n  }, [])\n\n  useEffect(() => {\n    const audio = audioRef.current\n    if (!audio) return\n\n    const onTime = () => setCurrent(audio.currentTime)\n    const onMeta = () => setDuration(audio.duration)\n    const onPlay = () => setPlaying(true)\n    const onPause = () => setPlaying(false)\n\n    audio.addEventListener(\"timeupdate\", onTime)\n    audio.addEventListener(\"loadedmetadata\", onMeta)\n    audio.addEventListener(\"play\", onPlay)\n    audio.addEventListener(\"pause\", onPause)\n    return () => {\n      audio.removeEventListener(\"timeupdate\", onTime)\n      audio.removeEventListener(\"loadedmetadata\", onMeta)\n      audio.removeEventListener(\"play\", onPlay)\n      audio.removeEventListener(\"pause\", onPause)\n    }\n  }, [])\n\n  const seek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n    const audio = audioRef.current\n    if (!audio) return\n    audio.currentTime = Number(e.target.value)\n  }, [])\n\n  const progress = duration > 0 ? (current / duration) * 100 : 0\n\n  return (\n    <div\n      data-slot=\"audio-player\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\n        \"flex items-center gap-3 rounded-xl border surface-card px-4 py-3\",\n        \"transition-all duration-200 hover:shadow-md\",\n        className,\n      )}\n    >\n      <audio ref={audioRef} src={src} preload=\"metadata\" />\n\n      <button\n        type=\"button\"\n        onClick={toggle}\n        className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-colors hover:bg-primary/90\"\n        aria-label={playing ? \"Pause\" : \"Play\"}\n      >\n        {playing ? (\n          <PauseIcon weight={sf.iconWeight} className=\"size-4\" />\n        ) : (\n          <PlayIcon weight={sf.iconWeight} className=\"ml-0.5 size-4\" />\n        )}\n      </button>\n\n      <div className=\"flex flex-1 flex-col gap-1\">\n        {title && <span className=\"text-xs font-medium truncate\">{title}</span>}\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-[10px] tabular-nums text-muted-foreground\">{formatTime(current)}</span>\n          <div className=\"relative flex-1\">\n            <div className=\"h-1 rounded-full bg-muted\">\n              <div\n                className=\"h-1 rounded-full bg-primary transition-[width]\"\n                style={{ width: `${progress}%` }}\n              />\n            </div>\n            <input\n              type=\"range\"\n              min={0}\n              max={duration || 0}\n              value={Math.min(current, duration || 0)}\n              onChange={seek}\n              className=\"absolute inset-0 h-1 w-full cursor-pointer opacity-0\"\n              aria-label=\"Seek\"\n            />\n          </div>\n          <span className=\"text-[10px] tabular-nums text-muted-foreground\">{formatTime(duration)}</span>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "target": "components/ui/audio-player.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/audio-player.json"
  }
}
