{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio",
  "type": "registry:block",
  "title": "Audio",
  "description": "Audio playback with artwork and metadata.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "button",
    "slider"
  ],
  "files": [
    {
      "path": "components/tool-ui/audio/_adapter.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/audio/_adapter.tsx",
      "content": "/**\n * Adapter: UI and utility re-exports for copy-standalone portability.\n */\n\"use client\";\n\nexport { cn } from \"@/lib/utils\";\nexport { Button } from \"@/components/ui/button\";\nexport { Slider } from \"@/components/ui/slider\";\n"
    },
    {
      "path": "components/tool-ui/audio/audio.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/audio/audio.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Pause, Play } from \"lucide-react\";\nimport { cn, Button, Slider } from \"./_adapter\";\n\nimport { AudioProvider, useAudio } from \"./context\";\nimport type { SerializableAudio, AudioVariant } from \"./schema\";\n\nconst FALLBACK_LOCALE = \"en-US\";\n\nfunction formatTime(seconds: number): string {\n  if (!Number.isFinite(seconds)) return \"0:00\";\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n}\n\nexport interface AudioProps extends SerializableAudio {\n  variant?: AudioVariant;\n  className?: string;\n  onMediaEvent?: (type: \"play\" | \"pause\" | \"mute\" | \"unmute\") => void;\n}\n\nexport function Audio(props: AudioProps) {\n  return (\n    <AudioProvider>\n      <AudioInner {...props} />\n    </AudioProvider>\n  );\n}\n\ninterface PlayerControls {\n  isPlaying: boolean;\n  currentTime: number;\n  duration: number;\n  onPlayPause: () => void;\n  onSeek: (value: number[]) => void;\n  onSeekStart: () => void;\n  onSeekEnd: () => void;\n}\n\ninterface FullPlayerProps {\n  artwork?: string;\n  title?: string;\n  description?: string;\n  controls: PlayerControls;\n}\n\nfunction FullPlayer({\n  artwork,\n  title,\n  description,\n  controls,\n}: FullPlayerProps) {\n  return (\n    <div className=\"flex w-full flex-col\">\n      {artwork && (\n        <div className=\"bg-muted relative aspect-[4/3] w-full overflow-hidden\">\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"absolute inset-0 h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      <div className=\"flex flex-col gap-5 p-4\">\n        {(title || description) && (\n          <div className=\"space-y-0.5\">\n            {title && (\n              <div className=\"text-foreground line-clamp-2 font-semibold leading-snug\">\n                {title}\n              </div>\n            )}\n            {description && (\n              <div className=\"text-muted-foreground line-clamp-2 text-sm leading-snug\">\n                {description}\n              </div>\n            )}\n          </div>\n        )}\n        <div className=\"flex items-start gap-3\">\n          <div className=\"flex flex-1 flex-col gap-2\">\n            <Slider\n              value={[controls.currentTime]}\n              max={controls.duration || 100}\n              step={0.1}\n              onValueChange={controls.onSeek}\n              onPointerDown={controls.onSeekStart}\n              onPointerUp={controls.onSeekEnd}\n              className=\"cursor-pointer [&_[data-slot=range]]:bg-foreground [&_[data-slot=thumb]]:size-3 [&_[data-slot=thumb]]:border-2 [&_[data-slot=thumb]]:border-background [&_[data-slot=thumb]]:bg-foreground\"\n              aria-label=\"Audio progress\"\n            />\n            <div className=\"text-muted-foreground flex items-center justify-between text-xs tabular-nums\">\n              <span>{formatTime(controls.currentTime)}</span>\n              <span>{formatTime(controls.duration)}</span>\n            </div>\n          </div>\n          <Button\n            variant=\"default\"\n            size=\"icon\"\n            onClick={controls.onPlayPause}\n            className=\"-mt-4 size-10 shrink-0 rounded-full\"\n            aria-label={controls.isPlaying ? \"Pause\" : \"Play\"}\n          >\n            {controls.isPlaying ? (\n              <Pause className=\"size-4\" fill=\"currentColor\" />\n            ) : (\n              <Play className=\"size-4 ml-0.5\" fill=\"currentColor\" />\n            )}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface CompactPlayerProps {\n  artwork?: string;\n  title?: string;\n  description?: string;\n  controls: PlayerControls;\n}\n\nfunction CompactPlayer({\n  artwork,\n  title,\n  description,\n  controls,\n}: CompactPlayerProps) {\n  const progress =\n    controls.duration > 0\n      ? (controls.currentTime / controls.duration) * 100\n      : 0;\n\n  return (\n    <div className=\"relative flex w-full items-center gap-3 overflow-hidden p-3\">\n      {artwork && (\n        <>\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute -left-1/4 top-1/2 h-[200%] w-auto -translate-y-1/2 object-cover opacity-40 blur-2xl saturate-150\"\n          />\n          <div className=\"from-card/60 to-card/90 pointer-events-none absolute inset-0 bg-gradient-to-r\" />\n        </>\n      )}\n      {artwork && (\n        <div className=\"ring-background/20 relative size-12 shrink-0 overflow-hidden rounded-lg shadow-lg ring-1\">\n          <img\n            src={artwork}\n            alt=\"\"\n            aria-hidden=\"true\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"absolute inset-0 h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      <div className=\"relative flex min-w-0 flex-1 flex-col justify-center\">\n        {title && (\n          <div className=\"text-foreground truncate text-sm font-semibold leading-tight\">\n            {title}\n          </div>\n        )}\n        {description && (\n          <div className=\"text-muted-foreground mt-0.5 truncate text-xs leading-tight\">\n            {description}\n          </div>\n        )}\n        {controls.duration > 0 && (\n          <div className=\"mt-1 flex items-center gap-2\">\n            <div className=\"bg-foreground/20 relative h-1 flex-1 overflow-hidden rounded-full\">\n              <div\n                className=\"bg-foreground absolute inset-y-0 left-0 rounded-full transition-all duration-150\"\n                style={{ width: `${progress}%` }}\n              />\n            </div>\n            <span className=\"text-muted-foreground text-xs tabular-nums\">\n              {formatTime(controls.currentTime)}\n            </span>\n          </div>\n        )}\n      </div>\n      <Button\n        variant=\"default\"\n        size=\"icon\"\n        onClick={controls.onPlayPause}\n        className=\"relative size-10 shrink-0 rounded-full shadow-md\"\n        aria-label={controls.isPlaying ? \"Pause\" : \"Play\"}\n      >\n        {controls.isPlaying ? (\n          <Pause className=\"size-4\" fill=\"currentColor\" />\n        ) : (\n          <Play className=\"size-4 ml-0.5\" fill=\"currentColor\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\nfunction AudioInner(props: AudioProps) {\n  const { variant = \"full\", className, onMediaEvent, ...serializable } = props;\n\n  const {\n    id,\n    src,\n    title,\n    description,\n    artwork,\n    locale: providedLocale,\n  } = serializable;\n\n  const locale = providedLocale ?? FALLBACK_LOCALE;\n\n  const { state, setState, setAudioElement } = useAudio();\n  const audioRef = React.useRef<HTMLAudioElement | null>(null);\n  const [currentTime, setCurrentTime] = React.useState(0);\n  const [duration, setDuration] = React.useState(0);\n  const [isSeeking, setIsSeeking] = React.useState(false);\n\n  React.useEffect(() => {\n    setAudioElement(audioRef.current);\n    return () => setAudioElement(null);\n  }, [setAudioElement]);\n\n  React.useEffect(() => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    if (state.playing && audio.paused) {\n      void audio.play().catch(() => undefined);\n    } else if (!state.playing && !audio.paused) {\n      audio.pause();\n    }\n  }, [state.playing]);\n\n  const handlePlayPause = () => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    if (audio.paused) {\n      void audio.play().catch(() => undefined);\n    } else {\n      audio.pause();\n    }\n  };\n\n  const handleSeek = (value: number[]) => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    const newTime = value[0];\n    audio.currentTime = newTime;\n    setCurrentTime(newTime);\n  };\n\n  const handleSeekStart = () => {\n    setIsSeeking(true);\n  };\n\n  const handleSeekEnd = () => {\n    setIsSeeking(false);\n  };\n\n  const controls: PlayerControls = {\n    isPlaying: state.playing,\n    currentTime,\n    duration,\n    onPlayPause: handlePlayPause,\n    onSeek: handleSeek,\n    onSeekStart: handleSeekStart,\n    onSeekEnd: handleSeekEnd,\n  };\n\n  const isCompact = variant === \"compact\";\n\n  return (\n    <article\n      className={cn(\n        \"@container/actions relative w-full\",\n        isCompact ? \"min-w-72 max-w-md\" : \"min-w-52 max-w-sm\",\n        className,\n      )}\n      lang={locale}\n      data-tool-ui-id={id}\n      data-slot=\"audio\"\n    >\n      <div\n        className={cn(\n          \"group @container relative isolate flex w-full min-w-0 flex-col overflow-hidden\",\n          \"border-border bg-card border text-sm shadow-xs\",\n          \"rounded-xl\",\n        )}\n      >\n        {isCompact ? (\n          <CompactPlayer\n            artwork={artwork}\n            title={title}\n            description={description}\n            controls={controls}\n          />\n        ) : (\n          <FullPlayer\n            artwork={artwork}\n            title={title}\n            description={description}\n            controls={controls}\n          />\n        )}\n\n        <audio\n          ref={audioRef}\n          src={src}\n          preload=\"metadata\"\n          className=\"hidden\"\n          onPlay={() => {\n            setState({ playing: true });\n            onMediaEvent?.(\"play\");\n          }}\n          onPause={() => {\n            setState({ playing: false });\n            onMediaEvent?.(\"pause\");\n          }}\n          onTimeUpdate={(event) => {\n            if (!isSeeking) {\n              setCurrentTime(event.currentTarget.currentTime);\n            }\n          }}\n          onLoadedMetadata={(event) => {\n            setDuration(event.currentTarget.duration);\n          }}\n          onDurationChange={(event) => {\n            setDuration(event.currentTarget.duration);\n          }}\n        />\n      </div>\n    </article>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/audio/context.tsx",
      "type": "registry:component",
      "target": "components/tool-ui/audio/context.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface AudioPlaybackState {\n  playing: boolean;\n  muted: boolean;\n}\n\nexport interface AudioContextValue {\n  state: AudioPlaybackState;\n  setState: (patch: Partial<AudioPlaybackState>) => void;\n  audioElement: HTMLAudioElement | null;\n  setAudioElement: (node: HTMLAudioElement | null) => void;\n}\n\nconst AudioContext = React.createContext<AudioContextValue | null>(null);\n\nexport function useAudio() {\n  const ctx = React.use(AudioContext);\n  if (!ctx) {\n    throw new Error(\"useAudio must be used within an <AudioProvider />\");\n  }\n  return ctx;\n}\n\nexport interface AudioProviderProps {\n  children: React.ReactNode;\n  defaultState?: Partial<AudioPlaybackState>;\n}\n\nexport function AudioProvider({ children, defaultState }: AudioProviderProps) {\n  const [state, setStateInternal] = React.useState<AudioPlaybackState>({\n    playing: defaultState?.playing ?? false,\n    muted: defaultState?.muted ?? false,\n  });\n\n  const [audioElement, setAudioElement] =\n    React.useState<HTMLAudioElement | null>(null);\n\n  const setState = React.useCallback((patch: Partial<AudioPlaybackState>) => {\n    setStateInternal((prev) => ({ ...prev, ...patch }));\n  }, []);\n\n  const value = React.useMemo(\n    () => ({ state, setState, audioElement, setAudioElement }),\n    [state, setState, audioElement],\n  );\n\n  return (\n    <AudioContext.Provider value={value}>{children}</AudioContext.Provider>\n  );\n}\n"
    },
    {
      "path": "components/tool-ui/audio/index.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/audio/index.ts",
      "content": "export { Audio } from \"./audio\";\nexport type { AudioProps } from \"./audio\";\nexport { AudioProvider, useAudio } from \"./context\";\nexport type { AudioPlaybackState, AudioContextValue } from \"./context\";\nexport type { SerializableAudio, Source, AudioVariant } from \"./schema\";\n"
    },
    {
      "path": "components/tool-ui/audio/README.md",
      "type": "registry:file",
      "target": "components/tool-ui/audio/README.md",
      "content": "# Audio\n\nImplementation for the \"audio\" Tool UI surface.\n\n## Files\n\n- public exports: components/tool-ui/audio/index.ts\n- serializable schema + parse helpers: components/tool-ui/audio/schema.ts\n\n## Companion assets\n\n- Docs page: app/docs/audio/content.mdx\n- Preset payload: lib/presets/audio.ts\n\n## Quick check\n\nRun this after edits:\n\npnpm test\n"
    },
    {
      "path": "components/tool-ui/audio/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/audio/schema.ts",
      "content": "import { z } from \"zod\";\nimport { defineToolUiContract } from \"../shared/contract\";\nimport {\n  ToolUIIdSchema,\n  ToolUIReceiptSchema,\n  ToolUIRoleSchema,\n} from \"../shared/schema\";\n\nexport const SourceSchema = z.object({\n  label: z.string(),\n  iconUrl: z.url().optional(),\n  url: z.url().optional(),\n});\n\nexport type Source = z.infer<typeof SourceSchema>;\n\nexport const SerializableAudioSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n  assetId: z.string(),\n  src: z.url(),\n  title: z.string().optional(),\n  description: z.string().optional(),\n  artwork: z.url().optional(),\n  durationMs: z.number().int().positive().optional(),\n  fileSizeBytes: z.number().int().positive().optional(),\n  createdAt: z.string().datetime().optional(),\n  locale: z.string().optional(),\n  source: SourceSchema.optional(),\n});\n\nexport type SerializableAudio = z.infer<typeof SerializableAudioSchema>;\n\nconst SerializableAudioSchemaContract = defineToolUiContract(\n  \"Audio\",\n  SerializableAudioSchema,\n);\n\nexport const parseSerializableAudio: (input: unknown) => SerializableAudio =\n  SerializableAudioSchemaContract.parse;\n\nexport const safeParseSerializableAudio: (\n  input: unknown,\n) => SerializableAudio | null = SerializableAudioSchemaContract.safeParse;\nexport type AudioVariant = \"full\" | \"compact\";\n"
    },
    {
      "path": "components/tool-ui/shared/contract.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/contract.ts",
      "content": "import { z } from \"zod\";\nimport { parseWithSchema, safeParseWithSchema } from \"./parse\";\n\nexport interface ToolUiContract<T> {\n  schema: z.ZodType<T>;\n  parse: (input: unknown) => T;\n  safeParse: (input: unknown) => T | null;\n}\n\nexport function defineToolUiContract<T>(\n  componentName: string,\n  schema: z.ZodType<T>,\n): ToolUiContract<T> {\n  return {\n    schema,\n    parse: (input: unknown) => parseWithSchema(schema, input, componentName),\n    safeParse: (input: unknown) => safeParseWithSchema(schema, input),\n  };\n}\n"
    },
    {
      "path": "components/tool-ui/shared/parse.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/parse.ts",
      "content": "import { z } from \"zod\";\n\nfunction formatZodPath(path: Array<string | number | symbol>): string {\n  if (path.length === 0) return \"root\";\n  return path\n    .map((segment) =>\n      typeof segment === \"number\" ? `[${segment}]` : String(segment),\n    )\n    .join(\".\");\n}\n\n/**\n * Format Zod errors into a compact `path: message` string.\n */\nexport function formatZodError(error: z.ZodError): string {\n  const parts = error.issues.map((issue) => {\n    const path = formatZodPath(issue.path);\n    return `${path}: ${issue.message}`;\n  });\n\n  return Array.from(new Set(parts)).join(\"; \");\n}\n\n/**\n * Parse unknown input and throw a readable error.\n */\nexport function parseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n  name: string,\n): T {\n  const res = schema.safeParse(input);\n  if (!res.success) {\n    throw new Error(`Invalid ${name} payload: ${formatZodError(res.error)}`);\n  }\n  return res.data;\n}\n\n/**\n * Parse unknown input, returning `null` instead of throwing on failure.\n *\n * Use this in assistant-ui `render` functions where `args` stream in\n * incrementally and may be incomplete until the tool call finishes.\n */\nexport function safeParseWithSchema<T>(\n  schema: z.ZodType<T>,\n  input: unknown,\n): T | null {\n  const res = schema.safeParse(input);\n  return res.success ? res.data : null;\n}\n"
    },
    {
      "path": "components/tool-ui/shared/schema.ts",
      "type": "registry:lib",
      "target": "components/tool-ui/shared/schema.ts",
      "content": "import { z } from \"zod\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Tool UI conventions:\n * - Serializable schemas are JSON-safe (no callbacks/ReactNode/`className`).\n * - Schema: `SerializableXSchema`\n * - Parser: `parseSerializableX(input: unknown)` (throws on invalid)\n * - Safe parser: `safeParseSerializableX(input: unknown)` (returns `null` on invalid)\n * - Actions: `LocalActions` for non-receipt actions and `DecisionActions` for consequential actions\n * - Root attrs: `data-tool-ui-id` + `data-slot`\n */\n\n/**\n * Schema for tool UI identity.\n *\n * Every tool UI should have a unique identifier that:\n * - Is stable across re-renders\n * - Is meaningful (not auto-generated)\n * - Is unique within the conversation\n *\n * Format recommendation: `{component-type}-{semantic-identifier}`\n * Examples: \"data-table-expenses-q3\", \"option-list-deploy-target\"\n */\nexport const ToolUIIdSchema = z.string().min(1);\n\nexport type ToolUIId = z.infer<typeof ToolUIIdSchema>;\n\n/**\n * Primary role of a Tool UI surface in a chat context.\n */\nexport const ToolUIRoleSchema = z.enum([\n  \"information\",\n  \"decision\",\n  \"control\",\n  \"state\",\n  \"composite\",\n]);\n\nexport type ToolUIRole = z.infer<typeof ToolUIRoleSchema>;\n\nexport const ToolUIReceiptOutcomeSchema = z.enum([\n  \"success\",\n  \"partial\",\n  \"failed\",\n  \"cancelled\",\n]);\n\nexport type ToolUIReceiptOutcome = z.infer<typeof ToolUIReceiptOutcomeSchema>;\n\n/**\n * Optional receipt metadata: a durable summary of an outcome.\n */\nexport const ToolUIReceiptSchema = z.object({\n  outcome: ToolUIReceiptOutcomeSchema,\n  summary: z.string().min(1),\n  identifiers: z.record(z.string(), z.string()).optional(),\n  at: z.string().datetime(),\n});\n\nexport type ToolUIReceipt = z.infer<typeof ToolUIReceiptSchema>;\n\n/**\n * Base schema for Tool UI payloads (id + optional role/receipt).\n */\nexport const ToolUISurfaceSchema = z.object({\n  id: ToolUIIdSchema,\n  role: ToolUIRoleSchema.optional(),\n  receipt: ToolUIReceiptSchema.optional(),\n});\n\nexport type ToolUISurface = z.infer<typeof ToolUISurfaceSchema>;\n\nexport const ActionSchema = z.object({\n  id: z.string().min(1),\n  label: z.string().min(1),\n  /**\n   * Canonical narration the assistant can use after this action is taken.\n   *\n   * Example: \"I exported the table as CSV.\" / \"I opened the link in a new tab.\"\n   */\n  sentence: z.string().optional(),\n  confirmLabel: z.string().optional(),\n  variant: z\n    .enum([\"default\", \"destructive\", \"secondary\", \"ghost\", \"outline\"])\n    .optional(),\n  icon: z.custom<ReactNode>().optional(),\n  loading: z.boolean().optional(),\n  disabled: z.boolean().optional(),\n  shortcut: z.string().optional(),\n});\n\nexport type Action = z.infer<typeof ActionSchema>;\nexport type LocalAction = Action;\nexport type DecisionAction = Action;\n\nexport const DecisionResultSchema = z.object({\n  kind: z.literal(\"decision\"),\n  version: z.literal(1),\n  decisionId: z.string().min(1),\n  actionId: z.string().min(1),\n  actionLabel: z.string().min(1),\n  at: z.string().datetime(),\n  payload: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type DecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DecisionResultSchema>, \"payload\"> & {\n  payload?: TPayload;\n};\n\nexport function createDecisionResult<\n  TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(args: {\n  decisionId: string;\n  action: { id: string; label: string };\n  payload?: TPayload;\n}): DecisionResult<TPayload> {\n  return {\n    kind: \"decision\",\n    version: 1,\n    decisionId: args.decisionId,\n    actionId: args.action.id,\n    actionLabel: args.action.label,\n    at: new Date().toISOString(),\n    payload: args.payload,\n  };\n}\n\nexport const ActionButtonsPropsSchema = z.object({\n  actions: z.array(ActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n  className: z.string().optional(),\n});\n\nexport const SerializableActionSchema = ActionSchema.omit({ icon: true });\nexport const SerializableActionsSchema = ActionButtonsPropsSchema.extend({\n  actions: z.array(SerializableActionSchema),\n}).omit({ className: true });\n\nexport interface ActionsConfig {\n  items: Action[];\n  align?: \"left\" | \"center\" | \"right\";\n  confirmTimeout?: number;\n}\n\nexport const SerializableActionsConfigSchema = z.object({\n  items: z.array(SerializableActionSchema).min(1),\n  align: z.enum([\"left\", \"center\", \"right\"]).optional(),\n  confirmTimeout: z.number().positive().optional(),\n});\n\nexport type SerializableActionsConfig = z.infer<\n  typeof SerializableActionsConfigSchema\n>;\n\nexport type SerializableAction = z.infer<typeof SerializableActionSchema>;\n"
    }
  ]
}
