{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-ai-audio-player",
  "title": "Aurora AI Audio Player",
  "author": "jmagar <https://github.com/jmagar>",
  "description": "Aurora-native audio player parity surface from AI Elements.",
  "dependencies": [
    "lucide-react@^0.487.0"
  ],
  "registryDependencies": [
    "@aurora/aurora-tokens",
    "@aurora/aurora-ai-elements"
  ],
  "files": [
    {
      "path": "registry/aurora/blocks/ai/elements/audio-player.tsx",
      "content": "\"use client\"\n\n/**\n * Aurora AudioPlayer — voice/audio response element.\n *\n * Visual layer ported 1:1 from the Claude Design source: a lit-outline cyan\n * transport button, a waveform scrubber whose played bars glow cyan over a dim\n * unplayed tail, a `synthesizing` state that swaps the transport for a spinner,\n * and a `compact` single-row pill. Reads only `--aurora-*` tokens so it renders\n * pixel-identical in dark + `.light`.\n *\n * Architecture stays shadcn/registry: the Aurora `Button` (Radix `Slot`/`cva`)\n * powers transport + download, `forwardRef`, `displayName`, and the full\n * `React.HTMLAttributes` escape hatch are preserved.\n */\n\nimport * as React from \"react\"\nimport { Download, Play, Sparkles } from \"lucide-react\"\nimport { Button } from \"@/registry/aurora/ui/button\"\nimport { Separator } from \"@/registry/aurora/ui/separator\"\nimport { Spinner } from \"@/registry/aurora/ui/spinner\"\n\nexport interface AudioPlayerProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Headline label for the clip. */\n  title?: string\n  /** Total duration, mm:ss. */\n  duration?: string\n  /** Playback progress 0–1 — drives the waveform fill and the current-time readout. */\n  progress?: number\n  /** Transport state. `synthesizing` swaps the play control for a spinner and hides the waveform. */\n  status?: \"idle\" | \"synthesizing\"\n  /** Render the Axon orange \"AI Voice\" pill beside the title. */\n  badge?: boolean\n  /** Show the square download control in the footer. */\n  download?: boolean\n  /** `default` is the full card; `compact` is the single-row pill. */\n  variant?: \"default\" | \"compact\"\n  /** Current speed multiplier label shown in the footer. */\n  speed?: string\n}\n\n// Deterministic waveform bar heights (0..1) — fixed so SSR + client agree.\nconst WAVE_FULL = [\n  0.32, 0.5, 0.42, 0.6, 0.38, 0.72, 0.48, 0.34, 0.55, 0.46, 0.82, 0.92, 0.7,\n  0.58, 0.86, 0.62, 0.44, 0.78, 0.36, 0.5, 0.4, 0.66, 0.3, 0.54, 0.48, 0.6,\n  0.42, 0.74, 0.5, 0.38, 0.68, 0.46, 0.56, 0.4, 0.62, 0.34, 0.7, 0.5, 0.44,\n  0.58, 0.36, 0.66, 0.48, 0.4, 0.6, 0.52, 0.34, 0.7, 0.46, 0.56,\n]\n\nconst WAVE_COMPACT = [\n  0.3, 0.18, 0.42, 0.56, 0.66, 0.48, 0.34, 0.5, 0.4, 0.24, 0.32, 0.46, 0.7,\n  0.84, 0.78, 0.62, 0.5, 0.36, 0.28, 0.44, 0.58, 0.66, 0.52, 0.4, 0.5, 0.6,\n  0.46, 0.34, 0.5, 0.42, 0.3, 0.56, 0.44, 0.36, 0.5, 0.28,\n]\n\nconst AI_ORANGE = \"var(--axon-orange)\"\n\nfunction parseSeconds(value: string): number {\n  const parts = value.split(\":\").map((p) => Number.parseInt(p, 10))\n  if (parts.some(Number.isNaN)) return 0\n  return parts.reduce((acc, n) => acc * 60 + n, 0)\n}\n\nfunction formatSeconds(total: number): string {\n  const safe = Math.max(0, Math.round(total))\n  const m = Math.floor(safe / 60)\n  const s = safe % 60\n  return `${String(m).padStart(2, \"0\")}:${String(s).padStart(2, \"0\")}`\n}\n\nfunction Waveform({\n  bars,\n  progress,\n  height,\n  className,\n}: {\n  bars: number[]\n  progress: number\n  height: number\n  className?: string\n}) {\n  const clampedProgress = Math.min(1, Math.max(0, progress))\n  const playedCount = Math.round(bars.length * clampedProgress)\n  return (\n    <span\n      role=\"img\"\n      aria-label={`Playback waveform ${Math.round(clampedProgress * 100)}% played`}\n      className={[\"flex min-w-0 flex-1 items-center\", className].filter(Boolean).join(\" \")}\n      style={{ gap: 3, height }}\n    >\n      {bars.map((amp, i) => {\n        const played = i < playedCount\n        return (\n          <span\n            key={i}\n            style={{\n              flex: \"1 1 0%\",\n              minWidth: 2,\n              maxWidth: 5,\n              height: `${Math.max(14, amp * 100)}%`,\n              borderRadius: 999,\n              background: played\n                ? \"var(--aurora-accent-primary)\"\n                : \"color-mix(in srgb, var(--aurora-accent-primary) 26%, var(--aurora-control-surface))\",\n              boxShadow: played\n                ? \"0 0 6px color-mix(in srgb, var(--aurora-accent-primary) 38%, transparent)\"\n                : \"none\",\n              transition: \"background var(--motion-duration-fast) var(--motion-ease-out, ease)\",\n            }}\n          />\n        )\n      })}\n    </span>\n  )\n}\n\nconst cardStyle: React.CSSProperties = {\n  background: \"var(--aurora-surface-raised)\",\n  border: \"1px solid var(--aurora-border-strong)\",\n  borderRadius: \"var(--aurora-radius-1)\",\n  boxShadow: \"var(--aurora-shadow-medium), var(--aurora-highlight-medium)\",\n}\n\nconst AudioPlayer = (\n    { ref,\n      title = \"Voice Response\",\n      duration = \"00:42\",\n      progress = 0,\n      status = \"idle\",\n      badge = false,\n      download = false,\n      variant = \"default\",\n      speed = \"1×\",\n      className,\n      style,\n      ...props\n    }: AudioPlayerProps & { ref?: React.Ref<HTMLDivElement> }\n  ) => {\n    const totalSeconds = parseSeconds(duration)\n    const current = formatSeconds(totalSeconds * Math.min(1, Math.max(0, progress)))\n    const synthesizing = status === \"synthesizing\"\n\n    // ── Compact: single-row pill ────────────────────────────────────────────\n    if (variant === \"compact\") {\n      return (\n        <div\n          ref={ref}\n          className={[\"flex items-center gap-3 px-3 py-2\", className].filter(Boolean).join(\" \")}\n          style={{ ...cardStyle, borderRadius: 999, ...style }}\n          {...props}\n        >\n          <Button\n            type=\"button\"\n            variant=\"aurora\"\n            size=\"icon\"\n            shape=\"pill\"\n            aria-label={`Play ${title}`}\n          >\n            <Play className=\"size-3.5\" aria-hidden style={{ marginLeft: 1 }} />\n          </Button>\n          <Waveform bars={WAVE_COMPACT} progress={progress} height={22} />\n          <span\n            className=\"aurora-text-control shrink-0 tabular-nums\"\n            style={{ color: \"var(--aurora-text-muted)\" }}\n          >\n            {duration}\n          </span>\n        </div>\n      )\n    }\n\n    // ── Default: full card ──────────────────────────────────────────────────\n    return (\n      <div\n        ref={ref}\n        className={[\"grid gap-3 p-4\", className].filter(Boolean).join(\" \")}\n        style={{ ...cardStyle, ...style }}\n        {...props}\n      >\n        <div className=\"grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3\">\n          {synthesizing ? (\n            <span\n              className=\"flex items-center justify-center\"\n              style={{\n                width: 44,\n                height: 44,\n                borderRadius: 999,\n                border: \"1px solid color-mix(in srgb, var(--aurora-accent-primary) 42%, var(--aurora-border-strong))\",\n                background:\n                  \"linear-gradient(180deg, color-mix(in srgb, var(--aurora-accent-primary) 9%, transparent), transparent 58%), var(--aurora-control-surface)\",\n              }}\n              aria-label=\"Synthesizing\"\n            >\n              <Spinner size=\"default\" tone=\"cyan\" />\n            </span>\n          ) : (\n            <Button\n              type=\"button\"\n              variant=\"aurora\"\n              size=\"icon\"\n              shape=\"pill\"\n              aria-label={`Play ${title}`}\n              style={{ width: 44, height: 44 }}\n            >\n              <Play className=\"size-[18px]\" aria-hidden style={{ marginLeft: 2 }} />\n            </Button>\n          )}\n          <div className=\"min-w-0\">\n            <div className=\"flex items-center gap-2.5\">\n              <span\n                className=\"truncate aurora-text-control\"\n                style={{ color: \"var(--aurora-text-primary)\", fontWeight: 700 }}\n              >\n                {title}\n              </span>\n              {badge ? (\n                <span\n                  className=\"inline-flex shrink-0 items-center gap-1 aurora-text-label uppercase\"\n                  style={{\n                    padding: \"3px 8px\",\n                    borderRadius: 999,\n                    color: AI_ORANGE,\n                    border: \"1px solid var(--axon-orange-border)\",\n                    background: \"var(--axon-orange-surface)\",\n                    fontFamily: \"var(--aurora-font-sans)\",\n                    letterSpacing: \"var(--aurora-letter-label)\",\n                    textTransform: \"none\",\n                  }}\n                >\n                  <Sparkles className=\"size-3\" aria-hidden />\n                  AI Voice\n                </span>\n              ) : null}\n            </div>\n            <div style={{ marginTop: 10 }}>\n              {synthesizing ? (\n                <span\n                  className=\"block text-right aurora-text-label\"\n                  style={{\n                    color: \"var(--aurora-text-muted)\",\n                    fontFamily: \"var(--aurora-font-sans)\",\n                    letterSpacing: \"var(--aurora-letter-eyebrow)\",\n                    textTransform: \"uppercase\",\n                  }}\n                >\n                  Synthesizing\n                </span>\n              ) : (\n                <Waveform bars={WAVE_FULL} progress={progress} height={36} />\n              )}\n            </div>\n          </div>\n        </div>\n\n        <Separator />\n\n        <div className=\"flex items-center justify-between gap-3\">\n          <span className=\"aurora-text-control tabular-nums\">\n            <span style={{ color: \"var(--aurora-text-primary)\", fontWeight: 600 }}>{current}</span>\n            <span style={{ color: \"var(--aurora-text-muted)\", margin: \"0 7px\" }}>/</span>\n            <span style={{ color: \"var(--aurora-text-muted)\" }}>{duration}</span>\n          </span>\n          <span className=\"flex items-center gap-3\">\n            <span\n              className=\"aurora-text-control tabular-nums\"\n              style={{ color: \"var(--aurora-text-primary)\", fontWeight: 700 }}\n            >\n              {speed}\n            </span>\n            {download ? (\n              <Button\n                type=\"button\"\n                variant=\"neutral\"\n                size=\"icon\"\n                aria-label={`Download ${title}`}\n              >\n                <Download className=\"size-4\" aria-hidden />\n              </Button>\n            ) : null}\n          </span>\n        </div>\n      </div>\n    )\n  }\nAudioPlayer.displayName = \"AudioPlayer\"\n\nexport { AudioPlayer }\nexport default AudioPlayer\n",
      "type": "registry:component",
      "target": "@components/aurora/ai/audio-player.tsx"
    }
  ],
  "meta": {
    "namespace": "@aurora",
    "style": "aurora",
    "family": "block",
    "requiresTokens": true,
    "sourcePath": "registry/aurora/blocks/ai/elements/audio-player.tsx",
    "installTarget": "@components/aurora/ai/audio-player.tsx",
    "taxonomy": "ai"
  },
  "docs": "Aurora block that expects `aurora-tokens` to be installed first.\n\nDefault install target: `@components/aurora/ai/audio-player.tsx`.\n\nRegistry dependencies: `aurora-tokens`, `aurora-ai-elements`.",
  "categories": [
    "aurora",
    "block",
    "ai"
  ],
  "type": "registry:block"
}