{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-sound",
  "title": "Sound Hook",
  "description": "Custom React hook to load and play a sound from a given URL using the Web Audio API.",
  "files": [
    {
      "path": "src/hooks/use-sound.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\n/**\n * Cache storage for loaded audio buffers to prevent duplicate network requests and memory usage.\n * Maps audio URL to its decoded AudioBuffer.\n */\nconst audioCache = new Map<\n  string,\n  {\n    buffer: AudioBuffer\n    loading: Promise<AudioBuffer>\n  } | null\n>()\n\n/**\n * Shared AudioContext instance to avoid creating multiple contexts.\n * Multiple AudioContexts can cause performance issues and resource exhaustion.\n */\nlet sharedAudioContext: AudioContext | null = null\n\n/**\n * Gets or creates a shared AudioContext instance.\n */\nfunction getAudioContext(): AudioContext | null {\n  if (sharedAudioContext) return sharedAudioContext\n\n  const AudioContextClass =\n    window.AudioContext ||\n    (window as unknown as { webkitAudioContext: typeof AudioContext })\n      .webkitAudioContext\n\n  if (!AudioContextClass) {\n    console.warn(\"Web Audio API is not supported in this browser.\")\n    return null\n  }\n\n  sharedAudioContext = new AudioContextClass()\n  return sharedAudioContext\n}\n\n/**\n * Custom React hook to load and play a sound from a given URL using the Web Audio API.\n *\n * This hook implements caching to prevent duplicate network requests and memory usage\n * when the same audio file is used across multiple components.\n *\n * @param url - The URL of the audio file to load and play.\n * @returns A function that, when called, plays the loaded sound.\n *\n * @remarks\n * - Audio buffers are cached globally, so the same file is only loaded once\n * - Uses a shared AudioContext to avoid resource exhaustion\n * - If the Web Audio API is not supported in the browser, a warning is logged and playback is disabled\n * - Errors during fetching or decoding the audio are logged to the console\n *\n * @example\n * ```tsx\n * const playClick = useSound('/sounds/click.mp3');\n * // Later in an event handler:\n * playClick();\n * ```\n */\nexport function useSound(url: string) {\n  const audioCtxRef = useRef<AudioContext | null>(null)\n  const bufferRef = useRef<AudioBuffer | null>(null)\n\n  useEffect(() => {\n    const audioCtx = getAudioContext()\n    if (!audioCtx) return\n\n    audioCtxRef.current = audioCtx\n\n    // Check if already cached\n    const cached = audioCache.get(url)\n    if (cached?.buffer) {\n      bufferRef.current = cached.buffer\n      return\n    }\n\n    // Check if already loading\n    if (cached?.loading) {\n      cached.loading\n        .then((decoded) => {\n          bufferRef.current = decoded\n        })\n        .catch(() => {\n          // Error already logged during fetch\n        })\n      return\n    }\n\n    // Start loading\n    const loadingPromise = fetch(url)\n      .then((res) => res.arrayBuffer())\n      .then((data) => audioCtx.decodeAudioData(data))\n      .then((decoded) => {\n        // Store in cache\n        audioCache.set(url, { buffer: decoded, loading: loadingPromise })\n        bufferRef.current = decoded\n        return decoded\n      })\n      .catch((err) => {\n        console.log(`Failed to load sound from ${url}:`, err)\n        // Mark as failed in cache\n        audioCache.set(url, null)\n        throw err\n      })\n\n    // Mark as loading in cache\n    audioCache.set(url, { buffer: null!, loading: loadingPromise })\n  }, [url])\n\n  const play = useCallback((volume: number = 1) => {\n    if (audioCtxRef.current && bufferRef.current) {\n      const source = audioCtxRef.current.createBufferSource()\n      const gainNode = audioCtxRef.current.createGain()\n\n      source.buffer = bufferRef.current\n      gainNode.gain.value = volume\n\n      source.connect(gainNode)\n      gainNode.connect(audioCtxRef.current.destination)\n      source.start(0)\n    }\n  }, [])\n\n  return play\n}\n\n/**\n * Custom React hook for lazy loading and playing sounds with manual preload control.\n *\n * Unlike `useSound()`, this hook does NOT load audio on mount. Audio is only fetched when:\n * - `preload()` is manually called (e.g., on hover)\n * - `play()` is called and audio is not yet loaded (auto-load fallback)\n *\n * This is ideal for audio that may not be needed by most users, saving initial bandwidth and memory.\n *\n * @param url - The URL of the audio file to load and play.\n * @returns Object with play function, preload function, and loading states.\n *\n * @remarks\n * - Audio buffers are cached globally and shared with `useSound()`\n * - Uses a shared AudioContext to avoid resource exhaustion\n * - If the Web Audio API is not supported, warnings are logged and playback is disabled\n * - Errors during fetching or decoding are logged to the console\n *\n * @example\n * ```tsx\n * const { play, preload, isLoading, isLoaded } = useSoundLazy('/sounds/rare.mp3');\n *\n * // Preload on hover for instant playback on click\n * <button\n *   onPointerEnter={() => preload()}\n *   onClick={() => play()}\n * >\n *   Play Sound\n * </button>\n * ```\n */\nexport function useSoundLazy(url: string) {\n  const audioCtxRef = useRef<AudioContext | null>(null)\n  const bufferRef = useRef<AudioBuffer | null>(null)\n  const loadingPromiseRef = useRef<Promise<AudioBuffer | void> | null>(null)\n  const [isLoading, setIsLoading] = useState(false)\n  const [isLoaded, setIsLoaded] = useState(() => {\n    // Check if already cached on initial render\n    const cached = audioCache.get(url)\n    return !!cached?.buffer\n  })\n\n  useEffect(() => {\n    // Initialize AudioContext reference\n    const audioCtx = getAudioContext()\n    if (audioCtx) {\n      audioCtxRef.current = audioCtx\n    }\n\n    // Check if already cached (e.g., loaded by another component)\n    const cached = audioCache.get(url)\n    if (cached?.buffer) {\n      bufferRef.current = cached.buffer\n    }\n  }, [url])\n\n  const load = useCallback(() => {\n    // Early return if already loaded\n    if (bufferRef.current) {\n      return Promise.resolve(bufferRef.current)\n    }\n\n    // Return existing loading promise if already loading\n    if (loadingPromiseRef.current) {\n      return loadingPromiseRef.current\n    }\n\n    const audioCtx = getAudioContext()\n    if (!audioCtx) {\n      return Promise.reject(new Error(\"Web Audio API not supported\"))\n    }\n\n    audioCtxRef.current = audioCtx\n\n    // Check cache\n    const cached = audioCache.get(url)\n    if (cached?.buffer) {\n      bufferRef.current = cached.buffer\n      setIsLoaded(true)\n      return Promise.resolve(cached.buffer)\n    }\n\n    // Check if already loading by another component\n    if (cached?.loading) {\n      setIsLoading(true)\n      const promise = cached.loading\n        .then((decoded) => {\n          bufferRef.current = decoded\n          setIsLoaded(true)\n          return decoded\n        })\n        .catch((err) => {\n          // Error already logged during fetch\n          throw err\n        })\n        .finally(() => {\n          setIsLoading(false)\n          loadingPromiseRef.current = null\n        })\n\n      loadingPromiseRef.current = promise\n      return promise\n    }\n\n    // Start new load\n    setIsLoading(true)\n    const loadingPromise = fetch(url)\n      .then((res) => res.arrayBuffer())\n      .then((data) => audioCtx.decodeAudioData(data))\n      .then((decoded) => {\n        audioCache.set(url, { buffer: decoded, loading: loadingPromise })\n        bufferRef.current = decoded\n        setIsLoaded(true)\n        return decoded\n      })\n      .catch((err) => {\n        console.log(`Failed to load sound from ${url}:`, err)\n        audioCache.set(url, null)\n        throw err\n      })\n      .finally(() => {\n        setIsLoading(false)\n        loadingPromiseRef.current = null\n      })\n\n    // Mark as loading in cache\n    audioCache.set(url, { buffer: null!, loading: loadingPromise })\n    loadingPromiseRef.current = loadingPromise\n    return loadingPromise\n  }, [url])\n\n  const preload = useCallback(() => {\n    load().catch(() => {\n      // Error already logged in load()\n    })\n  }, [load])\n\n  const play = useCallback(\n    (volume: number = 1) => {\n      const playSound = () => {\n        if (audioCtxRef.current && bufferRef.current) {\n          const source = audioCtxRef.current.createBufferSource()\n          const gainNode = audioCtxRef.current.createGain()\n\n          source.buffer = bufferRef.current\n          gainNode.gain.value = volume\n\n          source.connect(gainNode)\n          gainNode.connect(audioCtxRef.current.destination)\n          source.start(0)\n        }\n      }\n\n      // If already loaded, play immediately\n      if (bufferRef.current) {\n        playSound()\n        return\n      }\n\n      // Auto-load fallback: load then play\n      load()\n        .then(() => {\n          playSound()\n        })\n        .catch(() => {\n          // Error already logged in load()\n        })\n    },\n    [load]\n  )\n\n  return { play, preload, isLoading, isLoaded }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}