{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-form-assist",
  "title": "AI Form Assist",
  "description": "Model-filled form fields — a sparkle per row, ghost-rendered values with a confidence tint, source popovers, a staggered fill-all wave, and per-field accept, dismiss and undo.",
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/ai-form-assist.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowRight,\n  Check,\n  CheckCheck,\n  CornerDownLeft,\n  ExternalLink,\n  FileText,\n  Loader2,\n  Sparkles,\n  TriangleAlert,\n  Undo2,\n  X,\n} from \"lucide-react\"\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------------------------------------------------- *\n * Keyframes\n *\n * Shipped with the component through a React 19 hoisted <style> — no Tailwind\n * config edit, and duplicate instances dedupe by href.\n * -------------------------------------------------------------------------- */\nconst KEYFRAMES = `@keyframes afa-rise{from{opacity:0;transform:translateY(-0.2rem)}to{opacity:1;transform:translateY(0)}}@keyframes afa-sweep{from{transform:translateX(-100%)}to{transform:translateX(300%)}}`\n\n/* -------------------------------------------------------------------------- *\n * Types\n * -------------------------------------------------------------------------- */\n\n/** Where a suggested value was read from — the payload behind the \"from …\" chip. */\nexport interface AiFormAssistSource {\n  /** Chip text. Keep it short: \"Uploaded invoice\", \"CRM · Acme Ltd\". */\n  label: string\n  /** Second line in the popover: page, row, record id, timestamp. */\n  detail?: string\n  /** The raw span the value was extracted from; rendered verbatim, monospaced. */\n  excerpt?: string\n  /** Optional link to the document. http(s) and mailto only; anything else is dropped. */\n  href?: string\n}\n\nexport interface AiFormAssistSuggestion {\n  /** The value Accept writes into the field. Trimmed; an empty one counts as \"no answer\". */\n  value: string\n  /** Model confidence in 0..1. Drives the tint and the meter; omit it and both disappear. */\n  confidence?: number\n  source?: AiFormAssistSource\n  /** One short line of reasoning shown under the value. */\n  note?: string\n}\n\n/** What `onSuggest` may resolve with — a bare string is shorthand for `{ value }`. */\nexport type AiFormAssistAnswer = AiFormAssistSuggestion | string | null | undefined\n\n/** Whether a run came from a field's own button or from the fill-all wave. */\nexport type AiFormAssistTrigger = \"field\" | \"fill-all\"\n\n/** What moved a value: the user typing, an accepted suggestion, a bulk accept, an undo. */\nexport type AiFormAssistCause = \"input\" | \"accept\" | \"accept-all\" | \"undo\"\n\nexport interface AiFormAssistField {\n  /** Stable key. Also the control's `name`, so a plain form post works. */\n  name: string\n  label: string\n  /** Help text beside the label; joined into the control's aria-describedby. */\n  hint?: string\n  placeholder?: string\n  /** Native input type. Ignored when `multiline`. Default \"text\". */\n  type?: React.HTMLInputTypeAttribute\n  /** Render a textarea instead of an input (addresses, notes). */\n  multiline?: boolean\n  /** Visible rows when `multiline`. Default 3. */\n  rows?: number\n  required?: boolean\n  autoComplete?: string\n  inputMode?: \"none\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\" | \"search\"\n  /**\n   * `false` renders a plain field in the same rhythm with no assistant\n   * affordance at all — for values a model must never guess (card number, PIN).\n   * Default true.\n   */\n  assist?: boolean\n  /** Fill-all skips this field; its own button still works. Default false. */\n  skipFill?: boolean\n}\n\nexport interface AiFormAssistAcceptDetail {\n  field: AiFormAssistField\n  /** What is now in the field. */\n  value: string\n  /** What was in it before. */\n  previous: string\n  confidence: number | null\n  source: AiFormAssistSource | null\n  /** True when it came from Accept all. */\n  bulk: boolean\n}\n\nexport interface AiFormAssistRejectDetail {\n  field: AiFormAssistField\n  /** The refused value; null when an in-flight run was cancelled before answering. */\n  value: string | null\n  reason: \"reject\" | \"cancel\"\n  bulk: boolean\n}\n\nexport interface AiFormAssistUndoDetail {\n  field: AiFormAssistField\n  /** The value that came back. */\n  restored: string\n  /** The suggestion that was thrown away. */\n  discarded: string\n}\n\n/** Imperative driver, for a toolbar / shortcut / wizard step that lives elsewhere. */\nexport interface AiFormAssistHandle {\n  /** Ask for one field. Same guards as its own button; false means refused. */\n  suggest: (name: string) => boolean\n  /** Ask for every fillable field, staggered. */\n  fillAll: () => void\n  /** Cancel the wave: pending timers dropped, in-flight requests aborted. */\n  stop: () => void\n  /** Apply the pending suggestion of one field (one-shot, same lock as the button). */\n  accept: (name: string) => void\n  /** Cancel a request or refuse a suggestion for one field. */\n  dismiss: (name: string) => void\n  focus: (name: string) => void\n}\n\nexport interface AiFormAssistProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\" | \"defaultValue\" | \"onChange\"> {\n  /** The rows, in render order. */\n  fields: AiFormAssistField[]\n  /**\n   * The injected model call. Receives the field, an AbortSignal that fires on\n   * cancel and on unmount, every current value (for cross-field context) and\n   * what triggered the run. Resolve with a suggestion, a bare string, or null\n   * for \"nothing to offer\"; reject to show a retryable per-field error.\n   */\n  onSuggest: (\n    field: AiFormAssistField,\n    ctx: { signal: AbortSignal; values: Record<string, string>; trigger: AiFormAssistTrigger },\n  ) => AiFormAssistAnswer | Promise<AiFormAssistAnswer>\n  /** Controlled values keyed by field name. Omit and the component owns them. */\n  values?: Record<string, string>\n  defaultValues?: Record<string, string>\n  /**\n   * Every value change. `names` lists what moved — Accept all sends ONE record\n   * with every accepted field in it, never N sequential writes.\n   */\n  onValuesChange?: (next: Record<string, string>, meta: { names: string[]; cause: AiFormAssistCause }) => void\n  /** Fires once per accepted suggestion; the one-shot lock guarantees it. */\n  onAccept?: (detail: AiFormAssistAcceptDetail) => void\n  onReject?: (detail: AiFormAssistRejectDetail) => void\n  onUndo?: (detail: AiFormAssistUndoDetail) => void\n  heading?: string\n  description?: string\n  /** Which fields the wave asks about: only the blank ones (default) or all of them. */\n  fillMode?: \"empty\" | \"all\"\n  /** Gap between consecutive fill-all requests, ms. Default 120. */\n  staggerMs?: number\n  /** The bulk bar appears at this many pending suggestions. Default 2. */\n  bulkActionsFrom?: number\n  /** Confidence cut-offs in 0..1 for the tint buckets. Default { high: 0.8, medium: 0.5 }. */\n  confidenceThresholds?: { high: number; medium: number }\n  /** Show the per-suggestion confidence meter. Default true. */\n  showConfidence?: boolean\n  /** Inert but readable: controls become readOnly + aria-disabled, never natively disabled. */\n  disabled?: boolean\n  /** `status` (default) keeps one polite live region; `off` renders the same line inert. */\n  announce?: \"status\" | \"off\"\n  fillAllLabel?: string\n  cancelLabel?: string\n  acceptLabel?: string\n  rejectLabel?: string\n  undoLabel?: string\n  retryLabel?: string\n}\n\n/* -------------------------------------------------------------------------- *\n * Internal state\n * -------------------------------------------------------------------------- */\n\ntype FieldPhase = \"idle\" | \"requesting\" | \"suggested\" | \"error\"\n\ninterface PendingSuggestion {\n  /** Bumped per arrival; the one-shot accept lock is keyed on it. */\n  id: number\n  /** Exactly the value the field held when the run started — what \"stale\" is measured against. */\n  baseline: string\n  value: string\n  confidence: number | null\n  source: AiFormAssistSource | null\n  note: string | null\n  trigger: AiFormAssistTrigger\n}\n\ninterface FieldState {\n  phase: FieldPhase\n  pending: PendingSuggestion | null\n  message: string | null\n  tone: \"info\" | \"error\" | null\n  undo: { previous: string; applied: string } | null\n}\n\nconst IDLE_FIELD: FieldState = { message: null, pending: null, phase: \"idle\", tone: null, undo: null }\n\ninterface ActiveRun {\n  controller: AbortController\n  runId: number\n  /** Set when the run belongs to a fill-all wave, so stopping the wave finds it. */\n  fillId: number | undefined\n}\n\ninterface Notice {\n  tone: \"info\" | \"error\"\n  text: string\n}\n\ntype Bucket = \"high\" | \"medium\" | \"low\" | \"unknown\"\n\n/* -------------------------------------------------------------------------- *\n * Helpers\n * -------------------------------------------------------------------------- */\n\nfunction clamp01(value: number): number {\n  if (!Number.isFinite(value)) return 0\n  return Math.min(1, Math.max(0, value))\n}\n\n/** A bare string is shorthand for `{ value }`; an empty value means \"no answer\". */\nfunction normalizeAnswer(answer: AiFormAssistAnswer): Omit<PendingSuggestion, \"baseline\" | \"id\" | \"trigger\"> | null {\n  if (answer === null || answer === undefined) return null\n  const raw: AiFormAssistSuggestion = typeof answer === \"string\" ? { value: answer } : answer\n  const value = typeof raw.value === \"string\" ? raw.value.trim() : \"\"\n  if (!value) return null\n  return {\n    confidence: typeof raw.confidence === \"number\" ? clamp01(raw.confidence) : null,\n    note: raw.note?.trim() || null,\n    source: raw.source && raw.source.label ? raw.source : null,\n    value,\n  }\n}\n\nfunction confidenceBucket(confidence: number | null, thresholds: { high: number; medium: number }): Bucket {\n  if (confidence === null) return \"unknown\"\n  if (confidence >= thresholds.high) return \"high\"\n  if (confidence >= thresholds.medium) return \"medium\"\n  return \"low\"\n}\n\nfunction errorText(err: unknown, fallback: string): string {\n  if (err instanceof Error && err.message) return err.message\n  if (typeof err === \"string\" && err) return err\n  return fallback\n}\n\n/**\n * Source links usually come out of the same pipeline as the value, so they are\n * treated as untrusted: http(s) and mailto pass, every other explicit scheme is\n * dropped, and a protocol-relative URL (reads relative, is not) is dropped too.\n */\nfunction safeUrl(raw: string): string | null {\n  const url = raw.trim()\n  if (!url) return null\n  if (/^(?:https?:|mailto:)/i.test(url)) return url\n  if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return null\n  if (url.startsWith(\"//\")) return null\n  return url\n}\n\n/** setSelectionRange throws InvalidStateError on email / number / date inputs. */\nconst SELECTABLE_TYPES = new Set([\"\", \"password\", \"search\", \"tel\", \"text\", \"url\"])\n\nfunction caretToEnd(node: HTMLInputElement | HTMLTextAreaElement) {\n  node.focus()\n  if (node instanceof HTMLInputElement && !SELECTABLE_TYPES.has(node.type)) return\n  const end = node.value.length\n  node.setSelectionRange(end, end)\n}\n\n/* -------------------------------------------------------------------------- *\n * Confidence meter\n * -------------------------------------------------------------------------- */\n\nconst BUCKET_WORD: Record<Bucket, string> = {\n  high: \"high confidence\",\n  low: \"low confidence\",\n  medium: \"medium confidence\",\n  unknown: \"confidence unknown\",\n}\n\nfunction ConfidenceMeter({ bucket, confidence }: { bucket: Bucket; confidence: number }) {\n  const pct = Math.round(confidence * 100)\n  return (\n    <span className=\"inline-flex shrink-0 items-center gap-1.5\">\n      <span aria-hidden=\"true\" className=\"relative h-1 w-8 overflow-hidden rounded-full bg-muted-foreground/25\">\n        <span\n          className={cn(\n            \"absolute inset-y-0 left-0 rounded-full\",\n            bucket === \"high\" ? \"bg-primary\" : \"bg-muted-foreground\",\n          )}\n          style={{ width: `${pct}%` }}\n        />\n      </span>\n      <span className=\"text-[0.6875rem] tabular-nums text-muted-foreground\">\n        <span className=\"sr-only\">{BUCKET_WORD[bucket]}, </span>\n        {pct}%\n      </span>\n    </span>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Source hint\n *\n * A popover, not a tooltip: the panel holds an excerpt worth selecting and a\n * link worth clicking, and it has to be reachable on a touch screen.\n * -------------------------------------------------------------------------- */\n\nfunction SourceHint({ fieldLabel, source }: { fieldLabel: string; source: AiFormAssistSource }) {\n  const href = source.href ? safeUrl(source.href) : null\n  return (\n    <PopoverPrimitive.Root>\n      <PopoverPrimitive.Trigger asChild>\n        <button\n          className={cn(\n            \"inline-flex max-w-full min-w-0 items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] text-muted-foreground\",\n            \"transition-colors hover:bg-muted hover:text-foreground motion-reduce:transition-none\",\n            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\",\n            \"data-[state=open]:bg-muted data-[state=open]:text-foreground\",\n          )}\n          type=\"button\"\n        >\n          <FileText aria-hidden=\"true\" className=\"size-3 shrink-0\" />\n          <span className=\"sr-only\">Source of the suggestion for {fieldLabel}: </span>\n          <span className=\"truncate\">{source.label}</span>\n        </button>\n      </PopoverPrimitive.Trigger>\n      {/* Portalled: a row can sit inside a scroll container with overflow-hidden,\n          which would clip an in-place panel. */}\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Content\n          align=\"start\"\n          className={cn(\n            \"z-50 w-64 max-w-[calc(100vw-2rem)] rounded-lg border bg-popover p-3 text-popover-foreground shadow-md outline-none\",\n            \"motion-safe:data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n            \"motion-safe:data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n            \"motion-reduce:animate-none\",\n          )}\n          collisionPadding={8}\n          side=\"bottom\"\n          sideOffset={6}\n        >\n          <p className=\"text-xs font-medium\">{source.label}</p>\n          {source.detail ? <p className=\"mt-0.5 text-[0.6875rem] text-muted-foreground\">{source.detail}</p> : null}\n          {source.excerpt ? (\n            <p className=\"mt-2 border-l-2 border-primary/40 bg-muted py-1 pr-1.5 pl-2 font-mono text-[0.6875rem] leading-relaxed whitespace-pre-line text-muted-foreground wrap-anywhere\">\n              {source.excerpt}\n            </p>\n          ) : null}\n          {href ? (\n            <a\n              className=\"mt-2 inline-flex items-center gap-1 rounded-sm text-xs font-medium text-primary underline-offset-2 hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n              href={href}\n              rel=\"noopener noreferrer\"\n              target={/^https?:/i.test(href) ? \"_blank\" : undefined}\n            >\n              <ExternalLink aria-hidden=\"true\" className=\"size-3\" />\n              Open source\n            </a>\n          ) : null}\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * One field row\n *\n * Module scope on purpose: a component declared inside the parent would be a\n * new type on every render, remounting every input and dropping keyboard focus\n * at the exact moment a suggestion lands.\n * -------------------------------------------------------------------------- */\n\ninterface FieldRowProps {\n  field: AiFormAssistField\n  idBase: string\n  value: string\n  state: FieldState\n  disabled: boolean\n  showConfidence: boolean\n  thresholds: { high: number; medium: number }\n  acceptLabel: string\n  rejectLabel: string\n  undoLabel: string\n  retryLabel: string\n  onInput: (field: AiFormAssistField, next: string) => void\n  onRequest: (field: AiFormAssistField) => void\n  /** Same run, but the pressed button is about to unmount and hands focus on. */\n  onRetry: (field: AiFormAssistField) => void\n  onAccept: (field: AiFormAssistField, focusBack: boolean) => void\n  onDismiss: (field: AiFormAssistField, focusBack: boolean) => void\n  onUndo: (field: AiFormAssistField) => void\n  registerInput: (name: string, node: HTMLInputElement | HTMLTextAreaElement | null) => void\n  registerTrigger: (name: string, node: HTMLButtonElement | null) => void\n}\n\nfunction FieldRow({\n  acceptLabel,\n  disabled,\n  field,\n  idBase,\n  onAccept,\n  onDismiss,\n  onInput,\n  onRequest,\n  onRetry,\n  onUndo,\n  registerInput,\n  registerTrigger,\n  rejectLabel,\n  retryLabel,\n  showConfidence,\n  state,\n  thresholds,\n  undoLabel,\n  value,\n}: FieldRowProps) {\n  const controlId = `${idBase}-control`\n  const hintId = `${idBase}-hint`\n  const suggestionId = `${idBase}-suggestion`\n  const messageId = `${idBase}-message`\n\n  const assist = field.assist !== false\n  const requesting = state.phase === \"requesting\"\n  const pending = state.phase === \"suggested\" ? state.pending : null\n  const empty = value.trim() === \"\"\n  /** The model returned exactly what the user already has: nothing to apply. */\n  const same = pending !== null && pending.value === value\n  /** The field moved after the run started, so the answer was computed from other text. */\n  const stale = pending !== null && pending.baseline !== value\n  /** Ghost text can only be painted where there is no real text under it. */\n  const ghost = pending !== null && empty && !same\n  const bucket = confidenceBucket(pending?.confidence ?? null, thresholds)\n\n  const describedBy =\n    [field.hint ? hintId : null, pending ? suggestionId : null, state.message ? messageId : null]\n      .filter(Boolean)\n      .join(\" \") || undefined\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n    if (event.key === \"Escape\") {\n      // Only swallow the key when there is something of ours to close, so an\n      // Escape inside a dialog still reaches the dialog.\n      if (!requesting && pending === null) return\n      event.preventDefault()\n      event.stopPropagation()\n      onDismiss(field, false)\n      return\n    }\n    if (event.key !== \"Enter\") return\n    // While an IME composition is open, Enter commits the candidate — never the\n    // suggestion. `isComposing` is the only reliable signal for that.\n    if (event.nativeEvent.isComposing) return\n    if (pending === null || same || disabled) return\n    // In a textarea Enter is a newline, so the suggestion needs a modifier there.\n    if (field.multiline && !(event.metaKey || event.ctrlKey)) return\n    event.preventDefault()\n    event.stopPropagation()\n    onAccept(field, false)\n  }\n\n  const shared = {\n    \"aria-busy\": requesting || undefined,\n    \"aria-describedby\": describedBy,\n    \"aria-disabled\": disabled || undefined,\n    autoComplete: field.autoComplete,\n    className: cn(\n      \"block w-full min-w-0 bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground\",\n      field.multiline && \"resize-y leading-relaxed\",\n    ),\n    id: controlId,\n    inputMode: field.inputMode,\n    name: field.name,\n    onKeyDown: handleKeyDown,\n    // A placeholder would sit under the ghost value and read as one smear.\n    placeholder: ghost ? \"\" : field.placeholder,\n    readOnly: disabled,\n    required: field.required,\n    value,\n  }\n\n  return (\n    <div className=\"flex min-w-0 flex-col gap-1.5\" data-field={field.name} data-phase={state.phase}>\n      <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n        <label className=\"text-xs font-medium\" htmlFor={controlId}>\n          {field.label}\n          {field.required ? (\n            <>\n              <span aria-hidden=\"true\" className=\"text-destructive\">\n                {\" *\"}\n              </span>\n              <span className=\"sr-only\"> (required)</span>\n            </>\n          ) : null}\n        </label>\n        {field.hint ? (\n          <span className=\"text-[0.6875rem] text-muted-foreground\" id={hintId}>\n            {field.hint}\n          </span>\n        ) : null}\n      </div>\n\n      <div\n        className={cn(\n          \"relative flex min-w-0 items-start gap-1 overflow-hidden rounded-lg border border-input bg-background shadow-xs\",\n          \"transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 motion-reduce:transition-none\",\n          pending !== null && bucket === \"high\" && \"border-primary/50\",\n          pending !== null && bucket === \"medium\" && \"border-muted-foreground/40\",\n          pending !== null && bucket === \"low\" && \"border-dashed border-muted-foreground/50\",\n          disabled && \"opacity-60\",\n        )}\n      >\n        <div className=\"relative min-w-0 flex-1\">\n          {field.multiline ? (\n            <textarea\n              {...shared}\n              onChange={event => onInput(field, event.target.value)}\n              ref={node => registerInput(field.name, node)}\n              rows={Math.max(1, Math.floor(field.rows ?? 3))}\n            />\n          ) : (\n            <input\n              {...shared}\n              onChange={event => onInput(field, event.target.value)}\n              ref={node => registerInput(field.name, node)}\n              type={field.type ?? \"text\"}\n            />\n          )}\n          {ghost && pending !== null ? (\n            // Decorative and pointer-events-none: the caret keeps blinking under\n            // it and typing simply replaces it. The value itself is announced by\n            // the suggestion group below, so this copy is hidden from readers.\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"pointer-events-none absolute inset-0 px-3 py-2 text-sm text-muted-foreground\",\n                \"[animation:afa-rise_160ms_ease-out] motion-reduce:[animation:none]\",\n                field.multiline ? \"overflow-hidden leading-relaxed whitespace-pre-wrap\" : \"truncate\",\n              )}\n            >\n              {pending.value}\n            </span>\n          ) : null}\n        </div>\n\n        {assist ? (\n          // One slot, two states. Swapping two <button>s here would remount the\n          // node and drop keyboard focus at the exact moment a run starts.\n          <Button\n            aria-disabled={requesting ? false : disabled}\n            aria-label={requesting ? `Stop asking about ${field.label}` : `Suggest a value for ${field.label}`}\n            className=\"my-1 mr-1 aria-disabled:opacity-50\"\n            onClick={() => (requesting ? onDismiss(field, false) : onRequest(field))}\n            ref={node => registerTrigger(field.name, node)}\n            size=\"icon-sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            {requesting ? (\n              <Loader2 aria-hidden=\"true\" className=\"animate-spin\" />\n            ) : (\n              <Sparkles\n                aria-hidden=\"true\"\n                className={cn(pending !== null ? \"text-primary\" : \"text-muted-foreground\")}\n              />\n            )}\n          </Button>\n        ) : null}\n\n        {requesting ? (\n          <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 overflow-hidden motion-reduce:hidden\">\n            <span className=\"absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-primary/15 to-transparent [animation:afa-sweep_1.4s_linear_infinite]\" />\n          </span>\n        ) : null}\n      </div>\n\n      {pending !== null ? (\n        <div\n          aria-label={`Suggestion for ${field.label}`}\n          className={cn(\n            \"flex min-w-0 flex-col gap-2 rounded-lg border p-2 [animation:afa-rise_180ms_ease-out] motion-reduce:[animation:none]\",\n            bucket === \"high\" && \"border-primary/30 bg-primary/5\",\n            bucket === \"low\" && \"border-dashed bg-muted/50\",\n            (bucket === \"medium\" || bucket === \"unknown\") && \"bg-muted/50\",\n          )}\n          id={suggestionId}\n          role=\"group\"\n        >\n          <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1\">\n            <span className=\"inline-flex shrink-0 items-center gap-1 text-[0.6875rem] font-medium\">\n              <Sparkles aria-hidden=\"true\" className=\"size-3 text-primary\" />\n              {same ? \"Matches your value\" : empty ? \"Suggested\" : \"Replace with\"}\n            </span>\n            {showConfidence && pending.confidence !== null ? (\n              <ConfidenceMeter bucket={bucket} confidence={pending.confidence} />\n            ) : null}\n            {pending.source ? <SourceHint fieldLabel={field.label} source={pending.source} /> : null}\n          </div>\n\n          {same ? null : empty ? (\n            // The visible copy is the ghost inside the field; this is the one a\n            // screen reader reads.\n            <span className=\"sr-only\">Suggested value: {pending.value}</span>\n          ) : (\n            <p\n              className={cn(\n                \"flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-sm wrap-anywhere\",\n                // A multi-line value keeps its line breaks in the diff, otherwise\n                // an address collapses into one unreadable run.\n                field.multiline && \"whitespace-pre-wrap\",\n              )}\n            >\n              <del className=\"rounded-sm bg-muted px-1 text-muted-foreground line-through decoration-muted-foreground\">\n                {value}\n              </del>\n              <ArrowRight aria-hidden=\"true\" className=\"size-3 shrink-0 text-muted-foreground\" />\n              <ins className=\"rounded-sm bg-primary/10 px-1 text-foreground no-underline\">{pending.value}</ins>\n            </p>\n          )}\n\n          {pending.note ? <p className=\"text-[0.6875rem] text-muted-foreground\">{pending.note}</p> : null}\n          {stale ? (\n            <p className=\"text-[0.6875rem] text-muted-foreground\">\n              You edited this field after asking — accepting replaces what is in it now.\n            </p>\n          ) : null}\n\n          <div className=\"flex min-w-0 flex-wrap items-center gap-1.5\">\n            {same ? null : (\n              <Button onClick={() => onAccept(field, true)} size=\"xs\" type=\"button\">\n                <Check aria-hidden=\"true\" />\n                {acceptLabel}\n              </Button>\n            )}\n            <Button onClick={() => onDismiss(field, true)} size=\"xs\" type=\"button\" variant=\"ghost\">\n              <X aria-hidden=\"true\" />\n              {rejectLabel}\n            </Button>\n            {same ? null : (\n              <span className=\"ml-auto inline-flex items-center gap-1 text-[0.6875rem] text-muted-foreground\">\n                <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n                {field.multiline ? \"⌘/Ctrl+Enter\" : \"Enter\"} accepts · Esc dismisses\n              </span>\n            )}\n          </div>\n        </div>\n      ) : null}\n\n      {state.message !== null || state.undo !== null ? (\n        <div className=\"flex min-w-0 items-start justify-between gap-2\">\n          <p\n            className={cn(\n              \"flex min-w-0 items-start gap-1.5 py-0.5 text-[0.6875rem] wrap-anywhere\",\n              state.tone === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n            )}\n            id={state.message !== null ? messageId : undefined}\n          >\n            {state.tone === \"error\" ? <TriangleAlert aria-hidden=\"true\" className=\"mt-px size-3 shrink-0\" /> : null}\n            <span className=\"min-w-0\">{state.message}</span>\n          </p>\n          <span className=\"flex shrink-0 items-center gap-1\">\n            {state.phase === \"error\" && assist ? (\n              <Button onClick={() => onRetry(field)} size=\"xs\" type=\"button\" variant=\"ghost\">\n                {retryLabel}\n              </Button>\n            ) : null}\n            {state.undo !== null ? (\n              <Button onClick={() => onUndo(field)} size=\"xs\" type=\"button\" variant=\"ghost\">\n                <Undo2 aria-hidden=\"true\" />\n                {undoLabel}\n              </Button>\n            ) : null}\n          </span>\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Component\n * -------------------------------------------------------------------------- */\n\nconst DEFAULT_THRESHOLDS = { high: 0.8, medium: 0.5 }\n\n/**\n * A form whose fields can be filled by a model — one field at a time, or all at\n * once.\n *\n * Every row runs the same small machine (idle → requesting → suggested →\n * accepted / refused, plus a retryable error branch) and nothing is ever\n * written without a person pressing Accept. A suggestion carries the exact\n * value its field held when it was asked for, so editing mid-flight never\n * silently changes what is being replaced: the row is flagged stale instead.\n *\n * Fill-all is a staggered wave, not a burst: requests leave one every\n * `staggerMs`, each row resolves on its own, and stopping drops the timers that\n * have not fired and aborts the requests that have.\n */\nexport const AiFormAssist = React.forwardRef<AiFormAssistHandle, AiFormAssistProps>(function AiFormAssist(\n  {\n    acceptLabel = \"Accept\",\n    announce = \"status\",\n    bulkActionsFrom = 2,\n    cancelLabel = \"Stop\",\n    className,\n    confidenceThresholds,\n    defaultValues,\n    description,\n    disabled = false,\n    fields,\n    fillAllLabel = \"Fill with AI\",\n    fillMode = \"empty\",\n    heading = \"Assisted fields\",\n    onAccept,\n    onReject,\n    onSuggest,\n    onUndo,\n    onValuesChange,\n    rejectLabel = \"Dismiss\",\n    retryLabel = \"Retry\",\n    showConfidence = true,\n    staggerMs = 120,\n    undoLabel = \"Undo\",\n    values,\n    ...props\n  },\n  ref,\n) {\n  const reactId = React.useId()\n\n  // Clamp every numeric knob: a NaN / negative value would either freeze the\n  // wave or hide the bulk bar forever.\n  const step = Number.isFinite(staggerMs) ? Math.max(0, Math.floor(staggerMs)) : 120\n  const bulkFrom = Number.isFinite(bulkActionsFrom) ? Math.max(1, Math.floor(bulkActionsFrom)) : 2\n  const thresholds = React.useMemo(() => {\n    const high = clamp01(confidenceThresholds?.high ?? DEFAULT_THRESHOLDS.high)\n    const medium = clamp01(confidenceThresholds?.medium ?? DEFAULT_THRESHOLDS.medium)\n    // An inverted pair would bucket every answer as \"low\"; keep them ordered.\n    return { high: Math.max(high, medium), medium: Math.min(high, medium) }\n  }, [confidenceThresholds?.high, confidenceThresholds?.medium])\n\n  const controlled = values !== undefined\n  const [innerValues, setInnerValues] = React.useState<Record<string, string>>(() => ({ ...defaultValues }))\n  const current = values ?? innerValues\n\n  const [states, setStates] = React.useState<Record<string, FieldState>>({})\n  const [notice, setNotice] = React.useState<Notice | null>(null)\n  const [fillRun, setFillRun] = React.useState<{ id: number; total: number; answered: number } | null>(null)\n  const [focusTick, setFocusTick] = React.useState(0)\n\n  /**\n   * Latest values for callbacks that run outside a render closure — staggered\n   * timers and in-flight responses both land long after the click that started\n   * them, and must not compute a baseline from text the user has since changed.\n   * Written on commit (and eagerly by `commitValues`, so two writes in one task\n   * compose), never read during render.\n   */\n  const valuesRef = React.useRef(current)\n  React.useEffect(() => {\n    valuesRef.current = current\n  })\n\n  const runsRef = React.useRef(new Map<string, ActiveRun>())\n  const timersRef = React.useRef(new Set<number>())\n  const runIdRef = React.useRef(0)\n  const suggestionIdRef = React.useRef(0)\n  const fillIdRef = React.useRef(0)\n  /** One-shot accept lock: field name → the suggestion id already applied. */\n  const appliedRef = React.useRef(new Map<string, number>())\n  const inputsRef = React.useRef(new Map<string, HTMLInputElement | HTMLTextAreaElement>())\n  const triggersRef = React.useRef(new Map<string, HTMLButtonElement>())\n  const focusIntentRef = React.useRef<{ name: string; mode: \"caret\" | \"trigger\" } | null>(null)\n\n  // The cleanup closes over the stable Map/Set objects, never `ref.current`\n  // inside the returned function — that is exactly what the hooks lint flags.\n  React.useEffect(() => {\n    const runs = runsRef.current\n    const timers = timersRef.current\n    return () => {\n      for (const run of runs.values()) run.controller.abort()\n      runs.clear()\n      for (const timer of timers) window.clearTimeout(timer)\n      timers.clear()\n    }\n  }, [])\n\n  // Focus moves in a layout effect, not in the handler: the node that should\n  // receive it only exists after the suggestion panel has been removed.\n  React.useLayoutEffect(() => {\n    const intent = focusIntentRef.current\n    if (intent === null) return\n    focusIntentRef.current = null\n    if (intent.mode === \"trigger\") {\n      triggersRef.current.get(intent.name)?.focus()\n      return\n    }\n    const node = inputsRef.current.get(intent.name)\n    if (node) caretToEnd(node)\n  }, [focusTick])\n\n  const requestFocus = (name: string, mode: \"caret\" | \"trigger\") => {\n    focusIntentRef.current = { mode, name }\n    setFocusTick(tick => tick + 1)\n  }\n\n  const registerInput = React.useCallback((name: string, node: HTMLInputElement | HTMLTextAreaElement | null) => {\n    if (node) inputsRef.current.set(name, node)\n    else inputsRef.current.delete(name)\n  }, [])\n\n  const registerTrigger = React.useCallback((name: string, node: HTMLButtonElement | null) => {\n    if (node) triggersRef.current.set(name, node)\n    else triggersRef.current.delete(name)\n  }, [])\n\n  const stateOf = (name: string): FieldState => states[name] ?? IDLE_FIELD\n\n  const patchField = (name: string, patch: Partial<FieldState>) => {\n    setStates(prev => ({ ...prev, [name]: { ...(prev[name] ?? IDLE_FIELD), ...patch } }))\n  }\n\n  const commitValues = (patch: Record<string, string>, cause: AiFormAssistCause) => {\n    const next = { ...valuesRef.current, ...patch }\n    // Keep the ref ahead of the parent's re-render: two writes in the same task\n    // must not both be built from the same stale base.\n    valuesRef.current = next\n    if (!controlled) setInnerValues(next)\n    onValuesChange?.(next, { cause, names: Object.keys(patch) })\n  }\n\n  const countFillAnswer = (fillId: number | undefined) => {\n    if (fillId === undefined) return\n    setFillRun(prev => {\n      if (prev === null || prev.id !== fillId) return prev\n      const answered = prev.answered + 1\n      return answered >= prev.total ? null : { ...prev, answered }\n    })\n  }\n\n  const startRun = (field: AiFormAssistField, trigger: AiFormAssistTrigger, fillId?: number): boolean => {\n    if (disabled || field.assist === false) return false\n    const runs = runsRef.current\n    // One run per field: a second press while the first is in flight is a no-op,\n    // not a second request the user pays for.\n    if (runs.has(field.name)) return false\n\n    const baseline = valuesRef.current[field.name] ?? \"\"\n    const controller = new AbortController()\n    const runId = ++runIdRef.current\n    runs.set(field.name, { controller, fillId, runId })\n    patchField(field.name, { message: null, pending: null, phase: \"requesting\", tone: null, undo: null })\n    setNotice(null)\n\n    /** Still ours? A superseding run or a cancel replaced / removed the entry. */\n    const claim = () => {\n      const active = runs.get(field.name)\n      if (controller.signal.aborted || active === undefined || active.runId !== runId) return false\n      runs.delete(field.name)\n      countFillAnswer(fillId)\n      return true\n    }\n\n    // `new Promise(resolve => resolve(fn()))` — not Promise.resolve(fn()) — so a\n    // synchronous throw inside the callback becomes a rejection instead of\n    // escaping and pinning the row at \"requesting\" forever.\n    new Promise<AiFormAssistAnswer>(resolve => {\n      resolve(onSuggest(field, { signal: controller.signal, trigger, values: valuesRef.current }))\n    })\n      .then(answer => {\n        if (!claim()) return\n        const suggestion = normalizeAnswer(answer)\n        if (suggestion === null) {\n          // \"Nothing to offer\" is information, not a failure: no destructive\n          // colour, no retry pressure.\n          patchField(field.name, {\n            message: \"No suggestion for this field.\",\n            pending: null,\n            phase: \"idle\",\n            tone: \"info\",\n          })\n          return\n        }\n        suggestionIdRef.current += 1\n        patchField(field.name, {\n          message: null,\n          pending: { ...suggestion, baseline, id: suggestionIdRef.current, trigger },\n          phase: \"suggested\",\n          tone: null,\n        })\n      })\n      .catch((err: unknown) => {\n        if (!claim()) return\n        patchField(field.name, {\n          message: errorText(err, `Could not suggest a value for ${field.label}.`),\n          pending: null,\n          phase: \"error\",\n          tone: \"error\",\n        })\n      })\n\n    return true\n  }\n\n  const retry = (field: AiFormAssistField) => {\n    // The Retry button is removed by this very click (the error line goes with\n    // it), so focus is handed to the row's trigger — which is now the stop\n    // control for the run that just started.\n    if (startRun(field, \"field\")) requestFocus(field.name, \"trigger\")\n  }\n\n  const accept = (field: AiFormAssistField, focusBack: boolean) => {\n    if (disabled) return\n    const state = stateOf(field.name)\n    const pending = state.pending\n    if (state.phase !== \"suggested\" || pending === null) return\n    // One-shot: two clicks in the same task share this closure, and a second\n    // apply would push a duplicate undo entry and fire onAccept twice.\n    if (appliedRef.current.get(field.name) === pending.id) return\n    appliedRef.current.set(field.name, pending.id)\n\n    const previous = valuesRef.current[field.name] ?? \"\"\n    if (pending.value === previous) {\n      // Nothing to write; close the row instead of recording an empty undo.\n      patchField(field.name, { message: null, pending: null, phase: \"idle\", tone: null })\n      if (focusBack) requestFocus(field.name, \"caret\")\n      return\n    }\n\n    commitValues({ [field.name]: pending.value }, \"accept\")\n    patchField(field.name, {\n      message: pending.source ? `Filled from ${pending.source.label}.` : \"Filled by the assistant.\",\n      pending: null,\n      phase: \"idle\",\n      tone: \"info\",\n      undo: { applied: pending.value, previous },\n    })\n    setNotice({ text: `${field.label} filled.`, tone: \"info\" })\n    if (focusBack) requestFocus(field.name, \"caret\")\n    onAccept?.({\n      bulk: false,\n      confidence: pending.confidence,\n      field,\n      previous,\n      source: pending.source,\n      value: pending.value,\n    })\n  }\n\n  /** Cancel an in-flight run, or refuse a pending suggestion. The value never moves. */\n  const dismiss = (field: AiFormAssistField, focusBack: boolean) => {\n    const runs = runsRef.current\n    const active = runs.get(field.name)\n    const state = stateOf(field.name)\n    const pending = state.pending\n    if (active === undefined && pending === null && state.phase !== \"error\") return\n\n    if (active !== undefined) {\n      active.controller.abort()\n      runs.delete(field.name)\n      countFillAnswer(active.fillId)\n    }\n    patchField(field.name, { message: null, pending: null, phase: \"idle\", tone: null })\n    setNotice({\n      text: active !== undefined ? `Stopped asking about ${field.label}.` : `Kept your own ${field.label}.`,\n      tone: \"info\",\n    })\n    if (focusBack) requestFocus(field.name, \"caret\")\n    if (active !== undefined) onReject?.({ bulk: false, field, reason: \"cancel\", value: null })\n    else if (pending !== null) onReject?.({ bulk: false, field, reason: \"reject\", value: pending.value })\n  }\n\n  const undoField = (field: AiFormAssistField) => {\n    if (disabled) return\n    const entry = stateOf(field.name).undo\n    if (entry === null) return\n    patchField(field.name, { message: null, tone: null, undo: null })\n    commitValues({ [field.name]: entry.previous }, \"undo\")\n    setNotice({ text: `Reverted ${field.label}.`, tone: \"info\" })\n    // The Undo button is removed by this very click, so focus would land on\n    // <body> unless it is handed back to the field.\n    requestFocus(field.name, \"caret\")\n    onUndo?.({ discarded: entry.applied, field, restored: entry.previous })\n  }\n\n  const handleInput = (field: AiFormAssistField, next: string) => {\n    commitValues({ [field.name]: next }, \"input\")\n    const state = stateOf(field.name)\n    if (state.message !== null || state.undo !== null || state.phase === \"error\") {\n      patchField(field.name, {\n        message: null,\n        // A pending suggestion is deliberately kept: it turns stale, and the row\n        // says so, instead of vanishing while the user is looking at it.\n        phase: state.phase === \"error\" ? \"idle\" : state.phase,\n        tone: null,\n        undo: null,\n      })\n    }\n    if (notice !== null) setNotice(null)\n  }\n\n  const fillAll = () => {\n    if (disabled) return\n    const runs = runsRef.current\n    const targets = fields.filter(field => {\n      if (field.assist === false || field.skipFill) return false\n      if (runs.has(field.name)) return false\n      if (stateOf(field.name).phase === \"suggested\") return false\n      return fillMode === \"all\" || (valuesRef.current[field.name] ?? \"\").trim() === \"\"\n    })\n    if (targets.length === 0) {\n      setNotice({\n        text:\n          fillMode === \"all\"\n            ? \"Every field is already answered or waiting for review.\"\n            : \"Every blank field is already answered — switch fillMode to “all” to redo the filled ones.\",\n        tone: \"info\",\n      })\n      return\n    }\n    const id = ++fillIdRef.current\n    setFillRun({ answered: 0, id, total: targets.length })\n    setNotice(null)\n\n    targets.forEach((field, index) => {\n      if (index === 0 || step === 0) {\n        // A refusal still has to be counted, or the wave never reports done.\n        if (!startRun(field, \"fill-all\", id)) countFillAnswer(id)\n        return\n      }\n      const timer = window.setTimeout(() => {\n        timersRef.current.delete(timer)\n        // A stop (or a newer wave) bumped the id: this request is void, and the\n        // wave it belonged to has already been cleared.\n        if (fillIdRef.current !== id) return\n        if (!startRun(field, \"fill-all\", id)) countFillAnswer(id)\n      }, index * step)\n      timersRef.current.add(timer)\n    })\n  }\n\n  const stopFill = () => {\n    const id = fillIdRef.current\n    // Bump first: a timer firing between here and the loop must find a dead id.\n    fillIdRef.current += 1\n    for (const timer of timersRef.current) window.clearTimeout(timer)\n    timersRef.current.clear()\n\n    const runs = runsRef.current\n    const cancelled: string[] = []\n    for (const [name, run] of runs) {\n      if (run.fillId !== id) continue\n      run.controller.abort()\n      runs.delete(name)\n      cancelled.push(name)\n    }\n    setFillRun(null)\n    if (cancelled.length > 0) {\n      setStates(prev => {\n        const next = { ...prev }\n        for (const name of cancelled) {\n          next[name] = { ...(prev[name] ?? IDLE_FIELD), message: null, pending: null, phase: \"idle\", tone: null }\n        }\n        return next\n      })\n    }\n    setNotice({ text: \"Autofill stopped — nothing was changed.\", tone: \"info\" })\n  }\n\n  const acceptAll = () => {\n    if (disabled) return\n    const applied: { field: AiFormAssistField; pending: PendingSuggestion; previous: string }[] = []\n    const patch: Record<string, string> = {}\n    for (const field of fields) {\n      const state = stateOf(field.name)\n      const pending = state.pending\n      if (state.phase !== \"suggested\" || pending === null) continue\n      if (appliedRef.current.get(field.name) === pending.id) continue\n      appliedRef.current.set(field.name, pending.id)\n      const previous = valuesRef.current[field.name] ?? \"\"\n      if (pending.value !== previous) patch[field.name] = pending.value\n      applied.push({ field, pending, previous })\n    }\n    if (applied.length === 0) return\n\n    // ONE write carrying every accepted field. N sequential writes would each be\n    // built from the same base in a controlled parent that has not re-rendered\n    // yet, and all but the last would be lost.\n    if (Object.keys(patch).length > 0) commitValues(patch, \"accept-all\")\n    setStates(prev => {\n      const next = { ...prev }\n      for (const entry of applied) {\n        next[entry.field.name] = {\n          ...(prev[entry.field.name] ?? IDLE_FIELD),\n          message: \"Filled by the assistant.\",\n          pending: null,\n          phase: \"idle\",\n          tone: \"info\",\n          undo:\n            entry.pending.value === entry.previous\n              ? null\n              : { applied: entry.pending.value, previous: entry.previous },\n        }\n      }\n      return next\n    })\n    setNotice({ text: `${applied.length} fields filled — review before you submit.`, tone: \"info\" })\n    for (const entry of applied) {\n      onAccept?.({\n        bulk: true,\n        confidence: entry.pending.confidence,\n        field: entry.field,\n        previous: entry.previous,\n        source: entry.pending.source,\n        value: entry.pending.value,\n      })\n    }\n  }\n\n  const dismissAll = () => {\n    const refused: { field: AiFormAssistField; value: string }[] = []\n    for (const field of fields) {\n      const state = stateOf(field.name)\n      if (state.phase !== \"suggested\" || state.pending === null) continue\n      refused.push({ field, value: state.pending.value })\n    }\n    if (refused.length === 0) return\n    setStates(prev => {\n      const next = { ...prev }\n      for (const entry of refused) {\n        next[entry.field.name] = {\n          ...(prev[entry.field.name] ?? IDLE_FIELD),\n          message: null,\n          pending: null,\n          phase: \"idle\",\n          tone: null,\n        }\n      }\n      return next\n    })\n    setNotice({ text: `Dismissed ${refused.length} suggestions — your values are untouched.`, tone: \"info\" })\n    for (const entry of refused) onReject?.({ bulk: true, field: entry.field, reason: \"reject\", value: entry.value })\n  }\n\n  const findField = (name: string) => fields.find(field => field.name === name)\n\n  React.useImperativeHandle(ref, () => ({\n    accept: (name: string) => {\n      const field = findField(name)\n      if (field) accept(field, false)\n    },\n    dismiss: (name: string) => {\n      const field = findField(name)\n      if (field) dismiss(field, false)\n    },\n    fillAll,\n    focus: (name: string) => {\n      inputsRef.current.get(name)?.focus()\n    },\n    stop: stopFill,\n    suggest: (name: string) => {\n      const field = findField(name)\n      return field ? startRun(field, \"field\") : false\n    },\n  }))\n\n  let busyCount = 0\n  let pendingCount = 0\n  let errorCount = 0\n  for (const field of fields) {\n    const phase = stateOf(field.name).phase\n    if (phase === \"requesting\") busyCount += 1\n    else if (phase === \"suggested\") pendingCount += 1\n    else if (phase === \"error\") errorCount += 1\n  }\n\n  const filling = fillRun !== null\n  const canFill = !disabled && fields.some(field => field.assist !== false && !field.skipFill)\n\n  // An explicit notice always wins: a refusal raised while suggestions are on\n  // screen must not be swallowed by the derived summary.\n  const statusText = notice\n    ? notice.text\n    : fillRun !== null\n      ? `Filling ${fillRun.total} fields — ${fillRun.answered} answered.`\n      : busyCount > 0\n        ? `Asking the assistant about ${busyCount} field${busyCount === 1 ? \"\" : \"s\"}…`\n        : pendingCount > 0\n          ? `${pendingCount} suggestion${pendingCount === 1 ? \"\" : \"s\"} waiting for review.`\n          : errorCount > 0\n            ? `${errorCount} field${errorCount === 1 ? \"\" : \"s\"} could not be filled.`\n            : \"\"\n\n  // `off` renders the same sentence as inert text — for forms that already own a\n  // live region and would otherwise announce this one twice.\n  const liveProps: React.HTMLAttributes<HTMLParagraphElement> =\n    announce === \"off\" ? {} : { \"aria-live\": \"polite\", role: \"status\" }\n\n  return (\n    <div\n      className={cn(\"flex w-full min-w-0 flex-col gap-3\", className)}\n      data-disabled={disabled || undefined}\n      {...props}\n    >\n      <style href=\"zyeon-ai-form-assist\" precedence=\"medium\">\n        {KEYFRAMES}\n      </style>\n\n      <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-2\">\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <span className=\"text-sm font-medium\">{heading}</span>\n          {description ? <span className=\"text-xs text-muted-foreground\">{description}</span> : null}\n        </div>\n        <div className=\"flex shrink-0 items-center gap-2\">\n          {fillRun !== null ? (\n            <span className=\"text-xs tabular-nums text-muted-foreground\">\n              {fillRun.answered}/{fillRun.total}\n            </span>\n          ) : null}\n          {/* One slot, two states — same reason as the per-field trigger. */}\n          <Button\n            aria-disabled={filling ? false : !canFill}\n            className=\"aria-disabled:opacity-50\"\n            onClick={filling ? stopFill : fillAll}\n            size=\"sm\"\n            type=\"button\"\n            variant={filling ? \"outline\" : \"default\"}\n          >\n            {filling ? (\n              <>\n                <X aria-hidden=\"true\" />\n                {cancelLabel}\n              </>\n            ) : (\n              <>\n                <Sparkles aria-hidden=\"true\" />\n                {fillAllLabel}\n              </>\n            )}\n          </Button>\n        </div>\n      </div>\n\n      {pendingCount >= bulkFrom ? (\n        <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2 rounded-lg border border-primary/30 bg-primary/5 px-2.5 py-1.5\">\n          <span className=\"inline-flex min-w-0 items-center gap-1.5 text-xs\">\n            <Sparkles aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-primary\" />\n            {pendingCount} suggestions waiting\n          </span>\n          <span className=\"ml-auto flex shrink-0 items-center gap-1.5\">\n            <Button\n              aria-disabled={disabled || undefined}\n              className=\"aria-disabled:opacity-50\"\n              onClick={acceptAll}\n              size=\"xs\"\n              type=\"button\"\n            >\n              <CheckCheck aria-hidden=\"true\" />\n              Accept all\n            </Button>\n            <Button onClick={dismissAll} size=\"xs\" type=\"button\" variant=\"ghost\">\n              Dismiss all\n            </Button>\n          </span>\n        </div>\n      ) : null}\n\n      <div className=\"flex min-w-0 flex-col gap-3\">\n        {fields.map((field, index) => (\n          <FieldRow\n            acceptLabel={acceptLabel}\n            disabled={disabled}\n            field={field}\n            idBase={`${reactId}-f${index}`}\n            key={field.name}\n            onAccept={accept}\n            onDismiss={dismiss}\n            onInput={handleInput}\n            onRequest={target => startRun(target, \"field\")}\n            onRetry={retry}\n            onUndo={undoField}\n            registerInput={registerInput}\n            registerTrigger={registerTrigger}\n            rejectLabel={rejectLabel}\n            retryLabel={retryLabel}\n            showConfidence={showConfidence}\n            state={stateOf(field.name)}\n            thresholds={thresholds}\n            undoLabel={undoLabel}\n            value={current[field.name] ?? \"\"}\n          />\n        ))}\n      </div>\n\n      <p\n        className={cn(\n          \"min-h-4 px-0.5 text-xs wrap-anywhere\",\n          notice?.tone === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n        )}\n        {...liveProps}\n      >\n        {statusText}\n      </p>\n    </div>\n  )\n})\n\nAiFormAssist.displayName = \"AiFormAssist\"\n\nexport default AiFormAssist\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}