{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-recorder",
  "title": "AI Recorder",
  "description": "Voice recorder pill that morphs into a live waveform with timer, pause, cancel and stop, then a scrubbable playback row.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://trytweenly.vercel.app/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ai-recorder/ai-recorder.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, motion, useMotionValue, useTransform, type Transition } from \"motion/react\"\nimport { Check, Mic, MicOff, Pause, Play, RotateCcw, Trash2 } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotion } from \"@/registry/new-york/hooks/use-reduced-motion\"\n\nexport interface AiRecorderProps {\n  /** Called after stop with the recorded audio and its duration. In simulate mode the blob is empty. */\n  onRecordingComplete?: (blob: Blob, durationMs: number) => void\n  /** Stop automatically after this many seconds. Default: undefined (no limit) */\n  maxDuration?: number\n  /** Fake the waveform and timer without touching the microphone. Useful for demos. Default: false */\n  simulate?: boolean\n  /** Color of the stop button, recording dot and played waveform (any CSS color). Default: \"#ff4d12\" */\n  accent?: string\n  /** Height of the control in px. Default: 44 */\n  size?: number\n  /** Additional classes for the root element. Default: undefined */\n  className?: string\n}\n\ntype Phase = \"idle\" | \"requesting\" | \"recording\" | \"paused\" | \"recorded\" | \"denied\" | \"unavailable\"\ntype Recording = { url: string | null; duration: number; peaks: number[] }\n\nconst PEAKS = 36\nconst SAMPLE_MS = 50\nconst BAR = 2.5\nconst BAR_GAP = 2\nconst spring: Transition = { type: \"spring\", stiffness: 450, damping: 36 }\nconst ease = [0.22, 1, 0.36, 1] as const\n\nconst clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v))\nconst hash = (n: number, seed: number) => {\n  const x = Math.sin(n * 127.1 + seed * 311.7) * 43758.5453\n  return x - Math.floor(x)\n}\nconst noise = (x: number, seed = 0) => {\n  const i = Math.floor(x)\n  const f = x - i\n  const u = f * f * (3 - 2 * f)\n  return hash(i, seed) * (1 - u) + hash(i + 1, seed) * u\n}\nconst speech = (t: number) => {\n  const gate = clamp((noise(t * 0.45, 3) - 0.25) * 2.4)\n  return clamp(0.06 + gate * (0.2 + noise(t * 5.2, 11) * 0.8))\n}\nconst fmt = (ms: number) => {\n  const s = Math.max(0, Math.floor(ms / 1000))\n  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, \"0\")}`\n}\nconst toPeaks = (levels: number[]) => {\n  if (!levels.length) return Array.from({ length: PEAKS }, () => 0.1)\n  const out: number[] = []\n  for (let i = 0; i < PEAKS; i++) {\n    const a = Math.floor((i / PEAKS) * levels.length)\n    const b = Math.max(a + 1, Math.floor(((i + 1) / PEAKS) * levels.length))\n    let m = 0\n    for (let j = a; j < b && j < levels.length; j++) m = Math.max(m, levels[j])\n    out.push(m)\n  }\n  const max = Math.max(...out, 0.05)\n  return out.map((v) => clamp(0.12 + (v / max) * 0.88))\n}\n\nfunction IconButton({\n  label,\n  onClick,\n  children,\n  className,\n  style,\n  dim,\n  reduced,\n}: {\n  label: string\n  onClick: () => void\n  children: React.ReactNode\n  className?: string\n  style?: React.CSSProperties\n  dim: number\n  reduced: boolean\n}) {\n  return (\n    <motion.button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      onClick={onClick}\n      whileTap={reduced ? undefined : { scale: 0.9 }}\n      transition={spring}\n      className={cn(\n        \"grid shrink-0 place-items-center rounded-full text-muted-foreground transition-colors outline-none hover:bg-foreground/[0.06] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        className\n      )}\n      style={{ width: dim, height: dim, ...style }}\n    >\n      {children}\n    </motion.button>\n  )\n}\n\nexport function AiRecorder({\n  onRecordingComplete,\n  maxDuration,\n  simulate = false,\n  accent = \"#ff4d12\",\n  size = 44,\n  className,\n}: AiRecorderProps) {\n  const reduced = useReducedMotion()\n  const [phase, setPhase] = useState<Phase>(\"idle\")\n  const [elapsed, setElapsed] = useState(0)\n  const [rec, setRec] = useState<Recording | null>(null)\n  const [playing, setPlaying] = useState(false)\n  const [posSec, setPosSec] = useState(0)\n  const [announce, setAnnounce] = useState(\"\")\n\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const audioRef = useRef<HTMLAudioElement>(null)\n  const streamRef = useRef<MediaStream | null>(null)\n  const ctxRef = useRef<AudioContext | null>(null)\n  const analyserRef = useRef<AnalyserNode | null>(null)\n  const recorderRef = useRef<MediaRecorder | null>(null)\n  const chunksRef = useRef<Blob[]>([])\n  const levelsRef = useRef<number[]>([])\n  const baseRef = useRef(0)\n  const segRef = useRef(0)\n  const durationRef = useRef(0)\n  const cancelRef = useRef(false)\n  const urlRef = useRef<string | null>(null)\n  const aliveRef = useRef(true)\n  const simPosRef = useRef(0)\n  const phaseRef = useRef<Phase>(\"idle\")\n  const live = useRef({ onRecordingComplete, maxDuration })\n  const finishRef = useRef<(commit: boolean) => void>(() => {})\n\n  const progress = useMotionValue(0)\n  const clip = useTransform(progress, (p) => `inset(0 ${(1 - p) * 100}% 0 0)`)\n\n  const inner = Math.max(24, size - 12)\n  const icon = Math.round(inner * 0.5)\n  const capturing = phase === \"recording\" || phase === \"paused\"\n\n  useEffect(() => {\n    live.current = { onRecordingComplete, maxDuration }\n    phaseRef.current = phase\n  })\n\n  const release = () => {\n    streamRef.current?.getTracks().forEach((t) => t.stop())\n    streamRef.current = null\n    analyserRef.current = null\n    void ctxRef.current?.close().catch(() => {})\n    ctxRef.current = null\n  }\n\n  const elapsedNow = () => baseRef.current + (phaseRef.current === \"recording\" ? performance.now() - segRef.current : 0)\n\n  const begin = () => {\n    levelsRef.current = []\n    baseRef.current = 0\n    segRef.current = performance.now()\n    phaseRef.current = \"recording\"\n    setElapsed(0)\n    setPhase(\"recording\")\n    setAnnounce(\"Recording started\")\n  }\n\n  const finalize = (blob: Blob, duration: number) => {\n    if (!aliveRef.current) return\n    release()\n    if (cancelRef.current) {\n      setPhase(\"idle\")\n      setAnnounce(\"Recording discarded\")\n      return\n    }\n    const url = blob.size ? URL.createObjectURL(blob) : null\n    urlRef.current = url\n    progress.set(0)\n    simPosRef.current = 0\n    setPosSec(0)\n    setRec({ url, duration, peaks: toPeaks(levelsRef.current) })\n    setPhase(\"recorded\")\n    setAnnounce(`Recording saved, ${fmt(duration)}`)\n    live.current.onRecordingComplete?.(blob, duration)\n  }\n\n  const finish = (commit: boolean) => {\n    const duration = elapsedNow()\n    durationRef.current = duration\n    cancelRef.current = !commit\n    phaseRef.current = \"idle\"\n    const r = recorderRef.current\n    recorderRef.current = null\n    if (r && r.state !== \"inactive\") r.stop()\n    else finalize(new Blob([], { type: \"audio/webm\" }), duration)\n  }\n\n  useEffect(() => {\n    finishRef.current = finish\n  })\n\n  const start = async () => {\n    if (simulate) {\n      begin()\n      return\n    }\n    setPhase(\"requesting\")\n    setAnnounce(\"Requesting microphone\")\n    let stream: MediaStream\n    try {\n      if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === \"undefined\") {\n        throw new DOMException(\"Recording unsupported\", \"NotSupportedError\")\n      }\n      stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n    } catch (err) {\n      if (!aliveRef.current) return\n      const name = err instanceof DOMException ? err.name : \"\"\n      const denied = name === \"NotAllowedError\" || name === \"SecurityError\"\n      setPhase(denied ? \"denied\" : \"unavailable\")\n      setAnnounce(denied ? \"Microphone access denied\" : \"No microphone available\")\n      return\n    }\n    if (!aliveRef.current) {\n      stream.getTracks().forEach((t) => t.stop())\n      return\n    }\n    streamRef.current = stream\n    try {\n      const ctx = new AudioContext()\n      const analyser = ctx.createAnalyser()\n      analyser.fftSize = 512\n      analyser.smoothingTimeConstant = 0.5\n      ctx.createMediaStreamSource(stream).connect(analyser)\n      ctxRef.current = ctx\n      analyserRef.current = analyser\n    } catch {\n      // Waveform is optional; recording still works\n    }\n    const recorder = new MediaRecorder(stream)\n    chunksRef.current = []\n    recorder.ondataavailable = (e) => {\n      if (e.data.size) chunksRef.current.push(e.data)\n    }\n    recorder.onstop = () => {\n      finalize(new Blob(chunksRef.current, { type: recorder.mimeType || \"audio/webm\" }), durationRef.current)\n    }\n    recorderRef.current = recorder\n    recorder.start(250)\n    begin()\n  }\n\n  const pause = () => {\n    baseRef.current += performance.now() - segRef.current\n    phaseRef.current = \"paused\"\n    recorderRef.current?.pause()\n    setPhase(\"paused\")\n    setAnnounce(\"Recording paused\")\n  }\n\n  const resume = () => {\n    segRef.current = performance.now()\n    phaseRef.current = \"recording\"\n    recorderRef.current?.resume()\n    setPhase(\"recording\")\n    setAnnounce(\"Recording resumed\")\n  }\n\n  const remove = () => {\n    audioRef.current?.pause()\n    if (urlRef.current) URL.revokeObjectURL(urlRef.current)\n    urlRef.current = null\n    setPlaying(false)\n    setRec(null)\n    setPhase(\"idle\")\n    setAnnounce(\"Recording deleted\")\n  }\n\n  // Unmount cleanup\n  useEffect(() => {\n    aliveRef.current = true\n    return () => {\n      aliveRef.current = false\n      const r = recorderRef.current\n      recorderRef.current = null\n      if (r && r.state !== \"inactive\") {\n        r.onstop = null\n        r.stop()\n      }\n      streamRef.current?.getTracks().forEach((t) => t.stop())\n      void ctxRef.current?.close().catch(() => {})\n      if (urlRef.current) URL.revokeObjectURL(urlRef.current)\n      urlRef.current = null\n    }\n  }, [])\n\n  // Capture loop: samples level, draws the live waveform, drives the timer\n  useEffect(() => {\n    if (!capturing) return\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    let raf = 0\n    let lastSample = performance.now()\n    let shownSec = -1\n    let fill = \"currentColor\"\n    let W = 0\n    let H = 0\n    const samples: number[] = []\n    const buf = new Uint8Array(512)\n\n    const resize = () => {\n      if (!canvas || !ctx) return\n      const r = canvas.getBoundingClientRect()\n      W = r.width\n      H = r.height\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.max(1, Math.round(W * dpr))\n      canvas.height = Math.max(1, Math.round(H * dpr))\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      fill = getComputedStyle(canvas).color\n    }\n    resize()\n\n    const level = (now: number) => {\n      const an = analyserRef.current\n      if (an) {\n        an.getByteTimeDomainData(buf)\n        let sum = 0\n        for (let i = 0; i < an.fftSize && i < buf.length; i++) {\n          const v = (buf[i] - 128) / 128\n          sum += v * v\n        }\n        return clamp(Math.sqrt(sum / Math.min(an.fftSize, buf.length)) * 4.5)\n      }\n      return simulate ? speech(now / 1000) : 0\n    }\n\n    const tick = (now: number) => {\n      const recording = phaseRef.current === \"recording\"\n      if (recording) {\n        while (now - lastSample >= SAMPLE_MS) {\n          lastSample += SAMPLE_MS\n          const v = level(lastSample)\n          samples.push(v)\n          levelsRef.current.push(v)\n          if (samples.length > 160) samples.shift()\n        }\n        const ms = baseRef.current + now - segRef.current\n        const sec = Math.floor(ms / 1000)\n        if (sec !== shownSec) {\n          shownSec = sec\n          setElapsed(ms)\n        }\n        const max = live.current.maxDuration\n        if (max && ms >= max * 1000) {\n          finishRef.current(true)\n          return\n        }\n      } else {\n        lastSample = now\n      }\n\n      if (canvas && ctx) {\n        ctx.clearRect(0, 0, W, H)\n        ctx.fillStyle = fill\n        ctx.globalAlpha = recording ? 0.85 : 0.35\n        const step = BAR + BAR_GAP\n        const shift = recording && !reduced ? ((now - lastSample) / SAMPLE_MS) * step : 0\n        const count = Math.ceil(W / step) + 1\n        for (let i = 0; i < count; i++) {\n          const v = samples[samples.length - 1 - i] ?? 0\n          const x = W - BAR - i * step - shift + step\n          if (x < -BAR) break\n          const h = Math.max(BAR, v * H)\n          // Fade bars toward the left edge\n          const edge = clamp(x / (W * 0.25))\n          ctx.globalAlpha = (recording ? 0.85 : 0.35) * edge\n          ctx.beginPath()\n          if (ctx.roundRect) ctx.roundRect(x, (H - h) / 2, BAR, h, BAR / 2)\n          else ctx.rect(x, (H - h) / 2, BAR, h)\n          ctx.fill()\n        }\n        ctx.globalAlpha = 1\n      }\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [capturing, simulate, reduced])\n\n  // Playback loop\n  useEffect(() => {\n    if (!playing || !rec) return\n    let raf = 0\n    let last = performance.now()\n    let shown = -1\n    const tick = (now: number) => {\n      const a = audioRef.current\n      let ms: number\n      if (rec.url && a) ms = a.currentTime * 1000\n      else {\n        simPosRef.current += now - last\n        ms = simPosRef.current\n      }\n      last = now\n      const frac = clamp(ms / rec.duration)\n      progress.set(frac)\n      const sec = Math.floor((frac * rec.duration) / 1000)\n      if (sec !== shown) {\n        shown = sec\n        setPosSec(sec)\n      }\n      if (frac >= 1 || (rec.url && a?.ended)) {\n        setPlaying(false)\n        return\n      }\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [playing, rec, progress])\n\n  const togglePlay = () => {\n    if (!rec) return\n    const a = audioRef.current\n    if (playing) {\n      a?.pause()\n      setPlaying(false)\n      return\n    }\n    if (progress.get() >= 0.999) seek(0)\n    if (rec.url && a) {\n      a.play().catch(() => setPlaying(false))\n    }\n    setPlaying(true)\n  }\n\n  const seek = (frac: number) => {\n    if (!rec) return\n    const f = clamp(frac)\n    progress.set(f)\n    simPosRef.current = f * rec.duration\n    setPosSec(Math.floor((f * rec.duration) / 1000))\n    const a = audioRef.current\n    if (rec.url && a) a.currentTime = (f * rec.duration) / 1000\n  }\n\n  const seekFromPointer = (e: React.PointerEvent<HTMLDivElement>) => {\n    const r = e.currentTarget.getBoundingClientRect()\n    seek((e.clientX - r.left) / r.width)\n  }\n\n  const group = phase === \"recording\" || phase === \"paused\" ? \"rec\" : phase === \"recorded\" ? \"play\" : phase === \"denied\" || phase === \"unavailable\" ? \"blocked\" : \"idle\"\n  const fade: Transition = reduced ? { duration: 0 } : { duration: 0.25, ease }\n  const contentMotion = {\n    initial: { opacity: 0, filter: \"blur(4px)\" },\n    animate: { opacity: 1, filter: \"blur(0px)\" },\n    exit: { opacity: 0, filter: \"blur(4px)\" },\n    transition: fade,\n  }\n\n  return (\n    <div className={cn(\"inline-flex\", className)}>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {announce}\n      </span>\n      <motion.div\n        layout\n        transition={reduced ? { duration: 0 } : spring}\n        className={cn(\n          \"relative flex items-center overflow-hidden border bg-card text-foreground shadow-sm\",\n          group === \"idle\" ? \"p-0\" : \"px-1.5\"\n        )}\n        style={{ height: size, borderRadius: size / 2, minWidth: size }}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {group === \"idle\" && (\n            <motion.button\n              key=\"idle\"\n              type=\"button\"\n              layout=\"position\"\n              {...contentMotion}\n              onClick={() => void start()}\n              disabled={phase === \"requesting\"}\n              aria-label=\"Start recording\"\n              title=\"Start recording\"\n              whileTap={reduced ? undefined : { scale: 0.92 }}\n              className=\"grid shrink-0 place-items-center rounded-full outline-none transition-colors hover:bg-foreground/[0.05] focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-wait\"\n              style={{ width: size - 2, height: size - 2 }}\n            >\n              <motion.span\n                animate={phase === \"requesting\" && !reduced ? { opacity: [1, 0.35, 1] } : { opacity: 1 }}\n                transition={phase === \"requesting\" ? { duration: 1.2, repeat: Infinity, ease: [0.76, 0, 0.24, 1] } : fade}\n              >\n                <Mic style={{ width: icon, height: icon }} aria-hidden />\n              </motion.span>\n            </motion.button>\n          )}\n\n          {group === \"blocked\" && (\n            <motion.div key=\"blocked\" layout=\"position\" {...contentMotion} className=\"flex items-center gap-2 pr-1\">\n              <span className=\"grid shrink-0 place-items-center text-muted-foreground\" style={{ width: inner, height: inner }}>\n                <MicOff style={{ width: icon, height: icon }} aria-hidden />\n              </span>\n              <span className=\"text-xs whitespace-nowrap text-muted-foreground\">\n                {phase === \"denied\" ? \"Microphone access denied\" : \"No microphone found\"}\n              </span>\n              <IconButton label=\"Try again\" onClick={() => void start()} dim={inner} reduced={reduced}>\n                <RotateCcw style={{ width: icon * 0.85, height: icon * 0.85 }} aria-hidden />\n              </IconButton>\n            </motion.div>\n          )}\n\n          {group === \"rec\" && (\n            <motion.div key=\"rec\" layout=\"position\" {...contentMotion} className=\"flex items-center gap-1.5\">\n              <IconButton label=\"Discard recording\" onClick={() => finish(false)} dim={inner} reduced={reduced}>\n                <Trash2 style={{ width: icon * 0.85, height: icon * 0.85 }} aria-hidden />\n              </IconButton>\n              <div className=\"flex items-center gap-2 pl-0.5\">\n                <motion.span\n                  aria-hidden\n                  className=\"size-2 shrink-0 rounded-full\"\n                  style={{ backgroundColor: accent }}\n                  animate={phase === \"recording\" && !reduced ? { opacity: [1, 0.3, 1] } : { opacity: phase === \"recording\" ? 1 : 0.35 }}\n                  transition={phase === \"recording\" && !reduced ? { duration: 1.4, repeat: Infinity, ease: [0.76, 0, 0.24, 1] } : fade}\n                />\n                <span role=\"timer\" aria-label={`Recording time ${fmt(elapsed)}`} className=\"w-9 text-xs font-medium tabular-nums\">\n                  {fmt(elapsed)}\n                </span>\n              </div>\n              <canvas ref={canvasRef} aria-hidden className=\"block text-foreground\" style={{ width: 128, height: Math.round(size * 0.5) }} />\n              {phase === \"recording\" ? (\n                <IconButton label=\"Pause recording\" onClick={pause} dim={inner} reduced={reduced}>\n                  <Pause style={{ width: icon * 0.85, height: icon * 0.85 }} aria-hidden />\n                </IconButton>\n              ) : (\n                <IconButton label=\"Resume recording\" onClick={resume} dim={inner} reduced={reduced}>\n                  <Mic style={{ width: icon * 0.85, height: icon * 0.85 }} aria-hidden />\n                </IconButton>\n              )}\n              <IconButton\n                label=\"Stop and save recording\"\n                onClick={() => finish(true)}\n                dim={inner}\n                reduced={reduced}\n                className=\"text-white hover:bg-transparent hover:text-white hover:brightness-110\"\n                style={{ backgroundColor: accent }}\n              >\n                <Check style={{ width: icon, height: icon }} strokeWidth={2.5} aria-hidden />\n              </IconButton>\n            </motion.div>\n          )}\n\n          {group === \"play\" && rec && (\n            <motion.div key=\"play\" layout=\"position\" {...contentMotion} className=\"flex items-center gap-1.5\">\n              {rec.url && <audio ref={audioRef} src={rec.url} preload=\"metadata\" onEnded={() => setPlaying(false)} className=\"hidden\" />}\n              <IconButton\n                label={playing ? \"Pause playback\" : \"Play recording\"}\n                onClick={togglePlay}\n                dim={inner}\n                reduced={reduced}\n                className=\"bg-foreground/[0.06] text-foreground\"\n              >\n                {playing ? (\n                  <Pause style={{ width: icon * 0.8, height: icon * 0.8 }} fill=\"currentColor\" aria-hidden />\n                ) : (\n                  <Play style={{ width: icon * 0.8, height: icon * 0.8 }} fill=\"currentColor\" className=\"translate-x-px\" aria-hidden />\n                )}\n              </IconButton>\n              <div\n                role=\"slider\"\n                tabIndex={0}\n                aria-label=\"Playback position\"\n                aria-valuemin={0}\n                aria-valuemax={Math.round(rec.duration / 1000)}\n                aria-valuenow={posSec}\n                aria-valuetext={`${fmt(posSec * 1000)} of ${fmt(rec.duration)}`}\n                onPointerDown={(e) => {\n                  e.currentTarget.setPointerCapture(e.pointerId)\n                  seekFromPointer(e)\n                }}\n                onPointerMove={(e) => {\n                  if (e.currentTarget.hasPointerCapture(e.pointerId)) seekFromPointer(e)\n                }}\n                onKeyDown={(e) => {\n                  const step = 0.05\n                  const p = progress.get()\n                  if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") seek(p + step)\n                  else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") seek(p - step)\n                  else if (e.key === \"Home\") seek(0)\n                  else if (e.key === \"End\") seek(1)\n                  else if (e.key === \" \" || e.key === \"Enter\") togglePlay()\n                  else return\n                  e.preventDefault()\n                }}\n                className=\"relative flex h-full cursor-pointer touch-none items-center rounded-md px-1 outline-none select-none focus-visible:ring-2 focus-visible:ring-ring\"\n                style={{ height: size * 0.6 }}\n              >\n                <Bars peaks={rec.peaks} height={size * 0.5} className=\"text-foreground/25\" />\n                <motion.div className=\"absolute inset-y-0 left-1 flex items-center\" style={{ clipPath: clip, color: accent }} aria-hidden>\n                  <Bars peaks={rec.peaks} height={size * 0.5} />\n                </motion.div>\n              </div>\n              <span className=\"w-9 text-right text-xs tabular-nums text-muted-foreground\">\n                {fmt(playing || posSec > 0 ? posSec * 1000 : rec.duration)}\n              </span>\n              <IconButton label=\"Delete recording\" onClick={remove} dim={inner} reduced={reduced}>\n                <Trash2 style={{ width: icon * 0.85, height: icon * 0.85 }} aria-hidden />\n              </IconButton>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  )\n}\n\nfunction Bars({ peaks, height, className }: { peaks: number[]; height: number; className?: string }) {\n  return (\n    <div className={cn(\"flex items-center\", className)} style={{ height, gap: BAR_GAP }} aria-hidden>\n      {peaks.map((p, i) => (\n        <span key={i} className=\"shrink-0 rounded-full bg-current\" style={{ width: BAR, height: Math.max(BAR, p * height) }} />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/ai-recorder",
  "type": "registry:component"
}