{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-player",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/watermelon-ui/audio-player.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { Play, Pause, SkipBack, SkipForward, Volume2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useAudioPlayback } from \"@/hooks/use-audio-playback\";\n\n// Utility function to format time\nexport const formatTime = (seconds: number): string => {\n  if (!isFinite(seconds) || isNaN(seconds)) {\n    return \"0:00\";\n  }\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n};\n\n// Progress Bar Component\ninterface ProgressBarProps {\n  currentTime: number;\n  duration: number;\n  isLoaded: boolean;\n  onSeek?: (time: number) => void;\n  className?: string;\n}\n\nconst ProgressBar: React.FC<ProgressBarProps> = ({\n  currentTime,\n  duration,\n  isLoaded,\n  onSeek,\n  className,\n}) => {\n  const getProgressPercentage = (): number => {\n    if (duration === 0 || !isFinite(duration) || isNaN(duration)) return 0;\n    if (!isFinite(currentTime) || isNaN(currentTime)) return 0;\n    return Math.min(100, Math.max(0, (currentTime / duration) * 100));\n  };\n\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>): void => {\n    if (!onSeek || !isLoaded || duration === 0) return;\n\n    const rect = e.currentTarget.getBoundingClientRect();\n    const clickX = e.clientX - rect.left;\n    const percentage = clickX / rect.width;\n    const newTime = percentage * duration;\n\n    onSeek(Math.max(0, Math.min(duration, newTime)));\n  };\n\n  return (\n    <div className={cn(\"flex-1\", className)}>\n      <div\n        className={cn(\n          \"group relative h-2 rounded-full bg-secondary\",\n          onSeek && \"cursor-pointer\"\n        )}\n        onClick={handleClick}\n      >\n        <div\n          className=\"h-2 rounded-full bg-primary transition-all duration-100\"\n          style={{ width: `${getProgressPercentage()}%` }}\n        />\n        {onSeek && (\n          <div\n            className=\"absolute top-1/2 -translate-y-1/2 h-4 w-4 rounded-full bg-primary opacity-0 group-hover:opacity-100 transition-opacity\"\n            style={{ left: `calc(${getProgressPercentage()}% - 8px)` }}\n          />\n        )}\n      </div>\n      <div className=\"mt-1 flex justify-between text-xs text-muted-foreground\">\n        <span>{formatTime(currentTime)}</span>\n        <span>{isLoaded ? formatTime(duration) : \"Loading...\"}</span>\n      </div>\n    </div>\n  );\n};\n\n// Controls Component\ninterface ControlsProps {\n  isPlaying: boolean;\n  onPlayPause: () => void;\n  onSkipBackward?: () => void;\n  onSkipForward?: () => void;\n  disabled?: boolean;\n  size?: \"sm\" | \"md\" | \"lg\";\n  className?: string;\n}\n\nconst Controls: React.FC<ControlsProps> = ({\n  isPlaying,\n  onPlayPause,\n  onSkipBackward,\n  onSkipForward,\n  disabled = false,\n  size = \"md\",\n  className,\n}) => {\n  const sizeClasses = {\n    sm: \"h-8 w-8\",\n    md: \"h-10 w-10\",\n    lg: \"h-12 w-12\",\n  };\n\n  const iconSizes = {\n    sm: 16,\n    md: 20,\n    lg: 24,\n  };\n\n  return (\n    <div className={cn(\"flex items-center gap-2\", className)}>\n      {onSkipBackward && (\n        <button\n          onClick={onSkipBackward}\n          disabled={disabled}\n          className={cn(\n            \"flex items-center justify-center rounded-full transition-colors hover:bg-secondary disabled:opacity-50\",\n            sizeClasses[size]\n          )}\n        >\n          <SkipBack size={iconSizes[size]} />\n        </button>\n      )}\n\n      <button\n        onClick={onPlayPause}\n        disabled={disabled}\n        className={cn(\n          \"flex items-center justify-center rounded-full bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50\",\n          sizeClasses[size]\n        )}\n      >\n        {isPlaying ? (\n          <Pause size={iconSizes[size]} />\n        ) : (\n          <Play size={iconSizes[size]} />\n        )}\n      </button>\n\n      {onSkipForward && (\n        <button\n          onClick={onSkipForward}\n          disabled={disabled}\n          className={cn(\n            \"flex items-center justify-center rounded-full transition-colors hover:bg-secondary disabled:opacity-50\",\n            sizeClasses[size]\n          )}\n        >\n          <SkipForward size={iconSizes[size]} />\n        </button>\n      )}\n    </div>\n  );\n};\n\n// Main Audio Player Component\nexport interface AudioPlayerProps {\n  source: File | Blob | string;\n  title?: string;\n  artist?: string;\n  coverImage?: string;\n  showTitle?: boolean;\n  showProgress?: boolean;\n  showControls?: boolean;\n  showVolume?: boolean;\n  allowSeek?: boolean;\n  autoPlay?: boolean;\n  loop?: boolean;\n  onPlay?: () => void;\n  onPause?: () => void;\n  onEnd?: () => void;\n  onError?: (error: Error) => void;\n  className?: string;\n  variant?: \"default\" | \"minimal\" | \"compact\";\n}\n\nexport const AudioPlayer: React.FC<AudioPlayerProps> = ({\n  source,\n  title,\n  artist,\n  coverImage,\n  showTitle = true,\n  showProgress = true,\n  showControls = true,\n  showVolume = false,\n  allowSeek = true,\n  autoPlay = false,\n  loop = false,\n  onPlay,\n  onPause,\n  onEnd,\n  onError,\n  className,\n  variant = \"default\",\n}) => {\n  const {\n    isPlaying,\n    audioUrl,\n    playbackTime,\n    duration,\n    audioLoaded,\n    audioRef,\n    togglePlayback,\n    seek,\n  } = useAudioPlayback(source, { onPlay, onPause, onEnd, onError });\n\n  // Apply autoPlay and loop\n  React.useEffect(() => {\n    if (audioRef.current) {\n      audioRef.current.loop = loop;\n      if (autoPlay && audioLoaded) {\n        togglePlayback();\n      }\n    }\n  }, [audioLoaded, autoPlay, loop, audioRef, togglePlayback]);\n\n  const handleSeek = allowSeek ? seek : undefined;\n\n  if (variant === \"minimal\") {\n    return (\n      <div className={cn(\"flex items-center gap-3\", className)}>\n        {showControls && (\n          <Controls\n            isPlaying={isPlaying}\n            onPlayPause={togglePlayback}\n            disabled={!audioLoaded}\n            size=\"sm\"\n          />\n        )}\n        {showProgress && (\n          <ProgressBar\n            currentTime={playbackTime}\n            duration={duration}\n            isLoaded={audioLoaded}\n            onSeek={handleSeek}\n          />\n        )}\n        <audio ref={audioRef} src={audioUrl} preload=\"metadata\" />\n      </div>\n    );\n  }\n\n  if (variant === \"compact\") {\n    return (\n      <div className={cn(\"rounded-lg border bg-card p-4\", className)}>\n        <div className=\"flex items-center gap-3\">\n          {coverImage && (\n            <img\n              src={coverImage}\n              alt={title || \"Audio\"}\n              className=\"h-12 w-12 rounded object-cover\"\n            />\n          )}\n          <div className=\"flex-1 min-w-0\">\n            {showTitle && (title || artist) && (\n              <div className=\"mb-2\">\n                {title && (\n                  <p className=\"font-medium truncate text-sm\">{title}</p>\n                )}\n                {artist && (\n                  <p className=\"text-xs text-muted-foreground truncate\">\n                    {artist}\n                  </p>\n                )}\n              </div>\n            )}\n            <div className=\"flex items-center gap-3\">\n              {showControls && (\n                <Controls\n                  isPlaying={isPlaying}\n                  onPlayPause={togglePlayback}\n                  disabled={!audioLoaded}\n                  size=\"sm\"\n                />\n              )}\n              {showProgress && (\n                <ProgressBar\n                  currentTime={playbackTime}\n                  duration={duration}\n                  isLoaded={audioLoaded}\n                  onSeek={handleSeek}\n                />\n              )}\n            </div>\n          </div>\n        </div>\n        <audio ref={audioRef} src={audioUrl} preload=\"metadata\" />\n      </div>\n    );\n  }\n\n  // Default variant\n  return (\n    <div className={cn(\"rounded-lg border bg-card p-6\", className)}>\n      <div className=\"space-y-4\">\n        {coverImage && (\n          <div className=\"flex justify-center\">\n            <img\n              src={coverImage}\n              alt={title || \"Audio\"}\n              className=\"h-48 w-48 rounded-lg object-cover shadow-lg\"\n            />\n          </div>\n        )}\n\n        {showTitle && (title || artist) && (\n          <div className=\"text-center\">\n            {title && <h3 className=\"font-semibold text-lg\">{title}</h3>}\n            {artist && (\n              <p className=\"text-sm text-muted-foreground\">{artist}</p>\n            )}\n          </div>\n        )}\n\n        {showProgress && (\n          <ProgressBar\n            currentTime={playbackTime}\n            duration={duration}\n            isLoaded={audioLoaded}\n            onSeek={handleSeek}\n          />\n        )}\n\n        <div className=\"flex items-center justify-center gap-4\">\n          {showControls && (\n            <Controls\n              isPlaying={isPlaying}\n              onPlayPause={togglePlayback}\n              disabled={!audioLoaded}\n              size=\"lg\"\n            />\n          )}\n          {showVolume && (\n            <div className=\"flex items-center gap-2\">\n              <Volume2 size={20} className=\"text-muted-foreground\" />\n            </div>\n          )}\n        </div>\n      </div>\n\n      <audio ref={audioRef} src={audioUrl} preload=\"metadata\" />\n    </div>\n  );\n};",
      "type": "registry:ui",
      "target": "components/ui/audio-player.tsx"
    },
    {
      "path": "src/hooks/use-audio-playback.ts",
      "content": "import { useState, useRef, useEffect, useCallback } from \"react\";\n\nexport interface UseAudioPlaybackOptions {\n  onPlay?: () => void;\n  onPause?: () => void;\n  onEnd?: () => void;\n  onError?: (error: Error) => void;\n}\n\nexport interface UseAudioPlaybackReturn {\n  isPlaying: boolean;\n  audioUrl: string;\n  playbackTime: number;\n  duration: number;\n  audioLoaded: boolean;\n  audioRef: React.RefObject<HTMLAudioElement | null>;\n  togglePlayback: () => Promise<void>;\n  stop: () => void;\n  seek: (time: number) => void;\n  setPlaybackRate: (rate: number) => void;\n  resetAudio: () => void;\n}\n\nexport const useAudioPlayback = (\n  source: File | Blob | string | null,\n  options?: UseAudioPlaybackOptions\n): UseAudioPlaybackReturn => {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [audioUrl, setAudioUrl] = useState<string>(\"\");\n  const [playbackTime, setPlaybackTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const [audioLoaded, setAudioLoaded] = useState(false);\n\n  const audioRef = useRef<HTMLAudioElement>(null);\n  const animationRef = useRef<number | null>(null);\n  const objectUrlRef = useRef<string | null>(null);\n  const optionsRef = useRef(options);\n  optionsRef.current = options;\n\n  // Update audio URL when source changes\n  useEffect(() => {\n    // Clean up previous object URL\n    if (objectUrlRef.current) {\n      URL.revokeObjectURL(objectUrlRef.current);\n      objectUrlRef.current = null;\n    }\n\n    setAudioLoaded(false);\n    setDuration(0);\n    setPlaybackTime(0);\n    setIsPlaying(false);\n\n    if (source instanceof File || source instanceof Blob) {\n      const url = URL.createObjectURL(source);\n      objectUrlRef.current = url;\n      setAudioUrl(url);\n    } else if (typeof source === \"string\") {\n      setAudioUrl(source);\n    } else {\n      setAudioUrl(\"\");\n    }\n  }, [source]);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current);\n      }\n      if (objectUrlRef.current) {\n        URL.revokeObjectURL(objectUrlRef.current);\n      }\n    };\n  }, []);\n\n  const updatePlaybackTime = useCallback(function updateTime() {\n    if (audioRef.current && isPlaying && !audioRef.current.paused) {\n      setPlaybackTime(audioRef.current.currentTime);\n      animationRef.current = requestAnimationFrame(updateTime);\n    }\n  }, [isPlaying]);\n\n  const togglePlayback = useCallback(async () => {\n    if (!audioRef.current || !audioLoaded) return;\n\n    try {\n      if (isPlaying) {\n        audioRef.current.pause();\n        setIsPlaying(false);\n        if (animationRef.current) {\n          cancelAnimationFrame(animationRef.current);\n          animationRef.current = null;\n        }\n        optionsRef.current?.onPause?.();\n      } else {\n        await audioRef.current.play();\n        setIsPlaying(true);\n        updatePlaybackTime();\n        optionsRef.current?.onPlay?.();\n      }\n    } catch (error) {\n      console.error(\"Error playing audio:\", error);\n      setIsPlaying(false);\n      optionsRef.current?.onError?.(error as Error);\n    }\n  }, [isPlaying, audioLoaded, updatePlaybackTime]);\n\n  const stop = () => {\n    if (!audioRef.current) return;\n\n    audioRef.current.pause();\n    audioRef.current.currentTime = 0;\n    setIsPlaying(false);\n    setPlaybackTime(0);\n\n    if (animationRef.current) {\n      cancelAnimationFrame(animationRef.current);\n      animationRef.current = null;\n    }\n  };\n\n  const seek = (time: number) => {\n    if (!audioRef.current) return;\n    audioRef.current.currentTime = Math.max(0, Math.min(time, duration));\n    setPlaybackTime(audioRef.current.currentTime);\n  };\n\n  const setPlaybackRate = (rate: number) => {\n    if (!audioRef.current) return;\n    audioRef.current.playbackRate = rate;\n  };\n\n  const resetAudio = () => {\n    stop();\n\n    if (objectUrlRef.current) {\n      URL.revokeObjectURL(objectUrlRef.current);\n      objectUrlRef.current = null;\n    }\n\n    setDuration(0);\n    setAudioLoaded(false);\n    setAudioUrl(\"\");\n  };\n\n  // Audio event handlers\n  const handleLoadedMetadata = useCallback(() => {\n    if (\n      audioRef.current &&\n      !isNaN(audioRef.current.duration) &&\n      isFinite(audioRef.current.duration)\n    ) {\n      setDuration(audioRef.current.duration);\n      setAudioLoaded(true);\n    }\n  }, []);\n\n  const handleCanPlay = useCallback(() => {\n    if (\n      audioRef.current &&\n      !isNaN(audioRef.current.duration) &&\n      isFinite(audioRef.current.duration)\n    ) {\n      setDuration(audioRef.current.duration);\n      setAudioLoaded(true);\n    }\n  }, []);\n\n  const handleTimeUpdate = useCallback(() => {\n    if (audioRef.current && isPlaying) {\n      setPlaybackTime(audioRef.current.currentTime);\n    }\n  }, [isPlaying]);\n\n  const handleEnded = useCallback(() => {\n    setIsPlaying(false);\n    setPlaybackTime(0);\n    if (animationRef.current) {\n      cancelAnimationFrame(animationRef.current);\n      animationRef.current = null;\n    }\n    if (audioRef.current) {\n      audioRef.current.currentTime = 0;\n    }\n    optionsRef.current?.onEnd?.();\n  }, []);\n\n  const handleError = useCallback((e: React.SyntheticEvent<HTMLAudioElement>) => {\n    console.error(\"Audio error:\", e);\n    setAudioLoaded(false);\n    setIsPlaying(false);\n    optionsRef.current?.onError?.(new Error(\"Error loading audio file\"));\n  }, []);\n\n  // Attach event handlers\n  useEffect(() => {\n    const audio = audioRef.current;\n    if (!audio) return;\n\n    audio.addEventListener(\"loadedmetadata\", handleLoadedMetadata);\n    audio.addEventListener(\"canplay\", handleCanPlay);\n    audio.addEventListener(\"timeupdate\", handleTimeUpdate);\n    audio.addEventListener(\"ended\", handleEnded);\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    audio.addEventListener(\"error\", handleError as any);\n\n    return () => {\n      audio.removeEventListener(\"loadedmetadata\", handleLoadedMetadata);\n      audio.removeEventListener(\"canplay\", handleCanPlay);\n      audio.removeEventListener(\"timeupdate\", handleTimeUpdate);\n      audio.removeEventListener(\"ended\", handleEnded);\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      audio.removeEventListener(\"error\", handleError as any);\n    };\n  }, [handleTimeUpdate, handleEnded, handleError, handleLoadedMetadata, handleCanPlay]);\n\n  return {\n    isPlaying,\n    audioUrl,\n    playbackTime,\n    duration,\n    audioLoaded,\n    audioRef,\n    togglePlayback,\n    stop,\n    seek,\n    setPlaybackRate,\n    resetAudio,\n  };\n};",
      "type": "registry:hook",
      "target": "hooks/use-audio-playback.ts"
    }
  ],
  "type": "registry:ui"
}
