{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "pixeldosa",
  "homepage": "https://pixeldosa.akshayjayan.com",
  "items": [
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "activity-trail",
      "title": "Activity Trail",
      "description": "What the agent did, why it was allowed to, and what came of it. Takes `entries` (each with a pre-formatted `time`, an `actor` of `agent`, `person` or `system` with an `actorName`, an `action` phrased as its consequence, an optional `target`, an `authority` line naming what permitted it, a `result`, and `undoable`), plus `onUndo` and `onExport`. The `authority` field is the one audit logs leave out and the one people actually ask about: \"who let it do that?\" — an approval, an autonomy level, a schedule someone set. Compliance logging is becoming mandatory for high-risk systems under the EU AI Act, with months of retention expected in regulated industries, but those records are built for auditors; this is the readable version for the person whose inbox the agent used, and `onExport` hands the same records to your compliance process so the two never drift apart. Purely presentational. Use it under a run, in an agent's settings, or on a record the agent touched. Not a developer event log, and not a chat transcript.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/activity-trail/activity-trail.tsx",
          "target": "components/ui/activity-trail.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Write `action` as the consequence (\"Emailed the summary to 3 people\"), never as a function name.",
      "meta": {
        "engineeringNotes": "Entries are newest last, the order a story is read in, rather than the newest-first order of a log — this surface is meant to be reconstructed rather than monitored, and reversing it makes causation read backwards. `authority` is a required-shaped optional: nothing enforces it, but its absence in a row is visible, which pushes teams to record what permitted an action at the moment they render it rather than reconstructing it later from three systems. Undo is per entry and driven by the caller's `undoable` flag, because only the caller knows whether a specific action is still reversible; the component never infers reversibility from the action's words. Export exists so the human-readable trail and the compliance record come from one array instead of two implementations that disagree.",
        "motionNotes": "Nothing animates. Every row describes something that already happened, and this system reserves motion for a machine that is still busy; an entry that slid or faded in would imply the record was still being written. Only the standard instant colour change on Undo, dropped under `prefers-reduced-motion`.",
        "schema": {
          "component": "ActivityTrail",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ActivityTrail",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "entries": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/TrailEntry"
                },
                "description": "Newest last, the way a person reads a story."
              },
              "exportLabel": {
                "type": "string"
              },
              "emptyLabel": {
                "type": "string"
              }
            },
            "required": [
              "entries"
            ],
            "$defs": {
              "TrailEntry": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "time": {
                    "type": "string",
                    "description": "Formatted by you: \"14:02\", \"Yesterday 09:15\". Never recomputed here."
                  },
                  "actor": {
                    "enum": [
                      "agent",
                      "person",
                      "system"
                    ]
                  },
                  "actorName": {
                    "type": "string",
                    "description": "Who exactly: \"Research agent\", \"Dana\", \"Scheduler\"."
                  },
                  "action": {
                    "type": "string",
                    "description": "The consequence, in the user's words: \"Emailed the summary to 3 people\"."
                  },
                  "target": {
                    "type": "string",
                    "description": "What it touched: a file, a record, a recipient list."
                  },
                  "authority": {
                    "type": "string",
                    "description": "Why it was allowed to do this: \"You approved it\", \"Autonomy: acts on low-risk\nthings\", \"Scheduled by Dana\". The question every log leaves out."
                  },
                  "result": {
                    "type": "string",
                    "description": "What came of it: \"Sent\", \"3 rows changed\", \"No matches\"."
                  },
                  "undoable": {
                    "type": "boolean",
                    "description": "Set when this specific action can still be reversed."
                  }
                },
                "required": [
                  "id",
                  "time",
                  "actor",
                  "action"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onUndo",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onExport",
              "required": false,
              "params": [],
              "description": "Hands the whole trail to your export: CSV, JSON, a compliance system."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-ask",
      "title": "Agent Ask",
      "description": "A structured question from an agent that needs input before it can continue — the UI surface for MCP elicitation. Takes a targeted `question`, a required `source` naming who is asking, a `fields` array describing what is needed (`text`, `number`, `confirm`, or `select` with concrete options and hints), an optional `explanation` giving the context that makes the question answerable, and three separate outcomes: `onRespond` with the collected values, `onDecline` to refuse this particular question and continue without the information, and `onCancel` to abandon the whole operation. Decline and cancel are deliberately distinct answers rather than one dismiss action, and both are rendered as visible first-class controls rather than hidden behind an overflow. `source` is required rather than optional because MCP's elicitation specification obliges a client to show which server is requesting input — 'some software wants your data' is not a question a user can safely answer without knowing the asker. The component prefers concrete options over free text: a select renders as labelled radio cards with optional hints, and a confirm renders as explicit Yes/No controls, because a question with buttons is answerable in a second where the same question in a text box is a small piece of homework. Submit stays disabled until every required field is answered. Holds perfectly still, since it is the user's turn under the system's turn-taking grammar — pair it with Agent Presence in the `asking` state. Purely presentational: it does not call a model and does not own the paused run. Use it for disambiguation between plausible matches and for missing parameters the agent cannot safely guess. Not for approving a consequential action the agent has already decided on (use AI Approval Gate), and not for a general chat input.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-ask/agent-ask.tsx",
          "target": "components/ui/agent-ask.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. MCP's elicitation spec requires that a client show which server is asking and offer both decline and cancel — `source` is therefore a required prop, and you should wire `onDecline` and `onCancel` rather than omitting them, even though the component tolerates their absence for non-MCP uses.",
      "meta": {
        "engineeringNotes": "Three decisions separate this from a chat prompt asking the same question. First, provenance is a required prop rather than an optional nicety: MCP obliges a client to show which server is requesting input, and it is also the only basis a user has for judging whether the question is safe to answer at all. Second, refusal is modelled as two distinct outcomes instead of one dismissal — declining answers 'not this information' while cancelling answers 'not this operation', and collapsing them would silently convert a narrow refusal into an abort. Third, the field types deliberately bias toward concrete options: `select` renders as labelled radio cards carrying optional hints (the disambiguation case, which is what elicitation is mostly used for, is far better served by 'Q3 Revenue Final.xlsx — edited 3 days ago' than by a text box), and `confirm` renders explicit Yes/No buttons rather than a checkbox, since a checkbox's unchecked state is ambiguous between 'no' and 'not answered'. That ambiguity matters here because submit is gated on required fields actually being answered. The component deliberately does not compose AI Action Toolbar for its actions despite the system's compose-don't-reinvent rule: the primary control is a real form submit button, and wrapping form submission inside a `role=\"toolbar\"` would misreport the semantics to assistive technology for the sake of visual consistency. Text inputs are 16px on small viewports because anything smaller makes iOS Safari zoom the page on focus.",
        "motionNotes": "Almost none, and that is the design. This is a user-turn surface under the turn-taking grammar (DESIGN.md §3a), which means it holds perfectly still — the absence of motion is precisely what signals that nothing proceeds until the user acts, and animating it would put it back in the same visual register as the machine's own busy states. The only motion present is the shared colour transition on hover, press and focus at `--pd-duration-instant`, matching every other control in the system, and it is dropped entirely under `prefers-reduced-motion` while the selected-state border and background still communicate which option is chosen. Entrance animation belongs to whatever surface reveals this component, not to the component itself — the same rule Confidence Meter and AI Action Toolbar hold.",
        "schema": {
          "component": "AgentAsk",
          "element": "form",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentAsk",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "question": {
                "type": "string",
                "description": "The actual question. Targeted and answerable — never \"can you provide more information?\"."
              },
              "source": {
                "type": "string",
                "description": "Who is asking. Required, not optional: MCP's elicitation spec obliges a client to\nshow which server is requesting input, because \"some software wants your data\" is\nnot a question a user can safely answer without knowing the asker."
              },
              "fields": {
                "type": "array",
                "items": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "const": "text"
                        },
                        "label": {
                          "type": "string"
                        },
                        "placeholder": {
                          "type": "string"
                        },
                        "required": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "name",
                        "type",
                        "label"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "const": "number"
                        },
                        "label": {
                          "type": "string"
                        },
                        "placeholder": {
                          "type": "string"
                        },
                        "required": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "name",
                        "type",
                        "label"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "const": "confirm"
                        },
                        "label": {
                          "type": "string"
                        },
                        "required": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "name",
                        "type",
                        "label"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "const": "select"
                        },
                        "label": {
                          "type": "string"
                        },
                        "options": {
                          "type": "array",
                          "items": {
                            "$ref": "#/$defs/AskFieldOption"
                          }
                        },
                        "required": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "name",
                        "type",
                        "label",
                        "options"
                      ]
                    }
                  ]
                }
              },
              "explanation": {
                "type": "string",
                "description": "Why it's asking. Short context that makes the question answerable."
              },
              "submitLabel": {
                "type": "string"
              }
            },
            "required": [
              "question",
              "source",
              "fields"
            ],
            "$defs": {
              "AskFieldOption": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "hint": {
                    "type": "string"
                  }
                },
                "required": [
                  "value",
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onRespond",
              "required": true,
              "params": [
                {
                  "name": "values",
                  "type": "Record<string, string | boolean>"
                }
              ]
            },
            {
              "name": "onDecline",
              "required": false,
              "params": [],
              "description": "Answer refused, continue without it. Distinct from cancelling the run."
            },
            {
              "name": "onCancel",
              "required": false,
              "params": [],
              "description": "Abandon the whole operation. MCP requires this be offered alongside decline."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-console",
      "title": "Agent Console",
      "description": "The screen a person comes back to after leaving agents running, assembled from Run Inbox, Agent Plan, Activity Trail, Human Handoff and Agent Schedule. Takes a `briefing` (what changed since they were last here, as counts plus one sentence of what to do first), `runs` for the inbox, a `detail` for the selected run (its steps, its trail, and a handoff when a person is involved), and optional `triggers` for what runs on its own. The briefing is the design: two products shipped agent dashboards this year, and the part still treated as an afterthought is the return itself — research on agentic interfaces puts the moment someone comes back after four hours as where trust is won or lost, and says it should read as a briefing rather than a list the user reconstructs one from. Beneath it the inbox groups by what a run needs rather than when it happened, and the detail answers the two questions people actually ask: where did it get to, and who let it do that. Presentational: you own the runs, the selection and the history. Use it as the home screen of a product that runs background or scheduled agents. Not for a single agent run inside a chat — that is Thinking Experience.",
      "type": "registry:block",
      "categories": [
        "ai-assisted",
        "application",
        "block"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/run-inbox",
        "@pixeldosa/agent-plan",
        "@pixeldosa/activity-trail",
        "@pixeldosa/human-handoff",
        "@pixeldosa/agent-schedule"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-console/agent-console.tsx",
          "target": "components/blocks/agent-console.tsx",
          "type": "registry:block"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Write `briefing.nextUp` as the one thing worth doing first, not a summary — it is the sentence that has to survive being read on a phone at a bus stop. Times and counts are formatted by you.",
      "meta": {
        "engineeringNotes": "The briefing sits above everything and is written in words rather than badges, because the failure of a returned-to surface is that it hands back a pile and makes the person do the triage that the system already has the data to do. It turns the waiting colour only when something actually needs a person, so the colour means the same thing here as it does on a single approval. Zero is stated rather than hidden: 'Nothing changed while you were away' is a useful answer, and an empty space is not. The two-pane split puts the inbox at a fixed width and the detail fluid, because the detail is where the reading happens; the selected row is marked with `aria-current` as well as a background, so the pairing survives without colour. A person being involved is rendered above the machine's account of itself — the handoff comes before the plan and the trail — since a run waiting on somebody is not primarily a story about what the agent did.",
        "motionNotes": "Motion means the machine is busy, and a console is where that rule earns its keep: on a screen showing six runs, a plan, a trail and three schedules at once, only genuinely running things move — a running row's dot, a running step's mark. Everything waiting on a person is completely still in the waiting colour, so the eye lands on the one place work is actually happening and the stillness of everything else is itself information. The briefing never animates; a summary that moves reads as still arriving. Under `prefers-reduced-motion` every part of that stops and each state is carried by colour, position and words.",
        "schema": {
          "component": "AgentConsole",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentConsole",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4
                ],
                "description": "Level of the console's own headings. Defaults to 2, to sit under a page `h1`."
              },
              "briefing": {
                "$ref": "#/$defs/ConsoleBriefing",
                "description": "What changed since the user was last here. The return moment, in four numbers."
              },
              "runs": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/AgentRun"
                },
                "description": "Every run, grouped by what it needs."
              },
              "selectedId": {
                "type": "string"
              },
              "inboxEmptyLabel": {
                "type": "string"
              },
              "detail": {
                "$ref": "#/$defs/ConsoleRunDetail",
                "description": "The selected run, opened out."
              },
              "detailEmptyLabel": {
                "type": "string",
                "description": "Shown when nothing is selected."
              },
              "triggers": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ScheduleTrigger"
                },
                "description": "When the agent runs on its own. Omit the section entirely by leaving this out."
              }
            },
            "required": [
              "runs"
            ],
            "$defs": {
              "ConsoleBriefing": {
                "type": "object",
                "properties": {
                  "since": {
                    "type": "string",
                    "description": "When the user was last here, in words: \"since yesterday at 6pm\"."
                  },
                  "needsYou": {
                    "type": "number",
                    "description": "Counts of what changed while they were away. Zero is fine and says so."
                  },
                  "finished": {
                    "type": "number"
                  },
                  "failed": {
                    "type": "number"
                  },
                  "nextUp": {
                    "type": "string",
                    "description": "The one thing worth doing first, in a sentence."
                  }
                },
                "required": [
                  "needsYou",
                  "finished",
                  "failed"
                ]
              },
              "AgentRun": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "description": "What it was asked to do, in the user's words."
                  },
                  "state": {
                    "enum": [
                      "needsYou",
                      "running",
                      "scheduled",
                      "done",
                      "failed"
                    ]
                  },
                  "agent": {
                    "type": "string",
                    "description": "Which agent, if you run more than one."
                  },
                  "note": {
                    "type": "string",
                    "description": "Why it needs you, what it's doing, or what it produced. One line."
                  },
                  "time": {
                    "type": "string",
                    "description": "Formatted by you: \"2 minutes ago\", \"Tomorrow at 9:00\"."
                  },
                  "unread": {
                    "type": "boolean",
                    "description": "Not looked at yet."
                  }
                },
                "required": [
                  "id",
                  "title",
                  "state"
                ]
              },
              "ConsoleRunDetail": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "description": "What was asked for, in the user's words."
                  },
                  "steps": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/PlanStep"
                    },
                    "description": "Where the run got to. Each step carries its own status."
                  },
                  "runStatus": {
                    "type": "string",
                    "description": "What's happening right now, beside the progress count."
                  },
                  "trail": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/TrailEntry"
                    },
                    "description": "What the agent did, who allowed it, and what came of it."
                  },
                  "handoff": {
                    "type": "object",
                    "properties": {
                      "stage": {
                        "enum": [
                          "agentHandling",
                          "waitingForPerson",
                          "personHandling",
                          "returnedToAgent"
                        ]
                      },
                      "reason": {
                        "type": "string"
                      },
                      "context": {
                        "type": "array",
                        "items": {
                          "$ref": "#/$defs/HandoffFact"
                        }
                      },
                      "person": {
                        "type": "string"
                      },
                      "waitingFor": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "stage"
                    ],
                    "description": "Present only when a person is involved, or is being asked to be."
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              },
              "PlanStep": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "detail": {
                    "type": "string"
                  },
                  "needsApproval": {
                    "type": "boolean",
                    "description": "The step has an external effect and will stop for approval when it runs."
                  },
                  "status": {
                    "enum": [
                      "running",
                      "done",
                      "failed",
                      "pending",
                      "waiting",
                      "skipped"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "What happened, for a step that's running, waiting, failed or skipped."
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              },
              "TrailEntry": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "time": {
                    "type": "string",
                    "description": "Formatted by you: \"14:02\", \"Yesterday 09:15\". Never recomputed here."
                  },
                  "actor": {
                    "enum": [
                      "agent",
                      "person",
                      "system"
                    ]
                  },
                  "actorName": {
                    "type": "string",
                    "description": "Who exactly: \"Research agent\", \"Dana\", \"Scheduler\"."
                  },
                  "action": {
                    "type": "string",
                    "description": "The consequence, in the user's words: \"Emailed the summary to 3 people\"."
                  },
                  "target": {
                    "type": "string",
                    "description": "What it touched: a file, a record, a recipient list."
                  },
                  "authority": {
                    "type": "string",
                    "description": "Why it was allowed to do this: \"You approved it\", \"Autonomy: acts on low-risk\nthings\", \"Scheduled by Dana\". The question every log leaves out."
                  },
                  "result": {
                    "type": "string",
                    "description": "What came of it: \"Sent\", \"3 rows changed\", \"No matches\"."
                  },
                  "undoable": {
                    "type": "boolean",
                    "description": "Set when this specific action can still be reversed."
                  }
                },
                "required": [
                  "id",
                  "time",
                  "actor",
                  "action"
                ]
              },
              "HandoffFact": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Customer\", \"Order\", \"Tried already\"."
                  },
                  "value": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "value"
                ]
              },
              "ScheduleTrigger": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "when": {
                    "type": "string",
                    "description": "When it runs, in plain words: \"Every weekday at 9:00\", \"When an invoice arrives\nfrom a new supplier\". Never a cron string."
                  },
                  "does": {
                    "type": "string",
                    "description": "What it will do, phrased as the consequence."
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "nextRun": {
                    "type": "string",
                    "description": "Formatted by you: \"Tomorrow at 9:00\". Left out when it's off."
                  },
                  "lastRun": {
                    "type": "string",
                    "description": "How the last one went: \"Ran yesterday · 3 invoices matched\"."
                  },
                  "lastFailed": {
                    "type": "boolean",
                    "description": "Set when the last run had a problem, so an off-schedule failure is visible."
                  },
                  "asksFirst": {
                    "type": "boolean",
                    "description": "Set when a run of this will pause for approval."
                  }
                },
                "required": [
                  "id",
                  "when",
                  "does",
                  "enabled"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onSelect",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onStopRun",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onStopSelected",
              "required": false,
              "params": []
            },
            {
              "name": "onRetryStep",
              "required": false,
              "params": [
                {
                  "name": "stepId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onUndo",
              "required": false,
              "params": [
                {
                  "name": "entryId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onExportTrail",
              "required": false,
              "params": []
            },
            {
              "name": "onTakeOver",
              "required": false,
              "params": []
            },
            {
              "name": "onReturnToAgent",
              "required": false,
              "params": []
            },
            {
              "name": "onTriggerEnabledChange",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                },
                {
                  "name": "enabled",
                  "type": "boolean"
                }
              ]
            },
            {
              "name": "onRunTriggerNow",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onEditTrigger",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-figure",
      "title": "Agent Figure",
      "description": "A hand-drawn agent character whose pose shows what a run is doing, how sure it is, and when it needs the person. Takes a `pose` from a fourteen-value vocabulary: work poses (`listening`, `thinking`, `planning`, `searching`, `reading`, `comparing`, `working`), hand-off poses (`idle`, `asking`, `blocked`, `done`), and three result poses (`confident`, `probable`, `unsure`) that must be driven by the system's real confidence. `variant` is `figure` (the whole character) or `mark` (the head alone, for inline status and avatars); `size` is `sm`, `md` or `lg`. A visible `label` sits beside or below the figure by default, because the words are the real signal; `hideLabel` turns the drawing into a labelled image instead, and `announce` adds a polite live region. Motion follows the turn-taking grammar: on the agent's turn the pen lines re-draw slightly (a line boil) and small parts move in steps; on the person's turn every line holds still. Drawn in code as seeded SVG strokes, coloured only by theme tokens (ink, card, muted ink and the four agent markers), so it themes, stays small and renders identically on server and client. Use it for onboarding, empty states, run panels and consumer products where an agent should feel present. Not a replacement for status text (pair it with Live Status Line or a label), and not for dense tables — use Agent Presence's `line` form there.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-figure/agent-figure.tsx",
          "target": "components/ui/agent-figure.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and the agent marker colours (`--agent-working`, `--agent-waiting`, `--agent-blocked`, `--agent-done` and their `-soft` fills) from @pixeldosa/pixeldosa-theme.",
      "meta": {
        "engineeringNotes": "The character is generated, not illustrated: each stroke is a polyline pushed off its true path by a seeded wobble and smoothed with Catmull-Rom curves. Seeds come from the pose, the stroke name and the frame, so the server and client render byte-identical paths and there is no hydration mismatch, and a new pose costs a few coordinates rather than an asset. Every colour is a theme token, so the figure follows light and dark mode and a host's palette. Poses map to things a run actually does, never to feelings: 'blocked' is a state, 'sad' is not, and the three result poses exist so confidence can be shown honestly rather than always looking pleased. The frame clock runs only while the pose is on the agent's turn, the figure is on screen (IntersectionObserver), and the tab is visible; otherwise nothing ticks.",
        "motionNotes": "Two kinds of motion, both stepped at 280ms rather than eased, so the figure reads as drawn rather than rendered: the line boil (strokes re-seed every second frame) and a few discrete prop movements — eyes scanning a page, a key press, the thought bubble filling in. Both stop completely when the pose is the person's turn (`idle`, `asking`, `blocked`, `done` and the result poses), which is the system's rule that stillness means it's your turn. `still` holds the lines on the agent's turn too. Under `prefers-reduced-motion` nothing animates and each pose remains a distinct static drawing, so no information is lost.",
        "schema": {
          "component": "AgentFigure",
          "element": "span",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentFigure",
            "type": "object",
            "properties": {
              "pose": {
                "enum": [
                  "done",
                  "idle",
                  "listening",
                  "thinking",
                  "planning",
                  "searching",
                  "reading",
                  "comparing",
                  "working",
                  "asking",
                  "blocked",
                  "confident",
                  "probable",
                  "unsure"
                ]
              },
              "variant": {
                "enum": [
                  "figure",
                  "mark"
                ],
                "description": "`figure` is the whole character; `mark` is the head alone, for inline status and avatars."
              },
              "size": {
                "enum": [
                  "sm",
                  "md",
                  "lg"
                ]
              },
              "label": {
                "type": "string",
                "description": "What the pose means, in words. Defaults to the pose's own label. Read by assistive\ntechnology, and shown beside the figure unless `hideLabel` is set."
              },
              "hideLabel": {
                "type": "boolean"
              },
              "labelPosition": {
                "enum": [
                  "end",
                  "below"
                ],
                "description": "Where the visible label sits."
              },
              "announce": {
                "type": "boolean",
                "description": "Announce label changes through a polite live region. Off by default: most products\nalready announce status elsewhere (Live Status Line, Agent Presence), and two\nchannels saying the same thing is worse than one."
              },
              "still": {
                "type": "boolean",
                "description": "Hold the pen lines still even while the agent is busy."
              }
            },
            "required": [
              "pose"
            ]
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-memory",
      "title": "Agent Memory",
      "description": "What an agent remembers about someone, where they can read it, correct it, or make it forget. Takes `memories`, each with `text`, an `origin` of 'stated' (the user said it) or 'inferred' (the agent picked it up) and an optional `source` ('Noticed across 6 conversations'). Handlers are `onEdit`, `onForget`, and optionally `onRestore`, `onForgetAll`, plus `paused` with `onPausedChange`. What the user said and what the agent guessed are listed separately, and inferred memories are framed as guesses worth checking, because a wrong inference quietly skews every later answer. Edits happen inline (Enter saves, Escape cancels). Forget fires immediately, while the row stays in place, struck through, with Undo, and focus moves to it. Pausing is described exactly: it stops new memories and doesn't erase old ones. Forget all needs a second confirming step that says it can't be undone. Use it in agent settings or a 'what do you know about me' panel. Not a chat history or a data export.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-memory/agent-memory.tsx",
          "target": "components/ui/agent-memory.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. `onForget` should really delete. The component keeps its own copy for Undo. When a user edits an inferred memory, store it as `stated`, because they have now said it.",
      "meta": {
        "engineeringNotes": "Built around the difference between stated and inferred memory. From the model side, an inferred preference that is wrong is worse than no memory, because it shapes answers without the user ever seeing why. So origin is a required field, the two kinds are listed in separate sections (the inferred list has a dashed border and reads 'Guesses from how you work'), and each memory can carry its source. Forgetting is honest about timing. `onForget` fires at once, so the caller can really delete it, and the component keeps a local snapshot with the original position so the row stays in place, struck through, with Undo. Focus moves to Undo after a forget and back to Forget after a restore, so keyboard users never lose their place. Pausing uses a real `role=\"switch\"`, and its note says exactly what pausing does and doesn't do, because 'paused' is widely misread as 'deleted'. Forget all is the only irreversible action. It opens an inline confirmation with focus on Cancel and uses the destructive intent. A polite live region announces forgets and restores by content.",
        "motionNotes": "Still, apart from direct feedback. It is the user's turn, so nothing moves unless the user acts. The pause switch thumb slides and its track colour changes at `--pd-duration-fast` on the standard curve. Hover colour changes run at `--pd-duration-instant`. Forgetting doesn't collapse or fade the row. It swaps to its struck-through state instantly and keeps its height, so the list doesn't shift under the pointer and Undo appears exactly where the user clicked. Everything is instant under prefers-reduced-motion.",
        "schema": {
          "component": "AgentMemory",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentMemory",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "memories": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/MemoryItem"
                }
              },
              "paused": {
                "type": "boolean",
                "description": "Whether new things are being remembered. Pausing never forgets what's already here."
              }
            },
            "required": [
              "memories"
            ],
            "$defs": {
              "MemoryItem": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string",
                    "description": "One remembered thing, in plain words: \"Prefers metric units\"."
                  },
                  "origin": {
                    "enum": [
                      "stated",
                      "inferred"
                    ],
                    "description": "`stated` — the user said it. `inferred` — the agent picked it up from how they work."
                  },
                  "source": {
                    "type": "string",
                    "description": "Where it came from: \"From your chat on 3 Sep\", \"Noticed across 4 conversations\"."
                  }
                },
                "required": [
                  "id",
                  "text",
                  "origin"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onEdit",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                },
                {
                  "name": "text",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onForget",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Called immediately. Forget for real — the component keeps its own copy for Undo."
            },
            {
              "name": "onRestore",
              "required": false,
              "params": [
                {
                  "name": "memory",
                  "type": "MemoryItem"
                }
              ],
              "description": "Enables Undo on a forgotten row. Receives the memory as it was."
            },
            {
              "name": "onForgetAll",
              "required": false,
              "params": []
            },
            {
              "name": "onPausedChange",
              "required": false,
              "params": [
                {
                  "name": "paused",
                  "type": "boolean"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-plan",
      "title": "Agent Plan",
      "description": "An agent's plan laid out for agreement before anything runs — the user can reorder steps, remove one, or add their own, then run the plan as edited. The same component follows the run: give each step a `status` (`pending`, `running`, `waiting`, `done`, `failed`, `skipped`) and an optional `note`, and editing gives way to progress — the running step is marked and moves, a step waiting on the user holds still in the waiting colour, finished steps get a check, a failed step offers `onRetryStep`, `runStatus` names what's happening now, and `onStop` stays visible for the whole run. Takes `steps` (each with a `title`, optional `detail`, and an optional `needsApproval` flag for steps with external effects), an optional `title`, `onRun`, which receives the final ordered steps, an optional `onCancel`, and `allowAdd` to permit user-added steps. Steps that will stop for approval are labelled 'Asks for approval', and the header counts them, so the user knows before running where the agent will pause. Removing a step never deletes it outright: it stays in place struck through with an Undo, because a plan edit that cannot be seen or reversed is exactly the silent change the system exists to prevent. Reordering uses explicit move-up and move-down controls rather than drag, so it works identically by keyboard, touch and pointer, and every edit is announced through a live region. The run button reads 'Run edited plan' once anything changed. Plans are where an agent's misunderstanding becomes visible while it is still free to fix — once execution starts, a wrong step costs a tool call, a side effect, or a cancel and restart. This component is the agreement; execution belongs to Task Plan Runner. Composes AI Action Toolbar. Holds still, since it is the user's turn. Use it before multi-step runs, especially ones that touch external systems. Not for showing progress through a running plan.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/ai-action-toolbar"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-plan/agent-plan.tsx",
          "target": "components/ui/agent-plan.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Mark any step with an external effect as `needsApproval` so the user sees where the run will pause before it starts. This component does not execute steps — it hands the agreed plan to `onRun`.",
      "meta": {
        "engineeringNotes": "Separates agreeing on a plan from executing it, because the two need opposite designs: agreement is a still, editable surface where the user is in control, while execution is a moving status surface where the machine is. Merging them produces the common pattern where a plan is shown only as it starts running, which is too late to change it cheaply. Removal is modelled as a reversible state rather than a deletion — the step remains in place, struck through, numbered out of the sequence, with Undo — so an edit is always visible and never destructive. Reordering deliberately uses explicit up and down buttons instead of drag and drop: drag is pointer-first, hard to make accessible, and ambiguous on touch, while buttons behave identically across every input method and announce the resulting position through a live region (counting only steps that remain). The icon buttons are 32 pixels with hit areas extended vertically to 44, not horizontally, because the three controls sit side by side and overlapping hit areas would turn a near-miss into the wrong edit. Steps flagged `needsApproval` are labelled and counted in the header, so where the run will pause is known before it starts rather than discovered during it. The running view lives in this component rather than a separate runner because the plan people agreed to is the thing they want to watch; a second surface would make them re-read a list they had already accepted. The switch is derived, not configured: the first step carrying a status past `pending` ends editing, so a caller cannot show edit controls over a plan that is already executing. Progress is counted from finished steps, never an invented percentage, and each step announces itself once as its status changes, tracked against the previous value so a re-render with unchanged statuses stays silent.",
        "motionNotes": "Still, as a user-turn surface: nothing moves on its own and nothing shimmers. Removed steps drop to reduced opacity with a strikethrough rather than animating out, so an edit reads as a changed state instead of a disappearance and remains reversible in place. Reordering changes position immediately without a sliding animation — a swap that animates on every press would slow repeated keyboard moves and add motion to what is a precise, deliberate edit. The remaining motion is the shared colour transition on hover and focus at `--pd-duration-instant`, which drops to instant under prefers-reduced-motion. In the running view only the current step moves: its mark pulses on the working colour. A step waiting on the person holds completely still in the waiting colour, which is the turn-taking grammar applied per row instead of per panel. The progress bar animates its width over `--pd-duration-slow` because it tracks a real, discrete change, and under reduced motion the pulse stops while colour and words continue to carry every state.",
        "schema": {
          "component": "AgentPlan",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentPlan",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/PlanStep"
                },
                "description": "The agent's proposed plan, in order."
              },
              "title": {
                "type": "string"
              },
              "allowAdd": {
                "type": "boolean",
                "description": "Let the user add their own steps."
              },
              "runStatus": {
                "type": "string",
                "description": "Shown while the plan runs, beside the progress count — the one thing happening right\nnow, in the caller's words (\"Reading pricing pages\")."
              }
            },
            "required": [
              "steps"
            ],
            "$defs": {
              "PlanStep": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "detail": {
                    "type": "string"
                  },
                  "needsApproval": {
                    "type": "boolean",
                    "description": "The step has an external effect and will stop for approval when it runs."
                  },
                  "status": {
                    "enum": [
                      "running",
                      "done",
                      "failed",
                      "pending",
                      "waiting",
                      "skipped"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "What happened, for a step that's running, waiting, failed or skipped."
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onRun",
              "required": true,
              "params": [
                {
                  "name": "steps",
                  "type": "PlanStep[]"
                }
              ],
              "description": "Runs the plan as the user left it — reordered, trimmed, or extended."
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            },
            {
              "name": "onStop",
              "required": false,
              "params": [],
              "description": "Shown throughout the run, never hidden behind a hover."
            },
            {
              "name": "onRetryStep",
              "required": false,
              "params": [
                {
                  "name": "stepId",
                  "type": "string"
                }
              ],
              "description": "Offered on a failed step."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-presence",
      "title": "Agent Presence",
      "description": "An agent's live state, expressed visually rather than described in text. Takes a `state` from an eleven-value vocabulary grounded in real protocol events — eight machine states (`queued`, `thinking`, `deciding`, `working`, `streaming`, `done`, `failed`, `cancelled`) and three hand-off states (`suggesting`, `asking`, `awaitingApproval`) — plus an optional `label` that overrides the state's default text, and an `onCancel` shown throughout the machine's turn. The organising rule is a turn-taking grammar: motion means the machine is busy, stillness means it is the user's turn. Machine states animate; hand-off states stop completely and sit brighter and heavier, so a user learns to read 'am I needed?' without reading a word. The forms share the one vocabulary: `figure` draws the agent with Agent Figure (each state maps to a pose, and person's-turn poses hold still), `mark` draws only its head for inline use, `field` renders a cell matrix, `line` renders three inline dots for status bars and table rows, and the deprecated `orb` (a CSS 3D sphere, kept only so existing screens keep working; use Agent Figure for a visible agent). The text label is the real signal and is announced through an `aria-live` region; the indicator itself is `aria-hidden` and supplementary. Purely presentational: it does not fetch, does not own the agent run, and does not interpret protocol events for you — map your stream's events to a state and pass it in. Use it anywhere an agent's activity needs to be visible in ordinary product UI. Not for showing the reasoning text itself (that is Reasoning Stream), not for a per-artifact generation progress bar (that is Generation Placeholder), and not for the post-result action strip (that is AI Action Toolbar).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/agent-figure"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-presence/agent-presence.tsx",
          "target": "components/ui/agent-presence.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`, @pixeldosa/agent-figure for the `figure` and `mark` forms (installed automatically), and the `pd-orb-spin` / `pd-cell-pulse` keyframes supplied by @pixeldosa/pixeldosa-theme's css block. The state vocabulary maps directly onto AG-UI events, OpenAI Responses lifecycle events, AI SDK stream parts, LangGraph interrupts and MCP elicitation — see the docs page for the mapping table.",
      "meta": {
        "engineeringNotes": "The load-bearing decision is that the eleven states split into two classes with opposite visual treatments rather than into eleven variations of one treatment. Every surveyed implementation renders 'thinking' and 'waiting for your approval' in the same register — something moving plus a line of text — but those are opposites: one means relax, the other means nothing proceeds until you act. Encoding that as motion-versus-stillness makes the most important question on the surface ('is it my turn?') answerable pre-attentively. The vocabulary itself is taken from protocol events rather than invented: `deciding` exists because AG-UI streams TOOL_CALL_ARGS before execution, so choosing an action is distinguishable from performing it; `queued` exists because OpenAI Responses emits it and accepted-but-not-started is not the same as working; `asking` maps to MCP elicitation and `awaitingApproval` to LangGraph's interrupt(). The orb uses a Fibonacci spiral distribution rather than the flat ring of dots most AI orbs use, which is what makes it read as a volume rather than a circle, and it is rendered with CSS 3D transforms inside a preserve-3d container so perspective supplies depth scaling for free — no per-frame JavaScript and no canvas. All three forms animate via CSS keyframes with per-element negative delays rather than a requestAnimationFrame loop; an earlier component in this system was rebuilt for exactly this reason, since re-rendering hundreds of React nodes per frame to nudge a transform is what makes these indicators stutter.",
        "motionNotes": "Rotation speed is the state: 24s per revolution when queued, 12s thinking, 7s deciding, 4s working — so the same object communicates urgency through tempo rather than through a different animation per state, which is what lets states morph rather than cut. Terminal machine states (`done`, `failed`, `cancelled`) drop rotation entirely and settle to a smaller resting scale. Hand-off states stop dead: no keyframe is applied at all, and the dots transition to full scale and full opacity over `--pd-duration-slow`, so the arrival of the user's turn is a settling rather than a switch. Only `transform` and `opacity` animate, so every form composites on the GPU regardless of point count. Under `prefers-reduced-motion` every keyframe is disabled and each state still holds a visually distinct static configuration — differing scale and opacity per state — so the grammar survives with zero movement; the reduced-motion state is the same information without the motion, not a removal of the signal.",
        "schema": {
          "component": "AgentPresence",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentPresence",
            "type": "object",
            "properties": {
              "state": {
                "enum": [
                  "done",
                  "failed",
                  "thinking",
                  "working",
                  "asking",
                  "queued",
                  "deciding",
                  "streaming",
                  "cancelled",
                  "suggesting",
                  "awaitingApproval"
                ]
              },
              "form": {
                "enum": [
                  "figure",
                  "mark",
                  "line",
                  "field",
                  "orb"
                ]
              },
              "size": {
                "enum": [
                  "sm",
                  "lg",
                  "default"
                ]
              },
              "label": {
                "type": "string",
                "description": "Overrides the state's default label. For `working`, name the actual tool."
              },
              "hideLabel": {
                "type": "boolean",
                "description": "Hide the text label and show only the indicator. The label is still announced."
              }
            },
            "required": [
              "state"
            ]
          },
          "events": [
            {
              "name": "onCancel",
              "required": false,
              "params": [],
              "description": "Shown while it is the machine's turn. Never hidden behind a hover."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-schedule",
      "title": "Agent Schedule",
      "description": "When the agent runs on its own, in words anyone can check. Takes `triggers` (each a plain-words `when` — \"Every weekday at 9:00\", \"When an invoice arrives from a new supplier\", never a cron string — a `does` line phrased as the consequence, `enabled`, a pre-formatted `nextRun`, a `lastRun` summary with `lastFailed`, and `asksFirst` when a run will stop for approval), plus `onEnabledChange`, `onEdit` and `onRunNow`. Scheduled agents are where trust quietly breaks: the thing that runs at 9:00 every morning is the thing nobody is watching, and a schedule that has been failing for a week usually looks identical to one that is fine. So the last run's outcome sits on the row in the blocked colour when it failed, the next run is stated, and turning a trigger off is one control. Purely presentational: you own the scheduling, and you write the sentences. Use it in an agent's settings, or beside a Run Inbox. Not a cron editor, and not a calendar.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-schedule/agent-schedule.tsx",
          "target": "components/ui/agent-schedule.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Write `when` as a sentence a non-technical colleague could check, and keep the cron expression in your backend.",
      "meta": {
        "engineeringNotes": "Every trigger is caller-written prose rather than a structure the component renders into English, which is a deliberate trade: generated sentences from a rule object read fluently for simple cases and become nonsense at the first compound condition, and the person this surface protects is the one who has to verify that the sentence matches what the system does. The failed last run is rendered inline in the blocked colour rather than behind a status filter, because the state this component exists to prevent is a silent schedule failing unnoticed for a week. `asksFirst` is shown on the row so the approval behaviour of an unattended run is visible while it is being switched on, not discovered later. The switch is a real `role=\"switch\"` with an accessible name that includes the trigger's own sentence, so a screen-reader user hears which schedule they are turning off.",
        "motionNotes": "Only the switch moves, sliding its thumb over `--pd-duration-fast` so flipping one reads as a physical change rather than a repaint. Nothing else animates: this is a settings surface, and a schedule row that pulsed would imply the agent is running right now. Under `prefers-reduced-motion` the slide is dropped and the state is carried by position, colour and the accessible name.",
        "schema": {
          "component": "AgentSchedule",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentSchedule",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "triggers": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ScheduleTrigger"
                }
              },
              "emptyLabel": {
                "type": "string"
              }
            },
            "required": [
              "triggers"
            ],
            "$defs": {
              "ScheduleTrigger": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "when": {
                    "type": "string",
                    "description": "When it runs, in plain words: \"Every weekday at 9:00\", \"When an invoice arrives\nfrom a new supplier\". Never a cron string."
                  },
                  "does": {
                    "type": "string",
                    "description": "What it will do, phrased as the consequence."
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "nextRun": {
                    "type": "string",
                    "description": "Formatted by you: \"Tomorrow at 9:00\". Left out when it's off."
                  },
                  "lastRun": {
                    "type": "string",
                    "description": "How the last one went: \"Ran yesterday · 3 invoices matched\"."
                  },
                  "lastFailed": {
                    "type": "boolean",
                    "description": "Set when the last run had a problem, so an off-schedule failure is visible."
                  },
                  "asksFirst": {
                    "type": "boolean",
                    "description": "Set when a run of this will pause for approval."
                  }
                },
                "required": [
                  "id",
                  "when",
                  "does",
                  "enabled"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onEnabledChange",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                },
                {
                  "name": "enabled",
                  "type": "boolean"
                }
              ]
            },
            {
              "name": "onEdit",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onRunNow",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Runs one now, without waiting for its schedule."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "agent-steer",
      "title": "Agent Steer",
      "description": "Redirect a running agent without stopping it — 'not that file, this one'. Takes `running`, the `currentStep` the agent is on (so a redirect has something to be relative to), a caller-owned `steers` list where each redirect carries a real status — pending (sent, not yet acknowledged), queued (accepted, applies after the current step), applied, or declined with a reason — plus optional `suggestions` that fill the input for editing but never send, `onSteer(text, mode)`, and `onWithdraw(id)` for redirects the agent has not applied yet. Enter steers after the current step; interrupting mid-step is a separate, deliberate control, because stopping a tool call halfway can leave partial side effects. Status is never inferred from the key press: a redirect reads 'waiting for the agent' until the agent says otherwise, so it cannot look applied when it isn't. Use it beside a Live Status Line or Thinking Experience during long runs. Not a replacement for Stop, which stays visible, and not a chat composer.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/agent-steer/agent-steer.tsx",
          "target": "components/ui/agent-steer.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. The steers list is yours: set each status from what the agent actually reports. Keep a Stop control visible next to it — steering is not a replacement for stopping.",
      "meta": {
        "engineeringNotes": "Built for the gap between Stop and wait: in real use, people change their minds mid-run, and today's agent UIs make them throw away the work so far to say so. Two decisions carry the trust. First, the component owns no status. Each redirect's status comes from the caller, meaning from the agent: it shows 'Sent — waiting for the agent' until the agent acknowledges, so pressing Enter never makes a redirect look like it has taken effect. Declined redirects show the agent's reason rather than disappearing. Second, the default applies at the next step boundary, where the agent can re-plan cleanly. Interrupting is a separate, secondary control, because abandoning a tool call mid-flight can leave half-written files or half-sent requests, and that risk should be chosen rather than triggered by the Enter key. Redirects that haven't been applied can be withdrawn. Suggestions fill the input rather than sending, so a one-tap redirect is still the user's sentence. The live region announces status changes to the latest redirect in words that match the visible text.",
        "motionNotes": "Almost still. The agent's motion belongs to the status surface beside it, such as Live Status Line, not to the steer control. The one moving element is the shared `pd-shimmer` on a pending redirect's status. Under the turn-taking grammar that is the only state where work is genuinely happening on the machine's side for that redirect; queued, applied and declined are settled facts and hold still. Colour transitions on hover and focus run at `--pd-duration-instant`, and the shimmer falls back to static text under prefers-reduced-motion and where background-clip text is unsupported.",
        "schema": {
          "component": "AgentSteer",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AgentSteer",
            "type": "object",
            "properties": {
              "running": {
                "type": "boolean",
                "description": "Whether the agent is working. Steering only exists while there is a run to steer."
              },
              "currentStep": {
                "type": "string",
                "description": "What the agent is doing now, so a redirect has something to be relative to."
              },
              "steers": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/SteerMessage"
                }
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "One-tap redirects that fill the input for editing — never sent without the user."
              },
              "placeholder": {
                "type": "string"
              }
            },
            "required": [
              "running"
            ],
            "$defs": {
              "SteerMessage": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  },
                  "mode": {
                    "enum": [
                      "next",
                      "now"
                    ]
                  },
                  "status": {
                    "enum": [
                      "pending",
                      "queued",
                      "applied",
                      "declined"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "Agent-supplied context: when it will apply, or why it couldn't."
                  }
                },
                "required": [
                  "id",
                  "text",
                  "mode",
                  "status"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onSteer",
              "required": true,
              "params": [
                {
                  "name": "text",
                  "type": "string"
                },
                {
                  "name": "mode",
                  "type": "SteerMode"
                }
              ]
            },
            {
              "name": "onWithdraw",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Take back a steer the agent hasn't applied yet."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-action-toolbar",
      "title": "AI Action Toolbar",
      "description": "A keyboard-navigable toolbar of the standard post-generation actions on an AI-produced result — apply, explain, retry, regenerate, undo, report. Takes an `actions` array of `{ id, label, icon?, intent?, disabled?, busy? }` plus a single `onAction(actionId)` callback, and renders them as a row of real buttons with three levels of visual weight: `intent=\"primary\"` is filled (the recommended action, e.g. Apply), `secondary` is bordered (reversible alternatives, e.g. Regenerate), and `quiet` is underline-only (meta actions that don't change the result, e.g. Explain, Report, Undo). An optional `message` renders leading status text before the actions, which is how confirmation rows like 'Changes applied. Undo' are expressed. Implements the ARIA toolbar pattern properly — the whole strip is one tab stop with arrow keys, Home and End moving within it and skipping disabled actions — rather than the natural-tab-order approximation most implementations use. Busy state is a per-action prop owned by the caller, not internal state, so it reflects the caller's real in-flight request; while any action is busy the others disable, since two concurrent actions on one result is never a coherent request. Purely presentational otherwise: it does not fetch, does not own an undo timer, and does not own the result it acts on. Use it attached to any AI output the user must now decide about. Not for a selection-triggered floating toolbar (use Selection Actions), a text diff needing per-hunk accept/reject (use Diff Accept), or a single whole-field proposal (use Smart Field) — this is the shared control strip those patterns compose, not a replacement for them.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "actions",
        "application"
      ],
      "dependencies": [
        "class-variance-authority"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-action-toolbar/ai-action-toolbar.tsx",
          "target": "components/ui/ai-action-toolbar.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and `class-variance-authority`. No icon set is bundled — pass your own `size-4` icons via each action's optional `icon` prop, matching how Smart Field and Selection Actions already take icons. Exactly one action should carry `intent=\"primary\"`; the component does not enforce this, because a confirmation-only row (a lone quiet Undo) legitimately has none.",
      "meta": {
        "engineeringNotes": "An extraction, not new design: three already-shipped components had independently hand-rolled the same control strip. Smart Field renders Retry, Reject/Accept and an 'Value accepted. Undo' row; Diff Accept renders 'Changes applied. Undo' and a Regenerate-on-conflict button; Selection Actions renders Retry and Dismiss. Across those seven call sites exactly three button treatments recur verbatim — filled primary, bordered secondary, underline quiet — along with a character-for-character duplicated spinner SVG and a duplicated message+Undo confirmation row. Those three treatments became the CVA `intent` variants rather than being re-derived a fourth time. Two deliberate boundaries: (1) `busy` is a per-action prop rather than internal state, because every existing call site already holds the request and its AbortController, so owning a second copy here would let the toolbar's idea of 'in flight' drift from the caller's; (2) the undo *timer* stays with the consumer — Smart Field and Diff Accept both already run it inside their own status state machines, and absorbing it here would fight those machines rather than simplify them, so the toolbar renders the undo affordance while the consumer owns when it exists. The roving tabindex is kept as an action id rather than an index so the tab stop survives the actions array changing between renders, and it falls back to the first enabled action whenever the focused one becomes disabled — otherwise a toolbar whose focused action just disabled would have no tab stop at all and become unreachable by keyboard.",
        "motionNotes": "Deliberately minimal, and the restraint is the decision. Colour-only transitions on hover and focus at `--pd-duration-instant` (100ms) with `--pd-ease-standard`, matching every other control in the system. There is no entrance animation, on purpose: this toolbar is always revealed by a container that already animates — Smart Field's proposal panel owns a grid-template-rows expand, Selection Actions owns a fade-and-scale — so a second entrance here would double-animate the same appearance. That mirrors Confidence Meter's rule that motion belongs to what wraps a primitive, not the primitive itself. The one continuous animation is the busy spinner, which is honest: it maps to a real in-flight request and stops when that request settles. Under prefers-reduced-motion the colour transitions drop to instant via `motion-reduce:transition-none`, while the spinner deliberately keeps rotating — removing it would leave a busy state with no visible indication at all, and the reduced-motion contract is a fallback that preserves legibility, not a removal.",
        "schema": {
          "component": "AIActionToolbar",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIActionToolbar",
            "type": "object",
            "properties": {
              "actions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/AIAction"
                }
              },
              "message": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Leading status text, e.g. \"Changes applied.\" Renders before the actions."
              },
              "label": {
                "type": "string",
                "description": "Accessible name for the toolbar."
              }
            },
            "required": [
              "actions"
            ],
            "$defs": {
              "AIAction": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "icon": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "Optional leading icon. No icon set is bundled — pass your own `size-4` icon."
                  },
                  "intent": {
                    "enum": [
                      "primary",
                      "secondary",
                      "quiet",
                      "destructive"
                    ],
                    "description": "Visual weight. Exactly one action should be `primary`. Defaults to `secondary`."
                  },
                  "disabled": {
                    "type": "boolean"
                  },
                  "busy": {
                    "type": "boolean",
                    "description": "Owned by the consumer, not this component: the caller already holds the request\n(and usually its AbortController), so the toolbar reflects that state rather than\nkeeping a second copy that can drift out of sync with it."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onAction",
              "required": true,
              "params": [
                {
                  "name": "actionId",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-approval-gate",
      "title": "AI Approval Gate",
      "description": "A checkpoint before a consequential agent action — the human-in-the-loop surface for LangGraph's interrupt() and any equivalent pause. Takes an `action` phrased as a consequence the user can evaluate ('Send the Q3 summary to 243 contacts', never 'Execute send_email'), a `risk` level, a required `reversible` boolean, an optional `impact` list of countable facts describing the blast radius, optional `confidence` and `provenance`, optional `explanation` and `sources`, and three outcomes: `onApprove`, `onReject` and an optional `onEdit` for when the payload can genuinely be changed before it runs. `reversible` is required rather than optional because whether an action can be taken back is the single most decision-relevant fact on the surface and almost nothing surfaces it; it renders as a plain sentence rather than an icon. A `high` risk action renders its Approve control with the destructive treatment rather than the primary one, so a dangerous action never looks identical to a harmless confirmation. Composes this system's existing trust primitives rather than re-deriving them: Confidence Meter for certainty, AI Context Surface for the reasoning behind the proposal, and AI Action Toolbar for the decision strip — which means the decision controls inherit correct ARIA toolbar semantics, roving tabindex and busy handling for free. Holds perfectly still, since it is the user's turn under the turn-taking grammar; pair it with Agent Presence in the `awaitingApproval` state. Purely presentational: it does not execute the action or own the paused run. Use it before any external side effect — sending messages, spending money, publishing, deleting, or changing permissions. Not for disambiguating between options the agent cannot choose between (use Agent Ask), and not for reviewing a text rewrite hunk by hunk (use Diff Accept).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "actions",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/ai-context-surface",
        "@pixeldosa/confidence-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-approval-gate/ai-approval-gate.tsx",
          "target": "components/ui/ai-approval-gate.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Installing this pulls in AI Action Toolbar, AI Context Surface and Confidence Meter, which it composes. Phrase `action` as the consequence rather than the tool call — the whole component is worthless if the user cannot evaluate what they are approving.",
      "meta": {
        "engineeringNotes": "This is the payoff component for the system's trust thesis, and it is deliberately a composition rather than a new implementation: Confidence Meter, AI Context Surface and AI Action Toolbar are all consumed as-is, which is the 'compose existing trust primitives' rule (DESIGN.md §3.6) applied at the scale it was written for. Composing the toolbar rather than hand-rolling three buttons also means the decision strip inherits the real ARIA toolbar pattern — roving tabindex, arrow navigation, disabled-action skipping, busy handling — none of which a bespoke approval widget would have got right. Building it surfaced a genuine gap in that toolbar: it had no `destructive` intent, because nothing had needed one until an approval could be dangerous. Adding it there rather than special-casing here is the correct direction, since the house rule that a destructive action must never look identical to a harmless primary one applies system-wide. Two content decisions carry most of the value. `reversible` is a required prop rather than an optional flag: whether something can be undone determines how carefully the rest of the surface deserves to be read, and it renders as a plain sentence ('This cannot be undone') rather than an icon, because an icon is exactly the wrong weight for the most consequential fact present. The `impact` list exists because an approval without a scope is a yes/no question with the information removed — '243 recipients' is what makes the decision reviewable, and it uses tabular figures so the numbers align down the column.",
        "motionNotes": "Deliberately still. This is a user-turn surface under the turn-taking grammar (DESIGN.md §3a): motion means the machine is busy, and animating an approval gate would put it in the same visual register as the agent's own working states — precisely backwards, since nothing here proceeds until the user acts. The only motion is inherited: the composed AI Action Toolbar's colour transitions on hover and focus at `--pd-duration-instant`, and its busy spinner while an approve or reject is in flight, which maps to a real request rather than decorating a wait. AI Context Surface brings its own grid-template-rows disclosure transition when expanded. Nothing animates on entrance — that belongs to whatever surface reveals this gate, the same rule Confidence Meter and AI Action Toolbar hold. Under prefers-reduced-motion every inherited transition drops to instant and the component loses nothing, because it never carried information in movement to begin with.",
        "schema": {
          "component": "AIApprovalGate",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIApprovalGate",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "action": {
                "type": "string",
                "description": "What will happen, in consequence terms the user can evaluate — \"Send the Q3\nsummary to 243 contacts\", never \"Execute send_email\"."
              },
              "risk": {
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              },
              "reversible": {
                "type": "boolean",
                "description": "Whether this can be taken back. Stated plainly because it is the single most\ndecision-relevant fact on the surface, and almost nothing surfaces it."
              },
              "impact": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ApprovalImpact"
                },
                "description": "The blast radius as countable facts. This is what makes the action reviewable."
              },
              "confidence": {
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              },
              "provenance": {
                "type": "string"
              },
              "explanation": {
                "type": "string",
                "description": "Passed through to AI Context Surface — why the agent proposed this."
              },
              "sources": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ProvenanceSource"
                }
              },
              "busy": {
                "enum": [
                  "approve",
                  "reject"
                ],
                "description": "Which action is in flight, owned by the caller as everywhere else in this system."
              }
            },
            "required": [
              "action",
              "reversible"
            ],
            "$defs": {
              "ApprovalImpact": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  }
                },
                "required": [
                  "label",
                  "value"
                ]
              },
              "ProvenanceSource": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "snippet": {
                    "type": "string"
                  }
                },
                "required": [
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onApprove",
              "required": true,
              "params": []
            },
            {
              "name": "onReject",
              "required": true,
              "params": []
            },
            {
              "name": "onEdit",
              "required": false,
              "params": [],
              "description": "Offer only when the action's payload is genuinely editable before it runs."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-chat-experience",
      "title": "AI Chat Experience",
      "description": "A complete assistant conversation, orchestrated from shipped PixelDosa UI components: Message Scroller, Message, Prompt Composer, Suggestions, Code Block, Reasoning Stream, Tool Call Group, Inline Citations, Agent Presence and AI Action Toolbar. Takes `turns` as data. Each turn has a `role`, `text` (fenced code becomes Code Blocks) or custom `content`, and for assistant turns a caller-owned `status`, `reasoning`, `toolCalls`, `sources`, `suggestions` and `error`. Handlers are `onSend`, `onStop`, `onRetry`, `onContinue`, `onRegenerate` and `onApplyCode`, with an optional `renderText` for your markdown renderer, an `empty` state with starter suggestions, composer `controls` and attachments. The value is the orchestration a team would otherwise re-derive. Only one busy signal shows at a time: Agent Presence until reasoning or a tool call appears, then that trace, then the streaming cursor once text arrives. Reasoning stops streaming when the answer starts. Send becomes Stop exactly while the latest turn streams, and the composer stays editable. Follow-ups show only on the latest finished answer, and in `fill` mode they land in the composer. Unclosed code fences stream safely. Finished answers are announced once. Failed and stopped turns keep their content with Retry or Continue. Use it as the main assistant screen of a product.",
      "type": "registry:block",
      "categories": [
        "ai-assisted",
        "application",
        "block"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/message",
        "@pixeldosa/message-scroller",
        "@pixeldosa/prompt-composer",
        "@pixeldosa/suggestions",
        "@pixeldosa/code-block",
        "@pixeldosa/reasoning-stream",
        "@pixeldosa/tool-call-card",
        "@pixeldosa/inline-citations",
        "@pixeldosa/agent-presence",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/agent-figure"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-chat-experience/ai-chat-experience.tsx",
          "target": "components/blocks/ai-chat-experience.tsx",
          "type": "registry:block"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Installs the whole chat stack it composes. Keep `turns` in your state and update the latest assistant turn as your stream reports reasoning, tool calls, text and completion. Pass your markdown renderer as `renderText`.",
      "meta": {
        "engineeringNotes": "A Block, so an orchestration rather than new UI: every element is an existing component, and the contribution is the rules for combining them. Busy signals are exclusive. `presenceFor` shows Agent Presence only in the gaps where nothing else reports progress (before reasoning or tools exist, or between the last tool and the first token). A streaming reasoning trace or running tool call already says what is happening, and once text exists the Message's quantized cursor takes over, because two simultaneous 'working' indicators read as noise and make neither trustworthy. Reasoning is forced to stop streaming once answer text arrives, regardless of what the reasoning data says, since a ticking trace under a streaming answer misreports what the model is doing. `busy` derives from the latest assistant turn alone, so Send becomes Stop for exactly that window. The composer is controlled by the block, which lets a `fill` suggestion write into it while the user keeps editing during a stream. Suggestions render only on the latest assistant turn in 'done' status, and older turns' suggestions are hidden, so stale follow-ups never compete with current ones. Text goes through `splitCodeFences`, so an unclosed fence streams as a Code Block. Screen reader announcements are derived by diffing each turn's previous status: a transition out of 'streaming' produces one message (replied with a summary, failed, or stopped) and never a token stream. Turn data is plain objects, so it maps from any runtime (AI SDK, LangGraph, a custom SSE stream) without a dependency.",
        "motionNotes": "Inherited, not invented. The block adds no motion of its own. Agent Presence's line form moves while the agent thinks or works. Reasoning Stream and Tool Call Group shimmer only their verbs while running. The Message cursor pulses on the shared cell keyframe while text streams. Suggestions arrive with their staggered fade once an answer is done. The Message Scroller follows instantly while pinned and scrolls smoothly only on 'Jump to latest'. Because each part applies the same tokens and the turn-taking grammar, the whole screen has one rhythm: motion while the machine works, stillness when it's the user's turn. Every part handles prefers-reduced-motion itself, so the block needs no separate fallback.",
        "schema": {
          "component": "AIChatExperience",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIChatExperience",
            "type": "object",
            "properties": {
              "turns": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ChatTurn"
                }
              },
              "suggestionMode": {
                "enum": [
                  "fill",
                  "send"
                ],
                "description": "How follow-up suggestions behave. `fill` (default) puts them in the composer."
              },
              "assistantName": {
                "type": "string"
              },
              "empty": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "suggestions": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "prompt": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "id",
                            "label"
                          ]
                        }
                      ]
                    }
                  }
                },
                "required": [
                  "title"
                ],
                "description": "Shown when there are no turns yet."
              },
              "controls": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ComposerControl"
                }
              },
              "attachments": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ComposerAttachment"
                }
              },
              "placeholder": {
                "type": "string"
              },
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5
                ],
                "description": "Level of the empty-state title and of the Sources headings in answers (the title is gone once a conversation exists, so they share the level). Defaults to 2."
              },
              "expression": {
                "enum": [
                  "full",
                  "subtle",
                  "off"
                ],
                "description": "How much of the agent shows. `full` adds its drawn head as the assistant's avatar,\nwith a face that follows the turn; `subtle` (default) shows the head only beside\n\"Thinking\"; `off` uses plain dots."
              }
            },
            "required": [
              "turns"
            ],
            "$defs": {
              "ChatTurn": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "role": {
                    "enum": [
                      "user",
                      "assistant"
                    ]
                  },
                  "text": {
                    "type": "string",
                    "description": "Markdown-ish text. Fenced code becomes Code Blocks; the rest goes through `renderText`."
                  },
                  "content": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "Custom content instead of `text` — e.g. an answer marked up with `<Cite>`."
                  },
                  "status": {
                    "enum": [
                      "done",
                      "failed",
                      "streaming",
                      "stopped"
                    ],
                    "description": "Assistant turns. Defaults to \"done\"."
                  },
                  "error": {
                    "type": "string"
                  },
                  "reasoning": {
                    "type": "object",
                    "properties": {
                      "steps": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "streaming": {
                        "type": "boolean"
                      },
                      "durationMs": {
                        "type": "number"
                      }
                    },
                    "required": [
                      "steps"
                    ]
                  },
                  "toolCalls": {
                    "type": "array",
                    "items": {}
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/CitationSource"
                    },
                    "description": "Sources behind the answer. Rendered as a reference list; use `content` + `<Cite>` for inline markers."
                  },
                  "suggestions": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "prompt": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "id",
                            "label"
                          ]
                        }
                      ]
                    },
                    "description": "Follow-ups. Only shown on the latest assistant turn, once it's done."
                  }
                },
                "required": [
                  "id",
                  "role"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              },
              "CitationSource": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "publisher": {
                    "type": "string",
                    "description": "Who published it: \"Reuters\", \"Internal wiki\", \"Q3 board deck\"."
                  },
                  "date": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              },
              "ComposerControl": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Short label shown on the pill: \"Tone\", \"Length\", \"For\"."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "value",
                            "label"
                          ]
                        }
                      ]
                    }
                  },
                  "defaultValue": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "options",
                  "defaultValue"
                ]
              },
              "ComposerAttachment": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "detail": {
                    "type": "string",
                    "description": "e.g. \"PDF · 2.4 MB\"."
                  },
                  "status": {
                    "enum": [
                      "failed",
                      "uploading",
                      "ready"
                    ]
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onSend",
              "required": true,
              "params": [
                {
                  "name": "submission",
                  "type": "ComposerSubmission"
                }
              ]
            },
            {
              "name": "onStop",
              "required": false,
              "params": []
            },
            {
              "name": "onRetry",
              "required": false,
              "params": [
                {
                  "name": "turnId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onContinue",
              "required": false,
              "params": [
                {
                  "name": "turnId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onRegenerate",
              "required": false,
              "params": [
                {
                  "name": "turnId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onApplyCode",
              "required": false,
              "params": [
                {
                  "name": "code",
                  "type": "string"
                },
                {
                  "name": "language",
                  "type": "string | undefined"
                }
              ],
              "description": "Called on Apply in a code block."
            },
            {
              "name": "renderText",
              "required": false,
              "params": [
                {
                  "name": "text",
                  "type": "string"
                }
              ],
              "description": "Your markdown renderer. Defaults to paragraphs split on blank lines."
            },
            {
              "name": "onAttachFiles",
              "required": false,
              "params": [
                {
                  "name": "files",
                  "type": "File[]"
                }
              ]
            },
            {
              "name": "onRemoveAttachment",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onLoadOlder",
              "required": false,
              "params": [],
              "description": "Called near the top of the history."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-context-surface",
      "title": "AI Context / Provenance Surface",
      "description": "A collapsed-by-default 'Why this?' disclosure showing an AI-produced value's explanation, sources, and model metadata — the folded-together answer to Citation Card, Source Viewer, and Thinking Indicator, none of which make sense without a chat thread. Takes optional explanation (plain-language reasoning text), sources (an array of { label, url?, snippet? }, rendered as a numbered list, each a link when a url is supplied), model (a string name), and generatedAt (a string or Date, formatted as a relative time within 24 hours or an absolute date otherwise). Renders nothing if none of these props are supplied. The trigger is a small, muted button; expanding it reveals a quiet bg-muted panel with the explanation first, sources second, and a model/timestamp footer line last. Use anywhere an AI-influenced value needs transparency available on demand without competing with the content it explains — a Smart Field proposal, a table cell, a classification result from Selection Actions. Not for a full chat/conversation transcript, and not for a single short provenance caption (Smart Field already has its own inline provenance prop for that).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-context-surface/ai-context-surface.tsx",
          "target": "components/ui/ai-context-surface.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. `generatedAt` is formatted internally (relative within 24 hours via Intl.RelativeTimeFormat, an absolute locale date otherwise) rather than left to the consumer, since every consumer would otherwise reimplement the same relative-or-absolute decision. Renders `null` entirely when none of `explanation`/`sources`/`model`/`generatedAt` are supplied — a trigger with nothing behind it is worse than no component.",
      "meta": {
        "engineeringNotes": "Deliberately a disclosure (collapsed by default), not an always-visible panel: provenance detail is important to have available but wrong to force onto every view, since it would compete with the actual content for attention the moment it's always open — the same restraint Smart Field's inline `via {provenance}` caption already established, applied here at a larger, richer scale. Folds three previously-separate roadmap ideas (Citation Card, Source Viewer, Thinking Indicator) into one component because all three share the same real constraint this system cares about: working with zero chat thread present, unlike Vercel AI Elements' `Reasoning`/citation components, which are chat-message-scoped and excellent at that job specifically — competing with them there would be a bad trade, per the roadmap's own Reconciliation table. Timestamp formatting uses native `Intl.RelativeTimeFormat` rather than a date-formatting dependency, keeping this primitive dependency-free like every other component in the registry.",
        "motionNotes": "Expand/collapse reuses the same CSS grid-template-rows (0fr to 1fr) technique Smart Field's proposal panel and Diff Accept's conflict banner already use — a proven, token-free way to animate a height that depends on variable content, on --pd-duration-fast/--pd-ease-standard. The trigger's chevron rotates 90 degrees on open using --pd-duration-instant, matching Button's press-feedback timing, since it's an immediate state confirmation rather than content arriving. Under prefers-reduced-motion, both the panel's height transition and the chevron rotation drop via motion-reduce:transition-none, leaving the open/closed state instant but fully legible — aria-expanded already communicates the state to assistive technology regardless of the icon's rotation.",
        "schema": {
          "component": "AIContextSurface",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIContextSurface",
            "type": "object",
            "properties": {
              "explanation": {
                "type": "string"
              },
              "sources": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ProvenanceSource"
                }
              },
              "model": {
                "type": "string"
              },
              "generatedAt": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "x-type": "Date"
                  }
                ]
              },
              "triggerLabel": {
                "type": "string",
                "description": "Accessible label and visible text for the collapsed trigger."
              }
            },
            "$defs": {
              "ProvenanceSource": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "snippet": {
                    "type": "string"
                  }
                },
                "required": [
                  "label"
                ]
              }
            }
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-disclosure",
      "title": "AI Disclosure",
      "description": "What the AI did, what it used, and — when it matters — asking first. Two shapes in one component. `variant=\"notice\"` states a fact (\"Written by AI, from your project\") with the specifics one click away, so a label never becomes a wall of policy text; `variant=\"consent\"` asks before anything happens, shows the specifics up front, and gives both answers equal weight: same size, same prominence, nothing preselected, no styling that makes declining look like a mistake. Takes a `title`, a one-sentence `summary`, `facts` as label-and-value pairs (\"What it used\", \"How long it's kept\", \"Who can see it\"), an optional `learnMoreHref`, `onAllow` and `onDecline` for consent, and `onDismiss` for a notice. Purely presentational: it states what you tell it and reports the decision back. Use it on AI-written content, on recording or memory features, and anywhere a person should know or be asked. Not a cookie banner, and not a replacement for AI Approval Gate, which is about one consequential action rather than what the system does in general.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-disclosure/ai-disclosure.tsx",
          "target": "components/ui/ai-disclosure.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Write the `facts` as specifics about this feature, not policy boilerplate.",
      "meta": {
        "engineeringNotes": "Notice and consent are one component because they share a sentence, a set of facts and a visual home; splitting them produced two surfaces that drifted apart in wording, which is how products end up disclosing one thing and asking about another. What differs is only what the variant does with the facts: a notice hides them behind a disclosure button so the common case stays a single line, while consent renders them open, because a decision taken without the specifics is not consent and an expander is an easy thing to skip. Both consent buttons carry identical styling by construction rather than by convention — there is no primary variant available to the caller — so a product cannot make \"Allow\" the obvious answer. The consent variant intentionally offers no dismiss: a question you can close is a question that gets closed.",
        "motionNotes": "Nothing animates in either variant. Consent is the user's turn at its most consequential, and a notice is a statement of fact, so neither has anything to express through motion; the only transitions are the standard instant colour changes on buttons, disabled under `prefers-reduced-motion`.",
        "schema": {
          "component": "AIDisclosure",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIDisclosure",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "variant": {
                "enum": [
                  "notice",
                  "consent"
                ],
                "description": "`notice` states what happened or is happening. `consent` asks first and nothing\nproceeds until the person answers."
              },
              "title": {
                "type": "string",
                "description": "\"Written by AI\", \"This call will be recorded and summarised\"."
              },
              "summary": {
                "type": "string",
                "description": "One sentence in the user's words."
              },
              "facts": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/DisclosureFact"
                },
                "description": "The specifics, shown on demand for a notice and up front for consent."
              },
              "learnMoreHref": {
                "type": "string",
                "description": "Your policy page."
              },
              "learnMoreLabel": {
                "type": "string"
              },
              "allowLabel": {
                "type": "string"
              },
              "declineLabel": {
                "type": "string"
              }
            },
            "required": [
              "title"
            ],
            "$defs": {
              "DisclosureFact": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"What it used\", \"Who can see it\", \"How long it's kept\"."
                  },
                  "value": {
                    "type": "string",
                    "description": "\"Your last 20 messages in this project.\""
                  }
                },
                "required": [
                  "id",
                  "label",
                  "value"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onAllow",
              "required": false,
              "params": [],
              "description": "Consent only. Both answers are equally available; neither is preselected."
            },
            {
              "name": "onDecline",
              "required": false,
              "params": []
            },
            {
              "name": "onDismiss",
              "required": false,
              "params": [],
              "description": "Notice only. Lets the reader dismiss it once they've read it."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-form-fill",
      "title": "AI Form Fill",
      "description": "An agent filling out a form the user is already looking at — the embedded-in-product case rather than a chat. Takes a `fields` array describing each field and how to resolve its proposal, a controlled `values` record with `onValueChange`, an optional `onFill` hook that runs once per request so the whole batch can be fetched in a single round trip, and a required `triggerIcon` since no icon set is bundled. One action proposes values for every field at once, but every field is still reviewed on its own terms: each renders a Smart Field carrying its own confidence tier, provenance caption, explicit accept and reject, and one-step undo. The bulk request is the only thing that is bulk. There is deliberately no 'Accept all' control — per-field review is the entire point of the primitive underneath, and a button that let someone take twelve AI-proposed values in a single click would quietly undo the guarantee the whole AI tier is built on. Composes Smart Field, Field, AI Action Toolbar and Live Status Line, which transitively pulls Confidence Meter and Agent Presence. Fields propose together through a single token the block bumps, so no field gives up ownership of its own proposal state, undo window or in-flight request. Use it for enrichment and autofill inside real product forms — CRM records, onboarding, invoice details. Not for a single field, which is Smart Field used directly, and not for approving a consequential action, which is AI Approval Gate.",
      "type": "registry:block",
      "categories": [
        "ai-assisted",
        "forms",
        "block"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/smart-field",
        "@pixeldosa/field",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/live-status-line"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-form-fill/ai-form-fill.tsx",
          "target": "components/blocks/ai-form-fill.tsx",
          "type": "registry:block"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Installing this pulls in Smart Field, Field, AI Action Toolbar and Live Status Line, plus Confidence Meter and Agent Presence transitively. Fetch the whole batch once in `onFill` and have each field's `fetchProposal` read from that result — otherwise a five-field form makes five round trips for one user action.",
      "meta": {
        "engineeringNotes": "The load-bearing decision is what this block refuses to add. Bulk filling makes an 'Accept all' control feel obvious, and it is exactly wrong: Smart Field exists so that a proposed value is reviewed with its confidence and source before it becomes real data, and a single click that accepts twelve values with three different confidence tiers silently discards that. So the request is bulk and the review stays per-field — which is the honest reading of the system's 'nothing commits silently' rule rather than a convenience compromise. Making fields propose together required a small, consumer-driven addition to Smart Field rather than a workaround here: a `proposeToken` prop whose change requests a proposal. The alternatives were worse — remounting each field to re-fire `autoPropose` would have destroyed focus and in-flight requests, and hoisting proposal state into this block would have taken ownership of confidence, undo and abort away from the component that already does all of it correctly. The token deliberately ignores its first render, so mounting fields with a token present is not a request; that keeps unannounced fetching opt-in behind `autoPropose`, which is the posture the AI tier holds. `onFill` exists so the whole batch is one round trip: without it, a five-field form would make five separate model calls for a single user action, which is both slow and expensive in a way the user never asked for.",
        "motionNotes": "Almost nothing belongs to this block. Every moving part is inherited: Smart Field's proposal panel expands via the house grid-template-rows transition, Live Status Line swaps its status text and carries Agent Presence's indicator, and AI Action Toolbar supplies the fill button's colour transitions and its busy spinner during the batch request. The block itself adds no entrance animation and no motion of its own, which is the correct outcome for a composition — one rhythm, set once in the motion tokens, applied by the parts rather than re-stated by the whole. The one motion decision made here is that the status line appears only while a fill is actually in flight, so the surface is completely still whenever the agent is not working and the user is reviewing. Under prefers-reduced-motion every inherited transition drops to instant and each composed component handles its own fallback.",
        "schema": {
          "component": "AIFormFill",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AIFormFill",
            "type": "object",
            "properties": {
              "fields": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/FormFillField"
                }
              },
              "values": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "triggerIcon": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "No bundled icon set — supply your own `size-4` icon for each field's trigger."
              },
              "fillLabel": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            },
            "required": [
              "fields",
              "values",
              "triggerIcon"
            ],
            "$defs": {
              "FormFillField": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "placeholder": {
                    "type": "string"
                  },
                  "fetchProposal": {
                    "x-function": true,
                    "x-host": true,
                    "description": "Resolves this field's proposal. Usually reads from one batch the parent fetched."
                  }
                },
                "required": [
                  "name",
                  "label",
                  "fetchProposal"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onValueChange",
              "required": true,
              "params": [
                {
                  "name": "name",
                  "type": "string"
                },
                {
                  "name": "value",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onFill",
              "required": false,
              "params": [],
              "description": "Called once per fill request, before the fields propose. Use it to fetch the whole\nbatch in a single round trip — each field's `fetchProposal` then resolves from it."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ai-triage-table",
      "title": "AI Triage Table",
      "description": "Reviews many AI-proposed changes at once, organised around where human attention is needed rather than the order the AI produced them. Takes an `items` array of proposed changes — each with a `label`, an optional `before` value, the proposed `after` value, a `confidence` tier and an optional `provenance` — an optional `unchanged` list of things the AI examined and left alone, and an `onApply` callback that receives the ids the user accepted. Changes are grouped by confidence in attention order: low-confidence changes open first because they most need a human, medium follows, and high-confidence changes start collapsed to a count. Items the AI left unchanged collapse to a single line ('40 checked and left unchanged') so nobody scans forty rows to confirm nothing happened. Every row has its own Accept and Reject with a one-step Undo, before values are shown struck through beside the proposed value, and decisions are staged — nothing is written until Apply, which states how many changes it will commit and that unreviewed items stay as they are. It does not become an Accept-all: bulk acceptance exists only for the high-confidence group, and only after that group has been opened; low and medium changes must be decided individually. Composes AI Action Toolbar and Confidence Meter. Use it when AI output arrives in volume — enrichment across many records, bulk categorisation, data cleanup, batch corrections. Not for one field (use Smart Field), a single form (use AI Form Fill), or a text rewrite (use Diff Accept).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/confidence-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ai-triage-table/ai-triage-table.tsx",
          "target": "components/ui/ai-triage-table.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Installing this pulls in AI Action Toolbar and Confidence Meter. Pass `unchanged` whenever you have it — showing what the AI deliberately left alone is half of what makes review cheap.",
      "meta": {
        "engineeringNotes": "Built in direct response to the audit burden: AI now produces changes faster than people can check them, and every earlier component in this system enables review without reducing it, which at volume turns the user into a full-time auditor. Triage reduces review in three ways. It orders by attention rather than by arrival — low confidence first, high confidence collapsed — so effort lands where errors are likely. It collapses what the AI left unchanged into a count, since confirming that nothing happened is otherwise the most tedious part of reviewing a batch. And it states consequences at the point of commitment ('Apply 5', 'not reviewed will stay as they are'). The hard boundary is that it must not become Accept-all by another name. Bulk acceptance is limited to the high-confidence group, and gated on that group having been opened — tracked as an explicit inspected set, with the reason stated in text rather than hidden behind a disabled button. Decisions are staged rather than committed, so every row's Undo is simply removing a staged decision, with no undo timer to manage. Collapsed groups use the house grid-template-rows pattern with overflow-hidden, and are marked inert while closed: a 0fr row hides content visually but otherwise leaves its buttons focusable and exposed to assistive technology.",
        "motionNotes": "Deliberately quiet: this is a user-turn surface, so nothing moves on its own and nothing shimmers. Groups expand and collapse through the house grid-template-rows transition at `--pd-duration-fast`, the disclosure chevrons rotate at `--pd-duration-instant`, and a decided row fades to reduced opacity over `--pd-duration-fast` so the eye moves on to what remains without the row disappearing — its Undo stays reachable. Action colours transition through the composed AI Action Toolbar. There is no entrance animation and no motion tied to counts changing, since numbers updating in a review surface should read as information rather than event. Under prefers-reduced-motion every transition drops to instant and the grouping, counts and decided-state opacity still carry all the information.",
        "schema": {
          "component": "AITriageTable",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AITriageTable",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/TriageItem"
                }
              },
              "unchanged": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "label"
                  ]
                },
                "description": "Items the AI examined and chose not to change. Shown collapsed, so nobody scans for them."
              },
              "title": {
                "type": "string"
              }
            },
            "required": [
              "items"
            ],
            "$defs": {
              "TriageItem": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "What is being changed — a record, a field, a row."
                  },
                  "before": {
                    "type": "string",
                    "description": "The current value, shown struck through when present."
                  },
                  "after": {
                    "type": "string",
                    "description": "The AI's proposed value."
                  },
                  "confidence": {
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ]
                  },
                  "provenance": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "after",
                  "confidence"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onApply",
              "required": true,
              "params": [
                {
                  "name": "acceptedIds",
                  "type": "string[]"
                }
              ],
              "description": "Commits the accepted items. Nothing is written before this."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "autonomy-control",
      "title": "Autonomy Control",
      "description": "How much an agent may do without asking, as a setting people can actually read. Takes a `level`: 'ask' (propose everything), 'balanced' (handle small reversible things, ask before bigger ones) or 'act' (act, then report). It also takes the agent's real `actions`, each with a `risk` of low, medium or high, and optional `alwaysAsk` ids with `onAlwaysAskChange`. Instead of an abstract cautious-to-autonomous slider, each level is explained through those actions, and every row reads 'Asks first' or 'Does it, tells you', so choosing a level means choosing concrete outcomes. Risk sets a floor the user can raise but never lower: any action can be pinned to 'Always ask', and high-risk actions ask at every level. A live count ('Does 2 of 5 without asking') announces what a level change did. Use it in agent settings and onboarding. Not an approval prompt — that's AI Approval Gate, which this decides when to show.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/autonomy-control/autonomy-control.tsx",
          "target": "components/ui/autonomy-control.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. List real actions in the user's words, and put anything irreversible or external at `risk: \"high\"`. Enforce the same rule server-side: this control shows the policy, it doesn't enforce it.",
      "meta": {
        "engineeringNotes": "Research on agent products keeps finding people asking for lower automation and an off switch, while the 'match autonomy to risk' principle has no UI anywhere. Most autonomy settings fail because they are abstract. A slider labelled cautious-to-autonomous doesn't tell anyone what will happen to their inbox. So each level is explained through the agent's actual actions, with the outcome for each in words. The policy is a pure function of level and risk: 'ask' asks for everything, 'balanced' acts only on low risk, 'act' acts on low and medium. High risk asks at every level, and that is deliberately not configurable, because a setting that can turn off the approval for an irreversible send is a trap with a friendly label. User overrides only move in the safe direction: 'Always ask' pins an action to asking, and nothing un-pins the floor. Levels are native radios in a fieldset, so arrow keys, labels and form semantics come for free. The 'Does N of M without asking' count sits in a polite live region, because changing the level silently changes every row below it.",
        "motionNotes": "Still. This is a settings surface and the user's turn, so nothing animates on its own. Selection and hover change background and border colour at `--pd-duration-instant`. Row outcomes swap text instantly when the level changes, not with a transition, so the new policy can be read the moment it applies. The focus ring follows the radio via `has-[:focus-visible]`. Under prefers-reduced-motion the colour transitions become instant.",
        "schema": {
          "component": "AutonomyControl",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "AutonomyControl",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "level": {
                "enum": [
                  "ask",
                  "balanced",
                  "act"
                ]
              },
              "actions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/AutonomyAction"
                },
                "description": "The agent's real actions. The level is explained through these, not in the abstract."
              },
              "alwaysAsk": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Action ids the user has pinned to \"Asks first\" regardless of level."
              },
              "label": {
                "type": "string"
              }
            },
            "required": [
              "level",
              "actions"
            ],
            "$defs": {
              "AutonomyAction": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "A concrete action, in the user's words: \"Send an email\", \"Edit a draft\"."
                  },
                  "risk": {
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ]
                  }
                },
                "required": [
                  "id",
                  "label",
                  "risk"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onLevelChange",
              "required": true,
              "params": [
                {
                  "name": "level",
                  "type": "AutonomyLevel"
                }
              ]
            },
            {
              "name": "onAlwaysAskChange",
              "required": false,
              "params": [
                {
                  "name": "ids",
                  "type": "string[]"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "button",
      "title": "Button",
      "description": "A clickable control that triggers an action. Renders a native button element, or any element via the asChild prop when the trigger must be a link. Supports six visual variants (default, secondary, outline, ghost, destructive, link), four sizes including a square icon size, and a loading state that blocks interaction while announcing itself as busy to assistive technology. Use for actions such as submitting, saving or opening a dialog; use a plain anchor for navigation that changes the URL.",
      "type": "registry:ui",
      "categories": [
        "actions",
        "forms"
      ],
      "dependencies": [
        "@radix-ui/react-slot",
        "class-variance-authority",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/button/button.tsx",
          "target": "components/ui/button.tsx",
          "type": "registry:ui"
        }
      ],
      "cssVars": {
        "theme": {
          "--pd-duration-instant": "100ms",
          "--pd-ease-standard": "cubic-bezier(0.4, 0, 0.2, 1)"
        }
      },
      "docs": "Requires `cn` at `@/lib/utils`, which `npx shadcn init` creates. The press-feedback transition reads --pd-duration-instant and --pd-ease-standard; these are installed with this component, and the full token set arrives with the pixeldosa-theme item. When using asChild the rendered child receives aria-disabled rather than the disabled attribute, since non-button elements do not support it — handle click suppression in your own handler if the child is interactive.",
      "meta": {
        "engineeringNotes": "Variants are declared with CVA rather than conditional className strings so that the full variant matrix is a single serialisable object: it can be type-derived (VariantProps), documented from source, and extended by consumers via buttonVariants() without forking the component. The loading state disables the button but deliberately keeps it mounted and in the tab order — swapping a button for a spinner element moves focus to the document body mid-interaction, which is the most common accessibility regression in loading buttons. asChild uses Radix Slot instead of a render-prop so that ref forwarding and event composition are handled by the primitive rather than reimplemented here.",
        "motionNotes": "The only motion is a 2% active-state scale, driven by CSS transition rather than Motion — a button appears dozens of times per page and forcing a JS animation runtime into it would be a bundle cost with no interaction benefit. Duration (instant, 100ms) and easing come from CSS custom properties generated from the motion tokens, so the press feedback stays in step with the rest of the system. The motion solves one problem: confirming that a press registered before the resulting state change lands. Under prefers-reduced-motion the transform is dropped entirely and the state remains legible through the background and focus-ring changes, which is a fallback rather than a silent removal.",
        "schema": {
          "component": "Button",
          "element": "button",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Button",
            "type": "object",
            "properties": {
              "asChild": {
                "type": "boolean",
                "description": "Render the child element instead of a `<button>`, forwarding all styling and\nbehaviour onto it. Use for links that should look like buttons."
              },
              "loading": {
                "type": "boolean",
                "description": "Show a busy indicator and block interaction. The button stays in the tab\norder and is announced as busy rather than being removed from it."
              },
              "loadingLabel": {
                "type": "string",
                "description": "Accessible label announced while `loading` is true."
              }
            }
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "card",
      "title": "Card",
      "description": "A bordered content container exposed as composable sub-components — Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter, CardImage — rather than a single prop-driven component. Supports vertical (default) and horizontal orientations plus a compact showcase variant for preview-led catalogue tiles. CardImage reads the parent's orientation to switch between a top-rounded banner and a left-rounded side panel. Depth comes from a 1px border token rather than a shadow. Use to group related content on a page, including image-led listing cards and pricing cards built by composing these parts with your own content; use Dialog instead when the content needs to interrupt the user or block interaction.",
      "type": "registry:ui",
      "categories": [
        "content",
        "application"
      ],
      "dependencies": [
        "class-variance-authority"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/card/card.tsx",
          "target": "components/ui/card.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`, which `npx shadcn init` creates. CardTitle and CardDescription render `div` elements, not heading tags, matching the wider shadcn/ui convention — supply your own heading element as content if the card needs to participate in the page's document outline. In `orientation=\"horizontal\"`, Card does not auto-arrange its children into an image column and a content column — wrap the header/content/footer parts in your own flex column alongside CardImage.",
      "meta": {
        "engineeringNotes": "Exposed as eight composable parts rather than a single component with header/footer/title/image props, so a Block composing Card can rearrange or omit regions without fighting a monolithic API. CVA expresses two purposeful axes: orientation controls CardImage's banner-versus-side-panel geometry, while the showcase variant removes default vertical padding and gaps for preview-led catalogue cards. A pricing card still needs no special Card capability — it composes the existing parts and its own content. Depth uses border-border rather than a shadow, matching the system's dark-mode-first 'borders over shadows' decision documented in the design tokens.",
        "motionNotes": "None. Card is compositional infrastructure with no interaction state of its own — motion belongs on components built on top of it (a future SpotlightCard or an interactive card treatment), not baked into the base primitive, per the pillar rule that motion must earn its place.",
        "schema": {
          "component": "Card",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Card",
            "type": "object",
            "properties": {}
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "code-block",
      "title": "Code Block",
      "description": "Code in an AI answer, built for the fact that people paste it into real projects. `CodeBlock` takes `code`, optional `language` or `filename`, `streaming`, a pluggable `highlight(code, language)` renderer (bring Shiki or Prism, with no bundled grammar), `onApply` (sync or returning a promise), `showLineNumbers` and `collapseAfter`. While `streaming`, the header shows 'Writing' and Copy and Apply are disabled, so half a function can't be copied. Copy confirms 'Copied' or says 'Couldn't copy', both announced. Apply reports 'Applying', 'Applied' or 'Retry apply' from its promise. Long lines scroll horizontally by default, with a Wrap toggle. Blocks longer than `collapseAfter` lines (24) collapse behind 'Show all N lines'. The file also exports `splitCodeFences(markdown)`, which splits a possibly streaming markdown answer into text and code segments. An unclosed trailing fence becomes `{ complete: false }`, so it renders as a streaming CodeBlock instead of breaking layout. Use it in assistant messages, docs and diffs of generated code.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/code-block/code-block.tsx",
          "target": "components/ui/code-block.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Pass `streaming` while the fence is open. `splitCodeFences` tells you when that is. Highlighting is up to you: pass `highlight`, or leave it off for plain text.",
      "meta": {
        "engineeringNotes": "Built around the next action people take with generated code, which is copying it somewhere that matters. Streaming state is explicit: the group is `aria-busy`, the header reads 'Writing', and both Copy and Apply are disabled until the stream completes. Copy handles clipboard failure (permissions, insecure contexts) with a visible and announced 'Couldn't copy', because the silent alternative pastes stale clipboard contents. Apply is promise-aware, so the caller's real result drives 'Applying', 'Applied' or 'Retry apply'. `splitCodeFences` exists because markdown renderers handle an unclosed fence badly mid-stream. It detects fences with matching marker type and length (``` or ~~~, three or more), treats a trailing unclosed fence as an incomplete code segment, and leaves text segments for the caller's renderer. Highlighting is a render prop, so the registry item has zero dependencies and teams keep whichever highlighter their app already loads. Line numbers are a separate aria-hidden gutter computed from the line count, which works with any highlighter's output, and are hidden when wrapping, since wrapped lines would misalign them. A trailing newline is trimmed, so streams don't show a phantom empty line.",
        "motionNotes": "Motion only for machine work. 'Writing' in the header and 'Applying' on the button take the shared `pd-shimmer`, because both are the machine's turn. The code itself never shimmers or animates in, since it is content. Button feedback ('Copied', 'Applied') is a text swap, not an animation, so it can be read immediately, and 'Copied' reverts after 2s. Hover colour runs at `--pd-duration-instant`. The collapse fade is static. Everything is static under prefers-reduced-motion.",
        "schema": {
          "component": "CodeBlock",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CodeBlock",
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "language": {
                "type": "string"
              },
              "filename": {
                "type": "string",
                "description": "Shown instead of the language when the code belongs to a file."
              },
              "streaming": {
                "type": "boolean",
                "description": "The code is still arriving. Copy and Apply wait for the whole thing."
              },
              "applyLabel": {
                "type": "string"
              },
              "showLineNumbers": {
                "type": "boolean"
              },
              "collapseAfter": {
                "type": "number",
                "description": "Longer blocks collapse behind \"Show all N lines\". Set to `Infinity` to never collapse."
              }
            },
            "required": [
              "code"
            ]
          },
          "events": [
            {
              "name": "highlight",
              "required": false,
              "params": [
                {
                  "name": "code",
                  "type": "string"
                },
                {
                  "name": "language",
                  "type": "string | undefined"
                }
              ],
              "description": "Bring your own highlighter (Shiki, Prism, …). Return the highlighted markup for the\nwhole block. Without one, code renders as plain text, which is correct, just unstyled."
            },
            {
              "name": "onApply",
              "required": false,
              "params": [],
              "description": "Adds an Apply action. Resolve when done; reject to show a failure."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "command-menu",
      "title": "Command Menu",
      "description": "A global, keyboard-first overlay (⌘K/Ctrl+K) for finding resources and running actions across an app. Renders as a centered floating panel on desktop and a full-width sheet under the sm breakpoint. Exports CommandMenu (the controlled root), CommandMenuInput, CommandMenuList (which auto-renders a live result-count announcer), CommandMenuEmpty, CommandMenuGroup, CommandMenuSeparator, and CommandMenuItem (accepts onSelect, an optional leading icon, and an optional keybind hint rendered as a kbd badge). Composes cmdk for the ARIA combobox/filter engine under a Radix Dialog for focus trapping, portalling and dismiss-on-escape/outside-click, with a token-driven CSS fade+scale for entrance/exit. Controlled-only: there is no internal open state, because the global shortcut listener has to live in the host app regardless. Use for apps with enough destinations or repeat actions that menu-hunting has real cost — dashboards, admin panels, developer tools; do not use it for a menu triggered from one visible button or row, use a Menu or ContextMenu instead.",
      "type": "registry:ui",
      "categories": [
        "overlays"
      ],
      "dependencies": [
        "cmdk",
        "@radix-ui/react-dialog"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/command-menu/command-menu.tsx",
          "target": "components/ui/command-menu.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. The host app owns the global ⌘K/Ctrl+K keydown listener and the `open` boolean — this component has no built-in trigger or shortcut binding, matching its controlled-only API. `CommandMenuItem`'s `keybind` prop is a single visible string (e.g. \"⌘S\"), not per-key badges; split it yourself if you need individually boxed keys.",
      "meta": {
        "engineeringNotes": "Uses cmdk for the input/list/filter engine rather than hand-rolling the ARIA combobox contract (input keeps DOM focus while aria-activedescendant points at the highlighted listbox option) — that contract is easy to get subtly wrong and cmdk is the same engine Vercel's own Geist command menu and shadcn/ui's command component build on. Radix Dialog wraps the whole thing rather than cmdk's own Command.Dialog variant, so PixelDosa UI keeps ownership of the visual contract instead of inheriting cmdk's default dialog chrome. Deliberately does NOT compose the shared `overlay` primitive (Motion-driven), despite that being the original plan: `overlay`'s AnimatePresence usage has a confirmed upstream bug (motiondivision/motion#3243) where the exiting panel and its full-viewport scrim sometimes never unmount, permanently blocking further page interaction after the first close — reproduced independently in Overlay's own existing demo, in both dev and production builds, so it is a primitive-level defect, not a composition mistake here. This component instead drives its fade+scale with a plain two-phase (mount in the closed state, flip to open on the next animation frame) CSS transition gated by a deterministic `setTimeout`-based delayed-unmount hook, reading the same `--pd-duration-fast`/`--pd-ease-standard` tokens `overlay` uses, so the visual result is identical while sidestepping the bug entirely. Recommend re-composing `overlay` here once its exit-completion issue is fixed upstream or worked around at the primitive level. Controlled-only open state (no internal fallback) because the consumer's global keydown listener already has to own the boolean. Also works around a second, unrelated composition gotcha: cmdk's own keydown handling on the Command root prevents Escape from reaching Radix's document-level dismissable-layer listener when cmdk is composed manually instead of via its own Command.Dialog, so Escape-to-close is handled explicitly rather than left to Radix alone. The result-count aria-live announcer is rendered automatically inside CommandMenuList via cmdk's useCommandState hook rather than left for every consumer to wire up. A separate kbd primitive was considered (per the component plan) and deliberately deferred: the keybind badge is a few lines of markup used only inside CommandMenuItem for v1, and extracting it into its own registry item before a second consumer needs it would be speculative.",
        "motionNotes": "Fade (opacity) plus a slight scale-in (0.98 → 1) on both the scrim and panel, matching the shared overlay primitive's visual signature even though it isn't composed directly here (see engineeringNotes). Mounts in the closed state, then flips to the open data-state on the next animation frame so the browser actually has a 'from' value to transition from — setting both in the same commit would skip the transition entirely. Duration and easing come from --pd-duration-fast/--pd-ease-standard, the same tokens Overlay uses, so the timing stays in step with the rest of the system. Exit is handled by a matching deterministic delay (150ms, comfortably above the fast token) before the panel actually leaves the DOM, so the fade-out is never cut short. The mobile sheet layout (full-width, pinned near the viewport bottom under the sm breakpoint) intentionally keeps the same fade+scale rather than a directional slide-from-bottom, per the component plan's call to reuse one motion signature rather than add a second pattern for one breakpoint. Highlighted-row background change on CommandMenuItem uses --pd-duration-instant with --pd-ease-standard, matching Button's press-feedback timing, because it is state feedback rather than decoration. Under prefers-reduced-motion, both the panel/scrim transition and the row-highlight transition are removed via motion-reduce:transition-none (scale pinned to 100% via motion-reduce:scale-100), leaving the opacity and background-colour changes themselves intact so every state change stays legible without motion.",
        "schema": {
          "component": "CommandMenu",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CommandMenu",
            "type": "object",
            "properties": {
              "open": {
                "type": "boolean",
                "description": "Controlled open state. There is no uncontrolled fallback — see engineeringNotes."
              },
              "label": {
                "type": "string",
                "description": "Accessible dialog name. Not visible; announced once when the menu opens."
              }
            },
            "required": [
              "open"
            ]
          },
          "events": [
            {
              "name": "onOpenChange",
              "required": true,
              "params": [
                {
                  "name": "open",
                  "type": "boolean"
                }
              ]
            }
          ]
        },
        "subcomponents": [
          {
            "component": "CommandMenuInput",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "CommandMenuInput",
              "type": "object",
              "properties": {}
            },
            "events": []
          },
          {
            "component": "CommandMenuList",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "CommandMenuList",
              "type": "object",
              "properties": {}
            },
            "events": []
          },
          {
            "component": "CommandMenuEmpty",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "CommandMenuEmpty",
              "type": "object",
              "properties": {}
            },
            "events": []
          },
          {
            "component": "CommandMenuGroup",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "CommandMenuGroup",
              "type": "object",
              "properties": {}
            },
            "events": []
          },
          {
            "component": "CommandMenuItem",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "CommandMenuItem",
              "type": "object",
              "properties": {
                "icon": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "$ref": "#/$defs/UINode"
                    },
                    {
                      "type": "array",
                      "items": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "$ref": "#/$defs/UINode"
                          }
                        ]
                      }
                    }
                  ],
                  "x-react": "ReactNode",
                  "description": "Leading icon, muted by default so it recedes behind the label. size-4 recommended."
                },
                "keybind": {
                  "type": "string",
                  "description": "Rendered visibly in the row and, because it stays in the item's text flow rather\nthan an aria-hidden decoration, announced as part of the option's accessible name —\nsatisfying \"hotkeys are both visible and announced\" without extra ARIA wiring."
                }
              },
              "$defs": {
                "UINode": {
                  "type": "object",
                  "description": "A nested component: a name from this catalogue, and the props to give it.",
                  "required": [
                    "component"
                  ],
                  "properties": {
                    "component": {
                      "type": "string",
                      "description": "The component's exported name, e.g. \"MediaResult\"."
                    },
                    "props": {
                      "type": "object",
                      "description": "That component's own props, matching its entry in this catalogue."
                    }
                  }
                }
              }
            },
            "events": [
              {
                "name": "onSelect",
                "required": true,
                "params": [
                  {
                    "name": "value",
                    "type": "string"
                  }
                ]
              }
            ]
          }
        ]
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "compare-view",
      "title": "Compare View",
      "description": "Before and after, without the usual accessibility failure. Takes `before` and `after`, each with your own `content` and a required `label` that doubles as its description, a starting `view` of `slider` or `side`, `allowViewChange`, an `aspectRatio` so nothing jumps while media loads, and an optional `onChoose` returning `before` or `after`. The slider is a native `input[type=range]`, which is the point: comparison sliders in AI editing tools are almost all mouse-only, built from pointer maths with no keyboard path and no announcement, while a range input gives arrow keys, Home and End, touch, and a labelled value screen readers can read (\"60% AI edit, clear sky\"). The after side is clipped rather than resized, so both halves stay pixel-aligned, and side-by-side is one click away for anyone who would rather see both at once. Purely presentational: you render both sides. Use it for edits, upscales, restorations and style transfers. Not for several results of one prompt (that is Variation Grid), and not for text, which needs Diff Accept.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/compare-view/compare-view.tsx",
          "target": "components/ui/compare-view.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Write both `label`s as real descriptions: they are the accessible text for each side.",
      "meta": {
        "engineeringNotes": "The slider is a real range input for one reason that outweighs the styling cost: every keyboard, touch and assistive-technology behaviour comes from the platform, and the custom handles these tools ship are the single most common accessibility failure in AI editing UIs. Revealing is done with `clip-path` on an absolutely positioned overlay rather than by resizing the after side, because resizing re-lays-out the media and the two halves drift apart by a pixel or two at every step — visible exactly where people are looking for differences. `aria-valuetext` counts down from the slider value — the handle is the divider, so a higher value uncovers *less* of the new image — and names the side it is measuring, so the announcement matches the screen instead of contradicting it. Labels are required rather than optional on both sides, since a comparison whose halves are unlabelled is meaningless to anyone not looking at it.",
        "motionNotes": "Nothing animates. The handle follows the input directly with no easing, because a comparison is a direct-manipulation surface where a lagging reveal reads as broken rather than smooth. The view toggle changes layout instantly for the same reason, and `prefers-reduced-motion` has nothing to disable.",
        "schema": {
          "component": "CompareView",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CompareView",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "before": {
                "type": "object",
                "properties": {
                  "content": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode"
                  },
                  "label": {
                    "type": "string"
                  }
                },
                "required": [
                  "content",
                  "label"
                ],
                "description": "What it was. Give it a real description: this is also its alt text."
              },
              "after": {
                "type": "object",
                "properties": {
                  "content": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode"
                  },
                  "label": {
                    "type": "string"
                  }
                },
                "required": [
                  "content",
                  "label"
                ],
                "description": "What the AI made of it."
              },
              "view": {
                "enum": [
                  "slider",
                  "side"
                ],
                "description": "Start in `slider` (one image, a handle) or `side` (both, at once)."
              },
              "allowViewChange": {
                "type": "boolean",
                "description": "Locks the view when your layout only fits one of them."
              },
              "chooseBeforeLabel": {
                "type": "string"
              },
              "chooseAfterLabel": {
                "type": "string"
              },
              "aspectRatio": {
                "type": "string",
                "description": "Aspect ratio of the frame, so nothing jumps while the media loads."
              }
            },
            "required": [
              "before",
              "after"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onChoose",
              "required": false,
              "params": [
                {
                  "name": "choice",
                  "type": "\"before\" | \"after\""
                }
              ],
              "description": "Keeps whichever one the person picks."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "confidence-meter",
      "title": "Confidence Meter",
      "description": "A tiered (low/medium/high) confidence indicator with an optional provenance caption, shared by every AI-tier component that needs to communicate uncertainty about a proposed value, classification, or decision. Renders a three-bar filled/unfilled indicator (1/2/3 bars for low/medium/high) immediately followed by a text label ('Low confidence', 'Medium confidence', 'High confidence') — never a raw percentage, since that would claim precision the underlying model doesn't actually have. An optional provenance string renders as a second 'via {provenance}' caption alongside the tier. Purely presentational: no state, no async behaviour, a pure function of its props. Use anywhere an AI-proposed value or decision carries a confidence signal (Smart Field, Diff Accept, Selection Actions, and the future AI Approval Gate all compose this rather than each re-deriving their own indicator). Not for a full sources/context/explanation surface — use AI Context / Provenance Surface for that instead.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/confidence-meter/confidence-meter.tsx",
          "target": "components/ui/confidence-meter.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Has no client-side behaviour (no state, effects, or event handlers), so it carries no `\"use client\"` directive and can render on the server.",
      "meta": {
        "engineeringNotes": "Extracted from two already-shipped components (Smart Field, Selection Actions) that had independently hand-rolled the identical tiered indicator — the concrete signal per the system's own 'a component is not an island' rule that this belongs as its own primitive rather than being re-derived a third time. Confidence is a three-tier enum, not a raw number, by deliberate and unchanged decision from Smart Field's original plan: a fabricated percentage would claim precision the model doesn't actually have, and the system's token set has no 'success'/'warning' hue to spend on a red/yellow/green scale anyway — only `destructive` keeps real chroma. A numeric-score-to-tier bucketing input was considered and deferred: no current consumer has a raw calibrated score to map, and adding that path now would solve a hypothetical need rather than an observed one. Provenance is folded into this component as an optional prop rather than left as a sibling element every consumer composes separately, since the two already-shipped call sites both rendered them side by side anyway — one import now covers both.",
        "motionNotes": "None, deliberately. This is a static presentational primitive with no state transitions of its own — any entrance/exit motion belongs to whatever container shows it (e.g. Smart Field's proposal panel), the same way Card's motion belongs to whatever wraps it, not to the primitive itself. Nothing here required a component-specific motion or reduced-motion branch, which is itself a signal the component is doing exactly what it needs to and no more.",
        "schema": {
          "component": "ConfidenceMeter",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ConfidenceMeter",
            "type": "object",
            "properties": {
              "confidence": {
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              },
              "provenance": {
                "type": "string",
                "description": "Optional source caption, rendered as \"via {provenance}\" alongside the tier."
              }
            },
            "required": [
              "confidence"
            ]
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "cost-estimate",
      "title": "Cost Estimate",
      "description": "What a run will cost and how long it will take, shown before it starts. Takes a `cost` range (`low` and optional `high`, formatted by you in money or credits), an optional `duration` in plain words, a `confidence` tier from Confidence Meter, a `basis` naming where the estimate comes from, an `items` breakdown of what the spend is made of, and a `balance` with the user's remaining budget or credits — including `exceeds` when this run would go past it, which is stated before the button rather than after. A range is the default shape on purpose: a single confident figure claims precision an estimate doesn't have, and the confidence is a tier rather than a percentage for the same reason. Purely presentational and provider-free: you compute the numbers from your own pricing and history and pass them in formatted, so the component never guesses a currency, a model price or a token count. Holds completely still — it is the user's turn. Use it before a costly or long run, beside Intent Preview or Agent Plan. Not for live spend during a run, not for a billing page, and not for showing raw token counts, which are not a unit people can judge.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/confidence-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/cost-estimate/cost-estimate.tsx",
          "target": "components/ui/cost-estimate.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and @pixeldosa/confidence-meter, which installs automatically. Format `cost`, `duration` and `balance` yourself: the component never formats currency or time.",
      "meta": {
        "engineeringNotes": "The API refuses the two lies this surface usually tells. First, a single figure: `cost` is a range, and a caller who genuinely knows the price passes only `low`, which makes precision an explicit choice rather than the default. Second, a percentage of certainty: confidence reuses the shipped three-tier Confidence Meter instead of inventing a number, and `basis` names the evidence so the estimate can be judged. Everything arrives pre-formatted as strings because currency, locale and credit units belong to the host product; a component that formatted money would have to guess a currency and would be wrong in half the places it renders. The only proportion drawn is the budget bar, which comes from a measured `used` value — the same rule the rest of the system applies to progress. `exceeds` is a separate flag rather than something derived from the numbers, because only the caller knows whether the estimate's high end, the low end, or an average is the figure that counts against a budget.",
        "motionNotes": "Nothing moves. This is the user's turn in the fullest sense — a decision about money — so the component holds completely still, with no shimmer, pulse or progress. The only transitions are the standard instant colour changes on its buttons, and those are disabled under `prefers-reduced-motion`.",
        "schema": {
          "component": "CostEstimate",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CostEstimate",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "task": {
                "type": "string",
                "description": "What the run is, in the user's words: \"Research the top 5 competitors\"."
              },
              "cost": {
                "$ref": "#/$defs/CostRange",
                "description": "What it will spend. A range is the honest shape of an estimate."
              },
              "duration": {
                "type": "string",
                "description": "How long it will take: \"about 3 to 5 minutes\". Plain words, not a countdown."
              },
              "confidence": {
                "enum": [
                  "low",
                  "medium",
                  "high"
                ],
                "description": "How good the estimate is. Tiers, never a percentage: an estimate that claims\nprecision it doesn't have is worse than no estimate."
              },
              "basis": {
                "type": "string",
                "description": "Where the estimate comes from: \"your last 20 runs\"."
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/CostItem"
                },
                "description": "What the spend is made of. Shown as a short list, never a chart."
              },
              "balance": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "description": "\"1,250 credits left\", \"$18.40 of $50 left\"."
                  },
                  "used": {
                    "type": "number",
                    "description": "0–1, measured. Renders a bar; leave it out when you only have words."
                  },
                  "exceeds": {
                    "type": "boolean",
                    "description": "Set when this run would take them past their budget."
                  }
                },
                "required": [
                  "label"
                ],
                "description": "What's left, if the user has a budget or a credit balance."
              },
              "runLabel": {
                "type": "string"
              },
              "adjustLabel": {
                "type": "string",
                "description": "\"Use a cheaper model\", \"Narrow the sources\"."
              }
            },
            "required": [
              "cost"
            ],
            "$defs": {
              "CostRange": {
                "type": "object",
                "properties": {
                  "low": {
                    "type": "string",
                    "description": "Formatted by you, in your currency or unit: \"$1.20\", \"600 credits\"."
                  },
                  "high": {
                    "type": "string",
                    "description": "Leave out for a single figure you're sure of."
                  }
                },
                "required": [
                  "low"
                ]
              },
              "CostItem": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Reads 240 pages\", \"12 image generations\"."
                  },
                  "amount": {
                    "type": "string",
                    "description": "What it costs, already formatted by you: \"$0.40\", \"120 credits\", \"~2 min\"."
                  },
                  "detail": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onRun",
              "required": false,
              "params": []
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            },
            {
              "name": "onAdjust",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "credits-meter",
      "title": "Credits Meter",
      "description": "What's left, and what this will cost — before the click. Takes the `remaining` balance and optional `allowance`, both formatted by you, a measured `used` proportion, a `nextAction` naming the action and its `cost` together with `affordable` when it exceeds the balance, `resets` for when the balance renews or expires, `recent` for movement including refunds, `low` to mark a balance worth flagging, and `onTopUp`. The pairing of action and price is the point: buyers of these tools rank credit opacity as their first frustration, and the specific complaint is that you cannot tell what an action will cost until it has been spent. Purely presentational: you compute the numbers and format the units. Use it beside a generate button, in a studio's sidebar, or above a batch. Not a billing page, and not a place for raw token counts — convert to the unit your users are charged in.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/credits-meter/credits-meter.tsx",
          "target": "components/ui/credits-meter.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Keep `nextAction.label` specific to the click it sits beside: '8 images · 1024 × 1024', not 'Generate'.",
      "meta": {
        "engineeringNotes": "`nextAction` is a single object rather than separate label and cost props so the two can never be rendered apart: a price with no action, or an action with no price, is the pattern the component exists to replace. `affordable` is a caller flag instead of something derived from parsing the strings, because balances and costs arrive as formatted text in whatever unit and locale the product uses, and a component that tried to compare them would have to parse money. The bar is drawn only from a measured `used` value, which keeps it consistent with the rest of the system's rule that a proportion must be real, and it draws what is *left* rather than what is spent: it sits under a figure that counts down, and a bar filling up beside a balance running out reads as plenty-left at the exact moment it is not. Refunds get a place in `recent` because generation pipelines refund routinely and almost no interface shows it, which leaves people believing failures cost them money.",
        "motionNotes": "Nothing animates. A balance is a fact and the next action's price is a decision point, so neither has anything to express through motion; only the standard instant colour change on the top-up button, disabled under `prefers-reduced-motion`.",
        "schema": {
          "component": "CreditsMeter",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CreditsMeter",
            "type": "object",
            "properties": {
              "remaining": {
                "type": "string",
                "description": "What's left, formatted by you: \"1,250 credits\", \"$18.40\"."
              },
              "allowance": {
                "type": "string",
                "description": "The full allowance, when there is one: \"2,000 credits this month\"."
              },
              "used": {
                "type": "number",
                "description": "How much of the allowance is already spent, 0–1, measured. Renders a bar; leave it\nout when you only have words. The bar draws what is *left*, so it empties as the\nfigure beside it falls."
              },
              "nextAction": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "cost": {
                    "type": "string"
                  },
                  "affordable": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "label",
                  "cost"
                ],
                "description": "What the next action will take, named and priced: the thing reviewers say is missing\neverywhere. \"4 images · 40 credits\"."
              },
              "resets": {
                "type": "string",
                "description": "When the balance resets or expires, in plain words: \"Resets on 1 October\"."
              },
              "recent": {
                "type": "string",
                "description": "Recent movement, including refunds: \"12 refunded yesterday when a batch failed\"."
              },
              "low": {
                "type": "boolean",
                "description": "Set when the balance is low enough to say so."
              },
              "topUpLabel": {
                "type": "string"
              },
              "label": {
                "type": "string"
              }
            },
            "required": [
              "remaining"
            ]
          },
          "events": [
            {
              "name": "onTopUp",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "diff-accept",
      "title": "Diff Accept",
      "description": "Reviews an AI-proposed text change as an inline word-level diff, resolved hunk by hunk rather than all-or-nothing. Takes baseValue (the text the proposal was generated against), value (the current live value), and proposedValue (the AI's suggested replacement), and computes the diff between baseValue and proposedValue using the diff (jsdiff) library. Each contiguous run of changed words becomes one hunk with its own Accept/Reject controls, shown inline as struck-through removed text and underlined/tinted added text. If value no longer matches baseValue — the underlying text changed since the proposal was generated — the diff is not shown; a conflict banner renders instead with an optional onRegenerate callback. Apply commits the current resolution (accepted hunks use the proposed text, pending or rejected hunks keep the original) via onApply, with a brief Undo affordance after. Use for reviewing AI rewrites of existing text — a bio, a description, any content field — where accepting some proposed changes and rejecting others is a real, expected outcome. Not for a single atomic value with no internal structure to partially accept — use smart-field for that.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [
        "diff"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/diff-accept/diff-accept.tsx",
          "target": "components/ui/diff-accept.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Uses diffWords from the diff package for word-level diffing — not a line-level diff, since this targets prose fields, not code. Unlike smart-field/ghost-input, this component does not call an AI provider itself; the consumer supplies an already-generated proposedValue (which may itself come from a smart-field-style fetch). baseValue must be exactly the text the proposal was generated against, not the current value, or the conflict guard will trigger on every render.",
      "meta": {
        "engineeringNotes": "Uses diff (jsdiff) for the actual word-level diff computation rather than hand-rolling a diff algorithm — the same relationship command-menu has with cmdk: a solved problem with real edge cases (Unicode, whitespace runs, word-boundary detection) not worth re-solving here. Hunks are grouped from the raw diff parts by merging consecutive added/removed runs into one reviewable unit, so a 'replace X with Y' shows as a single hunk with both the removal and the replacement, not two separate uncoordinated changes. Unresolved (pending) hunks default to the original text when merging, never the proposed one — an AI change only lands in the final result if a hunk is explicitly accepted, matching the system-wide 'never silently commit' rule. The status model is a three-state machine (reviewing/applied/done) rather than a single boolean: collapsing 'applied' and 'done' into one flag caused a real bug during development, where the conflict check (value !== baseValue, which becomes true immediately after Apply changes the live value) would incorrectly override the Undo affordance and show a conflict banner instead — the fix was ordering status checks so 'applied'/'done' short-circuit before the conflict check ever runs, since conflict is only a meaningful question while still reviewing.",
        "motionNotes": "The system has no 'success'/'added' hue — destructive is the only token keeping real chroma — so additions and removals are distinguished primarily by text decoration (strikethrough for removed, underline plus a bg-accent/40 tint for added), not a red/green colour scheme, with destructive reserved for the removed text only since deletion is the one case that's legitimately danger-adjacent. This is also more accessible than colour alone, not just a token-budget workaround. Colour and background transitions on hunk resolution use --pd-duration-instant/--pd-ease-standard, matching Button's press-feedback timing, since resolving a hunk is an immediate state confirmation. Apply's brief settle state mirrors Smart Field's accept feedback for the same component family consistency. Under prefers-reduced-motion, all colour and background transitions drop via motion-reduce:transition-none, leaving the underlying decoration (strikethrough/underline) and resolved text fully legible without animation.",
        "schema": {
          "component": "DiffAccept",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "DiffAccept",
            "type": "object",
            "properties": {
              "baseValue": {
                "type": "string",
                "description": "The text the proposal was generated against. Compared to `value` to detect conflicts."
              },
              "value": {
                "type": "string",
                "description": "The current live value. If it no longer matches `baseValue`, a conflict banner renders instead of the diff."
              },
              "proposedValue": {
                "type": "string",
                "description": "The AI-proposed replacement text."
              },
              "undoWindowMs": {
                "type": "number",
                "description": "How long the Undo affordance stays visible after applying, in ms."
              }
            },
            "required": [
              "baseValue",
              "value",
              "proposedValue"
            ]
          },
          "events": [
            {
              "name": "onApply",
              "required": true,
              "params": [
                {
                  "name": "mergedValue",
                  "type": "string"
                }
              ],
              "description": "Fires with the merged result once the user applies their resolutions."
            },
            {
              "name": "onRegenerate",
              "required": false,
              "params": [],
              "description": "Shown on conflict instead of the diff. Typically re-requests a fresh proposal against the current value."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "draft-mode",
      "title": "Draft Mode",
      "description": "The agent prepares everything and nothing leaves until a person sends it. Takes `enabled` with `onEnabledChange` for the mode itself, the `items` the agent has prepared (each with a `title` phrased as the consequence, an optional `target` naming who or what it touches, a `detail`, and `irreversible` for anything that can't be taken back), then `onSend`, `onDiscard` and `onEdit`. Items are chosen explicitly with checkboxes; the send button names the count, the line beside it says how many of the chosen items can't be undone, and nothing is ever sent by a single glance-and-click. With the mode off, a still, orange-marked strip states what now happens instead and offers to turn it back on. It is the opposite failure mode to an approval dialog: an approval interrupts once per action and trains people to click through, while draft mode lets the agent work and collects the output for one review at a time the person chooses. Holds completely still — everything here is the user's turn. Use it for agents that send email, post, publish or change live data. Not for a single high-risk action awaiting a decision (that is AI Approval Gate), and not for reviewing many AI-proposed edits row by row (that is AI Triage Table).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/draft-mode/draft-mode.tsx",
          "target": "components/ui/draft-mode.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. The component owns only the selection; the held items, the mode and what sending means stay with you.",
      "meta": {
        "engineeringNotes": "Selection is the component's only state, and it is pruned against `items` on every change: an id that has left the list is dropped, because a stale id surviving in the selection is exactly how a batch action sends the wrong thing. There is no accept-all shortcut hiding behind a single control — 'Select all' is a visible, reversible selection step before the send, and the send button carries the count so the action names its own size. Irreversibility is counted from the chosen items rather than shown per row only, so the sentence under the button changes as the selection changes and the warning cannot be scrolled past. The mode switch is a real `role=\"switch\"` with an explanatory sentence that rewrites itself for each state, instead of a settings label that leaves people guessing what 'off' does; turning it off does not hide the consequence, it states it in the waiting colour and offers the way back.",
        "motionNotes": "Nothing animates except the switch itself, which slides its thumb over `--pd-duration-fast` so the state change reads as a physical flip rather than a repaint. No item pulses, shimmers or slides in: this surface is entirely the user's turn, and motion here would imply the agent is still doing something. Under `prefers-reduced-motion` the switch transition is dropped and the state is carried by position, colour and the word On or Off.",
        "schema": {
          "component": "DraftMode",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "DraftMode",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "enabled": {
                "type": "boolean",
                "description": "On: the agent prepares everything and nothing leaves until the user sends it."
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/DraftItem"
                },
                "description": "What's waiting. Empty is a normal, quiet state."
              },
              "title": {
                "type": "string"
              },
              "offWarning": {
                "type": "string",
                "description": "What happens with draft mode off, in your product's words."
              },
              "emptyLabel": {
                "type": "string"
              }
            },
            "required": [
              "enabled",
              "items"
            ],
            "$defs": {
              "DraftItem": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "description": "What it is, as the consequence of sending it: \"Email Dana about the renewal\"."
                  },
                  "target": {
                    "type": "string",
                    "description": "Who or what it touches: \"dana@acme.com\", \"3 recipients\", \"the live pricing page\"."
                  },
                  "detail": {
                    "type": "string"
                  },
                  "irreversible": {
                    "type": "boolean",
                    "description": "Marks an item that can't be taken back once it goes."
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onEnabledChange",
              "required": true,
              "params": [
                {
                  "name": "enabled",
                  "type": "boolean"
                }
              ]
            },
            {
              "name": "onSend",
              "required": true,
              "params": [
                {
                  "name": "ids",
                  "type": "string[]"
                }
              ],
              "description": "Sends the chosen items. Phrase your confirmation as the consequence."
            },
            {
              "name": "onDiscard",
              "required": false,
              "params": [
                {
                  "name": "ids",
                  "type": "string[]"
                }
              ]
            },
            {
              "name": "onEdit",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "field",
      "title": "Field",
      "description": "A composable form-field wrapper providing Field, FieldLabel, FieldControl, FieldDescription and FieldError parts that share one generated id set and automatically wire aria-describedby and aria-invalid onto whatever control is placed inside FieldControl. Has no opinion on form-state management — works with plain useState, react-hook-form, or any other library. Use it as the base every labeled input, textarea or custom control composes on top of; it is the primitive that GhostInput, SmartField and every other AI-in-product component in this system builds on.",
      "type": "registry:ui",
      "categories": [
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/field/field.tsx",
          "target": "components/ui/field.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "FieldControl clones its single child rather than rendering a wrapper element, so pass exactly one React element (an <input>, <textarea>, or a component that forwards id/aria-invalid/aria-describedby/ref to a real form control) as its child.",
      "meta": {
        "engineeringNotes": "Built as a compound-component set sharing React context rather than a single Field component with many props, because form fields have a genuinely variable shape (some have descriptions, some don't; some controls are custom comboboxes, not <input>) and a large prop-driven API would force every consumer through the same limited combinations. FieldControl clones its child instead of rendering its own DOM node specifically so that input+icon or input+button compositions keep their existing layout — a wrapper div would break those. FieldDescription and FieldError register their presence into context via an effect so FieldControl's aria-describedby is always accurate even when a description or error mounts after the control, which happens constantly with async validation.",
        "motionNotes": "No motion. This is a compound primitive other components render inside of, not a state that changes visibly on its own — anything Field itself animated would be inherited (and likely duplicated) by every component built on top of it. Error text appearing or disappearing is the one plausible candidate for a transition; it is left as a plain instantaneous swap here so that a consumer building a specific validation UX (e.g. GhostInput's inline error) chooses its own timing rather than inheriting one that might not fit.",
        "schema": {
          "component": "Field",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Field",
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Base id for the control. A stable id is generated if omitted."
              },
              "invalid": {
                "type": "boolean",
                "description": "Marks the field as invalid — the control gets aria-invalid and the error styles apply."
              }
            }
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "generation-job",
      "title": "Generation Job",
      "description": "A generation that takes minutes, told honestly. Takes a `status` (`queued`, `running`, `partial`, `done`, `failed`, `cancelled`), the `title` of what's being made, a `queue` with `position` and the `typicalWait` at this moment, the current `stage` as replaceable text, a **measured** `progress` (leave it out and no bar is drawn), `startedAt` for a self-ticking elapsed counter, the `cost` and your `refundNote` stated before anything fails, an `error` carrying both the reason and what happened to the money, and a `notify` toggle so nobody has to watch the screen. Finished outputs are children and appear the moment they exist, so a batch where two of four are ready shows two. Purely presentational and provider-free: you own the job, the queue and the refunds. Use it for image, video, audio or long document generation. Not for sub-second work (that is Generation Placeholder), and not for an agent run with steps (that is Agent Plan).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/generation-job/generation-job.tsx",
          "target": "components/ui/generation-job.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Pass `progress` only when you measure it; the component deliberately draws nothing otherwise.",
      "meta": {
        "engineeringNotes": "Built against the complaints this category actually generates: reviews of image and video tools rank credits and unexplained waits above quality, and queue times of ten to thirty minutes at peak are normal. So the queue is a first-class state with a position and a typical wait rather than a spinner; `progress` is optional and a bar is drawn only when it is passed, because an invented bar on a job whose extent is unknown is the same lie as fake typing; `partial` exists as a status so a batch with two of four finished can show two, which the pipeline literature treats as the correct outcome rather than a pass or fail; and `error` carries a refund string beside the reason, because a failure that silently keeps someone's credits is the complaint underneath most one-star reviews. The elapsed counter ticks inside the component — the one place this system breaks its caller-owns-async rule — so a minutes-long job doesn't force a re-render a second on the consumer.",
        "motionNotes": "Motion means the machine is busy. The status dot pulses while queued, running or partial, the stage verb shimmers while work is ongoing, and every bit of it stops the moment the job reaches a terminal state. A measured bar animates its width over `--pd-duration-slow` with a linear ease, because it tracks real progress rather than decorating a wait. Under `prefers-reduced-motion` the pulse and shimmer are dropped and the state is carried by the words and the colour.",
        "schema": {
          "component": "GenerationJob",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "GenerationJob",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "status": {
                "enum": [
                  "running",
                  "done",
                  "failed",
                  "queued",
                  "cancelled",
                  "partial"
                ]
              },
              "title": {
                "type": "string",
                "description": "What is being made, in the user's words: \"4 product images, 1024×1024\"."
              },
              "queue": {
                "type": "object",
                "properties": {
                  "position": {
                    "type": "number"
                  },
                  "typicalWait": {
                    "type": "string"
                  }
                },
                "description": "Where it sits in the queue, and what the wait usually is at this moment — the thing\nevery generation tool hides. Both optional: say what you know, not what you guess."
              },
              "stage": {
                "type": "string",
                "description": "The current stage: \"Rendering frame 12 of 48\". Replaced as work moves on."
              },
              "progress": {
                "type": "number",
                "description": "Measured progress, 0–1. Leave it out while the extent is unknown: an invented bar is\nthe fake this component exists to avoid."
              },
              "startedAt": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "x-type": "Date"
                  }
                ],
                "description": "When the job started. The elapsed counter ticks internally."
              },
              "cost": {
                "type": "string",
                "description": "What it costs, formatted by you: \"12 credits\"."
              },
              "refundNote": {
                "type": "string",
                "description": "Your refund rule, stated before it fails: \"Credits come back if it fails\"."
              },
              "children": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Finished outputs. Show them as they land, never only at the end."
              },
              "error": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string"
                  },
                  "refunded": {
                    "type": "string"
                  }
                },
                "required": [
                  "message"
                ],
                "description": "Why it failed, in plain words, plus what happened to the money."
              },
              "notify": {
                "type": "boolean",
                "description": "Lets someone leave: long jobs shouldn't hold a person at the screen."
              },
              "notifyLabel": {
                "type": "string"
              }
            },
            "required": [
              "status",
              "title"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onNotifyChange",
              "required": false,
              "params": [
                {
                  "name": "notify",
                  "type": "boolean"
                }
              ]
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            },
            {
              "name": "onRetry",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "generation-placeholder",
      "title": "Generation Placeholder",
      "description": "A layout-reserving placeholder for AI-generated media — image, video, or audio — that encodes real generation progress as a resolving dot field and then cross-fades into the finished artifact. Takes `status` (`queued` | `generating` | `processing` | `done` | `failed`), an optional `progress` between 0 and 1, a required `aspectRatio` that reserves the artifact's final bounds, and a `label` used as both the visible and announced status text. Renders its children only once `status` is `done`, so the component is a container rather than a sibling that gets swapped out — which is what prevents a layout shift at the moment the artifact appears. Two forms share one rendering engine: `field` draws a dot grid for image and video, `bars` draws a row of vertical bars for audio, where a rectangle of dots would misrepresent the medium. Progress is encoded spatially rather than temporally: every cell holds a stable pseudo-random threshold and resolves once progress passes it (ordered-dither logic), so the pattern itself carries the information and stays fully legible with all animation removed. `progress` is optional and never fabricated — omit it and the component shows an honest indeterminate wave rather than a synthetic percentage creeping toward 90%. Use it anywhere media is being generated. Not for streaming text (use Progressive Reveal, which appends whole items rather than refining in place), not for agent activity with no artifact attached, and not as a generic content skeleton — this component exists precisely because media generation progress is measurable, and a skeleton discards that.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/generation-placeholder/generation-placeholder.tsx",
          "target": "components/ui/generation-placeholder.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and the `pd-fade-in` keyframe supplied by @pixeldosa/pixeldosa-theme's css block. `aspectRatio` is deliberately required rather than optional — it is the prop that prevents a layout shift when the artifact arrives, which is the whole reason this is a container component.",
      "meta": {
        "engineeringNotes": "The load-bearing decision is that progress is encoded spatially, not temporally. Each cell derives a stable pseudo-random threshold from its index (a deterministic sine hash, so the pattern never reshuffles between renders — a reshuffling field reads as noise rather than a resolve), and a cell resolves once `progress` passes its threshold. That is ordered-dither logic borrowed from halftone rendering: it produces an organic dissolve that is nonetheless an exact function of real progress, rather than a decorative animation that merely implies work. The payoff is the reduced-motion story, which is unusually strong for an animated component: because the information lives in the pattern rather than the movement, `prefers-reduced-motion` removes every transition and the progress stays completely legible — no fallback had to be invented. `progress` is optional with no synthetic fallback, deliberately: media generation genuinely reports steps (diffusion iterations, encoded frames) where text streaming does not, so a percentage here is measured rather than invented — the system's rule is not 'never show a number' but 'never invent precision you don't have', which is the same rule that makes Confidence Meter refuse one. `aspectRatio` is required because a media placeholder that resizes on completion causes a layout shift at exactly the moment the user's attention is on it. `processing` exists as a state separate from `generating` so a user is never told 'done' while an upscale or encode is still running. The requestAnimationFrame loop that drives indeterminate mode is not started at all when the component is determinate or motion is reduced, since an animation frame loop nobody can see is pure battery cost.",
        "motionNotes": "Cells transition `transform: scale` at `--pd-duration-base` with `--pd-ease-standard`; because each cell's threshold differs, the resolve reads as a wave without any explicit stagger orchestration — the stagger is a free consequence of the threshold distribution. Indeterminate mode travels a phase offset across the same thresholds on a 2s loop, the single continuous animation in the component, and it maps to real ongoing work rather than decorating a wait. Completion is a cross-fade, never a pop: the artifact fades in over `--pd-duration-base` with `--pd-ease-decelerate` while the field is unmounted, so the two states morph rather than cut. Only `transform` and `opacity` animate, so the whole field composites on the GPU regardless of density. Under `prefers-reduced-motion` every transition is dropped and the wave loop never starts; cells snap to their correct scale and hold a static pattern, which costs nothing because progress was already spatial — the reduced-motion state is the same information without the movement, which is the fallback contract rather than a removal.",
        "schema": {
          "component": "GenerationPlaceholder",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "GenerationPlaceholder",
            "type": "object",
            "properties": {
              "status": {
                "enum": [
                  "done",
                  "failed",
                  "queued",
                  "generating",
                  "processing"
                ]
              },
              "progress": {
                "type": "number",
                "description": "0–1, and only when the backend actually reports it (diffusion steps, encoded\nframes). Omit for indeterminate — the component never fabricates a percentage or\ncreeps toward 90%, because a made-up number is worse than an honest \"still working\"."
              },
              "aspectRatio": {
                "type": "string",
                "description": "Reserves the artifact's final bounds so nothing shifts when it arrives. Required,\nnot optional: a media placeholder that resizes on completion causes a layout jump\nat exactly the moment the user is looking at it."
              },
              "label": {
                "type": "string",
                "description": "Visible and announced status text, e.g. \"Generating image\"."
              },
              "form": {
                "enum": [
                  "field",
                  "mosaic",
                  "sweep",
                  "bars"
                ]
              },
              "density": {
                "enum": [
                  "sm",
                  "lg",
                  "default"
                ]
              },
              "children": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "The finished artifact. Revealed when `status` is `done`."
              }
            },
            "required": [
              "status",
              "aspectRatio",
              "label"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onCancel",
              "required": false,
              "params": []
            },
            {
              "name": "onRetry",
              "required": false,
              "params": [],
              "description": "Recovery path for `failed`. Without it the failure state explains but can't be acted on."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "ghost-input",
      "title": "Ghost Input",
      "description": "A single-line text input with inline AI ghost-text completion: after the user pauses typing, a dimmed continuation appears past the caret, accepted with Tab or the right-arrow key (only when the caret is already at the end of the text) and dismissed with Escape. Takes a fetchSuggestion(value, signal) prop that the consumer implements against their own model — the component owns debouncing, request cancellation on every keystroke, staleness checking (a suggestion that doesn't continue the current text is discarded), and the accessible announcement of the suggestion via a live region. It never inserts text automatically; acceptance is always an explicit keypress. Use for reply drafting, search boxes, or any single-line field where predictive continuation should stay out of the way until the user asks for it. Not for multi-field or full-document autocomplete — this is a single <input>, not an editor.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/field"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/ghost-input/ghost-input.tsx",
          "target": "components/ui/ghost-input.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "value/onValueChange are required — this is a controlled component only. fetchSuggestion must resolve to the FULL continued string (current value + completion), not just the completion tail; the component slices the tail itself and drops any result that doesn't start with the value it was requested for, so a response arriving after the user has kept typing can't overwrite what they see. Drops into <FieldControl> directly since id/aria-invalid/aria-describedby/ref all forward to the underlying <input>.",
      "meta": {
        "engineeringNotes": "The ghost text is rendered as a separate mirror div positioned exactly behind the real <input>, with the typed-text portion set to transparent — not as a styled fake caret or a contentEditable trick. This is the only approach that keeps native input behaviour (IME composition, selection, undo, autofill, screen-reader field semantics) completely intact, at the cost of the mirror div needing identical padding/font/line-height to the input, which is why both come from the same className. The whole request lifecycle — debounce timer, AbortController — lives in refs rather than state so that a rapid keystroke can synchronously cancel the previous request without waiting for a re-render; a state-based cancellation would let a slow response race a fast one and briefly show a stale suggestion. The right-arrow accept only fires when the caret is already at the end of the value (checked via selectionStart/selectionEnd), because otherwise it would silently hijack normal in-text cursor movement, which is a worse failure mode than requiring Tab.",
        "motionNotes": "The suggestion's appearance is a plain opacity transition using duration.fast and easing.standard from the token scale — deliberately not a slide or typewriter reveal, both of which would misrepresent what happened: the whole continuation exists immediately (it was already fetched), so animating it in character-by-character would be a fabricated sense of the model 'typing', not a real progressive state. Under prefers-reduced-motion this reduces to a duration.instant swap via the same token path every other component in the system uses; nothing here required a component-specific reduced-motion branch, which is itself a signal the animation is doing the minimum necessary rather than something decorative.",
        "schema": {
          "component": "GhostInput",
          "element": "input",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "GhostInput",
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Current field value. GhostInput is controlled — there is no uncontrolled mode."
              },
              "debounceMs": {
                "type": "number",
                "description": "Debounce before `fetchSuggestion` fires, in ms."
              },
              "minChars": {
                "type": "number",
                "description": "Don't request a suggestion below this many characters."
              },
              "wrapperClassName": {
                "type": "string"
              }
            },
            "required": [
              "value"
            ]
          },
          "events": [
            {
              "name": "onValueChange",
              "required": true,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ]
            },
            {
              "name": "fetchSuggestion",
              "required": true,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                },
                {
                  "name": "signal",
                  "type": "AbortSignal"
                }
              ],
              "description": "Called after the user pauses typing, with an `AbortSignal` that fires the moment\nanother keystroke arrives. Must resolve to a full continuation of `value` — a\nresult that doesn't start with the current value is treated as stale and dropped.\nReturn `null`/`undefined`/`\"\"` for \"no suggestion\"."
            },
            {
              "name": "onAcceptSuggestion",
              "required": false,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ],
              "description": "Fired with the full accepted value when the user accepts the suggestion."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "human-handoff",
      "title": "Human Handoff",
      "description": "When a person takes over from the agent, and what they're handed. Takes a `stage` (`agentHandling`, `waitingForPerson`, `personHandling`, `returnedToAgent`), the agent's `reason` for stepping back in its own words, a `context` list of the facts a person would otherwise have to ask for again, the `person` who has it now, a `waitingFor` duration, and `onTakeOver` / `onReturnToAgent`. The context is the component, not the button: handoffs usually lose everything, so the customer repeats themselves and whoever takes over re-reads a transcript to find three facts. Listing what the agent already tried is part of that, since offering a customer the same rejected remedy twice is the most common way a handoff goes wrong. It also works in both directions, because handing a finished job back to the agent is the half products forget. Purely presentational. Use it in support, operations and any agent that can reach the end of what it may decide. Not for a single approval (that is AI Approval Gate), and not for a question the agent could answer itself (that is Agent Ask).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/human-handoff/human-handoff.tsx",
          "target": "components/ui/human-handoff.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Fill `context` with the facts a colleague would ask for in the first thirty seconds, including what the agent already tried.",
      "meta": {
        "engineeringNotes": "Stage is a four-value enum rather than a boolean because the return leg is a real state with its own copy, and products that model handoff as 'escalated: true' have nowhere to put the moment a person finishes and the agent resumes. Waiting is styled in the waiting colour with full stillness, the same treatment approvals get, so a queue of handoffs reads at a glance without a separate legend. The context list is `dl` markup rather than free text so a caller can render five facts consistently and a screen reader hears label-and-value pairs, and it sits inside the component rather than being left to a slot because the whole failure mode is teams shipping a handoff button with no payload.",
        "motionNotes": "Nothing animates, in any stage. A waiting handoff is a person's turn and the strongest signal available is complete stillness; a pulsing 'needs attention' badge here would compete with the queue around it and train people to ignore it.",
        "schema": {
          "component": "HumanHandoff",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "HumanHandoff",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "stage": {
                "enum": [
                  "agentHandling",
                  "waitingForPerson",
                  "personHandling",
                  "returnedToAgent"
                ]
              },
              "reason": {
                "type": "string",
                "description": "Why the agent is stepping back, in its own words."
              },
              "context": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/HandoffFact"
                },
                "description": "What the person receives: the facts they'd otherwise have to ask for again."
              },
              "person": {
                "type": "string",
                "description": "Who has it now, once a person took over: \"Dana in support\"."
              },
              "waitingFor": {
                "type": "string",
                "description": "How long the person has been waiting, formatted by you."
              },
              "takeOverLabel": {
                "type": "string"
              },
              "returnLabel": {
                "type": "string"
              }
            },
            "required": [
              "stage"
            ],
            "$defs": {
              "HandoffFact": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Customer\", \"Order\", \"Tried already\"."
                  },
                  "value": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "value"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onTakeOver",
              "required": false,
              "params": []
            },
            {
              "name": "onReturnToAgent",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "image-studio",
      "title": "Image Studio",
      "description": "A complete workspace for making images with a model, assembled from Prompt Composer, Parameter Panel, Generation Job, Variation Grid and Credits Meter. Takes `groups` and `values` for the settings that persist across runs, an optional `run` (status, queue, stage, measured progress, cost, per-result status), `credits`, and a `history` of past runs that each hand back their prompt *and* their settings through `onRecall`. The workspace is the design: generation tools are described as tossing prompts into a black hole, with a scrolling feed instead of an overview and settings that evaporate when you switch model, so the user ends up keeping a spreadsheet of prompts beside the tool. Here the settings sit beside the work and survive the prompt, the history is a recallable grid rather than a feed, and the price of the next run is named before the click. Presentational: you own the runs, the settings and the history. Use it as the whole screen of an image or video product. Not for a single generation in a chat thread — that is Generation Job on its own.",
      "type": "registry:block",
      "categories": [
        "ai-assisted",
        "application",
        "block"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/prompt-composer",
        "@pixeldosa/parameter-panel",
        "@pixeldosa/generation-job",
        "@pixeldosa/variation-grid",
        "@pixeldosa/credits-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/image-studio/image-studio.tsx",
          "target": "components/blocks/image-studio.tsx",
          "type": "registry:block"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Format every figure yourself — costs, times and totals are strings. `onRecall` should restore both the prompt and the settings, not just the prompt; restoring one without the other is the failure this history exists to fix.",
      "meta": {
        "engineeringNotes": "The layout encodes the three documented failures of generation workspaces. Settings live in a rail beside the work rather than inside the prompt, because the reviewed complaint about multi-model tools is that switching model reopens every consistency problem and leaves the user restating palette and framing by hand; a rail that survives the prompt is what stops that. History is a thumbnail grid with a recall control, not a feed: comparisons of the same review task put a grid at roughly five times the density of a scrolling list, and density is the whole job when you are looking for the good one from last Tuesday. Recall returns the settings as well as the prompt, which is the part a feed of images structurally cannot do. Partial results render inside the job rather than after it, so a finished image never waits for its siblings. The composer goes busy while a run is active instead of offering a second start on top of the first, and Stop stays on the job, where the run actually is, so there is exactly one place to end it.",
        "motionNotes": "Motion means the machine is busy, and in a workspace that has to hold across three regions at once: only the running job moves — its stage shimmers, its pulse marks the running cells in the grid — while the settings rail and the history hold completely still, because both are the user's to work with rather than the machine's to report on. The result is that the eye is drawn to the one region that is actually changing, in a screen that would otherwise have movement in every quarter. Under `prefers-reduced-motion` every part of that stops and the same states are carried by colour, position and words.",
        "schema": {
          "component": "ImageStudio",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ImageStudio",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5
                ],
                "description": "Level of the section headings. Defaults to 2, to sit under a page `h1`."
              },
              "placeholder": {
                "type": "string"
              },
              "controls": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ComposerControl"
                },
                "description": "Structured parts of the request that belong beside the prompt rather than in it."
              },
              "promptValue": {
                "type": "string",
                "description": "Pre-filled prompt text — set this when Recall puts a past run back."
              },
              "groups": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ParameterGroup"
                },
                "description": "Settings that persist across runs, as previews rather than words."
              },
              "values": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "settingsSummary": {
                "type": "string",
                "description": "The current settings as a sentence, so the choice is legible without reading the panel."
              },
              "credits": {
                "$ref": "#/$defs/CreditsMeterProps",
                "description": "The budget, always in view. Everything `CreditsMeter` takes."
              },
              "run": {
                "$ref": "#/$defs/StudioRun",
                "description": "The run in progress, or the one that just finished."
              },
              "regenerateCost": {
                "type": "string"
              },
              "useLabel": {
                "type": "string"
              },
              "refundNote": {
                "type": "string",
                "description": "What one more run of the current settings costs. Named before the click."
              },
              "notify": {
                "type": "boolean"
              },
              "history": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/StudioHistoryEntry"
                },
                "description": "Past runs, newest first."
              },
              "recallLabel": {
                "type": "string"
              },
              "empty": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "required": [
                  "title"
                ],
                "description": "Shown before the first run."
              },
              "aspectRatio": {
                "type": "string"
              }
            },
            "required": [
              "groups",
              "values"
            ],
            "$defs": {
              "ComposerControl": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Short label shown on the pill: \"Tone\", \"Length\", \"For\"."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "value",
                            "label"
                          ]
                        }
                      ]
                    }
                  },
                  "defaultValue": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "options",
                  "defaultValue"
                ]
              },
              "ParameterGroup": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Style\", \"Shape\", \"How many\"."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/ParameterOption"
                    }
                  }
                },
                "required": [
                  "id",
                  "label",
                  "options"
                ]
              },
              "ParameterOption": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Plain words, never a branded name: \"Wide, 16:9\", not \"Cinemascope Pro\"."
                  },
                  "preview": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "A small preview: a thumbnail, a shape, a swatch. Recognition beats recall."
                  },
                  "costNote": {
                    "type": "string",
                    "description": "What choosing this does to the price or the wait: \"+20 credits\"."
                  },
                  "detail": {
                    "type": "string",
                    "description": "One line of help, shown under the group when this option is chosen."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              },
              "CreditsMeterProps": {
                "type": "object",
                "properties": {
                  "remaining": {
                    "type": "string",
                    "description": "What's left, formatted by you: \"1,250 credits\", \"$18.40\"."
                  },
                  "allowance": {
                    "type": "string",
                    "description": "The full allowance, when there is one: \"2,000 credits this month\"."
                  },
                  "used": {
                    "type": "number",
                    "description": "How much of the allowance is already spent, 0–1, measured. Renders a bar; leave it\nout when you only have words. The bar draws what is *left*, so it empties as the\nfigure beside it falls."
                  },
                  "nextAction": {
                    "type": "object",
                    "properties": {
                      "label": {
                        "type": "string"
                      },
                      "cost": {
                        "type": "string"
                      },
                      "affordable": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "label",
                      "cost"
                    ],
                    "description": "What the next action will take, named and priced: the thing reviewers say is missing\neverywhere. \"4 images · 40 credits\"."
                  },
                  "resets": {
                    "type": "string",
                    "description": "When the balance resets or expires, in plain words: \"Resets on 1 October\"."
                  },
                  "recent": {
                    "type": "string",
                    "description": "Recent movement, including refunds: \"12 refunded yesterday when a batch failed\"."
                  },
                  "low": {
                    "type": "boolean",
                    "description": "Set when the balance is low enough to say so."
                  },
                  "onTopUp": {
                    "x-function": true,
                    "x-host": true
                  },
                  "topUpLabel": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "slot": {
                    "type": "string"
                  },
                  "style": {
                    "x-type": "CSSProperties"
                  },
                  "title": {
                    "type": "string"
                  },
                  "key": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      },
                      {}
                    ]
                  },
                  "defaultChecked": {
                    "type": "boolean"
                  },
                  "defaultValue": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  },
                  "suppressContentEditableWarning": {
                    "type": "boolean"
                  },
                  "suppressHydrationWarning": {
                    "type": "boolean"
                  },
                  "accessKey": {
                    "type": "string"
                  },
                  "autoCapitalize": {
                    "anyOf": [
                      {
                        "const": "off"
                      },
                      {},
                      {
                        "const": "none"
                      },
                      {
                        "const": "on"
                      },
                      {
                        "const": "sentences"
                      },
                      {
                        "const": "words"
                      },
                      {
                        "const": "characters"
                      }
                    ]
                  },
                  "autoFocus": {
                    "type": "boolean"
                  },
                  "className": {
                    "type": "string"
                  },
                  "contentEditable": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "inherit"
                      },
                      {
                        "const": "plaintext-only"
                      }
                    ]
                  },
                  "contextMenu": {
                    "type": "string"
                  },
                  "dir": {
                    "type": "string"
                  },
                  "draggable": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ]
                  },
                  "enterKeyHint": {
                    "enum": [
                      "search",
                      "done",
                      "next",
                      "send",
                      "enter",
                      "go",
                      "previous"
                    ]
                  },
                  "hidden": {
                    "type": "boolean"
                  },
                  "id": {
                    "type": "string"
                  },
                  "lang": {
                    "type": "string"
                  },
                  "nonce": {
                    "type": "string"
                  },
                  "spellCheck": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ]
                  },
                  "tabIndex": {
                    "type": "number"
                  },
                  "translate": {
                    "enum": [
                      "yes",
                      "no"
                    ]
                  },
                  "radioGroup": {
                    "type": "string"
                  },
                  "role": {
                    "x-type": "AriaRole | undefined"
                  },
                  "about": {
                    "type": "string"
                  },
                  "content": {
                    "type": "string"
                  },
                  "datatype": {
                    "type": "string"
                  },
                  "inlist": {},
                  "prefix": {
                    "type": "string"
                  },
                  "property": {
                    "type": "string"
                  },
                  "rel": {
                    "type": "string"
                  },
                  "resource": {
                    "type": "string"
                  },
                  "rev": {
                    "type": "string"
                  },
                  "typeof": {
                    "type": "string"
                  },
                  "vocab": {
                    "type": "string"
                  },
                  "autoCorrect": {
                    "type": "string"
                  },
                  "autoSave": {
                    "type": "string"
                  },
                  "color": {
                    "type": "string"
                  },
                  "itemProp": {
                    "type": "string"
                  },
                  "itemScope": {
                    "type": "boolean"
                  },
                  "itemType": {
                    "type": "string"
                  },
                  "itemID": {
                    "type": "string"
                  },
                  "itemRef": {
                    "type": "string"
                  },
                  "results": {
                    "type": "number"
                  },
                  "security": {
                    "type": "string"
                  },
                  "unselectable": {
                    "enum": [
                      "off",
                      "on"
                    ]
                  },
                  "popover": {
                    "enum": [
                      "",
                      "auto",
                      "manual",
                      "hint"
                    ]
                  },
                  "popoverTargetAction": {
                    "enum": [
                      "toggle",
                      "show",
                      "hide"
                    ]
                  },
                  "popoverTarget": {
                    "type": "string"
                  },
                  "inert": {
                    "type": "boolean"
                  },
                  "inputMode": {
                    "enum": [
                      "search",
                      "text",
                      "none",
                      "tel",
                      "url",
                      "email",
                      "numeric",
                      "decimal"
                    ],
                    "description": "Hints at the type of data that might be entered by the user while editing the element or its contents"
                  },
                  "is": {
                    "type": "string",
                    "description": "Specify that a standard HTML element should behave like a defined custom built-in element"
                  },
                  "exportparts": {
                    "type": "string"
                  },
                  "part": {
                    "type": "string"
                  },
                  "aria-activedescendant": {
                    "type": "string",
                    "description": "Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application."
                  },
                  "aria-atomic": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute."
                  },
                  "aria-autocomplete": {
                    "enum": [
                      "list",
                      "none",
                      "inline",
                      "both"
                    ],
                    "description": "Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be\npresented if they are made."
                  },
                  "aria-braillelabel": {
                    "type": "string",
                    "description": "Defines a string value that labels the current element, which is intended to be converted into Braille."
                  },
                  "aria-brailleroledescription": {
                    "type": "string",
                    "description": "Defines a human-readable, author-localized abbreviated description for the role of an element, which is intended to be converted into Braille."
                  },
                  "aria-busy": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ]
                  },
                  "aria-checked": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "mixed"
                      }
                    ],
                    "description": "Indicates the current \"checked\" state of checkboxes, radio buttons, and other widgets."
                  },
                  "aria-colcount": {
                    "type": "number",
                    "description": "Defines the total number of columns in a table, grid, or treegrid."
                  },
                  "aria-colindex": {
                    "type": "number",
                    "description": "Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid."
                  },
                  "aria-colindextext": {
                    "type": "string",
                    "description": "Defines a human readable text alternative of aria-colindex."
                  },
                  "aria-colspan": {
                    "type": "number",
                    "description": "Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid."
                  },
                  "aria-controls": {
                    "type": "string",
                    "description": "Identifies the element (or elements) whose contents or presence are controlled by the current element."
                  },
                  "aria-current": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "time"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "step"
                      },
                      {
                        "const": "page"
                      },
                      {
                        "const": "location"
                      },
                      {
                        "const": "date"
                      }
                    ],
                    "description": "Indicates the element that represents the current item within a container or set of related elements."
                  },
                  "aria-describedby": {
                    "type": "string",
                    "description": "Identifies the element (or elements) that describes the object."
                  },
                  "aria-description": {
                    "type": "string",
                    "description": "Defines a string value that describes or annotates the current element."
                  },
                  "aria-details": {
                    "type": "string",
                    "description": "Identifies the element that provides a detailed, extended description for the object."
                  },
                  "aria-disabled": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable."
                  },
                  "aria-dropeffect": {
                    "enum": [
                      "link",
                      "none",
                      "copy",
                      "execute",
                      "move",
                      "popup"
                    ],
                    "description": "Indicates what functions can be performed when a dragged object is released on the drop target."
                  },
                  "aria-errormessage": {
                    "type": "string",
                    "description": "Identifies the element that provides an error message for the object."
                  },
                  "aria-expanded": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed."
                  },
                  "aria-flowto": {
                    "type": "string",
                    "description": "Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,\nallows assistive technology to override the general default of reading in document source order."
                  },
                  "aria-grabbed": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates an element's \"grabbed\" state in a drag-and-drop operation."
                  },
                  "aria-haspopup": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "dialog"
                      },
                      {
                        "const": "menu"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "grid"
                      },
                      {
                        "const": "listbox"
                      },
                      {
                        "const": "tree"
                      }
                    ],
                    "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element."
                  },
                  "aria-hidden": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates whether the element is exposed to an accessibility API."
                  },
                  "aria-invalid": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "grammar"
                      },
                      {
                        "const": "spelling"
                      }
                    ],
                    "description": "Indicates the entered value does not conform to the format expected by the application."
                  },
                  "aria-keyshortcuts": {
                    "type": "string",
                    "description": "Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element."
                  },
                  "aria-label": {
                    "type": "string",
                    "description": "Defines a string value that labels the current element."
                  },
                  "aria-labelledby": {
                    "type": "string",
                    "description": "Identifies the element (or elements) that labels the current element."
                  },
                  "aria-level": {
                    "type": "number",
                    "description": "Defines the hierarchical level of an element within a structure."
                  },
                  "aria-live": {
                    "enum": [
                      "off",
                      "assertive",
                      "polite"
                    ],
                    "description": "Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region."
                  },
                  "aria-modal": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates whether an element is modal when displayed."
                  },
                  "aria-multiline": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates whether a text box accepts multiple lines of input or only a single line."
                  },
                  "aria-multiselectable": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates that the user may select more than one item from the current selectable descendants."
                  },
                  "aria-orientation": {
                    "enum": [
                      "horizontal",
                      "vertical"
                    ],
                    "description": "Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous."
                  },
                  "aria-owns": {
                    "type": "string",
                    "description": "Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship\nbetween DOM elements where the DOM hierarchy cannot be used to represent the relationship."
                  },
                  "aria-placeholder": {
                    "type": "string",
                    "description": "Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.\nA hint could be a sample value or a brief description of the expected format."
                  },
                  "aria-posinset": {
                    "type": "number",
                    "description": "Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM."
                  },
                  "aria-pressed": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      },
                      {
                        "const": "mixed"
                      }
                    ],
                    "description": "Indicates the current \"pressed\" state of toggle buttons."
                  },
                  "aria-readonly": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates that the element is not editable, but is otherwise operable."
                  },
                  "aria-relevant": {
                    "enum": [
                      "text",
                      "additions",
                      "additions removals",
                      "additions text",
                      "all",
                      "removals",
                      "removals additions",
                      "removals text",
                      "text additions",
                      "text removals"
                    ],
                    "description": "Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified."
                  },
                  "aria-required": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates that user input is required on the element before a form may be submitted."
                  },
                  "aria-roledescription": {
                    "type": "string",
                    "description": "Defines a human-readable, author-localized description for the role of an element."
                  },
                  "aria-rowcount": {
                    "type": "number",
                    "description": "Defines the total number of rows in a table, grid, or treegrid."
                  },
                  "aria-rowindex": {
                    "type": "number",
                    "description": "Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid."
                  },
                  "aria-rowindextext": {
                    "type": "string",
                    "description": "Defines a human readable text alternative of aria-rowindex."
                  },
                  "aria-rowspan": {
                    "type": "number",
                    "description": "Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid."
                  },
                  "aria-selected": {
                    "anyOf": [
                      {
                        "type": "boolean"
                      },
                      {
                        "type": "boolean"
                      },
                      {
                        "const": "true"
                      },
                      {
                        "const": "false"
                      }
                    ],
                    "description": "Indicates the current \"selected\" state of various widgets."
                  },
                  "aria-setsize": {
                    "type": "number",
                    "description": "Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM."
                  },
                  "aria-sort": {
                    "enum": [
                      "none",
                      "ascending",
                      "descending",
                      "other"
                    ],
                    "description": "Indicates if items in a table or grid are sorted in ascending or descending order."
                  },
                  "aria-valuemax": {
                    "type": "number",
                    "description": "Defines the maximum allowed value for a range widget."
                  },
                  "aria-valuemin": {
                    "type": "number",
                    "description": "Defines the minimum allowed value for a range widget."
                  },
                  "aria-valuenow": {
                    "type": "number",
                    "description": "Defines the current value for a range widget."
                  },
                  "aria-valuetext": {
                    "type": "string",
                    "description": "Defines the human readable text alternative of aria-valuenow for a range widget."
                  },
                  "dangerouslySetInnerHTML": {
                    "x-type": "{ __html: string | TrustedHTML; }"
                  },
                  "onCopy": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCopyCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCut": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCutCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPaste": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPasteCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionStartCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionUpdate": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCompositionUpdateCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onFocus": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onFocusCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onBlur": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onBlurCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onChange": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onChangeCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onBeforeInput": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onBeforeInputCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onInput": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onInputCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onReset": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onResetCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSubmit": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSubmitCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onInvalid": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onInvalidCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoad": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onError": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onErrorCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyDown": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyDownCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyPress": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyPressCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyUp": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onKeyUpCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAbort": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAbortCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCanPlay": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCanPlayCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCanPlayThrough": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCanPlayThroughCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDurationChange": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDurationChangeCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEmptied": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEmptiedCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEncrypted": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEncryptedCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEnded": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEndedCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadedData": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadedDataCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadedMetadata": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadedMetadataCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLoadStartCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPause": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPauseCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPlay": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPlayCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPlaying": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPlayingCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onProgress": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onProgressCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onRateChange": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onRateChangeCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSeeked": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSeekedCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSeeking": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSeekingCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onStalled": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onStalledCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSuspend": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSuspendCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTimeUpdate": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTimeUpdateCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onVolumeChange": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onVolumeChangeCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onWaiting": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onWaitingCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAuxClick": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAuxClickCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onClick": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onClickCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onContextMenu": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onContextMenuCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDoubleClick": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDoubleClickCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDrag": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragEnter": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragEnterCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragExit": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragExitCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragLeave": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragLeaveCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragOver": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragOverCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDragStartCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDrop": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDropCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseDown": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseDownCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseEnter": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseLeave": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseMove": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseMoveCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseOut": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseOutCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseOver": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseOverCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseUp": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onMouseUpCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSelect": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onSelectCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchCancel": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchCancelCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchMove": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchMoveCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTouchStartCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerDown": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerDownCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerMove": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerMoveCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerUp": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerUpCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerCancel": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerCancelCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerEnter": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerLeave": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerOver": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerOverCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerOut": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onPointerOutCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onGotPointerCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onGotPointerCaptureCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLostPointerCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onLostPointerCaptureCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onScroll": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onScrollCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onScrollEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onScrollEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onWheel": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onWheelCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationStartCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationIteration": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onAnimationIterationCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onToggle": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onBeforeToggle": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionCancel": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionCancelCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionEnd": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionEndCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionRun": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionRunCapture": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionStart": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onTransitionStartCapture": {
                    "x-function": true,
                    "x-host": true
                  }
                },
                "required": [
                  "remaining"
                ]
              },
              "StudioRun": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "prompt": {
                    "type": "string",
                    "description": "The ask, in the user's words. Shown as the job's title."
                  },
                  "status": {
                    "enum": [
                      "running",
                      "done",
                      "failed",
                      "queued",
                      "cancelled",
                      "partial"
                    ]
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/Variation"
                    },
                    "description": "Results. Show them as they land — a finished one doesn't wait for its siblings."
                  },
                  "keptIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Which results the user is keeping. Caller-owned, like the grid's own prop."
                  },
                  "queue": {
                    "type": "object",
                    "properties": {
                      "position": {
                        "type": "number"
                      },
                      "typicalWait": {
                        "type": "string"
                      }
                    }
                  },
                  "stage": {
                    "type": "string",
                    "description": "What's happening right now: \"Rendering frame 12 of 48\"."
                  },
                  "progress": {
                    "type": "number",
                    "description": "Measured only, 0–1. Leave it out while the extent is unknown."
                  },
                  "startedAt": {
                    "anyOf": [
                      {
                        "type": "number"
                      },
                      {
                        "x-type": "Date"
                      }
                    ]
                  },
                  "cost": {
                    "type": "string",
                    "description": "What this run costs, formatted by you: \"12 credits\"."
                  },
                  "error": {
                    "type": "object",
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "refunded": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message"
                    ]
                  }
                },
                "required": [
                  "id",
                  "prompt",
                  "status",
                  "items"
                ]
              },
              "Variation": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "preview": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "The result itself: an image, a video poster, a card of text. Yours to render."
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Version 3\", \"Warmer light\"."
                  },
                  "status": {
                    "enum": [
                      "running",
                      "done",
                      "failed"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "Why this one failed, and what happened to the credits."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "StudioHistoryEntry": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "prompt": {
                    "type": "string",
                    "description": "What was asked for."
                  },
                  "time": {
                    "type": "string",
                    "description": "Formatted by you: \"2 hours ago\"."
                  },
                  "thumbnails": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        },
                        {
                          "type": "array",
                          "items": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "$ref": "#/$defs/UINode"
                              }
                            ]
                          }
                        }
                      ],
                      "x-react": "ReactNode"
                    },
                    "description": "Thumbnails. The overview a scrolling feed never gives."
                  },
                  "settings": {
                    "type": "string",
                    "description": "The settings this run used, in words: \"Photographic · Square · Standard\"."
                  },
                  "total": {
                    "type": "number",
                    "description": "How many results it produced, when more exist than thumbnails shown."
                  }
                },
                "required": [
                  "id",
                  "prompt",
                  "thumbnails"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onGenerate",
              "required": true,
              "params": [
                {
                  "name": "submission",
                  "type": "ComposerSubmission"
                }
              ],
              "description": "The ask."
            },
            {
              "name": "onPromptValueChange",
              "required": false,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onValueChange",
              "required": true,
              "params": [
                {
                  "name": "groupId",
                  "type": "string"
                },
                {
                  "name": "optionId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onResetSettings",
              "required": false,
              "params": []
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            },
            {
              "name": "onRetry",
              "required": false,
              "params": []
            },
            {
              "name": "onKeptChange",
              "required": true,
              "params": [
                {
                  "name": "ids",
                  "type": "string[]"
                }
              ],
              "description": "Required: the grid always offers Keep, so without this the checkbox is a dead\ncontrol. Results are caller-owned, like everywhere else in the system."
            },
            {
              "name": "onRegenerateRest",
              "required": false,
              "params": [
                {
                  "name": "idsToReplace",
                  "type": "string[]"
                }
              ],
              "description": "Replaces only the results the user didn't keep."
            },
            {
              "name": "onUse",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onMore",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onRetryItem",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onNotifyChange",
              "required": false,
              "params": [
                {
                  "name": "notify",
                  "type": "boolean"
                }
              ]
            },
            {
              "name": "onRecall",
              "required": false,
              "params": [
                {
                  "name": "entryId",
                  "type": "string"
                }
              ],
              "description": "Puts a past run's prompt and settings back. The reason this history is worth keeping."
            },
            {
              "name": "onOpenHistoryEntry",
              "required": false,
              "params": [
                {
                  "name": "entryId",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "inline-citations",
      "title": "Inline Citations",
      "description": "Claim-level sources that take one step to check. `CitedText` takes the answer's `sources` (id, title, url, publisher, date), numbers them in order and renders a numbered reference list underneath. `Cite` wraps one claim, takes `source` (an id or ids), the `quote` that supports it (or one quote per source) and `support`: 'quoted' or 'paraphrased'. Each claim gets a small numbered marker. On hover, keyboard focus or tap, a popover shows the supporting passage itself next to the claim, says whether the source quotes it or the agent summarised it, and links to 'Open at this passage', which deep-links with a URL text fragment so the source opens scrolled to the highlighted words. The claim is highlighted while its source is open. A `Cite` with no source gets a dashed '?' marker and says it has nothing behind it, so unsourced statements can't pass for cited ones. Use it in AI answers, research summaries and reports. For 'why this?' about a whole answer, use AI Context Surface instead.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/inline-citations/inline-citations.tsx",
          "target": "components/ui/inline-citations.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Pass the exact retrieved passage as `quote` — that is what makes checking take one step. Set `support=\"paraphrased\"` when the claim is the model's summary. Leave `source` off rather than attaching a loosely related source.",
      "meta": {
        "engineeringNotes": "Designed from one research finding: people rarely verify citations, so a numbered marker is mostly a comfort signal that makes answers look more trustworthy than they are. The design makes the check cheap enough to actually happen. The supporting passage itself appears in the popover beside the claim, so comparing the two needs no navigation. The link opens the source at that passage using a URL text fragment, with start and end words for long quotes because fragments must match exactly. Support is a tier, not a score: 'quoted' vs 'paraphrased'. A claim without a source is marked with a dashed '?' rather than left bare, because an uncited sentence sitting among cited ones borrows their credibility. Numbering comes from the order of the `sources` array rather than render-order registration, so it is identical on server and client. The popover stays in the DOM next to its marker, so Tab moves from marker into the link. It is displayed with the Popover API's top layer and fixed coordinates, which keeps it clear of overflow-hidden containers and transformed ancestors. Without Popover API support it falls back to fixed positioning. It opens on mouse hover after a short delay, with a grace period so the pointer can move into it, and on keyboard focus-visible or tap. Clicking outside dismisses it, and Escape closes it and returns focus. The always-visible reference list is the fallback where hover doesn't exist and the audit view where it does.",
        "motionNotes": "Minimal, because reading is the user's turn. The popover fades in at `--pd-duration-fast` on the decelerate curve, with no scale or slide, so the quote is readable from its first frame. Hover opens after 180ms, so markers don't flash as the pointer crosses text, and closing waits 140ms so the pointer can travel into the popover. The claim highlight and the marker's active fill change colour at `--pd-duration-instant`. Nothing shimmers: citations describe finished content. All motion becomes instant under prefers-reduced-motion.",
        "schema": {
          "component": "CitedText",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "CitedText",
            "type": "object",
            "properties": {
              "sources": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/CitationSource"
                },
                "description": "Numbered in this order, so numbers are stable across server and client renders."
              },
              "showReferences": {
                "type": "boolean",
                "description": "The numbered reference list under the text. On by default: it is the no-hover fallback."
              },
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the \"Sources\" heading, so it fits the page's outline. Defaults to 4."
              }
            },
            "required": [
              "sources"
            ],
            "$defs": {
              "CitationSource": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "publisher": {
                    "type": "string",
                    "description": "Who published it: \"Reuters\", \"Internal wiki\", \"Q3 board deck\"."
                  },
                  "date": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              }
            }
          },
          "events": []
        },
        "subcomponents": [
          {
            "component": "Cite",
            "element": "span",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "Cite",
              "type": "object",
              "properties": {
                "children": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "$ref": "#/$defs/UINode"
                    },
                    {
                      "type": "array",
                      "items": {
                        "anyOf": [
                          {
                            "type": "string"
                          },
                          {
                            "$ref": "#/$defs/UINode"
                          }
                        ]
                      }
                    }
                  ],
                  "x-react": "ReactNode"
                },
                "source": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ],
                  "description": "Source id, or ids. Omit to mark the claim as unsourced — honestly, rather than silently."
                },
                "quote": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  ],
                  "description": "The passage that supports the claim, shown in place and used to deep-link to it. With\nseveral sources, pass `{ [sourceId]: passage }` — each source supports it in its own words."
                },
                "support": {
                  "enum": [
                    "quoted",
                    "paraphrased"
                  ]
                }
              },
              "required": [
                "children"
              ],
              "$defs": {
                "UINode": {
                  "type": "object",
                  "description": "A nested component: a name from this catalogue, and the props to give it.",
                  "required": [
                    "component"
                  ],
                  "properties": {
                    "component": {
                      "type": "string",
                      "description": "The component's exported name, e.g. \"MediaResult\"."
                    },
                    "props": {
                      "type": "object",
                      "description": "That component's own props, matching its entry in this catalogue."
                    }
                  }
                }
              }
            },
            "events": []
          }
        ]
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "intent-preview",
      "title": "Intent Preview",
      "description": "An agent restates what it understood — and the guesses it made — before any work starts. Takes an `interpretation` (the request restated as one plain sentence), optional `assumptions` (each a labelled best guess, optionally with alternative readings the user can switch to in place), optional `boundaries` listing what the agent will deliberately not do, and `onStart`, which receives the confirmed values, plus optional `onRephrase` and `onCancel`. The guesses arrive pre-filled, so when they are right confirming costs one click; when one is wrong it is corrected in place from a list of the other readings rather than by rewriting the prompt, and the start button changes to 'Start with changes' so the difference is visible. The reason it exists: a model that misreads a request early builds every later step on that mistake, so the cheapest correction in an agentic run is the first one. Distinct from Agent Ask, which blocks until the user supplies something the agent cannot guess — this is for when the agent could proceed on its best guess, but being wrong would be expensive. Holds perfectly still, since it is the user's turn. Composes AI Action Toolbar. Use it before long runs, runs with side effects, or requests with ambiguous scope. Not for every request — adding a confirmation to a cheap, reversible action is friction that trains people to click through without reading.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/select"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/intent-preview/intent-preview.tsx",
          "target": "components/ui/intent-preview.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Use it only where misreading the request would be costly — long runs, side effects, ambiguous scope. On cheap, reversible actions it is friction.",
      "meta": {
        "engineeringNotes": "Built from the model-side view of how agentic runs fail: interpretation happens before planning, and an early misreading compounds through every step that follows. Surfacing the interpretation in one sentence, together with the specific guesses that filled gaps in the request, moves correction to the single point where it is nearly free. Two decisions keep that from becoming friction. First, assumptions arrive pre-filled with the agent's best guess, so the common case — the guess is right — costs one click, and the start label changes to 'Start with changes' only when something was corrected, so the confirmation carries information rather than ceremony. Second, correction happens in place through a Select of alternative readings rather than by rephrasing the whole request, which is both faster and more precise. Boundaries ('I won't…') are included because what an agent will not do is part of its intent and is the thing users most want to know before a run with side effects. It is deliberately distinct from Agent Ask: Ask blocks on missing information the agent cannot guess, whereas Intent Preview is for requests the agent could act on immediately, where acting on a wrong guess would be expensive.",
        "motionNotes": "Still, by design. This is a user-turn surface under the system's turn-taking grammar, so nothing moves or shimmers while it waits — the absence of motion is the signal that nothing proceeds until the user confirms. Motion is limited to the shared colour transition on hover and focus at `--pd-duration-instant`, inherited from the composed AI Action Toolbar and Select, and the brief fade of a correction list as it opens; both drop away under prefers-reduced-motion. Entrance motion belongs to whatever surface reveals the preview, not to the component.",
        "schema": {
          "component": "IntentPreview",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "IntentPreview",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "interpretation": {
                "type": "string",
                "description": "The request restated as the agent understood it — one plain sentence."
              },
              "assumptions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/IntentAssumption"
                },
                "description": "The guesses the agent made to fill gaps in the request."
              },
              "boundaries": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "What the agent will deliberately not do. Boundaries are part of intent."
              },
              "startLabel": {
                "type": "string"
              }
            },
            "required": [
              "interpretation"
            ],
            "$defs": {
              "IntentAssumption": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "What the agent had to decide, e.g. \"Dataset\"."
                  },
                  "value": {
                    "type": "string",
                    "description": "The agent's best guess."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Other reasonable readings. When present, the guess can be corrected in place."
                  }
                },
                "required": [
                  "id",
                  "label",
                  "value"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onStart",
              "required": true,
              "params": [
                {
                  "name": "values",
                  "type": "Record<string, string>"
                }
              ]
            },
            {
              "name": "onRephrase",
              "required": false,
              "params": [],
              "description": "Send the user back to rephrase the request."
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "live-status-line",
      "title": "Live Status Line",
      "description": "A single line saying what an agent is doing right now — the status bar of an agent run. Takes a `state` from Agent Presence's vocabulary, a `status` describing the current micro-action, an optional `detail` naming what the action is acting on (a filename, a query, a table), an optional `startedAt` that drives a live elapsed counter, and an `onCancel` shown whenever the agent is active. It replaces rather than accumulates, which is the whole distinction from its neighbours: Reasoning Stream keeps a scrollable trace, Chain-of-Thought Timeline keeps the entire turn's history, and this keeps exactly one thing — which is what makes it cheap enough to leave in permanent product chrome like a toolbar, sidebar footer, or table row. Composes Agent Presence in its `line` form for the indicator rather than reimplementing one, so the turn-taking grammar (motion means the machine is busy, stillness means it's your turn) holds here automatically. The status text is keyed so React's reconciliation runs its swap animation exactly once per real change; a re-render with unchanged text never re-animates. Layout is deliberately stable: the row holds a minimum height so changing text length cannot reflow its container, and the detail truncates before the verb does so 'Reading' stays legible even when the filename cannot. Unlike most components in this system the elapsed counter ticks internally rather than being caller-owned, because forcing a re-render once a second for a display detail would be a poor API. Use it wherever an agent's current activity should be visible without dedicating a panel to it. Not for the reasoning text itself (use Reasoning Stream), not for a per-artifact generation progress surface (use Generation Placeholder), and not as a general toast or notification.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/agent-presence"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/live-status-line/live-status-line.tsx",
          "target": "components/ui/live-status-line.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`, the `pd-status-swap` keyframe from @pixeldosa/pixeldosa-theme, and @pixeldosa/agent-presence, which it composes for the indicator. Installing this pulls Agent Presence in automatically.",
      "meta": {
        "engineeringNotes": "The defining constraint is that this component replaces rather than accumulates. Accumulation is what makes agent activity surfaces expensive — a growing list needs scroll management, virtualisation at length, and a decision about when to collapse. Holding exactly one line keeps it cheap enough to live permanently in product chrome, and pushes the history question to Reasoning Stream and Chain-of-Thought Timeline where it belongs. It composes Agent Presence's `line` form rather than drawing its own dots, which is the system's compose-don't-reinvent rule applied where it genuinely fits: the turn-taking grammar then holds here for free, including the fact that a `suggesting` or `awaitingApproval` status stops moving. Two layout decisions prevent the jitter this pattern usually has: a minimum row height, so text of varying length cannot reflow the container around it, and truncation applied to the detail rather than the verb, so 'Reading' remains legible when a long filename cannot be. The elapsed counter is the one place this system deliberately breaks its own 'the caller owns async state' convention — every other component takes progress, busy and duration as props — because a live counter would otherwise force the consumer to re-render once a second purely for a display detail. The interval is only started while the agent is actually active, and never when no `startedAt` was supplied.",
        "motionNotes": "One animation, and it maps to one state change: when the status text is replaced, the new text fades up three pixels over `--pd-duration-fast` with `--pd-ease-decelerate`. Short deliberately — micro-actions can change every second or two, and anything longer would read as churn rather than as replacement. It is keyed on the status string so React's reconciliation fires it exactly once per genuine change; an unchanged status that re-renders does not re-animate, which is the same mechanism Progressive Reveal and Reasoning Stream use. The indicator's own motion is inherited from Agent Presence and is not duplicated here. Under `prefers-reduced-motion` the swap is dropped entirely: text simply changes, which loses nothing, because the information was always the words rather than the transition between them.",
        "schema": {
          "component": "LiveStatusLine",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "LiveStatusLine",
            "type": "object",
            "properties": {
              "state": {
                "enum": [
                  "done",
                  "failed",
                  "thinking",
                  "working",
                  "asking",
                  "queued",
                  "deciding",
                  "streaming",
                  "cancelled",
                  "suggesting",
                  "awaitingApproval"
                ]
              },
              "status": {
                "type": "string",
                "description": "The current micro-action. Replaced wholesale as work moves on — never accumulated."
              },
              "detail": {
                "type": "string",
                "description": "What the action is acting on — a filename, a query, a table. Truncates before the verb does."
              },
              "startedAt": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "x-type": "Date"
                  }
                ],
                "description": "When the run started. The component ticks the elapsed counter itself rather than\nmaking the caller re-render once a second for a display detail — a deliberate\nexception to this system's usual \"the caller owns async state\" rule."
              },
              "indicator": {
                "enum": [
                  "mark",
                  "line"
                ],
                "description": "The indicator before the status. `mark` (default) is the agent's drawn head, whose\nface follows the state; `line` is three dots, for the densest rows."
              }
            },
            "required": [
              "state",
              "status"
            ]
          },
          "events": [
            {
              "name": "onCancel",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "media-result",
      "title": "Media Result",
      "description": "A finished piece of generated media, with its provenance attached. Takes the media itself as children, a `title`, an `aspectRatio` so the layout never jumps, `madeBy` naming the model and time, a `credentials` object (`attached`, a `detail` such as \"Content Credentials + invisible watermark\", and an optional `href` to verify), a `note` for usage rights, and `actions` with `onAction`. Provenance is stated in words beside the result rather than hidden in a details panel, and when credentials are **not** attached the frame says so plainly, because an unmarked file is the one a team needs to know about before it ships. That reflects the law as of August 2026: the EU AI Act's Article 50 and California's SB 942 both require machine-readable disclosure of AI-generated media, and the major generators now sign output with C2PA credentials and an invisible watermark. Purely presentational: you render the media and own the signing. Use it wherever a generated image, video or audio clip is presented as finished. Not for work still in progress (that is Generation Job), and not for a grid of candidates (that is Variation Grid).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/media-result/media-result.tsx",
          "target": "components/ui/media-result.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Render the media yourself and give images real alt text. Pass `credentials.attached: false` honestly — the absence is the useful signal.",
      "meta": {
        "engineeringNotes": "`credentials` is a small object rather than a boolean because the three questions a team has are different: is it signed, what was applied, and where can it be checked. Treating the unsigned case as a visible state in the waiting colour — instead of simply omitting the line — is the deliberate inversion here: products that only show a badge when signing succeeds make an unsigned file indistinguishable from a file nobody thought about, which is precisely the one that must not ship under the 2026 disclosure rules. The frame is a `figure` with a `figcaption`, so provenance and title are associated with the media by the platform rather than by proximity, and the whole caption stays outside the media box so it is never baked into a screenshot of the image itself.",
        "motionNotes": "Nothing animates: the work is finished, and this system reserves motion for a machine that is still busy. Only the standard instant colour changes on the action buttons, dropped under `prefers-reduced-motion`.",
        "schema": {
          "component": "MediaResult",
          "element": "figure",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "MediaResult",
            "type": "object",
            "properties": {
              "children": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "The result itself: an `img`, a `video`, an audio player. Yours to render."
              },
              "title": {
                "type": "string",
                "description": "What it is, in the user's words: \"Bottle on stone, warm light\"."
              },
              "aspectRatio": {
                "type": "string",
                "description": "Aspect ratio of the frame, so the layout never jumps while media loads."
              },
              "madeBy": {
                "type": "string",
                "description": "What made it and when, formatted by you. Shown with the result, not hidden in a\ndetails panel."
              },
              "credentials": {
                "type": "object",
                "properties": {
                  "attached": {
                    "type": "boolean",
                    "description": "True once your pipeline has signed the file."
                  },
                  "detail": {
                    "type": "string",
                    "description": "\"Content Credentials + SynthID\", \"C2PA manifest signed by Acme\"."
                  },
                  "href": {
                    "type": "string",
                    "description": "Where someone can check it."
                  }
                },
                "required": [
                  "attached"
                ],
                "description": "Content Credentials (C2PA) and any visible or invisible watermark. Since August 2026\nthe EU AI Act's Article 50 and California's SB 942 both require machine-readable\ndisclosure of AI-generated media, so this is a product requirement, not a nicety."
              },
              "note": {
                "type": "string",
                "description": "Your own note: usage rights, model terms, \"for internal use\"."
              },
              "actions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/MediaAction"
                }
              }
            },
            "required": [
              "children"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              },
              "MediaAction": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "primary": {
                    "type": "boolean",
                    "description": "The one action that takes the result forward. At most one."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onAction",
              "required": false,
              "params": [
                {
                  "name": "actionId",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "message",
      "title": "Message",
      "description": "One turn in a conversation, and the container the rest of an AI answer lives inside. `Message` takes a `role` ('user' or 'assistant'), a caller-owned `status` for assistant turns ('streaming', 'done', 'failed' or 'stopped'), and optional `author`, `avatar`, `meta` and `actions` (usually an AI Action Toolbar), plus `error`, `onRetry` and `onContinue`. Children are the turn's parts in the order the work happened: Reasoning Stream, Tool Call Group, then `MessageContent`. `MessageContent` styles plain HTML from any markdown renderer. User turns render as a right-aligned bubble. Assistant turns render full-width with no bubble, so long answers, code and tables have room. While streaming, text renders as it really arrives with a quantized cell cursor, never simulated typing, and actions stay hidden so nobody acts on half an answer. A failed turn keeps what arrived, says why and offers Retry. A stopped turn keeps the partial answer, labelled incomplete, with Continue. Use it for any assistant or agent conversation. Pair it with Message Scroller for the list.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/message/message.tsx",
          "target": "components/ui/message.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Bring your own markdown renderer and wrap its output in `MessageContent`. Set `status` from the stream itself: 'done' only when the stream closes, 'stopped' when the user aborted.",
      "meta": {
        "engineeringNotes": "Designed as the turn, not the text bubble. An assistant reply contains reasoning, tool calls, citations and prose, and treating those as siblings of the message breaks both reading order and accessibility. Here they are children of one `<article>`, named 'You said' or '<author> said' and marked `aria-busy` while streaming, so assistive tech treats the turn as one unit that is still arriving. Status is caller-owned because only the stream knows it. Each status has distinct UI, and none discards content: failed keeps the partial answer (dimmed) with an actionable error in an alert region and Retry; stopped labels the answer incomplete with Continue and Start over. Actions on assistant turns render only once the status is 'done', because copying or applying half an answer is an error the interface shouldn't invite. The user bubble keeps its actions hidden until hover or focus-within, and always shows them on touch devices via `(hover: none)`. Assistant turns have no bubble so code, tables and long prose get the full width. `MessageContent` carries typography for the HTML any markdown renderer emits, so the component stays renderer-agnostic and dependency-free.",
        "motionNotes": "The only moving part is the streaming cursor: one quantized cell pulsing on the shared `pd-cell-pulse` keyframe, the system's machine-busy grammar, rather than a blinking text caret, which would imply typing. It disappears the moment status leaves 'streaming', and the answer holds still once done. Actions on the user's own message fade in at `--pd-duration-fast` on hover or focus. There's no entrance animation on the message itself, because the list decides how new turns arrive. Under prefers-reduced-motion the cursor is a static cell and the fade is instant.",
        "schema": {
          "component": "Message",
          "element": "article",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Message",
            "type": "object",
            "properties": {
              "role": {
                "enum": [
                  "user",
                  "assistant"
                ]
              },
              "status": {
                "enum": [
                  "done",
                  "failed",
                  "streaming",
                  "stopped"
                ],
                "description": "Assistant messages only. Owned by the caller, from the real stream."
              },
              "author": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Shown above the message: \"Assistant\", a model name, a teammate."
              },
              "avatar": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "A small avatar or an `AgentPresence` in its dot form."
              },
              "meta": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Timestamp or other quiet metadata, shown next to the author."
              },
              "actions": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Actions for a finished message, usually an `AIActionToolbar`. Hidden until it finishes."
              },
              "error": {
                "type": "string",
                "description": "Why it failed, in words a user can act on."
              },
              "showCursor": {
                "type": "boolean",
                "description": "The end-of-stream cursor while streaming. Turn it off while something else already\nshows progress (a reasoning trace, a running tool), so there's one busy signal at a time."
              }
            },
            "required": [
              "role"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onRetry",
              "required": false,
              "params": []
            },
            {
              "name": "onContinue",
              "required": false,
              "params": [],
              "description": "Offered on a stopped message, to pick up where it left off."
            }
          ]
        },
        "subcomponents": [
          {
            "component": "MessageContent",
            "element": "div",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "MessageContent",
              "type": "object",
              "properties": {}
            },
            "events": []
          }
        ]
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "message-scroller",
      "title": "Message Scroller",
      "description": "The scrolling list of a conversation, which follows new content only while the reader is following along. Wrap `Message` elements with stable keys in `MessageScroller` and give it a height. It starts at the latest message. While the reader is at the bottom, new messages and streaming growth stay in view. The moment they scroll up, it stops following, nothing moves under them, and a 'Jump to latest' pill appears, counting messages that arrived meanwhile ('2 new messages'). Clicking it scrolls smoothly back down and pins again. When older messages load above (`onReachTop` fires near the top), the view compensates for the added height, so the page doesn't jump. Screen readers hear `announcement`, which you set when a message finishes, never per token. The region is keyboard-scrollable and named 'Conversation'. Use it for any chat or agent transcript. Not for feeds that should always show the newest item first.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/message-scroller/message-scroller.tsx",
          "target": "components/ui/message-scroller.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Give children stable `key`s, because history prepends are detected by the first key changing. Give the scroller a height (`h-[…]` or `flex-1` in a flex column). Set `announcement` when a reply finishes.",
      "meta": {
        "engineeringNotes": "Auto-scroll is where chat interfaces most often break trust: a stream drags the view down while someone is reading an earlier answer. Here 'pinned' is a fact about the reader, recomputed from their own scroll position with a 48px tolerance. Following happens only while pinned, both when children are added (layout effect) and when the last message grows during streaming (a ResizeObserver on the content, since token growth adds no children). While unpinned, added children are counted into the jump pill. A smooth jump sets a guard, so the intermediate 'not at bottom' frames of the animation don't unpin and flash the pill back. A 700ms fallback lands on the bottom even if streaming moved the target mid-animation. Prepended history is detected by the first child's key changing alongside a higher count, and compensated by adding the height delta to scrollTop in a layout effect before paint. Native `overflow-anchor` is disabled, so browsers with and without support behave identically. Accessibility uses a named, focusable region for keyboard scrolling and a separate polite status for caller-supplied announcements, because a live log would read every streamed token.",
        "motionNotes": "Following while pinned is instant, not smooth: animating toward a target that moves with every token lags behind and reads as sluggish. Smooth scrolling is reserved for the user's explicit 'Jump to latest', and falls back to instant under prefers-reduced-motion. The jump pill fades in at `--pd-duration-fast` on the decelerate curve when the reader leaves the bottom, and hover colour runs at `--pd-duration-instant`. History prepends are compensated before paint, so there is no visible jump. Everything becomes instant under prefers-reduced-motion.",
        "schema": {
          "component": "MessageScroller",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "MessageScroller",
            "type": "object",
            "properties": {
              "children": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode"
              },
              "announcement": {
                "type": "string",
                "description": "Text for screen readers when something worth hearing happens, e.g. \"Assistant replied:\nChurn rose in August…\". Set it once a message *finishes*, never per token."
              },
              "label": {
                "type": "string",
                "description": "Accessible name for the scroll region."
              }
            },
            "required": [
              "children"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onReachTop",
              "required": false,
              "params": [],
              "description": "Called when the reader scrolls near the top. Load older messages here."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "overlay",
      "title": "Overlay Motion Primitive",
      "description": "The shared enter/exit animation layer for every overlay surface in the system — Dialog, Sheet, Popover and Drawer. Exports OverlayScrim (the backdrop) and OverlayContent (the panel, with a placement prop of center, top, bottom, left or right that controls which axis it translates from) plus a re-exported AnimatePresence. Not a standalone component: it has no dismiss behaviour, focus trapping or portal logic of its own. Compose it underneath a Radix primitive (Dialog.Overlay/Content, Sheet, Popover) so that primitive owns interaction semantics while this owns motion.",
      "type": "registry:ui",
      "categories": [
        "overlays",
        "application"
      ],
      "dependencies": [
        "motion"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/overlay/overlay.tsx",
          "target": "components/ui/overlay.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Render OverlayScrim and OverlayContent inside AnimatePresence, controlled by the same boolean/state that drives your Radix primitive's open prop. Neither component owns mount/unmount timing — AnimatePresence does, via whatever is conditionally rendering them.",
      "meta": {
        "engineeringNotes": "Extracted as its own primitive rather than duplicated inside Dialog, Sheet, Popover and Drawer because those four are the same motion problem (scrim fade + panel translate/scale) wearing four different Radix primitives underneath. Without this, four components would each hand-roll slightly different durations the first time someone patches one of them, and the system's visual coherence — the actual point of a token-driven motion scale — degrades one component at a time. Deliberately excludes dismiss-on-click-outside, focus trapping and portalling: those are interaction semantics Radix already solves correctly, and re-solving them here would create two competing sources of truth for overlay behaviour.",
        "motionNotes": "Two states only: scrim opacity, and content opacity+scale+translate along one axis determined by `placement`. Enter uses duration.fast with easing.decelerate (content is arriving, should feel like it's settling in); exit uses duration.instant with easing.accelerate (dismissal should feel immediate, not lingering) — this asymmetry is deliberate, not an oversight. The motion solves orientation: a Sheet sliding in from the right tells the user where it came from and, on exit, where it's returning to, which a plain fade does not. Reduced motion swaps the whole transition object for the shared `reducedMotion` token (near-zero duration) rather than removing the animation, so Motion's exit callback still fires and AnimatePresence still unmounts the content correctly.",
        "schema": {
          "component": "OverlayContent",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "OverlayContent",
            "type": "object",
            "properties": {
              "placement": {
                "enum": [
                  "center",
                  "top",
                  "bottom",
                  "left",
                  "right"
                ],
                "description": "Which edge the content enters from. `center` is Dialog/Popover; the rest are Sheet/Drawer."
              }
            }
          },
          "events": []
        },
        "subcomponents": [
          {
            "component": "OverlayScrim",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "OverlayScrim",
              "type": "object",
              "properties": {}
            },
            "events": []
          }
        ]
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "parameter-panel",
      "title": "Parameter Panel",
      "description": "The controls beside the prompt: pick, don't describe. Takes `groups` (each a label and a set of `options` with a plain `label`, an optional `preview` thumbnail, a `costNote` and a `detail` line), the chosen `values` per group, `onChange`, a `summary` of what the choices add up to, and `onReset`. It answers the articulation barrier research directly: people can picture what they want but cannot name it, and style vocabulary is exactly the knowledge they lack, so options are shown as small previews they can recognise instead of words they must recall. Two further findings are built in: every option keeps a text label, because novel icons are misread almost universally, and any option that changes the price says so where it is chosen rather than in a bill afterwards. Purely presentational: you own the options, the previews and the pricing. Use it beside a prompt box for image, video or audio generation. Not for a settings page, and not for free-text parameters, which belong in Prompt Composer's controls.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/parameter-panel/parameter-panel.tsx",
          "target": "components/ui/parameter-panel.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Give every option a real `preview` where you can — a thumbnail of that style, a shape for an aspect ratio — and never ship an option that is only an icon.",
      "meta": {
        "engineeringNotes": "Each group is a real `fieldset` of radio inputs with the input visually hidden behind its label, so arrow-key navigation, grouping and announcement come from the platform rather than from a custom roving-tabindex implementation that would have to be re-tested in every host app. The chosen value is caller-owned per group id, which lets a product persist presets, share them in a URL, or drive them from a model's suggestion without the component holding a second source of truth. `costNote` sits on the option rather than being derived, because only the caller knows how its pricing composes — quality multiplied by count multiplied by resolution is not something a component can infer from labels. `detail` renders once per group under the current choice instead of on every option, which keeps a panel of twelve options from becoming a wall of help text.",
        "motionNotes": "Nothing animates beyond the standard instant colour change on a choice. Selection is carried by border weight and label weight rather than motion: this is the user's turn, and a panel that animates under a moving cursor invites mis-clicks. Under `prefers-reduced-motion` even those transitions are dropped.",
        "schema": {
          "component": "ParameterPanel",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ParameterPanel",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "groups": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ParameterGroup"
                }
              },
              "values": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Chosen option id per group id."
              },
              "summary": {
                "type": "string",
                "description": "What the current choices add up to: \"4 images · 1024 × 1024 · 40 credits\"."
              }
            },
            "required": [
              "groups",
              "values"
            ],
            "$defs": {
              "ParameterGroup": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Style\", \"Shape\", \"How many\"."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/ParameterOption"
                    }
                  }
                },
                "required": [
                  "id",
                  "label",
                  "options"
                ]
              },
              "ParameterOption": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Plain words, never a branded name: \"Wide, 16:9\", not \"Cinemascope Pro\"."
                  },
                  "preview": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "A small preview: a thumbnail, a shape, a swatch. Recognition beats recall."
                  },
                  "costNote": {
                    "type": "string",
                    "description": "What choosing this does to the price or the wait: \"+20 credits\"."
                  },
                  "detail": {
                    "type": "string",
                    "description": "One line of help, shown under the group when this option is chosen."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onChange",
              "required": true,
              "params": [
                {
                  "name": "groupId",
                  "type": "string"
                },
                {
                  "name": "optionId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onReset",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "pixeldosa-agent-guide",
      "title": "PixelDosa UI Agent Guide",
      "description": "An instruction file for AI coding agents building with PixelDosa UI, installed into a team's repository so their agent knows not just which component to use but how to use it well. Covers registry setup, a job-to-component selection table, when to start from a Block instead of assembling parts, the non-negotiable UX rules (nothing commits silently, never add Accept all, approvals phrased as consequences with reversibility and impact stated, nothing waiting on the user is animated, shimmer only on active status verbs, a visible Stop during every working state, no faked typing, confidence as tiers not percentages), a mapping table from common agent events to component states, and a pre-finish checklist. Installs as a Claude Code skill; other agents can be pointed at the same file or at the site's llms.txt. Contains no code and adds no dependencies.",
      "type": "registry:file",
      "categories": [
        "ai-assisted",
        "agents"
      ],
      "dependencies": [],
      "registryDependencies": [],
      "files": [
        {
          "path": "../../packages/ui/src/registry/pixeldosa-agent-guide/SKILL.md",
          "target": "~/.claude/skills/pixeldosa/SKILL.md",
          "type": "registry:file"
        }
      ],
      "docs": "Installs to .claude/skills/pixeldosa/SKILL.md, where Claude Code discovers it automatically. For Cursor, Copilot, or other agents, reference this file from your rules or AGENTS.md, or point the agent at https://pixeldosa.akshayjayan.com/llms.txt.",
      "meta": {
        "engineeringNotes": "Coding agents already find and install PixelDosa UI through the shadcn MCP server and CLI search, because every registry description was written for cold LLM comprehension. What they could not do was use the components well: nothing stopped an agent from wiring an AI value straight into data or adding an Accept all button, since those decisions live in the design system's judgment rather than its props. This item transfers that judgment into the consumer's repository as a plain markdown skill, so the rules travel with the code instead of staying on a docs site the agent may never read. It is the same source the site's llms.txt draws its rules from, so the two cannot drift. It ships as registry:file with no dependencies, consistent with the principle that PixelDosa UI never requires an agent stack.",
        "motionNotes": "No motion — this item is a documentation file, not a component. It does, however, carry the system's motion rules to agents in written form: motion means the machine is busy and stillness means it is the user's turn, shimmer is reserved for active status verbs, completed text is never revealed character by character, and prefers-reduced-motion is always respected rather than overridden."
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "pixeldosa-theme",
      "title": "PixelDosa UI Theme",
      "description": "The PixelDosa UI design and motion token layer, installed as CSS custom properties. Provides semantic colors for light and dark mode, including surfaces, text, controls, agent-status roles, and soft illustration-only fills, plus the base corner radius and duration/easing/stagger variables. Illustration fills are decorative and do not communicate state. Install this once per project before or alongside any other PixelDosa UI component; installing a component without it leaves the component styled against the host project's existing theme values instead.",
      "type": "registry:theme",
      "categories": [
        "core",
        "theme"
      ],
      "files": [],
      "cssVars": {
        "theme": {
          "--pd-duration-instant": "100ms",
          "--pd-duration-fast": "200ms",
          "--pd-duration-base": "300ms",
          "--pd-duration-slow": "500ms",
          "--pd-duration-deliberate": "800ms",
          "--pd-ease-standard": "cubic-bezier(0.4, 0, 0.2, 1)",
          "--pd-ease-decelerate": "cubic-bezier(0, 0, 0.2, 1)",
          "--pd-ease-accelerate": "cubic-bezier(0.4, 0, 1, 1)",
          "--pd-stagger-tight": "30ms",
          "--pd-stagger-base": "60ms",
          "--pd-stagger-loose": "100ms"
        },
        "light": {
          "radius": "0.75rem",
          "background": "oklch(0.975 0.005 106)",
          "foreground": "oklch(0.231 0.004 264)",
          "card": "oklch(0.993 0.003 106)",
          "card-foreground": "oklch(0.231 0.004 264)",
          "popover": "oklch(0.993 0.003 106)",
          "popover-foreground": "oklch(0.231 0.004 264)",
          "illustration-lilac": "oklch(0.88 0.055 305)",
          "illustration-peach": "oklch(0.89 0.055 42)",
          "illustration-butter": "oklch(0.92 0.065 92)",
          "primary": "oklch(0.231 0.004 264)",
          "primary-foreground": "oklch(0.993 0.003 106)",
          "secondary": "oklch(0.948 0.008 99)",
          "secondary-foreground": "oklch(0.231 0.004 264)",
          "muted": "oklch(0.948 0.008 99)",
          "muted-foreground": "oklch(0.493 0.008 268)",
          "accent": "oklch(0.948 0.008 99)",
          "accent-foreground": "oklch(0.231 0.004 264)",
          "destructive": "oklch(0.55 0.22 27)",
          "destructive-foreground": "oklch(0.985 0.005 24)",
          "border": "oklch(0.909 0.008 99)",
          "input": "oklch(0.850 0.011 101)",
          "ring": "oklch(0.493 0.008 268)",
          "agent-working": "oklch(0.506 0.182 265)",
          "agent-working-soft": "oklch(0.937 0.024 268)",
          "agent-waiting": "oklch(0.532 0.137 50)",
          "agent-waiting-soft": "oklch(0.949 0.031 71)",
          "agent-blocked": "oklch(0.523 0.171 28)",
          "agent-blocked-soft": "oklch(0.932 0.028 26)",
          "agent-done": "oklch(0.516 0.119 149)",
          "agent-done-soft": "oklch(0.942 0.025 149)"
        },
        "dark": {
          "background": "oklch(0.166 0.004 129)",
          "foreground": "oklch(0.939 0.007 97)",
          "card": "oklch(0.203 0.004 129)",
          "card-foreground": "oklch(0.939 0.007 97)",
          "popover": "oklch(0.203 0.004 129)",
          "popover-foreground": "oklch(0.939 0.007 97)",
          "illustration-lilac": "oklch(0.32 0.033 305)",
          "illustration-peach": "oklch(0.33 0.033 42)",
          "illustration-butter": "oklch(0.35 0.038 92)",
          "primary": "oklch(0.939 0.007 97)",
          "primary-foreground": "oklch(0.166 0.004 129)",
          "secondary": "oklch(0.245 0.006 122)",
          "secondary-foreground": "oklch(0.939 0.007 97)",
          "muted": "oklch(0.245 0.006 122)",
          "muted-foreground": "oklch(0.711 0.010 100)",
          "accent": "oklch(0.251 0.006 122)",
          "accent-foreground": "oklch(0.939 0.007 97)",
          "destructive": "oklch(0.653 0.184 24)",
          "destructive-foreground": "oklch(0.16 0.02 24)",
          "border": "oklch(0.278 0.005 122)",
          "input": "oklch(0.336 0.007 118)",
          "ring": "oklch(0.68 0.010 100)",
          "agent-working": "oklch(0.731 0.122 267)",
          "agent-working-soft": "oklch(0.289 0.052 266)",
          "agent-waiting": "oklch(0.779 0.127 63)",
          "agent-waiting-soft": "oklch(0.299 0.038 69)",
          "agent-blocked": "oklch(0.712 0.147 26)",
          "agent-blocked-soft": "oklch(0.287 0.042 25)",
          "agent-done": "oklch(0.761 0.131 152)",
          "agent-done-soft": "oklch(0.296 0.043 152)"
        }
      },
      "css": {
        "@layer base": {
          "svg[aria-hidden=\"true\"], [data-pd-decorative]": {
            "user-select": "none",
            "-webkit-user-select": "none"
          }
        },
        "@keyframes pd-fade-in": {
          "from": {
            "opacity": "0"
          },
          "to": {
            "opacity": "1"
          }
        },
        "@keyframes pd-cell-pulse": {
          "0%, 100%": {
            "transform": "scale(0.3)"
          },
          "50%": {
            "transform": "scale(0.9)"
          }
        },
        "@keyframes pd-cell-pulse-opacity": {
          "0%, 100%": {
            "opacity": "0.08"
          },
          "50%": {
            "opacity": "0.5"
          }
        },
        "@keyframes pd-sweep": {
          "0%": {
            "transform": "translateX(-100%)"
          },
          "100%": {
            "transform": "translateX(100%)"
          }
        },
        "@keyframes pd-shimmer": {
          "from": {
            "background-position": "180% 0"
          },
          "to": {
            "background-position": "-80% 0"
          }
        },
        "@supports ((-webkit-background-clip: text) or (background-clip: text))": {
          ".pd-shimmer": {
            "background-image": "linear-gradient(90deg, var(--muted-foreground) 0%, var(--muted-foreground) 35%, var(--foreground) 50%, var(--muted-foreground) 65%, var(--muted-foreground) 100%)",
            "background-size": "250% 100%",
            "-webkit-background-clip": "text",
            "background-clip": "text",
            "color": "transparent",
            "animation": "pd-shimmer 2.4s linear infinite"
          }
        },
        "@media (prefers-reduced-motion: reduce)": {
          ".pd-shimmer": {
            "animation": "none",
            "background-image": "none",
            "-webkit-background-clip": "border-box",
            "background-clip": "border-box",
            "color": "inherit"
          }
        },
        "@keyframes pd-status-swap": {
          "from": {
            "opacity": "0",
            "transform": "translateY(3px)"
          },
          "to": {
            "opacity": "1",
            "transform": "translateY(0)"
          }
        },
        "@keyframes pd-orb-spin": {
          "from": {
            "transform": "rotateY(0deg)"
          },
          "to": {
            "transform": "rotateY(360deg)"
          }
        }
      },
      "docs": "The cssVars in this item are generated from packages/tokens. Do not edit them here — change the token module and rebuild.",
      "meta": {
        "engineeringNotes": "Shipped as a registry:theme item rather than baked into each component so that colour is a project-level decision made once, not a per-component import that would break the moment a consumer wants their own palette. Colours are OKLCH rather than HSL so the light and dark scales can be derived from matched perceptual lightness steps; the practical payoff is that a token pair like muted/muted-foreground holds its contrast ratio across both modes without per-mode hand-tuning. Motion values are emitted as CSS custom properties in addition to the TypeScript exports, so components that need only a CSS transition can honour the motion scale without pulling in a JS animation runtime.",
        "motionNotes": "This item defines the motion scale rather than animating anything itself. Durations are exposed in milliseconds (CSS) and seconds (TypeScript/Motion) from one source, because emitting only one unit guarantees a conversion bug at the first component that needs the other."
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "progressive-reveal",
      "title": "Progressive Reveal",
      "description": "Reveals a growing collection of items (rows, cards) one at a time as they arrive from a streaming or incrementally-loading source, with a stable layout and no per-character typewriter effect. Takes items (the current known array, expected to grow over time via the consumer's own state), a renderItem(item, index) function for the actual per-item markup, an optional keyExtractor for stable identity, and an isStreaming flag that shows a trailing 'More loading…' indicator while more items are expected. Each item is given its entrance transition exactly once: React's own keyed reconciliation means an item already present on a prior render keeps its existing DOM node and never remounts, so its animation can never replay. A layout prop switches between a vertical stack (default) and a CSS grid for card collections. Use for streaming search results, tool-call output rows, or generated card grids — any collection whose full membership isn't known upfront. Not for streaming prose (use an existing chat/response component) or a single changing value (that's Smart Field or Diff Accept's job, not a collection).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/progressive-reveal/progressive-reveal.tsx",
          "target": "components/ui/progressive-reveal.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Is a plain generic function component (no `forwardRef`) since nothing here needs an imperative handle and `forwardRef` has poor generic-type ergonomics. Pass a real `keyExtractor` whenever the source array might ever be spliced rather than purely appended — without one, index-based keys can misattribute 'already seen' identity to the wrong item if earlier entries are removed or reordered.",
      "meta": {
        "engineeringNotes": "Enforces 'animate a new item in exactly once' by relying on React's own keyed reconciliation instead of hand-rolled 'already seen' tracking: each item is wrapped in a small RevealItem component keyed by keyExtractor (or index), which mounts fresh — and therefore plays its entrance transition — only the first time its key appears in the list; on every subsequent render where that key was already present, React reuses the existing DOM node and the component is not remounted, so the animation structurally cannot replay. This is simpler and more reliable than maintaining a Set of seen keys by hand, which is easy to get subtly wrong around React's own re-render timing. Scoped deliberately to structured collections (rows, cards), not prose — an append-only list of discrete items is inherently more layout-stable than a single growing text block, since new rows appear below existing content without reflowing it, which is also why this differs from and does not compete with Vercel AI Elements' prose-streaming components (see the roadmap's North Star Reconciliation table).",
        "motionNotes": "Per-item entrance is a plain opacity + small upward translate (translate-y-1 to 0) using the same two-phase-mount pattern (mount closed, flip to open on the next animation frame) Command Menu and Selection Actions already use, on --pd-duration-fast/--pd-ease-standard. No item's own content is ever revealed character-by-character — the full item value already exists at arrival time, so a typewriter effect would misrepresent it as being generated live, the same reasoning every other AI-tier component in this system has already established independently. The trailing streaming indicator uses Tailwind's built-in animate-pulse (core Tailwind, no plugin required) gated by motion-safe:, falling back to a static reduced-opacity dot under motion-reduce: — the 'more is coming' text itself is unaffected either way, so the signal survives motion removal.",
        "schema": {
          "component": "ProgressiveReveal",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ProgressiveReveal",
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {}
              },
              "isStreaming": {
                "type": "boolean",
                "description": "Shows a trailing \"more incoming\" indicator while true."
              },
              "layout": {
                "enum": [
                  "grid",
                  "stack"
                ],
                "description": "`\"stack\"` (default) lays items in a vertical flex column; `\"grid\"` uses a CSS grid."
              }
            },
            "required": [
              "items"
            ]
          },
          "events": [
            {
              "name": "renderItem",
              "required": true,
              "params": [
                {
                  "name": "item",
                  "type": "T"
                },
                {
                  "name": "index",
                  "type": "number"
                }
              ]
            },
            {
              "name": "keyExtractor",
              "required": false,
              "params": [
                {
                  "name": "item",
                  "type": "T"
                },
                {
                  "name": "index",
                  "type": "number"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "prompt-composer",
      "title": "Prompt Composer",
      "description": "A request composer that isn't a blank chat box. Free text for what only words can say, structured `controls` for what shouldn't need prompt engineering (tone, length, audience, format), and attachments for context. Each control is a labelled pill ('Tone: Friendly') that opens a list of choices, and a pill changed from its default is outlined, so the specification can be read at a glance. `onSubmit` receives `{ text, values, attachments }`. The text area grows with its content up to a cap. Enter sends and Shift+Enter adds a line, except during IME composition. Attachments come in through the attach button, drag and drop, or paste. You upload them via `onAttachFiles` and pass `attachments` back with status uploading, ready or failed. Sending is blocked while an upload runs, and the composer says why. `suggestions` fill the text but never send, the character count appears only near `maxLength`, and while `busy`, Send becomes Stop. Use it for generation features: drafts, images, reports. Not a chat input: no thread, no history.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/select"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/prompt-composer/prompt-composer.tsx",
          "target": "components/ui/prompt-composer.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Put anything users would otherwise type as boilerplate (\"make it friendlier, keep it short\") into `controls`, and turn `values` into the prompt on your side. Upload in `onAttachFiles` and keep `attachments` in your state.",
      "meta": {
        "engineeringNotes": "Exists because the system argues against making every AI feature a blank chat box and needed an alternative. The composer splits a request into what only words can say (the text) and what is really a specification (controls), because specifications typed as prose are inconsistent and invisible until the result comes back wrong. Controls are Select pills: keyboard, typeahead and screen reader behaviour come from Select, each pill sizes to its current choice rather than its longest option, and the open list is drawn in the system's own surface instead of the platform's menu. A changed value gets a stronger outline, so the non-default parts of the request stand out. Details are handled where composers usually slip. Enter is ignored while `isComposing`, so IME users confirming a candidate don't send half a sentence. The text area autosizes from scrollHeight up to 240px, because `field-sizing: content` isn't universal yet. Send is `aria-disabled`, not `disabled`, so it stays focusable and the blocked reason ('Waiting for attachments to finish uploading') is shown and announced. The count appears only in the last 10% before `maxLength`. The file input is reset after each pick, so the same file can be picked again. Uploads are the caller's, and the composer only reflects their status. It stays headless about what happens next: the caller assembles the prompt from `values`.",
        "motionNotes": "Quiet, because composing is the user's turn. The only moving element is the shared `pd-shimmer` on an uploading attachment's 'Uploading' label, which is real machine work. Borders and backgrounds change colour at `--pd-duration-instant` on focus, hover, drag-over and control changes. The text area grows instantly as lines are added, with no height transition, so the caret never lags the text. Send swaps to Stop without animation, so the control is usable the moment generation starts. Everything is instant or static under prefers-reduced-motion.",
        "schema": {
          "component": "PromptComposer",
          "element": "form",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "PromptComposer",
            "type": "object",
            "properties": {
              "controls": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ComposerControl"
                },
                "description": "Structured parts of the request. The interface carries the specification, not the prompt."
              },
              "attachments": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ComposerAttachment"
                }
              },
              "accept": {
                "type": "string"
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Starting points that fill the text for editing. Never sent without the user."
              },
              "busy": {
                "type": "boolean",
                "description": "The result is being generated. Send becomes Stop."
              },
              "placeholder": {
                "type": "string"
              },
              "maxLength": {
                "type": "number"
              },
              "submitLabel": {
                "type": "string"
              },
              "label": {
                "type": "string",
                "description": "Visible label for the text area. Visually hidden by default."
              },
              "clearOnSubmit": {
                "type": "boolean",
                "description": "Clear the text after sending. Controls keep their values either way."
              },
              "value": {
                "type": "string",
                "description": "Controlled text, for filling the composer from outside (e.g. a follow-up suggestion)."
              }
            },
            "$defs": {
              "ComposerControl": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Short label shown on the pill: \"Tone\", \"Length\", \"For\"."
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "value",
                            "label"
                          ]
                        }
                      ]
                    }
                  },
                  "defaultValue": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "options",
                  "defaultValue"
                ]
              },
              "ComposerAttachment": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "detail": {
                    "type": "string",
                    "description": "e.g. \"PDF · 2.4 MB\"."
                  },
                  "status": {
                    "enum": [
                      "failed",
                      "uploading",
                      "ready"
                    ]
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onSubmit",
              "required": true,
              "params": [
                {
                  "name": "submission",
                  "type": "ComposerSubmission"
                }
              ]
            },
            {
              "name": "onAttachFiles",
              "required": false,
              "params": [
                {
                  "name": "files",
                  "type": "File[]"
                }
              ],
              "description": "Enables the attach button, drop and paste. You upload; pass the results back as `attachments`."
            },
            {
              "name": "onRemoveAttachment",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onStop",
              "required": false,
              "params": []
            },
            {
              "name": "onValueChange",
              "required": false,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "reasoning-stream",
      "title": "Reasoning Stream",
      "description": "An agent's reasoning shown as it arrives — a two-line ticker while it streams, folding to a single 'Thought for 12s' line once the answer exists, expandable on demand to the full trace. Takes `steps` (reasoning summary lines, oldest first, appended as they arrive), `isStreaming`, an optional measured `durationMs` shown on completion, and `defaultExpanded`. The default is collapsed, deliberately: reasoning is scaffolding that is useful while a thing is being built and noise once it stands, and a reasoning panel left expanded after the answer lands makes every answer look like it needs justifying. Handles scroll anchoring correctly — it follows new content only while the reader is at the bottom, and the moment they scroll up it holds their position and offers a 'Jump to latest' affordance instead of yanking them back. Steps fade in whole and exactly once, enforced by React's keyed reconciliation rather than a timer; text that has already arrived is never revealed character-by-character, since that would misrepresent content the model already finished producing. `steps` is an array rather than a raw string because providers emit reasoning summaries as discrete parts and deciding where one thought ends belongs to the caller. Maps to OpenAI Responses reasoning-summary deltas and the AI SDK's `reasoning` part type, with no AI dependency of its own. Use it wherever a model exposes reasoning and seeing the work builds warranted trust. Not for the agent's activity state (use Agent Presence), not for the full historical record across a whole turn (use Chain-of-Thought Timeline), and not for a collapsed 'why this?' over an already-finished value (use AI Context Surface).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/reasoning-stream/reasoning-stream.tsx",
          "target": "components/ui/reasoning-stream.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and the `pd-fade-in` keyframe supplied by @pixeldosa/pixeldosa-theme's css block. Show reasoning *summaries* where the provider exposes them; raw private chain-of-thought should not be presented to end users as fact.",
      "meta": {
        "engineeringNotes": "The component's thesis is the fold-away on completion, not the ticker. Reasoning text is long, low-value-per-token, and usually not what the user wants to read, which creates a bind: showing all of it buries the answer, showing none of it is the black box that destroys trust. Collapsing to a measured 'Thought for 12s' resolves both — the duration answers 'was this worth waiting for' better than any amount of reasoning text, and the disclosure keeps the trace available for anyone who actually wants it. Scroll anchoring is implemented as a correctness requirement rather than a nicety: the component tracks distance-from-end on scroll and only follows new content while the reader is within 24px of the bottom, because auto-scrolling someone who has deliberately scrolled up to read something is the most hostile thing a streaming surface can do. That behaviour came from a competitive scan of a peer library's Message Scroller and now applies as a standing requirement to every streaming surface in this system, not as a component of its own. Each step is its own keyed child component so React's reconciliation owns the entrance animation — a step already on screen keeps its DOM node and can never replay its fade however often the parent re-renders, which is the same mechanism Progressive Reveal uses and which a timer-based approach gets wrong on the first re-render. `steps` is an array rather than a raw accumulating string so the component never has to guess where one thought ends.",
        "motionNotes": "Steps fade in whole over `--pd-duration-base` with `--pd-ease-decelerate`, exactly once each. They are never typed out character-by-character: the text has already been produced by the time it reaches this component, and animating it as though it were being composed live would misrepresent what actually happened — the same non-negotiable rule Ghost Input, Smart Field, Diff Accept and Progressive Reveal all hold. The expand/collapse uses the house `grid-template-rows` 0fr→1fr transition at `--pd-duration-fast`, so no height measurement or ResizeObserver is needed and the panel animates correctly at any content length. The ticker's top edge is a CSS mask rather than an animated fade, so older lines dissolve out without anything moving. Under `prefers-reduced-motion` the step fades and the expand transition are both dropped; steps appear instantly and the disclosure snaps, and since the information was never carried by the motion, nothing is lost.",
        "schema": {
          "component": "ReasoningStream",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ReasoningStream",
            "type": "object",
            "properties": {
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Reasoning summary lines, oldest first, appended as they arrive. An array rather\nthan a raw string because providers emit reasoning *summaries* as discrete parts,\nand deciding where one thought ends is the consumer's business, not this\ncomponent's."
              },
              "isStreaming": {
                "type": "boolean"
              },
              "durationMs": {
                "type": "number",
                "description": "Measured elapsed time. Shown once streaming ends — never estimated."
              },
              "label": {
                "type": "string",
                "description": "Verb shown while streaming."
              },
              "defaultExpanded": {
                "type": "boolean"
              }
            },
            "required": [
              "steps"
            ]
          },
          "events": []
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "response-feedback",
      "title": "Response Feedback",
      "description": "Thumbs with a reason that goes somewhere. Takes `reasons` (yours, in the user's words; a sensible default set ships), `onSubmit` receiving `{ verdict, reasonIds, comment }`, an optional `destination` naming where the feedback goes, `askOnUp` to ask after praise too, and `submitted` when you've already recorded feedback for this answer. A thumbs up sends on its own — praise shouldn't cost a form — while a thumbs down opens a small panel of reasons as multi-select chips with an optional comment, then a single Send. The confirmation names the destination, because a product that collects feedback without saying what happens to it is asking a favour it never repays. Purely presentational: you route the submission. Use it under an answer, beside AI Action Toolbar. Not for a support ticket, not for a rating that decides what the model does next, and not as the only route for reporting something harmful — that needs a path a person reads.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/response-feedback/response-feedback.tsx",
          "target": "components/ui/response-feedback.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Route `onSubmit` somewhere a person actually reads.",
      "meta": {
        "engineeringNotes": "The asymmetry between up and down is the design: a thumbs up submits immediately and a thumbs down opens the reason panel, because the useful signal is concentrated in what went wrong and a form after praise costs goodwill for data nobody reads. Reasons are multi-select rather than a single choice, since an answer is usually wrong in more than one way, and they arrive as ids so the caller can route and aggregate them without parsing labels. The comment stays optional and is trimmed to undefined when empty, so a submission never carries a whitespace string that looks like feedback in a dashboard. After sending, the component collapses to a single sentence naming the destination instead of leaving the controls in place, which prevents the double-submit that turns one opinion into three.",
        "motionNotes": "Nothing moves except the confirmation, which fades in once over `--pd-duration-base`. The reason panel appears without a slide or a height animation: it is the user's turn, and an animating panel under a cursor that is already moving toward a chip costs a mis-click. Under `prefers-reduced-motion` the fade is dropped.",
        "schema": {
          "component": "ResponseFeedback",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ResponseFeedback",
            "type": "object",
            "properties": {
              "reasons": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/FeedbackReason"
                },
                "description": "Reasons offered after a thumbs down. Yours, not generic ones."
              },
              "askOnUp": {
                "type": "boolean",
                "description": "Also ask why after a thumbs up. Off by default: praise shouldn't cost a form."
              },
              "destination": {
                "type": "string",
                "description": "Where it goes and what happens to it — the thing most products never say."
              },
              "label": {
                "type": "string"
              },
              "submitted": {
                "type": "boolean",
                "description": "Set when the caller has already recorded feedback for this answer."
              }
            },
            "$defs": {
              "FeedbackReason": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "What was wrong, in the user's words: \"Made something up\", \"Wrong source\"."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onSubmit",
              "required": true,
              "params": [
                {
                  "name": "submission",
                  "type": "FeedbackSubmission"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "response-versions",
      "title": "Response Versions",
      "description": "Every answer the agent gave, kept. Takes `versions` (oldest first, each with `content`, an optional `label` naming what produced it and a pre-formatted `time`), the `activeId` and `onActiveChange`, plus optional `onRegenerate`, `onKeep` and `busy`. Stepping back and forth is two buttons and a count (\"Version 2 of 3\"); \"Compare with previous\" puts the current version beside the one before it, with the current one outlined; \"Keep this one\" hands a version back to you as the answer. Regenerating appends rather than replaces, which is the whole point: in most products regenerating destroys the previous answer, so a better answer is a gamble and people keep copies elsewhere. Purely presentational — you own the list, the regeneration and what keeping means. Use it under any answer people regenerate: chat replies, drafts, generated copy. Not for a full document's revision history, and not for comparing two arbitrary versions far apart in a list.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/response-versions/response-versions.tsx",
          "target": "components/ui/response-versions.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Append a new version on regenerate; never replace one.",
      "meta": {
        "engineeringNotes": "Versions are a caller-owned array and the component holds only the compare toggle, because the history has to outlive this component's mount — a version list kept in local state would be lost by any parent re-mount, which is exactly the loss the component exists to prevent. Comparison is deliberately limited to the current version and the one immediately before it: an arbitrary two-version picker doubles the controls for a case nobody had, and the common question is always \"is this better than the last try?\". The compare toggle resets itself when there is no previous version, so a shrinking list cannot leave the component in a state it cannot render. The announcement says which version is showing rather than the content, so switching versions with a screen reader gives position, not a re-read of the whole answer.",
        "motionNotes": "One animation: the new version fades in over `--pd-duration-base`, keyed on the version id so a re-render with the same version never re-animates. Regeneration is the only busy state, and it lives in the button's own label (\"Writing another…\") rather than a spinner over the text, because the old answer stays readable the entire time. Under `prefers-reduced-motion` the fade is dropped and the switch is instant.",
        "schema": {
          "component": "ResponseVersions",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ResponseVersions",
            "type": "object",
            "properties": {
              "versions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/ResponseVersion"
                },
                "description": "Oldest first. A regenerate appends; nothing is ever replaced."
              },
              "activeId": {
                "type": "string"
              },
              "busy": {
                "type": "boolean",
                "description": "True while a new version is being produced."
              },
              "label": {
                "type": "string"
              }
            },
            "required": [
              "versions",
              "activeId"
            ],
            "$defs": {
              "ResponseVersion": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "What produced it, in the user's words: \"Shorter\", \"With sources\", \"First answer\"."
                  },
                  "time": {
                    "type": "string",
                    "description": "Already formatted by you: \"2 minutes ago\". Rendered as given, never recomputed."
                  },
                  "content": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode"
                  }
                },
                "required": [
                  "id",
                  "content"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onActiveChange",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onRegenerate",
              "required": false,
              "params": [],
              "description": "Produces another version. The previous ones stay."
            },
            {
              "name": "onKeep",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Keeps this version as the answer and ends the comparison."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "run-inbox",
      "title": "Run Inbox",
      "description": "Every agent run in one place, with the ones that need a person at the top. Takes `runs` (each with a `title` in the user's words, a `state` of `needsYou`, `running`, `scheduled`, `done` or `failed`, an `agent` name, a one-line `note`, a pre-formatted `time` and an `unread` flag), plus `onOpen` and an optional `onStop`. The grouping is the design: an inbox is the familiar shape for background agents, and its familiar failure is strict chronology, where a run that has waited three hours for an answer sits below one that finished a minute ago. Here anything waiting on a person comes first whatever its age, failures come next, and each group carries a count so the size of the pile is visible before scrolling. Purely presentational: you own the runs, the routing and what opening one does. Use it as the home screen for background or scheduled agents. Not for the steps inside one run (that is Agent Plan), and not as a notification feed.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/run-inbox/run-inbox.tsx",
          "target": "components/ui/run-inbox.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Phrase `note` as the thing a person needs to decide or know, not as a status code.",
      "meta": {
        "engineeringNotes": "Grouping by state rather than sorting by time is the whole component, and it is why this is not a generic list: the research on agent management interfaces names the inbox's scaling problem exactly, and a pile ordered by recency buries the only rows that block progress. Counts sit on the group headers instead of a single unread badge, because 'four waiting on you' and 'four finished' are different feelings and one number cannot carry both. `unread` only changes weight and adds a screen-reader phrase rather than driving a separate 'new' section, since a run's state matters more than whether it has been opened. Stop appears only on running rows, so the control cannot be aimed at a run that has already ended.",
        "motionNotes": "Motion means the machine is busy: only a running row's dot pulses. Rows waiting on a person hold completely still in the waiting colour, which is the turn-taking grammar applied to a list — the eye is drawn by stillness and position rather than by something blinking for attention. Under `prefers-reduced-motion` the pulse stops and state is carried by colour, position and words.",
        "schema": {
          "component": "RunInbox",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "RunInbox",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "runs": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/AgentRun"
                }
              },
              "selectedId": {
                "type": "string",
                "description": "The run currently open beside this list, so the row it came from is marked."
              },
              "emptyLabel": {
                "type": "string"
              }
            },
            "required": [
              "runs"
            ],
            "$defs": {
              "AgentRun": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "description": "What it was asked to do, in the user's words."
                  },
                  "state": {
                    "enum": [
                      "needsYou",
                      "running",
                      "scheduled",
                      "done",
                      "failed"
                    ]
                  },
                  "agent": {
                    "type": "string",
                    "description": "Which agent, if you run more than one."
                  },
                  "note": {
                    "type": "string",
                    "description": "Why it needs you, what it's doing, or what it produced. One line."
                  },
                  "time": {
                    "type": "string",
                    "description": "Formatted by you: \"2 minutes ago\", \"Tomorrow at 9:00\"."
                  },
                  "unread": {
                    "type": "boolean",
                    "description": "Not looked at yet."
                  }
                },
                "required": [
                  "id",
                  "title",
                  "state"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onOpen",
              "required": true,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onStop",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Shown on a run that is still going."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "select",
      "title": "Select",
      "description": "A choice from a short, known list, with the open list drawn in the system's own popover surface instead of the platform's menu. Takes `options` (plain strings, or `{ value, label, disabled }` when what's shown differs from what's stored), `value` with `onValueChange` when the caller owns the state or `defaultValue` when it doesn't, and an optional `placeholder` shown muted while nothing is picked. Two looks: `variant=\"field\"` (the default) sits in a form beside inputs at Field height, or 32px with `size=\"sm\"`; `variant=\"pill\"` is a compact chip for toolbars and composers, with an optional visual `prefix` such as `Tone:` before the value. Keyboard opening, arrow navigation, typeahead, focus returned to the trigger and listbox semantics all come from Radix Select, and `name` adds a hidden native select so a form still submits the value. Works inside FieldControl, which passes it `id`, `aria-invalid` and `aria-describedby`. Use it for settings, corrections and filters with up to a few dozen options. Not for searching long or remote lists (use Command Menu) or for two or three options that should all stay visible.",
      "type": "registry:ui",
      "categories": [
        "forms",
        "application"
      ],
      "dependencies": [
        "@radix-ui/react-select"
      ],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/select/select.tsx",
          "target": "components/ui/select.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Option values must be non-empty strings. A `prefix` is hidden from screen readers, so name the control with `aria-label` or a label.",
      "meta": {
        "engineeringNotes": "Replaces the native selects the system used to use. A native select was the right default for input methods and phones, but its open list is an OS menu in the OS's colours and fonts, and it was the one surface the system couldn't style, which showed most in the dark theme. Radix Select keeps what native gave and draws the list itself: Space, Enter and arrows open it, arrows move, typeahead jumps, Escape and Tab close it with focus back on the trigger, and the trigger is a combobox over a listbox, so screen readers announce the label, the role and the current value. The API is data rather than composition (`options`, `value`, `onValueChange`), so a model rendering from the catalogue can drive it with plain JSON, and so both of the system's call sites, Intent Preview's corrections and Prompt Composer's pills, are one element each. The trigger sizes to its current value rather than the longest option, which is what Prompt Composer's pills had to fake with a transparent native select laid over a label. The pill's hit area stretches to 44px vertically because pills sit in a row; the field variant doesn't, because stacked fields would overlap. Options are 32px rows for a mouse and 44px on coarse pointers. With `name` set, Radix renders a hidden native select, so plain form submission keeps working.",
        "motionNotes": "Opening is the only motion, and it's brief: the list fades in and settles from 98% scale over `--pd-duration-fast` with `--pd-ease-decelerate`, growing out of the trigger because its transform origin comes from the side Radix opened it on. It opens because the person asked, so it holds still from then on: no staggered rows, no sliding highlight, and the highlight moves instantly with the keys. Closing is instant. The trigger shares the system's colour transition at `--pd-duration-instant`. Under prefers-reduced-motion the list appears with no fade or scale, and the trigger's transition is dropped.",
        "schema": {
          "component": "Select",
          "element": "button",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Select",
            "type": "object",
            "properties": {
              "options": {
                "type": "array",
                "items": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "$ref": "#/$defs/SelectOption"
                    }
                  ]
                },
                "description": "The choices: plain strings, or `{ value, label }` when what's shown differs from what's stored."
              },
              "value": {
                "type": "string",
                "description": "The selected value, when the caller owns the state."
              },
              "defaultValue": {
                "type": "string",
                "description": "The starting value, when the component owns the state."
              },
              "placeholder": {
                "type": "string",
                "description": "Shown, muted, while nothing is selected."
              },
              "variant": {
                "enum": [
                  "field",
                  "pill"
                ],
                "description": "`field` sits in a form beside inputs; `pill` is a compact chip for toolbars and composers."
              },
              "size": {
                "enum": [
                  "sm",
                  "default"
                ],
                "description": "Height of the `field` variant: `default` matches Field inputs, `sm` matches 32px controls."
              },
              "prefix": {
                "type": "string",
                "description": "Text before the value inside the trigger, such as `Tone:`. Visual only and hidden from\nscreen readers, so name the control with `aria-label` or a label."
              },
              "name": {
                "type": "string",
                "description": "Submits the value with a form under this name."
              },
              "required": {
                "type": "boolean",
                "description": "The form won't submit until something is picked."
              }
            },
            "required": [
              "options"
            ],
            "$defs": {
              "SelectOption": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string",
                    "description": "What `onValueChange` receives and `value` is matched against. Must not be empty."
                  },
                  "label": {
                    "type": "string",
                    "description": "What the list and the trigger show. Defaults to `value`."
                  },
                  "disabled": {
                    "type": "boolean",
                    "description": "Listed, but can't be picked."
                  }
                },
                "required": [
                  "value"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onValueChange",
              "required": false,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ],
              "description": "Called with the value a person picks."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "selection-actions",
      "title": "Selection Actions",
      "description": "A floating, selection-triggered toolbar for AI actions on a piece of text: select text inside a given container and a toolbar of consumer-defined actions (e.g. Explain, Rewrite, Translate, Classify, Extract) appears anchored to the selection. Takes a containerRef (scopes selection detection to one region of the page), an actions array ({id, label, icon}), and an onAction(actionId, selectedText, signal) handler returning a SelectionActionResult shaped as one of: { kind: 'replacement', value } which renders through Diff Accept for accept/reject review, { kind: 'text', value } shown as prose, { kind: 'classification', label, confidence? } shown as a label with a tiered confidence indicator, or { kind: 'list', items } shown as a bullet list. An optional onReplace(mergedValue, originalSelectedText) callback fires only when an accepted replacement is applied, letting a plain-string content model splice the result back in. Use for read or editable content surfaces where per-selection AI actions add value without a context switch to chat. Not for a whole-field or whole-document action (use smart-field or diff-accept directly for that).",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/diff-accept",
        "@pixeldosa/confidence-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/selection-actions/selection-actions.tsx",
          "target": "components/ui/selection-actions.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils` and the `diff-accept` primitive at `@/components/ui/diff-accept` (installed automatically as a registry dependency), used to render the 'replacement' result kind. Renders via a React portal to document.body with `position: fixed`, so it is not clipped by an `overflow: hidden` ancestor. Keyboard-only users generally cannot create an arbitrary text selection in static, non-editable content — a platform-level limitation this component cannot fix; the guarantee it makes is that once any selection exists, the toolbar itself is fully keyboard-operable.",
      "meta": {
        "engineeringNotes": "Composes Diff Accept for the 'replacement' result kind (Rewrite) rather than reimplementing accept/reject a second time — Rewrite is exactly the 'AI proposes replacement text, human reviews' problem Diff Accept already solves, and the two components sharing one review UI keeps the whole AI tier consistent instead of each component inventing its own version of the same interaction. Selection detection listens to document-level `selectionchange`, filtered to whether the selection's anchor node falls inside `containerRef`, debounced (default 200ms) so the toolbar appears once the selection has settled rather than jittering while the user is still dragging or extending with the keyboard — one mechanism covers both input methods, since `selectionchange` fires for either. The toolbar's root uses `onMouseDown` with `preventDefault` — the standard rich-text-editor-toolbar trick — because without it, focusing a toolbar button would collapse the browser's live selection before the click handler ever runs, since focusing an element outside the current selection normally clears it. `onReplace` deliberately takes the original selected text as a second argument rather than assuming a plain string content model on the component's own side: this component cannot know whether the consumer's content is a plain string, a contentEditable region, or a structured rich-text document, so it hands back enough context (the exact original substring) for a plain-string consumer to splice correctly via `content.replace(original, merged)`, while a richer content model would use its own document API and ignore the second argument.",
        "motionNotes": "The toolbar mounts in a closed visual state and flips to open on the next animation frame (a `visible` state lagging one rAF behind the selection existing), the same two-phase-mount pattern `Command Menu` uses, so the fade+scale-in (`--pd-duration-fast`/`--pd-ease-standard`) actually has a 'from' value to animate away from rather than popping in already-visible. No motion plays while the selection itself is still changing — the toolbar simply doesn't exist yet during that phase (enforced by the debounce), which is the correct behaviour research on floating selection toolbars confirms (centre after the selection settles, not during), achieved structurally rather than via a suppressed-animation flag. Result content (prose, a classification label, a list, or the composed Diff Accept panel) swaps in via a plain state change, never a per-character reveal, consistent with every other AI-tier component's 'the value already exists in full' reasoning. Under `prefers-reduced-motion`, the fade/scale transition drops via `motion-reduce:transition-none` (scale pinned via `motion-reduce:scale-100`), leaving the toolbar's appearance instant but still fully legible.",
        "schema": {
          "component": "SelectionActions",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "SelectionActions",
            "type": "object",
            "properties": {
              "containerRef": {
                "x-type": "RefObject<HTMLElement | null>",
                "description": "The region to watch for selections. A toolbar outside this element is ignored."
              },
              "actions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/SelectionAction"
                }
              },
              "settleMs": {
                "type": "number",
                "description": "Debounce after the selection stops changing before the toolbar appears, in ms."
              }
            },
            "required": [
              "containerRef",
              "actions"
            ],
            "$defs": {
              "SelectionAction": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "icon": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode"
                  }
                },
                "required": [
                  "id",
                  "label",
                  "icon"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onAction",
              "required": true,
              "params": [
                {
                  "name": "actionId",
                  "type": "string"
                },
                {
                  "name": "selectedText",
                  "type": "string"
                },
                {
                  "name": "signal",
                  "type": "AbortSignal"
                }
              ]
            },
            {
              "name": "onReplace",
              "required": false,
              "params": [
                {
                  "name": "mergedValue",
                  "type": "string"
                },
                {
                  "name": "originalSelectedText",
                  "type": "string"
                }
              ],
              "description": "Called only when an accepted `replacement` result is applied via Diff Accept.\nReceives the original selected text alongside the merged result so a plain-string\ncontent model can splice it back in (e.g. `content.replace(originalText,\nmergedValue)`); a rich-text/contentEditable consumer would instead use its own\ndocument model's replace-range primitive, ignoring the second argument."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "smart-field",
      "title": "Smart Field",
      "description": "An AI-proposed value for a single text field, shown with a confidence tier and its source before it becomes real data. The field's value never changes until the user explicitly accepts the proposal — clicking the trigger icon requests one via a consumer-supplied fetchProposal(currentValue, signal) function, which resolves to a SmartFieldProposal { value, confidence?: 'low' | 'medium' | 'high', provenance?: string } or null/undefined for no proposal. Once shown, the proposal renders in a panel below the field with Accept/Reject controls; accepting commits proposal.value into the field and shows a brief Undo affordance. Typing manually at any point (while loading or once a proposal is shown) cancels the in-flight request or dismisses the unaccepted proposal, since manual input always overrides an AI suggestion. Use for CRM/record enrichment, settings or profile fields, or any single structured field where AI has enough surrounding context to propose a value. Not for inline predictive continuation while typing (use ghost-input instead), multi-field or long-text diffing, or a consequential/risky action needing full review.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "forms",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/field",
        "@pixeldosa/confidence-meter"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/smart-field/smart-field.tsx",
          "target": "components/ui/smart-field.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "value/onValueChange are required — this is a controlled component only, matching ghost-input's contract. fetchProposal must resolve to the FULL proposed value (not a delta) plus optional confidence/provenance; return null/undefined for 'no proposal available' rather than throwing. triggerIcon is required with no bundled default — pass any size-4 SVG/icon element. autoPropose only fires once, on mount, and only when value is empty, so it can never silently overwrite existing data. Drops into <FieldControl> directly since id/aria-invalid/aria-describedby/ref all forward to the underlying <input>.",
      "meta": {
        "engineeringNotes": "Mirrors ghost-input's contract shape (value/onValueChange controlled-only, a fetchX behaviour injected by the consumer, an AbortSignal for cancellation) deliberately, so the AI tier reads as one consistent family rather than each component inventing its own pattern — the two differ only in what they return: a plain continuation string for ghost-input versus a structured { value, confidence, provenance } proposal here, because a complete replacement value needs that context attached and a continuation tail does not. Confidence rendering composes the shared `confidence-meter` primitive (extracted after Selection Actions needed the identical tiered indicator) rather than hand-rolling its own bars a second time — a three-tier enum (low/medium/high), not a raw number, because a fabricated percentage would claim precision the underlying model doesn't actually have, and because the system has no colour scale to spend on a red/yellow/green treatment anyway. autoPropose is opt-in and gated to empty fields only, adopted directly from CRM-enrichment research (Clay/HubSpot/Attio): confidence gates whether a proposal is allowed to overwrite existing data, so the safest default auto-behaviour is 'only ever fill a blank,' never 'silently reconsider what's already there.' Manual typing at any point cancels an in-flight fetch or dismisses a shown-but-unaccepted proposal, reusing ghost-input's staleness philosophy: a suggestion answering a question the field no longer asks is worse than no suggestion. The proposal panel animates open/closed via a CSS grid-template-rows trick (0fr to 1fr) rather than measuring and animating an explicit pixel height, which is the standard token-free way to animate a height that depends on variable content without a ResizeObserver.",
        "motionNotes": "The proposal panel's height expansion and the field's colour tint both use --pd-duration-fast/--pd-ease-standard; the trigger's busy spinner and accept/reject colour changes use --pd-duration-instant, matching Button's press-feedback timing, because those are immediate state confirmations rather than content arriving. The proposed value itself never animates in character-by-character — the full value already exists the moment it's returned, so a typewriter reveal would misrepresent it as being generated live, the same reasoning ghost-input's plan already established. Confidence Meter (composed, not reimplemented here) has no motion of its own — under prefers-reduced-motion, the panel's grid-template-rows transition and the colour transitions are dropped via motion-reduce:transition-none, leaving the underlying state (panel present/absent, tint present/absent) fully legible without animation.",
        "schema": {
          "component": "SmartField",
          "element": "input",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "SmartField",
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Current field value. SmartField is controlled — there is no uncontrolled mode."
              },
              "triggerIcon": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "Icon shown in the trigger button. No bundled icon set — supply your own, size-4 recommended."
              },
              "triggerLabel": {
                "type": "string",
                "description": "Accessible label for the trigger button."
              },
              "autoPropose": {
                "type": "boolean",
                "description": "Fetch a proposal automatically once mounted, if `value` is empty. Off by default —\nfetching without an explicit ask contradicts the AI tier's \"nothing happens\nunannounced\" posture, but some products (e.g. Notion's Custom Agent autofill) do\nwant this, so it's an opt-in rather than cut entirely."
              },
              "proposeToken": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "number"
                  }
                ],
                "description": "Changing this value requests a proposal. It exists so a parent can fill several\nfields from one action — `AI Form Fill` bumps a single token and every field it\ngoverns proposes at once — without this component giving up ownership of its own\nproposal state. Ignored on first render, so mounting is never a request; use\n`autoPropose` for that."
              },
              "undoWindowMs": {
                "type": "number",
                "description": "How long the Undo affordance stays visible after accepting, in ms."
              },
              "wrapperClassName": {
                "type": "string"
              }
            },
            "required": [
              "value",
              "triggerIcon"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onValueChange",
              "required": true,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                }
              ]
            },
            {
              "name": "fetchProposal",
              "required": true,
              "params": [
                {
                  "name": "currentValue",
                  "type": "string"
                },
                {
                  "name": "signal",
                  "type": "AbortSignal"
                }
              ],
              "description": "Fetches a proposal for the current value. Return null/undefined for \"no proposal\"."
            },
            {
              "name": "onAcceptProposal",
              "required": false,
              "params": [
                {
                  "name": "value",
                  "type": "string"
                },
                {
                  "name": "proposal",
                  "type": "SmartFieldProposal"
                }
              ],
              "description": "Fired after the user explicitly accepts a proposal."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "suggestions",
      "title": "Suggestions",
      "description": "Follow-up suggestions after an AI answer, where what a tap does is always clear. `Suggestions` takes `items` (strings, or `{ id, label, prompt? }` when the visible label differs from what gets asked), `onSelect(prompt, item)`, a `mode` of 'fill' (put the text in the composer to edit, the default) or 'send' (ask immediately), `streaming` (hides them while an answer is still arriving), and a `layout` of 'chips' (wrapping pills) or 'list' (full-width rows for longer questions). Fill and send look different, with a pencil or an arrow, and are named differently for screen readers ('Edit before asking: …' vs 'Ask: …'), so a tap never commits someone to words they didn't expect to send. The group is one tab stop, and arrow keys, Home and End move between items. Items arrive with a short staggered fade and then hold still. Use it under assistant messages and in empty states. Not for autocomplete while typing. Use Ghost Input for that.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "actions"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/suggestions/suggestions.tsx",
          "target": "components/ui/suggestions.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Default to `mode=\"fill\"`. Use `send` only when the suggestions are exactly what the user would type. Pass `streaming` from the message status.",
      "meta": {
        "engineeringNotes": "Most suggestion chips hide their most important property: whether tapping one sends a message. Sending words the user didn't write can't be taken back in a conversation, so the component makes mode explicit in three places: the trailing glyph (pencil for fill, arrow for send), the accessible name ('Edit before asking:' vs 'Ask:'), and the default, which is the safer `fill`. Items accept a separate `prompt`, so a short visible label can expand into a fuller question. Suggestions render nothing while `streaming`, because a follow-up to an unfinished answer is premature and often wrong once the answer completes. Keyboard behaviour follows the toolbar pattern already used in AI Action Toolbar: one tab stop with roving tabindex, arrows in both axes (so it works for wrapping chips and stacked lists), and Home and End. The active index resets if the list shrinks. Chips extend their hit area vertically with a transparent pseudo-element, and list rows are 44px tall.",
        "motionNotes": "One entrance, then stillness, because choosing a follow-up is the user's turn. Items fade in with the shared `pd-fade-in` at `--pd-duration-base` on the decelerate curve, staggered 40ms per item so the row reads left to right rather than popping in as a block. There's no slide or scale, so text is readable from the first frame. Hover colour runs at `--pd-duration-instant`. Nothing shimmers or pulses. Under prefers-reduced-motion the entrance is removed and items appear immediately.",
        "schema": {
          "component": "Suggestions",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "Suggestions",
            "type": "object",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        },
                        "prompt": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "id",
                        "label"
                      ]
                    }
                  ]
                }
              },
              "mode": {
                "enum": [
                  "fill",
                  "send"
                ],
                "description": "`fill` puts the text in the composer to edit; `send` sends it immediately. Each looks\ndifferent, so a tap never does something the user didn't expect."
              },
              "streaming": {
                "type": "boolean",
                "description": "Hides the suggestions: follow-ups for an answer that hasn't finished are premature."
              },
              "layout": {
                "enum": [
                  "list",
                  "chips"
                ]
              },
              "label": {
                "type": "string"
              }
            },
            "required": [
              "items"
            ]
          },
          "events": [
            {
              "name": "onSelect",
              "required": true,
              "params": [
                {
                  "name": "prompt",
                  "type": "string"
                },
                {
                  "name": "item",
                  "type": "Suggestion"
                }
              ],
              "description": "Receives the suggestion's prompt (or label) and the item."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "thinking-experience",
      "title": "Thinking Experience",
      "description": "A complete agent run, orchestrated — the first Level 2 Block in this system, and the composed surface the AI-tier components exist to be assembled into. Takes one `state` from the agent vocabulary plus a `status` line, optional `reasoning` steps, an optional `ask` request, an optional `approval` request, and a terminal `result` with its action strip, then decides which of those surfaces belongs on screen at each moment. The value over rendering the same components yourself is entirely the orchestration: a consumer wiring Live Status Line, Reasoning Stream, Agent Ask and AI Approval Gate together has to re-derive when each appears, when reasoning should stop streaming, when the elapsed counter should freeze, and what happens to the machine's own surfaces while a human is being asked something — and that last question is the one most implementations get wrong. This block applies the turn-taking grammar at the level of the whole panel rather than a single indicator: when control passes back to the user, the machine's status and reasoning visibly step down in emphasis and whatever needs an answer takes a ring, while remaining fully interactive so a run can always be stopped even while it waits on you. Composes Live Status Line, Reasoning Stream, Agent Ask, AI Approval Gate and AI Action Toolbar, which transitively pulls in Agent Presence, Confidence Meter and AI Context Surface. Use it as the agent panel in a product — a sidebar, a drawer, an inline run surface. Not a chat thread and not a transcript: it shows one run in progress, not a history of turns.",
      "type": "registry:block",
      "categories": [
        "ai-assisted",
        "application",
        "block"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme",
        "@pixeldosa/live-status-line",
        "@pixeldosa/reasoning-stream",
        "@pixeldosa/agent-ask",
        "@pixeldosa/ai-approval-gate",
        "@pixeldosa/ai-action-toolbar",
        "@pixeldosa/agent-figure"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/thinking-experience/thinking-experience.tsx",
          "target": "components/blocks/thinking-experience.tsx",
          "type": "registry:block"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Installing this pulls in the whole AI-tier stack it composes — Live Status Line, Reasoning Stream, Agent Ask, AI Approval Gate and AI Action Toolbar, plus Agent Presence, Confidence Meter and AI Context Surface transitively. Drive it from a single agent state; passing `ask` and `approval` alongside is harmless, since only the one matching the current state renders.",
      "meta": {
        "engineeringNotes": "The first Block in this system, and it is deliberately an orchestration rather than new UI: every visible element comes from an already-shipped component. What it contributes is the set of decisions a consumer would otherwise have to re-derive — which surface belongs on screen at which state, that reasoning stops streaming the moment control passes to a human rather than continuing to tick behind a question, that the elapsed counter freezes on terminal states instead of counting forever, and that a pending question or approval is what the eye should land on. The interesting decision is how the turn-taking grammar scales: at component level it is one indicator moving or holding still, but at panel level it becomes relative emphasis, so the machine's status and reasoning step down (indicator graphics fade to 40% and primary text drops to the muted colour, keeping text above 4.5:1 contrast rather than dimming it with opacity) while an interrupt takes a ring. They are dimmed but deliberately never disabled — a run must remain stoppable even while it is waiting on the person, and `pointer-events-none` on that region would have silently removed the Stop control at exactly the moment someone might want it. Emphasis is carried by a ring rather than by animation because this is the user's turn and stillness is the signal; animating the thing that wants attention would contradict the grammar the rest of the system holds. Ask and approval are passed as plain data objects rather than as slots so the block owns the decision of when each appears — a slot-based API would hand the orchestration question straight back to the consumer, which is the one thing this component exists to answer.",
        "motionNotes": "Two transitions, both mapped to a real state change. The machine region cross-fades its text colour and indicator opacity over `--pd-duration-base` with `--pd-ease-standard` as control passes between the agent and the user, which is the panel-level expression of the turn-taking grammar and the only motion that belongs to this block itself. The result fades in once over `--pd-duration-base` with `--pd-ease-decelerate` when the run reaches a terminal state. Everything else is inherited: Live Status Line's status swap, Agent Presence's rotation, Reasoning Stream's step fades and disclosure, AI Action Toolbar's colour transitions and busy spinner. Nothing is duplicated at this level, which is the point of composing rather than reimplementing — one rhythm, set once in the tokens, applied consistently by every part. Under `prefers-reduced-motion` the block's own two transitions drop to instant and each child handles its own fallback, leaving the layout, the ring emphasis and the opacity difference fully legible without any movement.",
        "schema": {
          "component": "ThinkingExperience",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ThinkingExperience",
            "type": "object",
            "properties": {
              "state": {
                "enum": [
                  "done",
                  "failed",
                  "thinking",
                  "working",
                  "asking",
                  "queued",
                  "deciding",
                  "streaming",
                  "cancelled",
                  "suggesting",
                  "awaitingApproval"
                ]
              },
              "status": {
                "type": "string"
              },
              "detail": {
                "type": "string"
              },
              "startedAt": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "x-type": "Date"
                  }
                ]
              },
              "reasoning": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "reasoningMs": {
                "type": "number"
              },
              "ask": {
                "$ref": "#/$defs/AskRequest",
                "description": "Rendered when `state` is `asking`."
              },
              "approval": {
                "$ref": "#/$defs/ApprovalRequest",
                "description": "Rendered when `state` is `awaitingApproval`."
              },
              "result": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "The finished output. Rendered once the run reaches a terminal state."
              },
              "resultActions": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/AIAction"
                }
              },
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Heading level for the question or approval shown during the run. Defaults to 3."
              },
              "expression": {
                "enum": [
                  "full",
                  "subtle",
                  "off"
                ],
                "description": "How much of the agent shows. `full` draws the whole figure beside the run, for\nonboarding and consumer products; `subtle` (default) keeps only its head in the\nstatus line; `off` uses plain dots, for dense professional tools."
              }
            },
            "required": [
              "state",
              "status"
            ],
            "$defs": {
              "AskRequest": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "string"
                  },
                  "question": {
                    "type": "string"
                  },
                  "explanation": {
                    "type": "string"
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "type": {
                              "const": "text"
                            },
                            "label": {
                              "type": "string"
                            },
                            "placeholder": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "name",
                            "type",
                            "label"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "type": {
                              "const": "number"
                            },
                            "label": {
                              "type": "string"
                            },
                            "placeholder": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "name",
                            "type",
                            "label"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "type": {
                              "const": "confirm"
                            },
                            "label": {
                              "type": "string"
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "name",
                            "type",
                            "label"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "type": {
                              "const": "select"
                            },
                            "label": {
                              "type": "string"
                            },
                            "options": {
                              "type": "array",
                              "items": {
                                "$ref": "#/$defs/AskFieldOption"
                              }
                            },
                            "required": {
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "name",
                            "type",
                            "label",
                            "options"
                          ]
                        }
                      ]
                    }
                  },
                  "onRespond": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onDecline": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onCancel": {
                    "x-function": true,
                    "x-host": true
                  }
                },
                "required": [
                  "source",
                  "question",
                  "fields",
                  "onRespond"
                ]
              },
              "AskFieldOption": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "hint": {
                    "type": "string"
                  }
                },
                "required": [
                  "value",
                  "label"
                ]
              },
              "ApprovalRequest": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string"
                  },
                  "reversible": {
                    "type": "boolean"
                  },
                  "risk": {
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ]
                  },
                  "impact": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/ApprovalImpact"
                    }
                  },
                  "confidence": {
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ]
                  },
                  "provenance": {
                    "type": "string"
                  },
                  "explanation": {
                    "type": "string"
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "$ref": "#/$defs/ProvenanceSource"
                    }
                  },
                  "onApprove": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onReject": {
                    "x-function": true,
                    "x-host": true
                  },
                  "onEdit": {
                    "x-function": true,
                    "x-host": true
                  },
                  "busy": {
                    "enum": [
                      "approve",
                      "reject"
                    ]
                  }
                },
                "required": [
                  "action",
                  "reversible",
                  "onApprove",
                  "onReject"
                ]
              },
              "ApprovalImpact": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  }
                },
                "required": [
                  "label",
                  "value"
                ]
              },
              "ProvenanceSource": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "snippet": {
                    "type": "string"
                  }
                },
                "required": [
                  "label"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              },
              "AIAction": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "icon": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "Optional leading icon. No icon set is bundled — pass your own `size-4` icon."
                  },
                  "intent": {
                    "enum": [
                      "primary",
                      "secondary",
                      "quiet",
                      "destructive"
                    ],
                    "description": "Visual weight. Exactly one action should be `primary`. Defaults to `secondary`."
                  },
                  "disabled": {
                    "type": "boolean"
                  },
                  "busy": {
                    "type": "boolean",
                    "description": "Owned by the consumer, not this component: the caller already holds the request\n(and usually its AbortController), so the toolbar reflects that state rather than\nkeeping a second copy that can drift out of sync with it."
                  }
                },
                "required": [
                  "id",
                  "label"
                ]
              }
            }
          },
          "events": [
            {
              "name": "onResultAction",
              "required": false,
              "params": [
                {
                  "name": "actionId",
                  "type": "string"
                }
              ]
            },
            {
              "name": "onCancel",
              "required": false,
              "params": []
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "tool-call-card",
      "title": "Tool Call Card",
      "description": "What an agent actually did — read a file, ran a search, fetched a page, wrote a change — shown as a record beside what it says. `ToolCallCard` takes a `kind` (read, search, fetch, write, run, call), the exact `target` it touched, a `status` (running, done, failed, blocked), and optional `name`, `input`, `output`, `error` and `durationMs`. It leads with a plain sentence, verb then target, and keeps the arguments and result one click away. `effect` defaults from the kind: read-only calls stay quiet, while calls that made changes or reached outside the app carry a label, because those are what a reviewer hunts for. `ToolCallGroup` folds a run's calls into one line ('Read 2 files, searched once, wrote 1 file') with counts of calls that made changes and calls that failed, and shows the running call in place of the tally. Use it next to agent answers so retrieved knowledge is distinguishable from asserted knowledge. Not a log viewer or a debugging console.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/tool-call-card/tool-call-card.tsx",
          "target": "components/ui/tool-call-card.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Map each tool your agent exposes to a `kind`, and override `effect` for tools whose side effects the kind doesn't imply (e.g. a `run` that is read-only). Show the group next to the answer it supports, not in a separate debug panel.",
      "meta": {
        "engineeringNotes": "Built for a trust gap seen from the model side. An agent's answer blends what it retrieved with what it inferred, and the text alone reads the same either way. A visible record of the calls lets a reader tell 'the config sets a 30s timeout' backed by a read of config.ts from the same sentence with nothing behind it, and it makes failures just as visible: the demo's answer admits the docs fetch failed, and the failed card is right there. Three decisions keep this from becoming a log dump. The summary is a sentence, verb then exact target in monospace, so it scans like prose. Evidence (arguments and result) sits in a disclosure, capped in height with its own scroll so one large result cannot swamp the run. And effect is a first-class prop defaulted from the kind, so the handful of calls that changed something are labelled among the many that only looked. Blocked calls never ran, so they never carry an effect label. The group tallies only finished calls and reports failures separately, so the headline never counts a failed read as a read. Collapsible regions use the house 0fr→1fr grid with overflow-hidden, padding inside the min-h-0 child, and inert while closed.",
        "motionNotes": "Motion only where the machine is working. A running call's verb takes the shared `pd-shimmer`. The target never shimmers, because it is content and must stay readable. While any call runs, the group headline swaps its tally for that call, so the one moving line is always the current action. Disclosures open with a grid-template-rows transition at `--pd-duration-fast` and a chevron rotation on the same curve. Hover colour runs at `--pd-duration-instant`. Finished, failed and blocked calls hold still. Everything falls back to instant or static text under prefers-reduced-motion.",
        "schema": {
          "component": "ToolCallCard",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "ToolCallCard",
            "type": "object",
            "properties": {
              "kind": {
                "enum": [
                  "search",
                  "read",
                  "fetch",
                  "write",
                  "run",
                  "call"
                ]
              },
              "target": {
                "type": "string",
                "description": "What it touched: a file path, a query, a URL, a command."
              },
              "status": {
                "enum": [
                  "running",
                  "done",
                  "failed",
                  "blocked"
                ]
              },
              "name": {
                "type": "string",
                "description": "The raw tool name (e.g. `fs.read_file`), shown for people who want the exact call."
              },
              "effect": {
                "enum": [
                  "read-only",
                  "changes",
                  "external"
                ],
                "description": "Defaults from `kind`: read, search and fetch are read-only; write, run and call change things."
              },
              "input": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "object",
                    "additionalProperties": {}
                  }
                ],
                "description": "Arguments as sent. Objects are shown as formatted JSON."
              },
              "output": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "$ref": "#/$defs/UINode"
                  },
                  {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "$ref": "#/$defs/UINode"
                        }
                      ]
                    }
                  }
                ],
                "x-react": "ReactNode",
                "description": "What came back — the evidence."
              },
              "error": {
                "type": "string",
                "description": "Why it failed or was blocked."
              },
              "durationMs": {
                "type": "number"
              },
              "defaultOpen": {
                "type": "boolean"
              }
            },
            "required": [
              "kind",
              "target",
              "status"
            ],
            "$defs": {
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": []
        },
        "subcomponents": [
          {
            "component": "ToolCallGroup",
            "element": "div",
            "props": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "title": "ToolCallGroup",
              "type": "object",
              "properties": {
                "calls": {
                  "type": "array",
                  "items": {}
                },
                "defaultOpen": {
                  "type": "boolean"
                }
              },
              "required": [
                "calls"
              ]
            },
            "events": []
          }
        ]
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "variation-grid",
      "title": "Variation Grid",
      "description": "The results of one prompt, with the good ones kept. Takes `items` (each with your own `preview` node, a `label`, a per-cell `status` of `running`, `done` or `failed`, and a `note` explaining a failure), the `keptIds` the user is holding with `onKeptChange`, `onRegenerateRest` which replaces only what isn't kept, a `regenerateCost` shown on the button before the click, plus `onUse`, `onMore` and a per-cell `onRetry`. Keeping is the point: research on how people use generation tools describes them keeping the two results they like and running the rest again, which almost no tool supports, so people download favourites and re-prompt by hand. Failures are per cell, so one timed-out result shows its own reason and Retry while the others stay usable. Purely presentational: you render the previews and own the generation. Use it for any prompt that returns several candidates — images, video takes, copy, layouts. Not for a single result (that is Generation Placeholder or Media Result), and not as a gallery of past work.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/variation-grid/variation-grid.tsx",
          "target": "components/ui/variation-grid.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Render each `preview` yourself: an `img`, a poster frame, a card of text. Set `aspectRatio` to your output's shape so the grid never jumps.",
      "meta": {
        "engineeringNotes": "The kept set is caller-owned and passed in, because it has to survive the regeneration that replaces the other cells — holding it internally would lose the user's choices exactly when the component re-renders with new results. `onRegenerateRest` hands back the ids it is replacing rather than a bare signal, so the caller can keep positions stable and never has to diff two arrays to work out what the user meant. A cell that is still running is excluded from the replaceable set, which stops a second click from cancelling work already in flight. Per-cell status rather than a batch status comes from how these pipelines actually fail: partial success is the common case, and a grid that collapses to a single error state throws away results the user already paid for. The regeneration cost is a caller-formatted string on the button because unpriced iteration is the single most common complaint in this category.",
        "motionNotes": "Only unfinished cells move: a running cell pulses its dot while the rest hold still, so a grid mid-regeneration reads as 'these two are working, the others are yours'. Kept cells change by border weight rather than by animation, since keeping is the user's turn and a celebratory motion there would be noise. Under `prefers-reduced-motion` the pulse stops and the running state is carried by its label.",
        "schema": {
          "component": "VariationGrid",
          "element": "section",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "VariationGrid",
            "type": "object",
            "properties": {
              "headingLevel": {
                "enum": [
                  2,
                  3,
                  4,
                  5,
                  6
                ],
                "description": "Level of the title heading, so it fits the outline of the page it's placed in. Defaults to 3."
              },
              "title": {
                "type": "string"
              },
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/Variation"
                }
              },
              "keptIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The ones the user is keeping. They survive the next regeneration."
              },
              "regenerateCost": {
                "type": "string",
                "description": "Cost of that regeneration, formatted by you: \"4 credits\". Shown before the click."
              },
              "useLabel": {
                "type": "string"
              },
              "aspectRatio": {
                "type": "string",
                "description": "Aspect ratio for each cell, so the grid never jumps as results land."
              }
            },
            "required": [
              "items",
              "keptIds"
            ],
            "$defs": {
              "Variation": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "preview": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "$ref": "#/$defs/UINode"
                      },
                      {
                        "type": "array",
                        "items": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "$ref": "#/$defs/UINode"
                            }
                          ]
                        }
                      }
                    ],
                    "x-react": "ReactNode",
                    "description": "The result itself: an image, a video poster, a card of text. Yours to render."
                  },
                  "label": {
                    "type": "string",
                    "description": "\"Version 3\", \"Warmer light\"."
                  },
                  "status": {
                    "enum": [
                      "running",
                      "done",
                      "failed"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "Why this one failed, and what happened to the credits."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "UINode": {
                "type": "object",
                "description": "A nested component: a name from this catalogue, and the props to give it.",
                "required": [
                  "component"
                ],
                "properties": {
                  "component": {
                    "type": "string",
                    "description": "The component's exported name, e.g. \"MediaResult\"."
                  },
                  "props": {
                    "type": "object",
                    "description": "That component's own props, matching its entry in this catalogue."
                  }
                }
              }
            }
          },
          "events": [
            {
              "name": "onKeptChange",
              "required": true,
              "params": [
                {
                  "name": "ids",
                  "type": "string[]"
                }
              ]
            },
            {
              "name": "onRegenerateRest",
              "required": false,
              "params": [
                {
                  "name": "idsToReplace",
                  "type": "string[]"
                }
              ],
              "description": "Regenerates only what isn't kept — the thing people do by hand today."
            },
            {
              "name": "onUse",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Takes one result forward: into the document, the post, the canvas."
            },
            {
              "name": "onMore",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "More like this one."
            },
            {
              "name": "onRetry",
              "required": false,
              "params": [
                {
                  "name": "id",
                  "type": "string"
                }
              ],
              "description": "Retries a single failed result."
            }
          ]
        }
      }
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "workflow-stepper",
      "title": "Workflow Stepper",
      "description": "A compact sequential progress indicator for multi-step work. Renders ordered `steps` with a title, optional description, optional badge, and a computed state of `complete`, `current`, or `upcoming`; the caller can provide an explicit `currentStep` or let the component infer it from the first step marked current. Use it to orient a person inside an approval flow, onboarding flow, setup sequence, or an agent run without turning the whole page into a tutorial. Not a timeline, not a chat transcript, and not a wizard with nested forms; it is the lightweight state layer that tells people where they are and what is next.",
      "type": "registry:ui",
      "categories": [
        "ai-assisted",
        "content",
        "application"
      ],
      "dependencies": [],
      "registryDependencies": [
        "@pixeldosa/pixeldosa-theme"
      ],
      "files": [
        {
          "path": "../../packages/ui/src/registry/workflow-stepper/workflow-stepper.tsx",
          "target": "components/ui/workflow-stepper.tsx",
          "type": "registry:ui"
        }
      ],
      "docs": "Requires `cn` at `@/lib/utils`. Keep each step concise: a short title, optional explanation, and a state that the user can understand in one glance.",
      "meta": {
        "engineeringNotes": "The component resolves state from the caller's `currentStep` or from the first step marked `current`, then derives the visible badge and connector colours rather than storing duplicated state. That keeps the API small and lets the page remain the source of truth for a live flow while the stepper stays presentational and easy to compose in an approval, onboarding, or agent-run layout. The alternative was a full state machine in the component itself, which would create stale logic when a step was already derived elsewhere in the app.",
        "motionNotes": "The stepper uses a token-driven instant colour transition and the standard ring treatment for the current step; there is no large animation because the job is orientation, not spectacle. Reduced motion keeps the transition off while preserving the state change through colour and ring contrast, so the user still understands which step is active without any movement.",
        "schema": {
          "component": "WorkflowStepper",
          "element": "div",
          "props": {
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "title": "WorkflowStepper",
            "type": "object",
            "properties": {
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/WorkflowStep"
                },
                "description": "Ordered steps, from first to last."
              },
              "currentStep": {
                "type": "number",
                "description": "The 0-based index of the current step. Defaults to the first step marked `current`."
              },
              "title": {
                "type": "string",
                "description": "Displayed above the step list."
              },
              "showStatus": {
                "type": "boolean",
                "description": "Show each step's computed state as a badge."
              }
            },
            "required": [
              "steps"
            ],
            "$defs": {
              "WorkflowStep": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "status": {
                    "enum": [
                      "complete",
                      "current",
                      "upcoming"
                    ]
                  },
                  "badge": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "title"
                ]
              }
            }
          },
          "events": []
        }
      }
    }
  ]
}
