# pixilui > Designed product UI for AI apps: approval cards, run timelines, token meters and the controls around them. Source you own, installed with the shadcn CLI. Install any item with the shadcn CLI after adding the registry to components.json: `"registries": { "@pixilui": "https://pixilui.com/r/{name}.json" }`, then `npx shadcn@latest add @pixilui/`. Items land in `components/pixilui/` and depend on `@pixilui/foundation` (tokens, motion presets, cn). ## Foundation (`foundation`, free) Tokens, motion presets and cn: the colour roles, radii, type scale, shadows and springs every pixilui item reads. Install: `npx shadcn@latest add @pixilui/foundation`. Import: `import { Foundation } from "@/components/pixilui/foundation"`. Use when: Installed once per app, automatically, by the first pixilui item you add. Not when: Do not copy values out of it into components; read the utilities (bg-surface, rounded-control, duration-fast). Notes: Use semantic utilities only: bg-surface, bg-surface-muted, text-foreground, text-secondary, text-muted, border-border, hairline, rounded-control, rounded-panel, h-control-md, text-14, shadow-raised. The accent fill is bg-accent-solid with text-accent-contrast. bg-accent stays shadcn's hover grey. Hover styles use hover-fine:, focus uses focus-visible:focus-ring. Theme with data-theme="dark" (or .dark) and data-accent="violet|blue|green|amber|orange|coral|rose|neutral". ## Approval card (`approval-card`, free) An agent asks before it acts: what it will do, the rows that matter, the risk, and approve or reject with a note. Install: `npx shadcn@latest add @pixilui/approval-card`. Import: `import { ApprovalCard } from "@/components/pixilui/approval-card"`. Use when: An agent is about to do something with a cost or a blast radius: send, pay, delete, deploy, email customers. A human has to sign off inside a run timeline or an inbox of agent requests. Not when: A yes or no with no consequence: use a button. A plan with many steps to review: use agent-plan. Confirming your own destructive action: use hold-to-confirm. Props: title: string; items?: ApprovalCardItem[]; risk?: "low" | "medium" | "high"; status?: "pending" | "approved" | "rejected"; defaultStatus?: "pending" | "approved" | "rejected"; source?: ReactNode; icon?: ReactNode; onApprove?: () => void | Promise; onReject?: (note: string) => void | Promise; receipt?: ReactNode; receiptIcon?: ReactNode; labels?: Partial Notes: Return a promise from onApprove and onReject: the card shows pending, recovers on a thrown error, and folds when it resolves. Name the action in labels.approve ("Approve and send"); keep the title one sentence in the agent's words. Put amounts, counts and IDs in items with numeric: true so they set in tabular figures. Pass status to control it from your store; leave it out and the card tracks its own decision. Pass the agent's mark as icon; the component never imports an icon library. ## Button (`button`, free) A button whose label can change without a jump: the width springs to the new words while they swap in place. Install: `npx shadcn@latest add @pixilui/button`. Import: `import { Button } from "@/components/pixilui/button"`. Use when: Any action someone triggers: submit, send, deploy, approve. Actions whose label follows the work (Send, Sending, Sent) so the button tells the story without a toast. Not when: Navigation to another page: use a link styled as you need. A destructive action on its own: pair the danger variant with a confirm step or hold-to-confirm. Props: aria-label?: string; variant?: "primary" | "secondary" | "ghost" | "danger"; size?: "sm" | "md" | "lg"; loading?: boolean; icon?: ReactNode; iconEnd?: ReactNode; children?: ReactNode | undefined Notes: Change the children to show progress: {pending ? "Sending" : "Send invoice"} with loading={pending}. The width morph handles the rest. Pass icons as props (icon, iconEnd); never import an icon library inside the component. Use variant="primary" once per surface; secondary or ghost for the rest. Do not use the disabled prop for in-flight work; use loading so focus survives. ## Dialog (`dialog`, free) A modal panel for one decision or one short task: a title, what happens, and the buttons that do it, with pending and error handled. Install: `npx shadcn@latest add @pixilui/dialog`. Import: `import { Dialog } from "@/components/pixilui/dialog"`. Use when: A decision has to be made before the person goes on: connect a tool, delete an agent, discard a run. A short task that needs focus and fits on one screen: rename, invite, set a budget. Not when: Details the person reads while working on the page: use a drawer. A few options anchored to a control: use a popover. An agent asking a human to sign off inside a run: use approval-card, which stays in the timeline. Props: children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; trigger?: ReactElement; size?: "sm" | "md" | "lg"; variant?: "default" | "danger"; dismissible?: boolean; icon?: ReactNode; container?: DialogContainer; modal?: boolean | "trap-focus" Notes: Pass the opener as trigger (any element; a Button is usual). For a dialog opened from code, leave trigger out and control open and onOpenChange. Compose the parts: DialogTitle, DialogDescription, your content, then DialogActions with confirm and onConfirm. Return a promise from onConfirm: the button shows pending, Esc and outside presses wait, and the dialog closes when it resolves or shows an error when it throws. Name the action in confirm ("Delete agent", "Connect Linear"), never "OK" or "Continue". Use variant danger for anything destructive; it styles the action with the danger tokens, puts it second and focuses Cancel. Pass container to keep the dialog inside a region of the page; it then traps focus without locking page scroll. ## Drawer (`drawer`, free) A sheet that slides in from the right, left or bottom for details read alongside the page: a run, a trace, a record, a settings panel. Install: `npx shadcn@latest add @pixilui/drawer`. Import: `import { Drawer } from "@/components/pixilui/drawer"`. Use when: Details of one row the person opens and closes while scanning a list: a run, a tool call, a customer. A form too long for a dialog that still belongs to the current page: agent settings, a schedule. On phones, actions and filters from the bottom edge where a thumb reaches them. Not when: A single decision: use a dialog. A few options anchored to a control: use a popover. Navigation that stays open: use a sidebar in the layout, not an overlay. Props: children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; trigger?: ReactElement; side?: "right" | "left" | "bottom"; size?: "sm" | "md" | "lg"; dismissible?: boolean; container?: DialogContainer; modal?: boolean | "trap-focus" Notes: Same API as dialog: trigger, open, defaultOpen, onOpenChange, dismissible, container. Compose DrawerTitle, DrawerDescription, your content and DrawerActions; they are the dialog parts under drawer names. Use side bottom for mobile action sheets and side right for detail panels on desktop. Return a promise from DrawerActions onConfirm to get pending and error handling for free. ## Input (`input`, free) A text field with its label, help, error, prefix and suffix slots and a clear button, wired for screen readers. Install: `npx shadcn@latest add @pixilui/input`. Import: `import { Input } from "@/components/pixilui/input"`. Use when: Single-line text: names, URLs, amounts, API keys, search inside a panel. Fields that need a unit or scheme beside the value (USD, https://, a shortcut hint). Not when: Several lines of text: use textarea. Numbers someone nudges up and down: use number-field. Picking from a known list: use select or combobox. Props: className?: string; label?: ReactNode; description?: ReactNode; error?: ReactNode; prefix?: ReactNode; suffix?: ReactNode; size?: "sm" | "md" | "lg"; clearable?: boolean; onClear?: () => void; clearLabel?: string; clearIcon?: ReactNode Notes: Native attributes (name, type, required, autoComplete) and the ref go to the input element; className goes to the wrapper. Pass error as a string after validation; do not also change the label colour. Use prefix and suffix for scheme, currency and units instead of putting them in the placeholder. ## Popover (`popover`, free) A small panel anchored to a control for a closer look or a quick change: a run's budget, a model's settings, who can see a thread. Install: `npx shadcn@latest add @pixilui/popover`. Import: `import { Popover } from "@/components/pixilui/popover"`. Use when: Details or a couple of controls that belong to one button: a budget, filters, sharing. Context the person checks and dismisses without leaving the page. Not when: A text label for an icon button: use a tooltip. A list of commands: use a menu. A decision that has to be made before going on: use a dialog. Props: children?: ReactNode; trigger: ReactElement; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; side?: "top" | "bottom" | "left" | "right"; align?: "start" | "center" | "end"; offset?: number; arrow?: boolean; hoverOpen?: boolean; container?: HTMLElement | null Notes: Pass the anchor as trigger; the popover opens from it and returns focus to it. Start content with PopoverTitle so the panel has an accessible name. Use PopoverClose for the action that finishes the job ("Raise to $20"); it closes and restores focus. Pass side and align for placement; it still flips when it would leave the screen. ## Run timeline (`run-timeline`, free) The steps of one agent run as a vertical timeline: a connector that fills as steps finish, a live timer on the active step, and a waiting step that holds an approval. Install: `npx shadcn@latest add @pixilui/run-timeline`. Import: `import { RunTimeline } from "@/components/pixilui/run-timeline"`. Use when: Showing an agent run in progress, step by step, with the evidence of what each step did. A run that pauses for a person: the waiting step hosts an approval card in place. A run history view where each step can open details elsewhere (onStepSelect). Not when: A flat list of tool calls inside one message: use tool-call-row with ToolCallRowGroup. A quiet status line while a model thinks: use thinking-state. A form wizard or onboarding progress: use a stepper. Props: steps: RunTimelineStep[]; compact?: boolean; onStepSelect?: (id: string) => void; selectedId?: string; labels?: Partial Notes: Keep one source of truth: map your run events to steps with id, title, status, startedAt and endedAt, and let the component animate the changes. Write detail as evidence in past tense once a step is done ("9 refunds pending for more than 72 hours"); on error, say what failed and what to do. Put an approval-card in the children of the waiting step; it stays visible in compact mode. Keep it there after approval so its receipt shows what was approved. Use onStepSelect with selectedId to open step details in a side pane. Statuses are pending, active, done, error and waiting; keep at most one active. ## Streaming text (`streaming-text`, free) Renders a model's answer as it streams: only new words rise into place at reading speed, a caret holds the end, and half-arrived words wait until they are whole. Install: `npx shadcn@latest add @pixilui/streaming-text`. Import: `import { StreamingText } from "@/components/pixilui/streaming-text"`. Use when: Showing a model's reply token by token in a chat, a summary panel or an inline suggestion. Any text that arrives in chunks of uneven size and should read as one calm stream. Not when: Markdown with headings, lists and code blocks: render it with your markdown renderer and stream into that. Static copy that is already complete: render it as text. Status while the model has not started answering: use thinking-state. Props: text: string; streaming?: boolean; caret?: boolean; speed?: number; pauseOnHover?: boolean; inlineCode?: boolean; onRevealComplete?: () => void Notes: Pass the whole text received so far on every chunk, not the delta. If the text changes in the middle (a regenerate), the component rewinds to the shared prefix. Set streaming to true while chunks arrive and false when the stream closes; the caret drops once the last word is shown. Text between backticks renders as inline code; turn it off with inlineCode={false}. Use pauseOnHover on long answers so a reader can stop the text under their mouse. onRevealComplete fires once the last word is on screen, useful for showing follow-up actions after the answer, not before. ## Thinking state (`thinking-state`, free) A quiet line that says what the agent is doing while it works, with a live timer, steps that unfold, and a fold into "Thought for 12s" when it is done. Install: `npx shadcn@latest add @pixilui/thinking-state`. Import: `import { ThinkingState } from "@/components/pixilui/thinking-state"`. Use when: A model or agent is working before it answers and the person needs to know it is alive and what it is on. You want to show the reasoning steps on demand without pushing them into the answer. Not when: A run with tool calls, durations and approvals to audit: use run-timeline. A single tool call: use tool-call-row. Page or data loading with no agent behind it: use a skeleton. Props: label?: string; steps?: ThinkingStateStep[]; startedAt?: number; elapsedMs?: number; expanded?: boolean; defaultExpanded?: boolean; onExpandedChange?: (expanded: boolean) => void; done?: boolean; doneLabel?: string | ((elapsedMs: number | null) => string); showTimer?: boolean; icon?: ReactNode Notes: Pass startedAt when the run begins and done when the first answer token arrives; the timer freezes and the line folds itself. For a run rendered later from history, pass elapsedMs with done so the line reads the real duration. Keep label to what the agent is doing in plain words ("Looking into the checkout errors"); put the steps in steps, newest last, with one active. Pass the agent's mark as icon. Never a sparkle; the component bakes in no icon. doneLabel can be a function of elapsed milliseconds for translation: (ms) => `Pensó durante ${Math.round(ms / 1000)} s`. ## Tool call row (`tool-call-row`, free) One line per tool call: what it did in plain words, the tool's name, a live timer, and a fold-out with the input and output as copyable JSON. ToolCallRowGroup sums several calls into one line. Install: `npx shadcn@latest add @pixilui/tool-call-row`. Import: `import { ToolCallRow } from "@/components/pixilui/tool-call-row"`. Use when: Showing the tools an agent called inside a chat message or a run, with evidence a person can open. Debug and audit views where people copy a call's arguments or result. Not when: The steps of a whole run with approvals: use run-timeline (it can hold tool rows as children). A long log of hundreds of calls: use a virtualised table. Status while a model is thinking with no tools involved: use thinking-state. Props: name: string; summary?: ReactNode; status: "running" | "done" | "error"; icon?: ReactNode; durationMs?: number; startedAt?: number; input?: ToolCallRowValue; output?: ToolCallRowValue; error?: ReactNode; expanded?: boolean; defaultExpanded?: boolean; onExpandedChange?: (expanded: boolean) => void; onRetry?: () => void | Promise; labels?: Partial Notes: Write summary in past tense with a number once done ("Searched 14 tickets") and in the present while running ("Searching tickets"). Pass input and output as plain JSON values or strings (SQL, shell commands); objects are pretty-printed with keys a step quieter. Pass startedAt while running for a live timer and durationMs once done. Wrap rows in ToolCallRowGroup to sum them; it reads each row's status, duration and icon, so keep the rows as direct children with stable keys. Return a promise from onRetry and the button shows its own pending state; update status to running from your store when the retry starts. Pass the tool's logo as icon; the component never imports an icon library. ## Tooltip (`tooltip`, free) A short text label for an icon button, with its keyboard shortcut, that waits 400ms and then opens neighbours instantly while you scan a toolbar. Install: `npx shadcn@latest add @pixilui/tooltip`. Import: `import { Tooltip } from "@/components/pixilui/tooltip"`. Use when: Naming icon buttons in a composer, a toolbar or a table row: Attach files, Dictate, Retry run. Teaching the keyboard shortcut for an action people repeat. Not when: Anything a person must read to proceed: write it on the page, because touch screens never show tooltips. Rich content, links or buttons: use a popover. Validation or status: use the field's error or a toast. Props: children: ReactElement<{ "aria-describedby"?: string; "aria-label"?: string }>; label: string; shortcut?: string; side?: "top" | "bottom" | "left" | "right"; align?: "start" | "center" | "end"; offset?: number; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; container?: HTMLElement | null Notes: Wrap the trigger: