{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wave-cn",
  "title": "Wave Core",
  "description": "Core hook, player and shared defaults powering all waves-cn components. Built on wavesurfer.js.",
  "dependencies": [
    "wavesurfer.js"
  ],
  "files": [
    {
      "path": "src/registry/lib/wave-cn.tsx",
      "content": "\"use client\";\n\nimport {\n  useState,\n  useEffect,\n  useRef,\n  memo,\n  type ReactElement,\n  type RefObject,\n} from \"react\";\nimport WaveSurfer, {\n  type WaveSurferEvents,\n  type WaveSurferOptions,\n} from \"wavesurfer.js\";\n\ntype WavesurferEventHandler<T extends unknown[]> = (\n  wavesurfer: WaveSurfer,\n  ...args: T\n) => void;\n\ntype OnWavesurferEvents = {\n  [K in keyof WaveSurferEvents as `on${Capitalize<K>}`]?: WavesurferEventHandler<\n    WaveSurferEvents[K]\n  >;\n};\n\ntype PartialWavesurferOptions = Omit<WaveSurferOptions, \"container\">;\n\nexport type WavesurferProps = PartialWavesurferOptions &\n  OnWavesurferEvents & {\n    className?: string;\n  };\n\nexport const WAVESURFER_DEFAULTS = {\n  waveColor: \"var(--muted-foreground)\",\n  progressColor: \"var(--primary)\",\n  height: 64,\n  barWidth: 3,\n  barGap: 2,\n  barRadius: 2,\n  minPxPerSec: 1,\n  cursorWidth: 0,\n} as const satisfies Partial<WaveSurferOptions>;\n\nconst EVENT_PROP_RE = /^on([A-Z])/;\nconst isEventProp = (key: string) => EVENT_PROP_RE.test(key);\nconst getEventName = (key: string) =>\n  key.replace(EVENT_PROP_RE, (_, $1) =>\n    $1.toLowerCase(),\n  ) as keyof WaveSurferEvents;\n\n// ─── Component ───────────────────────────────────────────────────────────────\nconst WavesurferPlayer = memo(\n  (props: WavesurferProps): ReactElement => {\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const wsRef = useRef<WaveSurfer | null>(null);\n    const { className, ...rest } = props;\n\n    // ── Separate options from event handlers\n    const options: Partial<WaveSurferOptions> = {};\n    const eventProps: OnWavesurferEvents = {};\n    for (const key in rest) {\n      if (isEventProp(key))\n        eventProps[key as keyof OnWavesurferEvents] = rest[\n          key as keyof typeof rest\n        ] as never;\n      else\n        options[key as keyof PartialWavesurferOptions] = rest[\n          key as keyof typeof rest\n        ] as never;\n    }\n\n    // ── Resolve CSS vars\n    const waveColor =\n      (options.waveColor as string | undefined) ??\n      WAVESURFER_DEFAULTS.waveColor;\n    const progressColor =\n      (options.progressColor as string | undefined) ??\n      WAVESURFER_DEFAULTS.progressColor;\n    const resolvedWaveColor = useCssVar(waveColor);\n    const resolvedProgressColor = useCssVar(progressColor);\n\n    // ── Keep event handlers in a ref — changes never cause re-subscription\n    const eventsRef = useRef(eventProps);\n    eventsRef.current = eventProps;\n\n    // ── Keep non-url options in a ref — changes applied imperatively\n    const optionsRef = useRef(options);\n    optionsRef.current = options;\n\n    // ── Create instance only when url or structural options change\n    const url = options.url as string | undefined;\n    const height =\n      (options.height as number | undefined) ?? WAVESURFER_DEFAULTS.height;\n    const barWidth =\n      (options.barWidth as number | undefined) ?? WAVESURFER_DEFAULTS.barWidth;\n    const barGap =\n      (options.barGap as number | undefined) ?? WAVESURFER_DEFAULTS.barGap;\n    const barRadius =\n      (options.barRadius as number | undefined) ??\n      WAVESURFER_DEFAULTS.barRadius;\n    const minPxPerSec =\n      (options.minPxPerSec as number | undefined) ??\n      WAVESURFER_DEFAULTS.minPxPerSec;\n    const cursorWidth =\n      (options.cursorWidth as number | undefined) ??\n      WAVESURFER_DEFAULTS.cursorWidth;\n    const dragToSeek = options.dragToSeek as boolean | undefined;\n    const media = options.media as HTMLMediaElement | undefined;\n\n    useEffect(() => {\n      if (!containerRef.current) return;\n\n      const ws = WaveSurfer.create({\n        ...WAVESURFER_DEFAULTS,\n        url,\n        height,\n        barWidth,\n        barGap,\n        barRadius,\n        minPxPerSec,\n        cursorWidth,\n        dragToSeek,\n        media,\n        plugins: optionsRef.current.plugins,\n        waveColor: resolvedWaveColor,\n        progressColor: resolvedProgressColor,\n        container: containerRef.current,\n      });\n\n      wsRef.current = ws;\n\n      // Subscribe to all events via ref — always calls latest handler\n      const eventEntries = Object.keys(eventsRef.current);\n      const unsubs = eventEntries.map((name) => {\n        const event = getEventName(name);\n        return ws.on(event, (...args) =>\n          (\n            eventsRef.current[\n              name as keyof OnWavesurferEvents\n            ] as WavesurferEventHandler<WaveSurferEvents[typeof event]>\n          )?.(ws, ...args),\n        );\n      });\n\n      return () => {\n        unsubs.forEach((fn) => fn());\n        ws.destroy();\n        wsRef.current = null;\n      };\n      // Only remount when these primitive options change — NOT handlers, NOT colors\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [\n      url,\n      height,\n      barWidth,\n      barGap,\n      barRadius,\n      minPxPerSec,\n      cursorWidth,\n      dragToSeek,\n    ]);\n\n    // ── Apply color changes imperatively — zero remount on theme switch\n    useEffect(() => {\n      wsRef.current?.setOptions({\n        waveColor: resolvedWaveColor,\n        progressColor: resolvedProgressColor,\n      });\n    }, [resolvedWaveColor, resolvedProgressColor]);\n\n    // ── Skeleton\n    const [isReady, setIsReady] = useState(false);\n    useEffect(() => {\n      const ws = wsRef.current;\n      if (!ws) return;\n\n      // Sync immediately with current instance — avoids skeleton flash on re-render\n      // when the instance already exists and audio is already decoded\n      setIsReady(ws.getDuration() > 0);\n\n      const unsubs = [\n        ws.on(\"ready\", () => setIsReady(true)),\n        ws.on(\"load\", () => setIsReady(false)),\n        ws.on(\"destroy\", () => setIsReady(false)),\n      ];\n      return () => unsubs.forEach((fn) => fn());\n      // Re-attach when instance changes (url change creates new instance)\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [wsRef.current]);\n\n    return (\n      <div className={className} style={{ position: \"relative\" }}>\n        {!isReady && (\n          <div\n            style={{\n              height,\n              width: \"100%\",\n              position: \"absolute\",\n              inset: 0,\n              borderRadius: 4,\n              background: \"hsl(var(--muted))\",\n              animation: \"pulse 2s cubic-bezier(0.4,0,0.6,1) infinite\",\n            }}\n          />\n        )}\n        <div ref={containerRef} style={!isReady ? { opacity: 0 } : undefined} />\n      </div>\n    );\n  },\n  (prev, next) => {\n    // Only remount when structural audio options change — ignore handlers and className\n    const STRUCTURAL_KEYS = [\n      \"url\",\n      \"height\",\n      \"barWidth\",\n      \"barGap\",\n      \"barRadius\",\n      \"minPxPerSec\",\n      \"cursorWidth\",\n      \"dragToSeek\",\n      \"waveColor\",\n      \"progressColor\",\n    ];\n    return STRUCTURAL_KEYS.every(\n      (k) =>\n        prev[k as keyof WavesurferProps] === next[k as keyof WavesurferProps],\n    );\n  },\n);\n\nexport default WavesurferPlayer;\n\n// ─── Hook ────────────────────────────────────────────────────────────────────\nexport function useWavesurfer({\n  container,\n  waveColor = WAVESURFER_DEFAULTS.waveColor,\n  progressColor = WAVESURFER_DEFAULTS.progressColor,\n  ...options\n}: Omit<WaveSurferOptions, \"container\"> & {\n  container: RefObject<HTMLDivElement | null>;\n}) {\n  const resolvedWaveColor = useCssVar(waveColor as string);\n  const resolvedProgressColor = useCssVar(progressColor as string);\n  const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);\n  const [isReady, setIsReady] = useState(false);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [currentTime, setCurrentTime] = useState(0);\n\n  const url = options.url as string | undefined;\n  const height =\n    (options.height as number | undefined) ?? WAVESURFER_DEFAULTS.height;\n  const barWidth =\n    (options.barWidth as number | undefined) ?? WAVESURFER_DEFAULTS.barWidth;\n  const barGap =\n    (options.barGap as number | undefined) ?? WAVESURFER_DEFAULTS.barGap;\n  const barRadius =\n    (options.barRadius as number | undefined) ?? WAVESURFER_DEFAULTS.barRadius;\n  const minPxPerSec =\n    (options.minPxPerSec as number | undefined) ??\n    WAVESURFER_DEFAULTS.minPxPerSec;\n\n  useEffect(() => {\n    if (!container.current) return;\n    const ws = WaveSurfer.create({\n      ...WAVESURFER_DEFAULTS,\n      ...options,\n      waveColor: resolvedWaveColor,\n      progressColor: resolvedProgressColor,\n      container: container.current,\n    });\n    setWavesurfer(ws);\n    const unsubs = [\n      ws.on(\"load\", () => {\n        setIsReady(false);\n        setIsPlaying(false);\n        setCurrentTime(0);\n      }),\n      ws.on(\"ready\", () => {\n        setIsReady(true);\n      }),\n      ws.on(\"play\", () => {\n        setIsPlaying(true);\n      }),\n      ws.on(\"pause\", () => {\n        setIsPlaying(false);\n      }),\n      ws.on(\"timeupdate\", () => {\n        setCurrentTime(ws.getCurrentTime());\n      }),\n      ws.on(\"destroy\", () => {\n        setIsReady(false);\n        setIsPlaying(false);\n      }),\n    ];\n    return () => {\n      unsubs.forEach((fn) => fn());\n      ws.destroy();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [url, height, barWidth, barGap, barRadius, minPxPerSec]);\n\n  useEffect(() => {\n    wavesurfer?.setOptions({\n      waveColor: resolvedWaveColor,\n      progressColor: resolvedProgressColor,\n    });\n  }, [wavesurfer, resolvedWaveColor, resolvedProgressColor]);\n\n  return { wavesurfer, isReady, isPlaying, currentTime };\n}\n\n// ─── CSS var resolver ────────────────────────────────────────────────────────\nexport function useCssVar(value: string): string {\n  const [resolved, setResolved] = useState(value);\n\n  useEffect(() => {\n    const match = value.match(/^var\\((--[^)]+)\\)$/);\n    if (!match) {\n      setResolved(value);\n      return;\n    }\n\n    const varName = match[1];\n    const resolve = () => {\n      const raw = getComputedStyle(document.documentElement)\n        .getPropertyValue(varName)\n        .trim();\n      const isHsl = /^[\\d.]+ [\\d.]+% [\\d.]+%$/.test(raw);\n      setResolved(raw ? (isHsl ? `hsl(${raw})` : raw) : value);\n    };\n\n    resolve();\n    const observer = new MutationObserver(resolve);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    });\n    return () => observer.disconnect();\n  }, [value]);\n\n  return resolved;\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:lib"
}