{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "record-button",
  "type": "registry:ui",
  "title": "Record Button",
  "description": "A voice-note record button: the circle morphs into a stop square, a ring pulses with the level, then play back or discard.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/record-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useMotionValue, useTransform } from \"motion/react\";\n\ntype RecordButtonSize = \"sm\" | \"md\" | \"lg\";\ntype Phase = \"idle\" | \"requesting\" | \"recording\" | \"recorded\";\n\nexport interface RecordButtonResult {\n  /** The recorded audio. Always null in simulated mode. */\n  blob: Blob | null;\n  /** Length in seconds (one decimal). */\n  duration: number;\n  /** MIME type chosen by MediaRecorder, or null in simulated mode. */\n  mimeType: string | null;\n}\n\nexport interface RecordButtonProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"div\">,\n    \"children\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\" | \"onAnimationEnd\"\n  > {\n  /**\n   * \"simulated\" fakes the input level and needs no permission. \"microphone\" calls getUserMedia\n   * only when the user presses record, and stops every track on stop and unmount.\n   */\n  mode?: \"simulated\" | \"microphone\";\n  /** Recording stops automatically after this many seconds. */\n  maxDuration?: number;\n  /** Visible title; also names the record button. */\n  label?: string;\n  /** Record glyph, level ring and played waveform colour. */\n  accent?: string;\n  size?: RecordButtonSize;\n  /** The ring around the button follows the input level while recording. */\n  pulse?: boolean;\n  /** Input level gain (microphone mode and the simulated meter). */\n  sensitivity?: number;\n  onRecordingStart?: () => void;\n  /** Called when a recording stops (by the user or at maxDuration), with the Blob in microphone mode. */\n  onRecorded?: (result: RecordButtonResult) => void;\n  /** Called when the user keeps the recording with the Done button. */\n  onDone?: (result: RecordButtonResult) => void;\n  /** Called when the user discards the recording. */\n  onDiscard?: () => void;\n}\n\nconst SIZES: Record<RecordButtonSize, { button: number; text: string }> = {\n  sm: { button: 44, text: \"text-[13px]\" },\n  md: { button: 56, text: \"text-sm\" },\n  lg: { button: 68, text: \"text-[15px]\" },\n};\n\nconst BARS = 36;\nconst SAMPLE_MS = 80;\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/** m:ss */\nfunction formatTime(seconds: number) {\n  const s = Math.max(0, Math.floor(seconds));\n  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, \"0\")}`;\n}\n\n/** A speech-like fake level: syllables inside phrases with occasional breaths. */\nfunction simulatedLevel(t: number) {\n  const syllable = Math.max(0, Math.sin(t * 7.3) * Math.sin(t * 2.9 + 1.3));\n  const phrase = 0.55 + 0.45 * Math.sin(t * 0.9);\n  const breath = Math.sin(t * 0.37 + 0.6) > 0.8 ? 0.12 : 1;\n  return Math.min(1, 0.06 + syllable * phrase * breath + Math.random() * 0.1);\n}\n\n/** Buckets raw samples into `count` bars, normalised to the loudest one. */\nfunction toBars(samples: number[], count: number) {\n  if (!samples.length) return Array.from({ length: count }, () => 0.15);\n  const out: number[] = [];\n  for (let i = 0; i < count; i++) {\n    const from = Math.floor((i / count) * samples.length);\n    const to = Math.max(from + 1, Math.floor(((i + 1) / count) * samples.length));\n    let peak = 0;\n    for (let j = from; j < to && j < samples.length; j++) peak = Math.max(peak, samples[j]);\n    out.push(peak);\n  }\n  const max = Math.max(...out, 0.01);\n  return out.map((v) => Number((0.14 + 0.86 * (v / max)).toFixed(3)));\n}\n\nfunction microphoneError(error: unknown) {\n  const name = error instanceof DOMException ? error.name : \"\";\n  if (name === \"NotAllowedError\" || name === \"SecurityError\") return \"Microphone access was blocked. Allow it in your browser's site settings to record.\";\n  if (name === \"NotFoundError\" || name === \"OverconstrainedError\") return \"No microphone was found.\";\n  if (name === \"NotReadableError\") return \"The microphone is in use by another app.\";\n  return \"The recording couldn't start.\";\n}\n\n/**\n * A voice-note record button: idle → recording (the circle morphs into a stop square, a ring pulses\n * with the input level and a timer runs) → recorded (play back, discard or keep). Simulated mode needs\n * no permissions; microphone mode asks only when the user presses record.\n */\nexport function RecordButton({\n  mode = \"simulated\",\n  maxDuration = 60,\n  label = \"Voice note\",\n  accent = \"#FF7448\",\n  size = \"md\",\n  pulse = true,\n  sensitivity = 1,\n  onRecordingStart,\n  onRecorded,\n  onDone,\n  onDiscard,\n  className = \"\",\n  style,\n  ...props\n}: RecordButtonProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const errorId = `${React.useId()}-error`;\n  const s = SIZES[size] ?? SIZES.md;\n  const maxSeconds = Math.max(1, maxDuration);\n\n  const [phase, setPhase] = React.useState<Phase>(\"idle\");\n  const [elapsed, setElapsed] = React.useState(0);\n  const [duration, setDuration] = React.useState(0);\n  const [bars, setBars] = React.useState<number[]>([]);\n  const [playing, setPlaying] = React.useState(false);\n  const [message, setMessage] = React.useState(\"\");\n  const [errorText, setErrorText] = React.useState(\"\");\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const recordRef = React.useRef<HTMLButtonElement>(null);\n  const playRef = React.useRef<HTMLButtonElement>(null);\n  const focusNext = React.useRef<\"record\" | \"play\" | null>(null);\n  const inView = React.useRef(true);\n\n  const level = useMotionValue(0);\n  const progress = useMotionValue(0);\n  const ringScale = useTransform(level, [0, 1], [1.04, 1.55]);\n  const ringOpacity = useTransform(level, [0, 1], [0.22, 0.55]);\n  const playedClip = useTransform(progress, (p) => `inset(0 ${Number((100 - p * 100).toFixed(2))}% 0 0)`);\n\n  // Everything the recording session owns. Refs: none of it should re-render the component.\n  const session = React.useRef({\n    start: 0,\n    raf: 0,\n    interval: 0,\n    smoothed: 0,\n    samples: [] as number[],\n    stream: null as MediaStream | null,\n    audioContext: null as AudioContext | null,\n    analyser: null as AnalyserNode | null,\n    buffer: null as Uint8Array<ArrayBuffer> | null,\n    recorder: null as MediaRecorder | null,\n    chunks: [] as Blob[],\n    result: null as RecordButtonResult | null,\n    url: \"\",\n    audio: null as HTMLAudioElement | null,\n    playRaf: 0,\n    playFrom: 0,\n    playStart: 0,\n    cancelled: false,\n  });\n\n  // Latest props for long-lived callbacks (intervals, recorder events).\n  const latest = React.useRef({ onRecordingStart, onRecorded, onDone, onDiscard, sensitivity, maxSeconds, mode, pulse, reduceMotion });\n  React.useEffect(() => {\n    latest.current = { onRecordingStart, onRecorded, onDone, onDiscard, sensitivity, maxSeconds, mode, pulse, reduceMotion };\n  });\n\n  React.useEffect(() => {\n    const node = rootRef.current;\n    if (!node || typeof IntersectionObserver === \"undefined\") return;\n    const io = new IntersectionObserver(([entry]) => (inView.current = entry.isIntersecting));\n    io.observe(node);\n    return () => io.disconnect();\n  }, []);\n\n  // Move focus to whichever main button replaced the one the user just pressed.\n  React.useEffect(() => {\n    const target = focusNext.current;\n    focusNext.current = null;\n    if (target === \"play\") playRef.current?.focus({ preventScroll: true });\n    if (target === \"record\") recordRef.current?.focus({ preventScroll: true });\n  }, [phase]);\n\n  const readLevel = React.useCallback(() => {\n    const sess = session.current;\n    const gain = latest.current.sensitivity;\n    if (sess.analyser && sess.buffer) {\n      sess.analyser.getByteTimeDomainData(sess.buffer);\n      let sum = 0;\n      for (let i = 0; i < sess.buffer.length; i++) {\n        const v = (sess.buffer[i] - 128) / 128;\n        sum += v * v;\n      }\n      return Math.min(1, Math.sqrt(sum / sess.buffer.length) * 4.5 * gain);\n    }\n    return Math.min(1, simulatedLevel((performance.now() - sess.start) / 1000) * gain);\n  }, []);\n\n  /** Releases the microphone and audio graph. Safe to call repeatedly. */\n  const releaseInput = React.useCallback(() => {\n    const sess = session.current;\n    cancelAnimationFrame(sess.raf);\n    window.clearInterval(sess.interval);\n    sess.stream?.getTracks().forEach((track) => track.stop());\n    sess.stream = null;\n    if (sess.audioContext && sess.audioContext.state !== \"closed\") sess.audioContext.close().catch(() => {});\n    sess.audioContext = null;\n    sess.analyser = null;\n    sess.buffer = null;\n  }, []);\n\n  const stopPlayback = React.useCallback(\n    (resetPosition: boolean) => {\n      const sess = session.current;\n      cancelAnimationFrame(sess.playRaf);\n      sess.audio?.pause();\n      if (resetPosition) {\n        sess.playFrom = 0;\n        if (sess.audio) sess.audio.currentTime = 0;\n        progress.set(0);\n      }\n      setPlaying(false);\n    },\n    [progress],\n  );\n\n  const clearRecording = React.useCallback(() => {\n    const sess = session.current;\n    stopPlayback(true);\n    if (sess.url) URL.revokeObjectURL(sess.url);\n    sess.url = \"\";\n    sess.audio = null;\n    sess.result = null;\n    sess.chunks = [];\n  }, [stopPlayback]);\n\n  // Unmount: stop everything, including the microphone.\n  React.useEffect(() => {\n    const sess = session.current;\n    // Reset on (re)mount: Strict Mode runs this cleanup once before the real mount.\n    sess.cancelled = false;\n    return () => {\n      sess.cancelled = true;\n      if (sess.recorder && sess.recorder.state !== \"inactive\") {\n        sess.recorder.onstop = null;\n        sess.recorder.stop();\n      }\n      releaseInput();\n      cancelAnimationFrame(sess.playRaf);\n      sess.audio?.pause();\n      if (sess.url) URL.revokeObjectURL(sess.url);\n    };\n  }, [releaseInput]);\n\n  const stopRecording = React.useCallback(\n    (reason: \"user\" | \"max\") => {\n      const sess = session.current;\n      const seconds = Math.min(latest.current.maxSeconds, (performance.now() - sess.start) / 1000);\n      const rounded = Math.round(seconds * 10) / 10;\n      const samples = sess.samples;\n      releaseInput();\n      level.set(0);\n\n      const recorder = sess.recorder;\n      sess.recorder = null;\n      if (recorder && recorder.state !== \"inactive\") {\n        recorder.onstop = () => {\n          if (sess.cancelled) return;\n          const type = recorder.mimeType || sess.chunks[0]?.type || \"audio/webm\";\n          const blob = new Blob(sess.chunks, { type });\n          sess.url = URL.createObjectURL(blob);\n          sess.result = { blob, duration: rounded, mimeType: type };\n          latest.current.onRecorded?.(sess.result);\n        };\n        recorder.stop();\n      } else {\n        sess.result = { blob: null, duration: rounded, mimeType: null };\n        latest.current.onRecorded?.(sess.result);\n      }\n\n      if (reason === \"user\" || document.activeElement === recordRef.current) focusNext.current = \"play\";\n      setBars(toBars(samples, BARS));\n      setDuration(rounded);\n      setPhase(\"recorded\");\n      setMessage(`${reason === \"max\" ? \"Maximum length reached. \" : \"\"}Recording stopped, ${formatTime(rounded)}`);\n    },\n    [level, releaseInput],\n  );\n\n  /** Starts the level meter, timer and auto-stop once input is ready. */\n  const beginRecording = React.useCallback(() => {\n    const sess = session.current;\n    sess.start = performance.now();\n    sess.samples = [];\n    sess.smoothed = 0;\n    setElapsed(0);\n    setPhase(\"recording\");\n    setMessage(\"Recording started\");\n    latest.current.onRecordingStart?.();\n\n    // Timer, waveform samples and auto-stop: an interval, so it keeps time offscreen too.\n    sess.interval = window.setInterval(() => {\n      const t = (performance.now() - sess.start) / 1000;\n      sess.samples.push(readLevel());\n      setElapsed(Math.floor(t));\n      if (t >= latest.current.maxSeconds) stopRecording(\"max\");\n    }, SAMPLE_MS);\n\n    // Visual level: rAF, idle while offscreen (and naturally paused in hidden tabs).\n    const frame = () => {\n      sess.raf = requestAnimationFrame(frame);\n      if (!inView.current) return;\n      const target = latest.current.pulse && !latest.current.reduceMotion ? readLevel() : 0.35;\n      sess.smoothed += (target - sess.smoothed) * 0.25;\n      level.set(sess.smoothed);\n    };\n    sess.raf = requestAnimationFrame(frame);\n  }, [level, readLevel, stopRecording]);\n\n  const startRecording = async () => {\n    setErrorText(\"\");\n    clearRecording();\n    const sess = session.current;\n    if (latest.current.mode !== \"microphone\") {\n      beginRecording();\n      return;\n    }\n    if (typeof navigator === \"undefined\" || !navigator.mediaDevices?.getUserMedia) {\n      const text = \"Recording isn't available here. It needs a secure (https) page and microphone support.\";\n      setErrorText(text);\n      setMessage(text);\n      return;\n    }\n    setPhase(\"requesting\");\n    setMessage(\"Waiting for microphone permission\");\n    try {\n      // The only place permission is requested: in direct response to the user pressing record.\n      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n      if (sess.cancelled) {\n        stream.getTracks().forEach((track) => track.stop());\n        return;\n      }\n      sess.stream = stream;\n      const AudioContextClass = window.AudioContext;\n      if (AudioContextClass) {\n        const context = new AudioContextClass();\n        const analyser = context.createAnalyser();\n        analyser.fftSize = 1024;\n        context.createMediaStreamSource(stream).connect(analyser);\n        sess.audioContext = context;\n        sess.analyser = analyser;\n        sess.buffer = new Uint8Array(new ArrayBuffer(analyser.fftSize));\n      }\n      sess.chunks = [];\n      if (typeof MediaRecorder !== \"undefined\") {\n        const recorder = new MediaRecorder(stream);\n        recorder.ondataavailable = (event) => {\n          if (event.data.size) sess.chunks.push(event.data);\n        };\n        recorder.start();\n        sess.recorder = recorder;\n      }\n      beginRecording();\n    } catch (error) {\n      releaseInput();\n      if (sess.cancelled) return;\n      const text = microphoneError(error);\n      focusNext.current = \"record\";\n      setPhase(\"idle\");\n      setErrorText(text);\n      setMessage(text);\n    }\n  };\n\n  const togglePlayback = () => {\n    const sess = session.current;\n    if (playing) {\n      if (sess.audio) sess.playFrom = sess.audio.currentTime;\n      else sess.playFrom += (performance.now() - sess.playStart) / 1000;\n      stopPlayback(false);\n      return;\n    }\n    const total = Math.max(0.1, duration);\n    if (sess.playFrom >= total - 0.05) sess.playFrom = 0;\n    if (sess.url) {\n      if (!sess.audio) sess.audio = new Audio(sess.url);\n      const audio = sess.audio;\n      audio.currentTime = sess.playFrom;\n      audio.onended = () => stopPlayback(true);\n      audio.play().catch(() => stopPlayback(true));\n    }\n    sess.playStart = performance.now();\n    setPlaying(true);\n    const frame = () => {\n      const position = sess.audio ? sess.audio.currentTime : sess.playFrom + (performance.now() - sess.playStart) / 1000;\n      progress.set(Math.min(1, position / total));\n      if (!sess.audio && position >= total) {\n        stopPlayback(true);\n        return;\n      }\n      sess.playRaf = requestAnimationFrame(frame);\n    };\n    sess.playRaf = requestAnimationFrame(frame);\n  };\n\n  const discard = () => {\n    clearRecording();\n    focusNext.current = \"record\";\n    setPhase(\"idle\");\n    setMessage(\"Recording discarded\");\n    latest.current.onDiscard?.();\n  };\n\n  const done = () => {\n    const sess = session.current;\n    const result = sess.result ?? { blob: null, duration, mimeType: null };\n    stopPlayback(true);\n    latest.current.onDone?.(result);\n    clearRecording();\n    focusNext.current = \"record\";\n    setPhase(\"idle\");\n    setMessage(`Recording done, ${formatTime(duration)}`);\n  };\n\n  const recording = phase === \"recording\";\n  const recorded = phase === \"recorded\";\n  const swap = reduceMotion\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.12 } }\n    : {\n        initial: { opacity: 0, scale: 0.6 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.6 },\n        transition: { type: \"spring\" as const, stiffness: 520, damping: 32 },\n      };\n  const iconButton =\n    \"grid size-9 shrink-0 place-items-center rounded-full text-zinc-500 outline-hidden transition-colors hover:bg-zinc-900/[0.06] hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-solid focus-visible:outline-[var(--rb-accent)] dark:text-zinc-400 dark:hover:bg-white/[0.08] dark:hover:text-zinc-50\";\n  const mainButton = `grid shrink-0 place-items-center rounded-full border border-zinc-900/10 bg-white shadow-[0_1px_0_rgb(0_0_0/0.04),0_8px_20px_-12px_rgb(0_0_0/0.45)] outline-hidden transition-colors hover:border-zinc-900/20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--rb-accent)] dark:border-white/10 dark:bg-zinc-900 dark:shadow-none dark:hover:border-white/20`;\n\n  return (\n    <div\n      {...props}\n      ref={rootRef}\n      role=\"group\"\n      aria-label={label}\n      className={`relative flex w-full max-w-[380px] items-center gap-3 rounded-full border border-zinc-900/10 bg-white/70 p-2 pr-3 dark:border-white/10 dark:bg-white/[0.03] ${className}`}\n      style={{ [\"--rb-accent\" as string]: accent, ...style }}\n    >\n      <div className=\"relative shrink-0\" style={{ width: s.button, height: s.button }}>\n        {/* Level ring (decorative) */}\n        {recording ? (\n          <motion.span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 rounded-full bg-[var(--rb-accent)]\"\n            style={reduceMotion || !pulse ? { scale: 1.14, opacity: 0.22 } : { scale: ringScale, opacity: ringOpacity }}\n          />\n        ) : null}\n        <AnimatePresence initial={false}>\n          {recorded ? (\n            <motion.button\n              key=\"play\"\n              ref={playRef}\n              type=\"button\"\n              aria-label={playing ? \"Pause playback\" : \"Play recording\"}\n              onClick={togglePlayback}\n              className={`${mainButton} absolute inset-0 text-zinc-900 dark:text-zinc-50`}\n              {...swap}\n            >\n              <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" className=\"size-[38%]\" fill=\"currentColor\">\n                {playing ? <path d=\"M7 5h3.2v14H7zM13.8 5H17v14h-3.2z\" /> : <path d=\"M8 5.2v13.6a.8.8 0 0 0 1.2.7l10.6-6.8a.8.8 0 0 0 0-1.4L9.2 4.5a.8.8 0 0 0-1.2.7Z\" />}\n              </svg>\n            </motion.button>\n          ) : (\n            <motion.button\n              key=\"record\"\n              ref={recordRef}\n              type=\"button\"\n              aria-label={`Record ${label.toLowerCase()}`}\n              aria-pressed={recording}\n              aria-busy={phase === \"requesting\" || undefined}\n              aria-describedby={errorText ? errorId : undefined}\n              onClick={() => {\n                if (phase === \"requesting\") return;\n                if (recording) stopRecording(\"user\");\n                else void startRecording();\n              }}\n              className={`${mainButton} absolute inset-0`}\n              {...swap}\n            >\n              {/* Circle ⇄ rounded square */}\n              <motion.span\n                aria-hidden=\"true\"\n                className=\"block bg-[var(--rb-accent)]\"\n                initial={false}\n                animate={{\n                  width: recording ? \"34%\" : \"42%\",\n                  height: recording ? \"34%\" : \"42%\",\n                  borderRadius: recording ? \"22%\" : \"50%\",\n                  opacity: phase === \"requesting\" ? 0.45 : 1,\n                }}\n                transition={reduceMotion ? { duration: 0 } : { type: \"spring\", stiffness: 420, damping: 26 }}\n              />\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <div className=\"min-w-0 flex-1\">\n        {recorded ? (\n          <div className=\"flex items-center gap-2.5\">\n            {/* Waveform: muted bars with an accent copy clipped to the playback position. */}\n            <div aria-hidden=\"true\" className=\"relative h-8 min-w-0 flex-1\">\n              <Bars bars={bars} className=\"bg-zinc-900/20 dark:bg-white/25\" />\n              <motion.div className=\"absolute inset-0\" style={{ clipPath: playedClip }}>\n                <Bars bars={bars} className=\"bg-[var(--rb-accent)]\" />\n              </motion.div>\n            </div>\n            <span className=\"shrink-0 font-mono text-[11px] tabular-nums text-zinc-600 dark:text-zinc-300\">{formatTime(duration)}</span>\n          </div>\n        ) : (\n          <div className=\"flex flex-col gap-1\">\n            <span className={`flex items-center gap-2 font-medium tracking-tight text-zinc-900 dark:text-zinc-50 ${s.text}`}>\n              {recording ? (\n                <>\n                  <span aria-hidden=\"true\" className=\"size-1.5 rounded-full bg-[var(--rb-accent)] motion-safe:animate-pulse\" />\n                  Recording\n                </>\n              ) : phase === \"requesting\" ? (\n                \"Waiting for microphone…\"\n              ) : (\n                label\n              )}\n            </span>\n            {errorText ? (\n              <span id={errorId} className=\"text-[12px] leading-snug text-[#D93D42] dark:text-[#FF8A8E]\">\n                {errorText}\n              </span>\n            ) : (\n              <span className=\"flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n                <span className=\"tabular-nums\">\n                  {recording ? formatTime(elapsed) : \"Tap to record\"} / {formatTime(maxSeconds)}\n                </span>\n                {recording ? (\n                  <span aria-hidden=\"true\" className=\"relative h-px min-w-6 flex-1 overflow-hidden bg-zinc-900/10 dark:bg-white/10\">\n                    <span\n                      className=\"absolute inset-y-0 left-0 bg-[var(--rb-accent)] transition-[width] duration-100 ease-linear\"\n                      style={{ width: `${Math.min(100, Math.round((elapsed / maxSeconds) * 1000) / 10)}%` }}\n                    />\n                  </span>\n                ) : null}\n              </span>\n            )}\n          </div>\n        )}\n      </div>\n\n      {recorded ? (\n        <div className=\"flex shrink-0 items-center gap-0.5\">\n          <button type=\"button\" aria-label=\"Discard recording\" onClick={discard} className={iconButton}>\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-[18px]\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n              <path d=\"M4.5 7h15M9.5 7V5.2c0-.66.54-1.2 1.2-1.2h2.6c.66 0 1.2.54 1.2 1.2V7m-8.3 0 .8 11.1A2 2 0 0 0 9 20h6a2 2 0 0 0 2-1.9L17.8 7\" />\n            </svg>\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Done\"\n            onClick={done}\n            className=\"grid size-9 shrink-0 place-items-center rounded-full bg-zinc-900 text-white outline-hidden transition-[background-color,scale] hover:bg-zinc-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--rb-accent)] active:scale-95 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-white\"\n          >\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-[18px]\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n              <path d=\"m5.5 12.5 4.2 4.2 8.8-9.4\" />\n            </svg>\n          </button>\n        </div>\n      ) : null}\n\n      <span role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {message}\n      </span>\n    </div>\n  );\n}\n\nfunction Bars({ bars, className }: { bars: number[]; className: string }) {\n  return (\n    <div className=\"absolute inset-0 flex items-center gap-[2px]\">\n      {bars.map((h, i) => (\n        <span key={i} className={`min-w-0 flex-1 rounded-full ${className}`} style={{ height: `${Math.round(h * 100)}%` }} />\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "buttons"
  ],
  "docs": "Documentation: https://wancraft.my/components/record-button"
}
