{
  "name": "arc",
  "homepage": "https://uiarc.dev",
  "generatedAt": "2026-11-08T14:02:51.252Z",
  "description": "Arc is a React component and block library designed for AI-assisted frontend development. It provides copy-paste components, complete interface blocks, machine-readable registry metadata, AI skills, and MCP compatibility.",
  "install": "npx shadcn@latest add https://uiarc.dev/r/<name>.json",
  "foundation": "https://uiarc.dev/r/arc-foundation.json",
  "skills": "https://uiarc.dev/r/skills/index.json",
  "mcp": "https://uiarc.dev/api/mcp",
  "items": [
    {
      "name": "button",
      "kind": "component",
      "title": "Button",
      "description": "A clear, responsive action with quiet secondary states.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "control"
      ],
      "docs": "https://uiarc.dev/components/button",
      "markdown": "https://uiarc.dev/components/button/markdown",
      "registry": "https://uiarc.dev/r/button.json",
      "keywords": [
        "react button",
        "animated button",
        "loading button",
        "button with spinner",
        "button press animation",
        "morphing button label",
        "primary button"
      ],
      "whenToUse": [
        "Any single action on a page, form, or dialog, such as Save, Continue, or Cancel.",
        "Actions whose label changes in place, like Save to Saved, where the width should spring instead of jump.",
        "Short async work where a spinner on the button is enough feedback, via loading."
      ],
      "whenNotToUse": [
        "Use action-button when the button itself should show pending and success states after an async commit.",
        "Use split-button when one default action has two to five close variants.",
        "Use hold-to-confirm for destructive actions that need more than a single click."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders a native button, so role and focus come for free.",
        "Loading uses aria-busy and aria-disabled instead of disabled, so keyboard focus is not lost mid-action.",
        "Icon-only buttons need an aria-label.",
        "Disabled primary buttons fade as a whole. Disabled secondary and danger buttons keep their border and take a muted fill and label, and disabled ghost buttons take a muted label, so an unavailable action still reads as a button."
      ],
      "motion": [
        "Presses scale to about 0.97 on a snappy spring; icon-sized buttons press slightly deeper.",
        "A new label crossfades with a short blur while the width springs to fit.",
        "Reduced motion drops the press scale and swaps labels with a plain fade."
      ],
      "responsive": [
        "Size is fixed by the size prop; the button never changes layout by breakpoint, so pick lg for primary touch targets.",
        "Hover styles apply only on hover-capable fine pointers, so taps on touch screens do not stick in a hover state."
      ],
      "performance": [
        "A ResizeObserver measures the label so the width can spring; one per button is cheap, but avoid hundreds in a long list.",
        "Label changes animate opacity, transform, and a small blur only, with no layout thrash beyond the width spring."
      ],
      "aiNotes": [
        "Default choice for any single action. Use action-button for dense icon toolbars and split-button when one action has close alternatives.",
        "Pass a changing label (Save → Saved) as children to get the width morph for free.",
        "Wrap in a Radix trigger with asChild; the press scale turns off automatically for popup anchors."
      ],
      "related": [
        "action-button",
        "split-button",
        "copy-button",
        "hold-to-confirm"
      ],
      "usage": "import { Button } from \"@/components/arc/button/button\";\n\nexport function SaveBar() {\n  return (\n    <Button variant=\"primary\" loading={saving} onClick={save}>\n      Save changes\n    </Button>\n  );\n}",
      "examples": [
        {
          "title": "Label that morphs after saving",
          "code": "<Button variant=\"secondary\" loading={saving} onClick={save}>\n  {saved ? \"Saved\" : \"Save draft\"}\n</Button>"
        }
      ],
      "exports": [
        "Button"
      ]
    },
    {
      "name": "action-swap",
      "kind": "component",
      "title": "Action swap",
      "description": "Swap an action label while preserving its place and intent.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/action-swap",
      "markdown": "https://uiarc.dev/components/action-swap/markdown",
      "registry": "https://uiarc.dev/r/action-swap.json",
      "keywords": [
        "react toggle button",
        "follow button",
        "state cycling button",
        "action swap",
        "morphing button",
        "send button animation",
        "async toggle button"
      ],
      "whenToUse": [
        "One control that cycles through related states, like Follow and Following or Send and Sent.",
        "Toggles that run async work on press and should show a working phase and a failure message in place.",
        "Short confirmations that should revert on their own after a moment, via holdFor."
      ],
      "whenNotToUse": [
        "Use action-button for a single save or publish that ends in a success tick.",
        "Use button when the label never changes state.",
        "Use theme-switch for a light and dark toggle, since it reports the theme change for a page transition."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Native button whose aria-label always matches the visible label, since the letters themselves are aria-hidden.",
        "The working phase sets aria-busy and aria-disabled, so focus stays on the button.",
        "A polite role=\"status\" region announces each new label, pending label, and failure."
      ],
      "motion": [
        "Width, padding, tone, and icon move on one morph spring; only the changed run of letters rises in from a soft blur.",
        "A working state without pendingLabel folds the pill into a circle; the working shape shows for at least 450ms.",
        "Reduced motion jumps width and fold and swaps icons and letters with an instant fade."
      ],
      "responsive": [
        "Width follows the current label, so keep labels similar in length to avoid shifting neighbours on narrow rows.",
        "Hover styles apply only on hover-capable fine pointers; on touch the press scale is the only feedback."
      ],
      "performance": [
        "Each letter is its own motion span with layout position animation; keep labels short and avoid many swaps on one screen.",
        "A ResizeObserver tracks the label width for the morph spring."
      ],
      "aiNotes": [
        "Pick this for one control that cycles through related states (Follow → Following, Send → Sent → Send). Use action-button for a single save with a success tick.",
        "Keep states few and labels short; the morph reads best between similar lengths.",
        "Do the work in onAction and return its promise; control value only when the state lives elsewhere."
      ],
      "related": [
        "action-button",
        "button",
        "copy-button"
      ],
      "usage": "import { ActionSwap } from \"@/components/arc/action-swap/action-swap\";\nimport { Send, Check } from \"lucide-react\";\n\nexport function SendInvite() {\n  return (\n    <ActionSwap\n      states={[\n        { id: \"idle\", label: \"Send invite\", icon: <Send size={16} /> },\n        { id: \"sent\", label: \"Sent\", icon: <Check size={16} />, tone: \"success\", pendingLabel: \"Sending\", holdFor: 2000 },\n      ]}\n      onAction={next => (next.id === \"sent\" ? sendInvite() : undefined)}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Follow toggle",
          "code": "<ActionSwap\n  states={[\n    { id: \"follow\", label: \"Follow\", tone: \"accent\" },\n    { id: \"following\", label: \"Following\", tone: \"quiet\", pendingLabel: \"Following\" },\n  ]}\n  onAction={next => setFollowing(next.id === \"following\")}\n/>"
        }
      ],
      "exports": [
        "ActionSwap",
        "ActionSwapState"
      ]
    },
    {
      "name": "action-button",
      "kind": "component",
      "title": "Action button",
      "description": "A compact button for frequent toolbar actions.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "toolbar"
      ],
      "docs": "https://uiarc.dev/components/action-button",
      "markdown": "https://uiarc.dev/components/action-button/markdown",
      "registry": "https://uiarc.dev/r/action-button.json",
      "keywords": [
        "react action button",
        "async button",
        "submit button with success state",
        "save button animation",
        "loading to success button",
        "cta button with arrow",
        "publish button"
      ],
      "whenToUse": [
        "A single async commit such as Save, Publish, or Submit where the result should appear on the button.",
        "Call-to-action buttons where a trailing arrow invites the next step.",
        "Flows where the button should reset to idle on its own after success, via resetAfterMs."
      ],
      "whenNotToUse": [
        "Use button with loading for plain form submits that do not need a success state.",
        "Use action-swap when the control toggles between lasting states.",
        "Use hold-to-confirm when the action is destructive and needs a deliberate press."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Native button; the visible label is aria-hidden and a visually hidden copy of label names it.",
        "Pending sets aria-busy and aria-disabled instead of disabled, so keyboard focus stays through the save.",
        "A role=\"status\" region announces the pending and success labels."
      ],
      "motion": [
        "Changed letters rise from a soft blur while the width springs to fit; the arrow leaves forward and a check draws itself in.",
        "Presses scale to about 0.97 on a snappy spring.",
        "Reduced motion drops the press scale, width spring, and stroke draw, and swaps with an instant fade."
      ],
      "responsive": [
        "Width springs to fit pending and success labels, so keep them close in length inside tight toolbars.",
        "Hover shadow applies only on hover-capable fine pointers; touch gets the press scale."
      ],
      "performance": [
        "Every letter is a motion span with layout position animation plus a small blur; fine for a few buttons, not for dense tables.",
        "A ResizeObserver drives the width spring."
      ],
      "aiNotes": [
        "Use for a single async commit (save, publish, submit) where the result should show on the button itself.",
        "Prefer button with loading for plain forms, and action-swap when the control toggles between lasting states.",
        "Return the real promise from onAction; handle errors in onActionError since the button just resets."
      ],
      "related": [
        "button",
        "action-swap",
        "hold-to-confirm",
        "toast"
      ],
      "usage": "import { ActionButton } from \"@/components/arc/action-button/action-button\";\n\nexport function PublishButton() {\n  return (\n    <ActionButton\n      label=\"Publish\"\n      pendingLabel=\"Publishing\"\n      successLabel=\"Published\"\n      onAction={publish}\n      onActionError={error => toast.error(String(error))}\n    />\n  );\n}",
      "exports": [
        "ActionButton"
      ]
    },
    {
      "name": "split-button",
      "kind": "component",
      "title": "Split button",
      "description": "A primary action with a menu of nearby alternatives.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "menu"
      ],
      "docs": "https://uiarc.dev/components/split-button",
      "markdown": "https://uiarc.dev/components/split-button/markdown",
      "registry": "https://uiarc.dev/r/split-button.json",
      "keywords": [
        "react split button",
        "button with dropdown",
        "split button menu",
        "merge button",
        "dropdown button",
        "primary action with options"
      ],
      "whenToUse": [
        "One default action with a few close variants, like Merge with Squash and Rebase.",
        "Export or share actions where one format is the usual pick and others sit behind the chevron.",
        "Copy actions that swap the label to Copied in place while offering alternatives."
      ],
      "whenNotToUse": [
        "Use dropdown-menu when there is no default action and every option is equal.",
        "Use button when there are no alternatives.",
        "Use context-menu for actions tied to a piece of content rather than a toolbar."
      ],
      "dependencies": [
        "@radix-ui/react-dropdown-menu",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Two native buttons; the chevron is labelled \"<label> more actions\".",
        "The menu is a Radix DropdownMenu with menu and menuitem roles and focus management.",
        "The main label is announced through a polite live region when it changes."
      ],
      "motion": [
        "The main label and icon morph in place while its width springs; the menu half never scales, so the menu opens from a still anchor.",
        "Menu items fade in with a short stagger.",
        "Reduced motion removes the press scale, width spring, and menu transform, leaving a quick opacity fade."
      ],
      "responsive": [
        "The menu opens aligned to the end of the button with 12px collision padding, so it stays on screen near viewport edges.",
        "The secondary variant sets a 142px minimum on the main half; the pair does not collapse on narrow screens, so give it its own row on mobile."
      ],
      "performance": [
        "The main label animates per letter with a ResizeObserver-driven width spring; the menu mounts only while open through a Radix portal."
      ],
      "aiNotes": [
        "Use when one action is the default and two to five close variants exist. For a menu with no default action use dropdown-menu.",
        "Keep the menu actions as variations of the main action; put unrelated commands elsewhere.",
        "Swap label and icon (Copy page → Copied) to show the result in place."
      ],
      "related": [
        "button",
        "dropdown-menu",
        "action-button",
        "copy-button"
      ],
      "usage": "import { SplitButton } from \"@/components/arc/split-button/split-button\";\n\nexport function MergeButton() {\n  return (\n    <SplitButton\n      label=\"Merge pull request\"\n      onClick={merge}\n      actions={[\n        { label: \"Squash and merge\", onSelect: squash },\n        { label: \"Rebase and merge\", onSelect: rebase },\n        { label: \"Close pull request\", onSelect: close, destructive: true },\n      ]}\n    />\n  );\n}",
      "exports": [
        "SplitButton"
      ]
    },
    {
      "name": "button-group",
      "kind": "component",
      "title": "Button group",
      "description": "Related actions joined into one surface with hairline dividers: a hover highlight glides between segments, the pressed one answers in place, and an attached menu can close the row.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "group",
        "toolbar",
        "new"
      ],
      "docs": "https://uiarc.dev/components/button-group",
      "markdown": "https://uiarc.dev/components/button-group/markdown",
      "registry": "https://uiarc.dev/r/button-group.json",
      "keywords": [
        "button group",
        "joined buttons",
        "action group",
        "toolbar",
        "segmented buttons",
        "button bar",
        "attached buttons",
        "zoom controls",
        "more actions",
        "dropdown",
        "split",
        "actions"
      ],
      "whenToUse": [
        "A small set of related actions on the same object, shown together in a header, toolbar or card footer.",
        "Compact tool clusters such as zoom out, the current level and zoom in, or stacked map controls.",
        "A primary action row, such as Reply, Reply all and Forward on a message."
      ],
      "whenNotToUse": [
        "Use toggle-group or segmented-control when the segments are options that stay selected.",
        "Use split-button when there is one main action and a few variants of it.",
        "Use separate buttons when the actions are unrelated or one of them is much more important than the rest.",
        "Use tabs for switching between views."
      ],
      "dependencies": [
        "motion",
        "@radix-ui/react-dropdown-menu",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a div with role=\"group\" named by label. Segments are native buttons (or links with href), so they keep their own roles and activation keys.",
        "Disabled segments use aria-disabled instead of the disabled attribute, so they stay focusable and are read as dimmed; their onSelect never runs. A disabled whole group uses the disabled attribute.",
        "Icon-only segments, segments with content and collapsed segments take their label as aria-label and as a tooltip; icons are hidden from assistive technology.",
        "When a segment's label changes within a second of its press, such as \"Share\" to \"Copied\", a polite live region in the group announces the new label once; the quiet return to \"Share\" is not announced.",
        "The chevron segment is a Radix dropdown trigger named by menu.label, with aria-expanded and aria-haspopup. The menu manages focus, typeahead, Escape and outside clicks, and returns focus to the chevron. It is non-modal, so opening it never locks scroll or shifts the page.",
        "Keyboard focus moves the same highlight the pointer does; pointer focus draws no ring and keyboard focus shows Arc's shared focus ring. A mouse click focuses without the highlight, so nothing stays lit after the pointer leaves.",
        "State is never carried by color alone: confirmations change the label and icon, and disabled segments are dimmed and announced."
      ],
      "motion": [
        "One highlight lives under all segments. Moving the pointer or keyboard focus to another segment carries it there on the snappy spring; arriving from outside the group it fades in where it lands, so it only travels between segments.",
        "The hairline dividers on either side of the highlighted segment fade out, so the highlight reads as one soft shape inside the joined surface.",
        "A press dips the segment's content to 0.97 (0.9 for icon-only) and springs back, while the highlight under it deepens. The group and its neighbours never move. The chevron anchors the menu, so it answers with the deeper highlight only.",
        "On touch, the highlight appears under the finger while pressed and fades on release.",
        "Nothing in the group moves or resizes on hover, focus, press, menu open or a label change: every reserved label sits invisibly in the segment, so a confirmation crossfades inside a fixed box. The new label rises in from a soft blur while the old one lifts away.",
        "A label that was not reserved never snaps either: the segment springs to its width on the morph spring, and the highlight stays locked to it while it resizes.",
        "The highlight is measured at sub-pixel precision, so it sits exactly on its segment at rest.",
        "The menu grows from the group's edge with a short offset and scale and leaves faster than it arrives; the chevron turns over while it is open.",
        "With reduced motion, the highlight jumps between segments, the press dip and chevron turn are removed, and labels and the menu crossfade."
      ],
      "responsive": [
        "A horizontal group never exceeds its container: when it would, segments with an icon drop their label (collapseLabels), and anything still too wide scrolls sideways without a visible scrollbar. At 320px the document example collapses to icons with its names kept as tooltips and aria-labels.",
        "The vertical orientation suits narrow rails and canvas corners; segments stretch to the widest one.",
        "Segments are 34px tall at sm and 42px at md inside the border, matching the button heights. The highlight follows the pointer only for mouse and pen; touch gets it while pressed."
      ],
      "performance": [
        "The highlight is one element moved by transforms and size motion values; segments never re-render on hover beyond a state update in the group.",
        "Two ResizeObservers per group keep the highlight on its segment and decide when labels collapse; each label slot has its own observer for the width morph.",
        "The menu renders only while open."
      ],
      "aiNotes": [
        "Choose it for two to five related actions on one object, such as a document's Share, Duplicate and Archive, or zoom controls. Put rarely used and destructive actions in menu.",
        "It is an action group: every segment does something now. For choosing one or more options that stay pressed, use toggle-group or segmented-control instead.",
        "Give confirmations by changing an item's label and icon (\"Share\" to \"Copied\") and resetting it after a moment, and list every label in reserve so the row never resizes. Keep confirmation labels close in length to the resting label, since the segment takes the widest.",
        "Use iconOnly only for actions with universally understood icons (zoom, alignment, playback). The label still names the action for screen readers and the tooltip.",
        "Use the solid variant for the main actions of a view, at most once per screen; outline for everything else."
      ],
      "related": [
        "button",
        "split-button",
        "toggle-group",
        "segmented-control",
        "floating-button-group",
        "expanding-button-group"
      ],
      "usage": "import { Archive, Check, CopyPlus, Link, Pin, Trash2 } from \"lucide-react\";\nimport { ButtonGroup } from \"@/components/arc/button-group/button-group\";\n\nexport function DocumentActions({ doc }: { doc: Doc }) {\n  const [copied, setCopied] = useState(false);\n  return (\n    <ButtonGroup\n      label=\"Document actions\"\n      items={[\n        {\n          id: \"share\",\n          label: copied ? \"Copied\" : \"Share\",\n          reserve: [\"Share\", \"Copied\"],\n          icon: copied ? <Check /> : <Link />,\n          onSelect: async () => {\n            await navigator.clipboard.writeText(doc.url);\n            setCopied(true);\n            setTimeout(() => setCopied(false), 1800);\n          },\n        },\n        { id: \"duplicate\", label: \"Duplicate\", icon: <CopyPlus />, onSelect: () => duplicate(doc.id) },\n        { id: \"archive\", label: \"Archive\", icon: <Archive />, onSelect: () => archive(doc.id) },\n      ]}\n      menu={{\n        label: \"More actions\",\n        items: [\n          { id: \"pin\", label: \"Pin to sidebar\", icon: <Pin />, onSelect: () => pin(doc.id) },\n          { id: \"delete\", label: \"Delete\", icon: <Trash2 />, destructive: true, onSelect: () => remove(doc.id) },\n        ],\n      }}\n    />\n  );\n}",
      "exports": [
        "ButtonGroup"
      ]
    },
    {
      "name": "floating-button-group",
      "kind": "component",
      "title": "Floating button group",
      "description": "Separate soft buttons in a quiet tray, with one shared highlight that morphs from button to button as you move, and a pressed state that settles in place.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "group",
        "toolbar",
        "new"
      ],
      "docs": "https://uiarc.dev/components/floating-button-group",
      "markdown": "https://uiarc.dev/components/floating-button-group/markdown",
      "registry": "https://uiarc.dev/r/floating-button-group.json",
      "keywords": [
        "toolbar",
        "button group",
        "floating toolbar",
        "action bar",
        "editor toolbar",
        "selection toolbar",
        "tools rail",
        "hover highlight",
        "shared highlight",
        "icon buttons"
      ],
      "whenToUse": [
        "A floating editor or canvas toolbar with undo, redo, share and present.",
        "A selection toolbar that appears over text or objects.",
        "A vertical tools rail with zoom and insert actions."
      ],
      "whenNotToUse": [
        "Use toggle-group for formatting toggles or alignment where the choice stays pressed.",
        "Use segmented-control to switch between views.",
        "Use split-button when there is one main action with a few alternatives.",
        "Use a dropdown menu when there are more actions than fit in one row."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The tray is a toolbar with aria-label and aria-orientation, and a single tab stop that roves between buttons.",
        "Every button has an accessible name from its label, also when it shows only an icon. Icon only buttons and buttons with a shortcut get a tooltip with the label and shortcut, which screen readers hear as the description.",
        "Mode actions set aria-pressed. Disabled actions use aria-disabled, stay readable with their tooltip, and are skipped by the arrow keys.",
        "Separators have the separator role with the orientation across the toolbar.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The shared highlight is the keyboard indicator, shown only for keyboard focus so a mouse click never leaves it behind.",
        "When a label changes, such as Share to Copied, the accessible name changes with it and the outgoing label is hidden from assistive technology while it fades. Reserved label ghosts are hidden from assistive technology."
      ],
      "motion": [
        "One highlight per tray, driven by motion values so moving it never re-renders the buttons. It travels in position and size to the hovered or focused button on the morph spring, and stays put while the pointer crosses a gap or a separator.",
        "Appearing from nothing it lands in place and grows in from 94 percent on the snappy spring, instead of sliding from wherever it last was. It fades when the pointer leaves the tray.",
        "Pressing darkens the highlight and settles it to 96.5 percent in place on the snappy spring; the buttons themselves never scale, so neighbours and tooltip anchors stay still.",
        "A new label crossfades in place with a short rise and blur. With reserveLabels the button already has the widest label's width, so nothing around it moves; without it, the highlight re-measures and follows the new width.",
        "Nothing but the highlight ever moves: hover, focus, press and label changes leave the tray and every button at the same box. The highlight is measured exactly in tray coordinates, snapped to device pixels and corrected for ancestor transforms.",
        "The pointer leaving the tray is detected with a native listener, because React bubbles pointer events through portals and a tooltip would otherwise count as inside the tray.",
        "With reduced motion the highlight jumps to its button and fades briefly, and labels crossfade without travel."
      ],
      "responsive": [
        "The tray keeps its natural size and never wraps or clips a button. At narrow widths make secondary items icon only; to switch without a layout shift after hydration, render both densities and pick one with a container query, as the demo does below 360px.",
        "Touch shows the highlight under the finger while pressed and fades it on release; hover only tracks a mouse or pen.",
        "The highlight re-measures when the tray or any button resizes, so it stays aligned through font loading, label changes and container reflow."
      ],
      "performance": [
        "The highlight moves on transform and size through motion values without React renders; only hover and focus changes re-render the buttons.",
        "One ResizeObserver per tray, observing the tray and its buttons. Reserved labels are plain CSS grid ghosts, measured by layout, not by script."
      ],
      "aiNotes": [
        "Use it for actions, not selection. When one or more options stay chosen, use toggle-group or segmented-control.",
        "Group related actions with separators and keep a toolbar to about seven buttons. Make secondary actions icon only before dropping labels from the main one.",
        "Give every icon only item a specific label, such as \"Zoom to fit\", not \"Fit\".",
        "shortcut is a hint only. Bind the key yourself, scoped so it does not fight the page.",
        "Pass reserveLabels whenever an item's label changes, so the toolbar never shifts.",
        "Use variant floating when the toolbar sits over content such as a canvas or a text selection; use muted in a page or a panel."
      ],
      "related": [
        "toggle-group",
        "button-group",
        "expanding-button-group",
        "split-button",
        "segmented-control",
        "tooltip"
      ],
      "usage": "import { Link2, MessageCircle, Play, Redo2, Undo2 } from \"lucide-react\";\nimport { FloatingButtonGroup } from \"@/components/arc/floating-button-group/floating-button-group\";\n\nexport function BoardToolbar({ history, copied, copyLink, presenting, onPresent }: BoardToolbarProps) {\n  return (\n    <FloatingButtonGroup\n      label=\"Board actions\"\n      variant=\"floating\"\n      items={[\n        { id: \"undo\", label: \"Undo\", icon: <Undo2 />, iconOnly: true, shortcut: \"⌘Z\", disabled: !history.canUndo, onSelect: history.undo },\n        { id: \"redo\", label: \"Redo\", icon: <Redo2 />, iconOnly: true, shortcut: \"⇧⌘Z\", disabled: !history.canRedo, onSelect: history.redo },\n        { type: \"separator\" },\n        { id: \"comment\", label: \"Comment\", icon: <MessageCircle />, shortcut: \"C\" },\n        { id: \"share\", label: copied ? \"Copied\" : \"Share\", reserveLabels: [\"Share\", \"Copied\"], icon: <Link2 />, onSelect: copyLink },\n        { type: \"separator\" },\n        { id: \"present\", label: presenting ? \"Stop\" : \"Present\", reserveLabels: [\"Present\", \"Stop\"], icon: <Play />, pressed: presenting, onSelect: onPresent },\n      ]}\n      onAction={id => track(\"toolbar\", id)}\n    />\n  );\n}",
      "exports": [
        "FloatingButtonGroup"
      ]
    },
    {
      "name": "expanding-button-group",
      "kind": "component",
      "title": "Expanding button group",
      "description": "Icon buttons in a compact group: the one you point at or focus grows to reveal its label while its neighbours slide aside, and an action confirms in place.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "group",
        "toolbar",
        "new"
      ],
      "docs": "https://uiarc.dev/components/expanding-button-group",
      "markdown": "https://uiarc.dev/components/expanding-button-group/markdown",
      "registry": "https://uiarc.dev/r/expanding-button-group.json",
      "keywords": [
        "button group",
        "toolbar",
        "icon buttons",
        "expanding",
        "reveal label",
        "hover label",
        "action bar",
        "message actions",
        "file actions",
        "confirm in place",
        "morph",
        "pill"
      ],
      "whenToUse": [
        "A compact action bar on a message, file, or card where space is tight but labels should be one glance away.",
        "Toolbars where actions finish in place and deserve a confirmation without a toast, such as Archive or Copy link."
      ],
      "whenNotToUse": [
        "Use toggle-group or segmented-control when the buttons select a value instead of running an action.",
        "Use plain labelled buttons when there are only one or two actions and space allows; hiding labels then costs clarity.",
        "Use a dropdown menu for more than about six actions, or for actions that need descriptions."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a toolbar named by label, with one tab stop and roving arrow key focus.",
        "Each button keeps its full label as its accessible name while collapsed; the visible label and icon are hidden from assistive technology so nothing is read twice.",
        "Disabled actions use aria-disabled instead of disabled, so they stay focusable and still reveal their label, but do nothing when activated.",
        "A confirmation is announced through a polite live region, such as \"Archived\". The same word is announced again on a repeat action.",
        "A busy action (a promise from onSelect) sets aria-busy until it settles.",
        "The expansion itself is the focus indicator; pointer focus draws no ring and keyboard focus shows Arc's shared focus ring. Only keyboard focus expands, so a mouse click does not leave a label open after the pointer leaves.",
        "Confirmation is never color alone: the icon becomes a check and the label changes to a past tense word."
      ],
      "motion": [
        "Each button's width springs on the morph spring between its icon size and icon plus measured label. Neighbours slide aside in normal flow and the group's own width follows, so nothing resizes in a single frame.",
        "The label slot always reserves the longer of label and doneLabel, so the confirmation never changes the width. The server render already shows the resting action at that exact width, so hydration moves nothing.",
        "Anchor the group at its start edge (not centred) and reserve its widest width in the layout, so expansion only moves buttons inside the group.",
        "The icon keeps the same left inset in both states; the label slides out from behind it with a short fade and blur, and tucks back faster than it came.",
        "A confirmation draws a check where the icon was and lifts the old word out while the new one rises in, while the width springs between the two words.",
        "Hover only follows real pointer movement, and moving to a neighbour needs the pointer 4px inside it, so a button that slides under a resting pointer never takes over and a pointer resting on a boundary never flips between two actions.",
        "Pressing shrinks the icon slightly; the button box never scales, because its width is the animation.",
        "With reduced motion, labels and widths change at once with a quick fade and the check appears without drawing."
      ],
      "responsive": [
        "Five medium actions with one label open take about 260 to 280px, depending on the longest label. Reserve that width in narrow layouts rather than letting the group squeeze content.",
        "Touch has no hover: the first tap expands an action and the second runs it. A tap outside the group closes a label opened by touch.",
        "Buttons are 36px tall at md and 28px at sm; use md where touch matters."
      ],
      "performance": [
        "Each label is measured from invisible copies in its own slot (and again if its font or text changes) with a ResizeObserver; width is a Motion value, so React renders only when the expanded action changes.",
        "Width animation causes layout in the group only; keep the group out of large reflowing containers."
      ],
      "aiNotes": [
        "Use it for a row of three to six related actions on one object, where icons are recognisable but labels help: message, file and card actions.",
        "Give every action a short verb label and a matching lucide icon. Use doneLabel only for actions that finish at once (Archive, Snooze, Copy link); leave it out for actions that open something (Reply, Rename).",
        "Mark at most one destructive action with tone=\"danger\". It needs no confirm dialog when the surrounding UI offers Undo.",
        "One action is always expanded. Put the primary action first, or set defaultExpanded to it, so it reads as a labelled button at rest.",
        "This is an action group. For choosing one or several values use toggle-group or segmented-control.",
        "On touch the first tap reveals the label and a second tap runs it; the resting action runs on the first tap.",
        "Place it start aligned in a row that reserves its widest width, so the expansion never moves surrounding content."
      ],
      "related": [
        "button-group",
        "floating-button-group",
        "toggle-group",
        "action-button",
        "confirm-morph",
        "swipe-actions"
      ],
      "usage": "import { Archive, Clock3, Forward, Reply, Trash2 } from \"lucide-react\";\nimport { ExpandingButtonGroup } from \"@/components/arc/expanding-button-group/expanding-button-group\";\n\nexport function MessageActions({ message }: { message: Message }) {\n  return (\n    <ExpandingButtonGroup\n      label=\"Message actions\"\n      defaultExpanded=\"reply\"\n      items={[\n        { id: \"reply\", label: \"Reply\", icon: <Reply />, onSelect: () => openReply(message.id) },\n        { id: \"forward\", label: \"Forward\", icon: <Forward />, onSelect: () => openForward(message.id) },\n        { id: \"archive\", label: \"Archive\", doneLabel: \"Archived\", icon: <Archive />, onSelect: () => archive(message.id) },\n        { id: \"snooze\", label: \"Snooze\", doneLabel: \"Snoozed\", icon: <Clock3 />, onSelect: () => snooze(message.id, \"tomorrow-8am\") },\n        { id: \"delete\", label: \"Delete\", doneLabel: \"Deleted\", tone: \"danger\", icon: <Trash2 />, onSelect: () => moveToTrash(message.id) },\n      ]}\n    />\n  );\n}",
      "exports": [
        "ExpandingButtonGroup"
      ]
    },
    {
      "name": "dropdown-menu",
      "kind": "component",
      "title": "Dropdown menu",
      "description": "A focused list of actions anchored to a trigger.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "menu",
        "action"
      ],
      "docs": "https://uiarc.dev/components/dropdown-menu",
      "markdown": "https://uiarc.dev/components/dropdown-menu/markdown",
      "registry": "https://uiarc.dev/r/dropdown-menu.json",
      "keywords": [
        "react dropdown menu",
        "actions menu",
        "menu button",
        "radix dropdown menu",
        "animated dropdown",
        "more actions menu",
        "kebab menu"
      ],
      "whenToUse": [
        "A list of commands behind a labelled button, such as Rename, Duplicate, and Delete.",
        "Row or card actions where a visible trigger is clearer than right-click.",
        "Short grouped menus with separators and one destructive item at the end."
      ],
      "whenNotToUse": [
        "Use split-button when one command is the default.",
        "Use context-menu for right-click actions on content.",
        "Use user-menu for the account menu, and command-palette for searching many commands."
      ],
      "dependencies": [
        "@radix-ui/react-dropdown-menu",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Built on Radix DropdownMenu: the trigger gets aria-haspopup and aria-expanded, items get menuitem roles and typeahead.",
        "Icons and the highlight are aria-hidden; destructive items are marked by color and should say what they destroy.",
        "Disabled items are skipped by the keyboard."
      ],
      "motion": [
        "The highlight glides between items on a snappy spring for the pointer and jumps instantly for the keyboard.",
        "The menu scales in from the trigger and items fade in with a short stagger.",
        "Reduced motion removes the menu transform and highlight glide, keeping a quick opacity fade."
      ],
      "responsive": [
        "The trigger label ellipsizes within max-width 100%, so it shrinks inside narrow toolbars instead of overflowing.",
        "The menu uses Radix collision handling with 12px padding and a 12rem minimum width, flipping away from viewport edges.",
        "Hover styles apply only on fine pointers; touch taps select without a stuck highlight."
      ],
      "performance": [
        "Menu content renders in a portal only while open; items are not virtualized, so keep lists short.",
        "The menu has a translucent backdrop blur, which drops to a solid surface under prefers-reduced-transparency."
      ],
      "aiNotes": [
        "Default choice for a list of commands behind a button. Use split-button when one command is the default, and context-menu for right-click on content.",
        "Group items with separatorBefore; put destructive items last.",
        "Items are data, not children, so build the array from your actions."
      ],
      "related": [
        "split-button",
        "context-menu",
        "user-menu",
        "popover"
      ],
      "usage": "import { DropdownMenu } from \"@/components/arc/dropdown-menu/dropdown-menu\";\nimport { Pencil, Copy, Trash2 } from \"lucide-react\";\n\nexport function ProjectActions() {\n  return (\n    <DropdownMenu\n      label=\"Actions\"\n      items={[\n        { label: \"Rename\", icon: <Pencil size={15} />, onSelect: rename },\n        { label: \"Duplicate\", icon: <Copy size={15} />, onSelect: duplicate },\n        { label: \"Delete\", icon: <Trash2 size={15} />, onSelect: remove, destructive: true, separatorBefore: true },\n      ]}\n    />\n  );\n}",
      "exports": [
        "DropdownMenu"
      ]
    },
    {
      "name": "context-menu",
      "kind": "component",
      "title": "Context menu",
      "description": "Secondary actions kept close to the selected object.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "menu",
        "action"
      ],
      "docs": "https://uiarc.dev/components/context-menu",
      "markdown": "https://uiarc.dev/components/context-menu/markdown",
      "registry": "https://uiarc.dev/r/context-menu.json",
      "keywords": [
        "react context menu",
        "right click menu",
        "custom context menu",
        "file actions menu",
        "shift f10 menu",
        "animated context menu"
      ],
      "whenToUse": [
        "Secondary actions on a file, card, or row where a visible menu button would add clutter.",
        "Desktop-style right-click menus that also open from the keyboard with Shift+F10 or the ContextMenu key.",
        "Short flat lists of item actions with one destructive option."
      ],
      "whenNotToUse": [
        "Use dropdown-menu when the actions deserve a visible trigger.",
        "Use swipe-actions for touch-first lists like an inbox.",
        "Avoid it on targets that need their own click action, since a plain click on the target also opens the menu."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The target is focusable with aria-haspopup=\"menu\" and aria-expanded; the menu has role=\"menu\" and items role=\"menuitem\".",
        "Opening focuses the first enabled item; closing returns focus to the target.",
        "Items without an icon show a check when checked, but checked state is not exposed to assistive tech, so say it in the label if it matters."
      ],
      "motion": [
        "The menu grows from the pointer on a snappy spring, even when clamped away from a viewport edge; reopening fades the old one out in place.",
        "A shared highlight glides between rows for the pointer and jumps for the keyboard.",
        "Reduced motion drops the scale and glide and uses a short opacity fade."
      ],
      "responsive": [
        "The menu is clamped 8px inside the viewport and capped at the viewport width minus 16px, with a max height of 300px and scrolling beyond that.",
        "On touch, a tap on the target opens the menu under it, so it works without a right mouse button.",
        "The menu closes on any scroll or resize instead of repositioning."
      ],
      "performance": [
        "The menu mounts only while open and is positioned once from the pointer, with no scroll tracking loop.",
        "Items are plain buttons with one shared highlight element; there is no virtualization, so keep the list short."
      ],
      "aiNotes": [
        "Use for secondary actions on a content object (file, card, row) where a visible menu button would add clutter. Use dropdown-menu when the commands deserve a visible trigger.",
        "A plain click on the target also opens the menu, so do not wrap targets that need their own click action.",
        "The menu closes on scroll, resize, and outside press; keep items to a short flat list."
      ],
      "related": [
        "dropdown-menu",
        "swipe-actions",
        "popover",
        "command-palette"
      ],
      "usage": "import { ContextMenu } from \"@/components/arc/context-menu/context-menu\";\nimport { Copy, Trash2 } from \"lucide-react\";\n\nexport function FileTile({ file }: { file: File }) {\n  return (\n    <ContextMenu\n      label={`Actions for ${file.name}`}\n      items={[\n        { id: \"copy\", label: \"Copy link\", icon: <Copy size={15} />, onSelect: () => copyLink(file) },\n        { id: \"delete\", label: \"Delete file\", icon: <Trash2 size={15} />, destructive: true, onSelect: () => remove(file) },\n      ]}\n    >\n      <FilePreview file={file} />\n    </ContextMenu>\n  );\n}",
      "exports": [
        "ContextMenu"
      ]
    },
    {
      "name": "copy-button",
      "kind": "component",
      "title": "Copy button",
      "description": "Copy a value with immediate confirmation.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "action",
        "utility"
      ],
      "docs": "https://uiarc.dev/components/copy-button",
      "markdown": "https://uiarc.dev/components/copy-button/markdown",
      "registry": "https://uiarc.dev/r/copy-button.json",
      "keywords": [
        "react copy button",
        "copy to clipboard",
        "copy button animation",
        "clipboard button",
        "copy code button",
        "copied feedback"
      ],
      "whenToUse": [
        "Next to API keys, install commands, links, or code snippets.",
        "Icon-only copy controls in dense rows, with an accessible label.",
        "Any copy action that should confirm Copied or Could not copy in place."
      ],
      "whenNotToUse": [
        "Use split-button when copying has alternatives, such as Copy link and Copy as Markdown.",
        "Use code-block for full code samples, which include their own copy control."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Native button named by label, including when iconOnly.",
        "A polite role=\"status\" region announces \"<label>: Copied\" or \"<label>: Could not copy\".",
        "The label cell reserves the widest of its three states, so the layout never shifts."
      ],
      "motion": [
        "The old icon and word fade out in about 100ms while the new ones fade in over about 160ms, with no blur; the check draws its stroke in.",
        "The label changes as one word, never letter by letter, and the same crossfade plays when it resets.",
        "Reduced motion swaps icon and text with an instant fade and skips the stroke draw."
      ],
      "responsive": [
        "The label reserves the width of its widest state, so the button never shifts its neighbours when it changes.",
        "It sizes to max-content with max-width 100%, so it stays compact in narrow rows."
      ],
      "performance": [
        "Each copy is one short crossfade of the icon and the word (about 160ms) plus a quick stroke draw on the check; nothing animates per letter.",
        "Clipboard access is async and needs a secure context; failures show the Failed state rather than throwing."
      ],
      "aiNotes": [
        "Use next to any copyable value: API keys, install commands, links, code. For a copy action that also has alternatives use split-button.",
        "Named export only; there is no default export.",
        "Clipboard state and reset timing come from lib/use-copy-feedback, so copy that file along with the component."
      ],
      "related": [
        "button",
        "code-block",
        "split-button",
        "action-button"
      ],
      "usage": "import { CopyButton } from \"@/components/arc/copy-button/copy-button\";\n\nexport function ApiKeyRow({ apiKey }: { apiKey: string }) {\n  return (\n    <div className=\"row\">\n      <code>{apiKey}</code>\n      <CopyButton value={apiKey} label=\"Copy key\" iconOnly />\n    </div>\n  );\n}",
      "exports": [
        "CopyButton"
      ]
    },
    {
      "name": "drawer",
      "kind": "component",
      "title": "Drawer",
      "description": "A temporary side surface for focused work.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "overlay",
        "surface"
      ],
      "docs": "https://uiarc.dev/components/drawer",
      "markdown": "https://uiarc.dev/components/drawer/markdown",
      "registry": "https://uiarc.dev/r/drawer.json",
      "keywords": [
        "react drawer",
        "side panel",
        "slide over panel",
        "sheet component",
        "draggable drawer",
        "radix dialog drawer",
        "filters drawer"
      ],
      "whenToUse": [
        "Side panels for filters, settings, or record details that keep the page in context.",
        "Forms that are too long for a dialog but should not leave the current view.",
        "Panels from any edge, via side, with drag-to-dismiss on the header."
      ],
      "whenNotToUse": [
        "Use dialog for short decisions and confirmations.",
        "Use bottom-sheet for mobile-first sheets with snap points.",
        "Use popover for small anchored content that does not need a modal overlay."
      ],
      "dependencies": [
        "@radix-ui/react-dialog",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Radix Dialog provides role=\"dialog\", aria-modal, focus trapping, and focus return.",
        "title and description are wired to Dialog.Title and Dialog.Description, so the panel is always named.",
        "The header close button is labelled \"Close drawer\"; dragging is optional and never the only way to close."
      ],
      "motion": [
        "The panel springs in from its edge and leaves faster on a tween; a drag past a third of the panel or a quick flick closes it and keeps the release velocity.",
        "Dragging the header away from the edge rubber-bands; the overlay fades in and out.",
        "Reduced motion disables dragging and replaces the slide with a short opacity fade."
      ],
      "responsive": [
        "Left and right panels are min(30rem, 100vw minus a gutter) wide; below 40rem they grow to nearly full width with tighter padding.",
        "Top and bottom panels span the full width and cap at min(32rem, 100dvh).",
        "The header is the drag handle with touch-action none, so a touch drag moves the panel while the body still scrolls normally."
      ],
      "performance": [
        "The overlay uses a 4px backdrop blur, which can cost frames on low-end devices over busy pages.",
        "Drag runs on motion pan handlers with no React re-render per frame; the panel stays mounted only while sliding out."
      ],
      "aiNotes": [
        "Use for side panels with forms, filters, settings, or detail views that keep the page in context. Use dialog for short decisions and bottom-sheet for mobile-first sheets.",
        "Always compose Drawer as the root; under a bare Radix Dialog root the panel falls back to CSS keyframes and loses drag.",
        "Control open when the drawer must close after an async submit."
      ],
      "related": [
        "dialog",
        "bottom-sheet",
        "popover",
        "button"
      ],
      "usage": "import { Drawer, DrawerTrigger, DrawerContent, DrawerClose } from \"@/components/arc/drawer/drawer\";\nimport { Button } from \"@/components/arc/button/button\";\n\nexport function FiltersDrawer() {\n  return (\n    <Drawer>\n      <DrawerTrigger asChild><Button variant=\"secondary\">Filters</Button></DrawerTrigger>\n      <DrawerContent title=\"Filters\" description=\"Narrow the list of projects.\">\n        <FilterForm />\n        <DrawerClose asChild><Button>Apply</Button></DrawerClose>\n      </DrawerContent>\n    </Drawer>\n  );\n}",
      "exports": [
        "Drawer",
        "DrawerTrigger",
        "DrawerContent",
        "DrawerClose"
      ]
    },
    {
      "name": "theme-switch",
      "kind": "component",
      "title": "Theme switcher",
      "description": "Four smooth ways to move between light and dark appearance.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "theme",
        "appearance",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/theme-switch",
      "markdown": "https://uiarc.dev/components/theme-switch/markdown",
      "registry": "https://uiarc.dev/r/theme-switch.json",
      "keywords": [
        "react theme switch",
        "dark mode toggle",
        "light dark switch",
        "theme toggle button",
        "view transition theme",
        "sun moon toggle"
      ],
      "whenToUse": [
        "A single light and dark toggle in an app or marketing top bar.",
        "Pages that want to run their own view transition from the button's position."
      ],
      "whenNotToUse": [
        "Use user-menu when theme choice, including system, belongs inside an account menu.",
        "Use theme-switch-rise for quiet product chrome, or theme-switch-eclipse for a dramatic showcase change."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders the library Button with aria-pressed set when the theme is dark.",
        "aria-label defaults to \"Switch to dark mode\" or \"Switch to light mode\"; pass label to override.",
        "The icons are aria-hidden."
      ],
      "motion": [
        "Sun and moon trade places with a shared rotation, scale, and blur on a snappy spring; the icon also nudges in the direction of the chosen variant.",
        "A theme that arrives during hydration swaps without motion, so a dark page never spins its switch on load.",
        "Reduced motion swaps the icon with an instant fade and removes the nudge; skip the page transition too when reduced."
      ],
      "responsive": [
        "The labelled button fits a desktop top bar; pass iconOnly for a fixed 38px square on narrow headers and mobile.",
        "Hover styles apply only on hover-capable fine pointers, so touch taps do not leave a stuck hover state."
      ],
      "performance": [
        "The button itself is tiny: one icon swap on a spring, and the first theme after hydration applies without animating.",
        "The page transition is yours; a full-page view transition snapshots the whole document, so keep it short and skip it for reduced motion."
      ],
      "aiNotes": [
        "Use for a single light/dark toggle in a top bar. For light, dark, and system inside an account menu use user-menu.",
        "The component only reports the change. Apply the theme in onThemeChange, typically inside document.startViewTransition, and use trigger's rect as the transition origin.",
        "variant is passed back to onThemeChange so one handler can run the matching page transition (reveal, eclipse, split, rise)."
      ],
      "related": [
        "theme-switch-eclipse",
        "theme-switch-split",
        "theme-switch-rise",
        "user-menu",
        "button"
      ],
      "usage": "import { ThemeSwitch } from \"@/components/arc/theme-switch/theme-switch\";\nimport type { Theme } from \"@/components/arc/theme-switch/theme-switch\";\n\nexport function AppearanceToggle({ theme, setTheme }: { theme: Theme; setTheme: (next: Theme) => void }) {\n  return (\n    <ThemeSwitch\n      theme={theme}\n      iconOnly\n      onThemeChange={next => {\n        if (!document.startViewTransition) return setTheme(next);\n        document.startViewTransition(() => setTheme(next));\n      }}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Reveal from the button",
          "code": "<ThemeSwitch\n  theme={theme}\n  onThemeChange={(next, _variant, trigger) => {\n    const { left, top, width, height } = trigger.getBoundingClientRect();\n    document.documentElement.style.setProperty(\"--x\", `${left + width / 2}px`);\n    document.documentElement.style.setProperty(\"--y\", `${top + height / 2}px`);\n    document.startViewTransition(() => setTheme(next));\n  }}\n/>"
        }
      ],
      "exports": [
        "ThemeSwitch"
      ]
    },
    {
      "name": "theme-switch-eclipse",
      "kind": "component",
      "title": "Eclipse",
      "description": "The next appearance crosses the page like an eclipse.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "theme",
        "appearance",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/theme-switch-eclipse",
      "markdown": "https://uiarc.dev/components/theme-switch-eclipse/markdown",
      "registry": "https://uiarc.dev/r/theme-switch-eclipse.json",
      "keywords": [
        "react theme switch",
        "eclipse theme transition",
        "dark mode toggle animation",
        "view transition dark mode",
        "theme sweep"
      ],
      "whenToUse": [
        "Marketing or showcase pages where the theme change should feel like an event.",
        "Layouts where a directional sweep across the page reads naturally."
      ],
      "whenNotToUse": [
        "Use theme-switch-rise for everyday product chrome where a calmer change fits.",
        "Use theme-switch-split when the button sits in the center and direction should not matter."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Same button as theme-switch: aria-pressed reflects dark mode and the label names the next theme.",
        "Keep the eclipse sweep off for reduced motion and swap the theme directly."
      ],
      "motion": [
        "variant=\"eclipse\": the next appearance crosses the page like an eclipse; run it in onThemeChange with a view transition.",
        "The icon shifts 2px left in dark mode to lean into the sweep.",
        "Reduced motion removes the nudge and swaps the icon with a fade."
      ],
      "responsive": [
        "The labelled button fits a desktop top bar; pass iconOnly for a fixed 38px square on narrow headers and mobile.",
        "Hover styles apply only on hover-capable fine pointers, so touch taps do not leave a stuck hover state."
      ],
      "performance": [
        "The button itself is tiny: one icon swap on a spring, and the first theme after hydration applies without animating.",
        "The page transition is yours; a full-page view transition snapshots the whole document, so keep it short and skip it for reduced motion."
      ],
      "aiNotes": [
        "Select with variant=\"eclipse\". The prop only styles the icon nudge and is passed back to onThemeChange; the page animation is your code.",
        "Pick eclipse for a dramatic, directional change on marketing or showcase pages; rise suits product chrome better."
      ],
      "related": [
        "theme-switch",
        "theme-switch-split",
        "theme-switch-rise"
      ],
      "usage": "import { ThemeSwitch } from \"@/components/arc/theme-switch/theme-switch\";\n\n<ThemeSwitch\n  theme={theme}\n  variant=\"eclipse\"\n  onThemeChange={(next, variant, trigger) => runThemeTransition(next, variant, trigger)}\n/>",
      "exports": [
        "ThemeSwitch"
      ]
    },
    {
      "name": "theme-switch-split",
      "kind": "component",
      "title": "Split",
      "description": "The next appearance opens from a slim center seam.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "theme",
        "appearance",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/theme-switch-split",
      "markdown": "https://uiarc.dev/components/theme-switch-split/markdown",
      "registry": "https://uiarc.dev/r/theme-switch-split.json",
      "keywords": [
        "react theme switch",
        "split theme transition",
        "dark mode toggle",
        "view transition split",
        "theme toggle animation"
      ],
      "whenToUse": [
        "Centered layouts where the theme should open symmetrically from a middle seam.",
        "Pages where the switch position varies, since the split does not depend on where the button sits."
      ],
      "whenNotToUse": [
        "Use theme-switch for a reveal that grows from the button itself.",
        "Use theme-switch-rise for the calmest option in product UI."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Same button as theme-switch: aria-pressed reflects dark mode and the label names the next theme.",
        "Keep the split reveal off for reduced motion and swap the theme directly."
      ],
      "motion": [
        "variant=\"split\": the next appearance opens from a slim center seam; run it in onThemeChange with a view transition.",
        "The icon shifts 1px right in dark mode.",
        "Reduced motion removes the nudge and swaps the icon with a fade."
      ],
      "responsive": [
        "The labelled button fits a desktop top bar; pass iconOnly for a fixed 38px square on narrow headers and mobile.",
        "Hover styles apply only on hover-capable fine pointers, so touch taps do not leave a stuck hover state."
      ],
      "performance": [
        "The button itself is tiny: one icon swap on a spring, and the first theme after hydration applies without animating.",
        "The page transition is yours; a full-page view transition snapshots the whole document, so keep it short and skip it for reduced motion."
      ],
      "aiNotes": [
        "Select with variant=\"split\". The prop only styles the icon nudge and is passed back to onThemeChange; the page animation is your code.",
        "Split is symmetric and does not depend on where the button sits, so it suits centered layouts."
      ],
      "related": [
        "theme-switch",
        "theme-switch-eclipse",
        "theme-switch-rise"
      ],
      "usage": "import { ThemeSwitch } from \"@/components/arc/theme-switch/theme-switch\";\n\n<ThemeSwitch\n  theme={theme}\n  variant=\"split\"\n  onThemeChange={(next, variant, trigger) => runThemeTransition(next, variant, trigger)}\n/>",
      "exports": [
        "ThemeSwitch"
      ]
    },
    {
      "name": "theme-switch-rise",
      "kind": "component",
      "title": "Rise",
      "description": "The next appearance rises into place.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "theme",
        "appearance",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/theme-switch-rise",
      "markdown": "https://uiarc.dev/components/theme-switch-rise/markdown",
      "registry": "https://uiarc.dev/r/theme-switch-rise.json",
      "keywords": [
        "react theme switch",
        "rise theme transition",
        "dark mode toggle",
        "icon theme toggle",
        "view transition theme"
      ],
      "whenToUse": [
        "Product chrome and dashboards where the theme change should be calm.",
        "Icon-only toggles in a top bar, as the library's own header uses it."
      ],
      "whenNotToUse": [
        "Use theme-switch-eclipse for a dramatic change on a showcase page.",
        "Use user-menu when theme belongs with account settings."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Same button as theme-switch: aria-pressed reflects dark mode and the label names the next theme.",
        "Keep the rise off for reduced motion and swap the theme directly."
      ],
      "motion": [
        "variant=\"rise\": the next appearance rises into place from the bottom; run it in onThemeChange with a view transition.",
        "The icon lifts 2px in dark mode.",
        "Reduced motion removes the nudge and swaps the icon with a fade."
      ],
      "responsive": [
        "The labelled button fits a desktop top bar; pass iconOnly for a fixed 38px square on narrow headers and mobile.",
        "Hover styles apply only on hover-capable fine pointers, so touch taps do not leave a stuck hover state."
      ],
      "performance": [
        "The button itself is tiny: one icon swap on a spring, and the first theme after hydration applies without animating.",
        "The page transition is yours; a full-page view transition snapshots the whole document, so keep it short and skip it for reduced motion."
      ],
      "aiNotes": [
        "Select with variant=\"rise\". The prop only styles the icon nudge and is passed back to onThemeChange; the page animation is your code.",
        "The calmest of the four; the library's own top bar uses it with iconOnly."
      ],
      "related": [
        "theme-switch",
        "theme-switch-eclipse",
        "theme-switch-split"
      ],
      "usage": "import { ThemeSwitch } from \"@/components/arc/theme-switch/theme-switch\";\n\n<ThemeSwitch\n  theme={theme}\n  variant=\"rise\"\n  iconOnly\n  onThemeChange={(next, variant, trigger) => runThemeTransition(next, variant, trigger)}\n/>",
      "exports": [
        "ThemeSwitch"
      ]
    },
    {
      "name": "avatar",
      "kind": "component",
      "title": "Avatar",
      "description": "A compact identity marker for people and accounts.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "identity",
        "people"
      ],
      "docs": "https://uiarc.dev/components/avatar",
      "markdown": "https://uiarc.dev/components/avatar/markdown",
      "registry": "https://uiarc.dev/r/avatar.json",
      "keywords": [
        "react avatar",
        "avatar component",
        "user avatar with initials",
        "profile picture",
        "avatar with status",
        "online indicator avatar"
      ],
      "whenToUse": [
        "Showing one person next to their name, comment, or record.",
        "Presence in a header or list, via the online or offline status dot.",
        "Places where a photo may be missing and initials should stand in."
      ],
      "whenNotToUse": [
        "Use avatar-group for several people with an overflow count.",
        "Use user-menu when the avatar is the trigger for account actions."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is role=\"img\" with an aria-label of the name plus status, such as \"Maya Chen, online\".",
        "The image and initials are decorative, so the name is read once.",
        "The status dot is hidden from assistive tech; the label carries the state."
      ],
      "motion": [
        "A photo still loading fades in from a soft blur; a cached one shows at once.",
        "The status dot pops in and out on a snappy spring.",
        "Reduced motion swaps the dot instantly and drops the image fade."
      ],
      "responsive": [
        "Size is fixed by the size prop, from 28px to 88px, and never changes by breakpoint.",
        "It passes a sizes hint per size, so with next/image (through ArcProvider) phones never download a larger photo than the circle shows."
      ],
      "performance": [
        "The photo is a plain lazy img by default. Pass next/image through ArcProvider to optimize it; remote hosts then need next.config images settings.",
        "A cached photo shows at once; only a still-loading photo runs the short blur fade."
      ],
      "aiNotes": [
        "Use for a single person. For a row of people with an overflow count use avatar-group.",
        "Renders a plain img, so any host works in any React app. To use next/image or your own image component, wrap the app once in ArcProvider from @/components/arc/lib/arc-provider: <ArcProvider image={Image}>. With next/image, remote hosts must be allowed in next.config images."
      ],
      "related": [
        "avatar-group",
        "user-menu",
        "card",
        "timeline"
      ],
      "usage": "import { Avatar } from \"@/components/arc/avatar/avatar\";\n\nexport function Owner() {\n  return <Avatar name=\"Maya Chen\" src=\"/people/maya.jpg\" size=\"lg\" status=\"online\" />;\n}",
      "exports": [
        "Avatar"
      ]
    },
    {
      "name": "avatar-group",
      "kind": "component",
      "title": "Avatar group",
      "description": "Show a team or set of contributors in a small space.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "identity",
        "people"
      ],
      "docs": "https://uiarc.dev/components/avatar-group",
      "markdown": "https://uiarc.dev/components/avatar-group/markdown",
      "registry": "https://uiarc.dev/r/avatar-group.json",
      "keywords": [
        "react avatar group",
        "avatar stack",
        "overlapping avatars",
        "team members avatars",
        "avatar overflow count",
        "collaborators list"
      ],
      "whenToUse": [
        "Showing who owns or edits something in a header, card, or table cell.",
        "Collaborator stacks where people join and leave while the page is open.",
        "Long member lists that should collapse into a +N chip."
      ],
      "whenNotToUse": [
        "Use avatar for a single person.",
        "Use a list or sortable-data-table when people need to see every name and role."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders role=\"group\" with the label as its name.",
        "The overflow chip is role=\"img\" labelled like \"2 more editors\".",
        "Each avatar keeps its own name and status label."
      ],
      "motion": [
        "Joining or leaving members open and close their slot on a spring, so the stack slides instead of jumping.",
        "The overflow count rolls up when it grows and down when it shrinks.",
        "Avatars never move on hover or focus; the hovered person gets a stronger ring and a name tip. Reduced motion swaps counts with a plain fade."
      ],
      "responsive": [
        "The stack is inline-flex and does not wrap; lower max on narrow rows so it stays compact.",
        "The hover ring and name tip apply only on hover-capable fine pointers; the stack never spreads or shifts."
      ],
      "performance": [
        "Only max avatars render; the rest collapse into one overflow chip, so long member lists stay cheap.",
        "Joins and leaves animate slot width on a spring; hover only changes a ring and a tip in CSS."
      ],
      "aiNotes": [
        "Use for presence and ownership in headers, cards, and table cells. Use avatar for one person.",
        "Pass the full member list and let max handle truncation; do not slice it yourself or the overflow count is lost."
      ],
      "related": [
        "avatar",
        "card",
        "user-menu"
      ],
      "usage": "import { AvatarGroup } from \"@/components/arc/avatar-group/avatar-group\";\n\nconst team = [\n  { name: \"Maya Chen\", src: \"/people/maya.jpg\", status: \"online\" as const },\n  { name: \"Leo Park\" },\n  { name: \"Sara Ruiz\" },\n  { name: \"Tom Hale\" },\n  { name: \"Ines Ma\" },\n];\n\nexport function Collaborators() {\n  return <AvatarGroup members={team} max={3} label=\"Editors\" />;\n}",
      "exports": [
        "AvatarGroup"
      ]
    },
    {
      "name": "input",
      "kind": "component",
      "title": "Input",
      "description": "A single line field with clear labels and useful states.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "form"
      ],
      "docs": "https://uiarc.dev/components/input",
      "markdown": "https://uiarc.dev/components/input/markdown",
      "registry": "https://uiarc.dev/r/input.json",
      "keywords": [
        "react input",
        "text field",
        "animated form input",
        "input with error message",
        "form field validation",
        "labelled input",
        "input helper text"
      ],
      "whenToUse": [
        "Any single-line text value in a form, such as name, email, or URL.",
        "Fields whose helper or error copy changes as the person types, where the message should reword in place.",
        "Plain form posts, since it forwards name and every native input attribute."
      ],
      "whenNotToUse": [
        "Use textarea for multi-line text.",
        "Use password-field, search-field, or number-field when the value has that shape.",
        "Use inline-edit for a value shown as page text and edited in place."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders a native input with a real label, so focus and form behavior are native.",
        "Description and error ids are merged into aria-describedby alongside any caller value.",
        "Errors set aria-invalid and announce through role=\"alert\"; animated words are aria-hidden with a plain screen reader copy."
      ],
      "motion": [
        "Helper and error rows open their height on a smooth spring, then changed words rise in and unblur while numbers roll digit by digit.",
        "Reduced motion mounts rows at full height and swaps words with an instant fade."
      ],
      "responsive": [
        "The field fills its grid column with min-width 0, so it shrinks inside narrow layouts without overflowing.",
        "Text stays at the small size on touch, unlike password-strength, so iOS may zoom on focus if your --text-sm is below 16px.",
        "Hover border styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "One ResizeObserver per field measures the helper row for its height spring; cheap for a form, but avoid hundreds in a table.",
        "Only changed words animate, and numbers roll only the digits that changed."
      ],
      "aiNotes": [
        "Default single-line text field. Use password-field, search-field, or number-field when the value has that shape.",
        "Works controlled or uncontrolled like a native input; pass name for plain form submission.",
        "Changing the error string rewords it in place, so derive it from state instead of toggling separate messages."
      ],
      "related": [
        "textarea",
        "password-field",
        "search-field",
        "number-field"
      ],
      "usage": "import { Input } from \"@/components/arc/input/input\";\n\nexport function EmailField() {\n  const [email, setEmail] = useState(\"\");\n  return (\n    <Input\n      label=\"Email\"\n      type=\"email\"\n      value={email}\n      onChange={(event) => setEmail(event.target.value)}\n      description=\"We only use this for receipts.\"\n      error={email && !email.includes(\"@\") ? \"Enter a valid email\" : undefined}\n    />\n  );\n}",
      "exports": [
        "Input"
      ]
    },
    {
      "name": "textarea",
      "kind": "component",
      "title": "Textarea",
      "description": "A multiline field for notes, descriptions, and longer text.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "form"
      ],
      "docs": "https://uiarc.dev/components/textarea",
      "markdown": "https://uiarc.dev/components/textarea/markdown",
      "registry": "https://uiarc.dev/r/textarea.json",
      "keywords": [
        "react textarea",
        "multi-line input",
        "textarea with character count",
        "animated textarea",
        "comment field",
        "form textarea"
      ],
      "whenToUse": [
        "Free-form multi-line text like bios, comments, or feedback.",
        "Fields with a live character count, which rolls its digits in the helper row."
      ],
      "whenNotToUse": [
        "Use input for single-line values.",
        "Use inline-edit with multiline for a description edited in place on a page.",
        "Use tag-input when the text is really a list of short values."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Native textarea with a real label.",
        "Description and error ids are merged into aria-describedby; errors set aria-invalid and use role=\"alert\".",
        "Animated copy is aria-hidden and mirrored in a visually hidden plain-text span."
      ],
      "motion": [
        "Message rows open their height on a smooth spring; changed words rise in with a soft blur and counts roll only the digits that changed.",
        "Reduced motion drops the roll, blur, and height spring for instant changes."
      ],
      "responsive": [
        "It fills its column with a 110px minimum height and a vertical resize handle; it does not auto-grow with content.",
        "Hover border styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "One ResizeObserver per field measures the helper row for its height spring; cheap for a form, but avoid hundreds in a table.",
        "Recomputing a character count on each keystroke only re-renders the helper row words that changed."
      ],
      "aiNotes": [
        "Use for free-form, multi-line text. Use input for single lines and inline-edit for text edited in place on a page.",
        "A live character count in description gets the rolling-digit treatment for free."
      ],
      "related": [
        "input",
        "inline-edit",
        "tag-input"
      ],
      "usage": "import { Textarea } from \"@/components/arc/textarea/textarea\";\n\nexport function BioField() {\n  const [bio, setBio] = useState(\"\");\n  return (\n    <Textarea\n      label=\"Bio\"\n      rows={4}\n      value={bio}\n      onChange={(event) => setBio(event.target.value)}\n      description={`${280 - bio.length} characters left`}\n    />\n  );\n}",
      "exports": [
        "Textarea"
      ]
    },
    {
      "name": "select",
      "kind": "component",
      "title": "Select",
      "description": "A compact choice field with a keyboard friendly menu.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "form"
      ],
      "docs": "https://uiarc.dev/components/select",
      "markdown": "https://uiarc.dev/components/select/markdown",
      "registry": "https://uiarc.dev/r/select.json",
      "keywords": [
        "react select",
        "dropdown select",
        "radix select",
        "animated select",
        "select menu",
        "form select field"
      ],
      "whenToUse": [
        "A short fixed list where typing is not needed, such as region or sort order.",
        "Form fields that should submit natively through Radix's hidden select via name."
      ],
      "whenNotToUse": [
        "Use combobox for long or searchable lists.",
        "Use multi-select when several values can be chosen.",
        "Use segmented-control for two to four choices that should stay visible."
      ],
      "dependencies": [
        "@radix-ui/react-select",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Built on Radix Select, so the trigger, listbox, and options get the right roles and focus handling.",
        "The real value renders in a visually hidden Radix Value; the animated copy is aria-hidden.",
        "Label is linked with htmlFor, and description through aria-describedby."
      ],
      "motion": [
        "A later option rises in from below and an earlier one drops from above, with a soft blur.",
        "Reduced motion swaps the value with an instant crossfade."
      ],
      "responsive": [
        "The menu matches the trigger width, caps at min(24rem, 100vw minus 20px), and at 320px or the available height, then scrolls.",
        "Long labels ellipsize in the trigger instead of widening it.",
        "The trigger never scales on press, so Radix measures a stable anchor on touch."
      ],
      "performance": [
        "The menu renders in a Radix portal only while open; items are not virtualized, so keep the list short.",
        "The translucent menu drops its backdrop blur under prefers-reduced-transparency."
      ],
      "aiNotes": [
        "Pick for a short fixed list where typing is not needed. Use combobox for long or searchable lists, multi-select for several values, and segmented-control for two to four visible choices.",
        "Controlled with value and onValueChange, or uncontrolled with defaultValue; pass name to submit with a form."
      ],
      "related": [
        "combobox",
        "multi-select",
        "segmented-control",
        "radio-group"
      ],
      "usage": "import { Select } from \"@/components/arc/select/select\";\n\nexport function RegionSelect() {\n  const [region, setRegion] = useState(\"eu\");\n  return (\n    <Select\n      label=\"Region\"\n      value={region}\n      onValueChange={setRegion}\n      options={[\n        { value: \"us\", label: \"United States\" },\n        { value: \"eu\", label: \"Europe\" },\n        { value: \"ap\", label: \"Asia Pacific\", disabled: true },\n      ]}\n    />\n  );\n}",
      "exports": [
        "Select"
      ]
    },
    {
      "name": "combobox",
      "kind": "component",
      "title": "Combobox",
      "description": "Search and select from a list without leaving the field.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "search"
      ],
      "docs": "https://uiarc.dev/components/combobox",
      "markdown": "https://uiarc.dev/components/combobox/markdown",
      "registry": "https://uiarc.dev/r/combobox.json",
      "keywords": [
        "react combobox",
        "autocomplete",
        "searchable select",
        "typeahead dropdown",
        "filterable select",
        "autocomplete input"
      ],
      "whenToUse": [
        "Single-choice fields with long lists, such as time zones or countries.",
        "Lists where people know the name and want to type, including aliases via keywords."
      ],
      "whenNotToUse": [
        "Use select for a short list where typing adds nothing.",
        "Use multi-select when several values can be chosen.",
        "Use expanding-search when the search navigates instead of setting a value."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Input has role=\"combobox\" with aria-expanded, aria-controls, aria-autocomplete=\"list\", and aria-activedescendant.",
        "Options use role=\"option\" with aria-selected and aria-disabled; the list is labelled \"<label> options\".",
        "The empty state is a role=\"status\" row; the clear button is labelled \"Clear selection\"."
      ],
      "motion": [
        "The popover springs in from slightly above and the listbox height follows filtering on a smooth spring.",
        "A chosen label rises into the field with a soft blur; the clear button scales in.",
        "Reduced motion keeps opacity-only fades and instant height changes."
      ],
      "responsive": [
        "The popover spans the field width and the listbox caps at min(300px, 40vh), scrolling with contained overscroll.",
        "It closes on any pointerdown outside, so a tap elsewhere dismisses it on touch."
      ],
      "performance": [
        "Filtering is memoized and runs on every keystroke across all options; results are not virtualized, so pre-filter very large lists.",
        "A ResizeObserver drives the listbox height spring while filtering."
      ],
      "aiNotes": [
        "Use for long or searchable single-choice lists. Use select for short lists, multi-select for several values, and expanding-search for search that navigates.",
        "Controlled with value and onValueChange, or uncontrolled with defaultValue. The input shows the label, not the value, so pair name with a hidden input if you post a form.",
        "Add keywords to options for synonyms and aliases."
      ],
      "related": [
        "select",
        "multi-select",
        "expanding-search",
        "search-field"
      ],
      "usage": "import { Combobox } from \"@/components/arc/combobox/combobox\";\n\nconst timezones = [\n  { value: \"utc\", label: \"UTC\" },\n  { value: \"cet\", label: \"Central European\", keywords: [\"zurich\", \"berlin\"] },\n  { value: \"pst\", label: \"Pacific\", keywords: [\"san francisco\"] },\n];\n\nexport function TimezoneField() {\n  const [zone, setZone] = useState(\"\");\n  return <Combobox label=\"Time zone\" options={timezones} value={zone} onValueChange={setZone} />;\n}",
      "exports": [
        "Combobox"
      ]
    },
    {
      "name": "checkbox",
      "kind": "component",
      "title": "Checkbox",
      "description": "A binary choice with a precise, legible state.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "form"
      ],
      "docs": "https://uiarc.dev/components/checkbox",
      "markdown": "https://uiarc.dev/components/checkbox/markdown",
      "registry": "https://uiarc.dev/r/checkbox.json",
      "keywords": [
        "react checkbox",
        "animated checkbox",
        "indeterminate checkbox",
        "radix checkbox",
        "checkbox with description",
        "terms checkbox"
      ],
      "whenToUse": [
        "Independent on and off choices confirmed by a submit, such as accepting terms.",
        "Parent rows that show a partial selection through the indeterminate state."
      ],
      "whenNotToUse": [
        "Use switch for settings that apply immediately.",
        "Use radio-group when only one option can be chosen.",
        "Use chip-group for filter facets people toggle often."
      ],
      "dependencies": [
        "@radix-ui/react-checkbox",
        "motion"
      ],
      "accessibility": [
        "Radix renders a button with role=\"checkbox\" and aria-checked, including \"mixed\" for indeterminate.",
        "The label is a real label element tied by id; description is linked through aria-describedby.",
        "The drawn mark is aria-hidden."
      ],
      "motion": [
        "The fill scales in on a snappy spring while the check path draws; switching to indeterminate morphs the same path into a dash.",
        "Reduced motion applies every state change instantly."
      ],
      "responsive": [
        "The hit box is a full control-height square, so it stays easy to tap even though the drawn box is smaller.",
        "The label and description wrap beside the box; the box stays top-aligned with the first line."
      ],
      "performance": [
        "A single spring on the fill and a path draw per toggle; long lists of checkboxes are fine."
      ],
      "aiNotes": [
        "Use for independent on/off choices in forms. Use switch for settings that apply immediately and chip-group for filter facets.",
        "Set checked=\"indeterminate\" on a parent checkbox when only some children are selected.",
        "Radix renders a hidden native input when name is set, so it submits with forms."
      ],
      "related": [
        "switch",
        "radio-group",
        "chip-group"
      ],
      "usage": "import { Checkbox } from \"@/components/arc/checkbox/checkbox\";\n\nexport function TermsCheckbox() {\n  const [accepted, setAccepted] = useState(false);\n  return (\n    <Checkbox\n      label=\"I agree to the terms\"\n      description=\"You can export your data at any time.\"\n      checked={accepted}\n      onCheckedChange={(next) => setAccepted(next === true)}\n    />\n  );\n}",
      "exports": [
        "Checkbox"
      ]
    },
    {
      "name": "switch",
      "kind": "component",
      "title": "Switch",
      "description": "A tactile toggle for settings that take effect immediately.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "toggle",
        "settings"
      ],
      "docs": "https://uiarc.dev/components/switch",
      "markdown": "https://uiarc.dev/components/switch/markdown",
      "registry": "https://uiarc.dev/r/switch.json",
      "keywords": [
        "react switch",
        "toggle switch",
        "ios toggle",
        "radix switch",
        "settings toggle",
        "animated switch"
      ],
      "whenToUse": [
        "Settings that take effect as soon as they are flipped, like notifications.",
        "Settings lists where each row is one on and off preference."
      ],
      "whenNotToUse": [
        "Use checkbox when the choice waits for a submit button.",
        "Use segmented-control for a choice between named options.",
        "Use theme-switch for a light and dark toggle."
      ],
      "dependencies": [
        "@radix-ui/react-switch",
        "motion"
      ],
      "accessibility": [
        "Radix renders a button with role=\"switch\" and aria-checked.",
        "The label prop becomes aria-label unless one is passed; icon-only usage needs an aria-label."
      ],
      "motion": [
        "Pressing stretches the thumb toward the other side like a held finger; releasing sends it across on a snappy spring.",
        "Reduced motion removes the stretch and moves the thumb instantly; CSS transitions are also disabled."
      ],
      "responsive": [
        "The switch is inline and keeps a control-height minimum, so the whole label is a touch target.",
        "Hover styles apply only on fine pointers; touch gets the press stretch."
      ],
      "performance": [
        "Motion is CSS transitions on the track and thumb, with no observers or per-frame work."
      ],
      "aiNotes": [
        "Use for settings that take effect immediately. Use checkbox for choices confirmed by a submit button.",
        "Exported both as a named and a default export.",
        "Pass name to submit with a form through Radix's hidden input."
      ],
      "related": [
        "checkbox",
        "segmented-control",
        "theme-switch"
      ],
      "usage": "import { Switch } from \"@/components/arc/switch/switch\";\n\nexport function NotificationsToggle() {\n  const [enabled, setEnabled] = useState(true);\n  return <Switch label=\"Email notifications\" checked={enabled} onCheckedChange={setEnabled} />;\n}",
      "exports": [
        "Switch"
      ]
    },
    {
      "name": "multi-select",
      "kind": "component",
      "title": "Multi-select",
      "description": "Select several values while keeping the field readable.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "choice"
      ],
      "docs": "https://uiarc.dev/components/multi-select",
      "markdown": "https://uiarc.dev/components/multi-select/markdown",
      "registry": "https://uiarc.dev/r/multi-select.json",
      "keywords": [
        "react multi select",
        "multiselect dropdown",
        "select multiple options",
        "multi select chips",
        "tag select",
        "checkbox dropdown"
      ],
      "whenToUse": [
        "Picking several values from a fixed list in a compact field, such as labels or assignees.",
        "Filters where the chosen values should show in the field with a +N overflow."
      ],
      "whenNotToUse": [
        "Use chip-group when every option should stay visible.",
        "Use tag-input for free-form values.",
        "Use select for a single value."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Trigger has aria-haspopup=\"listbox\", aria-expanded, and aria-labelledby combining the label and a hidden list of selected labels.",
        "The menu is role=\"listbox\" with aria-multiselectable; options carry aria-selected and aria-disabled.",
        "Clear button is labelled \"Clear selections\"; chips are aria-hidden in favor of the spoken summary."
      ],
      "motion": [
        "Chips open their slot width on a smooth spring and grow in from 0.9 with a blur; the +N count rolls up or down.",
        "Checks draw in the menu; the menu springs in from slightly above.",
        "Reduced motion turns every change into a short crossfade with no scale or width travel."
      ],
      "responsive": [
        "Chips cap at 9rem and ellipsize, and maxVisible limits how many show before +N, so the trigger holds one line.",
        "The menu spans the field width and has no max height or scroll, so keep option lists short on small screens."
      ],
      "performance": [
        "Options are not virtualized and the menu does not scroll; for long lists use combobox instead.",
        "Chips animate slot width and scale per change; fine for a few selections."
      ],
      "aiNotes": [
        "Use for picking several values from a fixed list in a compact field. Use chip-group when all options should stay visible and tag-input for free-form values.",
        "Controlled with value and onValueChange or uncontrolled with defaultValue. There is no name prop, so serialize the array yourself for forms."
      ],
      "related": [
        "select",
        "combobox",
        "chip-group",
        "tag-input"
      ],
      "usage": "import { MultiSelect } from \"@/components/arc/multi-select/multi-select\";\n\nexport function LabelPicker() {\n  const [labels, setLabels] = useState<string[]>([\"bug\"]);\n  return (\n    <MultiSelect\n      label=\"Labels\"\n      value={labels}\n      onValueChange={setLabels}\n      options={[\n        { value: \"bug\", label: \"Bug\" },\n        { value: \"feature\", label: \"Feature\" },\n        { value: \"docs\", label: \"Docs\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "MultiSelect"
      ]
    },
    {
      "name": "number-field",
      "kind": "component",
      "title": "Number field",
      "description": "Enter a bounded number with clear increment controls.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "numeric"
      ],
      "docs": "https://uiarc.dev/components/number-field",
      "markdown": "https://uiarc.dev/components/number-field/markdown",
      "registry": "https://uiarc.dev/r/number-field.json",
      "keywords": [
        "react number input",
        "number field",
        "stepper input",
        "quantity selector",
        "numeric input with buttons",
        "scrub input",
        "odometer number input"
      ],
      "whenToUse": [
        "Bounded quantities like seats, items, or prices where the exact number matters.",
        "Values people nudge with steppers, arrow keys, or by dragging the label with scrub.",
        "Numbers with units, using a prefix or a pluralizing suffix function."
      ],
      "whenNotToUse": [
        "Use slider when the approximate position matters more than the exact number.",
        "Use input for numeric strings like phone numbers or codes."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The input has role=\"spinbutton\" with aria-valuenow, aria-valuemin, aria-valuemax, and aria-valuetext including prefix and suffix.",
        "Stepper buttons are labelled \"Increase <label>\" and \"Decrease <label>\" and marked aria-disabled at a limit.",
        "Changes are announced through a polite aria-live region, and a clamp says which limit it met (\"10 seats, maximum\"); rolling digits are aria-hidden.",
        "A typed value past a limit sets aria-invalid and adds the limit note to aria-describedby until it commits. The warning also uses copy, never color alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focus darkens the shell border, and a focused stepper button takes its hover fill."
      ],
      "motion": [
        "Digits roll on wheels in the direction of change (up rolls up, down rolls down) with tabular numerals, and affixes reword in place.",
        "Holding a stepper repeats after 400ms and ramps from 150ms to 40ms per step.",
        "At a limit the value strains a few pixels toward the press and springs home; pushes in a row strain a little further, capped like overscroll. The refused button shakes once and the limit note rises in beside the label.",
        "A typed value past a limit tints the shell with the warning role; on Enter or blur it springs back to the limit with a digit roll.",
        "Reduced motion changes the value at once and answers a press at a limit with a brief warning tint on the number instead of the strain and shake."
      ],
      "responsive": [
        "The control is min(100%, 196px) wide at md (164px at sm, 228px at lg), overridable with --number-field-width. The label row, with its limit note, follows the same width.",
        "Stepper buttons are 28, 34, or 40px by size with touch-action manipulation, so fast taps do not zoom.",
        "Scrubbing the label uses pointer capture with touch-action pan-y, so vertical scrolling still works on touch."
      ],
      "performance": [
        "Holding a stepper repeats on timeouts that speed up, not a per-frame loop.",
        "Two ResizeObservers handle digit layout and the helper row; fine for forms, not for large grids."
      ],
      "aiNotes": [
        "Use for bounded integers or decimals like quantities, seats, and prices. Use slider when the approximate position matters more than the exact number.",
        "Controlled with value and onValueChange or uncontrolled with defaultValue. There is no name prop; add a hidden input for plain forms.",
        "Set min below zero to allow negatives; the input then accepts a minus sign.",
        "The limit note sits at the end of the label row, so keep labels short enough to share the control's width with it, or set limitHint={false}."
      ],
      "related": [
        "slider",
        "input",
        "animated-counter"
      ],
      "usage": "import { NumberField } from \"@/components/arc/number-field/number-field\";\n\nexport function SeatsField() {\n  const [seats, setSeats] = useState(5);\n  return (\n    <NumberField\n      label=\"Seats\"\n      value={seats}\n      onValueChange={setSeats}\n      min={1}\n      max={500}\n      suffix={(n) => (n === 1 ? \" seat\" : \" seats\")}\n      scrub\n    />\n  );\n}",
      "exports": [
        "NumberField"
      ]
    },
    {
      "name": "password-field",
      "kind": "component",
      "title": "Password field",
      "description": "Capture sensitive text with a visible reveal control.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "security"
      ],
      "docs": "https://uiarc.dev/components/password-field",
      "markdown": "https://uiarc.dev/components/password-field/markdown",
      "registry": "https://uiarc.dev/r/password-field.json",
      "keywords": [
        "react password input",
        "show hide password",
        "password toggle",
        "password field with eye icon",
        "login password field"
      ],
      "whenToUse": [
        "Sign in forms and password confirmation fields.",
        "Any secret the person may want to check by revealing it."
      ],
      "whenNotToUse": [
        "Use password-strength when creating or changing a password.",
        "Use input for non-secret values."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Toggle is a native button with aria-pressed and a label that switches between \"Show password\" and \"Hide password\" (messages.showPassword and messages.hidePassword).",
        "Description is linked through aria-describedby.",
        "Set autoComplete to current-password or new-password so password managers work."
      ],
      "motion": [
        "A slash draws across the eye and masks the outline beneath it instead of swapping icons.",
        "After the first toggle, the value resolves through a short CSS reveal on each change; reduced motion draws the slash instantly."
      ],
      "responsive": [
        "The field fills its column; the reveal button sits inside the shell, so it never wraps below the input.",
        "Hover styles apply only on fine pointers."
      ],
      "performance": [
        "One ResizeObserver per field measures the helper row for its height spring; cheap for a form, but avoid hundreds in a table.",
        "The reveal effect is a short CSS animation, and the slash is one drawn path."
      ],
      "aiNotes": [
        "Use for sign in and password confirmation. Use password-strength when the user is creating a new password.",
        "Uncontrolled by default; pass value and onChange to control it like a native input.",
        "Every built-in string can be translated through messages; the label and description are your own props."
      ],
      "related": [
        "password-strength",
        "input"
      ],
      "usage": "import { PasswordField } from \"@/components/arc/password-field/password-field\";\n\nexport function SignInPassword() {\n  return (\n    <PasswordField\n      label=\"Password\"\n      name=\"password\"\n      autoComplete=\"current-password\"\n      required\n    />\n  );\n}",
      "examples": [
        {
          "title": "Translated",
          "code": "<PasswordField label=\"Passwort\" messages={{ showPassword: \"Passwort anzeigen\", hidePassword: \"Passwort verbergen\" }} />"
        }
      ],
      "exports": [
        "PasswordField"
      ]
    },
    {
      "name": "search-field",
      "kind": "component",
      "title": "Search field",
      "description": "A recognizable search entry point with clear affordances.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "search"
      ],
      "docs": "https://uiarc.dev/components/search-field",
      "markdown": "https://uiarc.dev/components/search-field/markdown",
      "registry": "https://uiarc.dev/r/search-field.json",
      "keywords": [
        "react search input",
        "search field",
        "search bar with clear button",
        "filter input",
        "list filter search"
      ],
      "whenToUse": [
        "Filtering a visible list or table in place.",
        "Toolbar search where the query should be clearable with one click."
      ],
      "whenNotToUse": [
        "Use expanding-search for compact header search with results.",
        "Use combobox when the search sets a form value.",
        "Use filter-toolbar when search sits with other filters and sort."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Native input type=\"search\" with a real label.",
        "Clear button is labelled \"Clear search\" and returns focus to the input.",
        "The search icon is aria-hidden."
      ],
      "motion": [
        "The clear button scales in from 0.8 with a slight blur on a snappy spring and presses to 0.96.",
        "Reduced motion fades it in and out without scale or blur; the reserved slot keeps the field width fixed either way."
      ],
      "responsive": [
        "The clear button has a reserved slot, so the field width never shifts when it appears.",
        "It fills its column with min-width 0 and shrinks inside narrow toolbars."
      ],
      "performance": [
        "onValueChange fires on every keystroke; debounce expensive filtering or fetching yourself."
      ],
      "aiNotes": [
        "Use to filter a visible list in place. Use expanding-search for a compact header search with results, and combobox to choose a value.",
        "Always controlled: pass value and onValueChange, and debounce expensive filtering yourself."
      ],
      "related": [
        "expanding-search",
        "combobox",
        "filter-toolbar"
      ],
      "usage": "import { SearchField } from \"@/components/arc/search-field/search-field\";\n\nexport function MemberFilter() {\n  const [query, setQuery] = useState(\"\");\n  return (\n    <SearchField\n      label=\"Search members\"\n      placeholder=\"Name or email\"\n      value={query}\n      onValueChange={setQuery}\n    />\n  );\n}",
      "exports": [
        "SearchField"
      ]
    },
    {
      "name": "tag-input",
      "kind": "component",
      "title": "Tag input",
      "description": "Turn short text values into removable tags.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "tags"
      ],
      "docs": "https://uiarc.dev/components/tag-input",
      "markdown": "https://uiarc.dev/components/tag-input/markdown",
      "registry": "https://uiarc.dev/r/tag-input.json",
      "keywords": [
        "react tag input",
        "tags input",
        "chip input",
        "multi value input",
        "email chips input",
        "keyword input"
      ],
      "whenToUse": [
        "Free-form lists of short values like tags, keywords, or emails.",
        "Fields where people add values by pressing Enter or comma."
      ],
      "whenNotToUse": [
        "Use multi-select or chip-group when values come from a fixed list.",
        "Use input for a single value."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each tag has a remove button labelled \"Remove <tag>\".",
        "Adds, removals, picks, and duplicates are announced in a polite aria-live region.",
        "Blurring the input commits any typed draft as a tag."
      ],
      "motion": [
        "New tags blur and scale in where the text was typed; removed tags leave and the rest glide on the morph spring. The shell follows wrapped rows on a smooth spring.",
        "A ring glides between picked tags. Reduced motion removes layout travel, scale, and blur, keeping fades."
      ],
      "responsive": [
        "Tags wrap onto new rows and the shell height follows on a spring, so the field never scrolls horizontally.",
        "The text input keeps an 80px minimum, dropping to its own row when the last row is full."
      ],
      "performance": [
        "Every tag is a motion element with layout position animation; fine for dozens of tags, not hundreds.",
        "Two ResizeObservers track the content and message heights."
      ],
      "aiNotes": [
        "Use for free-form values like tags, emails, or keywords. Use multi-select or chip-group when values come from a fixed list.",
        "Controlled or uncontrolled. Tags are plain strings; validate or normalize in onValueChange."
      ],
      "related": [
        "multi-select",
        "chip-group",
        "input"
      ],
      "usage": "import { TagInput } from \"@/components/arc/tag-input/tag-input\";\n\nexport function TopicsField() {\n  const [topics, setTopics] = useState([\"design\", \"motion\"]);\n  return (\n    <TagInput\n      label=\"Topics\"\n      value={topics}\n      onValueChange={setTopics}\n      description=\"Press Enter or comma to add.\"\n    />\n  );\n}",
      "exports": [
        "TagInput"
      ]
    },
    {
      "name": "file-dropzone",
      "kind": "component",
      "title": "File dropzone",
      "description": "A generous target for dropping one or more files.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "files"
      ],
      "docs": "https://uiarc.dev/components/file-dropzone",
      "markdown": "https://uiarc.dev/components/file-dropzone/markdown",
      "registry": "https://uiarc.dev/r/file-dropzone.json",
      "keywords": [
        "react file upload",
        "file dropzone",
        "drag and drop upload",
        "upload with progress",
        "file uploader",
        "dropzone component",
        "multi file upload"
      ],
      "whenToUse": [
        "Attachments and uploads with drag and drop plus a file picker.",
        "Uploads that need per-file progress, retry, and cancel through onUpload."
      ],
      "whenNotToUse": [
        "Use input with type file only when a native picker with no list is enough.",
        "Use empty-state when the drop area is the only thing on an empty page and needs a larger call to action."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The target is a native button; the hidden file input is removed from the tab order.",
        "Uploading rows expose role=\"progressbar\" with aria-valuenow; remove and retry buttons are labelled with the file name.",
        "Additions, completions, and failures are announced through a polite role=\"status\" region; rejection errors use role=\"alert\".",
        "Removing a row moves focus to a neighbouring row so keyboard users are not dropped."
      ],
      "motion": [
        "The icon lifts while files hover, the edge animates, rows enter and collapse on a smooth spring, and progress bars ease toward their target.",
        "Reduced motion replaces height and lift animations with short fades and keeps progress changes instant."
      ],
      "responsive": [
        "The file list is a container query: below 440px retry becomes icon-only, and below 300px the file icon hides.",
        "File names and failure reasons ellipsize instead of wrapping.",
        "The whole target is one tap area that opens the native picker, so it works without drag and drop on touch."
      ],
      "performance": [
        "The dashed edge is refitted by a ResizeObserver, plus a rAF loop only while the corner radius is transitioning.",
        "Rows are not virtualized and maxFiles defaults to 5; row entrances stagger up to eight steps."
      ],
      "aiNotes": [
        "Use for file attachments and uploads. Without onUpload the list shows plain selections; read them from onFilesChange and submit yourself.",
        "Files can be dropped, picked, or pasted. Pass preview on defaultItems (for example a stored thumbnail URL) to show earlier image uploads as thumbnails.",
        "With onUpload, the component manages per-file status, retry, and abort; wire it to fetch or XHR and pass signal through.",
        "Also exports formatFileSize(bytes) and the FileDropzoneItem, FileDropzoneStatus, and FileDropzoneUpload types."
      ],
      "related": [
        "progress",
        "input",
        "empty-state"
      ],
      "usage": "import { FileDropzone } from \"@/components/arc/file-dropzone/file-dropzone\";\n\nexport function AttachmentUpload() {\n  return (\n    <FileDropzone\n      accept=\".pdf,image/*\"\n      maxFiles={3}\n      maxSize={10 * 1024 * 1024}\n      onUpload={async (item, { onProgress, signal }) => {\n        await uploadFile(item.file!, { onProgress, signal });\n      }}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Folding prompt with a size limit",
          "code": "<FileDropzone\n  label=\"Add receipts\"\n  accept=\"image/*,.pdf\"\n  maxFiles={10}\n  maxSize={5 * 1024 * 1024}\n  compactAt={2}\n  listPlacement=\"inside\"\n  onFilesChange={setFiles}\n/>"
        }
      ],
      "exports": [
        "FileDropzone",
        "FileDropzoneItem"
      ]
    },
    {
      "name": "radio-group",
      "kind": "component",
      "title": "Radio group",
      "description": "Choose one option from a visible set.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "field",
        "choice"
      ],
      "docs": "https://uiarc.dev/components/radio-group",
      "markdown": "https://uiarc.dev/components/radio-group/markdown",
      "registry": "https://uiarc.dev/r/radio-group.json",
      "keywords": [
        "react radio group",
        "radio buttons",
        "radio cards",
        "plan picker",
        "animated radio",
        "radio with description"
      ],
      "whenToUse": [
        "Two to six mutually exclusive options that need descriptions, such as plans.",
        "Form choices that should submit natively through name."
      ],
      "whenNotToUse": [
        "Use segmented-control for short inline view options.",
        "Use select for longer lists.",
        "Use checkbox when several options can be on at once."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Uses a fieldset and legend with native radio inputs wrapped in labels.",
        "The highlight and dot are aria-hidden decoration."
      ],
      "motion": [
        "One highlight glides to the chosen row on the morph spring while the new dot springs in and the old one shrinks away.",
        "Reduced motion places the highlight and dot instantly; resizes never animate."
      ],
      "responsive": [
        "Rows stack vertically and stretch to the column, so long descriptions wrap cleanly on narrow screens.",
        "A ResizeObserver re-places the highlight on resize without animating it."
      ],
      "performance": [
        "One shared highlight moves between rows instead of a layout animation per row."
      ],
      "aiNotes": [
        "Use for two to six mutually exclusive options that need descriptions. Use segmented-control for short inline choices and select for longer lists.",
        "Always controlled: value and onValueChange are required. Pass name to submit with a form."
      ],
      "related": [
        "segmented-control",
        "select",
        "checkbox"
      ],
      "usage": "import { RadioGroup } from \"@/components/arc/radio-group/radio-group\";\n\nexport function PlanPicker() {\n  const [plan, setPlan] = useState(\"team\");\n  return (\n    <RadioGroup\n      label=\"Plan\"\n      name=\"plan\"\n      value={plan}\n      onValueChange={setPlan}\n      options={[\n        { value: \"solo\", label: \"Solo\", description: \"One seat\" },\n        { value: \"team\", label: \"Team\", description: \"Up to 20 seats\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "RadioGroup"
      ]
    },
    {
      "name": "segmented-control",
      "kind": "component",
      "title": "Segmented control",
      "description": "Switch between a small set of related views.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "control",
        "choice"
      ],
      "docs": "https://uiarc.dev/components/segmented-control",
      "markdown": "https://uiarc.dev/components/segmented-control/markdown",
      "registry": "https://uiarc.dev/r/segmented-control.json",
      "keywords": [
        "react segmented control",
        "segmented button",
        "toggle group",
        "ios segmented control",
        "view switcher",
        "sliding pill toggle"
      ],
      "whenToUse": [
        "Two to five short view options like Day, Week, and Month.",
        "Toolbar toggles between layouts or modes that apply immediately."
      ],
      "whenNotToUse": [
        "Use tabs when each option swaps a panel of content.",
        "Use radio-group in forms or when options need descriptions.",
        "Use switch for a single on and off setting."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders role=\"group\" labelled by label, with native buttons using aria-pressed for the selected segment. Only the selected segment is a tab stop.",
        "The sliding pill is aria-hidden."
      ],
      "motion": [
        "A shared layoutId pill slides to the selected segment on the morph spring, scoped per instance by a LayoutGroup.",
        "Reduced motion moves the pill instantly and disables button transitions."
      ],
      "responsive": [
        "The row is inline with max-width 100% and scrolls horizontally with a hidden scrollbar when segments do not fit.",
        "Segments never shrink or wrap, so keep labels short on mobile."
      ],
      "performance": [
        "One shared layoutId pill per instance, scoped by a LayoutGroup; no observers."
      ],
      "aiNotes": [
        "Use for two to five short, mutually exclusive view options like time ranges or layouts. Use tabs when each option swaps a panel, and radio-group in forms.",
        "Always controlled. Import it as a default export."
      ],
      "related": [
        "tabs",
        "radio-group",
        "switch",
        "liquid-tab-bar"
      ],
      "usage": "import SegmentedControl from \"@/components/arc/segmented-control/segmented-control\";\n\nexport function RangeToggle() {\n  const [range, setRange] = useState(\"week\");\n  return (\n    <SegmentedControl\n      label=\"Range\"\n      value={range}\n      onValueChange={setRange}\n      options={[\n        { value: \"day\", label: \"Day\" },\n        { value: \"week\", label: \"Week\" },\n        { value: \"month\", label: \"Month\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "SegmentedControl"
      ]
    },
    {
      "name": "calendar",
      "kind": "component",
      "title": "Calendar",
      "description": "Browse dates in a clear, compact month view.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "date",
        "field"
      ],
      "docs": "https://uiarc.dev/components/calendar",
      "markdown": "https://uiarc.dev/components/calendar/markdown",
      "registry": "https://uiarc.dev/r/calendar.json",
      "keywords": [
        "react calendar",
        "date calendar",
        "month calendar",
        "inline date picker",
        "booking calendar",
        "animated calendar"
      ],
      "whenToUse": [
        "Inline date picking where the month should stay visible, such as booking pages.",
        "Picking one date with blocked days via minDate, maxDate, and disabledDates."
      ],
      "whenNotToUse": [
        "Use date-picker for a form field that opens a calendar popover.",
        "Use time-picker for times of day.",
        "Use booking-pill for a full compact booking flow."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Days sit in a role=\"grid\" with role=\"row\" and role=\"gridcell\" buttons carrying full date aria-labels, aria-selected, and aria-current=\"date\" for today.",
        "Roving tabindex keeps one day in the tab order; month changes are announced through a polite live region.",
        "Previous and next buttons are labelled and use aria-disabled at the min or max month."
      ],
      "motion": [
        "Months slide in the direction of travel while the title letters and year digits roll; the selection highlight glides between days on the morph spring.",
        "Reduced motion, applied after hydration, swaps months and moves the highlight instantly."
      ],
      "responsive": [
        "The calendar is min(100%, 328px) wide and days are square cells that scale with it.",
        "The header is a container query: below 260px the title takes its own row and the controls move under it."
      ],
      "performance": [
        "Only the incoming month is observed with a ResizeObserver, to morph between four, five, and six rows.",
        "Each day is a button, so a month is about 42 cells; render one calendar rather than many side by side."
      ],
      "aiNotes": [
        "Use inline where the calendar is always visible, such as booking pages. Use date-picker for a form field that opens a calendar popover.",
        "value is controlled only; month can be controlled or left internal.",
        "Also exports date helpers addDays, addMonths, sameDay, and startOfDay, and the CalendarDateMatcher type."
      ],
      "related": [
        "date-picker",
        "time-picker",
        "booking-pill"
      ],
      "usage": "import { Calendar } from \"@/components/arc/calendar/calendar\";\n\nexport function DeliveryDate() {\n  const [date, setDate] = useState<Date>();\n  return (\n    <Calendar\n      value={date}\n      onChange={setDate}\n      minDate={new Date()}\n      disabledDates={(day) => day.getDay() === 0}\n      showToday\n    />\n  );\n}",
      "exports": [
        "Calendar",
        "useToday"
      ]
    },
    {
      "name": "date-picker",
      "kind": "component",
      "title": "Date picker",
      "description": "Choose a date without losing context.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "date",
        "field"
      ],
      "docs": "https://uiarc.dev/components/date-picker",
      "markdown": "https://uiarc.dev/components/date-picker/markdown",
      "registry": "https://uiarc.dev/r/date-picker.json",
      "keywords": [
        "react date picker",
        "datepicker",
        "date input with calendar",
        "calendar popover",
        "form date field",
        "animated date picker"
      ],
      "whenToUse": [
        "A date field in a form, such as a start or due date.",
        "Dates with limits or blocked days, shown in a locale format."
      ],
      "whenNotToUse": [
        "Use calendar inline when the grid should stay visible.",
        "Use time-picker for times, and pair it with this for a full timestamp."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Trigger has aria-haspopup=\"dialog\" and aria-expanded; the popover is role=\"dialog\" labelled \"<label> calendar\".",
        "The formatted value lives in a visually hidden span; the rolling text is aria-hidden.",
        "Tabbing out or clicking outside closes it, and focus returns to the trigger when it was inside."
      ],
      "motion": [
        "Changed date parts roll up for a later date and down for an earlier one; the popover springs in and closes about 240 ms after a pick so the highlight can land.",
        "Reduced motion crossfades the value and closes immediately after a pick."
      ],
      "responsive": [
        "The popover is min(320px, 100vw minus 32px) wide, left-aligned to the field.",
        "Below 360px of viewport the popover stretches to both field edges.",
        "It closes on any pointerdown outside, so a tap elsewhere dismisses it."
      ],
      "performance": [
        "The calendar mounts only while open, and changed date parts roll with layout position animation."
      ],
      "aiNotes": [
        "Use for a date field in forms. Use calendar inline when the grid should stay visible, and time-picker for times.",
        "value is controlled only. There is no name prop; serialize the Date into a hidden input for plain forms."
      ],
      "related": [
        "calendar",
        "time-picker",
        "input"
      ],
      "usage": "import { DatePicker } from \"@/components/arc/date-picker/date-picker\";\n\nexport function StartDateField() {\n  const [start, setStart] = useState<Date>();\n  return (\n    <DatePicker\n      label=\"Start date\"\n      value={start}\n      onChange={setStart}\n      minDate={new Date()}\n      showToday\n    />\n  );\n}",
      "exports": [
        "DatePicker"
      ]
    },
    {
      "name": "time-picker",
      "kind": "component",
      "title": "Time picker",
      "description": "Choose a time with sensible keyboard behavior.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "time",
        "field"
      ],
      "docs": "https://uiarc.dev/components/time-picker",
      "markdown": "https://uiarc.dev/components/time-picker/markdown",
      "registry": "https://uiarc.dev/r/time-picker.json",
      "keywords": [
        "react time picker",
        "time select",
        "time input",
        "meeting time picker",
        "12 hour time picker",
        "time dropdown"
      ],
      "whenToUse": [
        "Picking a time of day at a fixed interval, such as meetings or reminders.",
        "Fields that show 12-hour times but store 24-hour HH:mm values."
      ],
      "whenNotToUse": [
        "Use date-picker for dates, pairing the two for a timestamp.",
        "Use time-dial for a more tactile, showcase time selection.",
        "Use select when only a few named times are allowed."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Trigger has aria-haspopup=\"listbox\", aria-expanded, aria-controls, and aria-labelledby combining the label and a hidden value.",
        "The menu is role=\"listbox\" with role=\"option\" items carrying aria-selected.",
        "The list opens centered on the selected time and scrolls only itself, never the page."
      ],
      "motion": [
        "A later time rises in from below and an earlier one drops from above with a soft blur; the menu springs in from slightly above.",
        "Reduced motion crossfades the value and opens the menu with opacity only."
      ],
      "responsive": [
        "The list spans the field width and caps at 250px, scrolling itself with contained overscroll.",
        "It closes on any pointerdown outside the field."
      ],
      "performance": [
        "All options at the chosen step render at once; minuteStep 1 means 1,440 rows, so prefer 5 or more."
      ],
      "aiNotes": [
        "Use for picking a time of day at a fixed interval. Pair with date-picker for a full timestamp.",
        "Controlled or uncontrolled; values are always 24-hour \"HH:mm\" strings regardless of display format."
      ],
      "related": [
        "date-picker",
        "calendar",
        "select",
        "time-dial"
      ],
      "usage": "import { TimePicker } from \"@/components/arc/time-picker/time-picker\";\n\nexport function MeetingTime() {\n  const [time, setTime] = useState(\"14:30\");\n  return <TimePicker label=\"Start time\" value={time} onChange={setTime} minuteStep={30} />;\n}",
      "exports": [
        "TimePicker"
      ]
    },
    {
      "name": "accordion",
      "kind": "component",
      "title": "Accordion",
      "description": "Progressively reveal supporting information in place.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "disclosure",
        "layout"
      ],
      "docs": "https://uiarc.dev/components/accordion",
      "markdown": "https://uiarc.dev/components/accordion/markdown",
      "registry": "https://uiarc.dev/r/accordion.json",
      "keywords": [
        "react accordion",
        "faq accordion",
        "collapsible sections",
        "animated accordion",
        "radix accordion",
        "expand collapse list",
        "faq component"
      ],
      "whenToUse": [
        "FAQ sections where only one answer should be open at a time.",
        "Settings or help pages that group long content under short, scannable questions.",
        "Page-level FAQs that need larger type, via size=\"lg\"."
      ],
      "whenNotToUse": [
        "Use expandable-card for a single standalone disclosure such as a plan or order summary.",
        "Use tabs when sections are peer views that people switch between.",
        "Use onboarding-checklist when the rows are setup tasks to complete."
      ],
      "dependencies": [
        "@radix-ui/react-accordion",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Radix wires aria-expanded, aria-controls, and region labelling between each trigger and panel.",
        "Closed panels stay mounted but switch to visibility hidden once collapsed, so they leave the accessibility tree.",
        "Triggers sit inside heading elements; the chevron is aria-hidden."
      ],
      "motion": [
        "Panel height springs without overshoot while the answer slides down 6px out of a subtle blur; the chevron rotates on a snappy spring.",
        "A toggle mid-flight retargets from the current height instead of restarting.",
        "Reduced motion applies the same end states in one step."
      ],
      "responsive": [
        "Rows fill their container width; the lg size caps answers at 62ch for readable line length.",
        "Below 520px the lg size drops to a 68px row and smaller answer text with tighter right padding.",
        "Hover color changes apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Closed panels stay mounted and hidden, so every answer is in the DOM; keep very heavy content lazy inside the panel.",
        "Height springs through motion on the one row that changes, with no layout observers."
      ],
      "aiNotes": [
        "Use for FAQs and settings groups where only one section should be open. Use expandable-card for a single standalone disclosure and tabs when sections are peers.",
        "Content is plain data; pass rich ReactNode answers directly. Only single mode is supported."
      ],
      "related": [
        "expandable-card",
        "tabs",
        "onboarding-checklist"
      ],
      "usage": "import { Accordion } from \"@/components/arc/accordion/accordion\";\n\nexport function Faq() {\n  return (\n    <Accordion\n      size=\"lg\"\n      items={[\n        { title: \"Can I cancel anytime?\", content: \"Yes. Your plan stays active until the period ends.\" },\n        { title: \"Do you offer refunds?\", content: \"Within 14 days of purchase, no questions asked.\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "Accordion"
      ]
    },
    {
      "name": "dialog",
      "kind": "component",
      "title": "Dialog",
      "description": "A focused surface for decisions that need attention.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "modal",
        "overlay"
      ],
      "docs": "https://uiarc.dev/components/dialog",
      "markdown": "https://uiarc.dev/components/dialog/markdown",
      "registry": "https://uiarc.dev/r/dialog.json",
      "keywords": [
        "react dialog",
        "modal",
        "animated modal",
        "radix dialog",
        "confirm dialog",
        "modal window",
        "popup dialog"
      ],
      "whenToUse": [
        "Confirmations and decisions that must interrupt, such as Delete project.",
        "Short forms like rename or invite that fit in one focused panel.",
        "Flows where the dialog title changes between steps and should crossfade in place."
      ],
      "whenNotToUse": [
        "Use drawer for long forms or detail panels that keep the page in context.",
        "Use bottom-sheet for mobile-first secondary tasks with snap heights.",
        "Use popover for light, non-modal content anchored to a trigger."
      ],
      "dependencies": [
        "@radix-ui/react-dialog",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Radix renders role=\"dialog\" with aria-modal, traps focus, and restores it to the trigger on close.",
        "title and description are wired to aria-labelledby and aria-describedby.",
        "The close button carries aria-label=\"Close dialog\"."
      ],
      "motion": [
        "The overlay fades while the panel rises 8px and scales from 0.96 on a smooth spring; closing is shorter and retargets from the current state.",
        "Title and description changes rise in with a soft blur.",
        "Reduced motion uses a plain opacity fade, including the CSS keyframe fallback for bare Radix roots."
      ],
      "responsive": [
        "The panel is min(100vw minus a 32px gutter, 440px) wide and centered, so it fits phones without extra CSS.",
        "Height caps at the viewport minus a gutter and the panel scrolls beyond that."
      ],
      "performance": [
        "The overlay uses a 7px backdrop blur, which can cost frames on low-end devices over busy pages.",
        "Content renders in a portal only while open and unmounts after the exit animation."
      ],
      "aiNotes": [
        "Use for decisions that must interrupt: confirmations, short forms. Use drawer for side panels, bottom-sheet for mobile-first secondary tasks, popover for light non-modal content.",
        "Always compose Dialog > DialogTrigger + DialogContent. Wrap your own buttons with asChild."
      ],
      "related": [
        "drawer",
        "bottom-sheet",
        "popover",
        "hold-to-confirm"
      ],
      "usage": "import { Dialog, DialogClose, DialogContent, DialogTrigger } from \"@/components/arc/dialog/dialog\";\nimport { Button } from \"@/components/arc/button/button\";\n\nexport function RenameProject() {\n  return (\n    <Dialog>\n      <DialogTrigger asChild><Button>Rename</Button></DialogTrigger>\n      <DialogContent title=\"Rename project\" description=\"This changes the URL too.\">\n        <input defaultValue=\"Arc\" aria-label=\"Project name\" />\n        <DialogClose asChild><Button>Save</Button></DialogClose>\n      </DialogContent>\n    </Dialog>\n  );\n}",
      "examples": [
        {
          "title": "Confirm with a custom cancel",
          "code": "<Dialog open={open} onOpenChange={setOpen}>\n  <DialogContent title=\"Delete project?\" description=\"This cannot be undone.\">\n    <DialogClose asChild><Button variant=\"secondary\">Cancel</Button></DialogClose>\n    <Button onClick={remove}>Delete</Button>\n  </DialogContent>\n</Dialog>"
        }
      ],
      "exports": [
        "Dialog",
        "DialogTrigger",
        "DialogContent",
        "DialogClose"
      ]
    },
    {
      "name": "popover",
      "kind": "component",
      "title": "Popover",
      "description": "A small anchored surface for contextual information.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "overlay",
        "menu"
      ],
      "docs": "https://uiarc.dev/components/popover",
      "markdown": "https://uiarc.dev/components/popover/markdown",
      "registry": "https://uiarc.dev/r/popover.json",
      "keywords": [
        "react popover",
        "radix popover",
        "floating panel",
        "click popover",
        "animated popover",
        "anchored popup"
      ],
      "whenToUse": [
        "Click-opened panels with interactive content, like share settings or a small filter form.",
        "Non-modal helpers that should stay open while people interact with the rest of the page.",
        "Custom pickers built from your own controls anchored to a button."
      ],
      "whenNotToUse": [
        "Use tooltip for short hover labels.",
        "Use hover-card for read-only previews that open on hover.",
        "Use dialog when the choice must block the page, and dropdown-menu for a list of commands."
      ],
      "dependencies": [
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "Radix sets aria-expanded, aria-controls, and aria-haspopup=\"dialog\" on the trigger.",
        "Focus moves into the content on open and back to the trigger on close.",
        "Content is non-modal by default; give it a heading or aria-label when it holds controls."
      ],
      "motion": [
        "CSS transitions: the panel fades and settles from 5px toward its trigger at 0.97 scale on a spring; it leaves in 140ms.",
        "Transitions instead of keyframes, so a reopen mid-close reverses from where the panel is.",
        "Reduced motion drops the transform and keeps a short opacity fade."
      ],
      "responsive": [
        "The panel is at least 12rem and at most min(22rem, 100vw minus 20px), so it never overflows a phone screen.",
        "Radix collision handling keeps it 10px from viewport edges by default and flips sides when needed."
      ],
      "performance": [
        "Pure CSS transitions with no motion runtime; content mounts in a portal only while open."
      ],
      "aiNotes": [
        "Use for click-opened, non-modal panels with interactive content. Use tooltip for short hover labels, hover-card for read-only previews, dialog when the choice must block.",
        "Compose Popover > PopoverTrigger asChild + PopoverContent. Pass side for placement."
      ],
      "related": [
        "tooltip",
        "hover-card",
        "dialog",
        "dropdown-menu"
      ],
      "usage": "import { Popover, PopoverContent, PopoverTrigger } from \"@/components/arc/popover/popover\";\nimport { Button } from \"@/components/arc/button/button\";\n\nexport function ShareMenu() {\n  return (\n    <Popover>\n      <PopoverTrigger asChild><Button variant=\"secondary\">Share</Button></PopoverTrigger>\n      <PopoverContent>\n        <p>Anyone with the link can view.</p>\n      </PopoverContent>\n    </Popover>\n  );\n}",
      "exports": [
        "Popover",
        "PopoverTrigger",
        "PopoverContent",
        "PopoverClose"
      ]
    },
    {
      "name": "tooltip",
      "kind": "component",
      "title": "Tooltip",
      "description": "Short supporting text for unfamiliar controls.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "hint",
        "overlay"
      ],
      "docs": "https://uiarc.dev/components/tooltip",
      "markdown": "https://uiarc.dev/components/tooltip/markdown",
      "registry": "https://uiarc.dev/r/tooltip.json",
      "keywords": [
        "react tooltip",
        "radix tooltip",
        "icon button tooltip",
        "hover label",
        "animated tooltip",
        "tooltip component"
      ],
      "whenToUse": [
        "One-line labels on icon-only buttons in toolbars.",
        "Revealing the full text of truncated labels on hover or focus.",
        "Dense toolbars where moving between icons should show labels instantly after the first one."
      ],
      "whenNotToUse": [
        "Use popover for anything with links or controls inside.",
        "Use hover-card for rich previews such as a person or link.",
        "Avoid it for information people must see on touch devices, where hover does not exist."
      ],
      "dependencies": [
        "@radix-ui/react-tooltip",
        "motion"
      ],
      "accessibility": [
        "Radix links the content to the trigger with aria-describedby and renders role=\"tooltip\".",
        "Content is supplemental: icon-only triggers still need their own aria-label.",
        "Do not put interactive elements inside; use popover instead."
      ],
      "motion": [
        "Opens after 250ms with a 3px rise from 0.97 scale; within 300ms of another tooltip it opens instantly with a fade only.",
        "Changing string content rises in with a blur while the bubble springs to the new size.",
        "Reduced motion removes the transform and keeps a 90ms opacity fade."
      ],
      "responsive": [
        "The bubble caps at 15rem and wraps longer text; Radix flips it to the other side near viewport edges.",
        "It opens on hover and keyboard focus, so touch users rarely see it; never hide essential information in one."
      ],
      "performance": [
        "Each tooltip brings its own provider and mounts content in a portal only while open.",
        "A ResizeObserver measures the text only to spring the bubble size when content changes while open."
      ],
      "aiNotes": [
        "Use for one-line labels on icon buttons and truncated text. Use hover-card for rich previews and popover for anything interactive.",
        "Each Tooltip brings its own provider; no app-level wrapper is needed."
      ],
      "related": [
        "popover",
        "hover-card",
        "action-button"
      ],
      "usage": "import { Tooltip } from \"@/components/arc/tooltip/tooltip\";\n\nexport function ArchiveButton() {\n  return (\n    <Tooltip content=\"Archive\">\n      <button type=\"button\" aria-label=\"Archive\">\n        <ArchiveIcon />\n      </button>\n    </Tooltip>\n  );\n}",
      "exports": [
        "Tooltip"
      ]
    },
    {
      "name": "tabs",
      "kind": "component",
      "title": "Tabs",
      "description": "Switch between related content in the same context.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "navigation",
        "layout"
      ],
      "docs": "https://uiarc.dev/components/tabs",
      "markdown": "https://uiarc.dev/components/tabs/markdown",
      "registry": "https://uiarc.dev/r/tabs.json",
      "keywords": [
        "react tabs",
        "animated tabs",
        "radix tabs",
        "tab component",
        "sliding tab indicator",
        "scrollable tabs",
        "tabs with animation"
      ],
      "whenToUse": [
        "Peer views of the same object, such as Overview, Activity, and Settings.",
        "Panels with different heights, where the container should spring between them.",
        "Long tab sets that need to scroll horizontally with edge buttons."
      ],
      "whenNotToUse": [
        "Use segmented-control for a compact value toggle not tied to panels.",
        "Use accordion for stacked sections people read in order.",
        "Use liquid-tab-bar or morph-nav for app-level navigation between routes."
      ],
      "dependencies": [
        "@radix-ui/react-tabs",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Radix provides role=\"tablist\", role=\"tab\", role=\"tabpanel\", aria-selected, and aria-controls.",
        "The outgoing panel is made inert while it fades so it cannot take focus.",
        "Overflow scroll buttons are labelled \"Scroll tabs left\" and \"Scroll tabs right\"; give TabsList an aria-label."
      ],
      "motion": [
        "The selection pill glides between triggers on a morph spring, scoped to one Tabs instance.",
        "Panels slide 8px in the direction of travel while the height springs from the previous panel.",
        "Reduced motion swaps the pill instantly and crossfades panels in place without height animation."
      ],
      "responsive": [
        "The tab list keeps max-width 100% and scrolls horizontally when triggers overflow, with scroll buttons at the edges.",
        "Focusing a tab scrolls it into view, so keyboard navigation works on narrow screens.",
        "Triggers keep a 5.5rem minimum width, so many tabs overflow sooner on mobile."
      ],
      "performance": [
        "ResizeObservers track list overflow and the active panel height; one per Tabs instance is cheap.",
        "The selection pill is a shared layout animation scoped to one Tabs; avoid dozens of tab sets on one screen."
      ],
      "aiNotes": [
        "Use for peer views of the same object. Use segmented-control for a compact value toggle that is not tied to panels, and accordion for stacked sections.",
        "Every TabsTrigger value needs a matching TabsContent. Long lists scroll horizontally on their own."
      ],
      "related": [
        "segmented-control",
        "accordion",
        "liquid-tab-bar",
        "morph-nav"
      ],
      "usage": "import { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/arc/tabs/tabs\";\n\nexport function ProjectTabs() {\n  return (\n    <Tabs defaultValue=\"overview\">\n      <TabsList aria-label=\"Project\">\n        <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n        <TabsTrigger value=\"activity\">Activity</TabsTrigger>\n      </TabsList>\n      <TabsContent value=\"overview\">Summary and stats</TabsContent>\n      <TabsContent value=\"activity\">Recent events</TabsContent>\n    </Tabs>\n  );\n}",
      "exports": [
        "Tabs",
        "TabsList",
        "TabsTrigger",
        "TabsContent"
      ]
    },
    {
      "name": "expandable-card",
      "kind": "component",
      "title": "Expandable card",
      "description": "Give a dense card more room when requested.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "disclosure",
        "layout"
      ],
      "docs": "https://uiarc.dev/components/expandable-card",
      "markdown": "https://uiarc.dev/components/expandable-card/markdown",
      "registry": "https://uiarc.dev/r/expandable-card.json",
      "keywords": [
        "react expandable card",
        "collapsible card",
        "expand card animation",
        "disclosure card",
        "show more card",
        "card with details"
      ],
      "whenToUse": [
        "A single summary card with optional details, like a plan, order, or log entry.",
        "Cards whose summary line updates and should roll changed words in place."
      ],
      "whenNotToUse": [
        "Use accordion for a list of mutually exclusive sections.",
        "Use card when there is nothing to reveal.",
        "Use card-stack for browsing several cards in one spot."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The header is a native button with aria-expanded and aria-controls pointing at the panel.",
        "The rolling description keeps a screen-reader-only copy of the full text; animated words are aria-hidden.",
        "The collapsed panel is inert, so its content is not reachable by keyboard or screen readers while closed."
      ],
      "motion": [
        "Open and close are one morph: width and height move together on the smooth spring, which never overshoots, and the arrow turns on the same spring.",
        "Details fade in once the box has made room and fade out before it shrinks. They are laid out at their open width throughout, so text never reflows or stretches.",
        "Clicking again mid-animation reverses from the current size with its velocity. Changed words in the description rise in; unchanged words hold still.",
        "Reduced motion applies end states instantly and swaps the description without animation."
      ],
      "responsive": [
        "Without width caps the card fills its container. With width and expandedWidth it stays centered and grows sideways to the open cap, never wider than its container, so on a phone it simply uses the full width.",
        "The whole header is the toggle with a 64px minimum height, so it is an easy touch target."
      ],
      "performance": [
        "The panel stays mounted but inert at zero height when collapsed, so a second click mid-animation reverses smoothly.",
        "Width and height are animated for real on one spring rather than scaled, so text and corners never stretch; keep the details short.",
        "The description splits into one span per word for the roll; keep it to a short line."
      ],
      "aiNotes": [
        "Use for a single standalone expandable summary such as a plan, order, or log entry. Use accordion for a list of mutually exclusive sections.",
        "State is internal; there is no controlled prop."
      ],
      "related": [
        "accordion",
        "card",
        "card-stack"
      ],
      "usage": "import { ExpandableCard } from \"@/components/arc/expandable-card/expandable-card\";\n\nexport function PlanDetails() {\n  return (\n    <ExpandableCard title=\"Pro plan\" description=\"Billed yearly, renews March 4\" width={400} expandedWidth={600}>\n      <ul>\n        <li>Unlimited projects</li>\n        <li>Priority support</li>\n      </ul>\n    </ExpandableCard>\n  );\n}",
      "exports": [
        "ExpandableCard"
      ]
    },
    {
      "name": "breadcrumb",
      "kind": "component",
      "title": "Breadcrumb",
      "description": "Show where a page sits in a hierarchy.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "navigation",
        "wayfinding"
      ],
      "docs": "https://uiarc.dev/components/breadcrumb",
      "markdown": "https://uiarc.dev/components/breadcrumb/markdown",
      "registry": "https://uiarc.dev/r/breadcrumb.json",
      "keywords": [
        "react breadcrumb",
        "breadcrumb navigation",
        "animated breadcrumb",
        "page path",
        "nextjs breadcrumb",
        "hierarchy navigation"
      ],
      "whenToUse": [
        "Showing where a page sits in a hierarchy, such as Workspace, Settings, Billing.",
        "Client-side paths like a file browser, where crumbs call onClick instead of navigating.",
        "Paths that grow as people drill in, where new crumbs should slide in."
      ],
      "whenNotToUse": [
        "Use tree-view when people need to browse the whole hierarchy.",
        "Use tabs for switching between sibling views.",
        "Use pagination for moving through pages of a list."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders a nav landmark with an ordered list.",
        "The last item is a span with aria-current=\"page\" and is never a link.",
        "Chevron separators are aria-hidden."
      ],
      "motion": [
        "Crumbs present on first render stay still; new crumbs slide in 8px from the left out of a blur while siblings shift on a smooth spring.",
        "Reduced motion adds and removes crumbs without movement."
      ],
      "responsive": [
        "The list wraps onto new lines on narrow screens; individual crumbs never wrap internally.",
        "It does not truncate or collapse long paths, so keep labels short or shorten the path on mobile yourself."
      ],
      "performance": [
        "Crumbs use layout position animation only when the path changes; static paths do not animate."
      ],
      "aiNotes": [
        "Use for hierarchical page location. For client-side paths such as a file browser, pass onClick without href.",
        "Links are plain anchors by default. For client-side navigation, wrap the app once in ArcProvider from @/components/arc/lib/arc-provider with your router's Link: <ArcProvider link={Link}>.",
        "Keep labels short; the component does not truncate or collapse long paths."
      ],
      "related": [
        "tree-view",
        "pagination",
        "tabs"
      ],
      "usage": "import { Breadcrumb } from \"@/components/arc/breadcrumb/breadcrumb\";\n\nexport function SettingsPath() {\n  return (\n    <Breadcrumb\n      items={[\n        { label: \"Workspace\", href: \"/\" },\n        { label: \"Settings\", href: \"/settings\" },\n        { label: \"Billing\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "Breadcrumb"
      ]
    },
    {
      "name": "alert",
      "kind": "component",
      "title": "Alert",
      "description": "A persistent message that helps people recover or continue.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "status",
        "message"
      ],
      "docs": "https://uiarc.dev/components/alert",
      "markdown": "https://uiarc.dev/components/alert/markdown",
      "registry": "https://uiarc.dev/r/alert.json",
      "keywords": [
        "react alert",
        "alert banner",
        "inline notification",
        "warning message",
        "animated alert",
        "dismissible alert",
        "callout"
      ],
      "whenToUse": [
        "Persistent, in-flow messages about a page or form, such as an expiring card.",
        "Status that changes over time, where one alert should morph between tones.",
        "Dismissible notices that should collapse and close the gap below them."
      ],
      "whenNotToUse": [
        "Use toast or toast-stack for transient results of an action.",
        "Use empty-state when there is no content to show.",
        "Use dialog when people must respond before continuing."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Danger alerts use role=\"alert\" and interrupt; other tones use role=\"status\" and announce politely.",
        "Outgoing copies are aria-hidden while they fade, so the live region reads only the current text.",
        "The dismiss button is labelled \"Dismiss: <title>\"; the tone icon is aria-hidden."
      ],
      "motion": [
        "Presence collapses or expands the height on a smooth spring with a fade, so content below closes the gap.",
        "Tone changes morph the icon through a small scale and blur; copy changes rise in while the height springs to fit.",
        "Reduced motion swaps content with short fades and no height animation."
      ],
      "responsive": [
        "The alert fills its container and the copy wraps; the icon and dismiss button keep fixed sizes.",
        "Hover styles on the dismiss button apply only on hover-capable fine pointers."
      ],
      "performance": [
        "A ResizeObserver lets the height spring when copy changes; keep only a few alerts mounted per page."
      ],
      "aiNotes": [
        "Use for persistent, in-flow messages tied to a page or form. Use toast or toast-stack for transient results of an action.",
        "Keep one Alert mounted and change tone, title, and children to morph between states instead of swapping components."
      ],
      "related": [
        "toast",
        "toast-stack",
        "empty-state"
      ],
      "usage": "import { Alert } from \"@/components/arc/alert/alert\";\n\nexport function BillingNotice() {\n  return (\n    <Alert tone=\"warning\" title=\"Card expires soon\" onDismiss={() => track(\"dismissed\")}>\n      Update your payment method before March 1 to avoid interruption.\n    </Alert>\n  );\n}",
      "exports": [
        "Alert"
      ]
    },
    {
      "name": "toast",
      "kind": "component",
      "title": "Toast",
      "description": "Brief confirmation for a completed background action.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "status",
        "message"
      ],
      "docs": "https://uiarc.dev/components/toast",
      "markdown": "https://uiarc.dev/components/toast/markdown",
      "registry": "https://uiarc.dev/r/toast.json",
      "keywords": [
        "react toast",
        "toast notification",
        "success toast",
        "swipe to dismiss toast",
        "animated toast",
        "snackbar"
      ],
      "whenToUse": [
        "A single success confirmation, like Changes saved, controlled with local state.",
        "Demos or small apps that do not need a toast queue."
      ],
      "whenNotToUse": [
        "Use toast-stack for app-wide notifications with types, actions, updates, and stacking.",
        "Use alert for persistent messages that should stay in the page flow."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders role=\"status\" with aria-live=\"polite\" and aria-atomic, so new content is announced without stealing focus.",
        "The dismiss button is labelled \"Dismiss notification\"; the check icon is aria-hidden.",
        "Positioning is up to the caller; the auto-dismiss timer does not pause on hover, so keep messages short."
      ],
      "motion": [
        "Enters rising 16px from 0.96 scale on a morph spring while the check draws in; a swipe past 80px or 480px/s throws it off with its release velocity.",
        "Closing sinks it 8px and fades. Copy changes rise in while the height springs to fit.",
        "Reduced motion uses opacity fades and disables dragging."
      ],
      "responsive": [
        "The toast is min(100%, 26rem) wide and the title ellipsizes on one line.",
        "Positioning is up to you; fix it to the bottom center on phones so it clears the thumb zone edges.",
        "It swipes horizontally on touch and mouse; drag is disabled under reduced motion."
      ],
      "performance": [
        "Drag and throw run on motion values, and a ResizeObserver springs the height when copy changes.",
        "The auto-dismiss timer does not pause on hover, so keep messages short."
      ],
      "aiNotes": [
        "Use for a single success confirmation you control with local state. Use toast-stack for an app-wide queue with types, actions, updates, and stacking.",
        "Import as a default export. Position it yourself, for example fixed to the bottom of the viewport."
      ],
      "related": [
        "toast-stack",
        "alert"
      ],
      "usage": "import Toast from \"@/components/arc/toast/toast\";\n\nexport function SavedToast() {\n  const [open, setOpen] = useState(false);\n  return (\n    <>\n      <button type=\"button\" onClick={() => setOpen(true)}>Save</button>\n      <Toast open={open} onOpenChange={setOpen} title=\"Changes saved\" description=\"Synced to all devices.\" />\n    </>\n  );\n}",
      "exports": [
        "Toast"
      ]
    },
    {
      "name": "rating",
      "kind": "component",
      "title": "Rating",
      "description": "A simple, accessible way to capture a numeric opinion.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "input",
        "feedback"
      ],
      "docs": "https://uiarc.dev/components/rating",
      "markdown": "https://uiarc.dev/components/rating/markdown",
      "registry": "https://uiarc.dev/r/rating.json",
      "keywords": [
        "react star rating",
        "rating component",
        "half star rating",
        "review stars",
        "accessible rating",
        "5 star rating"
      ],
      "whenToUse": [
        "Collecting a 1 to N score in reviews or feedback forms.",
        "Displaying average scores, with readOnly and allowHalf.",
        "Forms that submit natively, via name."
      ],
      "whenNotToUse": [
        "Use slider for continuous values.",
        "Use reactions for emoji feedback on content.",
        "Use radio-group when options are labelled choices rather than a scale."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Interactive ratings are role=\"radiogroup\" with one role=\"radio\" per step and a roving tab stop on the checked value.",
        "Each radio is labelled like \"Great, 4 of 5 stars\"; commits are announced in a polite live region.",
        "readOnly renders role=\"img\" with an aria-label such as \"Quality: 4 out of 5\"."
      ],
      "motion": [
        "The fill sweeps star by star with a small stagger in the direction the value moved; a commit pops the stars in a wave.",
        "Pressed stars dip to 0.86 scale and the value word rolls in the direction of change.",
        "Reduced motion sets fills instantly and drops the pop, press scale, and word roll."
      ],
      "responsive": [
        "Stars sit in 36px cells for easy touch targets, and the value label wraps under the stars when space runs out.",
        "Touch presses scrub across stars with pointer capture; touch-action pan-y keeps vertical scrolling working."
      ],
      "performance": [
        "Fills and pops are short motion animations per star; fine for a form, but use readOnly for long lists of scores."
      ],
      "aiNotes": [
        "Use for collecting or showing a 1 to N score. Use slider for continuous values and reactions for emoji feedback.",
        "Pass name to submit with a native form. Use readOnly to display averages; combine with allowHalf for fractional values."
      ],
      "related": [
        "slider",
        "reactions",
        "radio-group"
      ],
      "usage": "import { Rating } from \"@/components/arc/rating/rating\";\n\nexport function ReviewForm() {\n  const [stars, setStars] = useState(0);\n  return (\n    <form>\n      <Rating label=\"Overall experience\" value={stars} onChange={setStars} name=\"rating\" />\n    </form>\n  );\n}",
      "exports": [
        "Rating"
      ]
    },
    {
      "name": "progress",
      "kind": "component",
      "title": "Progress",
      "description": "Show how much of a known task is complete.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "status",
        "loading"
      ],
      "docs": "https://uiarc.dev/components/progress",
      "markdown": "https://uiarc.dev/components/progress/markdown",
      "registry": "https://uiarc.dev/r/progress.json",
      "keywords": [
        "react progress bar",
        "animated progress bar",
        "upload progress",
        "loading bar",
        "percentage bar",
        "progress indicator"
      ],
      "whenToUse": [
        "Determinate progress for uploads, imports, or long tasks.",
        "Progress with a visible percentage and a check at completion, via showValue."
      ],
      "whenNotToUse": [
        "Use skeleton while content is loading with no progress to report.",
        "Use usage-meter for quota against a limit, and gauge for dashboard metrics.",
        "Use stepper to show position in a multi-step flow."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders role=\"progressbar\" with aria-valuemin, aria-valuemax, aria-valuenow, and a percentage aria-valuetext.",
        "Label changes crossfade; outgoing copies are aria-hidden while they fade.",
        "It is not a live region; announce completion separately if it matters."
      ],
      "motion": [
        "One smooth spring drives both the fill and the counted number, so they always agree. The fill slides in from the left to keep its rounded end.",
        "At 100% the fill turns to the success colour and a check settles in beside the count.",
        "Reduced motion jumps the fill and count to the new value."
      ],
      "responsive": [
        "The bar fills its container width, so it works in cards, rows, and full-width layouts."
      ],
      "performance": [
        "One spring drives both the fill and the counted number; no observers or timers.",
        "It is not a live region, so announce completion separately if it matters."
      ],
      "aiNotes": [
        "Use for determinate task progress. Use skeleton while content has no progress to report, gauge or activity-rings for dashboard metrics, and usage-meter for quota against a limit.",
        "Pass raw value and max; the percentage is computed and clamped for you."
      ],
      "related": [
        "usage-meter",
        "skeleton",
        "gauge",
        "stepper"
      ],
      "usage": "import { Progress } from \"@/components/arc/progress/progress\";\n\nexport function UploadProgress({ sent, size }: { sent: number; size: number }) {\n  return <Progress label=\"Uploading report.pdf\" value={sent} max={size} showValue />;\n}",
      "exports": [
        "Progress"
      ]
    },
    {
      "name": "skeleton",
      "kind": "component",
      "title": "Skeleton",
      "description": "Reserve space while content is still loading.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "loading",
        "status"
      ],
      "docs": "https://uiarc.dev/components/skeleton",
      "markdown": "https://uiarc.dev/components/skeleton/markdown",
      "registry": "https://uiarc.dev/r/skeleton.json",
      "keywords": [
        "react skeleton",
        "skeleton loader",
        "loading placeholder",
        "content placeholder",
        "shimmer loading",
        "skeleton screen"
      ],
      "whenToUse": [
        "Loading states for content whose shape is known, like a profile or comment.",
        "Swapping a placeholder into real content with a crossfade and height spring."
      ],
      "whenNotToUse": [
        "Use progress when you can report a percentage.",
        "Use empty-state when loading finished and there is nothing to show.",
        "Use text-shimmer for an AI thinking or status line."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The placeholder is role=\"status\" with aria-busy and an aria-label; its shapes are aria-hidden.",
        "With children, the wrapper sets aria-busy while loading."
      ],
      "motion": [
        "Blocks pulse in a staggered wave, each a beat after the one above.",
        "On load the placeholder fades out, content rises 4px into place, and the height springs from placeholder to content.",
        "Reduced motion stops the pulse and swaps with short fades and no height animation."
      ],
      "responsive": [
        "Lines are percentages of the container width, so the placeholder scales with its slot.",
        "It only draws text lines and an avatar; build custom shapes for grids or media."
      ],
      "performance": [
        "The pulse is a CSS animation that stops under reduced motion.",
        "A ResizeObserver springs the height from placeholder to content; avoid hundreds of skeletons at once."
      ],
      "aiNotes": [
        "Use while fetching content whose shape is known. Use progress when you can report a percentage, and empty-state when there is nothing to show.",
        "Wrap the real content as children and drive loading to get the crossfade; without children it renders only the placeholder."
      ],
      "related": [
        "progress",
        "empty-state",
        "card"
      ],
      "usage": "import { Skeleton } from \"@/components/arc/skeleton/skeleton\";\n\nexport function Profile({ user }: { user?: User }) {\n  return (\n    <Skeleton avatar lines={2} loading={!user}>\n      {user && <ProfileCard user={user} />}\n    </Skeleton>\n  );\n}",
      "exports": [
        "Skeleton"
      ]
    },
    {
      "name": "badge",
      "kind": "component",
      "title": "Badge",
      "description": "A small label for status, category, or metadata.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "label",
        "status"
      ],
      "docs": "https://uiarc.dev/components/badge",
      "markdown": "https://uiarc.dev/components/badge/markdown",
      "registry": "https://uiarc.dev/r/badge.json",
      "keywords": [
        "react badge",
        "status badge",
        "status pill",
        "animated badge",
        "tag component",
        "label pill",
        "badge with icon"
      ],
      "whenToUse": [
        "Short statuses next to titles or in table cells, such as Live, Draft, or Failed.",
        "Counts or states that change in place and should morph instead of jump.",
        "Tagging a row with one tone plus an optional icon."
      ],
      "whenNotToUse": [
        "Use alert or toast when the message needs a full sentence.",
        "Use chip-group when people toggle the values.",
        "Use stat-card for a headline number with a trend."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders a plain span, so it is read inline with surrounding text.",
        "The icon is aria-hidden; the label must state the status on its own, not rely on tone color.",
        "Outgoing labels are hidden from assistive tech while they fade, so only the current text is read.",
        "It does not announce changes. Put it inside a live region if a status update must be spoken."
      ],
      "motion": [
        "A new label rises in with a short blur while the old one lifts away, and the pill width springs to fit.",
        "Passive reflows such as font swaps resize instantly; only a content change springs.",
        "Reduced motion swaps the label with a quick fade and snaps the width."
      ],
      "responsive": [
        "The pill sizes to its label and never wraps, so keep labels to a word or two in narrow cells.",
        "Hover styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Each badge has a ResizeObserver for the width spring; fine per row, but avoid thousands in one table.",
        "Font swaps and passive reflows resize instantly; only content changes animate."
      ],
      "aiNotes": [
        "Use for short statuses and counts next to titles or in table cells. Use alert or toast for messages with sentences.",
        "Use dot for live states (online, syncing, down) and an icon when the shape adds meaning; the label is optically centred either way.",
        "Use tone for status and color for categories (GET teal, POST sky, PUT orchid, PATCH violet, DELETE rose); never use a categorical colour to mean success or failure.",
        "Keep the badge mounted and change its children to get the morph; remounting with a new key loses it."
      ],
      "related": [
        "alert",
        "sortable-data-table"
      ],
      "usage": "import { Check } from \"lucide-react\";\nimport { Badge } from \"@/components/arc/badge/badge\";\n\nexport function DeployStatus({ live }: { live: boolean }) {\n  return (\n    <Badge tone={live ? \"success\" : \"neutral\"} icon={live ? <Check size={12} /> : undefined}>\n      {live ? \"Live\" : \"Draft\"}\n    </Badge>\n  );\n}",
      "exports": [
        "Badge"
      ]
    },
    {
      "name": "card",
      "kind": "component",
      "title": "Card",
      "description": "A contained group of related content and actions.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "surface",
        "layout"
      ],
      "docs": "https://uiarc.dev/components/card",
      "markdown": "https://uiarc.dev/components/card/markdown",
      "registry": "https://uiarc.dev/r/card.json",
      "keywords": [
        "react card",
        "card component",
        "card with image",
        "expand card to modal",
        "quick look card",
        "shared layout card",
        "project card"
      ],
      "whenToUse": [
        "Browsable items in a grid, such as projects, listings, or posts.",
        "Items that should open into a larger quick look dialog without leaving the page, via details.",
        "Content with media, an owner byline, and a status line that updates in place."
      ],
      "whenNotToUse": [
        "Use expandable-card for inline disclosure without a dialog.",
        "Use stat-card or metric-card for numbers.",
        "Use dialog when the content has no card to grow from."
      ],
      "dependencies": [
        "motion",
        "@radix-ui/react-dialog",
        "lucide-react"
      ],
      "accessibility": [
        "Renders an article with an h3 title; the quick look uses Radix Dialog with the title and description wired as its label and description.",
        "Only the title becomes a button, so nested actions stay separately focusable.",
        "Status changes are read through a role=\"status\" region.",
        "Give media images alt text or an empty alt when decorative, and an aria-label to icon-only actions."
      ],
      "motion": [
        "Hover lifts the card 2px and slowly zooms the media.",
        "The quick look shares layout ids with the card, so surface, photo, title, and byline travel on one spring; details fade in after.",
        "Reduced motion drops the lift, zoom, and morph, and the dialog simply fades."
      ],
      "responsive": [
        "The card fills its grid cell with min-width 0; the quick look panel is min(30rem, 100vw minus a gutter) wide and capped at the viewport height.",
        "Hover lift and media zoom run only for a mouse; touch and pen never lift."
      ],
      "performance": [
        "The quick look shares layout ids with the card, so the morph animates several elements; the dialog mounts only while open.",
        "The overlay uses a 7px backdrop blur, which can cost frames on low-end devices over busy pages."
      ],
      "aiNotes": [
        "Use for browsable items in a grid: projects, listings, posts. Use expandable-card for inline disclosure without a dialog, and stat-card or metric-card for numbers.",
        "Add details only when there is real extra content; without it the card is a static article.",
        "Keep status short and change it in place to get the rolling update."
      ],
      "related": [
        "expandable-card",
        "dialog",
        "avatar",
        "photo-grid"
      ],
      "usage": "import { Card } from \"@/components/arc/card/card\";\nimport { Avatar } from \"@/components/arc/avatar/avatar\";\n\nexport function ProjectCard() {\n  return (\n    <Card\n      title=\"Harbour redesign\"\n      description=\"New booking flow and room pages.\"\n      media={<img src=\"/harbour.jpg\" alt=\"\" />}\n      avatar={<Avatar name=\"Maya Chen\" size=\"sm\" />}\n      meta=\"Maya Chen\"\n      status=\"Updated 2 hours ago\"\n      details={<p>Scope, milestones, and open questions.</p>}\n    />\n  );\n}",
      "exports": [
        "Card"
      ]
    },
    {
      "name": "metric-card",
      "kind": "component",
      "title": "Metric card",
      "description": "A compact summary for a number that needs context.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "summary"
      ],
      "docs": "https://uiarc.dev/components/metric-card",
      "markdown": "https://uiarc.dev/components/metric-card/markdown",
      "registry": "https://uiarc.dev/r/metric-card.json",
      "keywords": [
        "react metric card",
        "kpi card",
        "odometer number",
        "rolling number card",
        "dashboard metric",
        "stat tile"
      ],
      "whenToUse": [
        "A single plain number with odometer style rolling digits, such as uptime or orders.",
        "Numbers that update live and should roll in the direction they moved."
      ],
      "whenNotToUse": [
        "Use stat-card for preformatted strings, trend arrows, tones, and icons.",
        "Use sparkline when the trend over time matters more than the latest value.",
        "Use animated-counter for a number without a card around it."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders an article; the number is read as plain text through a visually hidden copy.",
        "The change chip is plain text, so write the direction into it (+ or -) rather than relying on color.",
        "It does not announce updates; wrap it in a live region if values change while people watch."
      ],
      "motion": [
        "The value uses animated-counter: digits roll on view and turn the way the number moved.",
        "Label, context, and change copy roll in from the direction the number moved, and the chip width springs.",
        "Reduced motion jumps the digits and swaps copy with a quick fade."
      ],
      "responsive": [
        "Below 380px the padding tightens and the change chip wraps under the label instead of crowding it.",
        "Copy lines stay on one line and clip, so keep label and context short."
      ],
      "performance": [
        "Digits roll with animated-counter and a ResizeObserver drives the chip width spring; a dashboard row of cards is fine."
      ],
      "aiNotes": [
        "Use when the value is a plain number and you want the rolling digits. Use stat-card for preformatted strings, trend arrows, tones, and icons.",
        "value must be a number; put units in suffix and formatting-free context in context."
      ],
      "related": [
        "animated-counter",
        "sparkline",
        "gauge"
      ],
      "usage": "import { MetricCard } from \"@/components/arc/metric-card/metric-card\";\n\nexport function Uptime() {\n  return <MetricCard label=\"Uptime\" value={99.9} suffix=\"%\" context=\"Last 30 days\" change=\"+0.2%\" />;\n}",
      "exports": [
        "MetricCard"
      ]
    },
    {
      "name": "empty-state",
      "kind": "component",
      "title": "Empty state",
      "description": "A useful next step when there is nothing to show yet.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "guidance"
      ],
      "docs": "https://uiarc.dev/components/empty-state",
      "markdown": "https://uiarc.dev/components/empty-state/markdown",
      "registry": "https://uiarc.dev/r/empty-state.json",
      "keywords": [
        "react empty state",
        "no results",
        "empty list placeholder",
        "zero state",
        "blank slate",
        "first run empty view"
      ],
      "whenToUse": [
        "Empty lists, zero search results, and first-run views.",
        "A view that should morph between states, such as empty and success, by changing props."
      ],
      "whenNotToUse": [
        "Use skeleton while data is still loading.",
        "Use alert for errors inside a page that still has content.",
        "Use onboarding-checklist when first-run needs several steps."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders a section with an h3 title; pass label to name the region.",
        "The icon is aria-hidden.",
        "Outgoing copy is hidden from assistive tech while it fades."
      ],
      "motion": [
        "Changing title or description rolls the copy in place while the block height springs to fit.",
        "A new icon pops in with a short blur.",
        "Reduced motion swaps copy with a fade and snaps height; the icon's idle animation stops."
      ],
      "responsive": [
        "Padding scales with the viewport between fixed bounds, and the description caps at 18rem so lines stay short.",
        "Actions wrap and center, so two buttons stack on narrow screens."
      ],
      "performance": [
        "One ResizeObserver drives the height spring; the icon's idle animation is CSS and stops under reduced motion."
      ],
      "aiNotes": [
        "Use for empty lists, zero search results, and first-run views. Use skeleton while data is loading.",
        "Keep it mounted and change its props to morph between states such as empty and success."
      ],
      "related": [
        "skeleton",
        "alert",
        "onboarding-checklist",
        "sortable-data-table"
      ],
      "usage": "import { Search } from \"lucide-react\";\nimport { Button } from \"@/components/arc/button/button\";\nimport { EmptyState } from \"@/components/arc/empty-state/empty-state\";\n\nexport function NoResults({ onClear }: { onClear: () => void }) {\n  return (\n    <EmptyState\n      icon={<Search size={24} />}\n      title=\"No matches\"\n      description=\"Try a shorter search or clear the filters.\"\n      action={<Button variant=\"secondary\" onClick={onClear}>Clear filters</Button>}\n    />\n  );\n}",
      "exports": [
        "EmptyState"
      ]
    },
    {
      "name": "tree-view",
      "kind": "component",
      "title": "Tree view",
      "description": "Navigate nested folders and structured content.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "navigation"
      ],
      "docs": "https://uiarc.dev/components/tree-view",
      "markdown": "https://uiarc.dev/components/tree-view/markdown",
      "registry": "https://uiarc.dev/r/tree-view.json",
      "keywords": [
        "react tree view",
        "file tree",
        "file explorer component",
        "nested tree",
        "folder tree",
        "treeview keyboard navigation"
      ],
      "whenToUse": [
        "Files, folders, and nested categories people expand and select.",
        "Side panels next to code-block or a detail view where selection drives content.",
        "Hierarchies that need full keyboard navigation with arrow keys."
      ],
      "whenNotToUse": [
        "Use accordion for a few collapsible sections of content.",
        "Use breadcrumb to show the path to the current page."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Uses role=\"tree\" and role=\"treeitem\" with aria-level, aria-posinset, aria-setsize, aria-expanded, and aria-selected.",
        "Roving tabindex keeps one row in the tab order.",
        "Icons, chevrons, and branch lines are aria-hidden."
      ],
      "motion": [
        "Opening a folder grows the space and staggers its children in; closing collapses rows smoothly.",
        "The selection highlight glides between rows and the chevron rotates on a snappy spring.",
        "Reduced motion shows and hides rows instantly."
      ],
      "responsive": [
        "Rows are a fixed 36px and labels ellipsize, so deep paths clip rather than overflow on narrow panels.",
        "Each level indents 18px, so very deep trees lose label space on mobile.",
        "Hover highlights apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Every visible row renders with position layout animation; nodes are not virtualized, so keep open folders reasonable.",
        "Closed folders do not render their children."
      ],
      "aiNotes": [
        "Use for hierarchical data such as files or nested categories. Use accordion for a few collapsible sections of content.",
        "IDs must be unique across the whole tree. Control expandedIds when the open set must persist or sync with routing."
      ],
      "related": [
        "accordion",
        "code-block",
        "breadcrumb"
      ],
      "usage": "import { TreeView, type TreeNode } from \"@/components/arc/tree-view/tree-view\";\n\nconst files: TreeNode[] = [\n  { id: \"app\", label: \"app\", children: [\n    { id: \"page\", label: \"page.tsx\" },\n    { id: \"layout\", label: \"layout.tsx\" },\n  ] },\n  { id: \"readme\", label: \"README.md\" },\n];\n\nexport function Files() {\n  return <TreeView nodes={files} defaultExpandedIds={[\"app\"]} onSelect={node => open(node.id)} />;\n}",
      "exports": [
        "TreeView"
      ]
    },
    {
      "name": "pagination",
      "kind": "component",
      "title": "Pagination",
      "description": "Move through a long collection with clear bounds.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "navigation"
      ],
      "docs": "https://uiarc.dev/components/pagination",
      "markdown": "https://uiarc.dev/components/pagination/markdown",
      "registry": "https://uiarc.dev/r/pagination.json",
      "keywords": [
        "react pagination",
        "pagination component",
        "page numbers",
        "table pagination",
        "animated pagination",
        "next previous pages"
      ],
      "whenToUse": [
        "Paged server results and long tables.",
        "Lists where the page should live in the URL or state and fetch on change."
      ],
      "whenNotToUse": [
        "Use carousel for browsing slides.",
        "Use timeline with maxHeight for a scrolling feed instead of pages."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders a nav landmark with the label as its name.",
        "Page buttons are labelled \"Page 3\" and the current one has aria-current=\"page\".",
        "Previous and next are disabled at the ends and carry aria-labels."
      ],
      "motion": [
        "The current page mark springs to the selected button.",
        "When the window shifts, numbers slide like a belt by the number of slots moved.",
        "Reduced motion jumps the mark and swaps numbers without sliding."
      ],
      "responsive": [
        "It always shows a five page window plus arrows, so it stays the same width on any screen.",
        "The row wraps if its container is narrower than the buttons, and the current mark re-measures on resize."
      ],
      "performance": [
        "Only five page buttons render regardless of pageCount; one ResizeObserver repositions the current mark."
      ],
      "aiNotes": [
        "Use for paged server results and long tables. Use carousel for browsing slides.",
        "It is fully controlled: keep page in state or the URL and fetch on onPageChange."
      ],
      "related": [
        "sortable-data-table",
        "filter-toolbar",
        "carousel"
      ],
      "usage": "import { useState } from \"react\";\nimport { Pagination } from \"@/components/arc/pagination/pagination\";\n\nexport function Results() {\n  const [page, setPage] = useState(1);\n  return <Pagination page={page} pageCount={12} onPageChange={setPage} />;\n}",
      "exports": [
        "Pagination"
      ]
    },
    {
      "name": "filter-toolbar",
      "kind": "component",
      "title": "Filter toolbar",
      "description": "Keep collection filters close and easy to reset.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "filter"
      ],
      "docs": "https://uiarc.dev/components/filter-toolbar",
      "markdown": "https://uiarc.dev/components/filter-toolbar/markdown",
      "registry": "https://uiarc.dev/r/filter-toolbar.json",
      "keywords": [
        "react filter toolbar",
        "filter chips",
        "add filter menu",
        "faceted filters",
        "filter bar",
        "removable chips",
        "table filters"
      ],
      "whenToUse": [
        "Faceted filtering above tables and lists, with removable chips.",
        "Adding filters through a two step field then value menu.",
        "Views that pair with sortable-data-table and pagination for a full list page."
      ],
      "whenNotToUse": [
        "Use chip-group for a fixed set of toggles.",
        "Use search-field for free text search.",
        "Use multi-select when one field takes several values in a form."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The toolbar is role=\"group\"; the menu panel is a labelled role=\"dialog\" holding a role=\"menu\" of checkable items.",
        "Remove buttons are labelled like \"Remove Status: Open\", and removing a chip moves focus to its neighbor or the Add filter trigger.",
        "Additions and removals are announced through a role=\"status\" region."
      ],
      "motion": [
        "Chips open and close their slot on a spring so neighbors glide; values morph in place.",
        "The Add filter trigger morphs into the menu surface and steps slide between fields and values.",
        "Reduced motion replaces the morphs and slides with instant changes and short fades."
      ],
      "responsive": [
        "Below 520px the toolbar stacks into a column with actions aligned to the end.",
        "The menu panel is min(16rem, 100vw minus a gutter) wide, flips its edge when there is no room, and its list caps at min(20rem, 55vh).",
        "Hover highlights apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Several ResizeObservers measure chips and the menu for morphs; menu options are not virtualized, so keep value lists short."
      ],
      "aiNotes": [
        "Use above tables and lists for faceted filtering. Use chip-group for a fixed set of toggles and search-field for free text.",
        "Keep one chip per field: onAdd receives a chip keyed by field id, so replace any existing chip with that id.",
        "Pair with sortable-data-table and pagination for a full list view."
      ],
      "related": [
        "sortable-data-table",
        "chip-group",
        "search-field",
        "multi-select",
        "pagination"
      ],
      "usage": "import { useState } from \"react\";\nimport { FilterToolbar, type FilterChip } from \"@/components/arc/filter-toolbar/filter-toolbar\";\n\nconst fields = [\n  { id: \"status\", label: \"Status\", options: [\"Open\", \"Closed\"] },\n  { id: \"owner\", label: \"Owner\", options: [\"Maya\", \"Leo\"] },\n];\n\nexport function IssueFilters() {\n  const [filters, setFilters] = useState<FilterChip[]>([]);\n  return (\n    <FilterToolbar\n      filters={filters}\n      onRemove={id => setFilters(f => f.filter(x => x.id !== id))}\n      onClearAll={() => setFilters([])}\n      addFilter={{ fields, onAdd: chip => setFilters(f => [...f.filter(x => x.id !== chip.id), chip]) }}\n    />\n  );\n}",
      "exports": [
        "FilterToolbar",
        "FilterMenu"
      ]
    },
    {
      "name": "sortable-data-table",
      "kind": "component",
      "title": "Sortable data table",
      "description": "Compare structured records with sortable columns.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "table"
      ],
      "docs": "https://uiarc.dev/components/sortable-data-table",
      "markdown": "https://uiarc.dev/components/sortable-data-table/markdown",
      "registry": "https://uiarc.dev/r/sortable-data-table.json",
      "keywords": [
        "react data table",
        "sortable table",
        "table with row selection",
        "animated table sort",
        "responsive table",
        "checkbox table"
      ],
      "whenToUse": [
        "Tabular records people sort and select, such as projects, invoices, or users.",
        "Tables that need Shift-click range selection and a count line with Clear."
      ],
      "whenNotToUse": [
        "Use data-grid when people edit cells like a spreadsheet.",
        "Use timeline for chronological activity.",
        "Use reorderable-list when people set the order by hand."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A native table with caption, scoped headers, and aria-sort on sortable columns.",
        "Sort buttons are labelled like \"Sort by Budget, currently ascending\"; the select all checkbox shows a mixed state.",
        "Sort and selection changes are announced through a role=\"status\" region."
      ],
      "motion": [
        "Rows glide to their new positions on a spring when the sort changes; the sort arrow flips.",
        "The selection count rolls and Clear fades in with a short blur.",
        "Reduced motion reorders instantly and fades the count."
      ],
      "responsive": [
        "Wider layouts scroll horizontally inside the table instead of the page.",
        "Below 620px each row folds into two lines, and the header becomes a scrolling strip of sort buttons with Select all pinned.",
        "Hover row fills apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Rows are not virtualized and each uses position layout animation for re-sorts; paginate long lists with pagination.",
        "Sorting runs client side over the rows you pass."
      ],
      "aiNotes": [
        "Use for tabular records people sort and select. Use timeline for chronological activity and a plain list for simple items.",
        "Sorting is client side over the given rows; for server sorting, control defaultSort per fetch and page with pagination.",
        "Use render for badges, avatars, or formatted numbers inside cells."
      ],
      "related": [
        "filter-toolbar",
        "pagination",
        "badge",
        "empty-state",
        "checkbox"
      ],
      "usage": "import { SortableDataTable } from \"@/components/arc/sortable-data-table/sortable-data-table\";\n\nconst projects = [\n  { id: \"p1\", name: \"Harbour\", owner: \"Maya\", budget: 42000 },\n  { id: \"p2\", name: \"Atlas\", owner: \"Leo\", budget: 18500 },\n];\n\nexport function Projects() {\n  return (\n    <SortableDataTable\n      rows={projects}\n      rowKey=\"id\"\n      caption=\"Projects\"\n      columns={[{ key: \"name\", label: \"Name\" }, { key: \"owner\", label: \"Owner\" }, { key: \"budget\", label: \"Budget\" }]}\n      defaultSort={{ key: \"name\", direction: \"asc\" }}\n      selectable\n      itemName={{ one: \"project\", other: \"projects\" }}\n    />\n  );\n}",
      "exports": [
        "SortableDataTable"
      ]
    },
    {
      "name": "sparkline",
      "kind": "component",
      "title": "Sparkline",
      "description": "Show a compact trend beside a value.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart"
      ],
      "docs": "https://uiarc.dev/components/sparkline",
      "markdown": "https://uiarc.dev/components/sparkline/markdown",
      "registry": "https://uiarc.dev/r/sparkline.json",
      "keywords": [
        "react sparkline",
        "mini line chart",
        "trend line",
        "inline chart",
        "scrubbable chart",
        "svg sparkline"
      ],
      "whenToUse": [
        "A compact trend inside a card, table row, or KPI tile.",
        "Small charts people can scrub by pointer or keyboard to read past values."
      ],
      "whenNotToUse": [
        "Use bar-chart for comparing discrete periods with axes.",
        "Use stat-card or metric-card when only the latest value matters."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The figure has an aria-label describing the series; the SVG is hidden.",
        "When interactive, the plot is a focusable role=\"slider\" whose aria-valuetext reads the point label and value.",
        "Pass labels so scrubbed points read as dates rather than positions."
      ],
      "motion": [
        "The line draws in on first view and the end dot appears as it lands.",
        "While scrubbing, the cursor springs between points, later points dim, and headline copy rolls.",
        "Reduced motion shows the full line at once and moves the cursor without springing."
      ],
      "responsive": [
        "The plot measures its width with a ResizeObserver and redraws to fill its container.",
        "Scrubbing uses touch-action pan-y, so a horizontal drag scrubs while vertical swipes still scroll the page.",
        "On touch the readout returns to the latest value when the finger lifts."
      ],
      "performance": [
        "Each chart has its own ResizeObservers and an in-view draw; set interactive to false in dense tables to drop tab stops and handlers."
      ],
      "aiNotes": [
        "Use for a compact trend inside a card or table row. Use bar-chart for comparing discrete periods with axes.",
        "Pass value as the formatted latest reading; formatValue should match it so scrubbed values look consistent.",
        "Set interactive to false in dense tables to avoid many tab stops."
      ],
      "related": [
        "bar-chart",
        "metric-card",
        "gauge"
      ],
      "usage": "import { Sparkline } from \"@/components/arc/sparkline/sparkline\";\n\nexport function Signups() {\n  return (\n    <Sparkline\n      label=\"Signups\"\n      data={[12, 18, 15, 22, 30, 27, 34]}\n      labels={[\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]}\n      value=\"34\"\n      change=\"+26%\"\n      tone=\"success\"\n    />\n  );\n}",
      "exports": [
        "Sparkline"
      ]
    },
    {
      "name": "gauge",
      "kind": "component",
      "title": "Gauge",
      "description": "Show a value against a known range.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart"
      ],
      "docs": "https://uiarc.dev/components/gauge",
      "markdown": "https://uiarc.dev/components/gauge/markdown",
      "registry": "https://uiarc.dev/r/gauge.json",
      "keywords": [
        "react gauge",
        "gauge chart",
        "radial meter",
        "progress ring",
        "threshold gauge",
        "dashboard gauge"
      ],
      "whenToUse": [
        "One value against a known range, such as disk quota or health score.",
        "Values with labelled bands, like Healthy, Filling up, and Critical."
      ],
      "whenNotToUse": [
        "Use progress for task completion.",
        "Use usage-meter for plan limits and activity-rings for several goals at once."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The ring is role=\"meter\" with aria-valuemin, aria-valuemax, aria-valuenow, and an aria-valuetext including the band label.",
        "Threshold labels name the state in text, so it never rests on color alone.",
        "The SVG and animated readout are aria-hidden."
      ],
      "motion": [
        "The ring fills once when half in view without overshoot, then springs to new values while the percent counts.",
        "The band color and label change exactly as the count crosses a threshold.",
        "Reduced motion lands on the value at once."
      ],
      "responsive": [
        "The ring is min(100%, 176px) wide and keeps its aspect ratio, so it shrinks in narrow columns."
      ],
      "performance": [
        "It fills once when half in view, then springs to new values; it is one SVG arc with no running loop."
      ],
      "aiNotes": [
        "Use for one value against a known range, such as quota or health. Use progress for task completion and usage-meter for plan limits.",
        "Always add thresholds with labels when tone carries meaning."
      ],
      "related": [
        "progress",
        "usage-meter",
        "activity-rings"
      ],
      "usage": "import { Gauge } from \"@/components/arc/gauge/gauge\";\n\nexport function DiskUsage() {\n  return (\n    <Gauge\n      label=\"Disk usage\"\n      value={72}\n      detail=\"360 of 500 GB\"\n      thresholds={[\n        { from: 0, tone: \"success\", label: \"Healthy\" },\n        { from: 70, tone: \"warning\", label: \"Filling up\" },\n        { from: 90, tone: \"danger\", label: \"Critical\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "Gauge"
      ]
    },
    {
      "name": "animated-counter",
      "kind": "component",
      "title": "Animated counter",
      "description": "Give changing totals a clear sense of movement.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/animated-counter",
      "markdown": "https://uiarc.dev/components/animated-counter/markdown",
      "registry": "https://uiarc.dev/r/animated-counter.json",
      "keywords": [
        "react animated counter",
        "number ticker",
        "odometer counter",
        "rolling digits",
        "count up animation",
        "animated number"
      ],
      "whenToUse": [
        "Standalone numbers that change, such as totals, prices, or live counts.",
        "Hero stats that roll up from zero when they scroll into view, via animateOnView."
      ],
      "whenNotToUse": [
        "Use metric-card or stat-card when the number needs a card around it.",
        "Use text-morph for words rather than numbers."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "A visually hidden copy holds the full formatted text; the rolling digits are aria-hidden.",
        "It does not announce changes; wrap it in a live region if updates must be spoken."
      ],
      "motion": [
        "Each digit column turns in the direction the whole number moved, wrapping 9 to 0, with a slight stagger on first reveal.",
        "Columns and separators slide in or out when the digit count changes.",
        "Reduced motion jumps digits into place."
      ],
      "responsive": [
        "It sizes to its digits, and columns slide in or out when the digit count changes, so neighbours shift slightly."
      ],
      "performance": [
        "Each digit is its own motion column; fine for a few counters, not for every cell of a table.",
        "The first reveal waits for 60% visibility through useInView."
      ],
      "aiNotes": [
        "Use for standalone numbers that change: totals, prices, counts. Use metric-card or stat-card when the number needs a card around it.",
        "Pass a raw number and let decimals, prefix, and suffix format it; do not pass preformatted strings."
      ],
      "related": [
        "metric-card",
        "text-morph"
      ],
      "usage": "import { AnimatedCounter } from \"@/components/arc/animated-counter/animated-counter\";\n\nexport function Raised({ amount }: { amount: number }) {\n  return <AnimatedCounter label=\"Raised\" value={amount} prefix=\"$\" animateOnView />;\n}",
      "exports": [
        "AnimatedCounter"
      ]
    },
    {
      "name": "code-block",
      "kind": "component",
      "title": "Code block",
      "description": "Present code with legible hierarchy and copy access.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "content",
        "developer"
      ],
      "docs": "https://uiarc.dev/components/code-block",
      "markdown": "https://uiarc.dev/components/code-block/markdown",
      "registry": "https://uiarc.dev/r/code-block.json",
      "keywords": [
        "react code block",
        "syntax highlighting",
        "code snippet component",
        "copy code block",
        "collapsible code",
        "code viewer"
      ],
      "whenToUse": [
        "Documentation snippets and install commands with a copy button.",
        "Long files that should collapse to a few lines with Show all, via maxLines."
      ],
      "whenNotToUse": [
        "Use copy-button alone for a one-line value such as an API key.",
        "Use text-stream for code arriving from an AI response in progress.",
        "Use a full highlighter library when you need many languages."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The block is a section labelled like \"hello.ts source code\"; the pre is focusable so keyboard users can scroll it.",
        "The expand toggle uses aria-expanded and aria-controls pointing at the source.",
        "Copy feedback comes from copy-button."
      ],
      "motion": [
        "Expanding or collapsing springs the source height; new code fades up in place.",
        "The toggle label rolls and its chevron rotates.",
        "Reduced motion snaps height and swaps text without movement."
      ],
      "responsive": [
        "Long lines scroll horizontally inside the block, never the page.",
        "Below 420px the padding and font shrink and the copy button becomes icon-only."
      ],
      "performance": [
        "The built-in highlighter is a small regex pass with no language bundles.",
        "ResizeObservers measure the source for the height spring; many blocks on one page each run their own."
      ],
      "aiNotes": [
        "Use for documentation snippets and install commands. The highlighter is lightweight; use a full highlighter for many languages.",
        "Set maxLines for long files so pages stay scannable."
      ],
      "related": [
        "copy-button",
        "tree-view"
      ],
      "usage": "import { CodeBlock } from \"@/components/arc/code-block/code-block\";\n\nconst snippet = `export function hello() {\n  return \"world\";\n}`;\n\nexport function Example() {\n  return <CodeBlock code={snippet} filename=\"hello.ts\" language=\"ts\" maxLines={12} />;\n}",
      "exports": [
        "CodeBlock"
      ]
    },
    {
      "name": "reorderable-list",
      "kind": "component",
      "title": "Reorderable list",
      "description": "Organize items with direct drag and drop.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "interaction"
      ],
      "docs": "https://uiarc.dev/components/reorderable-list",
      "markdown": "https://uiarc.dev/components/reorderable-list/markdown",
      "registry": "https://uiarc.dev/r/reorderable-list.json",
      "keywords": [
        "react reorderable list",
        "drag and drop list",
        "sortable list",
        "drag to reorder",
        "keyboard reorder",
        "framer motion reorder"
      ],
      "whenToUse": [
        "Short user-ordered lists such as priorities or a playlist.",
        "Lists that must be reorderable by keyboard as well as by drag."
      ],
      "whenNotToUse": [
        "Use sortable-data-table when order comes from a column.",
        "Use swipe-actions when rows need quick actions rather than reordering."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each handle is a button labelled like \"Reorder Ship pricing page. Position 1 of 3. Use arrow keys.\"",
        "Focus stays on the moved item's handle after a keyboard move.",
        "Only the handle starts a drag, so text in rows stays selectable."
      ],
      "motion": [
        "Rows lift slightly on press and neighbors slide on a spring while dragging.",
        "The position number rolls in the direction the row moved.",
        "Reduced motion keeps pointer following but lands rows instantly without lift."
      ],
      "responsive": [
        "Only the 36px grip starts a drag and it sets touch-action none, so the rest of the row scrolls the page normally on touch.",
        "Rows fill the width and keep a 56px minimum height."
      ],
      "performance": [
        "Built on motion Reorder with layout animation per row; not virtualized, so keep lists short."
      ],
      "aiNotes": [
        "Use for short, user-ordered lists such as priorities or playlist order. Use sortable-data-table when order comes from a column.",
        "It is controlled: store items in state and persist the order from onReorder."
      ],
      "related": [
        "sortable-data-table",
        "swipe-actions",
        "onboarding-checklist"
      ],
      "usage": "import { useState } from \"react\";\nimport { ReorderableList } from \"@/components/arc/reorderable-list/reorderable-list\";\n\nexport function Priorities() {\n  const [items, setItems] = useState([\n    { id: \"a\", title: \"Ship pricing page\", detail: \"Due Friday\" },\n    { id: \"b\", title: \"Fix login redirect\" },\n    { id: \"c\", title: \"Write changelog\" },\n  ]);\n  return <ReorderableList items={items} onReorder={setItems} label=\"Priorities\" />;\n}",
      "exports": [
        "ReorderableList"
      ]
    },
    {
      "name": "text-reveal",
      "kind": "component",
      "title": "Text reveal",
      "description": "Reveal a short piece of content with restrained motion.",
      "tier": "free",
      "category": "Text",
      "tags": [
        "text",
        "motion",
        "content"
      ],
      "docs": "https://uiarc.dev/components/text-reveal",
      "markdown": "https://uiarc.dev/components/text-reveal/markdown",
      "registry": "https://uiarc.dev/r/text-reveal.json",
      "keywords": [
        "react text reveal",
        "headline animation",
        "hero text animation",
        "word by word reveal",
        "blur text reveal",
        "css text animation"
      ],
      "whenToUse": [
        "Above-the-fold hero headlines that animate on page load.",
        "Landing page intros where text must appear on first paint even if scripts are slow."
      ],
      "whenNotToUse": [
        "Use in-view-title for section titles further down the page.",
        "Use scroll-highlight for a key paragraph that reveals with scroll.",
        "Use word-rotate when one word in the headline should cycle."
      ],
      "accessibility": [
        "A heading carries the full text as its aria-label and the animated words are aria-hidden, so screen readers and crawlers read one clean sentence. A p keeps a visually hidden copy instead, since a paragraph cannot take a label.",
        "Line breaks are read as spaces.",
        "Pick the heading level with as so the page outline stays correct."
      ],
      "motion": [
        "Words rise out of a clip and sharpen from blur on a per-word stagger; total stagger is capped so long text never drags.",
        "The entrance is pure CSS, so text is never left hidden when scripts load slowly.",
        "Reduced motion drops the clip and rise and fades the words in quickly."
      ],
      "responsive": [
        "The element caps at 24ch with balanced wrapping, so lines stay even on any width.",
        "Use \\n only for breaks that work on every width, since forced breaks apply on mobile too."
      ],
      "performance": [
        "Pure CSS keyframes, no JavaScript animation or observers.",
        "Total stagger is capped, so long text does not lengthen the entrance; each word still gets its own blur filter."
      ],
      "aiNotes": [
        "Use for above-the-fold headlines that should animate on page load. For titles further down the page use in-view-title; for body copy worth slowing down on use scroll-highlight.",
        "It plays once per mount. Change the element's key to replay it."
      ],
      "related": [
        "in-view-title",
        "text-morph"
      ],
      "usage": "import { TextReveal } from \"@/components/arc/text-reveal/text-reveal\";\n\nexport function Hero() {\n  return <TextReveal as=\"h1\" text={\"Ship interfaces\\nthat feel precise\"} delay={0.1} />;\n}",
      "exports": [
        "TextReveal"
      ]
    },
    {
      "name": "in-view-title",
      "kind": "component",
      "title": "In-view title",
      "description": "Bring a section title in as it scrolls into view.",
      "tier": "free",
      "category": "Text",
      "tags": [
        "text",
        "motion",
        "heading"
      ],
      "docs": "https://uiarc.dev/components/in-view-title",
      "markdown": "https://uiarc.dev/components/in-view-title/markdown",
      "registry": "https://uiarc.dev/r/in-view-title.json",
      "keywords": [
        "react scroll animation title",
        "animate on scroll heading",
        "in view text animation",
        "section title reveal",
        "scroll reveal text",
        "framer motion heading"
      ],
      "whenToUse": [
        "Section headings that should animate once they scroll into view.",
        "Feature pages where each section title needs a consistent entrance style.",
        "Headings that should replay on each entry, via once={false}."
      ],
      "whenNotToUse": [
        "Use text-reveal for hero copy that animates on load.",
        "Use scroll-highlight for a paragraph that follows scroll progress.",
        "Use text-shimmer for status lines, not headings."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The heading carries aria-label with the full text; the animated parts are aria-hidden.",
        "Titles already scrolled past show immediately, and server-rendered titles appear on their own if scripts are slow.",
        "Choose as to match the page's heading outline."
      ],
      "motion": [
        "Triggers when 45% of the title is visible. Opacity leads, blur clears next, and the transform settles last, so text is readable before it fully arrives.",
        "Hiding (with once={false}) is a quick fade with no stagger.",
        "Reduced motion shows the title instantly with no transform, blur, or mask."
      ],
      "responsive": [
        "Word, blur, and tracking variants wrap naturally at any width.",
        "The line variant uses your explicit lines, which do not rewrap, so keep each line short enough for mobile."
      ],
      "performance": [
        "One IntersectionObserver per title via useInView, triggering at 45% visibility.",
        "Each word or line is a motion span with blur and transform; fine for headings, not for long body text."
      ],
      "aiNotes": [
        "Default choice for section headings that should animate on scroll. Use text-reveal for on-load hero copy and scroll-highlight for a key paragraph.",
        "Pass lines only with variant=\"line\"; the other variants split on words."
      ],
      "related": [
        "text-reveal",
        "text-shimmer"
      ],
      "usage": "import { InViewTitle } from \"@/components/arc/in-view-title/in-view-title\";\n\nexport function FeaturesHeader() {\n  return (\n    <InViewTitle\n      variant=\"line\"\n      text=\"Everything your team needs to ship\"\n      lines={[\"Everything your team\", \"needs to ship\"]}\n    />\n  );\n}",
      "exports": [
        "InViewTitle"
      ]
    },
    {
      "name": "text-morph",
      "kind": "component",
      "title": "Text morph",
      "description": "Morph a label into its next state, letter by letter.",
      "tier": "free",
      "category": "Text",
      "tags": [
        "text",
        "motion",
        "status"
      ],
      "docs": "https://uiarc.dev/components/text-morph",
      "markdown": "https://uiarc.dev/components/text-morph/markdown",
      "registry": "https://uiarc.dev/r/text-morph.json",
      "keywords": [
        "react text morph",
        "morphing text",
        "animated label",
        "text transition",
        "letter morph animation",
        "shared letter animation"
      ],
      "whenToUse": [
        "Status words that change a few letters at a time, such as Follow and Following.",
        "Custom buttons or badges that need a morphing label without the rest of the button.",
        "Inline counters or short state labels that should glide instead of jump."
      ],
      "whenNotToUse": [
        "Use word-rotate for a sentence with one cycling word.",
        "Use button or action-swap when you want the morph plus button behaviour.",
        "Avoid it for text that wraps, since it always renders a single line."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The plain label is in a visually hidden span; the animated glyphs are aria-hidden.",
        "It does not announce changes. Wrap it in your own aria-live region if a status change must be announced."
      ],
      "motion": [
        "Letters are matched by character and occurrence, so shared letters glide to their new positions on a spring while new letters stagger in with a blur.",
        "The frame width springs to the new label; font loading or resizes follow immediately without animating.",
        "Reduced motion swaps the label and width instantly."
      ],
      "responsive": [
        "The frame width springs to each label, and a ResizeObserver follows font loading and container resizes without animating.",
        "The label never wraps, so keep it short in narrow layouts."
      ],
      "performance": [
        "Every character is a motion span with layout animation; keep labels to a few words."
      ],
      "aiNotes": [
        "Use for status words that change a few letters at a time (Follow / Following, Save / Saved). For a sentence with a cycling word use word-rotate.",
        "Keep labels to one short line; it never wraps."
      ],
      "related": [
        "button",
        "action-swap",
        "text-shimmer"
      ],
      "usage": "import { TextMorph } from \"@/components/arc/text-morph/text-morph\";\n\nexport function PublishButton({ state }: { state: \"idle\" | \"busy\" | \"done\" }) {\n  const label = state === \"idle\" ? \"Publish\" : state === \"busy\" ? \"Publishing\" : \"Published\";\n  return (\n    <button type=\"button\">\n      <TextMorph>{label}</TextMorph>\n    </button>\n  );\n}",
      "exports": [
        "TextMorph"
      ]
    },
    {
      "name": "text-shimmer",
      "kind": "component",
      "title": "Text shimmer",
      "description": "Show ongoing work with a calm light across the words.",
      "tier": "free",
      "category": "Text",
      "tags": [
        "text",
        "motion",
        "loading"
      ],
      "docs": "https://uiarc.dev/components/text-shimmer",
      "markdown": "https://uiarc.dev/components/text-shimmer/markdown",
      "registry": "https://uiarc.dev/r/text-shimmer.json",
      "keywords": [
        "react text shimmer",
        "shimmer text",
        "ai thinking text",
        "loading text animation",
        "shiny text",
        "gradient text sweep"
      ],
      "whenToUse": [
        "AI thinking or loading labels such as Generating summary.",
        "Short status lines that should show ongoing work and settle when done."
      ],
      "whenNotToUse": [
        "Use text-stream for the response text itself.",
        "Use skeleton for content placeholders and progress for measurable progress."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Sets aria-busy while active; the text itself stays real, readable text.",
        "Announce completion from your own aria-live region; the component does not."
      ],
      "motion": [
        "A bell-shaped highlight band in the current text color sweeps linearly over a muted base, pausing briefly between passes.",
        "The sweep only runs while active, in view, and the page is visible; a paused band resumes where it stopped. A changed label rises in with a soft blur.",
        "Reduced motion stops the sweep and swaps labels with a plain fade."
      ],
      "responsive": [
        "Keeps max-width 100%, but the band is tuned for one short line, so avoid wrapping text.",
        "In forced-colors mode the gradient is removed and the text renders in the system color."
      ],
      "performance": [
        "The sweep runs only while active, within 64px of the viewport, and while the page is visible.",
        "It animates a background-clip gradient on one element, with no per-letter spans."
      ],
      "aiNotes": [
        "Use for AI thinking or loading labels. Pair with text-stream for the response itself, and use skeleton or progress for content placeholders.",
        "Flip active to false when work finishes instead of unmounting, so the text settles smoothly."
      ],
      "related": [
        "text-morph",
        "skeleton",
        "progress"
      ],
      "usage": "import { TextShimmer } from \"@/components/arc/text-shimmer/text-shimmer\";\n\nexport function AssistantStatus({ busy }: { busy: boolean }) {\n  return (\n    <TextShimmer active={busy}>\n      {busy ? \"Generating summary\" : \"Summary ready\"}\n    </TextShimmer>\n  );\n}",
      "exports": [
        "TextShimmer"
      ]
    },
    {
      "name": "hold-to-confirm",
      "kind": "component",
      "title": "Hold to confirm",
      "description": "Confirm a destructive action by holding, not tapping.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "hold",
        "long press",
        "confirm"
      ],
      "docs": "https://uiarc.dev/components/hold-to-confirm",
      "markdown": "https://uiarc.dev/components/hold-to-confirm/markdown",
      "registry": "https://uiarc.dev/r/hold-to-confirm.json",
      "keywords": [
        "react hold to confirm",
        "press and hold button",
        "long press delete",
        "hold to delete",
        "destructive action button",
        "confirm without dialog"
      ],
      "whenToUse": [
        "Destructive actions like delete or revoke where the consequence is already visible on screen.",
        "Replacing a confirm dialog with a deliberate press in compact settings rows.",
        "Actions that should not fire on an accidental tap on touch devices."
      ],
      "whenNotToUse": [
        "Use dialog when people need to read what will happen before confirming.",
        "Use button or action-button for safe, easily undone actions.",
        "Use swipe-actions for delete in touch lists."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "aria-describedby points to a hidden hint: \"Press and hold for N seconds to confirm. With a keyboard, hold Space or Enter.\"",
        "aria-label tracks the visible label; a role=\"status\" region announces confirmedLabel.",
        "Once done the button sets aria-disabled; the text is drawn twice so it changes color exactly at the fill edge."
      ],
      "motion": [
        "The fill advances on a linear timeline while held, rewinds on a smooth spring when released, and resumes from where it was on a quick re-press.",
        "The button presses to about 0.97; on completion the label morphs, the width springs, and a check draws itself in. Touch devices get a short vibration.",
        "Reduced motion drops the press scale and rewind spring and swaps label and icon with a fade; the fill still tracks the hold."
      ],
      "responsive": [
        "Uses pointer events with capture, so mouse, pen, and touch holds all work; sliding more than 24px off the button cancels the hold.",
        "Touch holds trigger a short vibration on completion where the device supports it.",
        "Keyboard users hold Space or Enter, and a hidden hint explains the duration."
      ],
      "performance": [
        "The fill runs on a single linear motion animation, not a React state update per frame.",
        "The label is drawn twice to change color at the fill edge; a ResizeObserver handles the width spring on completion."
      ],
      "aiNotes": [
        "Use for destructive or hard-to-undo actions when the consequence is already visible on screen. Use dialog when people need to read what will happen first.",
        "Write the label as an instruction (\"Hold to …\"); pass confirmed to reset it after an undo or a failed request.",
        "Pair with onHoldChange to show a nearby hint while the press is held."
      ],
      "related": [
        "button",
        "action-button",
        "dialog",
        "swipe-actions"
      ],
      "usage": "import { HoldToConfirm } from \"@/components/arc/hold-to-confirm/hold-to-confirm\";\n\nexport function DeleteProject({ onDelete }: { onDelete: () => void }) {\n  return (\n    <HoldToConfirm\n      label=\"Hold to delete project\"\n      confirmedLabel=\"Deleted\"\n      duration={1500}\n      onConfirm={onDelete}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Keep holding hint",
          "code": "const [holding, setHolding] = useState(false);\n\n<HoldToConfirm label=\"Hold to revoke key\" onConfirm={revoke} onHoldChange={setHolding} />\n<p aria-hidden=\"true\">{holding ? \"Keep holding\" : \"\"}</p>"
        }
      ],
      "exports": [
        "HoldToConfirm"
      ]
    },
    {
      "name": "swipe-actions",
      "kind": "component",
      "title": "Swipe actions",
      "description": "Reveal row actions with a swipe, or from the same actions in a menu.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "swipe",
        "gesture",
        "list"
      ],
      "docs": "https://uiarc.dev/components/swipe-actions",
      "markdown": "https://uiarc.dev/components/swipe-actions/markdown",
      "registry": "https://uiarc.dev/r/swipe-actions.json",
      "keywords": [
        "react swipe actions",
        "swipe to delete",
        "swipe to archive",
        "ios swipe row",
        "swipeable list item",
        "mobile list actions",
        "swipe gesture list"
      ],
      "whenToUse": [
        "Touch-first triage lists such as mail, notifications, or tasks.",
        "Rows with one or two quick actions per side, like Archive and Mark unread.",
        "Lists where a full swipe should commit the main action and the row should animate away."
      ],
      "whenNotToUse": [
        "Use context-menu or dropdown-menu for desktop-first tables.",
        "Use reorderable-list when rows need to be dragged vertically.",
        "Use hold-to-confirm for a single destructive action outside a list."
      ],
      "dependencies": [
        "@radix-ui/react-dropdown-menu",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The list is a ul with role=\"list\" and aria-label; each row is an li.",
        "Every row has a \"More actions for <label>\" menu with the same actions, so the gesture is never required.",
        "Revealed action buttons are aria-hidden and out of the tab order; after a removing action, focus moves to a neighbouring row."
      ],
      "motion": [
        "The row tracks the finger, rubber-bands past its last stop, and settles on a spring that carries the release velocity; a flick projects its travel.",
        "Past the threshold the outer action stretches across the row, then the row slides out and the list closes the gap.",
        "Reduced motion jumps the row to its stops and fades rows in and out instead of animating height."
      ],
      "responsive": [
        "Swipes lock to the horizontal axis after the first movement, so vertical page scrolling still works on touch.",
        "A full swipe commits past the larger of the open width plus 48px or 56% of the row, measured with a ResizeObserver.",
        "Every row also has a More actions menu, so mouse and keyboard users never need the gesture."
      ],
      "performance": [
        "Drag updates a motion value directly, so rows do not re-render while following the finger.",
        "Rows are not virtualized and each has its own ResizeObserver; paginate long lists."
      ],
      "aiNotes": [
        "Use for short, triage-style lists on touch (mail, notifications, tasks). For desktop-first tables use context-menu or a row menu.",
        "Remove the item from state inside onSelect; set keepRow for actions that keep the item, such as mark as unread.",
        "Rows must be direct children of SwipeActions, keyed by item id, so removal and undo animate."
      ],
      "related": [
        "context-menu",
        "hold-to-confirm",
        "reorderable-list",
        "dropdown-menu"
      ],
      "usage": "import { SwipeActions, SwipeActionsRow } from \"@/components/arc/swipe-actions/swipe-actions\";\nimport { Archive, MailOpen } from \"lucide-react\";\n\nexport function Inbox({ messages }: { messages: Message[] }) {\n  return (\n    <SwipeActions label=\"Inbox\">\n      {messages.map(message => (\n        <SwipeActionsRow\n          key={message.id}\n          label={message.subject}\n          leading={[{ label: \"Unread\", icon: <MailOpen size={18} />, tone: \"accent\", keepRow: true, onSelect: () => markUnread(message.id) }]}\n          trailing={[{ label: \"Archive\", icon: <Archive size={18} />, tone: \"danger\", onSelect: () => archive(message.id) }]}\n        >\n          <MessagePreview message={message} />\n        </SwipeActionsRow>\n      ))}\n    </SwipeActions>\n  );\n}",
      "exports": [
        "SwipeActions",
        "SwipeActionsRow",
        "SwipeAction"
      ]
    },
    {
      "name": "reactions",
      "kind": "component",
      "title": "Reactions",
      "description": "Acknowledge a post in one tap, with counts that roll.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "reactions",
        "social",
        "comments"
      ],
      "docs": "https://uiarc.dev/components/reactions",
      "markdown": "https://uiarc.dev/components/reactions/markdown",
      "registry": "https://uiarc.dev/r/reactions.json",
      "keywords": [
        "react reactions",
        "emoji reactions",
        "reaction picker",
        "like button with count",
        "slack reactions",
        "comment reactions"
      ],
      "whenToUse": [
        "Lightweight acknowledgement on comments, posts, or release notes.",
        "Slack-style reaction pills with counts and a picker for new reactions."
      ],
      "whenNotToUse": [
        "Use a reply field such as ai-composer when people need to say something specific.",
        "Use action-swap for a single like or follow toggle without counts.",
        "Use badge for static counts that people cannot change."
      ],
      "dependencies": [
        "@radix-ui/react-popover",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The bar is role=\"group\"; each pill is a button with aria-pressed and a label like \"Love, 4 reactions, including yours\".",
        "The picker is a role=\"toolbar\" with roving tabindex and labelled options; the add button is labelled \"Add reaction\".",
        "A role=\"status\" region announces each change; a pill that leaves while focused hands focus to its neighbour or the add button."
      ],
      "motion": [
        "Pills scale in from a blur and reflow on a morph spring; the icon gets a velocity kick that stacks on quick taps, and digits roll in the direction of the count.",
        "The picker grows out of the add button while a shared highlight glides between options.",
        "Reduced motion, read only after hydration, swaps everything with short fades and no layout springs."
      ],
      "responsive": [
        "The bar wraps onto new lines as pills are added, and its height springs to fit.",
        "The picker opens in a Radix popover, so it repositions near viewport edges.",
        "Hover highlights in the picker follow the mouse only; touch taps select directly."
      ],
      "performance": [
        "Pills use layout position animation and count digits roll individually; fine for a handful of reactions per item.",
        "In long comment threads each bar runs its own ResizeObserver, so avoid rendering hundreds at once."
      ],
      "aiNotes": [
        "Use for lightweight acknowledgement on comments, posts, or release notes. When people need to say something specific, add a reply field instead.",
        "Icons must be Lucide component types, not elements. Persist the change from onValueChange and control value when counts come from the server."
      ],
      "related": [
        "badge",
        "popover",
        "avatar-group",
        "animated-counter"
      ],
      "usage": "import { Reactions } from \"@/components/arc/reactions/reactions\";\nimport { Heart, PartyPopper, ThumbsUp } from \"lucide-react\";\n\nconst options = [\n  { id: \"like\", label: \"Like\", icon: ThumbsUp },\n  { id: \"love\", label: \"Love\", icon: Heart },\n  { id: \"party\", label: \"Celebrate\", icon: PartyPopper },\n];\n\nexport function CommentReactions() {\n  return <Reactions options={options} defaultValue={[{ id: \"like\", count: 3, reacted: false }]} onValueChange={(_, change) => save(change)} />;\n}",
      "exports": [
        "Reactions"
      ]
    },
    {
      "name": "slider",
      "kind": "component",
      "title": "Slider",
      "description": "Pick a value or a range on a track that follows your finger.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "slider",
        "range",
        "input"
      ],
      "docs": "https://uiarc.dev/components/slider",
      "markdown": "https://uiarc.dev/components/slider/markdown",
      "registry": "https://uiarc.dev/r/slider.json",
      "keywords": [
        "react slider",
        "range slider",
        "dual thumb slider",
        "price range slider",
        "slider with marks",
        "animated slider"
      ],
      "whenToUse": [
        "Approximate values like volume, opacity, or a price range.",
        "Ranges with two thumbs, labelled marks, and a formatted readout."
      ],
      "whenNotToUse": [
        "Use number-field when the exact number matters.",
        "Use progress to show a value people cannot change."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Each thumb is role=\"slider\" with aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext from format, and aria-orientation.",
        "Thumbs are named by label, or by thumbLabels for ranges.",
        "The readout, bubble, and marks are aria-hidden because the thumbs carry the value."
      ],
      "motion": [
        "Released drags carry a little momentum before snapping; the bubble springs up over an active thumb and digits roll in the readout. At a limit the thumb strains toward the key press and springs back.",
        "Reduced motion removes momentum, the strain, and digit rolling, and turns off CSS transitions."
      ],
      "responsive": [
        "It fills its column; the track uses touch-action pan-y, so horizontal drags move the thumb while vertical swipes scroll the page.",
        "On coarse pointers the value bubble sits higher so the finger does not cover it.",
        "The bubble and end mark labels clamp inside the track so they never overflow the edges."
      ],
      "performance": [
        "Drags write motion values with pointer capture, so the thumb follows 1:1 without React re-renders per frame.",
        "onValueChange fires on every step; put fetching or heavy work in onValueCommit."
      ],
      "aiNotes": [
        "Use for approximate values and ranges like volume, price filters, or opacity. Use number-field when the exact number matters.",
        "Controlled or uncontrolled; pass a tuple for a range. Put heavy work such as fetching in onValueCommit, not onValueChange.",
        "Pass name to submit through hidden inputs. Exported as both named and default."
      ],
      "related": [
        "number-field",
        "progress",
        "filter-toolbar"
      ],
      "usage": "import { Slider } from \"@/components/arc/slider/slider\";\n\nexport function PriceFilter() {\n  const [range, setRange] = useState<[number, number]>([20, 80]);\n  return (\n    <Slider\n      label=\"Price\"\n      value={range}\n      onValueChange={setRange}\n      step={5}\n      format={(v) => `$${v}`}\n      thumbLabels={[\"Minimum price\", \"Maximum price\"]}\n    />\n  );\n}",
      "exports": [
        "Slider"
      ]
    },
    {
      "name": "elastic-slider",
      "kind": "component",
      "title": "Elastic slider",
      "description": "A slider that stretches like rubber past its ends, with a swaying value tag and detents the thumb clicks into.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/elastic-slider",
      "markdown": "https://uiarc.dev/components/elastic-slider/markdown",
      "registry": "https://uiarc.dev/r/elastic-slider.json",
      "keywords": [
        "elastic slider",
        "rubber band slider",
        "react slider",
        "volume slider",
        "slider with detents",
        "stretchy slider",
        "ios slider",
        "animated range input"
      ],
      "whenToUse": [
        "A single setting people drag to taste, such as volume, brightness, or opacity.",
        "Media and creative tools where a physical, tactile control fits the product.",
        "Values with a few common stops that should feel like detents."
      ],
      "whenNotToUse": [
        "Use slider for dense forms and settings pages where a quiet control fits better.",
        "Use range-histogram-slider to pick a min and max over data.",
        "Use number-field or ruler-slider when the exact number matters more than the feel."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The thumb is role=\"slider\" with aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext from format.",
        "The thumb is labelled by the visible label through aria-labelledby.",
        "The readout, value tag, icons, and mark labels are aria-hidden; the slider carries the value for assistive tech.",
        "Clicking a mark label is a pointer shortcut; the same values are reachable with keys."
      ],
      "motion": [
        "The track rubber-bands past either end with iOS-style resistance and springs back with the release velocity.",
        "The thumb swells to 1.28x while dragged; the value tag trails it on a soft spring and tilts with its speed.",
        "Detents pop their tick and, on touch devices that support it, give a short vibration.",
        "Reduced motion removes the stretch, sway, and tick pop; the thumb jumps to its value and the tag fades."
      ],
      "responsive": [
        "The track fills its container; icons and the header stay on one row at any width.",
        "Touch drags use pointer capture, and detents add a short vibration where supported.",
        "Mark labels at the ends align to the track edges so they stay inside narrow containers."
      ],
      "performance": [
        "Thumb, fill, stretch, and tag are driven by motion values, so dragging does not re-render React per frame.",
        "Track width is measured with one ResizeObserver."
      ],
      "aiNotes": [
        "Choose it for one continuous value where the feel matters: volume, brightness, zoom, playback speed.",
        "Pass marks for values people aim for (0, 50, 100); they act as soft snaps without blocking values in between.",
        "Use onValueChange for live previews and onValueCommit for saves or network requests.",
        "For two thumbs or a range, use slider or range-histogram-slider instead."
      ],
      "related": [
        "slider",
        "range-histogram-slider",
        "ruler-slider",
        "scrub-input",
        "number-field"
      ],
      "usage": "import { ElasticSlider } from \"@/components/arc/elastic-slider/elastic-slider\";\nimport { Volume1, Volume2 } from \"lucide-react\";\n\nexport function VolumeControl() {\n  const [volume, setVolume] = useState(60);\n  return (\n    <ElasticSlider\n      label=\"Volume\"\n      value={volume}\n      onValueChange={setVolume}\n      marks={[0, { value: 50, label: \"50%\" }, 100]}\n      format={value => `${value}%`}\n      startIcon={<Volume1 size={16} />}\n      endIcon={<Volume2 size={16} />}\n    />\n  );\n}",
      "exports": [
        "ElasticSlider"
      ]
    },
    {
      "name": "range-histogram-slider",
      "kind": "component",
      "title": "Range histogram slider",
      "description": "A price range over a live histogram of the data, lighting the bars inside the range and settling on bar edges.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/range-histogram-slider",
      "markdown": "https://uiarc.dev/components/range-histogram-slider/markdown",
      "registry": "https://uiarc.dev/r/range-histogram-slider.json",
      "keywords": [
        "range slider with histogram",
        "price range filter",
        "airbnb price slider",
        "dual thumb slider",
        "histogram slider",
        "react range slider",
        "min max slider",
        "distribution filter"
      ],
      "whenToUse": [
        "Marketplace and search filters for price, size, or distance where the distribution guides the choice.",
        "Analytics filters that trim outliers from a numeric column.",
        "Any min and max choice where people benefit from seeing how many results fall inside."
      ],
      "whenNotToUse": [
        "Use slider for a plain range without data behind it.",
        "Use elastic-slider or slider for a single value.",
        "Use number-field pairs when people type exact bounds more often than they drag."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Each thumb is role=\"slider\" with its own name, aria-valuetext, and aria-valuemin/max that reflect the other thumb, so the pair never reads as crossing.",
        "The histogram and the readout are aria-hidden; the thumbs carry the values.",
        "Pass thumbLabels in the product's language when the defaults do not fit."
      ],
      "motion": [
        "Lit bars and the track fill are clipped by one path, so both cut exactly at each thumb on the same frame.",
        "Thumbs follow the pointer 1:1, rubber-band a few pixels past their limit, and settle with the release velocity; momentum can carry them at most one bar.",
        "Pressing the histogram or track springs the nearest thumb to that spot.",
        "Reduced motion removes the stretch and springs; thumbs jump to their values."
      ],
      "responsive": [
        "Chart and track fill the container width; bars shrink with it, so lower bins on narrow screens.",
        "Touch drags use pointer capture; thumbs keep a large hit area.",
        "The readout sits on the label row and uses tabular numbers."
      ],
      "performance": [
        "Histogram counts are computed once per data, min, max, and bins change.",
        "Thumbs and clipping are motion values, so dragging does not re-render the bars.",
        "Bars render twice (base and lit copy); keep bins under about 60."
      ],
      "aiNotes": [
        "Choose it for price, size, or duration filters where showing the data distribution helps people pick a sensible range.",
        "Pass the unfiltered values as data and filter your list on onValueCommit to avoid refetching on every frame.",
        "Keep data to the values the filter applies to; histogram counts are memoized on data, min, max, and bins.",
        "When the two thumbs sit together, the first drag direction decides which one moves."
      ],
      "related": [
        "slider",
        "elastic-slider",
        "filter-toolbar",
        "date-range-picker",
        "ruler-slider"
      ],
      "usage": "import { RangeHistogramSlider } from \"@/components/arc/range-histogram-slider/range-histogram-slider\";\n\nexport function PriceFilter({ prices }: { prices: number[] }) {\n  const [range, setRange] = useState<[number, number]>([80, 320]);\n  return (\n    <RangeHistogramSlider\n      label=\"Price per night\"\n      data={prices}\n      min={0}\n      max={500}\n      step={5}\n      bins={25}\n      value={range}\n      onValueChange={setRange}\n      onValueCommit={range => refetchListings(range)}\n      format={value => `$${value}`}\n    />\n  );\n}",
      "exports": [
        "RangeHistogramSlider",
        "histogram"
      ]
    },
    {
      "name": "ruler-slider",
      "kind": "component",
      "title": "Ruler slider",
      "description": "A tape of ticks that slides under a fixed needle, with momentum, snapping, and a rolling readout.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ruler-slider",
      "markdown": "https://uiarc.dev/components/ruler-slider/markdown",
      "registry": "https://uiarc.dev/r/ruler-slider.json",
      "keywords": [
        "ruler slider",
        "tape measure input",
        "height picker",
        "weight picker",
        "scrollable ruler",
        "momentum slider",
        "measurement input",
        "react ruler picker"
      ],
      "whenToUse": [
        "Onboarding and profile flows that ask for body measurements or goals.",
        "Mobile-first inputs where dragging a scale is easier than typing.",
        "Wide ranges with fine steps that a short slider track cannot resolve."
      ],
      "whenNotToUse": [
        "Use number-field when people know the exact number and prefer to type it.",
        "Use slider or elastic-slider when the value is relative, like volume.",
        "Use range-histogram-slider to pick a range."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The tape viewport is role=\"slider\" with aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext that includes the unit.",
        "It is labelled by the visible label through aria-labelledby.",
        "The rolling readout and needle are aria-hidden; the slider announces the value.",
        "Horizontal trackpad scrolling moves the tape; vertical scrolling is left to the page."
      ],
      "motion": [
        "Dragging moves the tape 1:1; a flick coasts with scroll-like deceleration and lands on a tick.",
        "Pulling past either end rubber-bands, and releasing pulls the tape home with the throw's speed.",
        "Taps and keys glide on a spring that keeps any speed already in the tape; grabbing a coasting tape catches it in place.",
        "Each digit of the readout rolls on its own column as ticks pass the needle.",
        "Reduced motion removes coasting, rubber-banding, and glides; the tape jumps to the nearest tick."
      ],
      "responsive": [
        "The viewport fills its container and fades the tape at both edges; the needle stays centered at any width.",
        "Touch drags, flicks, and taps work the same as mouse; small moves under 4px count as a tap.",
        "Horizontal trackpad scrolling moves the tape without capturing vertical page scroll."
      ],
      "performance": [
        "Ticks are drawn with CSS gradients, not one element per tick, so long ranges stay cheap.",
        "Only labelled ticks are elements; keep (max - min) / (step × majorEvery) to a few hundred.",
        "Tape position is a motion value, so dragging and coasting do not re-render per frame."
      ],
      "aiNotes": [
        "Choose it for measurements and precise values on touch: height, weight, temperature, tip amount, zoom level.",
        "Set step to the precision you store and majorEvery so labelled ticks land on round numbers (10 for cm, 5 or 10 for kg).",
        "Increase spacing when people need finer control on small screens.",
        "Use onValueCommit for saves: onValueChange fires on every tick while the tape coasts."
      ],
      "related": [
        "elastic-slider",
        "slider",
        "scrub-input",
        "number-field",
        "time-dial"
      ],
      "usage": "import { RulerSlider } from \"@/components/arc/ruler-slider/ruler-slider\";\n\nexport function HeightField() {\n  const [height, setHeight] = useState(172);\n  return (\n    <RulerSlider\n      label=\"Height\"\n      unit=\"cm\"\n      min={120}\n      max={220}\n      value={height}\n      onValueChange={setHeight}\n      onValueCommit={saveHeight}\n    />\n  );\n}",
      "exports": [
        "RulerSlider"
      ]
    },
    {
      "name": "inline-edit",
      "kind": "component",
      "title": "Inline edit",
      "description": "Rename in place: the text becomes a field without moving.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inline edit",
        "rename",
        "click to edit"
      ],
      "docs": "https://uiarc.dev/components/inline-edit",
      "markdown": "https://uiarc.dev/components/inline-edit/markdown",
      "registry": "https://uiarc.dev/r/inline-edit.json",
      "keywords": [
        "react inline edit",
        "click to edit",
        "editable text",
        "inline editing",
        "rename in place",
        "optimistic save field"
      ],
      "whenToUse": [
        "Titles and descriptions read far more often than edited, like a project name.",
        "Single fields that save on their own with optimistic updates and rollback."
      ],
      "whenNotToUse": [
        "Use input or textarea in a form when several fields save together.",
        "Use input when the field should always look editable."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The resting text is a native button labelled \"<label>: <value>\", so it is focusable and announced as editable.",
        "The field has aria-label and aria-invalid; validation and save errors render in a polite live region linked through aria-describedby.",
        "Save progress and results are announced through a role=\"status\" region; save and cancel buttons are labelled."
      ],
      "motion": [
        "Text rolls between values, a check draws once a save lands, and multiline boxes follow their height; failed saves roll back with motion.",
        "Reduced motion crossfades text, draws the check instantly, and replaces the spinner with a static mark."
      ],
      "responsive": [
        "Text wraps within the column and reserves 66px at the end for the save and cancel buttons.",
        "With multiline the box grows in height as the text wraps.",
        "Hover hints apply only on fine pointers; on touch a tap starts editing."
      ],
      "performance": [
        "Two ResizeObservers size the frame; sizes are measured in a layout effect, so no frame shows a wrong width."
      ],
      "aiNotes": [
        "Use for single fields read far more often than edited, like titles and descriptions. Use a regular form with input or textarea when several fields save together.",
        "value is controlled by the saved data; update it after onSave resolves. Return a promise from onSave to get saving, saved, and failed states.",
        "Exported as both named and default."
      ],
      "related": [
        "input",
        "textarea",
        "toast"
      ],
      "usage": "import { InlineEdit } from \"@/components/arc/inline-edit/inline-edit\";\n\nexport function ProjectTitle({ project }: { project: { id: string; name: string } }) {\n  return (\n    <InlineEdit\n      as=\"h1\"\n      label=\"Project name\"\n      value={project.name}\n      validate={(next) => (next.trim() ? null : \"Name can’t be empty\")}\n      onSave={(next) => renameProject(project.id, next)}\n    />\n  );\n}",
      "exports": [
        "InlineEdit"
      ]
    },
    {
      "name": "expanding-search",
      "kind": "component",
      "title": "Expanding search",
      "description": "An icon that morphs into a search field with results beneath it.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "search",
        "field",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/expanding-search",
      "markdown": "https://uiarc.dev/components/expanding-search/markdown",
      "registry": "https://uiarc.dev/r/expanding-search.json",
      "keywords": [
        "react expanding search",
        "search icon expand",
        "animated search bar",
        "header search",
        "search with results dropdown",
        "collapsible search"
      ],
      "whenToUse": [
        "Header or toolbar search where a full field would crowd the layout.",
        "Quick navigation across local items with grouped results and recent suggestions."
      ],
      "whenNotToUse": [
        "Use search-field to filter a visible list in place.",
        "Use combobox to set a form value.",
        "Use command-palette for searching commands across the app."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The field is role=\"combobox\" with aria-expanded, aria-controls, aria-autocomplete=\"list\", and aria-activedescendant.",
        "Results are a role=\"listbox\" of role=\"option\" items inside labelled role=\"group\" sections.",
        "Result counts and empty states are announced through a polite live region; the collapsed field is inert."
      ],
      "motion": [
        "The button morphs into the field on a spring, results unfold beneath with a panel that tracks the field width, and a highlight travels between options.",
        "Reduced motion swaps states with fades and no width or position travel."
      ],
      "responsive": [
        "The field grows to the smaller of expandedWidth and its container, and a ResizeObserver follows container resizes.",
        "The results panel matches the field width and caps at min(22rem, 60vh), scrolling itself.",
        "The idle lane takes no pointer events, so it never blocks what sits under it on small screens."
      ],
      "performance": [
        "Ranking is memoized and runs over all items per query; pass pre-filtered or server results for large sets.",
        "Result rows use layout position animation, capped by maxResults at 6 by default."
      ],
      "aiNotes": [
        "Use in headers and toolbars where a full field would crowd the layout. Use search-field to filter a list in place and combobox to set a form value.",
        "Place it in the space it may grow into; it fills that space up to expandedWidth. Filtering is local, so pass pre-fetched items or update items as the query changes.",
        "Exported as both named and default."
      ],
      "related": [
        "search-field",
        "combobox",
        "morph-nav"
      ],
      "usage": "import { ExpandingSearch } from \"@/components/arc/expanding-search/expanding-search\";\n\nexport function HeaderSearch() {\n  const router = useRouter();\n  return (\n    <ExpandingSearch\n      label=\"Search projects and docs\"\n      items={[\n        { id: \"p1\", title: \"Arc website\", group: \"Projects\", meta: \"Updated today\" },\n        { id: \"d1\", title: \"Motion tokens\", group: \"Docs\", keywords: [\"spring\"] },\n      ]}\n      onSelect={(item) => router.push(`/items/${item.id}`)}\n    />\n  );\n}",
      "exports": [
        "ExpandingSearch"
      ]
    },
    {
      "name": "chip-group",
      "kind": "component",
      "title": "Chip group",
      "description": "Filter by a few facets with chips that morph as you pick them.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "chips",
        "filter",
        "toggle"
      ],
      "docs": "https://uiarc.dev/components/chip-group",
      "markdown": "https://uiarc.dev/components/chip-group/markdown",
      "registry": "https://uiarc.dev/r/chip-group.json",
      "keywords": [
        "react chip group",
        "filter chips",
        "toggle chips",
        "tag filter",
        "selectable chips",
        "chip select"
      ],
      "whenToUse": [
        "Facet filters people toggle often, like topics or categories.",
        "Single clearable choices shown as chips, via multiple={false}.",
        "Long option sets folded behind a +N more chip with maxVisible."
      ],
      "whenNotToUse": [
        "Use multi-select when space is tight.",
        "Use checkbox in forms.",
        "Use segmented-control for one choice among a few views."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The group has role=\"group\" and aria-label; each chip is a button with aria-pressed.",
        "Roving tabindex keeps one chip in the tab order; the more chip uses aria-expanded.",
        "Check icons and surfaces are aria-hidden."
      ],
      "motion": [
        "Selecting grows a check in, slides the label over, and springs the chip edge while neighbours glide to new places, even across lines. Revealed chips stagger in and the group height morphs.",
        "Reduced motion replaces the morphs with short fades."
      ],
      "responsive": [
        "Chips wrap onto new lines and the frame height follows on a spring.",
        "Selected chips always stay visible, even beyond maxVisible, so folding never hides active filters."
      ],
      "performance": [
        "Every chip has layout position animation and its own ResizeObserver; keep sets to a few dozen and fold the rest.",
        "Revealed chips stagger in, capped at 0.3s total."
      ],
      "aiNotes": [
        "Use for facet filters people toggle often. Use multi-select when space is tight and checkbox in forms.",
        "Always controlled. Set multiple={false} for a single, clearable choice.",
        "Exported as both named and default."
      ],
      "related": [
        "multi-select",
        "filter-toolbar",
        "segmented-control",
        "tag-input"
      ],
      "usage": "import { ChipGroup } from \"@/components/arc/chip-group/chip-group\";\n\nexport function TopicFilter() {\n  const [topics, setTopics] = useState<string[]>([]);\n  return (\n    <ChipGroup\n      label=\"Topics\"\n      value={topics}\n      onValueChange={setTopics}\n      maxVisible={4}\n      options={[\"Design\", \"Motion\", \"Code\", \"Research\", \"Writing\"].map((t) => ({ value: t.toLowerCase(), label: t }))}\n    />\n  );\n}",
      "exports": [
        "ChipGroup"
      ]
    },
    {
      "name": "password-strength",
      "kind": "component",
      "title": "Password strength",
      "description": "Show how strong a new password is while it is typed.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "password",
        "strength meter",
        "validation"
      ],
      "docs": "https://uiarc.dev/components/password-strength",
      "markdown": "https://uiarc.dev/components/password-strength/markdown",
      "registry": "https://uiarc.dev/r/password-strength.json",
      "keywords": [
        "react password strength",
        "password strength meter",
        "password requirements checklist",
        "new password field",
        "password validation",
        "signup password"
      ],
      "whenToUse": [
        "Sign up and change password forms.",
        "Password rules that should check off live as the person types."
      ],
      "whenNotToUse": [
        "Use password-field for sign in.",
        "Use progress for measurable progress unrelated to passwords."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The meter is role=\"meter\" with aria-valuenow 0 to 4 and aria-valuetext set to the strength word.",
        "The rules list is linked to the input through aria-describedby along with any error; errors set aria-invalid.",
        "A role=\"status\" region summarizes strength and rules met; the reveal button uses aria-pressed and aria-controls."
      ],
      "motion": [
        "Segments fill and change tone in a staggered wave, rules check off with drawn ticks, remaining counts roll, and the word morphs up or down with strength.",
        "Reduced motion, applied after hydration, drops the shake, wave, and roll in favor of instant changes."
      ],
      "responsive": [
        "On coarse pointers the input grows to the base font size, so iOS does not zoom on focus.",
        "The four-segment meter shares the row width evenly and shrinks with the column."
      ],
      "performance": [
        "Scoring runs on the device on every keystroke with simple rule tests; there is no dictionary check.",
        "One ResizeObserver per field measures the helper row for its height spring; cheap for a form, but avoid hundreds in a table."
      ],
      "aiNotes": [
        "Use when creating or changing a password. Use password-field for sign in.",
        "Controlled or uncontrolled; read strength from onValueChange to gate submit. Scoring runs on the device.",
        "Also exports estimateStrength(password, rules, { shortLength, levels, countCharacters }), defaultPasswordRules, and createPasswordRules({ minLength, labels, countCharacters }) for server-side checks, translated rule labels, or your own length policy.",
        "To translate, pass messages for the component's own copy and build rules with createPasswordRules({ labels }) for the rule labels. Keep shortLength and the rules' minLength in line with your server's policy."
      ],
      "related": [
        "password-field",
        "input",
        "progress"
      ],
      "usage": "import { PasswordStrength } from \"@/components/arc/password-strength/password-strength\";\n\nexport function NewPasswordField() {\n  const [strong, setStrong] = useState(false);\n  return (\n    <PasswordStrength\n      label=\"New password\"\n      name=\"password\"\n      autoComplete=\"new-password\"\n      onValueChange={(_, strength) => setStrong(strength.level >= 3)}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Gate submit on strength",
          "code": "const [level, setLevel] = useState(0);\n\n<PasswordStrength\n  label=\"New password\"\n  name=\"password\"\n  autoComplete=\"new-password\"\n  onValueChange={(_, strength) => setLevel(strength.level)}\n/>\n<Button type=\"submit\" disabled={level < 3}>Create account</Button>"
        },
        {
          "title": "Translated, with your own length policy",
          "code": "import { PasswordStrength, createPasswordRules } from \"@/components/arc/password-strength/password-strength\";\n\nconst rules = createPasswordRules({\n  minLength: 10,\n  labels: {\n    length: min => `Mindestens ${min} Zeichen`,\n    case: \"Gross- und Kleinbuchstaben\",\n    number: \"Mindestens eine Zahl\",\n    symbol: \"Mindestens ein Sonderzeichen\",\n  },\n});\n\nexport function NewPassword() {\n  return (\n    <PasswordStrength\n      label=\"Neues Passwort\"\n      rules={rules}\n      shortLength={10}\n      messages={{\n        levels: { tooShort: \"Zu kurz\", weak: \"Schwach\", fair: \"Mittel\", good: \"Gut\", strong: \"Stark\" },\n        showPassword: \"Passwort anzeigen\",\n        meterLabel: \"Passwortstärke\",\n        noPassword: \"Noch kein Passwort\",\n        rulesLabel: \"Passwortanforderungen\",\n        ruleMet: \", erfüllt\",\n        ruleNotMet: \", nicht erfüllt\",\n        remainingPrefix: \"noch\",\n        remainingSuffix: \"\",\n        summary: ({ label, met, total }) => `Stärke: ${label}. ${met} von ${total} Anforderungen erfüllt.`,\n      }}\n    />\n  );\n}"
        }
      ],
      "exports": [
        "PasswordStrength"
      ]
    },
    {
      "name": "bottom-sheet",
      "kind": "component",
      "title": "Bottom sheet",
      "description": "A sheet that rests at a peek or full height and follows your finger.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "overlay",
        "gesture",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/bottom-sheet",
      "markdown": "https://uiarc.dev/components/bottom-sheet/markdown",
      "registry": "https://uiarc.dev/r/bottom-sheet.json",
      "keywords": [
        "react bottom sheet",
        "mobile bottom sheet",
        "draggable sheet",
        "ios sheet",
        "snap points sheet",
        "detents",
        "swipe up panel"
      ],
      "whenToUse": [
        "Mobile-first secondary tasks such as details, filters, or share options.",
        "Content that benefits from a peek height before expanding to nearly full screen.",
        "Maps and media views where the sheet should be dragged between detents."
      ],
      "whenNotToUse": [
        "Use dialog for interrupting decisions on any screen size.",
        "Use drawer for side panels on wide desktop layouts.",
        "Use popover for small anchored content that should not dim the page."
      ],
      "dependencies": [
        "@radix-ui/react-dialog",
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Built on Radix Dialog: role=\"dialog\", focus trap, and focus return to the trigger. Initial focus lands on the sheet itself.",
        "The grabber is a button with aria-expanded and an Expand or Collapse sheet label.",
        "Detent changes are announced through a polite live region.",
        "Tabbing into content below the fold expands the sheet so focused elements are visible."
      ],
      "motion": [
        "One motion value drives the sheet: drags follow the finger 1:1, rubber-band past the tallest detent, and releases project velocity to pick a detent or dismiss.",
        "The backdrop dim is a function of sheet position, so it tracks drags instead of running on a timer.",
        "Reduced motion jumps between detents and fades the sheet in and out."
      ],
      "responsive": [
        "The sheet is min(100%, 36rem) wide and detents are fractions of the dynamic viewport height, so it adapts to mobile browser chrome.",
        "Below 40rem the header and body use tighter side padding.",
        "Touch drags on the content move the sheet until it is fully open, then the content scrolls; mouse and pen drag from the header."
      ],
      "performance": [
        "One motion value drives position and backdrop, so drags do not re-render React per frame.",
        "A ResizeObserver re-fits detents on viewport changes; content is not virtualized, so paginate long lists inside it."
      ],
      "aiNotes": [
        "Use for mobile-first secondary tasks where a peek helps: details, filters, share options. Use dialog for interrupting decisions and drawer for side panels on wide layouts.",
        "Pass a trigger or control open yourself. Use a single-value detents array for a fixed-height sheet."
      ],
      "related": [
        "drawer",
        "dialog",
        "popover"
      ],
      "usage": "import { BottomSheet, BottomSheetClose } from \"@/components/arc/bottom-sheet/bottom-sheet\";\nimport { Button } from \"@/components/arc/button/button\";\n\nexport function TripSheet() {\n  return (\n    <BottomSheet\n      trigger={<Button>Trip details</Button>}\n      title=\"Lisbon, 3 nights\"\n      description=\"Oct 12 to Oct 15\"\n      detents={[0.4, 0.9]}\n    >\n      <p>Flights, hotel, and bookings.</p>\n      <BottomSheetClose asChild><Button variant=\"secondary\">Done</Button></BottomSheetClose>\n    </BottomSheet>\n  );\n}",
      "examples": [
        {
          "title": "Controlled sheet with detent tracking",
          "code": "<BottomSheet\n  open={open}\n  onOpenChange={setOpen}\n  title=\"Filters\"\n  detents={[0.5]}\n  onDetentChange={index => track(\"detent\", index)}\n>\n  <FilterList />\n</BottomSheet>"
        }
      ],
      "exports": [
        "BottomSheet",
        "BottomSheetClose"
      ]
    },
    {
      "name": "hover-card",
      "kind": "component",
      "title": "Hover card",
      "description": "Preview a person or link on hover or focus without leaving the page.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "preview",
        "people",
        "popover"
      ],
      "docs": "https://uiarc.dev/components/hover-card",
      "markdown": "https://uiarc.dev/components/hover-card/markdown",
      "registry": "https://uiarc.dev/r/hover-card.json",
      "keywords": [
        "react hover card",
        "profile preview",
        "user hover card",
        "mention preview",
        "link preview popup",
        "radix hover card"
      ],
      "whenToUse": [
        "Previews of people behind mentions, avatars, or author names.",
        "Link or reference previews where the click must stay free for navigation.",
        "Dense lists where moving between triggers should open cards almost instantly."
      ],
      "whenNotToUse": [
        "Use tooltip for a plain text label.",
        "Use popover when the content has buttons or inputs.",
        "Use user-menu for the account menu on your own avatar."
      ],
      "dependencies": [
        "@radix-ui/react-popover",
        "motion"
      ],
      "accessibility": [
        "The card has role=\"tooltip\" and the trigger gets aria-describedby while it is open.",
        "Focus never moves into the card, so keep its content read only.",
        "On touch, a tap toggles the card instead of relying on hover."
      ],
      "motion": [
        "Grows from the trigger edge: scale from 0.96 and a 4px offset on a smooth spring, with a separate fade. Mid-exit returns retarget instead of jumping.",
        "Within 300ms of another card, the next opens after 80ms with a fade only. Profile rows settle in with a small stagger.",
        "Reduced motion drops scale and travel and keeps a short fade."
      ],
      "responsive": [
        "The card is 18.5rem wide, capped at the space Radix reports as available or the viewport minus 24px.",
        "On touch, a tap toggles the card instead of waiting for hover.",
        "Long names and roles ellipsize and stats wrap onto new lines."
      ],
      "performance": [
        "Only one hover card is open on the page, and it mounts in a portal only while open.",
        "Open and close use timers, not pointer tracking loops; the profile avatar decodes async."
      ],
      "aiNotes": [
        "Use for previews of people, links, or references where the click must stay free for navigation. Use tooltip for plain labels and popover for interactive content.",
        "Only one hover card is open at a time on the page. Anything actionable belongs behind the trigger itself."
      ],
      "related": [
        "tooltip",
        "popover",
        "avatar",
        "user-menu"
      ],
      "usage": "import { HoverCard, HoverCardProfile } from \"@/components/arc/hover-card/hover-card\";\n\nexport function Mention() {\n  return (\n    <HoverCard\n      content={<HoverCardProfile name=\"Maya Chen\" role=\"Product designer, Payments\" stats={[{ label: \"Projects\", value: 12 }]} />}\n    >\n      <button type=\"button\">@maya</button>\n    </HoverCard>\n  );\n}",
      "exports": [
        "HoverCard",
        "HoverCardProfile"
      ]
    },
    {
      "name": "resizable-panels",
      "kind": "component",
      "title": "Resizable panels",
      "description": "Trade space between panes by dragging the divider between them.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "split view",
        "resizable",
        "panes"
      ],
      "docs": "https://uiarc.dev/components/resizable-panels",
      "markdown": "https://uiarc.dev/components/resizable-panels/markdown",
      "registry": "https://uiarc.dev/r/resizable-panels.json",
      "keywords": [
        "react resizable panels",
        "split view",
        "resizable sidebar",
        "draggable divider",
        "split pane",
        "collapsible panel",
        "resizable layout"
      ],
      "whenToUse": [
        "Editor, mail, or dashboard layouts where people trade space between side-by-side panes.",
        "Sidebars that should collapse by dragging or flicking toward the edge.",
        "Layouts whose pane sizes should persist between visits via onLayoutChange."
      ],
      "whenNotToUse": [
        "Use drawer for a panel that slides over the page.",
        "Use tabs when only one pane needs to be visible at a time.",
        "Avoid it for vertical splits, since it is horizontal only."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each divider is a focusable role=\"separator\" with aria-orientation, aria-controls, aria-valuenow, min, and max in percent.",
        "aria-valuetext announces the share, or that a pane is hidden and Enter shows it.",
        "Collapsed panes are inert. The group has role=\"group\"; pass label to name it."
      ],
      "motion": [
        "Dividers follow the pointer 1:1, rubber-band past limits, and spring to rest; flicks toward an edge collapse a pane. Double-click resets the layout.",
        "Pane content fades as it is squeezed below its minimum and a restore tab pops in on a snappy spring.",
        "Reduced motion jumps panes to their targets and fades the restore tab."
      ],
      "responsive": [
        "Panes do not stack on narrow screens; on mobile, render a single pane or switch to tabs yourself.",
        "Dividers use pointer capture with touch-action none, so touch, pen, and mouse drags all work.",
        "The group needs a definite height from its parent."
      ],
      "performance": [
        "A ResizeObserver on the group refits pane widths when the container resizes.",
        "Dragging sets widths directly and springs only on release or collapse; pane content is never re-rendered by the drag itself."
      ],
      "aiNotes": [
        "Use for editor, mail, or dashboard layouts where people trade space between side-by-side panes. Horizontal only.",
        "Give the parent a definite height and persist onLayoutChange shares to restore via defaultSize."
      ],
      "related": [
        "drawer",
        "tree-view",
        "tabs"
      ],
      "usage": "import { ResizablePanel, ResizablePanels } from \"@/components/arc/resizable-panels/resizable-panels\";\n\nexport function MailLayout() {\n  return (\n    <div style={{ height: 480 }}>\n      <ResizablePanels label=\"Mail\" onLayoutChange={saveLayout}>\n        <ResizablePanel id=\"folders\" label=\"Folders\" defaultSize={22} minSize={160} collapsible>\n          <FolderList />\n        </ResizablePanel>\n        <ResizablePanel id=\"messages\" label=\"Messages\" defaultSize={78}>\n          <MessageList />\n        </ResizablePanel>\n      </ResizablePanels>\n    </div>\n  );\n}",
      "exports": [
        "ResizablePanels",
        "ResizablePanel"
      ]
    },
    {
      "name": "toast-stack",
      "kind": "component",
      "title": "Toast stack",
      "description": "Stack short results at the edge until you reach for them.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "toast",
        "notification",
        "sonner"
      ],
      "docs": "https://uiarc.dev/components/toast-stack",
      "markdown": "https://uiarc.dev/components/toast-stack/markdown",
      "registry": "https://uiarc.dev/r/toast-stack.json",
      "keywords": [
        "react toast stack",
        "toast notifications",
        "sonner alternative",
        "stacked toasts",
        "notification queue",
        "promise toast",
        "undo toast"
      ],
      "whenToUse": [
        "App-wide notifications for async results, errors, and undo.",
        "Loading toasts that morph into success or error when work finishes.",
        "Toasts with an action button, like View or Undo."
      ],
      "whenNotToUse": [
        "Use toast for a single locally controlled confirmation.",
        "Use alert for persistent messages tied to a page.",
        "Use notification-center for a history people can come back to."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The viewport is a labelled section with aria-live=\"polite\" and aria-relevant=\"additions text\", so new and updated toasts are announced.",
        "Each toast prefixes a screen-reader-only type label such as \"Error:\"; outgoing copy is aria-hidden while it fades.",
        "Hidden and leaving toasts are inert. When a focused toast closes, keyboard focus moves to the next toast or back to where it came from.",
        "Timers pause while the stack is hovered, focused, dragged, or the tab is hidden."
      ],
      "motion": [
        "New toasts rise from their own height below the edge; older ones tuck behind at 14px peeks and 0.05 scale steps, and fan out on a morph spring.",
        "Swiping right follows the finger and throws with velocity; left rubber-bands. Updates morph the icon, copy, height, and action width in place.",
        "Reduced motion jumps positions and uses short opacity fades."
      ],
      "responsive": [
        "The viewport is min(22.5rem, 100% minus a 32px gutter) wide; below 30rem it centers at the bottom regardless of position.",
        "Below a 21rem toast width the action button drops under the copy instead of squeezing it.",
        "On touch, a tap fans out the stack, and a right swipe dismisses a toast."
      ],
      "performance": [
        "At most 12 toasts are kept by default and only 3 show; each has its own ResizeObserver for height.",
        "Timers pause while hovered, focused, dragged, or when the tab is hidden."
      ],
      "aiNotes": [
        "Default for app-wide notifications, async results, and undo. Use toast for a single locally controlled confirmation and alert for persistent inline messages.",
        "Render ToastStack once inside ToastStackProvider and call useToastStack anywhere below. Reuse an id or call update to morph loading into success or error."
      ],
      "related": [
        "toast",
        "alert",
        "hold-to-confirm"
      ],
      "usage": "import { ToastStack, ToastStackProvider, useToastStack } from \"@/components/arc/toast-stack/toast-stack\";\n\nfunction PublishButton() {\n  const { toast, update } = useToastStack();\n  async function publish() {\n    const id = toast({ type: \"loading\", title: \"Publishing\" });\n    await api.publish();\n    update(id, { type: \"success\", title: \"Published\", action: { label: \"View\", onClick: openSite } });\n  }\n  return <button type=\"button\" onClick={publish}>Publish</button>;\n}\n\nexport function App() {\n  return <ToastStackProvider><PublishButton /><ToastStack /></ToastStackProvider>;\n}",
      "examples": [
        {
          "title": "Undo action",
          "code": "const { toast, dismiss } = useToastStack();\n\ntoast({\n  type: \"info\",\n  title: \"Message archived\",\n  action: { label: \"Undo\", onClick: id => { restore(); dismiss(id); } },\n});"
        }
      ],
      "exports": [
        "ToastStackProvider",
        "ToastStack",
        "useToastStack"
      ]
    },
    {
      "name": "usage-meter",
      "kind": "component",
      "title": "Usage meter",
      "description": "Show what fills an allowance and how close it is to the limit.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "meter",
        "usage",
        "storage"
      ],
      "docs": "https://uiarc.dev/components/usage-meter",
      "markdown": "https://uiarc.dev/components/usage-meter/markdown",
      "registry": "https://uiarc.dev/r/usage-meter.json",
      "keywords": [
        "react usage meter",
        "storage usage bar",
        "quota meter",
        "plan limit indicator",
        "segmented progress bar",
        "billing usage"
      ],
      "whenToUse": [
        "Quota against a fixed allowance, such as storage, seats, or API calls.",
        "Billing pages that should show what takes the space and how close the plan is to full.",
        "Plans that can go over the limit, where the overage should be clear without relying on color."
      ],
      "whenNotToUse": [
        "Use progress for task completion.",
        "Use bar-chart to compare categories without a limit.",
        "Use gauge or stat-card for a single headline metric."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The meter is a role=\"group\" labelled by its title; the bar is role=\"img\" with a full text summary of every segment and the status.",
        "Rolling numbers are aria-hidden with screen-reader-only text alongside.",
        "Status changes (near limit, over limit, growing overage) are announced in a polite live region.",
        "Legend items are buttons with aria-pressed and labels like \"Media, 14.2 GB\"."
      ],
      "motion": [
        "Segments grow in left to right once scrolled into view, then re-flow on one spring each when values change; crossing the limit fades in a hatched overage and limit marker.",
        "Digits roll like an odometer in the direction the number moved; the status badge springs to its new width.",
        "Reduced motion jumps values and swaps text with short fades."
      ],
      "responsive": [
        "The legend is an auto-fit grid of 108px minimum columns, so it wraps to fewer columns on narrow cards.",
        "Hovering the bar highlights a category on mouse; on touch a tap pins it instead.",
        "The caption ellipsizes on one line, so long labels do not push the layout."
      ],
      "performance": [
        "Segments animate in once via useInView at 40% visibility, then re-flow on springs when values change.",
        "A ResizeObserver tracks the bar width; up to four segments read clearly."
      ],
      "aiNotes": [
        "Use for quota against a fixed allowance: storage, seats, API calls. Use progress for task completion and bar-chart for comparing categories without a limit.",
        "Keep segment ids stable across renders so updates animate instead of remounting."
      ],
      "related": [
        "progress",
        "bar-chart",
        "gauge"
      ],
      "usage": "import { UsageMeter } from \"@/components/arc/usage-meter/usage-meter\";\n\nexport function StorageCard() {\n  return (\n    <UsageMeter\n      label=\"Workspace storage\"\n      unit=\"GB\"\n      limit={50}\n      segments={[\n        { id: \"files\", label: \"Files\", value: 21.4 },\n        { id: \"media\", label: \"Media\", value: 14.2 },\n        { id: \"backups\", label: \"Backups\", value: 6.8 },\n      ]}\n    />\n  );\n}",
      "exports": [
        "UsageMeter"
      ]
    },
    {
      "name": "onboarding-checklist",
      "kind": "component",
      "title": "Onboarding checklist",
      "description": "Guide a new workspace through setup, one step at a time.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "onboarding",
        "checklist",
        "getting started"
      ],
      "docs": "https://uiarc.dev/components/onboarding-checklist",
      "markdown": "https://uiarc.dev/components/onboarding-checklist/markdown",
      "registry": "https://uiarc.dev/r/onboarding-checklist.json",
      "keywords": [
        "react onboarding checklist",
        "getting started checklist",
        "setup checklist",
        "user onboarding",
        "progress checklist",
        "task list onboarding"
      ],
      "whenToUse": [
        "A one-time getting-started card with three to eight loosely ordered steps.",
        "Setup steps that run async actions, like Connect GitHub, and check off when they resolve.",
        "Checklists people can hide and come back to, with state persisted through callbacks."
      ],
      "whenNotToUse": [
        "Use stepper when the order is strict and steps map to screens.",
        "Use multi-step-form when every step collects form input.",
        "Use accordion for plain collapsible content without completion."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each step has a role=\"checkbox\" button with aria-checked, and a title button with aria-expanded controlling a labelled region.",
        "The ring is role=\"progressbar\" with aria-valuetext like \"3 of 5 steps done\"; progress and errors are announced via role=\"status\" and role=\"alert\".",
        "When a focused step finishes, focus moves to the next step's action; leaving rows and the hidden body are inert."
      ],
      "motion": [
        "Checks fill from the center and draw their tick; after 720ms the finished step folds into the completed group while the ring and count roll forward.",
        "Rows and panels open and close on a non-overshooting spring; the title morphs to doneTitle and a tick draws in the ring at 100%.",
        "Reduced motion removes height and scale animation and keeps short fades."
      ],
      "responsive": [
        "The card is a container query context; below 380px the Hide button collapses to an icon.",
        "Step titles and checkboxes keep 40px targets, and the completed group toggle is 44px tall."
      ],
      "performance": [
        "Steps are not virtualized; the component is built for short lists.",
        "Each finished step waits 720ms on a timer before folding into the completed group."
      ],
      "aiNotes": [
        "Use for a short, one-time setup of three to eight loosely ordered steps. Use stepper when the order is strict and steps map to screens.",
        "Make onAction async and throw an Error with a readable message on failure. Persist onCompletedChange and onHiddenChange to restore state via the default props."
      ],
      "related": [
        "stepper",
        "progress",
        "accordion",
        "checkbox"
      ],
      "usage": "import { OnboardingChecklist } from \"@/components/arc/onboarding-checklist/onboarding-checklist\";\n\nexport function GettingStarted() {\n  return (\n    <OnboardingChecklist\n      title=\"Set up your workspace\"\n      defaultCompleted={savedIds}\n      onCompletedChange={saveIds}\n      steps={[\n        { id: \"invite\", title: \"Invite your team\", actionLabel: \"Invite\", onAction: openInvite },\n        { id: \"connect\", title: \"Connect GitHub\", description: \"Sync issues and PRs.\", actionLabel: \"Connect\", onAction: connectGithub },\n        { id: \"profile\", title: \"Add a profile photo\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "OnboardingChecklist"
      ]
    },
    {
      "name": "image-compare",
      "kind": "component",
      "title": "Image compare",
      "description": "Drag a divider across two images to see what changed.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "before after",
        "comparison",
        "image"
      ],
      "docs": "https://uiarc.dev/components/image-compare",
      "markdown": "https://uiarc.dev/components/image-compare/markdown",
      "registry": "https://uiarc.dev/r/image-compare.json",
      "keywords": [
        "react image compare",
        "before after slider",
        "image comparison slider",
        "compare images",
        "photo diff slider"
      ],
      "whenToUse": [
        "Before and after visuals such as photo edits, redesigns, or cleanup results.",
        "Top and bottom comparisons, via orientation vertical."
      ],
      "whenNotToUse": [
        "Use carousel to browse many images.",
        "Use resizable-panels to split live content rather than images."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The handle is a focusable role=\"slider\" with aria-orientation and aria-valuetext like \"40% after\".",
        "Caption chips are aria-hidden; give both images meaningful alt text.",
        "Dragging works anywhere on the frame, not only on the handle."
      ],
      "motion": [
        "The divider follows the pointer and settles with release velocity; the handle stretches into a capsule while dragging.",
        "Keyboard presses at an edge give a small bump; captions fade as the divider reaches them.",
        "Reduced motion jumps the divider and handle without springs."
      ],
      "responsive": [
        "The frame is full width with a fixed aspect ratio, so it scales to any column.",
        "On touch the photo waits for a sideways drag before moving the divider, so vertical swipes still scroll the page; a tap jumps the divider there."
      ],
      "performance": [
        "The divider runs on motion values and one ResizeObserver, with no React re-render per drag frame.",
        "Both images stay mounted and stacked, so size them for the frame."
      ],
      "aiNotes": [
        "Use for visual diffs: photo edits, redesigns, before and after results. Use carousel to browse many images.",
        "Both images should share the same crop and size so the reveal lines up."
      ],
      "related": [
        "carousel",
        "photo-grid",
        "slider",
        "resizable-panels"
      ],
      "usage": "import { ImageCompare } from \"@/components/arc/image-compare/image-compare\";\n\nexport function Retouch() {\n  return (\n    <ImageCompare\n      before={<img src=\"/photo-raw.jpg\" alt=\"Unedited photo\" />}\n      after={<img src=\"/photo-edit.jpg\" alt=\"Retouched photo\" />}\n      aspectRatio=\"4 / 3\"\n    />\n  );\n}",
      "exports": [
        "ImageCompare"
      ]
    },
    {
      "name": "carousel",
      "kind": "component",
      "title": "Carousel",
      "description": "Browse a row of slides by dragging, flicking, or arrowing through them.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "carousel",
        "slider",
        "gallery"
      ],
      "docs": "https://uiarc.dev/components/carousel",
      "markdown": "https://uiarc.dev/components/carousel/markdown",
      "registry": "https://uiarc.dev/r/carousel.json",
      "keywords": [
        "react carousel",
        "image slider",
        "swipe carousel",
        "autoplay carousel",
        "accessible carousel",
        "slideshow component"
      ],
      "whenToUse": [
        "A short set of peer items where neighbouring slides hint at more.",
        "Galleries browsed by drag, flick, trackpad swipe, or keys.",
        "Rotating highlights with a visible pause control, via interval."
      ],
      "whenNotToUse": [
        "Use cover-flow for a showpiece gallery and photo-grid to show everything at once.",
        "Use card-stack for deciding on items one at a time."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a section with aria-roledescription=\"carousel\"; slides are labelled tabpanels and the indicators a role=\"tablist\".",
        "Inactive slides are inert, so their links are not focusable.",
        "Slide changes are announced in a polite live region.",
        "Rotation pauses on hover, keyboard focus, and drag, and has a visible pause control."
      ],
      "motion": [
        "Slides follow the finger and settle with momentum; neighbors shrink and dim with distance.",
        "The indicator stretches into a pill that tracks the slides, and drains to show rotation time.",
        "Reduced motion jumps between slides and disables rotation."
      ],
      "responsive": [
        "Slides default to min(80cqw, 340px), so they size to the carousel's container rather than the viewport.",
        "Controls tighten when the container is under 340px."
      ],
      "performance": [
        "All slides stay mounted; inactive ones are inert, so keep the set short.",
        "Rotation pauses offscreen via IntersectionObserver and in hidden tabs via visibilitychange."
      ],
      "aiNotes": [
        "Use for a short set of peer items where neighbors should hint at more. Use cover-flow for a showpiece gallery and photo-grid to show everything at once.",
        "Give each slide a stable key if the list can change, and write a specific label.",
        "Keep autoplay off unless rotation is essential."
      ],
      "related": [
        "cover-flow",
        "photo-grid",
        "card-stack",
        "image-compare"
      ],
      "usage": "import { Carousel } from \"@/components/arc/carousel/carousel\";\n\nexport function Suites() {\n  return (\n    <Carousel label=\"Harbour suites\" interval={5000}>\n      <img src=\"/suite-1.jpg\" alt=\"Sea view suite\" />\n      <img src=\"/suite-2.jpg\" alt=\"Garden suite\" />\n      <img src=\"/suite-3.jpg\" alt=\"Loft suite\" />\n    </Carousel>\n  );\n}",
      "exports": [
        "Carousel"
      ]
    },
    {
      "name": "card-stack",
      "kind": "component",
      "title": "Card stack",
      "description": "Review a deck one card at a time, with a throw and an undo.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "gesture",
        "motion",
        "triage"
      ],
      "docs": "https://uiarc.dev/components/card-stack",
      "markdown": "https://uiarc.dev/components/card-stack/markdown",
      "registry": "https://uiarc.dev/r/card-stack.json",
      "keywords": [
        "react card stack",
        "swipe cards",
        "tinder swipe",
        "swipeable card deck",
        "card triage",
        "swipe left right"
      ],
      "whenToUse": [
        "One-by-one triage such as shortlisting, review queues, or onboarding picks.",
        "Decks where swiping left or right should be undoable."
      ],
      "whenNotToUse": [
        "Use a list or sortable-data-table when people compare items side by side.",
        "Use carousel to browse items without deciding.",
        "Use swipe-actions for quick actions on rows in a list."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stage is a focusable role=\"group\" with aria-roledescription=\"card stack\" and a hidden hint describing drag and keys.",
        "Each card is labelled like \"Lisbon, 1 of 3\".",
        "Decisions, undo, and reset are announced in a polite status region.",
        "Buttons mirror every gesture, so swiping is never required."
      ],
      "motion": [
        "The top card follows the drag with tilt and stamps; a flick or far drag throws it off with its velocity.",
        "Cards beneath rise into place, and undo flies a card back from where it left.",
        "Reduced motion fades cards out and in at home without travel."
      ],
      "responsive": [
        "The stage is min(100%, 20rem) wide; below 26rem the cards get shorter, and below 22.5rem Undo becomes icon-only.",
        "Cards use touch-action pan-y, so vertical swipes still scroll the page."
      ],
      "performance": [
        "Only the top four cards render in the stage, however long the deck.",
        "Drag and throw run on motion values without per-frame React updates."
      ],
      "aiNotes": [
        "Use for one-by-one triage: shortlisting, review queues, onboarding picks. Use a list or sortable-data-table when people compare items side by side.",
        "Persist decisions from onDecide and reverse them in onUndo."
      ],
      "related": [
        "swipe-actions",
        "carousel",
        "wallet-stack"
      ],
      "usage": "import { CardStack } from \"@/components/arc/card-stack/card-stack\";\n\nconst cities = [{ id: \"lis\", name: \"Lisbon\" }, { id: \"osl\", name: \"Oslo\" }, { id: \"kyo\", name: \"Kyoto\" }];\n\nexport function Shortlist() {\n  return (\n    <CardStack\n      items={cities}\n      getKey={city => city.id}\n      getLabel={city => city.name}\n      renderCard={city => <h3>{city.name}</h3>}\n      labels={{ left: \"Skip\", right: \"Shortlist\" }}\n      onDecide={(city, decision) => save(city.id, decision)}\n    />\n  );\n}",
      "exports": [
        "CardStack"
      ]
    },
    {
      "name": "morph-nav",
      "kind": "component",
      "title": "Morph nav",
      "description": "A navigation bar that morphs into rich menus, search, and a compact state as one surface.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "navigation",
        "morph",
        "tabs"
      ],
      "docs": "https://uiarc.dev/components/morph-nav",
      "markdown": "https://uiarc.dev/components/morph-nav/markdown",
      "registry": null,
      "keywords": [
        "react navigation bar",
        "react morph nav",
        "mega menu",
        "animated navbar",
        "site header with dropdown",
        "navbar with search",
        "stripe style navigation"
      ],
      "whenToUse": [
        "The top navigation of a marketing or product site with a few sections that each hold several destinations.",
        "Headers where search should grow out of the bar instead of opening a separate modal.",
        "Sites that want one primary action, such as Sign up, pinned to the end of the bar."
      ],
      "whenNotToUse": [
        "Use liquid-tab-bar for an app-style switcher between a handful of peer sections.",
        "Use tabs for switching views inside a page.",
        "Use dropdown-menu when you only need one menu of commands, not site navigation."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders a nav landmark; the current item carries aria-current=\"page\" and panel triggers use aria-expanded and aria-controls.",
        "Open panels are regions labelled by their trigger; the panel closes when focus leaves the nav.",
        "Search is a combobox driving a listbox with aria-activedescendant, and the result count is announced politely. With an empty query it lists the first five entries as Suggested.",
        "A pointer down outside the nav closes the panel or search."
      ],
      "motion": [
        "The surface springs to each panel's size, with a little bounce when it grows and none when it shrinks. Moving to a neighbour slides the new panel in from that side while a highlight glides between items.",
        "Hover opens a panel after a 60ms intent delay and leaving the nav closes it after 180ms, so crossing a gap does not flicker it shut.",
        "Search grows out of the same surface as the bar fades and blurs away; compact and narrow states retune the bar.",
        "Reduced motion drops travel, scale, and blur and keeps short fades."
      ],
      "responsive": [
        "The root is a container; below collapseBelow (720px of its own width by default) the items fold into one menu.",
        "Panels and search cap at the container width minus a gutter, and below 420px viewport width link panels drop to one column.",
        "Hover-to-open only reacts to mouse pointers, so touch opens panels on tap without a hover flash."
      ],
      "performance": [
        "The bar uses a 22px backdrop blur, which falls back to a solid surface under prefers-reduced-transparency and prefers-contrast.",
        "Two ResizeObservers track the container and the open panel size; only the open panel is mounted.",
        "Search ranks entries in memory on each keystroke and shows at most six results, so large indexes should be pre-filtered."
      ],
      "aiNotes": [
        "Choose it for the top navigation of a marketing or product site with a handful of sections that each hold several destinations. Use tabs for in-page view switching and liquid-tab-bar for an app-style section switcher.",
        "Pass current from the router and compact from a scroll listener. Route in onNavigate for client-side navigation; plain items and links with href also work as anchors."
      ],
      "related": [
        "tabs",
        "liquid-tab-bar",
        "dropdown-menu",
        "search-field",
        "popover"
      ],
      "usage": "import { MorphNav } from \"@/components/arc/morph-nav/morph-nav\";\n\nexport function SiteHeader({ scrolled }: { scrolled: boolean }) {\n  return (\n    <MorphNav\n      brand={{ name: \"Arc\", mark: <Logo />, href: \"/\" }}\n      items={[\n        { value: \"product\", label: \"Product\", links: [\n          { label: \"Components\", description: \"Motion-first building blocks\", href: \"/components\" },\n          { label: \"Blocks\", description: \"Full sections\", href: \"/blocks\" },\n        ] },\n        { value: \"pricing\", label: \"Pricing\", href: \"/pricing\" },\n      ]}\n      search={{ items: [{ label: \"Dialog\", group: \"Components\", href: \"/components/dialog\" }] }}\n      action={{ label: \"Sign up\", href: \"/sign-up\" }}\n      current=\"product\"\n      compact={scrolled}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Tighten on scroll",
          "code": "const [scrolled, setScrolled] = useState(false);\nuseEffect(() => {\n  const onScroll = () => setScrolled(window.scrollY > 24);\n  window.addEventListener(\"scroll\", onScroll, { passive: true });\n  return () => window.removeEventListener(\"scroll\", onScroll);\n}, []);\n\n<MorphNav items={items} current={section} compact={scrolled} onNavigate={d => d.href && router.push(d.href)} />"
        }
      ],
      "exports": [
        "MorphNav"
      ]
    },
    {
      "name": "dock",
      "kind": "component",
      "title": "Dock",
      "description": "A floating tool dock with a gliding label, a springy selection, and trays that grow out of it.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "dock",
        "toolbar"
      ],
      "docs": "https://uiarc.dev/components/dock",
      "markdown": "https://uiarc.dev/components/dock/markdown",
      "registry": null,
      "keywords": [
        "react dock",
        "floating toolbar",
        "tool switcher",
        "canvas toolbar",
        "macos dock",
        "toolbar with tray",
        "reorderable toolbar"
      ],
      "whenToUse": [
        "A floating tool or mode switcher, such as the tools of a canvas or whiteboard editor.",
        "Toolbars where related tools share one slot and open a tray, like shape variants.",
        "Tool sets people may reorder by drag or Alt with the arrow keys."
      ],
      "whenNotToUse": [
        "Use segmented-control for two to four inline options.",
        "Use action-button for one-off actions that do not stay selected.",
        "Use liquid-tab-bar for switching between app sections rather than tools."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Items sit in a horizontal toolbar with a roving tabindex; plain items use aria-pressed for selection.",
        "Groups use aria-haspopup, aria-expanded, and aria-controls pointing at the tray, which is a labelled group that stays inert while closed.",
        "Accessible names include the shortcut hint and badge count, such as \"Pen, P\". A group reads as its label and shown member, such as \"Shapes: Ellipse, O\", plus \"selected\" when it holds the value.",
        "The floating label is aria-hidden. It shows on mouse hover and on keyboard focus, not on touch.",
        "A pointer down outside the dock closes an open tray."
      ],
      "motion": [
        "The label glides between items after a short first hover delay and stays warm so the next one follows at once; the accent tint springs between items with a shared layoutId whose corner radius is corrected, so it never distorts.",
        "A group's glyph crossfades with a short blur when its member changes; a press dips the item on a quick spring and releases with a little overshoot.",
        "Opening a group grows a tab of the same material out of the dock with members staggering in; dragged items lift and neighbours reflow on a layout spring.",
        "Reduced motion makes the label, tray, and selection jump and keeps short fades."
      ],
      "responsive": [
        "The dock sizes to its items at a fixed 40px slot and caps at max-width 100%; it does not shrink items, so keep the count small on phones.",
        "The tray and label rise above the dock on purpose, so leave room over it in the layout.",
        "Labels only appear for mouse hover and keyboard focus; touch shows no label, so pick icons that read on their own.",
        "Item slots use touch-action pan-y, so a vertical swipe over the dock still scrolls the page while sideways drags reorder when onItemsChange is set."
      ],
      "performance": [
        "Selection is one shared layoutId element and drag uses Reorder, so only the moved items reflow.",
        "A single ResizeObserver measures the bar. The tray mounts when a group first opens and then stays mounted, inert, for the last opened group."
      ],
      "aiNotes": [
        "Choose it for a floating tool or mode switcher, such as the tools of a canvas editor. Use segmented-control for two to four inline options and action-button for one-off toolbar actions.",
        "Control value and handle shortcut keys on the page; the dock only displays them. Use items on an entry to group related tools under one slot.",
        "Pass onItemsChange only if people may reorder the dock."
      ],
      "related": [
        "segmented-control",
        "action-button",
        "liquid-tab-bar",
        "reorderable-list",
        "orbit-menu"
      ],
      "usage": "import { Dock } from \"@/components/arc/dock/dock\";\nimport { Hand, MousePointer2, Pencil, Square, Circle } from \"lucide-react\";\n\nexport function CanvasTools() {\n  const [tool, setTool] = useState(\"select\");\n  const [items, setItems] = useState([\n    { id: \"select\", label: \"Select\", icon: <MousePointer2 />, shortcut: \"V\" },\n    { id: \"hand\", label: \"Hand\", icon: <Hand />, shortcut: \"H\" },\n    { id: \"shapes\", label: \"Shapes\", icon: <Square />, items: [\n      { id: \"rect\", label: \"Rectangle\", icon: <Square />, shortcut: \"R\" },\n      { id: \"ellipse\", label: \"Ellipse\", icon: <Circle />, shortcut: \"O\" },\n    ] },\n    { id: \"pen\", label: \"Pen\", icon: <Pencil />, shortcut: \"P\" },\n  ]);\n  return <Dock items={items} value={tool} onValueChange={setTool} onItemsChange={next => setItems(next)} />;\n}",
      "exports": [
        "Dock"
      ]
    },
    {
      "name": "wallet-stack",
      "kind": "component",
      "title": "Wallet stack",
      "description": "Fan a stack of cards and lift one out to see its activity.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "wallet",
        "cards"
      ],
      "docs": "https://uiarc.dev/components/wallet-stack",
      "markdown": "https://uiarc.dev/components/wallet-stack/markdown",
      "registry": null,
      "keywords": [
        "react wallet",
        "credit card stack",
        "apple wallet ui",
        "payment cards component",
        "card stack animation",
        "fintech card ui"
      ],
      "whenToUse": [
        "A few payment cards, passes, or memberships where people open one to see its balance and activity.",
        "Fintech or wallet screens that want a physical card metaphor with realistic materials."
      ],
      "whenNotToUse": [
        "Use card-stack for generic swipeable content that is not a card wallet.",
        "Use a plain list or data-grid for many accounts.",
        "Use expandable-card when one card just needs to grow in place."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stack is a labelled group with a described keyboard hint. Only the front card is in the tab order (roving tabindex); each card is a button named by issuer, product, last four digits, position, and balance, with aria-expanded while open.",
        "The masked digits and decorative artwork are hidden from assistive technology; only the last four digits are read.",
        "The activity region is inert until a card is open; the header switches from the card count to an All cards button that closes it.",
        "Cycling and opening are announced in a polite live region, and focus returns to the card when it goes back into the stack."
      ],
      "motion": [
        "Every card's transform is one pure function of a continuous stack position, the open amount, and the fan amount, so springs, drags, and interruptions all move the same shape. Z order never changes, so cards never pop over one another.",
        "Hovering fans the stack upward on a no-overshoot spring; the hovered card rises out of the fan and its shadow deepens. The front card tilts up to 5 degrees and a specular highlight follows the pointer, or device tilt on phones that report it.",
        "Dragging down pushes the front card into the pocket at the bottom and brings the next one forward one to one under the pointer; dragging up pulls it back. A flick lands where its momentum projects, one card at a time, with rubber banding past the first and last card.",
        "Choosing a card lifts it to the top on one critically damped spring while the others tuck into the pocket; its balance counts up and three transactions stagger in. Pull the open card down to put it back, or flick it.",
        "Only transform and opacity animate: shadows fade their opacity, the light moves a pre-painted layer. MotionConfig and a reduced motion branch remove travel, tilt, and the light; state changes apply instantly."
      ],
      "responsive": [
        "The wallet is min(100%, 18.5rem) wide. Cards keep the ID-1 ratio of 85.6 by 53.98 and every detail is sized in container units, so the card scales as one object; the stage height follows the card height.",
        "The stage uses touch-action pan-x, so a vertical drag cycles cards while horizontal page gestures still work. An open card switches to touch-action none so it can be pulled down by touch.",
        "Pointer tilt and the fan react to mouse only; on phones that report device orientation the light follows the tilt of the device instead."
      ],
      "performance": [
        "Position, open amount, fan, and stage size are motion values, so gestures and springs never re-render React; the stage is measured once before paint and on resize.",
        "Only transform and opacity animate. Shadows and the specular light are painted once and faded or moved; the stage clips vertically instead of masking so the glass card's backdrop blur stays cheap and correct.",
        "Every card renders its artwork and transactions; keep the stack to a handful of cards."
      ],
      "aiNotes": [
        "Choose it for a few payment cards, passes, or memberships where people open one at a time to see its activity. Use card-stack for generic swipeable content and a plain list for many accounts.",
        "Pass fully shaped cards including balanceLabel and transactions, and a variant per card so the materials read as different cards. Selection is managed internally and reported through onSelectedChange.",
        "The network mark is fictional. Never pass real card numbers; only lastFour is shown."
      ],
      "related": [
        "card-stack",
        "expandable-card"
      ],
      "usage": "import { WalletStack } from \"@/components/arc/wallet-stack/wallet-stack\";\n\nexport function Cards() {\n  return (\n    <WalletStack\n      cards={[\n        { id: \"debit\", issuer: \"Northbank\", product: \"Everyday debit\", lastFour: \"4821\", holder: \"Ada Lovelace\", expires: \"09/29\", variant: \"metal\",\n          balance: 2480.5, balanceLabel: \"Available\",\n          transactions: [{ id: \"t1\", merchant: \"Blue Bottle\", detail: \"Coffee · Today\", amount: -6.5 }] },\n        { id: \"travel\", issuer: \"Northbank\", product: \"Travel credit\", lastFour: \"0917\", holder: \"Ada Lovelace\", expires: \"03/28\", variant: \"color\", tint: \"oklch(42% .15 262)\",\n          balance: 812.4, balanceLabel: \"Spent this month\", transactions: [] },\n      ]}\n      onSelectedChange={card => console.log(card?.id)}\n    />\n  );\n}",
      "exports": [
        "WalletStack"
      ]
    },
    {
      "name": "liquid-tab-bar",
      "kind": "component",
      "title": "Liquid tab bar",
      "description": "Tabs with a lens selection that slides between them, swaps labels cleanly, and fills in icons as it passes.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "tabs",
        "navigation"
      ],
      "docs": "https://uiarc.dev/components/liquid-tab-bar",
      "markdown": "https://uiarc.dev/components/liquid-tab-bar/markdown",
      "registry": null,
      "keywords": [
        "react tab bar",
        "liquid glass tabs",
        "animated tab bar",
        "ios tab bar",
        "bottom navigation",
        "tab bar with badges",
        "gooey tabs"
      ],
      "whenToUse": [
        "Switching between a handful of peer sections with icons, such as an app's main areas.",
        "Bottom or top bars on mobile-style layouts where the selection should feel physical.",
        "Tabs that carry badge counts for new items."
      ],
      "whenNotToUse": [
        "Use tabs for text-only panels inside a page.",
        "Use segmented-control for compact filters or view toggles.",
        "Use morph-nav for site navigation with dropdown panels."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Uses the tablist and tab roles with aria-selected, aria-controls, and a roving tabindex.",
        "Each tab's accessible name includes its badge, using badgeLabel when given.",
        "The inverted copy inside the lens is decorative; only one set of tabs is exposed."
      ],
      "motion": [
        "The lens slides and resizes to the new tab on the morph spring as one shape, with no stretch or squash; it leans slightly toward a hovered tab.",
        "The label being left fades out at once and the new label fades in only as the lens arrives, so two labels never show together; icons, labels, and badges keep their places.",
        "Grab the lens on the selected tab and drag or throw it; it lands where its momentum reaches, a quick flick always moves at least one tab, and it resists past either end. The selected tab's label opens on a spring and badge digits roll.",
        "Reduced motion jumps the lens and labels to their places without travel or lean."
      ],
      "responsive": [
        "Slots are a fixed 44px plus the open label, and the bar caps at max-width 100%; set labels to none when space is tight.",
        "The bar uses touch-action pan-y, so dragging the lens sideways works on touch without blocking page scroll.",
        "Hover lean follows the mouse only; touch selects on tap or throw."
      ],
      "performance": [
        "The lens animates a clip-path over an inverted copy of the tabs, so every tab is rendered twice.",
        "Label widths are measured from hidden copies whenever tabs change; keep the tab count to a handful."
      ],
      "aiNotes": [
        "Choose it for switching between a handful of peer sections with icons, such as an app's main areas. Use tabs for text-only in-page panels and segmented-control for compact filters.",
        "It is controlled: keep value in state. Wire panels with liquidPanelId and liquidTabId using the same id prop."
      ],
      "related": [
        "tabs",
        "segmented-control",
        "morph-nav",
        "dock"
      ],
      "usage": "import { LiquidTabBar, liquidPanelId, liquidTabId } from \"@/components/arc/liquid-tab-bar/liquid-tab-bar\";\nimport { Home, Library, Radio } from \"lucide-react\";\n\nexport function Sections() {\n  const [tab, setTab] = useState(\"home\");\n  return (\n    <>\n      <LiquidTabBar id=\"nav\" value={tab} onValueChange={setTab} tabs={[\n        { value: \"home\", label: \"Home\", icon: <Home /> },\n        { value: \"radio\", label: \"Radio\", icon: <Radio />, badge: 2, badgeLabel: \"2 new\" },\n        { value: \"library\", label: \"Library\", icon: <Library /> },\n      ]} />\n      <section id={liquidPanelId(\"nav\", tab)} role=\"tabpanel\" aria-labelledby={liquidTabId(\"nav\", tab)}>{/* ... */}</section>\n    </>\n  );\n}",
      "exports": [
        "LiquidTabBar",
        "liquidTabId / liquidPanelId"
      ]
    },
    {
      "name": "now-playing",
      "kind": "component",
      "title": "Now playing",
      "description": "Grow a mini player into the full player in one continuous morph.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "media",
        "music"
      ],
      "docs": "https://uiarc.dev/components/now-playing",
      "markdown": "https://uiarc.dev/components/now-playing/markdown",
      "registry": null,
      "keywords": [
        "react music player",
        "now playing bar",
        "mini player",
        "audio player component",
        "spotify style player",
        "waveform scrubber"
      ],
      "whenToUse": [
        "Media that keeps playing while people browse, such as music, podcasts, or audio lessons.",
        "A mini player bar that expands into a full player with artwork and a scrubber."
      ],
      "whenNotToUse": [
        "Use bottom-sheet or dialog for a one-off overlay that is not a player.",
        "Use voice-recorder for recording and playing back short voice notes.",
        "Use slider for a plain seek bar without the player chrome."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The waveform is a slider with aria-valuetext such as \"1:05 of 3:32\".",
        "Expand and collapse buttons report aria-expanded. Focus moves to the collapse button on open and back to the mini bar on close.",
        "Previous restarts the current track when more than 3 seconds have played, and skips back otherwise. A restart calls onSeek(0).",
        "The current track and play state are announced in a polite live region; artwork is decorative unless artworkAlt is set."
      ],
      "motion": [
        "The mini bar morphs into the full player with shared layout while the shell height springs to fit. In the full player, pulling the artwork down more than 70px or flicking it closes the player; an upward pull rubber-bands.",
        "Artwork slides and blurs in the direction you skip; the waveform stretches under a scrub and level bars dance while playing.",
        "MotionConfig follows the user's reduced motion setting, replacing travel with short fades; the level bars hold still."
      ],
      "responsive": [
        "The shell is min(100%, 21rem) wide; below 360px viewport width the mini bar hides the next button to fit.",
        "The expanded artwork uses touch-action none for the pull-down gesture, while the waveform keeps pan-y so the page still scrolls."
      ],
      "performance": [
        "Playback is an internal useAnimationFrame clock that ticks every frame; it returns early while paused but the frame callback stays registered.",
        "The waveform is 54 bars and one ResizeObserver; wire a real audio element through the callbacks rather than the internal clock for long sessions."
      ],
      "aiNotes": [
        "Choose it for media that keeps playing while people browse. For a one-off overlay use bottom-sheet or dialog.",
        "Playback runs on an internal clock, so it works without audio; wire onPlayingChange and onIndexChange to a real audio element when needed."
      ],
      "related": [
        "bottom-sheet",
        "cover-flow",
        "slider",
        "voice-recorder"
      ],
      "usage": "import { useRef, useState } from \"react\";\nimport { NowPlaying } from \"@/components/arc/now-playing/now-playing\";\n\nconst tracks = [\n  { id: \"1\", title: \"Night Drive\", artist: \"Arc Ensemble\", duration: 212, artwork: \"/covers/night-drive.jpg\", src: \"/audio/night-drive.mp3\" },\n  { id: \"2\", title: \"Low Tide\", artist: \"Harbor\", duration: 185, artwork: \"/covers/low-tide.jpg\", src: \"/audio/low-tide.mp3\" },\n];\n\nexport function Player() {\n  const audio = useRef<HTMLAudioElement>(null);\n  const [index, setIndex] = useState(0);\n  const [playing, setPlaying] = useState(false);\n  const [time, setTime] = useState(0);\n\n  return (\n    <>\n      <audio ref={audio} src={tracks[index].src} autoPlay={playing} onTimeUpdate={event => setTime(event.currentTarget.currentTime)} />\n      <NowPlaying\n        tracks={tracks}\n        index={index}\n        onIndexChange={setIndex}\n        playing={playing}\n        onPlayingChange={next => { setPlaying(next); if (next) audio.current?.play(); else audio.current?.pause(); }}\n        currentTime={time}\n        onSeek={seconds => { if (audio.current) audio.current.currentTime = seconds; }}\n      />\n    </>\n  );\n}",
      "exports": [
        "NowPlaying",
        "NowPlayingLevels"
      ]
    },
    {
      "name": "photo-grid",
      "kind": "component",
      "title": "Photo grid",
      "description": "Pinch through zoom levels and open any photo straight from its cell.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "photos",
        "gallery"
      ],
      "docs": "https://uiarc.dev/components/photo-grid",
      "markdown": "https://uiarc.dev/components/photo-grid/markdown",
      "registry": null,
      "keywords": [
        "react photo grid",
        "photo library",
        "pinch to zoom grid",
        "ios photos grid",
        "image gallery with viewer",
        "album grid",
        "photo viewer"
      ],
      "whenToUse": [
        "A browsable photo library or album where people zoom between column counts.",
        "Galleries that need favorites and a full-screen viewer with a filmstrip.",
        "Touch and trackpad users who expect pinch to change density."
      ],
      "whenNotToUse": [
        "Use lightbox-gallery for a masonry layout that keeps each photo's aspect ratio.",
        "Use carousel for a short inline slideshow.",
        "Use image-compare for before and after pairs."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The grid is a labelled list of buttons named by each photo's alt text, with \", favorite\" appended when favorited.",
        "Zoom buttons use aria-pressed; the viewer is a labelled dialog and its filmstrip is a slider with the photo title in aria-valuetext.",
        "Viewer changes are announced in a polite status region."
      ],
      "motion": [
        "Pinch zoom scales live and reflows around the photo under the fingers; the viewer grows from the tapped cell and can be dragged down to dismiss.",
        "Counters and favorite icons roll or pop in with a short blur.",
        "Reduced motion removes layout travel and keeps fades."
      ],
      "responsive": [
        "Columns come from zoomLevels, not breakpoints; the root is a container and needs a height, with a 320px minimum.",
        "Below 480px the header and filmstrip shrink, previous and next buttons hide, and swipe or the filmstrip pages instead.",
        "Previous and next arrows are hidden on hover-less devices, where swipe is the main gesture."
      ],
      "performance": [
        "The grid is not virtualized; every photo renders a thumbnail, so paginate very large libraries.",
        "The viewer only mounts photos near the current one, and passing srcSet keeps thumbnails light.",
        "The header and viewer use backdrop blur, which drops to solid surfaces under prefers-reduced-transparency."
      ],
      "aiNotes": [
        "Choose it for a browsable photo library or album. Use carousel for a short slideshow and image-compare for before/after.",
        "Always pass width and height so the viewer and filmstrip can fit each photo without layout shift; pass srcSet for sharp thumbnails."
      ],
      "related": [
        "carousel",
        "image-compare",
        "cover-flow",
        "dialog"
      ],
      "usage": "import { PhotoGrid, type GridPhoto } from \"@/components/arc/photo-grid/photo-grid\";\n\nexport function Album({ photos }: { photos: GridPhoto[] }) {\n  return (\n    <PhotoGrid\n      title=\"September\"\n      photos={photos}\n      defaultColumns={4}\n      onFavoriteChange={(id, favorite) => saveFavorite(id, favorite)}\n    />\n  );\n}",
      "exports": [
        "PhotoGrid"
      ]
    },
    {
      "name": "control-center",
      "kind": "component",
      "title": "Control center",
      "description": "Workspace quick settings: tiles that morph into detail, a duration dial, and rubber-banded meters.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "settings",
        "control center"
      ],
      "docs": "https://uiarc.dev/components/control-center",
      "markdown": "https://uiarc.dev/components/control-center/markdown",
      "registry": null,
      "keywords": [
        "react control center",
        "quick settings panel",
        "ios control center",
        "settings toggles",
        "focus mode ui",
        "volume slider panel"
      ],
      "whenToUse": [
        "A quick settings panel with a handful of toggles and levels for a web workspace.",
        "Focus session controls with a duration dial and allowed people.",
        "Notification channel toggles grouped behind one tile."
      ],
      "whenNotToUse": [
        "Use switch, slider, and radio-group directly in a regular settings form.",
        "Use time-dial to pick a time across time zones.",
        "Use dropdown-menu for a short list of commands."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Tiles are toggle buttons with aria-pressed; chevrons open a modal dialog with a focus trap and aria-haspopup.",
        "Meters and the session dial are sliders with formatted aria-valuetext; channel toggles use role=\"switch\".",
        "Focus modes are a radiogroup, and the grid behind an open detail is inert. Clicking the scrim closes the detail."
      ],
      "motion": [
        "A tile grows into its detail with a shared element morph, and meters follow the pointer 1:1 from where they were grabbed, with rubber-banded edges. A tap without a drag jumps the meter to that spot, and the track swells slightly while held.",
        "The session dial counts minutes as the knob travels, keeps counting across twelve o'clock, resists past 5 and 120 minutes, and snaps to 5 minute steps on release.",
        "Icons and status text swap with a short blur; the dial knob springs to snapped values.",
        "Reduced motion is applied through MotionConfig, removing travel and keeping fades."
      ],
      "responsive": [
        "The panel is min(100%, 22.5rem) wide with a fixed height, sized for a popover or side column.",
        "Below 360px viewport width padding tightens and the session dial shrinks from 164px to 150px.",
        "Meters keep touch-action pan-y so the page still scrolls, while the dial uses none so it can be turned by touch."
      ],
      "performance": [
        "Tiles morph into details through shared layoutIds; only one detail is mounted at a time.",
        "Meters follow the pointer through motion values, so drags do not re-render the panel per frame."
      ],
      "aiNotes": [
        "Choose it for a quick settings panel with a handful of toggles and levels. For ordinary forms use switch, slider, and radio-group directly.",
        "It is uncontrolled: seed it with defaults and persist through onStateChange. Pass lucide icon components (not elements) for focus modes."
      ],
      "related": [
        "switch",
        "slider",
        "radio-group",
        "time-dial",
        "dialog"
      ],
      "usage": "import { ControlCenter } from \"@/components/arc/control-center/control-center\";\nimport { Moon, Briefcase } from \"lucide-react\";\n\nexport function QuickSettings() {\n  return (\n    <ControlCenter\n      focusModes={[\n        { id: \"deep\", label: \"Deep work\", icon: Briefcase, description: \"Silences everything but mentions\", defaultMinutes: 50 },\n        { id: \"rest\", label: \"Rest\", icon: Moon, description: \"Silences all alerts\", defaultMinutes: 30 },\n      ]}\n      channels={[{ id: \"mentions\", label: \"Mentions\", defaultOn: true }, { id: \"deploys\", label: \"Deploys\" }]}\n      onStateChange={state => saveSettings(state)}\n    />\n  );\n}",
      "exports": [
        "ControlCenter"
      ]
    },
    {
      "name": "cover-flow",
      "kind": "component",
      "title": "Cover flow",
      "description": "A depth rail of images you can throw, with soft grounded shadows and a quiet reflection.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "cover flow",
        "carousel"
      ],
      "docs": "https://uiarc.dev/components/cover-flow",
      "markdown": "https://uiarc.dev/components/cover-flow/markdown",
      "registry": null,
      "keywords": [
        "react cover flow",
        "coverflow carousel",
        "3d carousel",
        "album carousel",
        "image rail",
        "itunes cover flow"
      ],
      "whenToUse": [
        "Six to twelve visual items where looking at one at a time is the point, such as albums, places, or collections.",
        "Showcase rails that should respond to drag, throw, and sideways trackpad scroll."
      ],
      "whenNotToUse": [
        "Use carousel for general slides or content cards.",
        "Use photo-grid or lightbox-gallery for a large library.",
        "Use depth-cards for about three rich choices that open into detail."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is labelled with aria-roledescription=\"carousel\"; each card is a group with aria-roledescription=\"slide\" and a label like \"2 of 8: Ridge Loop\".",
        "Cards away from the front are aria-hidden, and position ticks are labelled \"Show <title>\" with aria-current on the front one. Ticks are out of the tab order; the rail itself takes focus and handles the keys.",
        "The front card is announced in a polite live region."
      ],
      "motion": [
        "Drag or throw the rail and it lands on the card its momentum reaches, on a spring tuned to the release speed; a quick flick always moves at least one card. The ends rubber-band.",
        "Sideways trackpad scrolls move the rail directly, then settle on a card with the swipe's momentum. Clicking a card beside the front one brings it forward.",
        "Cards scale, dim, turn toward the center, and recede with distance while each image drifts inside its frame; the caption rises in from the direction of travel.",
        "Reduced motion jumps between cards, turns off the drift and the turn, and keeps short fades. A drag past 40px or one trackpad swipe steps one card."
      ],
      "responsive": [
        "Cards size with the container at clamp(168px, 38cqw, 272px), so the rail scales from phones to wide sections.",
        "The stage uses touch-action pan-y, so vertical swipes still scroll the page and sideways ones move the rail."
      ],
      "performance": [
        "Every card is rendered, but cards far from the front get visibility hidden; keep the collection to a few dozen at most.",
        "Transforms and image drift run on motion values, and one ResizeObserver reads the card width."
      ],
      "aiNotes": [
        "Choose it for six to twelve visual items where one at a time is the point (places, albums, collections). Use carousel for general slides and photo-grid for a large library.",
        "Items are generic, so pass your own records and render extra fields through renderCaption."
      ],
      "related": [
        "carousel",
        "card-stack",
        "photo-grid"
      ],
      "usage": "import { CoverFlow } from \"@/components/arc/cover-flow/cover-flow\";\n\nexport function Hikes() {\n  return (\n    <CoverFlow\n      label=\"Hikes for this fall\"\n      items={[\n        { id: \"ridge\", title: \"Ridge Loop\", subtitle: \"Mount Tam\", meta: \"8 km\", image: \"/hikes/ridge.jpg\" },\n        { id: \"falls\", title: \"Cataract Falls\", subtitle: \"Marin\", meta: \"5 km\", image: \"/hikes/falls.jpg\" },\n      ]}\n      onIndexChange={setIndex}\n    />\n  );\n}",
      "exports": [
        "CoverFlow"
      ]
    },
    {
      "name": "activity-rings",
      "kind": "component",
      "title": "Activity rings",
      "description": "Daily goals as tick rings that sweep, count up, and trace a second lap past 100%.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "activity",
        "rings"
      ],
      "docs": "https://uiarc.dev/components/activity-rings",
      "markdown": "https://uiarc.dev/components/activity-rings/markdown",
      "registry": null,
      "keywords": [
        "react activity rings",
        "apple watch rings",
        "fitness rings",
        "goal progress rings",
        "concentric progress",
        "habit tracker rings"
      ],
      "whenToUse": [
        "Daily goals that fill up and can be exceeded, like move, exercise, and stand.",
        "Fitness or habit dashboards with a week picker to compare days."
      ],
      "whenNotToUse": [
        "Use progress or gauge for a single value.",
        "Use usage-meter for quotas and limits.",
        "Use activity-heatmap for streaks across many weeks."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The rings are an img whose label lists each goal's percent for the day.",
        "Legend rows are toggle buttons with aria-pressed and a spoken summary such as \"Move, 420 of 500 kcal, 84 percent\".",
        "The week picker is a radiogroup with a roving tabindex, named by each day's full label."
      ],
      "motion": [
        "Every change sweeps from what is on screen to the new values, so a quick second change redirects mid flight; past 100% a second lap traces the inside edge.",
        "Hovering or selecting a goal dims the others, and numbers count to their new amounts.",
        "MotionConfig follows the user's reduced motion setting, and values jump without the sweep."
      ],
      "responsive": [
        "The root is a container up to 38rem; from 30rem wide the legend moves beside the rings as a list.",
        "Narrower containers stack the rings above a compact legend."
      ],
      "performance": [
        "The rings are SVG arcs driven by motion values, with no animation loop between changes.",
        "Each metric adds one ring; three to four stay legible within the fixed 220 unit viewBox."
      ],
      "aiNotes": [
        "Choose it for daily goals that fill up and can be exceeded. Use progress or gauge for a single value and usage-meter for quotas.",
        "Keep metric ids stable and use them as keys in each day's values. Control day when another view drives the date."
      ],
      "related": [
        "progress",
        "gauge",
        "usage-meter",
        "activity-heatmap",
        "animated-counter"
      ],
      "usage": "import { ActivityRings } from \"@/components/arc/activity-rings/activity-rings\";\n\nexport function Today() {\n  return (\n    <ActivityRings\n      metrics={[\n        { id: \"move\", label: \"Move\", unit: \"kcal\", goal: 500 },\n        { id: \"exercise\", label: \"Exercise\", unit: \"min\", goal: 30 },\n        { id: \"stand\", label: \"Stand\", unit: \"hr\", goal: 12 },\n      ]}\n      days={week}\n      onRingClose={(metric, day) => celebrate(metric, day)}\n    />\n  );\n}",
      "exports": [
        "ActivityRings"
      ]
    },
    {
      "name": "bar-chart",
      "kind": "component",
      "title": "Bar chart",
      "description": "Compare one measure across days and scrub any bar for its value.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "chart",
        "bar",
        "data"
      ],
      "docs": "https://uiarc.dev/components/bar-chart",
      "markdown": "https://uiarc.dev/components/bar-chart/markdown",
      "registry": "https://uiarc.dev/r/bar-chart.json",
      "keywords": [
        "react bar chart",
        "column chart",
        "animated bar chart",
        "svg bar chart",
        "scrubbable chart",
        "daily activity chart"
      ],
      "whenToUse": [
        "One measure across days, weeks, or months, with an average line.",
        "Charts with a range switch, where stable keys let bars morph between ranges."
      ],
      "whenNotToUse": [
        "Use sparkline for a compact trend.",
        "Use activity-heatmap for daily rhythm over a year."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The SVG is role=\"img\" with a summary of average, highest, and lowest.",
        "A focusable role=\"slider\" scrubber reads each bar as its label and value.",
        "A visually hidden table lists every value, and range changes are announced politely."
      ],
      "motion": [
        "Bars grow in with a stagger on first view; new ranges morph bars, ticks, and the average line.",
        "Scrubbing moves a cursor and rolls the headline value.",
        "Reduced motion shows bars at once and swaps values without movement."
      ],
      "responsive": [
        "The plot measures its width with a ResizeObserver and bars cap at 28px, so wide containers get spacing rather than fat bars.",
        "Scrubbing uses touch-action pan-y, so a horizontal drag scrubs while vertical swipes scroll the page."
      ],
      "performance": [
        "Bars, ticks, and the average line animate with motion values; one series and many bars per view is the intended load.",
        "A visually hidden table renders every value, so very long ranges double the DOM."
      ],
      "aiNotes": [
        "Use for one measure across days, weeks, or months. Use sparkline for a compact trend and activity-heatmap for daily rhythm over a year.",
        "Use stable keys such as ISO dates so switching ranges morphs instead of redrawing."
      ],
      "related": [
        "sparkline",
        "activity-heatmap",
        "segmented-control"
      ],
      "usage": "import { BarChart } from \"@/components/arc/bar-chart/bar-chart\";\n\nconst week = [\n  { key: \"2026-09-14\", label: \"Mon, Sep 14\", axisLabel: \"M\", value: 32 },\n  { key: \"2026-09-15\", label: \"Tue, Sep 15\", axisLabel: \"T\", value: 48 },\n  { key: \"2026-09-16\", label: \"Wed, Sep 16\", axisLabel: \"W\", value: 27 },\n];\n\nexport function ActiveMinutes() {\n  return <BarChart data={week} label=\"Active minutes\" period=\"Sep 14–16, 2026\" unit=\"min\" />;\n}",
      "exports": [
        "BarChart"
      ]
    },
    {
      "name": "activity-heatmap",
      "kind": "component",
      "title": "Activity heatmap",
      "description": "See a year of activity at a glance, one square per day.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "calendar"
      ],
      "docs": "https://uiarc.dev/components/activity-heatmap",
      "markdown": "https://uiarc.dev/components/activity-heatmap/markdown",
      "registry": "https://uiarc.dev/r/activity-heatmap.json",
      "keywords": [
        "react activity heatmap",
        "contribution graph",
        "github calendar heatmap",
        "calendar heatmap",
        "streak chart",
        "activity calendar"
      ],
      "whenToUse": [
        "Daily rhythm over a long range, like contributions or workouts.",
        "Views where streaks and quiet weeks matter more than exact comparison."
      ],
      "whenNotToUse": [
        "Use bar-chart when the exact comparison is the point.",
        "Use calendar to pick dates."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The grid is role=\"grid\" with labelled gridcells and a hidden legend explaining the levels.",
        "Legend swatches are toggle buttons with aria-pressed that highlight days of one level.",
        "The total and hovered day are announced through status and polite live regions; the tooltip itself is aria-hidden."
      ],
      "motion": [
        "Cells wave in once on view and a new range recolors the grid in a sweep.",
        "The tooltip glides between cells and its text rolls; the total counts to new values.",
        "Reduced motion drops the wave, sweep, and glide in favor of short fades."
      ],
      "responsive": [
        "Cells scale between 9px and 15px with the container, then the grid scrolls horizontally from the newest week.",
        "Below a 440px container the caption switches to a short form.",
        "On touch the tooltip lingers after a tap instead of hiding at once."
      ],
      "performance": [
        "Every day is its own cell, so a year is about 370 elements; the reveal wave is capped in total time.",
        "One shared tooltip serves the whole grid."
      ],
      "aiNotes": [
        "Use when rhythm, streaks, and quiet weeks matter more than exact comparison. Use bar-chart when the exact comparison is the point.",
        "Pass a range switch through actions and swap days to change the period."
      ],
      "related": [
        "bar-chart",
        "calendar",
        "activity-rings",
        "sparkline"
      ],
      "usage": "import { useState } from \"react\";\nimport { ActivityHeatmap } from \"@/components/arc/activity-heatmap/activity-heatmap\";\n\nexport function Contributions({ days }: { days: { date: string; count: number }[] }) {\n  const [selected, setSelected] = useState<string | null>(null);\n  return (\n    <ActivityHeatmap days={days} label=\"Contributions in 2026\" period=\"2026\" selectedDate={selected} onSelectDate={setSelected} />\n  );\n}",
      "exports": [
        "ActivityHeatmap"
      ]
    },
    {
      "name": "timeline",
      "kind": "component",
      "title": "Timeline",
      "description": "Follow what happened, newest first, grouped by day.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "feed",
        "activity"
      ],
      "docs": "https://uiarc.dev/components/timeline",
      "markdown": "https://uiarc.dev/components/timeline/markdown",
      "registry": "https://uiarc.dev/r/timeline.json",
      "keywords": [
        "react timeline",
        "activity feed",
        "audit log",
        "vertical timeline",
        "event feed",
        "changelog timeline"
      ],
      "whenToUse": [
        "Project history, audit logs, and deploy streams where recency matters.",
        "Live feeds where new updates slide in at the top.",
        "Rows that expand in place to show logs or detail."
      ],
      "whenNotToUse": [
        "Use sortable-data-table when people sort or compare.",
        "Use stepper for progress through fixed steps."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The feed is a labelled role=\"region\"; day labels are headings at headingLevel with a hidden update count.",
        "Expandable rows are buttons with aria-expanded and aria-controls; rows without detail are not interactive.",
        "Times use a time element with the full date in hidden text, and new updates are announced in a status region.",
        "Say the outcome in the title, since tone is color only."
      ],
      "motion": [
        "The connecting line draws as rows come into view and markers pop in.",
        "New updates slide in at the top while the rest glide down; details expand on a spring.",
        "Reduced motion replaces travel with short fades and instant height changes."
      ],
      "responsive": [
        "Day labels stay pinned while updates scroll under them, inside maxHeight or the page.",
        "Below 420px row padding tightens so text keeps its width."
      ],
      "performance": [
        "Events are not virtualized; set maxHeight for long feeds and trim old events.",
        "Pass a now that ticks about once a minute rather than every second, since it re-renders every row."
      ],
      "aiNotes": [
        "Use for project history, audit logs, and deploy streams where order and recency matter. Use sortable-data-table when people sort or compare.",
        "Pass a ticking now (for example updated every minute) so relative times stay current, and a fixed timeZone to avoid hydration mismatches."
      ],
      "related": [
        "sortable-data-table",
        "avatar",
        "badge",
        "stepper"
      ],
      "usage": "import { Timeline } from \"@/components/arc/timeline/timeline\";\n\nconst events = [\n  { id: \"e1\", at: \"2026-09-22T09:12:00Z\", actor: \"Maya\", title: \"merged Checkout redesign into main\", meta: \"PR #482\" },\n  { id: \"e2\", at: \"2026-09-22T08:40:00Z\", title: \"Deploy failed\", tone: \"danger\" as const, detail: <pre>Build step exited 1</pre> },\n];\n\nexport function Activity({ now }: { now: number }) {\n  return <Timeline events={events} now={now} label=\"Project activity\" maxHeight={420} />;\n}",
      "exports": [
        "Timeline"
      ]
    },
    {
      "name": "stretch-refresh",
      "kind": "component",
      "title": "Stretch refresh",
      "description": "Pull a feed down and a line stretches to tell you when to let go.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/stretch-refresh",
      "markdown": "https://uiarc.dev/components/stretch-refresh/markdown",
      "registry": null,
      "keywords": [
        "react pull to refresh",
        "pull to refresh",
        "refresh feed",
        "rubber band refresh",
        "mobile pull down refresh",
        "feed refresh animation"
      ],
      "whenToUse": [
        "Timelines, inboxes, and activity feeds where new entries arrive at the top.",
        "Panels that should support pull to refresh by touch, mouse drag, and trackpad scroll."
      ],
      "whenNotToUse": [
        "Use a plain list for static content that never refreshes.",
        "Use pagination for paging through older items.",
        "Use timeline for a static history of events."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A refresh button gives keyboard and switch users the same path as the pull gesture, with aria-disabled while busy.",
        "The feed is a labelled, focusable region and the list sets aria-busy while refreshing.",
        "\"Refreshing\" and the result are announced in a polite status region."
      ],
      "motion": [
        "The sheet rubber-bands with growing resistance while rows spread apart; scrolling up again at the top also pulls.",
        "A line in the gap reaches full length exactly at the threshold, runs while refreshing, then gives way to the result as new rows settle in from the top.",
        "Reduced motion keeps the same states and only fades."
      ],
      "responsive": [
        "The root is a flex column with min-height 0, so give it a sized parent; the feed scrolls inside it.",
        "Touch pulls use native listeners and only take over once the feed is at the top, so normal scrolling is untouched."
      ],
      "performance": [
        "Rows are not virtualized and use layout position animation; keep the feed to what one panel shows plus a page or so.",
        "Wheel and touchmove listeners are non-passive so the pull can prevent scrolling, but they return early unless pulling."
      ],
      "aiNotes": [
        "Choose it for timelines, inboxes, and activity feeds where new entries arrive at the top. For static lists use a plain list, and for paging use pagination.",
        "Update items inside onRefresh before it resolves, and keep the call at least half a second so the running line reads as work."
      ],
      "related": [
        "timeline",
        "skeleton",
        "toast",
        "reorderable-list"
      ],
      "usage": "import { StretchRefresh } from \"@/components/arc/stretch-refresh/stretch-refresh\";\n\nexport function Inbox() {\n  const [messages, setMessages] = useState(initial);\n  return (\n    <StretchRefresh\n      title=\"Inbox\"\n      items={messages}\n      getKey={m => m.id}\n      renderItem={m => <MessageRow message={m} />}\n      onRefresh={async () => {\n        const fresh = await fetchNew();\n        setMessages(prev => [...fresh, ...prev]);\n        return `${fresh.length} new messages`;\n      }}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Fill the height of a sidebar",
          "code": "<aside style={{ display: \"flex\", flexDirection: \"column\", height: \"100dvh\" }}>\n  <StretchRefresh\n    title=\"Activity\"\n    items={events}\n    getKey={e => e.id}\n    renderItem={e => <EventRow event={e} />}\n    onRefresh={reload}\n    className=\"grow\"\n  />\n</aside>"
        }
      ],
      "exports": [
        "StretchRefresh"
      ]
    },
    {
      "name": "orbit-menu",
      "kind": "component",
      "title": "Orbit menu",
      "description": "Hold a button and its actions orbit out, ready to pick with a drag.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/orbit-menu",
      "markdown": "https://uiarc.dev/components/orbit-menu/markdown",
      "registry": null,
      "keywords": [
        "react radial menu",
        "orbit menu",
        "fan menu",
        "arc menu",
        "floating action menu",
        "press and drag menu",
        "circular menu"
      ],
      "whenToUse": [
        "Three to six quick actions on one object, such as a note or photo.",
        "Touch-first surfaces where press, drag, and release should fire an action in one gesture."
      ],
      "whenNotToUse": [
        "Use dropdown-menu or context-menu for longer or text-heavy lists.",
        "Use dock for tools that stay selected.",
        "Use action-button for a single action."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"menu\", aria-expanded, and aria-controls; actions are role=\"menuitem\" buttons named by their label.",
        "The closed menu is inert, and a roving tabindex keeps one action in the tab order.",
        "Confirmations such as \"Pinned\" are read from a role=\"status\" region when announce is on."
      ],
      "motion": [
        "Actions swirl out along the arc in reading order and fold back in reverse; a lens glides between targeted actions and nearby actions lean toward the pointer.",
        "The trigger glyph morphs between ellipsis, close, and check, and a label pill springs to fit its text. It names the targeted action, shows Release to cancel when a drag leaves the arc, and shows the done text for about 1.4 seconds after firing.",
        "Targeting goes by angle, like a pie menu: the pointer only has to head toward an action, outside a 34px dead zone around the trigger. Releasing off the arc, or a hold without a drag, folds it away without firing.",
        "Reduced motion drops the swirl, blur, and magnet pull, leaving short fades and instant position changes."
      ],
      "responsive": [
        "The arc is 104px from the trigger by default and is not clamped to the viewport; set direction and radius so it opens into free space.",
        "Trigger and actions use touch-action none so drag-to-select works on touch; pointer magnet effects skip touch."
      ],
      "performance": [
        "The open trigger and the label pill use backdrop blur, removed under prefers-reduced-transparency. Actions use a near solid material, so six moving items never stack blurs.",
        "The closed menu stays mounted but inert; with six actions the cost is small."
      ],
      "aiNotes": [
        "Use for three to six quick actions on one object (a note, a photo) where a dropdown is too heavy and a toolbar too loud. Use dropdown-menu or context-menu for longer or text-heavy lists.",
        "Give each action a done string for the confirmation pill, mark destructive actions tone=\"danger\", and set direction so the arc opens into free space."
      ],
      "related": [
        "dropdown-menu",
        "context-menu",
        "action-button",
        "dock"
      ],
      "usage": "import { OrbitMenu } from \"@/components/arc/orbit-menu/orbit-menu\";\nimport { Pin, Share2, Trash2 } from \"lucide-react\";\n\nexport function NoteActions({ onAction }: { onAction: (id: string) => void }) {\n  return (\n    <OrbitMenu\n      label=\"Note actions\"\n      onAction={onAction}\n      actions={[\n        { id: \"pin\", label: \"Pin\", icon: <Pin size={18} />, done: \"Pinned\" },\n        { id: \"share\", label: \"Share\", icon: <Share2 size={18} /> },\n        { id: \"delete\", label: \"Delete\", icon: <Trash2 size={18} />, tone: \"danger\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "OrbitMenu"
      ]
    },
    {
      "name": "time-dial",
      "kind": "component",
      "title": "Time dial",
      "description": "Spin a dial to pick an hour and see which team cities are at work.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/time-dial",
      "markdown": "https://uiarc.dev/components/time-dial/markdown",
      "registry": null,
      "keywords": [
        "react time zone picker",
        "world clock dial",
        "meeting time planner",
        "timezone converter ui",
        "rotary time picker",
        "time zone dial"
      ],
      "whenToUse": [
        "Planning a moment across two to five cities, such as calls, releases, or handovers.",
        "Distributed teams that need to see at a glance who is inside working hours."
      ],
      "whenNotToUse": [
        "Use time-picker for a single local time.",
        "Use date-picker or calendar when the day matters more than the hour.",
        "Use slider for a plain numeric range."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The dial is a focusable role=\"slider\" with aria-valuenow in home-city minutes and a spoken aria-valuetext.",
        "The city list uses buttons with aria-pressed for the home city.",
        "Changes are announced through a polite live region; the SVG face and readout are aria-hidden."
      ],
      "motion": [
        "Drags have soft quarter-hour detents, flicks coast on a spring into the nearest quarter hour, and bounds rubber-band.",
        "Readout digits roll like an odometer, city marks swap between knob and dot as the home city changes, and the sun and moon turn over at the day/night boundary.",
        "Reduced motion removes flick coasting and replaces rises and blurs with plain fades."
      ],
      "responsive": [
        "The root is a container up to 720px; from 600px the dial and city list sit side by side, below that they stack.",
        "The dial is min(100%, 256px) with touch-action none, so dragging it on touch never scrolls the page.",
        "Below 360px of container width the city rows tighten."
      ],
      "performance": [
        "The face is one SVG and rotation runs on motion values, so drags do not re-render city rows.",
        "Offsets are fixed per city; there is no time zone database, so pass DST-correct offsets yourself."
      ],
      "aiNotes": [
        "Use to plan a moment across two to five offices (calls, releases, handovers). For a single local time use time-picker.",
        "Store the value in UTC minutes and convert per city yourself; offsets are fixed, so pass DST-correct offsets for the date in question.",
        "Set workingHours to the team's shared day so the band shows at a glance who would be at work, and hourCycle to match the locale."
      ],
      "related": [
        "time-picker",
        "date-picker",
        "calendar",
        "slider"
      ],
      "usage": "import { useState } from \"react\";\nimport { TimeDial } from \"@/components/arc/time-dial/time-dial\";\n\nconst cities = [\n  { id: \"nyc\", name: \"New York\", code: \"NYC\", offset: -240 },\n  { id: \"lon\", name: \"London\", code: \"LON\", offset: 60 },\n  { id: \"tyo\", name: \"Tokyo\", code: \"TYO\", offset: 540 },\n];\n\nexport function CallPlanner() {\n  const [time, setTime] = useState(15 * 60);\n  return <TimeDial cities={cities} value={time} onValueChange={setTime} hourCycle={24} workingHours={[480, 1020]} label=\"Call time\" />;\n}",
      "exports": [
        "TimeDial"
      ]
    },
    {
      "name": "booking-pill",
      "kind": "component",
      "title": "Booking pill",
      "description": "One pill that reshapes through party size, date, time, and a ticket.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/booking-pill",
      "markdown": "https://uiarc.dev/components/booking-pill/markdown",
      "registry": null,
      "keywords": [
        "react booking widget",
        "table reservation",
        "restaurant booking ui",
        "date and time picker flow",
        "reservation pill",
        "booking flow"
      ],
      "whenToUse": [
        "Venue or service pages where booking a table or slot is the one action people came for.",
        "Mobile-first flows that walk through party size, date, time, and confirm in one surface."
      ],
      "whenNotToUse": [
        "Use date-picker and time-picker inside a regular form for multi-field checkouts.",
        "Use stepper or multi-step-form for longer flows with many fields.",
        "Use calendar when people need to see a whole month."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The date strip is a role=\"slider\" with a spoken aria-valuetext; times are a role=\"radiogroup\" with unavailable times marked aria-disabled.",
        "Every round icon button has an aria-label (Back, Fewer guests, Next, choose a date).",
        "Step changes are announced in a polite live region, and booking errors use role=\"alert\"."
      ],
      "motion": [
        "The pill's shape springs to fit each step while content slides in a beat later in the direction of travel.",
        "The date strip follows drags 1:1, rubber-bands at the ends, and flicks glide to a day; party count digits roll.",
        "Reduced motion jumps the strip, drops slides and blurs, and swaps faces with fades."
      ],
      "responsive": [
        "The root is a container; each step's face is capped at the container width minus 24px, up to 340 to 420px.",
        "The date strip drags horizontally with touch-action pan-y, so vertical page scroll still works on touch."
      ],
      "performance": [
        "The pill uses a 24px backdrop blur; the date strip renders every day in days, 21 by default.",
        "ResizeObservers track each face for the shape spring, and only the current step is mounted."
      ],
      "aiNotes": [
        "Use on a venue or service page where booking is the one action people came for. Use date-picker and time-picker in a regular form for multi-field checkouts.",
        "Anchor it to the bottom of its container, pass startDate from the server, and wire isAvailable and onConfirm to your backend. Set hourCycle for display only; data stays in \"HH:MM\"."
      ],
      "related": [
        "date-picker",
        "time-picker",
        "calendar",
        "number-field",
        "stepper"
      ],
      "usage": "import { BookingPill, type Booking } from \"@/components/arc/booking-pill/booking-pill\";\n\nexport function ReserveBar({ today, book }: { today: string; book: (booking: Booking) => Promise<void> }) {\n  return (\n    <BookingPill\n      venue=\"Osteria Nove\"\n      venueDetail=\"12 Harbour Street\"\n      startDate={today}\n      hourCycle={24}\n      isAvailable={(date, time, party) => !(party > 6 && time > \"20:00\")}\n      onConfirm={book}\n    />\n  );\n}",
      "exports": [
        "BookingPill"
      ]
    },
    {
      "name": "voice-recorder",
      "kind": "component",
      "title": "Voice recorder",
      "description": "Record with a live waveform, review and scrub, then send the take into the thread.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion"
      ],
      "docs": "https://uiarc.dev/components/voice-recorder",
      "markdown": "https://uiarc.dev/components/voice-recorder/markdown",
      "registry": null,
      "keywords": [
        "react voice recorder",
        "voice message",
        "audio recorder ui",
        "whatsapp voice note",
        "record audio button",
        "voice memo"
      ],
      "whenToUse": [
        "Voice notes in chats, comments, or field notes where speaking beats typing.",
        "A thread of voice bubbles with playback and scrubbing."
      ],
      "whenNotToUse": [
        "Use now-playing for music or long-form audio.",
        "Use hold-to-confirm for press-and-hold actions that do not record.",
        "Use stopwatch to time something without audio."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The recorder is a labelled section with a labelled message list; every control is a native button with an aria-label, such as \"Stop recording at 0:04\" or \"Play voice message from Sam, 0:07\".",
        "Recording, review, send, and discard are announced in a polite live region that also describes the mic.",
        "Waveforms, the live canvas, and timers are aria-hidden; the button labels carry the durations."
      ],
      "motion": [
        "Mic, recording bar, review, and sent bubble share one layout id, so a take morphs from the mic into the thread on a spring.",
        "The live waveform scrolls on a canvas and playback fills the bars left to right; side buttons pop in and content crossfades with a slight blur.",
        "Motion follows the user's reduced motion setting: layout morphs are skipped, the waveform steps instead of growing, and the thread scrolls without smoothing."
      ],
      "responsive": [
        "The recorder fills its container width, and the review waveform flexes its bars between 1.5px and 3px to fit.",
        "Waveforms keep touch-action pan-y, so scrubbing by tap works without blocking scroll."
      ],
      "performance": [
        "Recording is simulated; the live waveform is painted to a canvas each frame only while recording.",
        "Each message runs its own frame callback that returns early unless playing; long threads are not virtualized."
      ],
      "aiNotes": [
        "Use for voice notes in chats, comments, or field notes where speaking beats typing. Audio is simulated; add MediaRecorder capture and upload in onSend before shipping.",
        "Seed history with voicePeaks for deterministic bars. Messages live in internal state, so persist them from onSend and pass them back as defaultMessages."
      ],
      "related": [
        "now-playing",
        "slider",
        "hold-to-confirm"
      ],
      "usage": "import { VoiceRecorder, voicePeaks } from \"@/components/arc/voice-recorder/voice-recorder\";\n\nconst history = [\n  { id: \"m1\", duration: 7, peaks: voicePeaks(1, 7), from: \"Sam\", avatar: \"/people/sam.jpg\", meta: \"9:41 AM\" },\n];\n\nexport function Thread() {\n  return <VoiceRecorder defaultMessages={history} maxDuration={30} onSend={message => console.log(\"sent\", message.id)} />;\n}",
      "exports": [
        "VoiceRecorder"
      ]
    },
    {
      "name": "user-menu",
      "kind": "component",
      "title": "User menu",
      "description": "Your account, settings, theme, and sign out behind the avatar. Opens as a bottom sheet on phones.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "motion",
        "product"
      ],
      "docs": "https://uiarc.dev/components/user-menu",
      "markdown": "https://uiarc.dev/components/user-menu/markdown",
      "registry": "https://uiarc.dev/r/user-menu.json",
      "keywords": [
        "react user menu",
        "account menu",
        "avatar dropdown",
        "profile menu",
        "user menu with status",
        "sign out menu",
        "presence indicator"
      ],
      "whenToUse": [
        "The single account entry point in an app top bar.",
        "Menus that combine presence status, theme preference, account links, and sign out.",
        "Apps that want async sign out with a spinner in the menu."
      ],
      "whenNotToUse": [
        "Use dropdown-menu for generic command lists.",
        "Use theme-switch for a standalone light and dark toggle.",
        "Use workspace-sidebar when account and workspace switching live in a side navigation."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"menu\" and aria-expanded, and is labelled \"Account menu, <name>\" plus the status when one is shown.",
        "Rows are menuitem buttons; the theme and status switches are labelled groups of menuitemradio buttons with aria-checked.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The highlight marks the keyboard position. Outside press, the scrim, and swipe down close the menu.",
        "Sign out sets aria-busy while an async handler runs; shortcut hints are aria-hidden."
      ],
      "motion": [
        "The panel scales and fades out of the avatar on a spring that never overshoots; rows fade in with a short stagger, and closing is a quick fade.",
        "One highlight glides between rows for the pointer and the keyboard; the switch thumb slides between options.",
        "On phones the sheet rises from the bottom edge and follows a downward drag, closing past a short distance or a quick flick.",
        "Reduced motion opens and closes with a short fade, jumps the highlight, and turns off dragging."
      ],
      "responsive": [
        "The menu is 17.5rem wide, capped at the viewport width minus 24px, with 12px collision padding.",
        "Long names and emails ellipsize instead of widening the menu.",
        "The trigger is a fixed 40px avatar, and the theme row keeps a 44px minimum height for touch."
      ],
      "performance": [
        "Content renders only while open; each opening gets a fresh layout namespace so shared indicators do not animate from stale positions.",
        "The avatar image decodes async and falls back to initials on error."
      ],
      "aiNotes": [
        "Use as the single account entry point in an app top bar. For generic command lists use dropdown-menu; for a standalone light/dark toggle use theme-switch.",
        "Keep items to three or four account destinations; the theme switch and sign out are built in.",
        "onThemeChange only reports; apply the theme to the document yourself, optionally with a view transition.",
        "Return the sign out promise so the item shows progress and the menu closes when it settles."
      ],
      "related": [
        "dropdown-menu",
        "avatar",
        "theme-switch",
        "workspace-sidebar"
      ],
      "usage": "import { UserMenu } from \"@/components/arc/user-menu/user-menu\";\nimport { CreditCard, Settings, UserRound } from \"lucide-react\";\n\nexport function TopBarAccount({ user }: { user: { name: string; email: string } }) {\n  return (\n    <UserMenu\n      user={{ ...user, plan: \"Pro\" }}\n      showName\n      onThemeChange={setThemePreference}\n      items={[\n        { label: \"Profile\", icon: <UserRound size={16} />, onSelect: openProfile },\n        { label: \"Settings\", icon: <Settings size={16} />, keys: [\"⌘\", \",\"], onSelect: openSettings },\n        { label: \"Billing\", icon: <CreditCard size={16} />, onSelect: openBilling },\n      ]}\n      onSignOut={() => signOut()}\n    />\n  );\n}",
      "exports": [
        "UserMenu",
        "PresenceDot"
      ]
    },
    {
      "name": "data-grid",
      "kind": "component",
      "title": "Data grid",
      "description": "A spreadsheet grid with range selection, inline editing, a fill handle, and animated sorting.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "table",
        "spreadsheet",
        "editing"
      ],
      "docs": "https://uiarc.dev/components/data-grid",
      "markdown": "https://uiarc.dev/components/data-grid/markdown",
      "registry": null,
      "keywords": [
        "react data grid",
        "spreadsheet component",
        "editable table",
        "excel like grid",
        "inline cell editing",
        "copy paste from excel",
        "fill handle",
        "virtualized table",
        "pinned columns",
        "column filters"
      ],
      "whenToUse": [
        "Editing many values in place like a spreadsheet, such as budgets, price lists, or inventory.",
        "Data people move to and from Excel or Google Sheets with copy and paste.",
        "Work that needs range selection, a fill handle, fill down, and undo.",
        "Large tables, up to tens of thousands of rows, that people filter, sort, group with subtotals, and total.",
        "Server data too large to send at once, loaded in blocks as people scroll through a dataSource.",
        "Finance tables with many period columns, date columns, and exports people open in Excel.",
        "Bulk work on checked rows: copy, CSV export, delete, or your own actions."
      ],
      "whenNotToUse": [
        "Use sortable-data-table for read-only records with sorting and row selection.",
        "Use a form when people edit one record at a time.",
        "Use a card list or card grid when each record needs rich content like images or long text."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The grid is one focusable role=\"grid\" with aria-multiselectable, aria-rowcount, aria-colcount, and aria-busy while loading. The active cell is exposed through aria-activedescendant, not moving focus.",
        "Rows and cells carry aria-rowindex and aria-colindex, so counts stay right while rows are virtualized. The active and editing rows stay mounted when scrolled away.",
        "Cells report aria-selected and aria-readonly. Rows report aria-selected when checked.",
        "Headers are role=\"columnheader\" with aria-sort. The sort button label names the direction, the sort priority, and the Alt + arrow resize keys.",
        "The cell editor is labelled by column and row name and sets aria-invalid on a bad value. On select columns it is a combobox that controls a listbox, with aria-activedescendant on the highlighted option.",
        "Row checkboxes are role=\"checkbox\" and stay out of the tab order; the select-all box in the header is tabbable and shows a mixed state.",
        "Number filter inputs describe their syntax and set aria-invalid on an unreadable expression. Select filters open a labelled checklist.",
        "Edits, sorts, checks, copies, pastes, fills, undo, and redo are announced in a polite live region. The row count is also a polite live region.",
        "Totals cells carry a full aria-label, such as \"Q1 sum $15,200\".",
        "With groupBy the grid becomes role=\"treegrid\". Group rows carry aria-level and aria-expanded, their toggle is a button that names the group, row count, and action, and subtotal cells carry full labels. Data rows carry aria-level too.",
        "The date editor is a combobox with a dialog popup. The calendar opens in a portal so the grid never clips it, and focus returns to the cell when it closes.",
        "Scrolling columns outside the view are not rendered, but every cell keeps its aria-colindex, and the active and editing cells stay mounted.",
        "With a data source, rows still loading render as placeholder rows with aria-busy."
      ],
      "motion": [
        "The selection rectangle and active cell spring between ranges on motionTokens.spring.snappy. Scrolling moves them at once, and resizing a column snaps them.",
        "After a sort, rows slide to their new place on the spring curve for 700ms, and rows arriving from off screen fade in.",
        "Pasted, filled, undone, and redone cells flash a wash that fades out over 0.9s.",
        "The toolbar swaps to bulk actions with a short slide, fade, and blur. Status messages enter the same way.",
        "Totals, the selection summary, and the checked count roll with animated-counter.",
        "An invalid edit shakes once. Menus and the option list fade and scale in from their anchor.",
        "A group's chevron turns on the spring curve as it opens and closes.",
        "Reduced motion jumps the selection, drops the row slide, the shake, the press scales, the chevron turn, and the skeleton pulse, and keeps short fades."
      ],
      "responsive": [
        "The grid scrolls in both directions inside maxHeight. The header and totals row stay sticky, and pinned columns and the row gutter stay on the left.",
        "Pinned columns are scaled down to about half of the grid width so a narrow grid still shows scrolling columns.",
        "Layout reacts to the grid's own width with container queries. Below 640px toolbar buttons become icons. Below 460px the row count and undo and redo buttons hide. Below 480px the status message wraps to its own line.",
        "On touch a pan scrolls the grid. A tap selects a cell on release, a second tap edits it, and only the fill handle takes over a touch drag.",
        "Row checkboxes and header menu buttons are always visible on devices without hover."
      ],
      "performance": [
        "Rows are virtualized: only the visible rows plus 6 above and below render, each positioned with translateY. Rows are memoized, so scrolling and typing re-render only the rows they touch.",
        "Filters and search use useDeferredValue, so typing a filter stays responsive on large data.",
        "The sorted and filtered view is recomputed only when sort, filters, search, visible columns, or the set of row ids change, not on every edit.",
        "Select column options are derived from up to 200 distinct values when options is not given; pass options for large or wide data.",
        "Columns are virtualized too: pinned columns always render, and scrolling columns render only within about 240px of the view, updated every 64px of horizontal scroll.",
        "Group rows are computed with the view, not on every edit, so an edited row stays in its group until the next sort, filter, or grouping change. Subtotals update at once.",
        "A dataSource keeps up to 40 blocks and aborts requests that a sort, filter, or search change makes stale. Undo holds up to 100 full row snapshots."
      ],
      "aiNotes": [
        "Choose it when people edit many values in place like a spreadsheet. Use sortable-data-table for read-only records. For rows that roll up under a shared value, such as journal lines by account and period, use groupBy.",
        "Every row needs a unique, stable id. Column keys map to row fields. Keep rows from onRowsChange to persist edits.",
        "Use number, currency, or percent for numeric data so parsing, right alignment, totals, and the status bar sum work. Store raw numbers, not formatted strings.",
        "Mark computed or locked columns editable: false. Give select columns options so pasted and typed values are checked against them.",
        "Copy writes tab separated text plus an HTML table with formatted values, so Excel and Sheets read typed numbers. Paste reads an HTML table first, then TSV with quoted cells. A selection that is a whole multiple of the clipboard is tiled; values that do not parse are skipped and reported.",
        "The bulk Copy button copies checked rows with a header row through navigator.clipboard, and Export downloads CSV of checked rows. The toolbar Export downloads all filtered rows in view order.",
        "A sort reorders once, like a spreadsheet. Editing a value does not re-sort or re-filter, so rows never jump under the cursor.",
        "Undo keeps 100 steps and covers edits, fills, pastes, clears, and row deletes. Sort, filter, and column changes are view state and are not undone.",
        "Text filters match accent-insensitive substrings; prefix = for an exact match or ! to exclude. The quick search matches every word across visible columns.",
        "Filtering, sorting, and grouping run on the client over the rows you pass. For data too large to send at once, pass a dataSource: the grid asks for blocks of rows as people scroll and sends sort, filters, and search with each request.",
        "Use a date column for days. Values stay ISO day strings, so they round-trip with no time zone shift; sort and filter compare them as days, and the fill handle continues a daily or monthly series.",
        "Grouping reuses each column's aggregate for its subtotals, so one setting drives the group rows and the totals row. A grouped .xlsx export writes SUBTOTAL formulas, so Excel never counts a subtotal twice.",
        "For a small form table, turn off rowNumbers when rows already carry their own number, and selectionStats when the address and counts mean nothing to the reader."
      ],
      "related": [
        "sortable-data-table",
        "filter-toolbar",
        "date-picker",
        "animated-counter",
        "pagination"
      ],
      "usage": "import { useState } from \"react\";\nimport { Archive } from \"lucide-react\";\nimport { DataGrid, type DataGridColumn, type DataGridRow, type DataGridSort } from \"@/components/arc/data-grid/data-grid\";\n\nconst columns: DataGridColumn[] = [\n  { key: \"item\", label: \"Item\", width: 180, pinned: true },\n  { key: \"status\", label: \"Status\", type: \"select\", options: [\"Planned\", \"Active\", \"Done\"] },\n  { key: \"q1\", label: \"Q1\", type: \"currency\" },\n  { key: \"q2\", label: \"Q2\", type: \"currency\" },\n  { key: \"margin\", label: \"Margin\", type: \"percent\", decimals: 1, editable: false },\n  { key: \"notes\", label: \"Notes\", hidden: true, aggregate: \"count\" },\n];\n\nexport function Budget() {\n  const [rows, setRows] = useState<DataGridRow[]>([\n    { id: \"r1\", item: \"Design\", status: \"Active\", q1: 12000, q2: 14500, margin: 32.5, notes: \"\" },\n    { id: \"r2\", item: \"Hosting\", status: \"Planned\", q1: 3200, q2: 3400, margin: 18, notes: \"Annual plan\" },\n  ]);\n  const [sort, setSort] = useState<DataGridSort[]>([{ key: \"q1\", dir: \"desc\" }]);\n  return (\n    <DataGrid\n      label=\"Budget\"\n      columns={columns}\n      rows={rows}\n      onRowsChange={setRows}\n      sort={sort}\n      onSortChange={setSort}\n      maxHeight={480}\n      exportFileName=\"budget-2026\"\n      bulkActions={[{ label: \"Archive\", icon: <Archive size={16} aria-hidden />, onAction: checked => console.log(checked.map(r => r.id)) }]}\n    />\n  );\n}",
      "exports": [
        "DataGrid",
        "DataGridColumn",
        "DataGridBulkAction",
        "DataGridDataSource",
        "Types",
        "data-grid-model helpers"
      ]
    },
    {
      "name": "lightbox-gallery",
      "kind": "component",
      "title": "Lightbox gallery",
      "description": "A masonry grid where photos zoom from their slot into a viewer you can swipe, pinch, and drag away.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "gallery",
        "lightbox",
        "images"
      ],
      "docs": "https://uiarc.dev/components/lightbox-gallery",
      "markdown": "https://uiarc.dev/components/lightbox-gallery/markdown",
      "registry": null,
      "keywords": [
        "react lightbox",
        "image gallery",
        "masonry gallery",
        "photo lightbox",
        "fullscreen image viewer",
        "zoomable gallery",
        "next image gallery"
      ],
      "whenToUse": [
        "Photo collections people browse and inspect up close, such as trips, portfolios, or listings.",
        "Mixed portrait and landscape sets that should keep their aspect ratios in a masonry grid."
      ],
      "whenNotToUse": [
        "Use photo-grid for a square grid with pinch zoom between densities.",
        "Use carousel for a single inline row without a viewer.",
        "Use dialog to show one arbitrary piece of content."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The grid is a labelled region of buttons named like \"Open Harbor, photo 1 of 3\"; the viewer is a portalled role=\"dialog\" with aria-modal and a focus trap, and focus starts on Close.",
        "Only the current slide is exposed to assistive tech, and a polite live counter reads \"2 of 12\" as photos change.",
        "Zoom is an aria-pressed toggle, previous and next are disabled at the ends, and the thumbnail strip marks the current photo with aria-current."
      ],
      "motion": [
        "Opening flies the photo from its grid slot to fit the stage on a spring with a constant corner radius; closing flies it back to the current photo's slot while the backdrop fades.",
        "Drag down to dismiss with resistance, swipe between photos with momentum, and pinch, Ctrl+scroll, or double click to zoom around the pointer.",
        "Reduced motion skips the flight and swiping springs, fades the viewer in and out, and jumps zoom and slides into place."
      ],
      "responsive": [
        "Columns are computed from the gallery's own width and minColumnWidth, with at least two.",
        "Below 640px viewport width the side arrows hide and swipe navigates; pinch, drag to dismiss, and double tap zoom work on touch."
      ],
      "performance": [
        "Grid images pass responsive sizes hints and viewer slides load lazily except the current one. Images are plain img elements unless ArcProvider sets an image component such as next/image.",
        "The grid is not virtualized, so paginate very large collections.",
        "With next/image set through ArcProvider, remote image hosts must be allowed in next.config."
      ],
      "aiNotes": [
        "Use for a photo collection people browse and inspect up close, such as trips, portfolios, or listings. Use photo-grid for a grid without a fullscreen viewer, carousel for a single inline row with no zoom, and dialog to show one arbitrary piece of content.",
        "Always pass real width, height, and alt for every image so the masonry and flights keep aspect ratios; images render as plain img elements unless you pass next/image (or your own image component) through ArcProvider from @/components/arc/lib/arc-provider."
      ],
      "related": [
        "photo-grid",
        "carousel",
        "dialog",
        "cover-flow"
      ],
      "usage": "import { LightboxGallery } from \"@/components/arc/lightbox-gallery/lightbox-gallery\";\n\nconst photos = [\n  { src: \"/trip/harbor.jpg\", width: 1600, height: 1067, alt: \"Boats in a harbor at dawn\", title: \"Harbor\", caption: \"May 2026\" },\n  { src: \"/trip/alley.jpg\", width: 1067, height: 1600, alt: \"Narrow alley with lanterns\", title: \"Old town\" },\n  { src: \"/trip/cliffs.jpg\", width: 1600, height: 1200, alt: \"Cliffs over the sea\" },\n];\n\nexport function TripPhotos() {\n  return <LightboxGallery images={photos} label=\"Trip photos\" minColumnWidth={180} />;\n}",
      "exports": [
        "LightboxGallery"
      ]
    },
    {
      "name": "stepper",
      "kind": "component",
      "title": "Stepper",
      "description": "Show where a person is in a multi-step flow and what is done.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "motion",
        "product"
      ],
      "docs": "https://uiarc.dev/components/stepper",
      "markdown": "https://uiarc.dev/components/stepper/markdown",
      "registry": "https://uiarc.dev/r/stepper.json",
      "keywords": [
        "react stepper",
        "step indicator",
        "progress steps",
        "wizard steps",
        "checkout steps",
        "vertical stepper",
        "multi step progress"
      ],
      "whenToUse": [
        "Showing position in a strict multi-step flow like checkout or account setup.",
        "Flows where people can jump back to completed steps, via onStepSelect.",
        "Vertical timelines of steps with descriptions and error states."
      ],
      "whenNotToUse": [
        "Use multi-step-form when you also want the step content, navigation, and success state.",
        "Use onboarding-checklist for loosely ordered tasks.",
        "Use progress when a single percentage is enough."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders a labelled nav when interactive, otherwise role=\"group\"; the current step has aria-current=\"step\".",
        "Each step appends screen-reader-only status text: Completed, Not started, or Error.",
        "Step changes are announced in a polite live region as \"Step 2 of 4: Shipping\".",
        "Upcoming steps are aria-disabled and removed from the tab order."
      ],
      "motion": [
        "Connectors fill one after another when progress jumps several steps; the ring then grows around the new current marker.",
        "Numbers morph into drawn checks or alerts; descriptions rise in while their slot springs to the new height.",
        "Reduced motion applies all changes instantly."
      ],
      "responsive": [
        "Horizontal steppers switch to markers only with a caption below a 30rem container width.",
        "Set orientation to vertical for sidebars or narrow columns where labels and descriptions should stay visible.",
        "Labels and descriptions wrap with overflow-wrap, so long words do not overflow."
      ],
      "performance": [
        "The stepper only indicates; it renders markers and text, with a ResizeObserver for the description slot height."
      ],
      "aiNotes": [
        "Use to show position in a strict multi-step flow such as checkout or setup. Use onboarding-checklist for loosely ordered tasks.",
        "The stepper only indicates; render the step content yourself and drive current. Set current to steps.length when done."
      ],
      "related": [
        "onboarding-checklist",
        "progress",
        "tabs",
        "breadcrumb"
      ],
      "usage": "import { Stepper } from \"@/components/arc/stepper/stepper\";\n\nexport function CheckoutSteps({ step, goTo }: { step: number; goTo: (index: number) => void }) {\n  return (\n    <Stepper\n      current={step}\n      onStepSelect={goTo}\n      steps={[\n        { id: \"cart\", label: \"Cart\" },\n        { id: \"shipping\", label: \"Shipping\", description: \"Address and delivery\" },\n        { id: \"payment\", label: \"Payment\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "Stepper"
      ]
    },
    {
      "name": "morph-loader",
      "kind": "component",
      "title": "Morph loader",
      "description": "Tiny loaders that morph between shapes and fold into a check or a cross when done.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "loading",
        "new"
      ],
      "docs": "https://uiarc.dev/components/morph-loader",
      "markdown": "https://uiarc.dev/components/morph-loader/markdown",
      "registry": null,
      "keywords": [
        "loader",
        "spinner",
        "loading indicator",
        "morphing loader",
        "success check",
        "error cross",
        "loading to check",
        "svg spinner"
      ],
      "whenToUse": [
        "Inline progress inside a button, row, or field where the result should appear in the same spot.",
        "Short operations that end in a clear success or failure, such as save, publish, or upload.",
        "Any spinner where a tiny, crisp, brand-consistent mark matters."
      ],
      "whenNotToUse": [
        "Use progress when the percentage is known.",
        "Use skeleton when content is loading into a layout.",
        "Use action-button when the whole button should morph its label too."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is role=\"status\" with visually hidden text that changes to label, successLabel, or errorLabel.",
        "The drawing is aria-hidden; completion is never shown by color alone because the shape becomes a check or a cross.",
        "Set decorative inside buttons that already announce progress, so the state is not read twice."
      ],
      "motion": [
        "Each stroke is a center, length, direction, bend, and width, each on its own spring, so any shape can morph into any other and interruptions keep velocity.",
        "Dots bounce in a wave, bars grow in sequence, the ring turns while its arcs breathe, and the square tumbles a quarter turn at a time while its corners open.",
        "On success or error the loop fades, the drawing turns forward to upright, the strokes draw the mark in order, and the mark pops once.",
        "Reduced motion shows a static shape per state and swaps instantly."
      ],
      "responsive": [
        "Vector drawing at any size; 16 to 32px suit inline use and 48px or more suit empty states.",
        "The footprint is fixed to size, so state changes never shift layout."
      ],
      "performance": [
        "Four SVG paths; one animation frame loop runs only while loading or settling.",
        "Path data is built from motion values without React renders."
      ],
      "aiNotes": [
        "Drive it with one prop: set status from your request state. Keep variant fixed per product for consistency.",
        "It inherits currentColor, so place it inside buttons and text without extra styling.",
        "At 16px use strokeWidth 3 for weight that matches 1.75px icon strokes.",
        "Return status to loading to reuse the same loader for the next request; the strokes gather back into the variant."
      ],
      "related": [
        "progress",
        "skeleton",
        "action-button",
        "icon-morph"
      ],
      "usage": "import { MorphLoader } from \"@/components/arc/morph-loader/morph-loader\";\n\nexport function PublishStatus({ state }: { state: \"loading\" | \"success\" | \"error\" }) {\n  return <MorphLoader variant=\"ring\" status={state} size={20} successLabel=\"Published\" errorLabel=\"Publish failed\" />;\n}",
      "exports": [
        "MorphLoader"
      ]
    },
    {
      "name": "voice-orb",
      "kind": "component",
      "title": "Voice orb",
      "description": "A voice mode orb that listens, thinks, and speaks, with a live transcript and call controls.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "new"
      ],
      "docs": "https://uiarc.dev/components/voice-orb",
      "markdown": "https://uiarc.dev/components/voice-orb/markdown",
      "registry": null,
      "keywords": [
        "react voice orb",
        "voice assistant ui",
        "ai voice mode",
        "audio visualizer orb",
        "microphone level",
        "speaking animation",
        "voice chat controls",
        "live transcript"
      ],
      "whenToUse": [
        "A voice mode for an assistant where the user needs to see whether it is listening, thinking, or speaking.",
        "Hands-free or call-like flows that need start, mute, a running clock, and end in one compact control.",
        "Live captions under a speaking agent, via VoiceTranscript."
      ],
      "whenNotToUse": [
        "Use voice-recorder when the user records a clip to send or save.",
        "Use chat-thread with text-stream for typed chat where no audio is involved.",
        "Use progress or skeleton for a generic loading state; the orb implies a voice session."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The orb is a canvas with role=\"img\" and an aria-label that follows the state (Listening, Thinking, Speaking, Muted).",
        "VoiceTranscript hides the animated words and announces only the completed turn through a polite live region.",
        "The mute button uses aria-pressed and switches its label between Mute and Unmute microphone; the clock is role=\"timer\" with a spoken minutes and seconds label.",
        "Focus moves to mute when a session starts and back to start when it ends, so keyboard users never lose their place."
      ],
      "motion": [
        "Nine shape parameters (breath, wobble, swirl, tint, ring, and more) follow the state on a hand-rolled spring each frame, so state changes interrupt cleanly.",
        "Listening swells with the input level and draws two ripple rings; speaking pulses with the output level; thinking contracts into a slow swirl.",
        "The controls pill springs its width to fit while the start button and live controls crossfade with a small scale and blur.",
        "Reduced motion stops the rAF loop and draws one still frame per state; transcript and controls fall back to short opacity fades."
      ],
      "responsive": [
        "The orb has a fixed pixel size from the size prop; pick a smaller value such as 144 on narrow screens.",
        "The transcript caps at 30rem wide and two lines tall, with a top fade mask so older lines lift away.",
        "Hover styles on the controls apply only on hover-capable fine pointers; buttons are 40px round for touch."
      ],
      "performance": [
        "One requestAnimationFrame loop per orb draws a 96-point blob and gradients on a canvas capped at 2x device pixel ratio.",
        "An IntersectionObserver and visibilitychange stop the loop while offscreen or in a background tab; a MutationObserver rereads theme colors.",
        "useMicLevel runs its own rAF loop over a 1024-sample AnalyserNode and closes the AudioContext when disabled."
      ],
      "aiNotes": [
        "Use for a voice assistant surface. Feed useMicLevel().level to inputLevel while listening and your TTS analyser level to outputLevel while speaking.",
        "The orb only visualises; map your session events to the four states yourself (idle, listening, thinking, speaking).",
        "The body uses --foreground; the inner light uses the brand gradient (--arc-gradient-from and --arc-gradient-to, falling back to --accent). Set --voice-orb-tint and optionally --voice-orb-tint-2 on a parent to recolor it; with only --voice-orb-tint set, both lights use it.",
        "For recording and saving audio clips use voice-recorder instead; this component never records."
      ],
      "related": [
        "voice-recorder",
        "chat-thread"
      ],
      "usage": "import { useEffect, useState } from \"react\";\nimport { VoiceControls, VoiceOrb, VoiceTranscript, useMicLevel } from \"@/components/arc/voice-orb/voice-orb\";\nimport type { VoiceOrbState } from \"@/components/arc/voice-orb/voice-orb\";\n\nexport function VoiceMode() {\n  const [active, setActive] = useState(false);\n  const [muted, setMuted] = useState(false);\n  const [state, setState] = useState<VoiceOrbState>(\"idle\");\n  const [elapsed, setElapsed] = useState(0);\n  const mic = useMicLevel(active && !muted);\n\n  useEffect(() => {\n    if (!active) return;\n    const timer = window.setInterval(() => setElapsed(value => value + 1), 1000);\n    return () => window.clearInterval(timer);\n  }, [active]);\n\n  return (\n    <div style={{ display: \"grid\", justifyItems: \"center\", gap: 24 }}>\n      <VoiceOrb state={state} inputLevel={mic.level} muted={muted} />\n      <VoiceTranscript text=\"How can I help?\" turn={1} complete />\n      <VoiceControls\n        active={active}\n        muted={muted}\n        elapsed={elapsed}\n        onStart={() => { setActive(true); setState(\"listening\"); }}\n        onMuteChange={setMuted}\n        onEnd={() => { setActive(false); setState(\"idle\"); setElapsed(0); }}\n      />\n    </div>\n  );\n}",
      "exports": [
        "VoiceOrb",
        "useMicLevel",
        "VoiceTranscript",
        "VoiceControls"
      ]
    },
    {
      "name": "signature-pad",
      "kind": "component",
      "title": "Signature pad",
      "description": "Smooth ink that thins with speed, with undo, replay, and PNG or SVG export.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/signature-pad",
      "markdown": "https://uiarc.dev/components/signature-pad/markdown",
      "registry": "https://uiarc.dev/r/signature-pad.json",
      "keywords": [
        "react signature pad",
        "signature capture",
        "draw signature",
        "e-signature field",
        "pressure sensitive ink",
        "signature to png",
        "sign here component"
      ],
      "whenToUse": [
        "Contracts, delivery confirmations, and consent forms that need a drawn signature.",
        "Initials or quick sketches where pen pressure and natural stroke width matter.",
        "Flows that need a clean, cropped PNG or SVG of the signature."
      ],
      "whenNotToUse": [
        "Use input for a typed name when a legal drawn signature is not required.",
        "Use file-upload when the user already has a scanned signature image.",
        "Use hold-to-confirm for a simple deliberate confirmation without a signature artifact."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The pad is focusable with role=\"img\", aria-roledescription=\"signature pad\", and a label that states the stroke count.",
        "Keyboard shortcuts are described through aria-describedby on a visually hidden paragraph.",
        "Ink color and width are radio groups with roving tabindex; toolbar icon buttons carry aria-label and title.",
        "A polite status region announces strokes added, undo, redo, clear, replay, and export results."
      ],
      "motion": [
        "Clear sweeps a squeegee edge across the pad while a clip-path wipe removes the ink over 0.56s.",
        "Replay redraws each stroke in its original rhythm with a nib dot, capping gaps between strokes and fitting the whole replay into about 3.2s.",
        "The hint fades up from a soft blur; the selected color and width highlight glides with a shared layout spring.",
        "Reduced motion skips the wipe and blur and uses short opacity fades."
      ],
      "responsive": [
        "The pad keeps a 600 / 260 aspect ratio at every width, and strokes scale with it.",
        "touch-action: none on the pad keeps finger strokes from scrolling the page; coalesced pointer events capture fast strokes on touch and pen.",
        "The toolbar wraps on narrow widths; hover styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "The live stroke writes its SVG path directly to the DOM on each pointer move, without a React render.",
        "History keeps the last 60 states; replay runs a rAF loop that re-renders the visible strokes each frame, so very long signatures cost more to replay.",
        "PNG export draws to an offscreen canvas at 3x by default."
      ],
      "aiNotes": [
        "Use for capturing a handwritten signature or initials. Store the InkStroke[] from onChange, or convert it with signatureToSvg or signatureToPng on submit.",
        "Strokes are stored in a 600 by 260 coordinate space (PAD_WIDTH, PAD_HEIGHT), so they scale with the pad and can be replayed later.",
        "getStrokeOutline, outlineToPath, and strokePath are also exported if you need the raw outline math.",
        "The pad is uncontrolled; to reset it, remount it with a new key."
      ],
      "related": [
        "file-upload",
        "multi-step-form",
        "hold-to-confirm",
        "input"
      ],
      "usage": "import { useState } from \"react\";\nimport { SignaturePad, signatureToSvg } from \"@/components/arc/signature-pad/signature-pad\";\nimport type { InkStroke } from \"@/components/arc/signature-pad/signature-pad\";\n\nexport function SignContract() {\n  const [strokes, setStrokes] = useState<InkStroke[]>([]);\n\n  return (\n    <form onSubmit={event => { event.preventDefault(); submit(signatureToSvg(strokes)); }}>\n      <SignaturePad signer=\"Ada Lovelace\" fileName=\"ada-signature\" onChange={setStrokes} />\n      <button type=\"submit\" disabled={!strokes.length}>Sign</button>\n    </form>\n  );\n}",
      "exports": [
        "SignaturePad",
        "signatureToSvg",
        "signatureToPng"
      ]
    },
    {
      "name": "icon-morph",
      "kind": "component",
      "title": "Icon morph",
      "description": "Icons that morph between states: menu to close, play to pause, copy to check, and more.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "actions",
        "new"
      ],
      "docs": "https://uiarc.dev/components/icon-morph",
      "markdown": "https://uiarc.dev/components/icon-morph/markdown",
      "registry": "https://uiarc.dev/r/icon-morph.json",
      "keywords": [
        "react icon morph",
        "animated icon",
        "hamburger to close",
        "play pause morph",
        "copy check icon",
        "sun moon toggle icon",
        "like heart animation",
        "morphing svg icon"
      ],
      "whenToUse": [
        "Icon buttons that toggle between two states, such as a hamburger menu or a play control.",
        "Copy and add actions that should confirm with a check in place.",
        "Like and notify toggles that deserve a small moment of delight."
      ],
      "whenNotToUse": [
        "Use copy-button for a complete copy-to-clipboard control with its own feedback.",
        "Use theme-switch for a labelled light and dark toggle wired to the page theme.",
        "Use a plain lucide icon when the icon never changes state."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Every icon renders aria-hidden and focusable=\"false\"; put it inside a button that carries the accessible name.",
        "Reflect the state on the button with aria-pressed, aria-expanded, or a changing label, since the icon itself says nothing."
      ],
      "motion": [
        "Lines rotate and slide, and shapes interpolate their path data, all on interruptible springs, so fast toggling reverses from wherever the icon is.",
        "The bell ring and heart burst are one-shot moments that replay on each turn on.",
        "Reduced motion makes every morph instant and skips the ring and burst."
      ],
      "responsive": [
        "Icons are fixed-size SVGs on a 24 unit grid; set size per breakpoint from the parent.",
        "Pair with a hit area of at least 40px on touch; the icon itself adds none."
      ],
      "performance": [
        "Each icon animates a few SVG attributes with motion; many on one screen are cheap.",
        "SunMoonIcon and BellIcon use an SVG mask with a per-instance id from useId."
      ],
      "aiNotes": [
        "Use inside icon buttons whose meaning flips between two states: menu and close, play and pause, copy and copied, like and liked.",
        "Import the specific icon (HeartIcon, ChevronIcon) when only one is needed; IconMorph is a name switch over the same components.",
        "Icons use currentColor, so color them from the parent button.",
        "For a whole button that changes label and state use action-swap; for theme switching with a label use theme-switch."
      ],
      "related": [
        "action-swap",
        "copy-button",
        "theme-switch",
        "reactions",
        "button"
      ],
      "usage": "import { useState } from \"react\";\nimport { IconMorph } from \"@/components/arc/icon-morph/icon-morph\";\n\nexport function MenuToggle() {\n  const [open, setOpen] = useState(false);\n  return (\n    <button type=\"button\" aria-label=\"Menu\" aria-expanded={open} onClick={() => setOpen(value => !value)}>\n      <IconMorph name=\"menu\" state={open} size={20} />\n    </button>\n  );\n}",
      "exports": [
        "IconMorph",
        "MenuIcon",
        "PlayPauseIcon",
        "PlusCheckIcon",
        "CopyCheckIcon",
        "SunMoonIcon",
        "ChevronIcon",
        "BellIcon",
        "HeartIcon"
      ]
    },
    {
      "name": "date-range-picker",
      "kind": "component",
      "title": "Date range picker",
      "description": "A range picker that grows from its trigger into two months with presets and a stretching range highlight.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/date-range-picker",
      "markdown": "https://uiarc.dev/components/date-range-picker/markdown",
      "registry": "https://uiarc.dev/r/date-range-picker.json",
      "keywords": [
        "react date range picker",
        "date range",
        "calendar range selection",
        "analytics date filter",
        "start and end date",
        "date presets",
        "booking date picker"
      ],
      "whenToUse": [
        "Analytics and reporting filters that need presets like Last 7 days or This quarter.",
        "Booking or leave requests where a start and end day are picked together.",
        "Toolbars where the picker must stay compact until opened."
      ],
      "whenNotToUse": [
        "Use date-picker for a single date.",
        "Use calendar when the month should stay visible on the page.",
        "Use time-picker when the user chooses a time of day rather than days."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"dialog\", aria-expanded, and a label that includes the current range.",
        "Each month is a role=\"grid\" labelled by its title, with columnheaders and gridcells using aria-selected for days in range and aria-current=\"date\" for today.",
        "Days have full date labels and a roving tabindex; months that are leaving turn inert so focus only finds the current set.",
        "A polite live region announces the start day, then the range and its length."
      ],
      "motion": [
        "The trigger surface grows into the panel on a physical spring and shrinks back on Apply while the formatted label flies into the trigger with a shared layoutId.",
        "The range highlight is one bar per week that stretches as you hover, and the two ends glide between days.",
        "Months slide in the direction of travel, and label words roll up or down with time's direction.",
        "Reduced motion jumps the surface size, drops the shared layout and slide, and uses short opacity fades."
      ],
      "responsive": [
        "months=\"auto\" shows two months beside a vertical preset rail when the boundary is at least 712px wide, otherwise one month under a scrolling preset row.",
        "In compact mode the panel width is min(352px, available) and day cells size between 32 and 42px to fit.",
        "The panel shifts horizontally to stay 8px inside the boundary, and recomputes on window resize."
      ],
      "performance": [
        "ResizeObservers on the trigger and panel feed the size springs; updates are batched through a microtask.",
        "Each week's highlight and each end is a motion value animation, so hovering does not re-render the grid beyond the range change.",
        "Intl.DateTimeFormat instances are memoized per locale."
      ],
      "aiNotes": [
        "Use when a start and end date are chosen together: report filters, analytics periods, bookings.",
        "Selection is draft until Apply; onChange fires only on Apply. Cancel, Escape, or an outside click discards the draft.",
        "Pass custom presets as { label, range: today => ({ start, end }) }; today is the viewer's local date.",
        "The trigger is disabled until hydration because today is unknown on the server.",
        "For a single date use date-picker; for an always-visible month use calendar."
      ],
      "related": [
        "date-picker",
        "calendar",
        "time-picker",
        "filter-toolbar"
      ],
      "usage": "import { useState } from \"react\";\nimport { DateRangePicker } from \"@/components/arc/date-range-picker/date-range-picker\";\nimport type { DateRange } from \"@/components/arc/date-range-picker/date-range-picker\";\n\nexport function ReportRange() {\n  const [range, setRange] = useState<DateRange | null>(null);\n  return (\n    <DateRangePicker\n      label=\"Report period\"\n      value={range}\n      onChange={setRange}\n      weekStartsOn={1}\n      maxDate={new Date()}\n    />\n  );\n}",
      "exports": [
        "DateRangePicker",
        "useToday"
      ]
    },
    {
      "name": "color-picker",
      "kind": "component",
      "title": "Color picker",
      "description": "A swatch that grows into a picker with format morphing, eyedropper, saved swatches, and contrast readout.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/color-picker",
      "markdown": "https://uiarc.dev/components/color-picker/markdown",
      "registry": "https://uiarc.dev/r/color-picker.json",
      "keywords": [
        "react color picker",
        "hex color input",
        "oklch color picker",
        "hsl picker",
        "eyedropper",
        "color contrast checker",
        "color swatches",
        "alpha color picker"
      ],
      "whenToUse": [
        "Theme editors and brand settings where users choose an exact color.",
        "Design tools that need hex, RGB, HSL, and OKLCH input plus opacity.",
        "Any color choice where contrast against a background should be visible while picking."
      ],
      "whenNotToUse": [
        "Use segmented-control or radio-group when the choice is between a few fixed colors.",
        "Use input for a plain hex field with no visual picking.",
        "Use chip-group for tagging items with a small preset palette."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"dialog\" and aria-expanded; the panel is a labelled dialog that turns inert while closing.",
        "The area thumb is a role=\"slider\" with aria-roledescription=\"2D slider\" and a value text naming saturation and brightness; hue and opacity are sliders with degree and percent value texts.",
        "The text field sets aria-invalid and points to a polite error message; the contrast ratio and its WCAG grade are also written out for screen readers.",
        "Saved swatches are a horizontal listbox with a roving tabindex, aria-selected on the current color, and labels that explain move and delete."
      ],
      "motion": [
        "The panel grows out of the swatch with an animated clip-path inset on a spring, and its drop shadow follows the clipped shape.",
        "Thumbs chase the pointer on a quick spring with slight bounce, and the contrast ratio counts to its new value.",
        "The format button morphs the text between hex, RGB, HSL, and OKLCH with text-morph; swatches pop in and reorder with layout springs.",
        "Reduced motion jumps the thumbs, fades the panel instead of clipping it, and skips the format morph and shake."
      ],
      "responsive": [
        "The panel is min(19rem, 100vw - 32px) wide and opens down and to the right of the swatch, so leave room there.",
        "Below 360px viewport width the \"against background\" caption hides to keep the contrast row on one line.",
        "The area and sliders use touch-action: none with pointer capture, so dragging on touch never scrolls the page."
      ],
      "performance": [
        "Colors are converted in plain math (HSV, HSL, OKLCH) with no color library.",
        "The panel mounts only while open, and a ResizeObserver measures it for the clip-path animation.",
        "Thumbs animate motion values, so dragging does not re-render the slider tracks."
      ],
      "aiNotes": [
        "Use for picking a single brand or theme color with an alpha channel. onValueChange always returns hex; parse it elsewhere if you need another format.",
        "Pass the real surface color as background so the contrast readout reflects where the color will be used.",
        "The eyedropper button appears only where window.EyeDropper exists (Chromium desktop).",
        "Imports TextMorph from the Arc text-morph component, so install that alongside it."
      ],
      "related": [
        "text-morph",
        "slider",
        "popover",
        "segmented-control"
      ],
      "usage": "import { useState } from \"react\";\nimport { ColorPicker } from \"@/components/arc/color-picker/color-picker\";\nimport type { ColorSwatch } from \"@/components/arc/color-picker/color-picker\";\n\nexport function BrandColor() {\n  const [color, setColor] = useState(\"#2F6BFF\");\n  const [swatches, setSwatches] = useState<ColorSwatch[]>([\n    { id: \"ink\", color: \"#17171A\" },\n    { id: \"sky\", color: \"#3A8DFF\" },\n  ]);\n\n  return (\n    <ColorPicker\n      label=\"Accent\"\n      value={color}\n      onValueChange={setColor}\n      background=\"#FFFFFF\"\n      swatches={swatches}\n      onSwatchesChange={setSwatches}\n    />\n  );\n}",
      "exports": [
        "ColorPicker",
        "parseColor",
        "toHex"
      ]
    },
    {
      "name": "morph-select",
      "kind": "component",
      "title": "Morph select",
      "description": "A select whose trigger grows into the list, with a gliding highlight and type-ahead.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/morph-select",
      "markdown": "https://uiarc.dev/components/morph-select/markdown",
      "registry": "https://uiarc.dev/r/morph-select.json",
      "keywords": [
        "react select",
        "animated select",
        "morphing dropdown",
        "listbox",
        "select with search",
        "grouped select",
        "custom select component",
        "type-ahead select"
      ],
      "whenToUse": [
        "Form fields and property panels where one value is picked from a known list.",
        "Grouped lists like time zones, assignees, or projects, with optional icons and meta.",
        "Compact toolbars where the picker should look like a pill until opened."
      ],
      "whenNotToUse": [
        "Use combobox when users type free text or the list is very long and remote.",
        "Use multi-select when more than one value can be chosen.",
        "Use dropdown-menu for actions rather than a stored value."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger is a role=\"combobox\" with aria-haspopup=\"listbox\", aria-expanded, and aria-activedescendant pointing at the active option.",
        "With search on, the input takes over as the combobox with aria-autocomplete=\"list\", and the trigger turns inert.",
        "Options are role=\"option\" with aria-selected and aria-disabled; groups are role=\"group\" labelled by their heading.",
        "A polite status announces the number of search results or No matches."
      ],
      "motion": [
        "The trigger surface grows into the list on a physical spring with slight bounce and folds back without overshoot; the corner radius morphs with it.",
        "One highlight glides between options; the chosen label lifts out of the list and flies into the trigger while the trigger width springs.",
        "Values in the trigger roll up or down depending on whether the new option is later or earlier in the list.",
        "Reduced motion jumps the size, removes the fly and roll, and uses short fades."
      ],
      "responsive": [
        "The closed trigger sizes to its label up to min(20rem, 100vw - 2rem); the open list is max(trigger width, min(panelWidth, 100vw - 2rem)).",
        "Set align=\"end\" for triggers near the right edge so the surface grows leftward.",
        "Hover highlight follows the mouse only; on touch a tap picks directly, and hover styles apply only on fine pointers."
      ],
      "performance": [
        "A hidden measuring copy and a ResizeObserver size the trigger and list; the surface animates width, height, and radius through motion values.",
        "All options render; there is no virtualization, so keep lists to a few hundred items.",
        "Search filters in memory on label, meta, and keywords."
      ],
      "aiNotes": [
        "Use for compact property pickers and form fields with up to a few hundred options, including avatars or icons.",
        "Add keywords to options so search matches synonyms; meta shows trailing detail like counts or offsets.",
        "Pass name to submit with a native form; the hidden input carries the value.",
        "Use combobox when people mostly type free text, and multi-select for several values."
      ],
      "related": [
        "select",
        "combobox",
        "multi-select",
        "dropdown-menu"
      ],
      "usage": "import { useState } from \"react\";\nimport { MorphSelect } from \"@/components/arc/morph-select/morph-select\";\n\nexport function TimezoneField() {\n  const [zone, setZone] = useState<string | null>(\"europe/zurich\");\n  return (\n    <MorphSelect\n      label=\"Time zone\"\n      name=\"timezone\"\n      value={zone}\n      onValueChange={setZone}\n      items={[\n        { label: \"Europe\", options: [\n          { value: \"europe/london\", label: \"London\", meta: \"UTC+0\" },\n          { value: \"europe/zurich\", label: \"Zurich\", meta: \"UTC+1\", keywords: \"switzerland\" },\n        ] },\n        { label: \"Americas\", options: [\n          { value: \"america/new_york\", label: \"New York\", meta: \"UTC−5\" },\n          { value: \"america/los_angeles\", label: \"Los Angeles\", meta: \"UTC−8\" },\n        ] },\n      ]}\n    />\n  );\n}",
      "exports": [
        "MorphSelect"
      ]
    },
    {
      "name": "action-morph",
      "kind": "component",
      "title": "Action morph",
      "description": "A floating button that morphs into a quick-action menu, then an inline form, then back with a check.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "new"
      ],
      "docs": "https://uiarc.dev/components/action-morph",
      "markdown": "https://uiarc.dev/components/action-morph/markdown",
      "registry": null,
      "keywords": [
        "react floating action button",
        "fab menu",
        "quick actions",
        "morphing fab",
        "inline composer",
        "speed dial",
        "quick create button"
      ],
      "whenToUse": [
        "Quick capture of tasks, notes, or comments without leaving the page.",
        "Apps with a few frequent create actions that deserve a persistent button.",
        "Mobile layouts where a corner button should expand into a small form."
      ],
      "whenNotToUse": [
        "Use dialog or drawer for longer forms with several fields.",
        "Use dropdown-menu when actions run immediately and need no input.",
        "Use action-button for one async save on a button in place."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The button has aria-haspopup=\"menu\"; after success its label adds the success message.",
        "The menu is role=\"menu\" labelled by its title, with role=\"menuitem\" buttons and aria-keyshortcuts for letter shortcuts.",
        "Composer fields set aria-invalid and aria-describedby on an empty or failed submit; the form sets aria-busy while sending.",
        "A polite status region announces the success label, and focus moves into each new face and back to the button on close."
      ],
      "motion": [
        "One surface animates width, height, and corner radius on physical springs: growing carries a little bounce, folding settles without overshoot.",
        "Going deeper slides the new face in from the right and pushes the old one left; menu rows stagger in; a highlight glides between actions.",
        "Submitting folds the composer into the button, which tints to success while a check draws itself, then the plus rotates back in.",
        "Reduced motion jumps the size, swaps faces with fades, drops the press scale and shake, and slows the spinner."
      ],
      "responsive": [
        "The menu is min(16rem, 100vw - 2rem) wide and the composer min(20rem, 100vw - 2rem), so both fit narrow phones.",
        "corner=\"start\" grows the surface to the right for bottom-left placement.",
        "The button is 56px with touch-action: manipulation; hover styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "Each face reports its size through a ResizeObserver only while it is current; the shape animates from those sizes.",
        "Only the current face and the one leaving are mounted."
      ],
      "aiNotes": [
        "Use for the two or three things people create most often on a page, such as a task, note, or event.",
        "Return the real promise from onSubmit; on rejection the draft stays and a retry message shows.",
        "Drafts are kept per action while the component is mounted, so going back and forth does not lose text.",
        "The component does not position itself; wrap it in a fixed container at the matching corner.",
        "For a single async commit use action-button; for longer forms use dialog or drawer."
      ],
      "related": [
        "action-button",
        "orbit-menu",
        "dropdown-menu",
        "dialog",
        "toast"
      ],
      "usage": "import { CheckSquare, StickyNote } from \"lucide-react\";\nimport { ActionMorph } from \"@/components/arc/action-morph/action-morph\";\n\nexport function QuickCreate() {\n  return (\n    <div style={{ position: \"fixed\", right: 24, bottom: 24 }}>\n      <ActionMorph\n        label=\"Create\"\n        actions={[\n          {\n            id: \"task\",\n            label: \"New task\",\n            description: \"Add to your list\",\n            icon: <CheckSquare strokeWidth={1.75} />,\n            shortcut: \"t\",\n            composer: {\n              title: \"New task\",\n              placeholder: \"What needs doing?\",\n              choices: { label: \"Due\", options: [\"Today\", \"Tomorrow\", \"Next week\"] },\n              submitLabel: \"Add\",\n              successLabel: \"Task added\",\n            },\n          },\n          {\n            id: \"note\",\n            label: \"New note\",\n            icon: <StickyNote strokeWidth={1.75} />,\n            shortcut: \"n\",\n            composer: { title: \"New note\", placeholder: \"Write a note\", multiline: true, submitLabel: \"Save\", successLabel: \"Note saved\" },\n          },\n        ]}\n        onSubmit={({ action, text, choice }) => save(action, text, choice)}\n      />\n    </div>\n  );\n}",
      "exports": [
        "ActionMorph"
      ]
    },
    {
      "name": "share-sheet",
      "kind": "component",
      "title": "Share sheet",
      "description": "A Share button that opens into a panel with copy link, access level, channels, and people to send to.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "new"
      ],
      "docs": "https://uiarc.dev/components/share-sheet",
      "markdown": "https://uiarc.dev/components/share-sheet/markdown",
      "registry": null,
      "keywords": [
        "react share sheet",
        "share button",
        "share dialog",
        "copy link",
        "share with people",
        "link access level",
        "morphing share panel",
        "share to slack"
      ],
      "whenToUse": [
        "A Share button on a document, design, or board header where people share often.",
        "Sharing flows that combine copy link, access level, and sending to a few recent people.",
        "Posting to a small set of channels, such as Slack or email, with in-place pending and done feedback."
      ],
      "whenNotToUse": [
        "Use copy-button when all you need is to copy a URL.",
        "Use dialog when sharing needs a full form, such as roles per person or an email search.",
        "Use popover for a small menu of share targets with no people picker."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"dialog\"; the open panel is role=\"dialog\" labelled by its title.",
        "Opening moves focus to Copy, sending moves it to Done, and closing returns it to the Share button.",
        "Access options are a listbox of role=\"option\" buttons with aria-selected and roving tabindex.",
        "People are toggle buttons with aria-pressed; chips have a Remove <name> button.",
        "A polite status region announces copies, picks, removals, channel results, and who a share was sent to.",
        "A press outside the component closes the panel without stealing focus."
      ],
      "motion": [
        "One shape springs its width, height, and corner radius between the button, the panel, and the confirmation; faces cross with a short blur and scale.",
        "A picked avatar flies from the recent row to its chip on a separate layer, so the scrolling chips row never clips it.",
        "Send with nobody picked shakes the Send to row on a spring; the count in the send button rolls one digit up or down.",
        "Reduced motion jumps the shape to size, swaps faces with a plain fade, skips the avatar flight and the shake, and slows the spinner."
      ],
      "responsive": [
        "The panel is min(24rem, 100vw - 2rem) wide; set --share-sheet-width on an ancestor to fit a narrower container. It is a container, so its rows adapt to that width.",
        "The panel is min(24rem, 100vw - 2rem) wide and the confirmation min(19rem, 100vw - 2rem), so both fit a 320px phone.",
        "align=\"start\" grows the panel from the left edge of the button; use it when the button sits near the left of the viewport.",
        "Hover styles apply only on hover-capable fine pointers; the trigger uses touch-action: manipulation for fast taps."
      ],
      "performance": [
        "Each face runs a ResizeObserver while it is current, and the shape animates three motion values; nothing runs while closed.",
        "Avatars are plain img tags from your URLs; pass small, pre-sized images since each person renders at up to 80px."
      ],
      "aiNotes": [
        "Use for sharing a document, board, or file from a toolbar without leaving the page.",
        "Wire onSend and onChannel to your API and return the promise; the component handles pending, error, and done states itself.",
        "people is a short list of recent collaborators, not a directory search. For inviting by email use the invite-people block.",
        "The root reserves only the button's size; the panel floats over the page, so leave room below or pick align to grow away from an edge."
      ],
      "related": [
        "popover",
        "copy-button",
        "avatar-group",
        "dialog",
        "toast"
      ],
      "usage": "import { ShareSheet } from \"@/components/arc/share-sheet/share-sheet\";\nimport { Globe, Lock, Mail, MessageSquare } from \"lucide-react\";\n\nexport function DocumentShare() {\n  return (\n    <ShareSheet\n      title=\"Q3 roadmap\"\n      link=\"https://example.com/d/q3-roadmap\"\n      people={[\n        { id: \"ana\", name: \"Ana Ruiz\", avatar: \"/avatars/ana.jpg\" },\n        { id: \"sam\", name: \"Sam Lee\", avatar: \"/avatars/sam.jpg\" },\n      ]}\n      access={[\n        { value: \"team\", label: \"Team\", description: \"Anyone on the team can view\", icon: <Lock size={16} /> },\n        { value: \"public\", label: \"Anyone with the link\", description: \"Can view and comment\", icon: <Globe size={16} /> },\n      ]}\n      channels={[\n        { id: \"slack\", label: \"Slack\", doneLabel: \"Posted\", icon: <MessageSquare size={16} /> },\n        { id: \"email\", label: \"Email\", doneLabel: \"Sent\", icon: <Mail size={16} /> },\n      ]}\n      onSend={({ people, access }) => api.share(people.map(person => person.id), access)}\n      onChannel={channel => api.post(channel)}\n    />\n  );\n}",
      "exports": [
        "ShareSheet"
      ]
    },
    {
      "name": "line-chart",
      "kind": "component",
      "title": "Line chart",
      "description": "A multi-series line chart with a gliding crosshair, legend toggles, and paths that morph between ranges.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/line-chart",
      "markdown": "https://uiarc.dev/components/line-chart/markdown",
      "registry": "https://uiarc.dev/r/line-chart.json",
      "keywords": [
        "react line chart",
        "time series chart",
        "animated line chart",
        "chart with crosshair",
        "trend chart",
        "multi series line chart",
        "dashboard chart",
        "accessible chart"
      ],
      "whenToUse": [
        "Traffic, revenue, or latency over days or weeks in a dashboard card.",
        "Comparing this period against a previous period or target with a dashed series.",
        "Charts that switch ranges (7d, 30d, 90d) and should morph rather than redraw."
      ],
      "whenNotToUse": [
        "Use sparkline for a tiny inline trend with no axes or tooltip.",
        "Use bar-chart for discrete categories or counts per bucket.",
        "Use donut-chart when the question is the share of a whole, not change over time."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The figure is named by label; the plot is a role=\"slider\" whose aria-valuetext reads the date and every visible series value.",
        "A visually hidden summary gives the range and latest values, and a hidden table lists every point for screen readers.",
        "Legend toggles are buttons with aria-pressed; a hidden live region says Loading while loading is true.",
        "The SVG and tooltip are aria-hidden, so values are never read twice."
      ],
      "motion": [
        "Lines draw left to right the first time the chart scrolls into view.",
        "New data morphs every line from the shape on screen, sampled on shared x positions, and the value scale springs to its new range while gridlines slide.",
        "A hidden series flattens into the baseline as it fades; the crosshair glides between points and the tooltip follows on its own spring, flipping sides near the edge.",
        "Reduced motion skips the draw, morphs, and glides, and stops the loading pulse."
      ],
      "responsive": [
        "Width follows the container through a ResizeObserver; the plot redraws in its own pixels, and axis labels thin to one per 76px.",
        "Touch scrubs with pointer capture and releases on lift; touch-action: pan-y keeps vertical page scroll working over the plot.",
        "A mouse scrubs on hover and releases on leave. The value gutter is a fixed 48px column."
      ],
      "performance": [
        "Paths are written straight to the DOM from motion values, not React state, so scrubbing and morphs do not re-render the tree.",
        "Smooth curves are sampled up to 16 points per segment (about 192 per line); keep data to a few hundred points per series.",
        "One ResizeObserver per chart; the draw-in waits for useInView."
      ],
      "aiNotes": [
        "Use for one to three measures over time when the trend shape matters. For category comparison use bar-chart; for parts of a whole use donut-chart.",
        "Keep series and datum keys stable across range changes so lines morph instead of redrawing.",
        "Set axisLabel only on the dates you want on the axis; the chart thins them further to one per 76px, always keeping the newest.",
        "Drive a headline number from onActiveChange, as in metric-card or stat-card layouts."
      ],
      "related": [
        "bar-chart",
        "sparkline",
        "donut-chart",
        "metric-card"
      ],
      "usage": "import { LineChart } from \"@/components/arc/line-chart/line-chart\";\n\nconst data = [\n  { key: \"2026-09-01\", label: \"Tue, Sep 1\", axisLabel: \"Sep 1\", values: { signups: 120, previous: 98 } },\n  { key: \"2026-09-02\", label: \"Wed, Sep 2\", values: { signups: 142, previous: 104 } },\n  { key: \"2026-09-03\", label: \"Thu, Sep 3\", axisLabel: \"Sep 3\", values: { signups: 131, previous: 110 } },\n];\n\nexport function SignupsChart() {\n  return (\n    <LineChart\n      label=\"Signups\"\n      data={data}\n      series={[\n        { key: \"signups\", label: \"This period\" },\n        { key: \"previous\", label: \"Previous period\", dashed: true },\n      ]}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Headline readout driven by the crosshair",
          "code": "const [active, setActive] = useState<LineChartDatum | null>(null);\n\n<p>{(active ?? data[data.length - 1]).values.signups}</p>\n<LineChart label=\"Signups\" data={data} series={series} onActiveChange={(_, datum) => setActive(datum)} />"
        }
      ],
      "exports": [
        "LineChart"
      ]
    },
    {
      "name": "donut-chart",
      "kind": "component",
      "title": "Donut chart",
      "description": "A donut whose arcs morph between datasets, with the active value rolling into the center.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/donut-chart",
      "markdown": "https://uiarc.dev/components/donut-chart/markdown",
      "registry": "https://uiarc.dev/r/donut-chart.json",
      "keywords": [
        "react donut chart",
        "pie chart",
        "ring chart",
        "share of total chart",
        "animated donut",
        "chart with legend",
        "category breakdown",
        "accessible pie chart"
      ],
      "whenToUse": [
        "Traffic by source, spend by category, or storage by file type.",
        "A dashboard card where the total and one highlighted share matter most.",
        "Switching between datasets, such as this month and last month, with the same categories."
      ],
      "whenNotToUse": [
        "Use bar-chart when precise comparison between parts matters or there are many parts.",
        "Use gauge or usage-meter for a single value against a limit.",
        "Use line-chart for change over time."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The legend is the keyboard path: each row is a button with aria-pressed (shown, or pinned with legendAction select) and a label giving value, share, and grouped members.",
        "Showing or hiding a segment is announced in a polite live region with the new total. Without a legend the ring is focusable and announces each segment as the arrows reach it.",
        "The ring and center readout are aria-hidden; a hidden summary and table list every part, including the members of Other.",
        "Keyboard focus previews a segment in the ring; hover previews with a mouse only. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One sweep the first time the chart scrolls into view: every segment leaves the top together and each trailing edge follows a beat behind the one before.",
        "New data and hidden segments morph start and end angles, never paths, from wherever each arc is on screen. Springs keep their velocity, so an interrupted change continues smoothly. Segments keep their order and never remount mid-morph.",
        "The active segment slides out along its middle and thickens slightly while the others dim. One pointer handler hit-tests the angle, so crossing a gap never drops the hover.",
        "The center readout rolls like a drum toward the active segment with tabular numbers in a fixed cell; totals and shares count to new values.",
        "Reduced motion jumps arcs, lifts, and text to their final state."
      ],
      "responsive": [
        "The figure is a container: at 460px and wider the legend sits beside the ring, below that it stacks under it.",
        "The ring scales down from size to fit narrower containers while keeping its aspect ratio.",
        "Legend hover styles only apply on hover-capable fine pointers; taps pin a segment."
      ],
      "performance": [
        "Arc angles and lifts are motion values; one batched paint per frame writes every path d and transform straight to the DOM, with no React render per frame.",
        "Counting numbers write their text directly; center readouts stay mounted, so hovering never mounts or unmounts nodes."
      ],
      "aiNotes": [
        "Use for two to six parts of one whole. With more parts, lean on groupBelow and maxSegments or use bar-chart.",
        "Keep keys stable between datasets so arcs morph in place.",
        "Leave color out to get the shared --series-1 to --series-4 palette in data order, then neutral steps; a key keeps its color across datasets. Pass colors only when they carry meaning.",
        "Legend rows show and hide segments by default; set legendAction to select for the older pin behavior.",
        "Drive activeKey from a table or filter to highlight the same category elsewhere."
      ],
      "related": [
        "bar-chart",
        "line-chart",
        "gauge",
        "usage-meter"
      ],
      "usage": "import { DonutChart } from \"@/components/arc/donut-chart/donut-chart\";\n\nexport function TrafficSources() {\n  return (\n    <DonutChart\n      label=\"Visits by source\"\n      unit=\"visits\"\n      data={[\n        { key: \"search\", label: \"Search\", value: 4210 },\n        { key: \"direct\", label: \"Direct\", value: 2380 },\n        { key: \"social\", label: \"Social\", value: 1190 },\n        { key: \"email\", label: \"Email\", value: 640 },\n        { key: \"ads\", label: \"Ads\", value: 120 },\n        { key: \"other\", label: \"Referral\", value: 90 },\n      ]}\n    />\n  );\n}",
      "exports": [
        "DonutChart"
      ]
    },
    {
      "name": "streamgraph",
      "kind": "component",
      "title": "Streamgraph",
      "description": "Layered streams on a wiggle baseline that morph between ranges, with a layer you can isolate and read week by week.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/streamgraph",
      "markdown": "https://uiarc.dev/components/streamgraph/markdown",
      "registry": "https://uiarc.dev/r/streamgraph.json",
      "keywords": [
        "streamgraph",
        "stream graph react",
        "stacked area chart",
        "wiggle chart",
        "theme river",
        "stacked stream chart",
        "layered area chart",
        "animated streamgraph"
      ],
      "whenToUse": [
        "Showing the changing share of topics, channels, or genres week by week.",
        "Storytelling dashboards where the shape of seasonality and spikes is the point.",
        "Comparing ranges where a morph between them explains what changed."
      ],
      "whenNotToUse": [
        "Use line-chart when exact values or comparisons between series matter.",
        "Use bar-chart for a few periods or categories.",
        "Avoid it for data with negative values; streams stack magnitudes."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The plot is a focusable group; each reading is announced with the point, the isolated layer's value, and the total.",
        "Legend toggles are buttons with aria-pressed; hovering or focusing one isolates its layer.",
        "A visually hidden table lists every point and layer value.",
        "Layers are separated by a 2px surface gap and named in place, so identity never rests on shade alone."
      ],
      "motion": [
        "The stream swells out of a hairline along its centre the first time it is seen.",
        "New data blends every layer on a shared grid, so ranges of different lengths morph without redrawing.",
        "Hidden layers thin to nothing while the rest reflow; the crosshair and tooltip glide on springs.",
        "Reduced motion jumps to the final shapes and keeps the crosshair and tooltip without travel."
      ],
      "responsive": [
        "The plot fills its container; axis labels thin out to one per 72px, counted back from the latest.",
        "Direct labels hide where a layer is too thin or the plot too narrow; the tooltip and legend still name it.",
        "Touch drags scrub through time with pointer capture while vertical page scroll stays free."
      ],
      "performance": [
        "Each layer is resampled once per data change onto 161 points; paint recomputes the baseline and writes paths directly.",
        "Hover state re-renders only the tooltip and a few attributes; morphs never re-render React per frame."
      ],
      "aiNotes": [
        "Choose it for how a mix changes over time when the overall rhythm matters more than exact totals.",
        "Put the most important layer first; it runs through the middle of the stream.",
        "Use offset=\"zero\" when people need to read totals against a baseline, like a stacked area chart.",
        "Keep layers to about eight; fold the rest into Other."
      ],
      "related": [
        "line-chart",
        "brush-chart",
        "bar-chart",
        "sankey-flow"
      ],
      "usage": "import { Streamgraph } from \"@/components/arc/streamgraph/streamgraph\";\n\nconst series = [{ key: \"bugs\", label: \"Bugs\" }, { key: \"billing\", label: \"Billing\" }, { key: \"onboarding\", label: \"Onboarding\" }];\nconst data = weeks.map(week => ({ key: week.iso, label: `Week of ${week.name}`, axisLabel: week.monthStart ? week.month : undefined, values: week.tickets }));\n\nexport function TicketsByTopic() {\n  return <Streamgraph data={data} series={series} label=\"Support tickets by topic\" unit=\"tickets\" categoryLabel=\"Week\" />;\n}",
      "exports": [
        "Streamgraph"
      ]
    },
    {
      "name": "brush-chart",
      "kind": "component",
      "title": "Brush chart",
      "description": "A dense time series with an overview strip: drag a window to zoom, resize it by its handles, and read events in place.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/brush-chart",
      "markdown": "https://uiarc.dev/components/brush-chart/markdown",
      "registry": "https://uiarc.dev/r/brush-chart.json",
      "keywords": [
        "brush chart",
        "zoomable time series",
        "chart with overview",
        "range selector chart",
        "focus and context chart",
        "stock chart brush",
        "d3 brush react",
        "annotated line chart"
      ],
      "whenToUse": [
        "Product analytics with a year or more of daily data.",
        "Incident reviews where events need to be read against a metric.",
        "Any chart where people ask to zoom into a stretch without losing context."
      ],
      "whenNotToUse": [
        "Use line-chart for a few weeks of data or several series.",
        "Use sparkline for a small inline trend without interaction.",
        "Avoid it for data that dips below zero or needs a log scale."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The window and both handles are sliders with dates as aria-valuetext.",
        "The plot is a focusable group; each reading is announced with its date, value, and any event on that day.",
        "A visually hidden table lists every point in the window with its event.",
        "Event markers pair the second series hue with a text label or tooltip; they never rely on color alone."
      ],
      "motion": [
        "The line draws in from the left once, the first time it is seen.",
        "Dragging tracks the pointer 1:1; presets, resets, and controlled changes glide the window on a spring.",
        "The value axis springs to the tallest point in view, and the raw line crossfades with the smoothed trend as density changes.",
        "Reduced motion places the window directly and keeps the crosshair without travel."
      ],
      "responsive": [
        "Both charts fill their container; date ticks pick days, weeks, months, or years to fit the width.",
        "Handles have a 28 by 44px hit area for touch, and the strip keeps vertical page scroll free.",
        "Event labels are placed left to right and skip themselves when they would collide."
      ],
      "performance": [
        "Past two points per pixel, each pixel column keeps only its low and high, so spikes survive and paths stay small.",
        "The overview path is built once per width; the rolling average is computed once per data change.",
        "Window moves re-render one component; a few thousand points stay smooth."
      ],
      "aiNotes": [
        "Choose it for long daily or hourly series where people need both the whole history and a close look.",
        "Control range to sync presets (30D, 90D, 1Y) or other charts with the window.",
        "Use annotations for launches, incidents, and pricing changes; keep labels to a word or two.",
        "The value axis starts at zero, so it suits counts and totals rather than prices that hover far from zero."
      ],
      "related": [
        "line-chart",
        "streamgraph",
        "sparkline",
        "date-range-picker"
      ],
      "usage": "import { useState } from \"react\";\nimport { BrushChart } from \"@/components/arc/brush-chart/brush-chart\";\n\nexport function ActiveUsers({ days }: { days: { date: number; value: number }[] }) {\n  const [range, setRange] = useState<[number, number]>([days[days.length - 90].date, days[days.length - 1].date]);\n  return <BrushChart data={days} label=\"Daily active users\" unit=\"users\" range={range} onRangeChange={setRange}\n    annotations={[{ date: Date.UTC(2026, 2, 24), label: \"v2\", description: \"Offline mode and shared spaces\" }]} />;\n}",
      "exports": [
        "BrushChart"
      ]
    },
    {
      "name": "race-bar-chart",
      "kind": "component",
      "title": "Race bar chart",
      "description": "A ranking over time: bars overtake each other on springs, values count up, and a timeline you can play or scrub.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/race-bar-chart",
      "markdown": "https://uiarc.dev/components/race-bar-chart/markdown",
      "registry": null,
      "keywords": [
        "bar chart race",
        "racing bars",
        "ranking over time",
        "animated bar chart",
        "leaderboard",
        "timeline",
        "data story"
      ],
      "whenToUse": [
        "Showing how the leaders of a ranking change across many periods.",
        "A launch or recap moment where the motion itself is the point.",
        "Letting people scrub to any period and read exact values."
      ],
      "whenNotToUse": [
        "Use line-chart when exact trends over time matter more than rank.",
        "Use bar-chart for a single period's comparison.",
        "Avoid for dashboards people glance at; autoplay competes with other content."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plot is a focusable group with a description of its keys; the timeline is role=\"slider\" with the period as aria-valuetext.",
        "Stepping announces the period and the leader through a polite live region; reading a bar announces its name, rank, and value.",
        "A visually hidden table lists every item's value for every period.",
        "Group hue is never the only cue: every bar carries its name and value."
      ],
      "motion": [
        "Time is continuous: values interpolate between periods, so bars grow smoothly and numbers count rather than jump.",
        "Rank changes move each row on its own spring, so overtakes read as one continuous slide; bars entering the top set rise from below and fade in.",
        "The drawn clock follows the requested time on a critically damped spring, so steps glide and a replay sweeps back through every period.",
        "Axis ticks glide with the scale and crossfade when the step changes; the period label rolls in with a short blur.",
        "Playback pauses while the chart is off screen. Reduced motion skips autoplay, steps periods without interpolation, and places bars immediately."
      ],
      "responsive": [
        "Below 400px names move inside the bars so bar length keeps its room; values sit past the name when a bar is short.",
        "The tick count adapts to the plot width and the period label steps down a size on small screens.",
        "The timeline is a 36px tall touch target and scrubs with a finger."
      ],
      "performance": [
        "One requestAnimationFrame loop writes transforms, widths, and text straight to the DOM; React renders only on period changes.",
        "The loop sleeps when paused and settled."
      ],
      "aiNotes": [
        "Choose it for a ranking that changes over time, told as a story: top products, cities, or accounts month by month.",
        "Pass cumulative values (year to date) for a race that only grows, or raw values for a ranking that rises and falls.",
        "Keep items to about 20 and bars to 6 to 10; the table carries the rest.",
        "Colors come from the shared --series-1 to --series-4 tokens in foundation.css: series 1 is the accent itself and series 2 to 4 rotate its hue, validated all-pairs for color vision deficiency across every accent in light and dark. Override the tokens on any ancestor to rebrand."
      ],
      "related": [
        "bar-chart",
        "line-chart",
        "streamgraph",
        "slope-chart"
      ],
      "usage": "import { RaceBarChart } from \"@/components/arc/race-bar-chart/race-bar-chart\";\n\nconst items = [\n  { id: \"chair\", label: \"Oak lounge chair\", group: \"furniture\" },\n  { id: \"lamp\", label: \"Mushroom lamp\", group: \"lighting\" },\n];\nconst frames = [\n  { period: \"January\", values: { chair: 14000, lamp: 66000 } },\n  { period: \"February\", values: { chair: 30000, lamp: 128000 } },\n];\n\nexport function ProductRace() {\n  return <RaceBarChart items={items} frames={frames} groups={[{ id: \"furniture\", label: \"Furniture\" }, { id: \"lighting\", label: \"Lighting\" }]} label=\"Revenue by product\" highlight=\"chair\" />;\n}",
      "exports": [
        "RaceBarChart"
      ]
    },
    {
      "name": "ridgeline",
      "kind": "component",
      "title": "Ridgeline",
      "description": "Overlapping distributions, one ridge per group: hover to lift a ridge and read its quartiles, switch datasets and every curve morphs.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ridgeline",
      "markdown": "https://uiarc.dev/components/ridgeline/markdown",
      "registry": "https://uiarc.dev/r/ridgeline.json",
      "keywords": [
        "ridgeline",
        "joy plot",
        "joyplot",
        "density plot",
        "distribution",
        "kde",
        "quartiles"
      ],
      "whenToUse": [
        "Seasonal or categorical distributions such as temperatures by month or response times by region.",
        "Showing where two groups differ in shape, such as bimodal winters."
      ],
      "whenNotToUse": [
        "Use beeswarm when each individual observation matters.",
        "Use bar-chart when only one summary number per group matters."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The plot is a focusable group; the lifted ridge's median, middle half, range, and count are announced through a polite live region.",
        "A visually hidden table lists median, quartiles, extremes, and count for every series.",
        "Every row is labelled in text; the median tint is backed by the table and tooltip."
      ],
      "motion": [
        "Curves are sampled on a shared grid, so a new dataset or domain interpolates every point: ridges swell, slide, and split in one spring.",
        "The first reveal rolls the ridges in from the top row down.",
        "The lifted ridge is a copy drawn above the others that rises 6px on a spring, with its middle half shaded and its median marked.",
        "The tooltip glides after the pointer and stays inside the chart. Reduced motion draws final shapes immediately."
      ],
      "responsive": [
        "Label gutter and tick count adapt to width; the plot redraws at the measured size so strokes stay crisp.",
        "Touch taps lift a ridge and read at the tapped value."
      ],
      "performance": [
        "Densities are computed once per dataset (96 samples per ridge); animation interpolates arrays and writes path strings directly.",
        "Suitable for up to a few thousand observations per series."
      ],
      "aiNotes": [
        "Choose it to compare the shape of many distributions: spread, skew, and bimodality, not just averages.",
        "Pass raw observations, at least 20 per series, and fix domain when switching datasets so shapes move on a still axis.",
        "Keep rows to about 20; beyond that, use small multiples or a box plot.",
        "Colors come from the shared --series-1 to --series-4 tokens in foundation.css: series 1 is the accent itself and series 2 to 4 rotate its hue, validated all-pairs for color vision deficiency across every accent in light and dark. Override the tokens on any ancestor to rebrand."
      ],
      "related": [
        "streamgraph",
        "line-chart",
        "activity-heatmap"
      ],
      "usage": "import { Ridgeline } from \"@/components/arc/ridgeline/ridgeline\";\n\nexport function Latency({ regions }: { regions: { id: string; label: string; values: number[] }[] }) {\n  return <Ridgeline series={regions} label=\"API latency by region\" unit=\" ms\" />;\n}",
      "exports": [
        "Ridgeline"
      ]
    },
    {
      "name": "treemap",
      "kind": "component",
      "title": "Treemap",
      "description": "A squarified treemap: click to drill and the tiles grow to fill the view, with a breadcrumb back and metrics that morph every tile.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/treemap",
      "markdown": "https://uiarc.dev/components/treemap/markdown",
      "registry": "https://uiarc.dev/r/treemap.json",
      "keywords": [
        "treemap",
        "tree map",
        "squarified",
        "hierarchy",
        "part to whole",
        "drill down",
        "zoomable treemap"
      ],
      "whenToUse": [
        "Many parts where the biggest ones matter and people drill for detail.",
        "Showing size and a second measure at once."
      ],
      "whenNotToUse": [
        "Use bar-chart when exact comparisons between similar values matter.",
        "Use sunburst when the depth of the hierarchy is the story."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The tiles form a focusable group with a description of its keys; the breadcrumb is a nav with aria-current on the current level.",
        "The tile in focus is announced with its path, value, share of parent, and shading measure through a polite live region.",
        "A visually hidden table lists every node's path, value, share, and measure."
      ],
      "motion": [
        "Drilling in carries every tile through one affine zoom: the chosen tile grows to fill the view, its siblings fly outward, and its children open inside it.",
        "Zooming out reverses the same path, so each level shrinks back into the tile it came from.",
        "Changing a measure re-lays out the same ids, so tiles resize and recolor in place; totals roll and breadcrumbs slide.",
        "Text never scales: tiles are sized each frame and labels appear only where they fit. Reduced motion places tiles immediately."
      ],
      "responsive": [
        "The layout squarifies to the measured width, so tiles stay close to square on any screen; headers and labels hide where they would not fit.",
        "Tap a tile to read it, tap a branch to drill in."
      ],
      "performance": [
        "Layouts are computed per focus; animation interpolates rects and writes transforms and sizes directly. Comfortable up to a few hundred nodes."
      ],
      "aiNotes": [
        "Choose it for part-to-whole across a hierarchy with many leaves, such as revenue by region, country, and plan.",
        "Give nodes stable ids so switching the size measure morphs rather than rebuilds.",
        "The shading measure should be a magnitude (growth, margin), not identity; identity comes from the top level hue.",
        "Colors come from the shared --series-1 to --series-4 tokens in foundation.css: series 1 is the accent itself and series 2 to 4 rotate its hue, validated all-pairs for color vision deficiency across every accent in light and dark. Override the tokens on any ancestor to rebrand."
      ],
      "related": [
        "sunburst",
        "donut-chart",
        "bar-chart",
        "waffle-chart"
      ],
      "usage": "import { Treemap } from \"@/components/arc/treemap/treemap\";\n\nconst data = {\n  id: \"all\", label: \"All regions\",\n  children: [\n    { id: \"na\", label: \"North America\", children: [{ id: \"us\", label: \"United States\", value: 18400, color: 24 }, { id: \"ca\", label: \"Canada\", value: 2350, color: 19 }] },\n    { id: \"eu\", label: \"Europe\", children: [{ id: \"de\", label: \"Germany\", value: 4120, color: 28 }] },\n  ],\n};\n\nexport function Revenue() {\n  return <Treemap data={data} label=\"ARR\" colorLabel=\"Growth\" formatColor={value => `+${value}%`} />;\n}",
      "exports": [
        "Treemap"
      ]
    },
    {
      "name": "sunburst",
      "kind": "component",
      "title": "Sunburst",
      "description": "A hierarchy in rings: click a segment and every arc swings around it as the new centre, with a breadcrumb back and a rolling total.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/sunburst",
      "markdown": "https://uiarc.dev/components/sunburst/markdown",
      "registry": null,
      "keywords": [
        "sunburst chart",
        "zoomable sunburst",
        "hierarchical pie chart",
        "radial treemap",
        "drill down chart",
        "multi level donut",
        "cloud cost breakdown",
        "react sunburst"
      ],
      "whenToUse": [
        "Drilling into where a total comes from, one level at a time.",
        "Showing the share of each part within its parent and within the whole.",
        "Dashboards where a compact, explorable overview beats a long table."
      ],
      "whenNotToUse": [
        "Use donut-chart for a single level of parts.",
        "Use tree-table when people need to compare exact values across many rows.",
        "Avoid it for more than about 150 leaves; thin slivers stop being readable."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The chart is a focusable group; the highlighted segment is announced with its path, value, and share of its parent.",
        "The breadcrumb is a nav landmark with the current level marked aria-current.",
        "Legend rows are buttons that name the segment, its value and share, and whether it zooms in.",
        "A visually hidden table lists every node's full path, value, and share of its parent."
      ],
      "motion": [
        "Zooming interpolates every arc's angles and ring from where it is on screen to its place around the new centre, on one spring.",
        "Rings re-share the radius as levels appear or run out, so a shallow branch fills the chart as it zooms.",
        "The centre value rolls digit by digit; labels rise in from a soft blur; the breadcrumb slides and reflows.",
        "Reduced motion jumps arcs to their final place and swaps text with a short fade."
      ],
      "responsive": [
        "The ring scales to its container; the legend sits beside it when there is room and below it on narrow screens.",
        "Arc labels follow the ring's curve and appear only where the arc can hold them.",
        "Touch taps zoom; the centre is a large target to zoom back out."
      ],
      "performance": [
        "Partitioning runs once per data change; arcs are painted by writing path attributes, not by re-rendering React each frame.",
        "Label fit is measured per arc during paint; keep the tree under a few hundred nodes."
      ],
      "aiNotes": [
        "Choose it for exploring a two to four level hierarchy by share: spend, storage, headcount, time.",
        "Give every node a stable, unique id so switching datasets morphs the arcs.",
        "Keep top level branches to about eight; each takes the next series hue derived from the accent and its children step lighter; past four they fall back to ink.",
        "Control focus when a URL or another view should drive the zoom."
      ],
      "related": [
        "donut-chart",
        "sankey-flow",
        "tree-table",
        "tree-view"
      ],
      "usage": "import { Sunburst } from \"@/components/arc/sunburst/sunburst\";\n\nconst spend = {\n  id: \"all\", label: \"All teams\",\n  children: [\n    { id: \"platform\", label: \"Platform\", children: [\n      { id: \"platform/compute\", label: \"Compute\", children: [{ id: \"api\", label: \"api-cluster\", value: 18910 }, { id: \"batch\", label: \"batch-workers\", value: 8160 }] },\n      { id: \"platform/storage\", label: \"Storage\", value: 8670 },\n    ] },\n    { id: \"data\", label: \"Data\", children: [{ id: \"data/warehouse\", label: \"Warehouse\", value: 31020 }] },\n  ],\n};\n\nexport function CloudSpend() {\n  return <Sunburst data={spend} label=\"Cloud spend, September\" formatValue={value => `$${(value / 1000).toFixed(1)}K`} />;\n}",
      "exports": [
        "Sunburst"
      ]
    },
    {
      "name": "sankey-flow",
      "kind": "component",
      "title": "Sankey flow",
      "description": "Flows between stages as ribbons with drifting particles; hover any node or ribbon to pour its share through the whole journey.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/sankey-flow",
      "markdown": "https://uiarc.dev/components/sankey-flow/markdown",
      "registry": null,
      "keywords": [
        "sankey diagram",
        "sankey chart react",
        "flow diagram",
        "funnel flow",
        "user journey chart",
        "alluvial diagram",
        "particle flow chart",
        "conversion flow"
      ],
      "whenToUse": [
        "Showing how a total splits and recombines across three to five stages.",
        "Explaining where a funnel leaks, with the share at each step one hover away.",
        "Comparing two periods of the same flow by switching data."
      ],
      "whenNotToUse": [
        "Use bar-chart for a simple linear funnel with no branches.",
        "Use sunburst for a strict hierarchy where each part has one parent.",
        "Avoid more than about 12 nodes per column; small flows become unreadable slivers."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The plot is one focusable group; the node in focus is announced with its value, share of the total, and where it flows next.",
        "A visually hidden table lists every flow with its value and share of the source.",
        "Labels, particles, and ribbons are aria-hidden; the table and announcements carry the data.",
        "Highlighting uses the accent plus dimming of everything else, never color alone; values are printed on nodes."
      ],
      "motion": [
        "Nodes and ribbons grow in from their centres on first view, and morph from the layout on screen to the new one when data changes.",
        "Focus pours the traced share into each ribbon on a short spring and drains it back out when focus leaves.",
        "Particles move at one constant speed on a canvas and pause when the chart is off screen.",
        "Reduced motion jumps to the final layout and removes particles and the toggle."
      ],
      "responsive": [
        "Below 420px wide the diagram turns upright: stages run top to bottom and labels sit under each node at full width.",
        "On wide layouts the last column labels to its right in a gutter sized to its labels, so no two labels share a gap.",
        "Touch taps a node or ribbon to trace it and taps again to clear; the tooltip stays inside the plot."
      ],
      "performance": [
        "Layout is computed once per data and size change; morphs interpolate numbers and write path attributes directly.",
        "Particles draw on one canvas, capped at 34 per ribbon, and stop when the chart leaves the viewport.",
        "Tracing walks each column once, so it stays instant for a few hundred links."
      ],
      "aiNotes": [
        "Choose it for funnels that split and merge: acquisition to activation to plan, budget sources to spend, traffic between pages.",
        "Keep node ids stable across datasets so switching periods morphs instead of redrawing.",
        "Order nodes within a column the way people read them, such as good outcomes on top; the layout keeps that order.",
        "Tracing assumes each node mixes what reaches it, which fits aggregates. Say so if exact per person paths matter."
      ],
      "related": [
        "sunburst",
        "streamgraph",
        "bar-chart",
        "donut-chart"
      ],
      "usage": "import { SankeyFlow } from \"@/components/arc/sankey-flow/sankey-flow\";\n\nconst nodes = [\n  { id: \"organic\", label: \"Organic\" }, { id: \"paid\", label: \"Paid\" },\n  { id: \"activated\", label: \"Activated\" }, { id: \"inactive\", label: \"Not activated\" },\n  { id: \"free\", label: \"Free\" }, { id: \"pro\", label: \"Pro\" },\n];\nconst links = [\n  { source: \"organic\", target: \"activated\", value: 3380 }, { source: \"organic\", target: \"inactive\", value: 1820 },\n  { source: \"paid\", target: \"activated\", value: 1990 }, { source: \"paid\", target: \"inactive\", value: 1910 },\n  { source: \"activated\", target: \"free\", value: 3860 }, { source: \"activated\", target: \"pro\", value: 1510 },\n];\n\nexport function SignupJourney() {\n  return <SankeyFlow nodes={nodes} links={links} label=\"Signup journey\" columns={[\"Source\", \"Activation\", \"Plan\"]} unit=\"people\" totalLabel=\"signups\" />;\n}",
      "exports": [
        "SankeyFlow"
      ]
    },
    {
      "name": "waffle-chart",
      "kind": "component",
      "title": "Waffle chart",
      "description": "A ten by ten unit chart where every cell is one percent, and cells fly to their new group when the data changes.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/waffle-chart",
      "markdown": "https://uiarc.dev/components/waffle-chart/markdown",
      "registry": "https://uiarc.dev/r/waffle-chart.json",
      "keywords": [
        "waffle chart",
        "unit chart",
        "square pie chart",
        "percentage grid",
        "part to whole chart",
        "react waffle chart",
        "animated waffle",
        "isotype chart"
      ],
      "whenToUse": [
        "Showing shares of one whole where people should be able to count units.",
        "Comparing the same categories across a few datasets, such as countries or years.",
        "Replacing a pie or donut when small shares need to stay visible."
      ],
      "whenNotToUse": [
        "Use line-chart or streamgraph for how shares change over time.",
        "Use bar-chart when exact comparison of many categories matters more than the whole.",
        "Use sunburst for nested parts of a whole."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The grid is a focusable group with a roledescription and instructions; cells are decorative and aria-hidden.",
        "A polite live region reads the category, share, and value under the keyboard cursor.",
        "A visually hidden summary and a table list every category with its value, share, and cell count.",
        "Legend items are toggle buttons with aria-pressed; identity never relies on colour alone because every category is named in the legend."
      ],
      "motion": [
        "Cells are persistent: a data change keeps as many cells in place as possible and flies the rest to their new block, staggered by position so the grid refills like a wave.",
        "A travelling cell dips in scale mid-flight and changes colour with the same delay, so it reads as lifting out of one group and landing in another.",
        "Shares in the legend roll to their new values on a critically damped spring.",
        "The first time the chart is seen, cells pop in from the bottom-left corner.",
        "Reduced motion places cells and shares immediately and drops the dip, pop, and stagger."
      ],
      "responsive": [
        "The grid is square and fills up to 300px; the legend moves beside it from 420px of container width.",
        "Legend labels truncate with an ellipsis; the full name stays in the tooltip, the live region, and the table.",
        "Touch reads cells with a press and drag; the page still scrolls vertically."
      ],
      "performance": [
        "One element per cell, 100 by default, animated with transforms only.",
        "Hover is resolved from pointer position arithmetic, not per-cell listeners.",
        "Grid size is measured with one ResizeObserver."
      ],
      "aiNotes": [
        "Choose it for part-to-whole with four to eight categories where countable units help, such as energy mix, survey answers, or budget split.",
        "Keep categories in a meaningful order; the fill order is the reading order.",
        "Use accentKey for the category the story is about and leave the rest neutral.",
        "Switching datasets with the same keys is where it shines: cells travel instead of repainting."
      ],
      "related": [
        "donut-chart",
        "sunburst",
        "bar-chart",
        "usage-meter",
        "funnel-chart"
      ],
      "usage": "import { WaffleChart } from \"@/components/arc/waffle-chart/waffle-chart\";\n\nconst mix = [\n  { key: \"wind-solar\", label: \"Wind and solar\", value: 218 },\n  { key: \"hydro\", label: \"Hydro\", value: 20 },\n  { key: \"gas\", label: \"Gas\", value: 76 },\n  { key: \"coal\", label: \"Coal\", value: 132 },\n  { key: \"other\", label: \"Other\", value: 61 },\n];\n\nexport function PowerMix() {\n  return <WaffleChart data={mix} label=\"Electricity generation, 2023\" unit=\"TWh\" />;\n}",
      "exports": [
        "WaffleChart"
      ]
    },
    {
      "name": "funnel-chart",
      "kind": "component",
      "title": "Funnel chart",
      "description": "A tapered conversion funnel with drop-off at every step, a device split that morphs the band, and dots that stream through it.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/funnel-chart",
      "markdown": "https://uiarc.dev/components/funnel-chart/markdown",
      "registry": null,
      "keywords": [
        "funnel chart",
        "conversion funnel",
        "react funnel chart",
        "drop-off chart",
        "checkout funnel",
        "animated funnel",
        "segment comparison funnel",
        "growth funnel"
      ],
      "whenToUse": [
        "Conversion through a fixed sequence of steps.",
        "Comparing how two segments convert through the same steps.",
        "A hero chart on a growth or analytics page where the drop-off is the story."
      ],
      "whenNotToUse": [
        "Use sankey-flow when people can branch, skip, or loop between steps.",
        "Use bar-chart for unordered categories.",
        "Use line-chart for conversion over time."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The chart is a focusable group with a roledescription and instructions; the band, dots, and headings are aria-hidden.",
        "A polite live region reads the step, its count, the share kept from the previous step, and the share of the first.",
        "A visually hidden summary and a table list every step per segment with both rates.",
        "Drop-off uses an arrow glyph and a number, never colour alone."
      ],
      "motion": [
        "Switching views morphs the band from the shape on screen with a spring; the split gap opens and closes with the same spring.",
        "Counts and rates roll to their new values.",
        "A soft column and an accent clip glide to the step in focus; the tooltip follows on its own spring and stays inside the chart.",
        "The band grows out of its centre line the first time it is seen.",
        "Dots run on a canvas at the display rate, pause off screen and in hidden tabs, and are never drawn with reduced motion, where the morphs also land immediately."
      ],
      "responsive": [
        "Horizontal from 420px of container width, vertical below with step names down the right side.",
        "The tooltip flips and clamps so it never leaves the chart.",
        "Touch reads steps with a press and drag; the page still scrolls vertically."
      ],
      "performance": [
        "Band paths are rewritten from a single paint function; React does not re-render during morphs.",
        "Dots are drawn on one canvas and stop when the chart leaves the viewport.",
        "Around 40 dots are alive at once."
      ],
      "aiNotes": [
        "Choose it for ordered steps where each keeps part of the previous one: signup, checkout, onboarding, sales pipeline.",
        "Pass segments and drive view from a segmented control to compare devices, plans, or channels.",
        "Keep stage labels short; they wrap to two lines above the band.",
        "Turn flow off for dense dashboards where motion would distract."
      ],
      "related": [
        "sankey-flow",
        "bar-chart",
        "waffle-chart",
        "stepper"
      ],
      "usage": "import { FunnelChart } from \"@/components/arc/funnel-chart/funnel-chart\";\n\nconst segments = [{ key: \"mobile\", label: \"Mobile\" }, { key: \"desktop\", label: \"Desktop\" }];\nconst stages = [\n  { key: \"visit\", label: \"Visited store\", values: { mobile: 68420, desktop: 31580 } },\n  { key: \"cart\", label: \"Added to cart\", values: { mobile: 9140, desktop: 7280 } },\n  { key: \"purchase\", label: \"Purchased\", values: { mobile: 1540, desktop: 2050 } },\n];\n\nexport function Checkout() {\n  const [view, setView] = useState(\"total\");\n  return <FunnelChart stages={stages} segments={segments} view={view} label=\"Checkout funnel\" unit=\"sessions\" />;\n}",
      "exports": [
        "FunnelChart"
      ]
    },
    {
      "name": "radar-chart",
      "kind": "component",
      "title": "Radar chart",
      "description": "Two or three profiles on one web: shapes morph when you swap a profile, and a sweep across the axes compares values in place.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/radar-chart",
      "markdown": "https://uiarc.dev/components/radar-chart/markdown",
      "registry": null,
      "keywords": [
        "radar chart",
        "spider chart",
        "web chart",
        "react radar chart",
        "comparison chart",
        "animated radar chart",
        "profile comparison",
        "polar chart"
      ],
      "whenToUse": [
        "Comparing the profile of a few options across shared measures.",
        "Showing strengths and gaps at a glance, such as scores or ratings."
      ],
      "whenNotToUse": [
        "Use bar-chart when the precise difference on one measure matters.",
        "Use plan-matrix for feature comparisons with yes or no values.",
        "Avoid more than three profiles; the shapes stop being readable."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The web is a focusable group with a roledescription and instructions; shapes and labels are aria-hidden.",
        "A polite live region reads the axis and every profile's value there.",
        "A visually hidden summary and a table list every value by measure and profile.",
        "Profiles are named in the legend and the tooltip with a line key, so identity never relies on colour alone."
      ],
      "motion": [
        "Swapping a profile morphs its shape from the one on screen with a gently bouncing spring.",
        "The web unfolds from the centre the first time it is seen.",
        "Vertex markers and the tooltip glide between axes on springs; the tooltip sits past the axis label so the label and vertices stay visible.",
        "Reduced motion swaps shapes and moves markers immediately."
      ],
      "responsive": [
        "The web shrinks to the container and the label ring tightens below 360px; axis labels wrap.",
        "The tooltip clamps inside the chart at every width.",
        "Touch reads axes with a press and drag."
      ],
      "performance": [
        "Shapes are rewritten from one paint function during morphs; React renders once per change.",
        "One ResizeObserver measures the container."
      ],
      "aiNotes": [
        "Choose it for the overall shape of two or three profiles on the same five to eight measures: products, plans, locations, candidates.",
        "Put the subject of the story first so it takes the accent derived first series, and drive the second position from a picker to get the morph.",
        "Every axis must point the same way, higher is better, or the shape misleads.",
        "For exact comparison of values, pair it with a table or use bar-chart."
      ],
      "related": [
        "bar-chart",
        "slope-chart",
        "donut-chart"
      ],
      "usage": "import { RadarChart } from \"@/components/arc/radar-chart/radar-chart\";\n\nconst axes = [\n  { key: \"talent\", label: \"Talent pool\" },\n  { key: \"afford\", label: \"Affordability\" },\n  { key: \"transit\", label: \"Transit\" },\n  { key: \"life\", label: \"Quality of life\" },\n  { key: \"tax\", label: \"Tax climate\" },\n];\n\nexport function Offices({ rival }: { rival: RadarSeries }) {\n  return <RadarChart axes={axes} series={[lisbon, rival]} label=\"Office locations\" />;\n}",
      "exports": [
        "RadarChart"
      ]
    },
    {
      "name": "realtime-stream",
      "kind": "component",
      "title": "Realtime stream",
      "description": "A live line that scrolls at 60fps as readings arrive, holds still on hover to read, and pops a marker on every anomaly.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "live",
        "new"
      ],
      "docs": "https://uiarc.dev/components/realtime-stream",
      "markdown": "https://uiarc.dev/components/realtime-stream/markdown",
      "registry": null,
      "keywords": [
        "realtime chart",
        "live chart",
        "streaming chart",
        "react live line chart",
        "monitoring chart",
        "scrolling chart",
        "anomaly chart",
        "60fps chart"
      ],
      "whenToUse": [
        "Live monitoring panels and status pages.",
        "A hero on an infrastructure or observability page where motion shows the system is alive."
      ],
      "whenNotToUse": [
        "Use line-chart for static time series.",
        "Use brush-chart for long histories people zoom into.",
        "Use sparkline for a tiny trend inside a stat."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The current value is text with an accessible name; the rolling digits are aria-hidden.",
        "The status button is a toggle with aria-pressed and shows live or paused with a shape and a label, never colour alone.",
        "Anomalies are announced once in a polite live region as they arrive; point readings are announced as the keyboard moves.",
        "A visually hidden table lists the latest twenty readings with their notes."
      ],
      "motion": [
        "The line scrolls continuously at the display rate; the head interpolates between readings, so new points slide in rather than pop.",
        "The value axis eases toward the visible range, so gridlines slide instead of jumping.",
        "Anomaly markers and tags pop in with one overshoot as the head reaches them.",
        "Resuming glides from the held view back to live.",
        "Reduced motion stops the scroll: the chart redraws only when a reading arrives, without interpolation or pops."
      ],
      "responsive": [
        "The canvas fills its container at the device pixel ratio and redraws on resize.",
        "Time labels thin out as the plot narrows.",
        "Touch reads points with a press and drag, which also holds the view."
      ],
      "performance": [
        "One canvas, one requestAnimationFrame loop, stopped off screen and in hidden tabs.",
        "Only points inside the window are drawn, found by binary search.",
        "Theme tokens are resolved from hidden probes a few times a second, not every frame."
      ],
      "aiNotes": [
        "Choose it for one live measure where the last seconds or minutes matter: latency, throughput, queue depth, active users.",
        "Set interval to the feed's cadence and windowMs to a few dozen points; trim the array to a few windows.",
        "Flag anomalies on the data itself with a short tag; the chart does not detect them.",
        "For history or several series, use line-chart or brush-chart instead."
      ],
      "related": [
        "line-chart",
        "brush-chart",
        "sparkline",
        "animated-counter"
      ],
      "usage": "import { RealtimeStream, type StreamPoint } from \"@/components/arc/realtime-stream/realtime-stream\";\n\nexport function Latency({ socket }: { socket: WebSocket }) {\n  const [points, setPoints] = useState<StreamPoint[]>([]);\n  useEffect(() => {\n    const onMessage = (event: MessageEvent) => {\n      const { t, p95 } = JSON.parse(event.data);\n      setPoints(list => [...list.slice(-200), { t, value: p95, anomaly: p95 > 300 ? \"Spike\" : undefined }]);\n    };\n    socket.addEventListener(\"message\", onMessage);\n    return () => socket.removeEventListener(\"message\", onMessage);\n  }, [socket]);\n  return <RealtimeStream points={points} label=\"p95 latency\" unit=\"ms\" interval={500} />;\n}",
      "exports": [
        "RealtimeStream"
      ]
    },
    {
      "name": "slope-chart",
      "kind": "component",
      "title": "Slope chart",
      "description": "Before and after on two axes: lines draw in, rank moves sit beside each value, and switching datasets slides every line to its new slope.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "new"
      ],
      "docs": "https://uiarc.dev/components/slope-chart",
      "markdown": "https://uiarc.dev/components/slope-chart/markdown",
      "registry": "https://uiarc.dev/r/slope-chart.json",
      "keywords": [
        "slope chart",
        "slopegraph",
        "before after chart",
        "react slope chart",
        "rank change chart",
        "two period comparison",
        "bump chart",
        "animated slope chart"
      ],
      "whenToUse": [
        "Showing which items rose, which fell, and how the order changed between two periods.",
        "Replacing a grouped bar chart when the change is the story."
      ],
      "whenNotToUse": [
        "Use line-chart for three or more moments.",
        "Use bar-chart when only the latest values matter.",
        "Avoid more than about ten items; labels start to crowd."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The chart is a focusable group with a roledescription and instructions; lines and labels are aria-hidden.",
        "A polite live region reads both values, the change, and the rank move for the item in focus.",
        "A visually hidden summary and a table list every item in end order.",
        "Rank moves use an arrow glyph and a number, never colour alone."
      ],
      "motion": [
        "On first view each line draws from start to end with a small stagger, and the end dots and labels arrive as it lands.",
        "Switching datasets moves lines, dots, labels, and leader hairlines on the shared morph spring; rank numbers roll in the direction they moved.",
        "The item in focus comes forward while the others fade; the tooltip glides to the middle of its line.",
        "Reduced motion shows every line drawn and moves it immediately."
      ],
      "responsive": [
        "Label columns narrow below 440px and names truncate with an ellipsis; the full name stays in the tooltip and table.",
        "Overlapping labels spread apart with a hairline to their point instead of colliding.",
        "Touch reads the nearest line with a press and drag."
      ],
      "performance": [
        "SVG lines and HTML labels animated with springs; around ten items render in a few dozen nodes.",
        "One ResizeObserver measures the container."
      ],
      "aiNotes": [
        "Choose it for exactly two moments and three to ten items: before and after a launch, quarter over quarter, last year against this year.",
        "Use highlightKey for the item the story is about.",
        "Give the columns short headings; names sit on the left only, values on both sides.",
        "For more than two moments use line-chart."
      ],
      "related": [
        "line-chart",
        "bar-chart",
        "radar-chart",
        "streamgraph",
        "sortable-data-table"
      ],
      "usage": "import { SlopeChart } from \"@/components/arc/slope-chart/slope-chart\";\n\nconst channels = [\n  { key: \"email\", label: \"Email\", start: 3.1, end: 4.6 },\n  { key: \"search\", label: \"Organic search\", start: 3.4, end: 3.6 },\n  { key: \"social\", label: \"Paid social\", start: 1.9, end: 1.3 },\n];\n\nexport function Channels() {\n  return <SlopeChart data={channels} label=\"Conversion by channel\" startLabel=\"Q1\" endLabel=\"Q2\" highlightKey=\"email\" formatValue={value => `${value.toFixed(1)}%`} />;\n}",
      "exports": [
        "SlopeChart"
      ]
    },
    {
      "name": "countdown",
      "kind": "component",
      "title": "Countdown",
      "description": "A launch countdown with rolling digits that morphs into a live state at zero.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "feedback",
        "new"
      ],
      "docs": "https://uiarc.dev/components/countdown",
      "markdown": "https://uiarc.dev/components/countdown/markdown",
      "registry": "https://uiarc.dev/r/countdown.json",
      "keywords": [
        "react countdown",
        "countdown timer",
        "launch countdown",
        "event timer",
        "digit roll animation",
        "deadline timer",
        "time zone countdown"
      ],
      "whenToUse": [
        "A launch page or event hero counting to a keynote.",
        "A compact deadline pill in a table row or banner.",
        "Sale or registration windows that should flip to a Live now state on their own."
      ],
      "whenNotToUse": [
        "Use stopwatch for time counting up from a start.",
        "Use announcement-bar when the countdown sits inside a site-wide banner with a call to action."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is role=\"timer\" named by label; the visible digits are aria-hidden.",
        "A hidden text reads the remaining time in words, dropping seconds until under a minute so it does not chatter.",
        "When complete, a string completeLabel is read; other nodes read as Complete."
      ],
      "motion": [
        "Each digit is a clipped wheel that turns the short way, so 00 to 59 is one step down and the window feathers the edges.",
        "Digits roll in with a small stagger on first reveal, and the shell springs its width and height when a unit column appears or drops.",
        "At zero the digits blur and scale out into the complete label, and the live dot pulses.",
        "Reduced motion jumps the wheels, sizes the shell instantly, swaps with a fade, and stops the pulse."
      ],
      "responsive": [
        "The large variant is a container: digits size to clamp(1.75rem, 12.5cqi, text-5xl), so four groups fit at 320px.",
        "The compact variant is an inline pill with 32px minimum height that springs its width as units drop."
      ],
      "performance": [
        "A single timeout wakes just after each whole second; it pauses while the tab is hidden or the countdown is off screen.",
        "A separate long timeout still fires onComplete on time when the tab is hidden.",
        "Each digit renders its whole wheel (10 or fewer glyphs), so keep a page to a few countdowns."
      ],
      "aiNotes": [
        "Use for a fixed moment people wait for: a launch, a live event, a sale ending. For elapsed time use stopwatch.",
        "Give targets an offset or a timeZone so every visitor counts to the same instant; never rely on the visitor's zone.",
        "Pass clock from a server time offset when device clocks may be wrong, such as ticketed drops.",
        "Use variant=\"compact\" inside announcement bars, table cells, and badges."
      ],
      "related": [
        "animated-counter",
        "announcement-bar",
        "badge"
      ],
      "usage": "import { Countdown } from \"@/components/arc/countdown/countdown\";\n\nexport function LaunchCountdown() {\n  return (\n    <Countdown\n      target=\"2026-10-06T10:00:00\"\n      timeZone=\"America/Los_Angeles\"\n      label=\"Keynote starts in\"\n      onComplete={() => router.refresh()}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Compact hours and minutes",
          "code": "<Countdown variant=\"compact\" units={[\"hours\", \"minutes\"]} target={saleEndsAt} completeLabel=\"Ended\" />"
        }
      ],
      "exports": [
        "Countdown",
        "toEpoch"
      ]
    },
    {
      "name": "cookie-consent",
      "kind": "component",
      "title": "Cookie consent",
      "description": "A consent bar that morphs into preferences and folds into a reopen button.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "feedback",
        "new"
      ],
      "docs": "https://uiarc.dev/components/cookie-consent",
      "markdown": "https://uiarc.dev/components/cookie-consent/markdown",
      "registry": "https://uiarc.dev/r/cookie-consent.json",
      "keywords": [
        "react cookie consent",
        "cookie banner",
        "gdpr consent",
        "cookie preferences",
        "consent manager",
        "privacy banner",
        "tracking consent"
      ],
      "whenToUse": [
        "Asking for cookie or tracking consent on a marketing site or app.",
        "Sites that need per-category choices with a way to reopen them later."
      ],
      "whenNotToUse": [
        "Use announcement-bar for a site-wide promotion or notice that needs no choice.",
        "Use toast for a one-off message that dismisses itself.",
        "Use dialog when the page must block until the visitor decides."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The bar and panel are non-modal dialogs; the bar is described by its message and the panel is labelled by its heading.",
        "Opening the panel focuses its heading; saving focuses the reopen button, which has aria-haspopup=\"dialog\" and the reopen label.",
        "Each switch is labelled by its category and described by its hint; required categories show Always on text instead of a switch.",
        "Save is described by a hidden count such as 2 of 3 optional categories allowed.",
        "Reject all sits beside Accept all at the same weight, so refusing is as easy as accepting."
      ],
      "motion": [
        "One surface springs its position, width, height, and radius between the bar, the panel, and the 44px reopen button; faces rise from a short blur.",
        "Growing uses a slightly bouncy spring and shrinking a flat one, so the surface never overshoots when it folds away.",
        "The surface enters rising 24px and scaling from 0.98.",
        "Reduced motion jumps the surface to each shape and swaps faces with a plain fade."
      ],
      "responsive": [
        "The bar is at most 660px and the panel 420px wide, both clamped to the layer, which sits 16px from the viewport edges.",
        "Below a 560px bar width the message stacks above the actions; panel actions go on one row at a 340px container width.",
        "The layer ignores pointer input, so only the surface blocks clicks on the page behind it."
      ],
      "performance": [
        "Nothing renders until hydration and the stored choice are read, so a decided visitor never sees the bar flash.",
        "One ResizeObserver tracks the layer width and one tracks the current face; motion runs only when the shape changes."
      ],
      "aiNotes": [
        "Mount once near the root layout. Gate optional scripts on onChange or on readCookieConsent(storageKey, version) at startup.",
        "Bump version whenever categories or the policy change to ask everyone again.",
        "Wire a footer Cookie settings link to ref.openPreferences(). Set showReopenButton={false} if that link is the only way back.",
        "Use strategy=\"absolute\" inside a positioned preview frame, not for production."
      ],
      "related": [
        "switch",
        "button",
        "announcement-bar",
        "toast",
        "dialog"
      ],
      "usage": "import { useRef } from \"react\";\nimport { CookieConsent, type CookieConsentHandle } from \"@/components/arc/cookie-consent/cookie-consent\";\n\nexport function Consent() {\n  const consent = useRef<CookieConsentHandle>(null);\n  return (\n    <>\n      <CookieConsent\n        ref={consent}\n        storageKey=\"acme-consent\"\n        policy={{ label: \"Cookie policy\", href: \"/cookies\" }}\n        onChange={value => { if (value?.analytics) loadAnalytics(); }}\n      />\n      <button type=\"button\" onClick={() => consent.current?.openPreferences()}>Cookie settings</button>\n    </>\n  );\n}",
      "exports": [
        "CookieConsent",
        "readCookieConsent",
        "defaultCookieCategories"
      ]
    },
    {
      "name": "announcement-bar",
      "kind": "component",
      "title": "Announcement bar",
      "description": "A top banner that rotates messages, counts down, and collapses smoothly when dismissed.",
      "tier": "free",
      "category": "Feedback",
      "tags": [
        "feedback",
        "new"
      ],
      "docs": "https://uiarc.dev/components/announcement-bar",
      "markdown": "https://uiarc.dev/components/announcement-bar/markdown",
      "registry": "https://uiarc.dev/r/announcement-bar.json",
      "keywords": [
        "react announcement bar",
        "top banner",
        "promo banner",
        "site notice bar",
        "dismissible banner",
        "rotating announcements",
        "sale countdown banner"
      ],
      "whenToUse": [
        "Site-wide promotions, launches, or maintenance notices at the top of every page.",
        "Rotating two to four short announcements in one slim bar.",
        "A sale banner with a live countdown to its end."
      ],
      "whenNotToUse": [
        "Use alert for a message tied to one section or form.",
        "Use toast for feedback on something the visitor just did.",
        "Use cookie-consent for consent choices."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The bar is a labelled section; with several messages it is a carousel and each message a slide labelled n of total.",
        "The viewport is aria-live off while rotating and polite while paused, so auto-rotation is not read aloud.",
        "With controls, the pause ring is a toggle button with aria-pressed and previous and next buttons control the slides region. Without controls, rotation still pauses on hover and focus.",
        "Countdowns are role=\"timer\" with aria-live off and a spoken label in hours and minutes.",
        "Rotation pauses on hover, keyboard focus inside, or a hidden tab."
      ],
      "motion": [
        "Next messages rise from below and leave upward (previous runs the other way) while the viewport springs to the new height.",
        "With controls, the ring around the pause button fills linearly over the interval, stops where it is on pause, and finishes the rest on resume.",
        "Countdown digits drop in from above; dismissing collapses the bar's height so the page below eases up.",
        "Reduced motion turns rotation off, swaps messages and digits with fades, and removes the height collapse."
      ],
      "responsive": [
        "Wide bars mirror the controls with an empty column so the message stays centered on the page.",
        "The bar is an inline-size container; at 560px and below messages align to the start edge instead of centering.",
        "Messages wrap on small screens and the viewport springs to the wrapped height; icon buttons are 32px."
      ],
      "performance": [
        "One linear motion value drives the ring; it stops while paused, hidden, or dismissed.",
        "A countdown ticks with one timeout per second, waking just after each whole second."
      ],
      "aiNotes": [
        "Place it above the site header. With an id, dismissal persists; call clearAnnouncementDismissal(id) or change the id to show it again.",
        "Keep messages to one short sentence; the call to action is a link when href is set, a button otherwise.",
        "For a standalone countdown elsewhere on the page use countdown; this bar has its own lighter timer."
      ],
      "related": [
        "countdown",
        "cookie-consent",
        "alert",
        "toast",
        "badge"
      ],
      "usage": "import { AnnouncementBar } from \"@/components/arc/announcement-bar/announcement-bar\";\n\nexport function SiteBanner() {\n  return (\n    <AnnouncementBar\n      id=\"fall-sale-2026\"\n      tone=\"inverted\"\n      messages={[\n        { id: \"sale\", message: \"Fall sale: 30% off annual plans.\", countdown: { to: \"2026-10-01T00:00:00Z\", label: \"Ends in\" }, action: { label: \"See plans\", href: \"/pricing\" } },\n        { id: \"launch\", message: \"Workflows are now in beta.\", action: { label: \"Read more\", href: \"/blog/workflows\" } },\n      ]}\n    />\n  );\n}",
      "exports": [
        "AnnouncementBar",
        "clearAnnouncementDismissal"
      ]
    },
    {
      "name": "json-viewer",
      "kind": "component",
      "title": "JSON viewer",
      "description": "A collapsible JSON tree with search, paging for long arrays, and copy value or path.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/json-viewer",
      "markdown": "https://uiarc.dev/components/json-viewer/markdown",
      "registry": "https://uiarc.dev/r/json-viewer.json",
      "keywords": [
        "react json viewer",
        "json tree",
        "json inspector",
        "collapsible json",
        "json explorer",
        "copy json path",
        "search json",
        "api response viewer"
      ],
      "whenToUse": [
        "Inspecting a JSON payload in a developer dashboard, such as webhooks or API logs.",
        "Large nested objects where search and copy path save time.",
        "Arrays with hundreds of items, which load in pages of pageSize."
      ],
      "whenNotToUse": [
        "Use code-block to show JSON as formatted source for copying whole.",
        "Use tree-view for folders, navigation, or other non-JSON hierarchies.",
        "Use data-grid when the data is a flat list of records."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Follows the WAI-ARIA tree pattern: role=\"tree\" with treeitems carrying aria-level, aria-posinset, aria-setsize, aria-expanded, and aria-selected.",
        "One row is tabbable at a time; each row's label reads its key and value, or its type and item count.",
        "The match counter is aria-live polite, and a status region announces copies and failures.",
        "Row copy buttons are out of the tab order; use Cmd or Ctrl with C, or the path bar's copy button, from the keyboard."
      ],
      "motion": [
        "Each row unfolds its own height, so opening a branch, loading a page, or expanding all share one motion.",
        "The current-row highlight glides between rows with a shared layout id, and the tree scroll springs to where the row will sit once rows settle.",
        "Copy glyphs swap to a check or cross with a small scale and blur.",
        "Reduced motion, or more than 400 visible rows, makes row changes instant; reduced motion also jumps the scroll and highlight."
      ],
      "responsive": [
        "The tree scrolls inside maxHeight; long keys and values truncate with an ellipsis and strings over 40 characters show in full on hover.",
        "At 420px viewport width and below the indent drops from 16px to 12px, keys cap at 40% of the row, and the path type hides.",
        "Row copy actions show on hover with a fine pointer; on touch they show on the current row."
      ],
      "performance": [
        "Rows are a flat list rebuilt only when data, open branches, pages, or search change; nothing is virtualized, so paging keeps the DOM small.",
        "Above 400 visible rows the height animations switch off to keep expand all fast.",
        "Search walks the whole value on each keystroke; for multi-megabyte payloads control query and debounce it."
      ],
      "aiNotes": [
        "Use for API responses, webhook payloads, logs, and config inspection in developer tools.",
        "Search matches keys and primitive values case-insensitively, opens the branches that hold them, and stops at 2000 matches.",
        "Control expanded to persist open branches across reloads, or to open a path from elsewhere in the page.",
        "For formatted source code use code-block; for tabular data use data-grid."
      ],
      "related": [
        "tree-view",
        "code-block",
        "search-field",
        "copy-button"
      ],
      "usage": "import { JsonViewer } from \"@/components/arc/json-viewer/json-viewer\";\n\nexport function WebhookPayload({ payload }: { payload: unknown }) {\n  return (\n    <JsonViewer\n      data={payload}\n      rootName=\"event\"\n      defaultExpandDepth={2}\n      maxHeight={360}\n      onCopy={({ kind }) => toast(kind === \"path\" ? \"Path copied\" : \"Value copied\")}\n    />\n  );\n}",
      "exports": [
        "JsonViewer"
      ]
    },
    {
      "name": "scrub-input",
      "kind": "component",
      "title": "Scrub input",
      "description": "A Figma-style number field: drag the label to scrub, or type values, units, and arithmetic.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/scrub-input",
      "markdown": "https://uiarc.dev/components/scrub-input/markdown",
      "registry": "https://uiarc.dev/r/scrub-input.json",
      "keywords": [
        "react scrub input",
        "drag to change number",
        "scrubbable number field",
        "inspector input",
        "figma number input",
        "math expression input",
        "unit input"
      ],
      "whenToUse": [
        "Position, size, rotation, and opacity fields in an inspector panel.",
        "Dense settings where typing exact values and dragging both matter.",
        "Values with units such as px, %, or deg that people type mixed."
      ],
      "whenNotToUse": [
        "Use number-field for a normal form with stepper buttons and a visible label.",
        "Use slider when the range and position matter more than the exact number.",
        "Use money-input for currency amounts."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The input is role=\"spinbutton\" with aria-valuenow, finite aria-valuemin and aria-valuemax, and aria-valuetext that spells out percent and degrees.",
        "The label is a real label for the input; clicking it focuses and selects the value.",
        "Invalid typed text sets aria-invalid for 900ms and keeps the previous value.",
        "The unit cycle button is labelled Unit: <unit>. Change unit."
      ],
      "motion": [
        "Past min or max the field strains with rubber-band resistance, up to 12px, and springs back on release; the number leans with it.",
        "The track fill follows a drag one to one and glides to typed or stepped values.",
        "Invalid input and steps against a limit kick the field with a short shake; unit changes rise from a small blur.",
        "Reduced motion removes the strain, shake, and glide, and swaps the unit with a fade."
      ],
      "responsive": [
        "The field fills its column; set --scrub-input-width to size it.",
        "Touch scrubs through pointer capture on the label, with touch-action: pan-y so vertical page scroll still works; pointer lock is mouse only.",
        "With the pointer locked, a stand-in cursor portaled to the body wraps at the window edges so a long drag never runs out of screen."
      ],
      "performance": [
        "Scrubbing writes motion values directly and emits only when the rounded value changes.",
        "One ResizeObserver per field tracks its width for the strain scale."
      ],
      "aiNotes": [
        "Use in design tool inspectors, property panels, and editors where people tune values by feel.",
        "Save on onValueCommit, not onValueChange, which fires on every scrub step.",
        "Set min and max for bounded values to get the track fill; set wrap for angles.",
        "For a labelled form field with plus and minus buttons use number-field; for a range with a thumb use slider."
      ],
      "related": [
        "number-field",
        "slider",
        "input",
        "money-input",
        "color-picker"
      ],
      "usage": "import { useState } from \"react\";\nimport { ScrubInput } from \"@/components/arc/scrub-input/scrub-input\";\n\nexport function LayerInspector() {\n  const [width, setWidth] = useState(240);\n  const [opacity, setOpacity] = useState(100);\n  return (\n    <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8 }}>\n      <ScrubInput label=\"W\" value={width} onValueChange={setWidth} min={0} units={[\"px\", \"%\"]} resetValue={240} />\n      <ScrubInput label=\"Opacity\" value={opacity} onValueChange={setOpacity} min={0} max={100} unit=\"%\" />\n    </div>\n  );\n}",
      "examples": [
        {
          "title": "Rotation that wraps",
          "code": "<ScrubInput label=\"R\" min={0} max={360} wrap unit=\"°\" units={[\"°\"]} fineStep={0.1} onValueCommit={value => setRotation(value)} />"
        }
      ],
      "exports": [
        "ScrubInput",
        "evaluateScrubExpression"
      ]
    },
    {
      "name": "product-gallery",
      "kind": "component",
      "title": "Product gallery",
      "description": "A product gallery with a hover magnifier, gliding thumbnails, and color and size variants that crossfade photos.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "new"
      ],
      "docs": "https://uiarc.dev/components/product-gallery",
      "markdown": "https://uiarc.dev/components/product-gallery/markdown",
      "registry": null,
      "keywords": [
        "react product gallery",
        "product image zoom",
        "ecommerce gallery",
        "image magnifier",
        "variant picker",
        "color and size selector",
        "product detail page",
        "fullscreen image viewer"
      ],
      "whenToUse": [
        "Product detail pages with several photos per color and a size picker.",
        "Stores where shoppers need to zoom into fabric, texture, or small print.",
        "Variants that sell out per color and should be struck through, not hidden."
      ],
      "whenNotToUse": [
        "Use lightbox-gallery for a photo grid that opens into a viewer, with no variants.",
        "Use carousel for marketing slides or cards.",
        "Use photo-grid for a masonry of images without a main photo."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The main photo is a button labelled with the photo's alt text, its position, and Open fullscreen.",
        "Thumbnails are a tablist; swatches and sizes are radiogroups with roving tabindex, and sold-out sizes use aria-disabled.",
        "The viewer is a modal dialog that focuses Close on open, traps Tab, locks body scroll, and announces the photo count politely.",
        "Every image needs a meaningful alt; thumbnails and the lens are decorative copies."
      ],
      "motion": [
        "The photo track springs to the photo on show and follows a finger one to one, with rubber-band edges; one flick moves one photo.",
        "The lens rides a spring behind the pointer; rail, swatch, and size highlights glide to the active option.",
        "The viewer grows out of the photo's own rectangle with counter-scaled corners and returns to it on close; zoom scales on a spring and pans after the pointer.",
        "Changing color crossfades every photo from a blur. Reduced motion jumps the track, lens, and zoom, and replaces the viewer flight with a fade."
      ],
      "responsive": [
        "The root is a container: photos and details sit side by side above 680px, stack at 680px and below, and the rail moves under the photo at 520px and below.",
        "On touch screens (hover: none) the photo shows dots and a permanent expand icon; the lens is mouse only.",
        "The viewer frame sizes to fit the viewport at the photo's ratio, with 12px padding under 640px and 48px above; it refits on resize."
      ],
      "performance": [
        "Only the photo on show loads eagerly; the rest and every thumbnail are lazy with async decoding.",
        "Each highlight (rail, swatches, sizes) and the stage run one ResizeObserver; the lens reuses the current image at zoom size.",
        "The viewer is portaled to the body only while open."
      ],
      "aiNotes": [
        "Use on a product detail page. Put the name, price, and reviews in header and purchase buttons in footer.",
        "Give each color its own photos; if a size is sold out in the new color, the selection clears through onSizeChange.",
        "Photos are plain img tags; pass optimized src and srcSet URLs. Set magnifier={false} for photos without fine detail.",
        "For a general photo grid with a lightbox use lightbox-gallery; for a simple slide rotation use carousel."
      ],
      "related": [
        "lightbox-gallery",
        "carousel",
        "photo-grid",
        "segmented-control",
        "radio-group"
      ],
      "usage": "import { useState } from \"react\";\nimport { ProductGallery } from \"@/components/arc/product-gallery/product-gallery\";\nimport Button from \"@/components/arc/button/button\";\n\nexport function ProductPage() {\n  const [size, setSize] = useState<string | null>(null);\n  return (\n    <ProductGallery\n      label=\"Field jacket\"\n      colors={[\n        { value: \"olive\", label: \"Olive\", swatch: \"#6b6a4a\", images: [{ src: \"/jacket/olive-1.jpg\", alt: \"Olive jacket, front\" }, { src: \"/jacket/olive-2.jpg\", alt: \"Olive jacket, back\" }] },\n        { value: \"navy\", label: \"Navy\", swatch: \"#27304a\", images: [{ src: \"/jacket/navy-1.jpg\", alt: \"Navy jacket, front\" }] },\n      ]}\n      sizes={[\n        { value: \"s\", label: \"S\" },\n        { value: \"m\", label: \"M\", soldOut: [\"navy\"] },\n        { value: \"l\", label: \"L\" },\n      ]}\n      size={size}\n      onSizeChange={setSize}\n      header={<><h1>Field jacket</h1><p>$240</p></>}\n      footer={<Button disabled={!size}>Add to bag</Button>}\n    />\n  );\n}",
      "exports": [
        "ProductGallery",
        "isSoldOut"
      ]
    },
    {
      "name": "tree-table",
      "kind": "component",
      "title": "Tree table",
      "description": "A table with nested rows, tri-state selection, hierarchy-preserving sort, and lazy-loaded children.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/tree-table",
      "markdown": "https://uiarc.dev/components/tree-table/markdown",
      "registry": "https://uiarc.dev/r/tree-table.json",
      "keywords": [
        "react tree table",
        "treegrid",
        "nested table rows",
        "expandable table",
        "hierarchical data table",
        "tri-state checkbox tree",
        "lazy load tree rows",
        "file tree table"
      ],
      "whenToUse": [
        "File or folder listings with size, owner, and modified columns.",
        "Budgets, org charts, or accounts where totals roll up through nested rows.",
        "Large trees whose deeper levels should load from the server on demand."
      ],
      "whenNotToUse": [
        "Use tree-view for navigation trees without columns.",
        "Use data-grid or sortable-data-table for flat records.",
        "Use accordion when each item opens into free form content rather than child rows."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-checkbox"
      ],
      "accessibility": [
        "Follows the treegrid pattern: rows carry aria-level, aria-posinset, aria-setsize, and aria-expanded, with a single roving tabindex.",
        "Sortable headers expose aria-sort; checkboxes are named from rowLabel and show the indeterminate state for partly selected parents.",
        "Loading, load errors, sorting, and expand all are announced through a polite live region.",
        "If the focused row disappears (its parent collapsed), focus moves to the nearest visible ancestor."
      ],
      "motion": [
        "Rows open and close on a height spring with a short stagger per sibling; sorting moves rows with a position layout animation.",
        "The chevron rotates on a snappy spring and the indent guide of the hovered or focused branch lights up.",
        "Reduced motion removes the height spring, stagger, and rotation; rows appear with an instant fade."
      ],
      "responsive": [
        "A ResizeObserver on the root hides any column whose hideBelow is wider than the table.",
        "Column widths are grid tracks, so minmax tracks shrink and the tree column truncates instead of overflowing.",
        "Hover styles apply only on hover-capable fine pointers; touch taps toggle and activate rows directly."
      ],
      "performance": [
        "Every visible row renders and animates; it is not virtualized, so keep expanded trees to a few hundred rows.",
        "Selection normalization walks the whole known tree on each change; very large preloaded trees should load lazily."
      ],
      "aiNotes": [
        "Use when hierarchical rows also need columns: file sizes, budgets by team, nested tasks. For a plain nested list with no columns use tree-view.",
        "Give every row a stable id. For lazy data, set hasChildren and leave children undefined, then pass loadChildren.",
        "Sort by giving columns a sortValue; siblings sort within their parent so the tree never breaks.",
        "Use hideBelow on secondary columns so narrow containers keep the tree column readable."
      ],
      "related": [
        "tree-view",
        "data-grid",
        "sortable-data-table",
        "checkbox"
      ],
      "usage": "import { TreeTable } from \"@/components/arc/tree-table/tree-table\";\nimport type { TreeTableColumn, TreeTableRow } from \"@/components/arc/tree-table/tree-table\";\n\ntype Item = { name: string; size: number; owner: string };\n\nconst columns: TreeTableColumn<Item>[] = [\n  { id: \"name\", header: \"Name\", cell: item => item.name, sortValue: item => item.name, width: \"minmax(0, 2fr)\" },\n  { id: \"size\", header: \"Size\", cell: item => `${item.size} MB`, sortValue: item => item.size, align: \"end\", width: \"96px\" },\n  { id: \"owner\", header: \"Owner\", cell: item => item.owner, hideBelow: 520 },\n];\n\nexport function Files({ rows }: { rows: TreeTableRow<Item>[] }) {\n  return (\n    <TreeTable\n      label=\"Files\"\n      rows={rows}\n      columns={columns}\n      selectable\n      rowLabel={item => item.name}\n      defaultExpanded={[\"design\"]}\n      loadChildren={row => fetchChildren(row.id)}\n      maxHeight={480}\n    />\n  );\n}",
      "exports": [
        "TreeTable",
        "collectExpandableIds"
      ]
    },
    {
      "name": "sheet-stack",
      "kind": "component",
      "title": "Sheet stack",
      "description": "Nested sheets that stack with depth, drag to dismiss, and become stacked dialogs on wide screens.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "new"
      ],
      "docs": "https://uiarc.dev/components/sheet-stack",
      "markdown": "https://uiarc.dev/components/sheet-stack/markdown",
      "registry": null,
      "keywords": [
        "react sheet stack",
        "nested bottom sheets",
        "stacked modals",
        "drill down sheet",
        "ios sheet stack",
        "nested dialogs",
        "draggable sheet",
        "sheet navigation"
      ],
      "whenToUse": [
        "Mobile settings where each row opens a deeper page and back should feel spatial.",
        "Nested pickers, such as choosing a workspace and then a project inside it.",
        "Flows that should be a bottom sheet on phones and a centered dialog on desktop from one declaration."
      ],
      "whenNotToUse": [
        "Use dialog for a single confirmation or form with no nested steps.",
        "Use bottom-sheet for one sheet with snap points.",
        "Use multi-step-form when steps are sequential rather than drill-down."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each panel is a dialog labelled by its title and described by description; only the top one is aria-modal, lower ones are inert.",
        "Opening moves focus into the new sheet; popping returns focus to the element that opened it.",
        "The back button names the sheet it returns to; the close button reads \"Close all\" when more than one sheet is open.",
        "Page scroll is locked while any sheet is open, unless contained."
      ],
      "motion": [
        "A new sheet rises from the bottom edge (or eases in as a dialog) while the one below scales back 5%, dims, and peeks above it.",
        "The top sheet follows a vertical drag with rubber banding upward; the sheets below ease back up as it goes and a fling past 35% or 450px/s pops it.",
        "Height changes spring as content changes, and all springs keep velocity when interrupted.",
        "Reduced motion removes the depth scale, lift, and drag physics; sheets fade in and out."
      ],
      "responsive": [
        "mode=\"auto\" measures the layer with a ResizeObserver: under breakpoint (640px) sheets rise from the bottom; at or above it they are centered dialogs.",
        "Each level loses room for the peek (10px per level for sheets, 14px for dialogs), and bodies scroll inside past that height.",
        "Drag to dismiss works with any pointer; a drag starting in a scrolled body scrolls instead of dragging."
      ],
      "performance": [
        "Transforms and dims come from motion values, so drags and depth changes do not rerender React per frame.",
        "Each open sheet has a ResizeObserver on its content; the stack is meant for a handful of levels, not dozens."
      ],
      "aiNotes": [
        "Use for short drill-down flows (settings, filters, account steps) where each level should keep its parent in view.",
        "Declare every Sheet once inside the SheetStack; open with SheetTrigger or useSheetStack().push. Pushing an open id pops back to it.",
        "Add data-sheet-no-drag to content that handles its own vertical drags, such as sliders or maps.",
        "For a single sheet with no nesting use bottom-sheet or drawer."
      ],
      "related": [
        "bottom-sheet",
        "drawer",
        "dialog",
        "settings-page",
        "multi-step-form"
      ],
      "usage": "import { Sheet, SheetStack, SheetTrigger, useSheetStack } from \"@/components/arc/sheet-stack/sheet-stack\";\n\nfunction SaveButton() {\n  const { close } = useSheetStack();\n  return <button type=\"button\" onClick={close}>Save</button>;\n}\n\nexport function Settings() {\n  return (\n    <SheetStack>\n      <SheetTrigger sheet=\"settings\">Settings</SheetTrigger>\n\n      <Sheet id=\"settings\" title=\"Settings\">\n        <SheetTrigger sheet=\"notifications\">Notifications</SheetTrigger>\n      </Sheet>\n\n      <Sheet id=\"notifications\" title=\"Notifications\" footer={<SaveButton />}>\n        <label><input type=\"checkbox\" defaultChecked /> Email me about mentions</label>\n      </Sheet>\n    </SheetStack>\n  );\n}",
      "exports": [
        "SheetStack",
        "Sheet",
        "SheetTrigger",
        "useSheetStack"
      ]
    },
    {
      "name": "phone-input",
      "kind": "component",
      "title": "Phone input",
      "description": "A phone field with a country picker, formatting as you type, and E.164 output.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/phone-input",
      "markdown": "https://uiarc.dev/components/phone-input/markdown",
      "registry": "https://uiarc.dev/r/phone-input.json",
      "keywords": [
        "react phone input",
        "phone number field",
        "country code picker",
        "international phone input",
        "e164 phone",
        "phone number formatting",
        "tel input with flag"
      ],
      "whenToUse": [
        "Sign up, checkout, and contact forms that need a phone number in E.164.",
        "International audiences where the country and calling code must be clear.",
        "Two factor setup before sending an SMS code."
      ],
      "whenNotToUse": [
        "Use input with type=\"tel\" when you only store free text and never dial or text the number.",
        "Use otp-input for the verification code itself.",
        "Use a full metadata library if you need carrier or number type validation beyond length."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The search is a combobox with aria-activedescendant over a listbox of options grouped into Suggested and All countries.",
        "The country button reads \"Country, <name> +<code>\"; picks, pasted country changes, and result counts are announced politely.",
        "The number input is type=\"tel\" with autocomplete=\"tel\"; errors are linked with aria-describedby and shown with role=\"alert\" after blur.",
        "A valid number is announced as \"Valid <country> number\" while a check appears."
      ],
      "motion": [
        "One surface springs its width, height, and corner radius from the country button into the list, and back when closing.",
        "The flag and code roll in the direction of the list when the country changes; the highlight glides between rows.",
        "Messages open on a height spring with a small rise and blur; the valid check pops in.",
        "Reduced motion jumps the surface to size and replaces rolls, blurs, and glides with short fades."
      ],
      "responsive": [
        "The open list is as wide as the field up to 340px, measured with a ResizeObserver, so it fits phones without covering the page.",
        "The number input uses inputMode=\"tel\" for the phone keypad; hover styles apply only on fine pointers."
      ],
      "performance": [
        "The country table is about 50 entries inline, with no network request; search filters it on each keystroke.",
        "The morph animates width and height on one element; keep one picker open at a time."
      ],
      "aiNotes": [
        "Store the E.164 value; format it for display with formatPhoneNumber. details.valid tells you when the length matches the country.",
        "Validation checks length per country only; confirm ownership with an OTP step (otp-input) when it matters.",
        "Pasted or autofilled international numbers choose their own country, and +1 splits into Canada by area code.",
        "Restrict countries with countries and pin the likely ones with preferredCountries; the table ships inline with no metadata download."
      ],
      "related": [
        "input",
        "otp-input",
        "combobox",
        "select",
        "signup-form"
      ],
      "usage": "import { useState } from \"react\";\nimport { PhoneInput } from \"@/components/arc/phone-input/phone-input\";\n\nexport function ContactPhone() {\n  const [phone, setPhone] = useState(\"\");\n  const [valid, setValid] = useState(false);\n  return (\n    <PhoneInput\n      label=\"Phone number\"\n      name=\"phone\"\n      value={phone}\n      onValueChange={(value, details) => { setPhone(value); setValid(details.valid); }}\n      defaultCountry=\"GB\"\n      preferredCountries={[\"GB\", \"IE\", \"US\"]}\n      description={valid ? undefined : \"We only text about your order\"}\n    />\n  );\n}",
      "exports": [
        "PhoneInput",
        "parsePhoneNumber / formatPhoneNumber / formatNational",
        "PHONE_COUNTRIES / flagOf"
      ]
    },
    {
      "name": "money-input",
      "kind": "component",
      "title": "Money input",
      "description": "A currency field with live grouping, stable width, rolling digits, and minor-unit output.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/money-input",
      "markdown": "https://uiarc.dev/components/money-input/markdown",
      "registry": "https://uiarc.dev/r/money-input.json",
      "keywords": [
        "react money input",
        "currency input",
        "amount field",
        "price input",
        "cents input",
        "currency selector",
        "formatted number input",
        "payment amount"
      ],
      "whenToUse": [
        "Payment, tip, transfer, or budget amounts entered by hand.",
        "Invoice and expense forms where the currency may change.",
        "Amount fields that benefit from quick add chips such as +$10."
      ],
      "whenNotToUse": [
        "Use number-field for quantities and non currency numbers.",
        "Use slider or price-ladder when people pick from a range rather than type.",
        "Use scrub-input for design tool style drag adjustments."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The input has role=\"spinbutton\" with aria-valuenow in major units, aria-valuetext as the full currency string, and min and max when set.",
        "The animated digits are aria-hidden; the real input carries the value, so screen readers and autofill see plain text.",
        "The range, hint, and error are linked with aria-describedby; limits, chip adds, and currency changes are announced politely.",
        "The currency menu is a listbox with aria-activedescendant and typeahead."
      ],
      "motion": [
        "Chips and arrow keys roll each digit column in the direction of change while new columns open their width; typing cuts straight to the result so the caret never lags.",
        "A refused change kicks the amount sideways on a bouncy spring and flashes the range.",
        "The currency symbol swaps with a short roll while its slot springs to the new width; long amounts scale down to fit on a spring.",
        "Reduced motion removes the rolls, kick, and width springs; digits fade and sizes jump."
      ],
      "responsive": [
        "Under 420px viewport width the amount text drops a size and the padding tightens.",
        "A ResizeObserver scales the amount down to fit its box instead of scrolling, so long numbers stay readable on phones.",
        "inputMode is decimal (numeric for zero decimal currencies like JPY), which brings up the number keypad on touch."
      ],
      "performance": [
        "Intl.NumberFormat instances are cached per locale and currency.",
        "Each digit is its own animated column; amounts are capped at 12 integer digits, which keeps the column count small."
      ],
      "aiNotes": [
        "Store the value in minor units (integers) to avoid floating point errors; details.major and details.formatted are for display.",
        "Use for money only. For plain numbers with units use number-field, and for drag-to-adjust values use scrub-input.",
        "Pass one code in currencies to lock the currency; the code then shows as static text.",
        "max refuses typing past it; min is only checked on blur, so people can type through smaller amounts on the way."
      ],
      "related": [
        "number-field",
        "scrub-input",
        "input",
        "pricing-calculator",
        "invoice-studio"
      ],
      "usage": "import { useState } from \"react\";\nimport { MoneyInput } from \"@/components/arc/money-input/money-input\";\n\nexport function TipAmount() {\n  const [cents, setCents] = useState<number | null>(1500);\n  return (\n    <MoneyInput\n      label=\"Amount\"\n      name=\"amount\"\n      value={cents}\n      onValueChange={setCents}\n      currencies={[\"USD\"]}\n      min={100}\n      max={100_000}\n      quickAdd={[5, 10, 20]}\n    />\n  );\n}",
      "exports": [
        "MoneyInput"
      ]
    },
    {
      "name": "shortcut-recorder",
      "kind": "component",
      "title": "Shortcut recorder",
      "description": "Record key combinations into key caps, with conflict warnings, Kbd, and a searchable cheatsheet.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/shortcut-recorder",
      "markdown": "https://uiarc.dev/components/shortcut-recorder/markdown",
      "registry": "https://uiarc.dev/r/shortcut-recorder.json",
      "keywords": [
        "react shortcut recorder",
        "keyboard shortcut input",
        "hotkey recorder",
        "keybinding editor",
        "kbd component",
        "keyboard shortcuts cheatsheet",
        "record hotkey"
      ],
      "whenToUse": [
        "Settings pages where people remap keyboard shortcuts.",
        "A keyboard shortcut cheatsheet or help dialog, with ShortcutList.",
        "Inline key hints in menus, tooltips, and docs, with ShortcutKbd or ShortcutKeys."
      ],
      "whenNotToUse": [
        "Use ShortcutKbd alone when shortcuts are fixed and only need to be shown.",
        "Use command-palette to let people run actions by name rather than bind keys.",
        "Avoid it on touch-only products where no hardware keyboard is expected."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The recorder is a button with aria-pressed while recording, named by the label plus the spoken shortcut, such as \"Command Shift K\".",
        "Recording, results, conflicts, and clears are announced through a polite live region; the message row is linked with aria-describedby.",
        "Key caps are aria-hidden inside the recorder; ShortcutKeys uses role=\"img\" with a spoken aria-label.",
        "Conflicts offer Use anyway and Cancel as real buttons, and focus returns to the recorder after either."
      ],
      "motion": [
        "Caps pop in with a small scale and blur, staggered when a finished shortcut settles, and slide on a snappy layout spring as the chord changes.",
        "A recording dot pulses; the message row opens on a height spring.",
        "In ShortcutList, groups and rows collapse on a height spring as search filters them.",
        "Reduced motion removes the scale, stagger, layout slide, and height springs; caps and messages fade."
      ],
      "responsive": [
        "ShortcutList is a container with a query at 520px that puts groups in two columns; below that they stack.",
        "The recorder keeps a fixed height and caps wrap within it; hover styles apply only on fine pointers."
      ],
      "performance": [
        "usePressedKeys adds window keydown and keyup listeners only while enabled; turn off highlightPressed for lists that do not need it.",
        "ShortcutList filters on every keystroke by building token strings per row; it suits lists of a few hundred shortcuts."
      ],
      "aiNotes": [
        "Store shortcuts as normalized strings with mod (\"mod+shift+k\"); they read as ⌘ on Mac and Ctrl elsewhere without a second binding.",
        "Run bound actions with matchesShortcut in a keydown listener; it compares physical keys, so ⌥K matches \"alt+k\" rather than \"˚\".",
        "Pass every existing binding in bindings, and use details.replaced from onValueChange to unbind the old owner after Use anyway.",
        "Use ShortcutList for a help sheet and ShortcutKbd for inline hints such as in menus or command-palette rows."
      ],
      "related": [
        "command-palette",
        "input",
        "tooltip",
        "dropdown-menu",
        "settings-page"
      ],
      "usage": "import { useEffect, useState } from \"react\";\nimport { ShortcutRecorder, matchesShortcut, usePlatform } from \"@/components/arc/shortcut-recorder/shortcut-recorder\";\n\nexport function SearchShortcut() {\n  const platform = usePlatform();\n  const [shortcut, setShortcut] = useState<string | null>(\"mod+k\");\n\n  useEffect(() => {\n    if (!shortcut) return;\n    const onKey = (event: KeyboardEvent) => {\n      if (matchesShortcut(event, shortcut, platform)) { event.preventDefault(); openSearch(); }\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [platform, shortcut]);\n\n  return (\n    <ShortcutRecorder\n      label=\"Open search\"\n      value={shortcut}\n      onValueChange={setShortcut}\n      resetValue=\"mod+k\"\n      bindings={[{ shortcut: \"mod+p\", label: \"Print\" }]}\n    />\n  );\n}",
      "exports": [
        "ShortcutRecorder",
        "ShortcutList",
        "ShortcutKeys",
        "ShortcutKbd",
        "matchesShortcut / formatShortcut / usePlatform / usePressedKeys"
      ]
    },
    {
      "name": "confirm-morph",
      "kind": "component",
      "title": "Confirm morph",
      "description": "A destructive button that morphs into an inline confirmation, a spinner, and a result with undo.",
      "tier": "free",
      "category": "Actions",
      "tags": [
        "actions",
        "new"
      ],
      "docs": "https://uiarc.dev/components/confirm-morph",
      "markdown": "https://uiarc.dev/components/confirm-morph/markdown",
      "registry": "https://uiarc.dev/r/confirm-morph.json",
      "keywords": [
        "react confirm button",
        "inline confirmation",
        "delete confirmation button",
        "confirm delete without modal",
        "undo button",
        "two step button",
        "destructive action button"
      ],
      "whenToUse": [
        "Deleting a selection in a table or file list, right where the button sits.",
        "Revoking access, removing a member, or discarding a draft.",
        "Actions that should offer Undo in place after they finish."
      ],
      "whenNotToUse": [
        "Use dialog when the consequence needs more than one line, or when typing a name to confirm is required.",
        "Use action-button for safe async actions that need no question.",
        "Use hold-to-confirm when an accidental tap must be nearly impossible."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The question face is a group labelled by the prompt. Confirm takes focus so Enter answers yes, but a press within 280ms of opening and auto-repeated keys are ignored, so a double click or a held Enter never confirms by accident.",
        "Focus follows the morph: Confirm while asking, the root during a countdown or while pending (with aria-busy), Undo or Retry on a result, and the trigger back at rest.",
        "In hold mode the confirm button is described as \"Press and hold\", and letting go early announces that it needs a longer hold.",
        "Prompts, pending labels, results, and \"Undo is available\" are announced through a polite live region.",
        "Timeouts pause while the pointer rests on the control and while the tab is hidden."
      ],
      "motion": [
        "Only the pill's width moves, on the morph spring growing and the smooth spring shrinking, so corners and text never scale or wobble and nothing around it jumps.",
        "Every face sits at its natural width against the pill's leading edge: the question starts where the label was and the springing edge reveals the rest, so text never squashes, slides, or leaves its baseline.",
        "The new face's parts arrive in reading order, each clearing a small blur; the old face fades out where it stood. The done check pops and draws itself, and countdown digits swap on tabular figures.",
        "In hold mode a solid danger copy of the button is revealed from the left as the hold fills and drains back on a spring if released early.",
        "Reduced motion removes the width spring, blur, and draw; faces crossfade and the spinner slows. The hold fill and countdown ring still show progress."
      ],
      "responsive": [
        "The control is as wide as its current face; leave room beside it for the question face, which is wider than the resting button.",
        "Hover styles apply only on hover-capable fine pointers; the control keeps the small control height, so give Cancel and Confirm some space from nearby targets on touch layouts."
      ],
      "performance": [
        "Only the current face and the one leaving render; a ResizeObserver per face measures width for the spring.",
        "The timeout is a single linear motion value animation that pauses and resumes, not a timer per frame."
      ],
      "aiNotes": [
        "Use for destructive or important actions that deserve a second press but not a modal: delete, revoke, discard.",
        "Return the real promise from onConfirm; rejections show the error face with Retry, which reruns the same handler.",
        "Offer onUndo when the action can be reversed; then consider a short confirmTimeout or skipping the question in your own flow.",
        "Use confirmMode=\"hold\" for the riskiest deletes and \"countdown\" when there is no Undo; use dialog when the consequence needs a full explanation.",
        "Keep the resting button neutral; tone=\"danger\" only colours the confirm answer, and tone=\"neutral\" moves it to the accent for reversible actions."
      ],
      "related": [
        "hold-to-confirm",
        "action-button",
        "action-morph",
        "dialog",
        "toast"
      ],
      "usage": "import { Trash2 } from \"lucide-react\";\nimport { ConfirmMorph } from \"@/components/arc/confirm-morph/confirm-morph\";\n\nexport function DeleteSelection({ ids }: { ids: string[] }) {\n  return (\n    <ConfirmMorph\n      label=\"Delete files\"\n      icon={<Trash2 size={16} strokeWidth={1.75} />}\n      prompt={`Delete ${ids.length} files?`}\n      confirmMode=\"hold\"\n      onConfirm={() => deleteFiles(ids)}\n      onUndo={() => restoreFiles(ids)}\n    />\n  );\n}",
      "exports": [
        "ConfirmMorph"
      ]
    },
    {
      "name": "mention-input",
      "kind": "component",
      "title": "Mention input",
      "description": "A textarea with @people and #channel mentions that act as single tokens, with suggestions at the caret.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/mention-input",
      "markdown": "https://uiarc.dev/components/mention-input/markdown",
      "registry": "https://uiarc.dev/r/mention-input.json",
      "keywords": [
        "react mention input",
        "at mention textarea",
        "mentions autocomplete",
        "tag people in comment",
        "channel mention",
        "slack style mentions",
        "mention suggestions",
        "textarea with mentions"
      ],
      "whenToUse": [
        "Comment and reply boxes where people are tagged with @ and should be notified by id.",
        "Chat or task composers that link #channels or projects inline.",
        "Any text field whose stored value needs structured references alongside the plain text."
      ],
      "whenNotToUse": [
        "Use textarea for plain notes with no references.",
        "Use tag-input when the tokens are a separate list rather than part of a sentence.",
        "Use rich-text-editor when the text also needs headings, lists, or links."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The textarea is a combobox with aria-autocomplete=\"list\", aria-expanded, aria-controls, and aria-activedescendant pointing at the highlighted option.",
        "Suggestions are a listbox labelled People or Channels, with options marked aria-selected.",
        "A polite live region announces the result count, such as 3 people or No matches.",
        "Mention tints come from an aria-hidden mirror behind a transparent textarea, so assistive tech reads the plain text.",
        "Choosing a suggestion types through execCommand insertText, so native undo keeps working."
      ],
      "motion": [
        "The field height springs to its content between minRows and maxRows.",
        "Suggestions pop in from the caret with a small scale and a snappy spring; the highlight glides between rows and the panel height springs as results change.",
        "Reduced motion jumps the height and highlight and fades the panel without scale or offset."
      ],
      "responsive": [
        "Fills its container width. The suggestion panel is 272px wide, capped to the field width, and clamped so it never runs past the right edge.",
        "With placement auto the panel flips above the caret near the bottom of the viewport, which suits composers pinned to the bottom of a mobile screen.",
        "Options use pointer events and click, so touch taps choose a suggestion without a hover state."
      ],
      "performance": [
        "Each edit rebuilds a mirror of the text to draw token tints and reconciles mention offsets with a prefix and suffix diff; fine for messages, not for pages of text.",
        "Two ResizeObservers drive the field and panel height springs; ranking runs over the full people and channels arrays each keystroke, so pass a pre-filtered list past a few thousand entries."
      ],
      "aiNotes": [
        "Choose it for comment boxes, message composers, and task descriptions that tag people or channels. Use textarea for plain multi-line text and tag-input for a list of chips outside the text.",
        "Store serializeMentions(value) or the mentions array, not only value.text, so renamed people still resolve by id.",
        "Filter people on the server by passing a fresh people array per keystroke; ranking is prefix, then word start, then substring, then role or description.",
        "Hook a toolbar @ button to ref.openSuggestions(\"person\")."
      ],
      "related": [
        "textarea",
        "tag-input",
        "combobox",
        "chat-thread",
        "rich-text-editor"
      ],
      "usage": "import { useState } from \"react\";\nimport { MentionInput, serializeMentions, type MentionValue } from \"@/components/arc/mention-input/mention-input\";\n\nconst people = [\n  { id: \"u1\", name: \"Maya Chen\", role: \"Design lead\" },\n  { id: \"u2\", name: \"Theo Park\", role: \"Engineer\" },\n];\nconst channels = [{ id: \"c1\", name: \"launch\", description: \"Release planning\", members: 14 }];\n\nexport function CommentBox({ onPost }: { onPost: (body: string) => void }) {\n  const [value, setValue] = useState<MentionValue>({ text: \"\", mentions: [] });\n  return (\n    <MentionInput\n      aria-label=\"Comment\"\n      placeholder=\"Write a comment, @ to mention\"\n      people={people}\n      channels={channels}\n      value={value}\n      onChange={setValue}\n      submitOnEnter\n      onSubmit={next => {\n        onPost(serializeMentions(next));\n        setValue({ text: \"\", mentions: [] });\n      }}\n    />\n  );\n}",
      "exports": [
        "MentionInput",
        "MentionInputHandle",
        "serializeMentions",
        "mentionText"
      ]
    },
    {
      "name": "chat-thread",
      "kind": "component",
      "title": "Chat thread",
      "description": "A chat thread with grouped messages, reactions, read receipts, typing, and a composer with attachments.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/chat-thread",
      "markdown": "https://uiarc.dev/components/chat-thread/markdown",
      "registry": "https://uiarc.dev/r/chat-thread.json",
      "keywords": [
        "react chat",
        "chat thread",
        "messaging ui",
        "support chat widget",
        "chat bubbles",
        "read receipts",
        "typing indicator",
        "message reactions",
        "chat composer with attachments"
      ],
      "whenToUse": [
        "Customer support or in-app messaging with read receipts and typing state.",
        "Team or DM threads that need reactions and image or file attachments.",
        "Comment threads on an object where replies arrive live."
      ],
      "whenNotToUse": [
        "Use log-stream for streaming system or build output.",
        "Use timeline for an activity history rather than a two-way conversation.",
        "Use textarea or mention-input when you only need the input box inside your own layout."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Messages are an ordered list with role log, aria-live polite, and aria-relevant additions, so new messages are read out.",
        "Typing is announced in a separate status region, such as Sam is typing; the visual dots are aria-hidden.",
        "Reaction chips are toggle buttons with aria-pressed and labels like 👍 3, including you.",
        "Read receipts carry an aria-label listing who read the message; avatars are decorative.",
        "The picker focuses its first emoji on open and uses a roving tabindex."
      ],
      "motion": [
        "New messages rise in from their own side with a small scale on a smooth spring, and the list glides up by the added height instead of jumping.",
        "Reader avatars share a layoutId, so a receipt travels to the newly read message.",
        "Reaction chips pop in, counts roll up or down, and the reaction row and attachment tray spring open.",
        "The send arrow lifts out and a new one rises in on each send; the jump to latest pill springs in from below.",
        "Reduced motion turns these into short fades, stops the typing dots, and scrolls to the bottom instantly."
      ],
      "responsive": [
        "Bubbles are capped at min(78%, 34rem); below 420px the content padding tightens to 12px and bubbles may reach 84%.",
        "On touch devices (hover none) the add reaction button stays visible at reduced opacity instead of waiting for hover.",
        "The reaction picker opens above the bubble unless that would pass the top of the scroller, then opens below."
      ],
      "performance": [
        "Every message renders; there is no virtualization, so page older history in chunks past a few hundred messages.",
        "A ResizeObserver on the content keeps the list pinned and drives the glide; images take width and height to avoid layout shift.",
        "Uncontrolled sends create object URLs for attached files and never revoke them; controlled threads should upload and pass real URLs."
      ],
      "aiNotes": [
        "Choose it for support widgets, DMs, and team chat. Use log-stream for machine output and timeline for dated events that are not a conversation.",
        "Give the parent a fixed height; the root is a flex column at height 100% with its own scroller.",
        "For a server-backed thread, pass messages and append an optimistic message with status sending in onSend, then update status or set failed and handle onRetry.",
        "Keep readBy in sync from your realtime source; only the furthest read message marks the viewer's latest message as Read.",
        "Use ChatComposer alone when messages render elsewhere, such as an AI chat."
      ],
      "related": [
        "mention-input",
        "reactions",
        "file-dropzone",
        "avatar",
        "textarea"
      ],
      "usage": "import { ChatThread, type ChatMessage } from \"@/components/arc/chat-thread/chat-thread\";\n\nconst participants = [\n  { id: \"me\", name: \"Alex Rivera\" },\n  { id: \"sam\", name: \"Sam Lee\", avatar: \"/avatars/sam.jpg\" },\n];\n\nconst messages: ChatMessage[] = [\n  { id: \"m1\", authorId: \"sam\", text: \"Is the build green?\", createdAt: \"2026-09-23T09:12:00Z\" },\n  { id: \"m2\", authorId: \"me\", text: \"Yes, shipping now.\", createdAt: \"2026-09-23T09:13:00Z\", status: \"delivered\" },\n];\n\nexport function SupportChat() {\n  return (\n    <div style={{ height: 520 }}>\n      <ChatThread\n        participants={participants}\n        currentUserId=\"me\"\n        defaultMessages={messages}\n        readBy={{ sam: \"m2\" }}\n        typing={[\"sam\"]}\n        onSend={draft => console.log(draft.text, draft.files)}\n      />\n    </div>\n  );\n}",
      "exports": [
        "ChatThread",
        "ChatThreadHandle",
        "ChatComposer",
        "ChatMessage",
        "formatBytes"
      ]
    },
    {
      "name": "rich-text-editor",
      "kind": "component",
      "title": "Rich text editor",
      "description": "A lightweight editor with markdown shortcuts, a floating toolbar, a slash menu, and HTML and markdown output.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/rich-text-editor",
      "markdown": "https://uiarc.dev/components/rich-text-editor/markdown",
      "registry": "https://uiarc.dev/r/rich-text-editor.json",
      "keywords": [
        "react rich text editor",
        "markdown editor",
        "wysiwyg editor",
        "slash command menu",
        "floating formatting toolbar",
        "contenteditable editor",
        "notion style editor",
        "lightweight text editor"
      ],
      "whenToUse": [
        "Comments, notes, and posts where people expect markdown shortcuts and a selection toolbar.",
        "Forms that need Markdown or clean HTML out without pulling in a full editor framework.",
        "Short to medium documents with headings, lists, quotes, and code blocks."
      ],
      "whenNotToUse": [
        "Use textarea for plain text, or mention-input when the only rich part is tagging people.",
        "Use inline-edit for a single line edited in place.",
        "Use code-block to display code, not to write prose."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The editing surface has role textbox with aria-multiline and aria-label, and aria-readonly when read-only.",
        "The slash menu is a listbox wired to the textbox with aria-controls and aria-activedescendant.",
        "The floating toolbar has role toolbar with toggle buttons using aria-pressed; titles show shortcuts.",
        "A polite live region announces Formatted, Link added, Undone, and similar results.",
        "Toolbar buttons prevent mousedown so the text selection survives clicks."
      ],
      "motion": [
        "The toolbar glides with the selection and its surface springs between the formatting face and the link face, which slide and blur across each other.",
        "The slash menu pops from the slash, its highlight glides between rows, and its height springs as results filter.",
        "Reduced motion jumps the toolbar position and size, crossfades the faces, and fades menus without scale or offset."
      ],
      "responsive": [
        "The root is an inline-size container; under 330px wide the toolbar hides its heading and quote buttons and keeps the inline marks.",
        "The toolbar and slash menu flip above or below based on the nearest scrolling or clipping ancestor, and the toolbar is clamped inside the editor width.",
        "Enter, Backspace, and undo go through beforeinput, so mobile keyboards and IME composition behave the same as hardware keys."
      ],
      "performance": [
        "Each selection change is batched to one requestAnimationFrame; each edit clones the DOM once to produce HTML, Markdown, and text for onChange.",
        "Undo keeps up to 300 HTML snapshots, merging typing within one second; very long documents make each snapshot and the per-edit clone more costly.",
        "No editor framework: only motion and lucide-react icons ship with it."
      ],
      "aiNotes": [
        "Choose it for comments, notes, posts, and descriptions that need headings, lists, quotes, code, and links. Use textarea for plain text and mention-input when the only structure is @ mentions.",
        "Store onChange markdown or html; both are clean. The value prop is HTML and only rewrites the DOM when it differs from the last emitted HTML, so feeding onChange html back is safe.",
        "Supported blocks are p, h1 to h3, ul, ol, blockquote, pre, and hr; inline marks are strong, em, s, code, and a. Images, tables, and underline are not supported.",
        "Paste runs through sanitizeHtml, and markdown-looking plain text is converted, so content from docs and other editors stays tidy.",
        "Build your own undo buttons with onHistoryChange and ref.undo and ref.redo."
      ],
      "related": [
        "textarea",
        "mention-input",
        "code-block",
        "inline-edit"
      ],
      "usage": "import { useState } from \"react\";\nimport { RichTextEditor, type RichTextValue } from \"@/components/arc/rich-text-editor/rich-text-editor\";\n\nexport function PostEditor() {\n  const [doc, setDoc] = useState<RichTextValue | null>(null);\n  return (\n    <>\n      <RichTextEditor\n        aria-label=\"Post body\"\n        placeholder=\"Write your update\"\n        defaultMarkdown={\"# Release notes\\n\\n- Faster search\\n- New **dark** theme\"}\n        onChange={setDoc}\n      />\n      <button type=\"button\" disabled={!doc || doc.empty} onClick={() => save(doc!.markdown)}>\n        Publish\n      </button>\n    </>\n  );\n}\n\ndeclare function save(markdown: string): void;",
      "examples": [
        {
          "title": "Controlled HTML with custom undo buttons",
          "code": "const editor = useRef<RichTextEditorHandle>(null);\nconst [html, setHtml] = useState(\"<p>Draft</p>\");\nconst [history, setHistory] = useState({ canUndo: false, canRedo: false });\n\n<>\n  <button type=\"button\" disabled={!history.canUndo} onClick={() => editor.current?.undo()}>Undo</button>\n  <button type=\"button\" disabled={!history.canRedo} onClick={() => editor.current?.redo()}>Redo</button>\n  <RichTextEditor ref={editor} value={html} onChange={value => setHtml(value.html)} onHistoryChange={setHistory} />\n</>"
        }
      ],
      "exports": [
        "RichTextEditor",
        "RichTextEditorHandle",
        "markdownToHtml",
        "htmlToMarkdown",
        "sanitizeHtml",
        "normalizeUrl"
      ]
    },
    {
      "name": "billing-toggle",
      "kind": "component",
      "title": "Billing toggle",
      "description": "A monthly and yearly switch with a savings badge and prices that roll to the new amount.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/billing-toggle",
      "markdown": "https://uiarc.dev/components/billing-toggle/markdown",
      "registry": "https://uiarc.dev/r/billing-toggle.json",
      "keywords": [
        "react billing toggle",
        "monthly yearly switch",
        "pricing period toggle",
        "annual billing discount",
        "save 20 percent badge",
        "animated price",
        "pricing toggle"
      ],
      "whenToUse": [
        "The billing period switch at the top of a pricing page.",
        "Any price display that should roll to a new amount when a period or plan changes, using BillingPrice.",
        "A small plan picker with a savings note, such as yearly versus lifetime."
      ],
      "whenNotToUse": [
        "Use segmented-control for switching views or filters with no pricing meaning.",
        "Use radio-group when options need descriptions or a vertical layout.",
        "Use switch for a single on or off setting."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The toggle is role=\"radiogroup\" labelled by label. Each option is a button with role=\"radio\" and aria-checked.",
        "Badges are part of the option text, so the saving is read with the option name.",
        "The sliding thumb is aria-hidden.",
        "BillingPrice uses del for the old price. The rolling digits come from AnimatedCounter."
      ],
      "motion": [
        "One thumb is measured from the selected option and glides to its x and width on a critically damped spring (0.34s, no bounce).",
        "When an option with a badge is selected, the badge takes a faint --billing-accent tint and its text crossfades with a short rise. Every candidate text reserves space, so the badge never changes width.",
        "BillingPrice rolls digits to the new amount, fades the old price in from the left, and swaps the period text in place.",
        "Reduced motion jumps the thumb and swaps badge text without travel."
      ],
      "responsive": [
        "The group is inline-flex and capped at 100% width. Options do not wrap, so keep labels and badges short on phones.",
        "Hover color only applies on devices with a fine pointer. Tap highlight is removed on touch."
      ],
      "performance": [
        "Only the thumb and badge text animate. One ResizeObserver re-measures the thumb when the group resizes.",
        "BillingPrice renders one AnimatedCounter; keep it to a few per page section."
      ],
      "aiNotes": [
        "Choose it for the monthly or yearly switch above pricing cards. Use segmented-control for general view switching that has nothing to do with price.",
        "It is controlled only. Keep the period in state and derive every price from it so all cards update together.",
        "Put the concrete saving in activeBadge, and a percentage in badge, so the confirmed number appears once chosen.",
        "Recolor the savings note with --billing-accent on any ancestor, for example --billing-accent: var(--accent) in a wrapper's CSS.",
        "Use BillingPrice for each card's amount so the digits roll together when the period flips.",
        "More than two options work, such as monthly, yearly, and lifetime."
      ],
      "related": [
        "segmented-control",
        "animated-counter",
        "radio-group"
      ],
      "usage": "import { useState } from \"react\";\nimport { BillingPrice, BillingToggle } from \"@/components/arc/billing-toggle/billing-toggle\";\n\nexport function ProPrice() {\n  const [period, setPeriod] = useState(\"monthly\");\n  const yearly = period === \"yearly\";\n  return (\n    <div>\n      <BillingToggle\n        value={period}\n        onValueChange={setPeriod}\n        options={[\n          { value: \"monthly\", label: \"Monthly\" },\n          { value: \"yearly\", label: \"Yearly\", badge: \"Save 20%\", activeBadge: \"You save $48\" },\n        ]}\n      />\n      <BillingPrice amount={yearly ? 16 : 20} was={yearly ? 20 : undefined} period={yearly ? \"per month, billed yearly\" : \"per month\"} />\n    </div>\n  );\n}",
      "exports": [
        "BillingToggle",
        "BillingToggleOption",
        "BillingPrice"
      ]
    },
    {
      "name": "scroll-area",
      "kind": "component",
      "title": "Scroll area",
      "description": "A native scroll container with thin overlay scrollbars and edge fades that appear only when content overflows.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "disclosure",
        "new"
      ],
      "docs": "https://uiarc.dev/components/scroll-area",
      "markdown": "https://uiarc.dev/components/scroll-area/markdown",
      "registry": "https://uiarc.dev/r/scroll-area.json",
      "keywords": [
        "react scroll area",
        "custom scrollbar",
        "overlay scrollbar",
        "scroll fade edges",
        "horizontal scroll container",
        "scroll snap container",
        "scrollable panel"
      ],
      "whenToUse": [
        "Panels, sidebars, and popovers whose content can outgrow their box.",
        "Horizontal strips of cards or chips that need mouse-wheel scrolling and snap points.",
        "Places where platform scrollbars look heavy but native scrolling must be kept."
      ],
      "whenNotToUse": [
        "Use carousel when items should page one at a time with controls.",
        "Do not wrap the whole page; let the document scroll natively.",
        "Use data-grid for large tabular data; it virtualizes its own scroll."
      ],
      "accessibility": [
        "Scrolling stays native, so keyboard, screen reader, and touch behavior match the platform.",
        "With label, the viewport is a named region; without one, give it context another way.",
        "The overlay tracks and thumbs are aria-hidden; the hidden native scrollbar remains the real control."
      ],
      "motion": [
        "Scrollbars fade in while scrolling or on hover and fade out after hideDelay; the thumb thickens under the pointer.",
        "Pressing the track pages 90% of the viewport toward the pointer with smooth scrolling, or instantly under reduced motion.",
        "Reduced motion removes scrollbar transitions."
      ],
      "responsive": [
        "Touch keeps native momentum scrolling; the overlay bars only appear while scrolling.",
        "Thumbs only thicken on hover-capable fine pointers.",
        "Content changes and resizes are tracked with ResizeObserver and MutationObserver, so fades and thumbs stay correct as layouts reflow."
      ],
      "performance": [
        "Fades and thumb positions are written straight to the DOM on each scroll event; scrolling never re-renders React.",
        "Horizontal wheel conversion only captures the wheel while there is room to scroll, so the page still scrolls at the ends.",
        "Edge fades use CSS mask-image, which is cheap but still composited; set fade={0} in very long lists if you see cost."
      ],
      "aiNotes": [
        "Use it wherever content scrolls inside a fixed box: sidebars, panels, menus, horizontal card strips.",
        "Give vertical areas a height or maxHeight; the viewport scrolls only when it has a constrained size.",
        "Use onEdgeChange to show a \"more below\" affordance or to load more when bottom becomes false."
      ],
      "related": [
        "resizable-panels",
        "carousel",
        "data-grid",
        "tree-view"
      ],
      "usage": "import { ScrollArea } from \"@/components/arc/scroll-area/scroll-area\";\n\nexport function ActivityPanel({ events }: { events: { id: string; text: string }[] }) {\n  return (\n    <ScrollArea maxHeight={320} label=\"Recent activity\">\n      <ul>{events.map(event => <li key={event.id}>{event.text}</li>)}</ul>\n    </ScrollArea>\n  );\n}",
      "examples": [
        {
          "title": "Horizontal strip with snap",
          "code": "<ScrollArea orientation=\"horizontal\" snap=\"x mandatory\" label=\"Templates\">\n  <div style={{ display: \"flex\", gap: 12 }}>\n    {templates.map(item => <TemplateCard key={item.id} {...item} style={{ scrollSnapAlign: \"start\" }} />)}\n  </div>\n</ScrollArea>"
        }
      ],
      "exports": [
        "ScrollArea"
      ]
    },
    {
      "name": "radio-cards",
      "kind": "component",
      "title": "Radio cards",
      "description": "Selectable option cards with a sliding selection ring, price and description slots, and radio keyboard behavior.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "inputs",
        "new"
      ],
      "docs": "https://uiarc.dev/components/radio-cards",
      "markdown": "https://uiarc.dev/components/radio-cards/markdown",
      "registry": "https://uiarc.dev/r/radio-cards.json",
      "keywords": [
        "react radio cards",
        "selectable cards",
        "radio card group",
        "plan selector",
        "shipping options",
        "option cards",
        "card radio buttons"
      ],
      "whenToUse": [
        "Choices where each option needs a description, price, or estimate to decide.",
        "Plan, shipping, region, or size pickers inside checkout and setup flows.",
        "Options that may be unavailable and need a visible reason."
      ],
      "whenNotToUse": [
        "Use radio-group for short text-only options.",
        "Use segmented-control for two to four view modes that switch content instantly.",
        "Use select or combobox for long lists."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "role=\"radiogroup\" with role=\"radio\" cards, aria-checked, and aria-disabled; give the group an aria-label or aria-labelledby.",
        "Each card is labelled by its label and described by its description, or by disabledReason when disabled.",
        "Selection is shown by a ring and a filled indicator dot, not color alone."
      ],
      "motion": [
        "One ring springs to the selected card's box and its size; resizes snap it without replaying the glide.",
        "The indicator dot scales in on the snappy spring.",
        "Reduced motion jumps the ring and dot to their final state."
      ],
      "responsive": [
        "The grid uses auto-fill columns with a minimum of minColumnWidth, so it drops to one column in narrow containers.",
        "List layout keeps the indicator, text, and meta on one row and truncates nothing; long descriptions wrap."
      ],
      "performance": [
        "One ResizeObserver on the group keeps the ring aligned; the ring moves with motion values without re-rendering cards."
      ],
      "aiNotes": [
        "Choose it for three to six options that each need a description or price: plans, shipping speeds, regions, instance sizes.",
        "Pass name to submit with a native form; the value is carried by a hidden input.",
        "Use disabledReason to explain why an option is unavailable instead of hiding it."
      ],
      "related": [
        "radio-group",
        "segmented-control",
        "billing-toggle"
      ],
      "usage": "import { RadioCards } from \"@/components/arc/radio-cards/radio-cards\";\n\nexport function ShippingSpeed() {\n  return (\n    <RadioCards\n      aria-label=\"Shipping speed\"\n      name=\"shipping\"\n      layout=\"list\"\n      defaultValue=\"standard\"\n      options={[\n        { value: \"standard\", label: \"Standard\", description: \"4 to 6 business days\", meta: \"Free\" },\n        { value: \"express\", label: \"Express\", description: \"2 business days\", meta: \"$12\" },\n        { value: \"overnight\", label: \"Overnight\", meta: \"$29\", disabled: true, disabledReason: \"Not available for this address\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "RadioCards"
      ]
    },
    {
      "name": "comment-thread",
      "kind": "component",
      "title": "Comment thread",
      "description": "Threaded comments with replies, reactions, mentions, inline edit, and resolve.",
      "tier": "free",
      "category": "Data",
      "tags": [
        "data",
        "new"
      ],
      "docs": "https://uiarc.dev/components/comment-thread",
      "markdown": "https://uiarc.dev/components/comment-thread/markdown",
      "registry": "https://uiarc.dev/r/comment-thread.json",
      "keywords": [
        "react comment thread",
        "threaded comments",
        "comment replies",
        "resolve thread",
        "mentions in comments",
        "comment reactions",
        "review comments component"
      ],
      "whenToUse": [
        "Review threads on documents, designs, tickets, or pull requests.",
        "Discussions that need replies, reactions, mentions, and a resolved state.",
        "Sidebars of feedback attached to one object."
      ],
      "whenNotToUse": [
        "Use chat-thread for real-time back-and-forth conversation.",
        "Use ai-chat for assistant conversations with streaming replies.",
        "Use mention-input alone when you only need a composer."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The composer is a combobox with a listbox of people and aria-activedescendant.",
        "Each comment is an article labelled with author and time; deleted ones read \"Deleted comment\".",
        "Reaction chips are toggle buttons with aria-pressed and a label like \"👍 3, including you\".",
        "Reply toggles expose aria-expanded; icon-only actions have specific labels."
      ],
      "motion": [
        "The thread height springs when replies expand, comments arrive, or the thread folds into the resolved chip.",
        "Reaction counts roll to the new value; chips move with layout animation as they are added or removed.",
        "Edit, confirm, and picker rows slide in from the side.",
        "Reduced motion swaps content without travel and turns off transitions."
      ],
      "responsive": [
        "Below 420px the composer hides its avatar and nested replies indent less (12px).",
        "Hover styles only apply on fine pointers; every action is a visible button on touch."
      ],
      "performance": [
        "Each change rebuilds the tree immutably and renders every comment; paginate or collapse very long threads.",
        "Reply nesting is capped by maxDepth, which keeps indentation and render depth bounded."
      ],
      "aiNotes": [
        "Choose it for comments attached to a document, design, or record. Persist changes with the event passed to onCommentsChange rather than diffing trees.",
        "currentUser decides which comments show Edit and Delete; enforce the same rule on your server.",
        "Deleting a comment with replies keeps a placeholder so the thread structure survives."
      ],
      "related": [
        "chat-thread",
        "mention-input",
        "reactions",
        "avatar",
        "inline-edit"
      ],
      "usage": "import { CommentThread, type ThreadComment } from \"@/components/arc/comment-thread/comment-thread\";\n\nexport function HeadlineDiscussion({ initial, me }: { initial: ThreadComment[]; me: { id: string; name: string; avatar?: string } }) {\n  return (\n    <CommentThread\n      title=\"Hero headline\"\n      currentUser={me}\n      defaultComments={initial}\n      onCommentsChange={(_, event) => syncComment(event)}\n      onResolvedChange={resolved => setThreadResolved(\"hero\", resolved)}\n    />\n  );\n}",
      "exports": [
        "CommentThread",
        "ThreadComment"
      ]
    },
    {
      "name": "dot-grid",
      "kind": "component",
      "title": "Dot grid",
      "description": "A dot field that bulges around the cursor like a lens and ripples from every click.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/dot-grid",
      "markdown": "https://uiarc.dev/components/dot-grid/markdown",
      "registry": null,
      "keywords": [
        "dot grid",
        "interactive dot background",
        "dot field",
        "magnetic dots",
        "ripple background",
        "hero background",
        "canvas dots",
        "lens effect"
      ],
      "whenToUse": [
        "A landing page hero that should feel alive without video.",
        "A background for an empty state, a sign in panel, or a launch countdown.",
        "A playful section divider people can poke at."
      ],
      "whenNotToUse": [
        "Behind dense text or data; the moving dots compete with reading.",
        "Use a static CSS dot pattern when no interaction is wanted.",
        "Avoid stacking several on one screen; one reactive background is enough."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The canvas is aria-hidden and not focusable; the field is decoration and adds nothing to the reading order.",
        "Children render as normal DOM above the canvas, so links and buttons keep their semantics and pointer events.",
        "With reduced motion dots never move; the lens only tints dots near the pointer and clicks do not ripple."
      ],
      "motion": [
        "Each dot rides its own damped spring toward the lens and ripple offsets, so the field wobbles and settles instead of snapping.",
        "The lens peaks a third of the way out and fades to nothing at its rim, and it trails the pointer slightly and fades in and out.",
        "Ripples travel at about 620px per second, fade over 1.5s, and up to six can overlap.",
        "Dots under the lens and in a ripple grow and take the active color."
      ],
      "responsive": [
        "The grid rebuilds on resize and centers itself in the box.",
        "On touch screens the lens appears only while a finger is down, and every tap ripples.",
        "Device pixel ratio is capped at 2."
      ],
      "performance": [
        "Dot state lives in typed arrays; resting dots draw in a single path fill.",
        "The loop stops when the pointer leaves and all dots settle, and pauses when the field is off screen.",
        "A 720 by 420 field at gap 22 is about 650 dots; gap 14 is about 1,600."
      ],
      "aiNotes": [
        "Choose it as a hero or section background that reacts to the visitor.",
        "Put the headline and actions inside as children; the field listens to the pointer across the whole root.",
        "Raise gap for large areas and low powered devices; 22 to 32 suits full width heroes.",
        "Pass color and activeColor to match a brand; any CSS color works, including var() values resolved on the root."
      ],
      "related": [
        "stretch-refresh"
      ],
      "usage": "import { DotGrid } from \"@/components/arc/dot-grid/dot-grid\";\n\nexport function Hero() {\n  return (\n    <DotGrid style={{ height: 480 }}>\n      <h1>Interfaces with a pulse</h1>\n    </DotGrid>\n  );\n}",
      "exports": [
        "DotGrid"
      ]
    },
    {
      "name": "holo-card",
      "kind": "component",
      "title": "Holo card",
      "description": "A member card that tilts toward you with a holographic foil and flips to its back.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/holo-card",
      "markdown": "https://uiarc.dev/components/holo-card/markdown",
      "registry": null,
      "keywords": [
        "holographic card",
        "holo card",
        "foil card",
        "pokemon card effect",
        "tilt card",
        "3d card flip",
        "membership card",
        "gyroscope tilt",
        "shiny card"
      ],
      "whenToUse": [
        "A membership or subscription card on an account or welcome page.",
        "A shareable profile, badge, or achievement that people will screenshot.",
        "A collectible or reward reveal."
      ],
      "whenNotToUse": [
        "Use card for ordinary content; the effect is too loud for lists.",
        "Use depth-cards when several items need a subtle parallax.",
        "Avoid it for text people need to read carefully; the sheen moves over the content."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "With a back, the card is role=\"button\" with aria-roledescription=\"flip card\" and aria-pressed for the flipped state.",
        "The hidden face is aria-hidden, so assistive tech reads only the side that is showing.",
        "Foil, glitter, and glare layers are decorative and aria-hidden.",
        "prefers-reduced-transparency removes the foil and glitter."
      ],
      "motion": [
        "Tilt follows the pointer on a lightly underdamped spring, and the light position trails on a softer one, so the sheen lags the lean just enough to feel physical.",
        "The foil slides and shifts hue with the angle; glitter drifts against the tilt and only sparkles near the glare.",
        "Each flip adds half a turn toward the side that was pressed on a spring with a little bounce, and the foil sweeps across the card mid spin.",
        "A floating shadow moves opposite the lean.",
        "Reduced motion removes tilt, spin, and glitter; the faces cross fade."
      ],
      "responsive": [
        "The root is width limited to 300px by default and keeps a 5:7 ratio, so it fits a 320px screen.",
        "On touch screens tilt comes from the device when available; otherwise dragging a finger over the card tilts it.",
        "Hover lift and tilt only react to real pointer movement; taps flip."
      ],
      "performance": [
        "All tilt and light values are motion values written to transform and CSS variables, with no React render per frame.",
        "Foil, glitter, and glare are three blended layers per face; keep one or two cards on screen, not a grid.",
        "Glitter is a small tiled SVG data URI."
      ],
      "aiNotes": [
        "Choose it for one hero object: a membership card, a conference badge, an achievement, or a gift card reveal.",
        "Design the front edge to edge; the card clips it to its radius and adds the foil on top.",
        "Dark or saturated card stock shows the foil best; on very light stock lower intensity.",
        "Leave out back for a tilt only card, for example inside a link.",
        "Gyroscope needs HTTPS and, on iOS, a tap to grant permission; the first tap that flips also asks."
      ],
      "related": [
        "sticker-peel",
        "wallet-stack",
        "cover-flow"
      ],
      "usage": "import { HoloCard } from \"@/components/arc/holo-card/holo-card\";\n\nexport function MemberCard() {\n  return (\n    <HoloCard\n      label=\"Jasmine Brooks, member card\"\n      front={<div className=\"front\">...</div>}\n      back={<div className=\"back\">...</div>}\n    />\n  );\n}",
      "exports": [
        "HoloCard"
      ]
    },
    {
      "name": "keycap-keyboard",
      "kind": "component",
      "title": "Keycap keyboard",
      "description": "Mechanical keycaps that sink and light up as you type, with the text above the board.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "keyboard",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/keycap-keyboard",
      "markdown": "https://uiarc.dev/components/keycap-keyboard/markdown",
      "registry": null,
      "keywords": [
        "mechanical keyboard",
        "keycap animation",
        "3d keyboard react",
        "keyboard component",
        "typing animation",
        "key press effect",
        "virtual keyboard"
      ],
      "whenToUse": [
        "Landing pages for keyboard-first products and developer tools.",
        "Onboarding that teaches shortcuts by lighting the keys they use.",
        "Playful empty states and easter eggs."
      ],
      "whenNotToUse": [
        "Form input; use a real text field.",
        "On-screen keyboards for accessibility needs; use the platform's."
      ],
      "dependencies": [
        "lucide-react"
      ],
      "accessibility": [
        "Every cap is a real button named by its character or function (Space, Delete, Return, Shift).",
        "The typed text is a read-only, multiline textbox named Typed text; focusing it lets physical keys type in focus mode.",
        "Shortcuts with Command, Control, or Alt pass through untouched, and window mode never captures keys typed into other fields."
      ],
      "motion": [
        "A cap drops 92% of its travel in 35ms on key down and returns in 200ms with a small lift past rest, so each press feels crisp.",
        "A faint light blooms on the board under the cap and fades over 700ms; fast typing leaves a soft trail.",
        "The caret holds still while you type and starts blinking again after 600ms idle. New characters fade in over 140ms.",
        "Reduced motion removes cap travel, character fades, and the blink; the light and legend color still respond."
      ],
      "responsive": [
        "One size unit comes from a container query so the widest row always fits, from 320px up to a 54px cap on wide screens.",
        "Travel, gaps, corner radii, and legends all scale from the same unit.",
        "Taps press caps on touch down; pointer capture keeps a cap down until the finger lifts."
      ],
      "performance": [
        "Caps are memoized with stable props and never re-render while typing; a press flips one data attribute on one button.",
        "One native keydown and keyup listener on the window or the board. No layout reads on the key path.",
        "Travel and light are CSS transitions on transform and opacity only. A burst of 15 keys a second holds 16.7ms frames.",
        "The text area stays pinned to its newest line through column-reverse layout, with no scroll code."
      ],
      "aiNotes": [
        "Use it as a playful hero or a try-it moment: keyboard-first products, typing tests, launch pages.",
        "listen=\"window\" makes the page feel alive immediately; use \"focus\" when other inputs share the page.",
        "Pass rows to build a macro pad or a single word, such as [[\"a\", \"r\", \"c\"]].",
        "It is not a replacement for a real text field in forms."
      ],
      "related": [
        "shortcut-recorder",
        "command-palette",
        "hold-to-confirm"
      ],
      "usage": "import { KeycapKeyboard } from \"@/components/arc/keycap-keyboard/keycap-keyboard\";\n\nexport function TypeToTry() {\n  return <KeycapKeyboard listen=\"window\" placeholder=\"Type anything\" />;\n}\n\n// Letters only\n<KeycapKeyboard layout=\"letters\" />\n\n// A four key macro pad\n<KeycapKeyboard rows={[[\"a\", \"r\", \"c\", \"backspace\"]]} showOutput={false} />",
      "exports": [
        "KeycapKeyboard"
      ]
    },
    {
      "name": "slot-text",
      "kind": "component",
      "title": "Slot text",
      "description": "Text and numbers that spin into their new value on staggered slot machine reels.",
      "tier": "free",
      "category": "Special",
      "tags": [
        "special",
        "text",
        "numbers",
        "new"
      ],
      "docs": "https://uiarc.dev/components/slot-text",
      "markdown": "https://uiarc.dev/components/slot-text/markdown",
      "registry": "https://uiarc.dev/r/slot-text.json",
      "keywords": [
        "slot machine text",
        "rolling number",
        "number ticker",
        "odometer react",
        "slot text animation",
        "price animation",
        "animated digits"
      ],
      "whenToUse": [
        "Pricing toggles, KPI cards, dashboards changing period.",
        "Launch moments, raffles, and reveals."
      ],
      "whenNotToUse": [
        "Values that update several times a second.",
        "Long sentences; reels are for short values."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The full value is real text in a visually hidden span; the reels are aria-hidden.",
        "announce adds a polite live region for values that change without user action."
      ],
      "motion": [
        "Each reel spins with a fast start and a long, soft landing that never passes the target.",
        "Reels stop one after another left to right; blur follows each reel's speed, so fast spins smear and landings are sharp.",
        "A reel's width follows its new character on a critically damped spring, and new characters grow in while removed ones collapse.",
        "Leading symbols such as a currency sign stay on their own reels, and separators stay aligned from the end, so 999 to 1,000 only spins digits.",
        "Changing the value mid-spin restarts from the cell on screen, keeping its offset, so nothing jumps.",
        "Reduced motion swaps values instantly."
      ],
      "responsive": [
        "It is inline text and scales with its font size; reels are measured in em."
      ],
      "performance": [
        "Each reel is one transform on a motion value; React re-renders only when the value changes.",
        "Blur is removed when a reel is still."
      ],
      "aiNotes": [
        "Use it where a change deserves a beat: prices switching plans, stats changing period, a launch status flipping.",
        "Put it inside text you style; it inherits font, size, weight, and color. Digits use tabular numerals.",
        "For values that tick every second, use a counter with spins={0} or animated-counter; long spins on every tick get tiring."
      ],
      "related": [
        "animated-counter",
        "countdown",
        "text-morph",
        "billing-toggle"
      ],
      "usage": "import { SlotText } from \"@/components/arc/slot-text/slot-text\";\n\n<SlotText value={revenue} format={value => `$${value.toLocaleString(\"en-US\")}`} announce />\n\n<SlotText value={live ? \"Live now\" : \"Soon\"} spins={2} />",
      "exports": [
        "SlotText"
      ]
    },
    {
      "name": "sticker-peel",
      "kind": "component",
      "title": "Sticker peel",
      "description": "Peel stickers off a surface with a real curl, move them around, and stick them back down.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/sticker-peel",
      "markdown": "https://uiarc.dev/components/sticker-peel/markdown",
      "registry": null,
      "keywords": [
        "sticker peel",
        "peel effect",
        "sticker drag",
        "page curl",
        "laptop stickers",
        "draggable stickers",
        "react sticker",
        "peel off animation"
      ],
      "whenToUse": [
        "A personal or team page where people arrange badges, achievements, or brand stickers.",
        "A launch or marketing moment that invites people to play with the product's marks.",
        "A reward screen where earned stickers are placed on a surface."
      ],
      "whenNotToUse": [
        "Use reorderable-list or a sortable grid when the order of items carries meaning.",
        "Use note-stack for content people need to read and rearrange.",
        "Avoid it on dense work screens; it is a delight surface, not an input."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The surface is a labelled group; each sticker is a button with aria-roledescription=\"sticker\", its label, and aria-pressed while carried.",
        "A visually hidden description explains the keyboard path, and a polite status region announces peeled off, picked up, stuck down, and put back.",
        "Sticker artwork is aria-hidden; the label carries the meaning.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The lift and shadow of a carried sticker show which one is active."
      ],
      "motion": [
        "The peel is a real fold: the pulled part is mirrored across the fold line with clip-path and foreshortened a touch, showing a plain paper backing with one soft shade at the bend and a light shadow that always falls downward.",
        "Circles peel from the point of the rim nearest the pointer; rounded shapes and pills peel from a corner along its diagonal. The fold may lean at most 35 degrees, so it is always a short, clean corner.",
        "Hovering with a mouse lifts the nearest corner a few pixels past the outline. Moving to another corner lays the first one flat before lifting the next.",
        "All springs are critically damped: the paper trails the finger slightly and settles without overshoot, bounce, or swing.",
        "It comes off once the fold passes the middle, sooner on a quick tug, or once the hand has clearly carried it away. It then rises 3% with a soft shadow and follows the hand at its own angle.",
        "Dropping lands it exactly where it is on screen and the shadow fades in 200ms. Grabbing it again while it settles catches it in place.",
        "Reduced motion skips the peel and hover lift; pressing picks the sticker up directly and it follows the pointer 1:1."
      ],
      "responsive": [
        "Placements are fractions of the surface, so the arrangement scales with its container and survives resizes.",
        "Touch drags use pointer capture with touch-action none on stickers only, so the page still scrolls from the surface.",
        "Carried stickers rubber-band at the surface edges and are clamped inside on drop, accounting for their angle."
      ],
      "performance": [
        "Peel geometry is written straight to clip-path and transform on three elements per sticker from motion value events, with no React render per frame.",
        "Body movement and lift are motion values on transform only; nothing runs per frame while stickers are at rest."
      ],
      "aiNotes": [
        "Choose it for one playful, tactile moment: a profile page, a launch page, a team wall, or a collectible badge shelf.",
        "Give each sticker content a fixed size; the die cut border and shape are added for you.",
        "Store onPositionsChange output to persist the arrangement, and pass it back as positions.",
        "Changing positions from outside, such as a reset, glides each sticker to its new spot.",
        "Keep it under about a dozen stickers so the surface stays readable."
      ],
      "related": [
        "holo-card"
      ],
      "usage": "import { useState } from \"react\";\nimport { StickerPeel } from \"@/components/arc/sticker-peel/sticker-peel\";\nimport type { StickerPlacement } from \"@/components/arc/sticker-peel/sticker-peel\";\n\nconst stickers = [\n  { id: \"ship\", label: \"Ship it sticker\", shape: \"pill\" as const, x: .3, y: .35, rotate: -6, content: <span className=\"ship\">Ship it</span> },\n  { id: \"hello\", label: \"Hello sticker\", shape: \"circle\" as const, x: .68, y: .6, rotate: 8, content: <span className=\"hello\">hello</span> },\n];\n\nexport function Lid() {\n  const [positions, setPositions] = useState<Record<string, StickerPlacement>>();\n  return <StickerPeel className=\"lid\" stickers={stickers} positions={positions} onPositionsChange={setPositions} label=\"Laptop lid\" />;\n}",
      "exports": [
        "StickerPeel",
        "StickerPeelItem"
      ]
    },
    {
      "name": "ticket-tear",
      "kind": "component",
      "title": "Ticket tear",
      "description": "Pull a ticket stub and it tears along the perforation hole by hole, then drops away.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "gesture",
        "ticket",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ticket-tear",
      "markdown": "https://uiarc.dev/components/ticket-tear/markdown",
      "registry": null,
      "keywords": [
        "ticket tear",
        "tear off stub",
        "perforated ticket",
        "event ticket",
        "boarding pass",
        "coupon tear",
        "react ticket",
        "tear animation"
      ],
      "whenToUse": [
        "Event tickets, boarding passes, and coupons that are used once.",
        "A playful confirmation step where tearing means committing."
      ],
      "whenNotToUse": [
        "Use hold-to-confirm or a dialog for destructive actions that need clear wording.",
        "Use a plain card for tickets that are only displayed, not redeemed."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The stub is role=\"button\" with the label as its name.",
        "The flap copy of the stub used for the bend is aria-hidden, so the stub is read once.",
        "A polite status announces \"Stub torn off\"."
      ],
      "motion": [
        "Pulling down or away tears the perforation from the top, tracking the pointer directly; the tear only grows, like real paper.",
        "The edge passes through the center of each hole and wanders between them, coarse and fine, with loose paper fibers just past it on both sides.",
        "The loose flap follows the pull on a spring, so it has weight, and springs most of the way back when released mid-tear.",
        "Enter, Space or the torn prop rip the rest on their own, accelerating like a perforation that has started to give; a flick finishes it even faster.",
        "Freed, the stub pops up a hair, falls with gravity, drifts with the release speed, spins on a spring, and fades in about half a second.",
        "Touch devices get a tiny vibration as the tear crosses each hole, where supported.",
        "Reduced motion removes the bend, the fibers and the fall; the stub fades out."
      ],
      "responsive": [
        "The ticket fills its width; the stub keeps a fixed width and the body shrinks, down to 320px screens.",
        "Pointer capture keeps the tear tracking outside the stub, and touch-action is disabled on the stub only."
      ],
      "performance": [
        "The tear is clip-path polygons driven by motion values; React does not re-render while dragging.",
        "Notches and holes are CSS masks, so the paper stays one element per piece."
      ],
      "aiNotes": [
        "Choose it for check-in, redeeming a pass, or any one-time confirmation that should feel final.",
        "Keep the stub content short: a seat, a gate, a code.",
        "Control torn to reset it (a New ticket button) or to tear it from a server event."
      ],
      "related": [
        "hold-to-confirm",
        "swipe-actions",
        "sticker-peel"
      ],
      "usage": "import { TicketTear } from \"@/components/arc/ticket-tear/ticket-tear\";\n\nexport function Ticket() {\n  const [torn, setTorn] = useState(false);\n  return (\n    <TicketTear torn={torn} onTornChange={setTorn} label=\"Tear off stub to check in\"\n      stub={<div className=\"stub\">Row F, seat 12</div>}>\n      <div className=\"ticket\">\n        <p>Arc Conf</p>\n        <p>Teatro Tivoli, Lisbon</p>\n      </div>\n    </TicketTear>\n  );\n}",
      "exports": [
        "TicketTear"
      ]
    },
    {
      "name": "page-curl",
      "kind": "component",
      "title": "Page curl",
      "description": "A small magazine you read by dragging page corners, with a real fold, lit curl, and flick to turn.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "gesture",
        "book",
        "new"
      ],
      "docs": "https://uiarc.dev/components/page-curl",
      "markdown": "https://uiarc.dev/components/page-curl/markdown",
      "registry": null,
      "keywords": [
        "page curl",
        "page flip",
        "flipbook",
        "turn page animation",
        "book component",
        "magazine viewer",
        "react page flip",
        "page turn"
      ],
      "whenToUse": [
        "Editorial and brand showcases where paper is part of the story.",
        "Menus, catalogs, and yearbooks with a handful of spreads."
      ],
      "whenNotToUse": [
        "Use carousel or a scrolling page for long content or content people search.",
        "Use a document viewer for dense text that needs zoom and selection."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The book is a focusable group with aria-roledescription=\"book\" and the label as its name.",
        "A polite status reads the visible pages, such as \"Page 2 and 3 of 8\".",
        "Only the resting pages are in the accessibility tree; the turning copies are aria-hidden.",
        "Previous and next are real buttons with labels and disable at the covers. The visible count and the hint are decorative duplicates of the status."
      ],
      "motion": [
        "The fold line is the perpendicular bisector between the corner and the pointer, so the page follows the finger from where it was grabbed.",
        "The corner is held within a page width of the spine, like paper bound on one side.",
        "The flap is a mirror of the sheet across the fold with a lit curl at the crease; the page below gets a soft shadow at the fold.",
        "Release projects the pointer's momentum: past the middle or a flick turns the page on a spring that keeps the release velocity; otherwise it lays back down.",
        "The first time the book is mostly in view, the cover corner lifts about a quarter of the page on a soft spring, holds, and lays back down. Until the first turn, entering the book with a mouse repeats it at half the size, at most every 2.5 seconds.",
        "Hovering a corner lifts it. Hovering a page shows a chevron at its outer edge for the direction a tap turns. Taps, arrow keys, and the buttons turn along a lifted arc.",
        "The hint fades out for good after the first drag, tap, key, or button press.",
        "The closed book slides to center on its cover and back cover.",
        "Reduced motion swaps spreads without the curl and skips the tease."
      ],
      "responsive": [
        "Page size derives from the root width, so a 288px wide book still shows two readable pages.",
        "Each page is one tap and drag target with touch-action pan-y, so vertical swipes still scroll the page on touch."
      ],
      "performance": [
        "Fold geometry is computed once per frame from two motion values; clip paths and a matrix update without React renders.",
        "Up to three copies of a page render while it turns; only the resting spread renders otherwise."
      ],
      "aiNotes": [
        "Choose it for short, visual sequences: lookbooks, menus, reports, zines, and portfolios.",
        "Keep pages simple and fixed in size; each page renders into a slot of the same width and aspect ratio.",
        "An even number of pages ends on a back cover alone on the left.",
        "Make page one a real cover with a title, so the closed book reads as something to open."
      ],
      "related": [
        "cover-flow",
        "carousel",
        "lightbox-gallery"
      ],
      "usage": "import { PageCurl } from \"@/components/arc/page-curl/page-curl\";\n\nexport function Lookbook() {\n  return (\n    <PageCurl\n      label=\"Autumn lookbook\"\n      style={{ width: 540 }}\n      pages={[<Cover key=\"cover\" />, <Intro key=\"intro\" />, <Plate key=\"plate\" />]}\n    />\n  );\n}",
      "exports": [
        "PageCurl"
      ]
    },
    {
      "name": "image-trail",
      "kind": "component",
      "title": "Image trail",
      "description": "Moving the pointer leaves a curated trail of photos that pop in, lean, and fade away.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "media",
        "cursor",
        "new"
      ],
      "docs": "https://uiarc.dev/components/image-trail",
      "markdown": "https://uiarc.dev/components/image-trail/markdown",
      "registry": null,
      "keywords": [
        "image trail",
        "cursor image trail",
        "mouse trail photos",
        "hover photo trail",
        "portfolio hero effect",
        "image cursor follow",
        "react image trail"
      ],
      "whenToUse": [
        "A portfolio or agency hero that should feel alive on first move.",
        "Travel, food, or product stories told through a sequence of photos.",
        "Launch pages that want an instantly playful surface."
      ],
      "whenNotToUse": [
        "Use photo-grid or lightbox-gallery when people need to look at each photo.",
        "Use hover-image-list to preview one image per row in a list.",
        "Avoid it behind long text that people need to read."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Trail photos are decorative and aria-hidden; put anything people need in children.",
        "Children stay in normal reading order and remain interactive.",
        "The effect is pointer only and adds nothing a keyboard or screen reader user misses."
      ],
      "motion": [
        "A new photo appears only after the pointer has travelled spacing px, and never faster than every 40ms, so the trail stays curated.",
        "Each photo enters on the shared spring curve from 60% scale, leans with horizontal speed plus a small fixed variation, then drifts along the pointer's path as it shrinks and fades.",
        "Photos are pooled DOM nodes animated with the Web Animations API on transform and opacity only.",
        "Reduced motion keeps the trail as a calm crossfade in place, with no scale, tilt, or drift, and wider spacing."
      ],
      "responsive": [
        "Works at any size; the demo drops to 360px tall and a smaller headline on phones.",
        "Touch drags draw the trail with the finger; taps do nothing."
      ],
      "performance": [
        "A fixed pool of image nodes is reused, so a long trail never grows the DOM.",
        "Images are preloaded on mount. Animations run on the compositor through WAAPI."
      ],
      "aiNotes": [
        "Choose it for portfolio, travel, or studio heroes where photos are the story.",
        "Pass 8 to 14 small images (about 384px wide) in a deliberate order; the trail shows them in sequence.",
        "Give the root a height. A white headline with mix-blend-mode: difference reads over the photos in both themes.",
        "The area sets touch-action: none so a finger drag draws the trail; keep it to a hero sized block, or set disabled on small screens if scrolling matters more."
      ],
      "related": [
        "photo-grid",
        "lightbox-gallery"
      ],
      "usage": "import { ImageTrail } from \"@/components/arc/image-trail/image-trail\";\n\nconst images = [\n  { src: \"/photos/tram.jpg\", alt: \"A yellow tram in Lisbon\" },\n  { src: \"/photos/rooftops.jpg\", alt: \"Terracotta rooftops\" },\n  { src: \"/photos/sea.jpg\", alt: \"The sea at dusk\" },\n];\n\nexport function Hero() {\n  return (\n    <ImageTrail images={images} style={{ height: 420 }}>\n      <h1>Lisbon, in passing</h1>\n    </ImageTrail>\n  );\n}",
      "exports": [
        "ImageTrail"
      ]
    },
    {
      "name": "black-hole",
      "kind": "component",
      "title": "Black hole",
      "description": "A delete transition that pulls the chosen items into a tiny singularity while the rest of the list bends around it; undo spins them back out.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "webgl",
        "new"
      ],
      "docs": "https://uiarc.dev/components/black-hole",
      "markdown": "https://uiarc.dev/components/black-hole/markdown",
      "registry": null,
      "keywords": [
        "delete animation",
        "archive transition",
        "black hole",
        "singularity",
        "gravitational lensing",
        "spiral",
        "webgl",
        "undo",
        "remove items",
        "list delete"
      ],
      "whenToUse": [
        "Deleting or archiving a handful of selected rows, cards, or files, with an Undo right after.",
        "Emptying a trash, clearing notifications, or dismissing a batch where the removal should feel final."
      ],
      "whenNotToUse": [
        "High frequency deletes such as clearing a single chip or tag. Use a quick height collapse instead.",
        "Irreversible actions without undo. The spectacle should not stand in for a confirmation.",
        "Very long lists that scroll; the effect paints the visible wrapper, so wrap only the scrolling viewport."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The canvas is aria-hidden and only covers the content while the effect runs; the real DOM is back in place the moment it ends.",
        "Announce the result yourself (for example \"2 conversations deleted\" with an Undo button in a live region). The effect is visual only.",
        "Pointer events are not blocked on the rest of the page; disable the trigger while a transition runs to avoid double deletes."
      ],
      "motion": [
        "Items fall along a logarithmic spiral. Points nearer the hole start earlier, so the inner edge falls faster than the outer one and rows stretch into arms.",
        "The rest of the content is lensed with the Einstein ring relation, so text near the hole bends and briefly mirrors around it.",
        "The hole opens with a slight overshoot, grows a little as it eats, then collapses and sends out one soft shockwave ring. Undo plays the same path backwards.",
        "With reduced motion, or without WebGL2, the items fade out in 200ms and fade back in on release."
      ],
      "responsive": [
        "Measures the wrapper at the start of every run, so it works at any width including 320px.",
        "The hole and lensing scale with device pixel ratio, capped at 2x."
      ],
      "performance": [
        "Painting the wrapper to two textures takes a few milliseconds for a typical list; the shaders and context are prepared while the page is idle.",
        "Drawing is three passes on the GPU per frame (lensed content, a 5px mesh for the falling items, and the hole), well within a frame at 120Hz.",
        "Textures and buffers are released after each run; one WebGL2 context is kept per instance."
      ],
      "aiNotes": [
        "Use for destructive or archive actions on a list, grid, or board where a moment of drama is welcome and undo is offered.",
        "Keep refs to the item elements (a Map keyed by id works well) and pass the selected ones to `swallow`. Remove the data only after the promise resolves.",
        "For undo, restore the data, then call `release` with the re-inserted elements in `useLayoutEffect` so they never flash. Pair the list with height exit and enter animations so gaps close and reopen smoothly.",
        "The painter handles backgrounds, borders, radii, text, images and inline SVG icons. Box shadows, gradients, pseudo elements and cross origin images are not painted."
      ],
      "related": [
        "swipe-actions",
        "toast-stack"
      ],
      "usage": "import { useLayoutEffect, useRef, useState } from \"react\";\nimport { BlackHole } from \"@/components/arc/black-hole/black-hole\";\nimport type { BlackHoleHandle } from \"@/components/arc/black-hole/black-hole\";\n\nexport function Inbox({ initial }: { initial: Mail[] }) {\n  const hole = useRef<BlackHoleHandle>(null);\n  const rows = useRef(new Map<string, HTMLLIElement>());\n  const [items, setItems] = useState(initial);\n  const [selected, setSelected] = useState<string[]>([]);\n\n  async function remove() {\n    await hole.current?.swallow(selected.map(id => rows.current.get(id)!));\n    setItems(current => current.filter(item => !selected.includes(item.id)));\n    setSelected([]);\n  }\n\n  return (\n    <BlackHole ref={hole}>\n      <ul>\n        {items.map(item => (\n          <li key={item.id} ref={el => { if (el) rows.current.set(item.id, el); }}>{item.subject}</li>\n        ))}\n      </ul>\n    </BlackHole>\n  );\n}",
      "exports": [
        "BlackHole",
        "BlackHoleHandle",
        "BlackHoleOptions"
      ]
    },
    {
      "name": "ink-fluid",
      "kind": "component",
      "title": "Ink fluid",
      "description": "A real time fluid simulation behind your hero: the cursor stirs soft ink that swirls, settles and fades; calm when left alone.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "background",
        "webgl",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ink-fluid",
      "markdown": "https://uiarc.dev/components/ink-fluid/markdown",
      "registry": null,
      "keywords": [
        "fluid simulation",
        "ink",
        "smoke",
        "hero background",
        "webgl",
        "stable fluids",
        "interactive background",
        "cursor effect",
        "navier stokes"
      ],
      "whenToUse": [
        "A landing hero or sign in screen that should feel alive without a video.",
        "Empty states and waiting screens where a gentle, interactive background rewards curiosity."
      ],
      "whenNotToUse": [
        "Dense product surfaces such as tables and forms. Movement behind data distracts.",
        "More than one per page; each instance runs its own simulation."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The canvas is aria-hidden and ignores pointer hits; content inside stays fully interactive and readable.",
        "Ink is capped at a low opacity so text over it keeps its contrast in both themes.",
        "With reduced motion it renders one composed still frame and stops."
      ],
      "motion": [
        "Semi Lagrangian advection, vorticity confinement and a 20 step pressure solve on a small grid, drawn with linear upscaling and a soft light term from the ink gradient so wisps read silky.",
        "Pointer velocity drives the force; the amount of ink scales with speed so slow moves leave faint trails and quick flicks bloom.",
        "Ink fades over a couple of seconds; accent tinted ink turns neutral as it spreads, so fresh strokes carry colour and old ones go grey.",
        "It pauses when scrolled out of view or when the tab is hidden, and resumes where it left off."
      ],
      "responsive": [
        "Resizes its simulation with the element, debounced by 120ms.",
        "Touch devices stir while a finger is down; `touch-action: pan-y` keeps vertical scrolling working."
      ],
      "performance": [
        "About 30 small GPU passes per frame on a 128 cell grid; well under 2ms on integrated graphics.",
        "Device pixel ratio is capped at 1.5 for the output canvas; the ink texture is 512 cells on the short side at medium quality.",
        "Stops its animation frame loop off screen and in background tabs."
      ],
      "aiNotes": [
        "Use as a hero or empty state background. Give the root a min height, a background colour and a border radius; the ink draws on transparent pixels over it.",
        "Colours come from `--foreground` (ink) and `--accent` (tint) on the root, so it follows the theme and accent switcher automatically. Override `--ink-color` or `--ink-accent` on the root to change them.",
        "Put buttons inside with `pointer-events: auto`; the root listens to pointer moves so stirring works over text too.",
        "Falls back to a plain background when WebGL2 or float render targets are unavailable."
      ],
      "related": [
        "dot-grid",
        "image-trail"
      ],
      "usage": "import { InkFluid } from \"@/components/arc/ink-fluid/ink-fluid\";\n\nexport function Hero() {\n  return (\n    <InkFluid className=\"hero\">\n      <h1>Ship interfaces that feel alive</h1>\n      <p>Components with motion that explains every change.</p>\n    </InkFluid>\n  );\n}",
      "exports": [
        "InkFluid"
      ]
    },
    {
      "name": "glass-menu",
      "kind": "component",
      "title": "Glass menu",
      "description": "A glass action menu whose trigger morphs into the panel, with a highlight pill that glides between rows, one in-place submenu, and full keyboard support.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "glass",
        "menu",
        "new"
      ],
      "docs": "https://uiarc.dev/components/glass-menu",
      "markdown": "https://uiarc.dev/components/glass-menu/markdown",
      "registry": null,
      "keywords": [
        "glass menu",
        "dropdown",
        "context menu",
        "action menu",
        "liquid glass",
        "frosted",
        "shared layout",
        "morph",
        "submenu"
      ],
      "whenToUse": [
        "Actions for a photo, file, or card shown over an image or a colorful canvas.",
        "A title menu in an editor or viewer, where the title itself opens the actions."
      ],
      "whenNotToUse": [
        "Plain settings pages with no imagery behind the menu. Use dropdown-menu on a neutral surface.",
        "Navigation between pages. Use morph-nav or tabs.",
        "Deep hierarchies. Use a command palette or a dedicated screen."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has `aria-haspopup=\"menu\"`, `aria-expanded`, and `aria-controls` pointing at the open menu.",
        "Rows are `menuitem`, or `menuitemcheckbox` with `aria-checked` when `checked` is set; rows with a submenu carry `aria-haspopup`.",
        "Focus moves to the highlighted row, so screen readers announce it; with no row highlighted the menu itself holds focus.",
        "Pressing outside closes the menu without stealing focus from what was pressed.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The highlight pill marks the active row for keyboard and pointer alike."
      ],
      "motion": [
        "The trigger's glass and the panel's glass share a layout id, so opening is one spring morph from the trigger's box to the panel's, and closing morphs back.",
        "Rows fade and sharpen in a beat after the glass starts to grow, so text never shows outside the glass.",
        "The highlight pill glides between rows on the shared morph spring.",
        "The submenu slides in the direction of travel while the panel height follows on a critically damped spring.",
        "With reduced motion the panel and submenu appear in place and the pill jumps."
      ],
      "responsive": [
        "The panel width is capped to the viewport, and the submenu slides in place instead of flying out to the side.",
        "Touch opens on tap; rows are 36px tall with a full width hit area."
      ],
      "performance": [
        "The open and close morph is a transform on one element; the backdrop blur is the main paint cost, on a single layer.",
        "The menu mounts only while open."
      ],
      "aiNotes": [
        "Use for a compact action menu on media, cards, or toolbars that sit over imagery, where the glass reads against the content behind it.",
        "Put the most common actions first, keep shortcuts accurate, and put destructive rows in their own last group with `danger`.",
        "Only one submenu level is supported, by design; it slides in place so it works at 320px.",
        "For checkable rows, pass `checked` from your state and `keepOpen` so people can toggle without reopening."
      ],
      "related": [
        "dropdown-menu",
        "orbit-menu",
        "morph-nav"
      ],
      "usage": "import { Copy, FolderInput, Trash2 } from \"lucide-react\";\nimport { GlassMenu } from \"@/components/arc/glass-menu/glass-menu\";\n\nexport function PhotoActions() {\n  return (\n    <GlassMenu\n      label=\"Photo actions\"\n      onSelect={id => console.log(id)}\n      groups={[\n        { id: \"edit\", items: [\n          { id: \"duplicate\", label: \"Duplicate\", icon: <Copy size={16} />, shortcut: \"⌘D\" },\n          { id: \"move\", label: \"Move to\", icon: <FolderInput size={16} />, items: [\n            { id: \"lisbon\", label: \"Lisbon trip\" },\n            { id: \"archive\", label: \"Archive\" },\n          ] },\n        ] },\n        { id: \"danger\", items: [{ id: \"delete\", label: \"Delete\", icon: <Trash2 size={16} />, danger: true }] },\n      ]}\n    >\n      Pool house\n    </GlassMenu>\n  );\n}",
      "exports": [
        "GlassMenu",
        "GlassMenuItem"
      ]
    },
    {
      "name": "glass-segmented",
      "kind": "component",
      "title": "Glass segmented",
      "description": "A glass segmented control whose liquid indicator stretches between segments and can be dragged and flung.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "glass",
        "input",
        "new"
      ],
      "docs": "https://uiarc.dev/components/glass-segmented",
      "markdown": "https://uiarc.dev/components/glass-segmented/markdown",
      "registry": null,
      "keywords": [
        "segmented control",
        "liquid glass",
        "toggle group",
        "tabs",
        "gooey indicator",
        "stretch",
        "frosted",
        "drag"
      ],
      "whenToUse": [
        "Switching the view of a photo, map, or media surface.",
        "A compact filter over imagery, such as day, week, month on a hero chart."
      ],
      "whenNotToUse": [
        "Plain forms on neutral surfaces. Use segmented-control.",
        "More than five options or long labels. Use tabs or a select."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The control is a `radiogroup` with a roving tab stop; each segment is a `radio` with `aria-checked`.",
        "The indicator is decorative SVG and hidden from assistive tech.",
        "Selection is also shown by label color, not only by the indicator."
      ],
      "motion": [
        "Each edge of the indicator has its own spring: the edge facing the travel is quick, the other lags, so the drop stretches and then settles.",
        "The drop thins as it stretches, like liquid keeping its volume.",
        "Drag the selected segment and the drop follows with the same liquid lag; on release, velocity is projected to pick where it lands.",
        "With reduced motion the indicator jumps and dragging tracks directly."
      ],
      "responsive": [
        "`fullWidth` gives equal segments that fill the container at 320px.",
        "Touch drag uses `touch-action: pan-y`, so vertical scroll still works over the control."
      ],
      "performance": [
        "The indicator is an SVG rect whose attributes update from motion values, so the stretch is paint only with no layout.",
        "Segment positions are measured once and on resize."
      ],
      "aiNotes": [
        "Use for two to five short, mutually exclusive views on top of imagery or maps, where the glass reads against the content.",
        "Use `fullWidth` on mobile or inside a card; keep labels to one or two words.",
        "Drag only starts on the selected segment, so taps elsewhere stay plain clicks."
      ],
      "related": [
        "segmented-control",
        "liquid-tab-bar",
        "billing-toggle"
      ],
      "usage": "import { useState } from \"react\";\nimport { GlassSegmented } from \"@/components/arc/glass-segmented/glass-segmented\";\n\nexport function MapMode() {\n  const [mode, setMode] = useState(\"explore\");\n  return (\n    <GlassSegmented\n      label=\"Map mode\"\n      value={mode}\n      onValueChange={setMode}\n      options={[\n        { value: \"explore\", label: \"Explore\" },\n        { value: \"drive\", label: \"Drive\" },\n        { value: \"transit\", label: \"Transit\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "GlassSegmented"
      ]
    },
    {
      "name": "glass-card",
      "kind": "component",
      "title": "Glass card",
      "description": "A glass info panel over a photo whose rim catches a pointer-following highlight while the card tilts; tap to morph it into a detail sheet.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "glass",
        "card",
        "new"
      ],
      "docs": "https://uiarc.dev/components/glass-card",
      "markdown": "https://uiarc.dev/components/glass-card/markdown",
      "registry": null,
      "keywords": [
        "glass card",
        "glassmorphism card",
        "profile card",
        "tilt card",
        "specular highlight",
        "frosted glass",
        "expandable card",
        "shared layout"
      ],
      "whenToUse": [
        "Profile, host, or creator cards on a landing page or in a discovery grid.",
        "A featured product or stay where the photo sells and the details can wait for a tap."
      ],
      "whenNotToUse": [
        "Dense grids of many cards; tilt and glass on every tile gets busy. Use card.",
        "Cards that should open a full page; use card-to-article."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The glass header is a real button with aria-expanded and aria-controls pointing at the detail region.",
        "Clicking anywhere on the photo is a pointer shortcut for the same toggle; keyboard users use the header.",
        "Detail content is removed from the DOM when collapsed, so hidden buttons are never focusable.",
        "Under prefers-reduced-transparency the glass turns solid."
      ],
      "motion": [
        "Tilt and highlight follow the pointer through springs, so the card eases into position and settles back when the pointer leaves. Touch input skips the tilt.",
        "Expanding is one shared-layout spring on the panel; the header keeps its position and the details fade up with a short blur.",
        "The photo scales up slightly and the floor shade deepens while open.",
        "Reduced motion removes tilt and the morph; the panel switches size and the details fade."
      ],
      "responsive": [
        "Fills its container width and keeps a 4 by 5 ratio, so it works from 280px up.",
        "Touch devices get the tap to expand and skip the pointer tilt."
      ],
      "performance": [
        "Pointer tracking writes springs and CSS variables only, with no React renders per frame.",
        "One backdrop filter on the panel. The highlight is a masked gradient, not a filter."
      ],
      "aiNotes": [
        "Use it for one featured entity over a strong photo: a person, a stay, a product. Put the essentials in title, subtitle, and aside, and the rest in children.",
        "Keep detail content short: a line of text, three stats, and one or two actions. Longer content belongs on a page.",
        "Give the card a width through className; it sets its own height from the 4 by 5 ratio.",
        "The accent belongs in the detail actions or a rating icon, not on the glass itself."
      ],
      "related": [
        "holo-card",
        "expandable-card",
        "card-to-article"
      ],
      "usage": "import { GlassCard } from \"@/components/arc/glass-card/glass-card\";\n\nexport function Host() {\n  return (\n    <GlassCard image=\"/people/jasmine.jpg\" imageAlt=\"Portrait of Jasmine Brooks\" title=\"Jasmine Brooks\" subtitle=\"Design lead, Lisbon\" aside=\"4.9\">\n      <p>Leads design systems at Meridian.</p>\n      <button type=\"button\">Follow</button>\n    </GlassCard>\n  );\n}",
      "exports": [
        "GlassCard"
      ]
    },
    {
      "name": "glass-tabbar",
      "kind": "component",
      "title": "Glass tab bar",
      "description": "A floating Liquid Glass tab bar that refracts what scrolls beneath it, with a glass lens that glides, magnifies and can be dragged between tabs, and an optional search bubble; it compacts on scroll down.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "glass",
        "navigation",
        "new"
      ],
      "docs": "https://uiarc.dev/components/glass-tabbar",
      "markdown": "https://uiarc.dev/components/glass-tabbar/markdown",
      "registry": null,
      "keywords": [
        "liquid glass",
        "glass tab bar",
        "floating tab bar",
        "bottom navigation",
        "mobile tab bar",
        "ios tab bar",
        "refraction",
        "lens indicator",
        "draggable tab indicator",
        "collapsing tab bar",
        "hide on scroll",
        "glassmorphism navigation"
      ],
      "whenToUse": [
        "Mobile web apps and PWAs with three to five top level sections and one global action such as search.",
        "Content feeds where the bar should step aside while reading and come back on scroll up."
      ],
      "whenNotToUse": [
        "Desktop layouts with many sections; use a sidebar or morph-nav.",
        "Switching views inside a card; use segmented-control or tabs."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Renders a nav landmark around a tablist; every tab, including the action, has role=\"tab\" and aria-selected, with a roving tabindex.",
        "Labels stay in the accessibility tree when the bar is compact, so each tab keeps its name.",
        "Badges carry hidden text such as \"3 new\" for screen readers.",
        "Selection is carried by the lens, the accent tint, and aria-selected, never by color alone.",
        "Reduced transparency swaps the glass for a solid raised surface; increased contrast strengthens the tint, border, and ink.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "The lens glides on the smooth spring, which never overshoots. Its speed lifts it off the bar: it grows, stretches along the direction of travel, and turns into a clear lens that magnifies the icons under it, then settles back into a soft fill.",
        "Each tab's tint and icon scale follow the lens position, so the accent glides with it instead of switching.",
        "Press the selected tab and the lens lifts; drag past a few pixels and it follows the pointer across the bar; release snaps it to the nearest tab and selects it.",
        "One spring drives the compact morph: slot width, bar height, icon lift, and label fade all read from it, so the lens stays on its tab mid morph.",
        "Reduced motion moves the lens and the size instantly, with no lift, stretch, or magnification."
      ],
      "responsive": [
        "Tabs share the available width, from 78px down to 56px each, so five tabs and an action fit a 390px screen. Compact tabs are 50px.",
        "The bar and lens take touch with pan-y, so vertical scrolling that starts on the bar still works."
      ],
      "performance": [
        "Motion values drive every frame; React renders only on tab, compact, or size changes. Nothing animates at rest.",
        "Chromium gets real refraction: an SVG displacement filter inside backdrop-filter bends the backdrop at the rim, with a slight per channel dispersion. Safari and Firefox are detected and keep a blur and saturation glass.",
        "The lens refraction is a fixed layer clipped to the lens; while it moves, only the clip and the filter's map rectangle update."
      ],
      "aiNotes": [
        "Use it as the primary navigation of a mobile web app. Keep it to three to five tabs with short labels, plus at most one action.",
        "Pass the page's scroll container as scrollRef when the app scrolls inside an element instead of the window.",
        "Place it with position fixed or absolute at the bottom; the component centers itself and only the glass takes pointer events.",
        "Put photos or colorful content behind it; the material is designed to be seen over real content, not a flat page.",
        "Leave content room at the bottom (about 110px) so the last item is not hidden behind the bar."
      ],
      "related": [
        "liquid-tab-bar",
        "morph-nav",
        "dock",
        "tabs"
      ],
      "usage": "import { GlassTabBar } from \"@/components/arc/glass-tabbar/glass-tabbar\";\nimport { Bookmark, Compass, House, Search, UserRound } from \"lucide-react\";\n\nexport function AppShell() {\n  const [tab, setTab] = useState(\"home\");\n  return (\n    <GlassTabBar\n      className=\"tabbar\"\n      value={tab}\n      onValueChange={setTab}\n      items={[\n        { value: \"home\", label: \"Home\", icon: <House /> },\n        { value: \"explore\", label: \"Explore\", icon: <Compass /> },\n        { value: \"saved\", label: \"Saved\", icon: <Bookmark />, badge: 3 },\n        { value: \"profile\", label: \"Profile\", icon: <UserRound /> },\n      ]}\n      action={{ value: \"search\", label: \"Search\", icon: <Search /> }}\n    />\n  );\n}",
      "exports": [
        "GlassTabBar",
        "GlassTabBarItem"
      ]
    },
    {
      "name": "card-to-article",
      "kind": "component",
      "title": "Card to article",
      "description": "Story cards that open into full articles with one seamless morph and shrink back into their exact slot when dragged down.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "layout",
        "gesture",
        "new"
      ],
      "docs": "https://uiarc.dev/components/card-to-article",
      "markdown": "https://uiarc.dev/components/card-to-article/markdown",
      "registry": null,
      "keywords": [
        "app store card",
        "card expand",
        "shared element transition",
        "card to detail",
        "magic motion",
        "expanding card",
        "blog grid",
        "article modal"
      ],
      "whenToUse": [
        "Blogs, journals, and changelogs where each entry has a strong image.",
        "Case study and portfolio grids.",
        "Editorial landing sections that preview stories without leaving the page."
      ],
      "whenNotToUse": [
        "Use a normal link to a page when articles are long or need their own URL and SEO.",
        "Use expandable-card for small inline details.",
        "Text only lists with no imagery."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Cards are buttons with aria-haspopup=\"dialog\" and aria-expanded.",
        "The open article is a modal dialog labelled by its title, with focus moved to the close button and trapped inside.",
        "Closing returns focus to the card it came from.",
        "Page scroll is locked while the article is open, keeping the scrollbar's space so nothing shifts."
      ],
      "motion": [
        "The card itself becomes the article: its box and corners spring to the sheet while the hero scales as one piece, so image and title never distort.",
        "The body rises in after the morph; the close button pops in last.",
        "Drag down on touch, drag the hero with a mouse, or pull past the top with a trackpad: the sheet shrinks and drops with the gesture and closes past a threshold or on a flick.",
        "Closing re-measures the card's slot and lands flush in it, no bounce. A reading progress bar tracks the scroll.",
        "Reduced motion opens and closes without travel."
      ],
      "responsive": [
        "Two columns, then one below 520px of container width.",
        "The article fills the screen with a 12px margin on phones and caps at 680px wide on desktop.",
        "Title and tag sizes follow the hero's own width through container units."
      ],
      "performance": [
        "The morph is one layout animation plus a scale corrected hero; nothing else re-lays out.",
        "The pull gesture is transform only and runs from motion values."
      ],
      "aiNotes": [
        "Keep every image the same ratio as aspectRatio so the hero scales cleanly.",
        "Titles are drawn on the photo in white over a legibility scrim; pick photos with calm lower areas.",
        "Use onValueChange to sync the open article with the URL for deep links."
      ],
      "related": [
        "expandable-card",
        "lightbox-gallery",
        "photo-grid",
        "dialog"
      ],
      "usage": "import { CardToArticle } from \"@/components/arc/card-to-article/card-to-article\";\n\nconst posts = [\n  {\n    id: \"glass-house\",\n    tag: \"Case study\",\n    title: \"A glass house that stays cool in August\",\n    meta: \"5 min read\",\n    image: \"/media/photos/pool-house.jpg\",\n    imageAlt: \"A modern glass house beside a long pool\",\n    body: <p>The brief was simple and a little unreasonable...</p>,\n  },\n];\n\nexport function Journal() {\n  return <CardToArticle items={posts} />;\n}",
      "exports": [
        "CardToArticle"
      ]
    },
    {
      "name": "skeleton-morph",
      "kind": "component",
      "title": "Skeleton morph",
      "description": "Loading skeletons that grow into the real content, block by block, instead of swapping.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "loading",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/skeleton-morph",
      "markdown": "https://uiarc.dev/components/skeleton-morph/markdown",
      "registry": null,
      "keywords": [
        "skeleton loader",
        "loading skeleton",
        "skeleton morph",
        "shared layout loading",
        "content placeholder",
        "shimmer loading",
        "react skeleton animation"
      ],
      "whenToUse": [
        "Profile cards, feeds, and lists that load in under a few seconds.",
        "Dashboards where the layout is known before the data arrives.",
        "Places where a hard swap from gray boxes to content would feel jarring."
      ],
      "whenNotToUse": [
        "Use skeleton for long lists of hundreds of rows; layout animation there is costly.",
        "Use a progress indicator when loading takes more than a few seconds.",
        "Avoid it when the loaded layout is unknown in advance."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is aria-busy while loading and carries a visually hidden status with loadingLabel.",
        "Skeleton shapes are aria-hidden; content is not rendered until it is ready, so nothing half loaded is read.",
        "Blocks keep reading order, so focus order is the same before and after loading."
      ],
      "motion": [
        "Blocks resolve one after another in DOM order, 40ms apart by default.",
        "Each placeholder stretches from its skeleton box to the content's measured box on a critically damped spring while it fades; the content sharpens out of a 4px blur underneath. Blocks change in place and never travel across the layout.",
        "The shell eases to its new height and clips while it does, so the card border never jumps.",
        "A calm opacity pulse runs on the skeletons while loading and stops when they resolve.",
        "Going back to loading is immediate, so fast reloads never leave a block stuck halfway.",
        "Reduced motion removes the pulse and the stretch and uses a short crossfade."
      ],
      "responsive": [
        "Blocks size to their content once loaded, so layouts reflow naturally at any width.",
        "Skeleton widths accept percentages for fluid placeholders."
      ],
      "performance": [
        "Each block measures itself once when it resolves; there is no shared layout projection, so it stays cheap in long cards.",
        "The height ease uses one ResizeObserver per region.",
        "The sweep is a CSS transform animation on a pseudo-element."
      ],
      "aiNotes": [
        "Wrap every visible piece of the loaded layout in a MorphBlock; unwrapped elements change without animation.",
        "Size skeletons to the real content: give text an explicit px line-height and pass the same lineHeight and lines, and set width near the typical text width.",
        "Put the card styles on SkeletonMorph itself so the border follows the eased height.",
        "For a simple placeholder without the morph, use skeleton."
      ],
      "related": [
        "skeleton",
        "morph-loader",
        "empty-state"
      ],
      "usage": "import { MorphBlock, SkeletonMorph } from \"@/components/arc/skeleton-morph/skeleton-morph\";\n\nexport function ProfileCard({ user }: { user?: User }) {\n  return (\n    <SkeletonMorph loading={!user} className=\"card\">\n      <MorphBlock radius=\"circle\" width={48} height={48}>\n        <img src={user?.avatar} alt=\"\" width={48} height={48} />\n      </MorphBlock>\n      <MorphBlock width={120} lines={1} lineHeight={20}><p style={{ lineHeight: \"20px\" }}>{user?.name}</p></MorphBlock>\n      <MorphBlock lines={3} lineHeight={20}><p style={{ lineHeight: \"20px\" }}>{user?.bio}</p></MorphBlock>\n    </SkeletonMorph>\n  );\n}",
      "exports": [
        "SkeletonMorph",
        "MorphBlock"
      ]
    },
    {
      "name": "orbit-logos",
      "kind": "component",
      "title": "Orbit logos",
      "description": "An integrations showcase with brand logos orbiting your product mark on tilted rings. Hover or focus one to rest the orbit, draw a connection, and see what it does.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "integrations",
        "logos",
        "new"
      ],
      "docs": "https://uiarc.dev/components/orbit-logos",
      "markdown": "https://uiarc.dev/components/orbit-logos/markdown",
      "registry": null,
      "keywords": [
        "orbit logos",
        "integrations",
        "integration hero",
        "logo orbit",
        "orbiting logos",
        "integrations page",
        "connect apps",
        "ecosystem",
        "partners",
        "tech stack"
      ],
      "whenToUse": [
        "An integrations or ecosystem hero on a marketing site.",
        "An integrations settings screen where people browse, read about, and connect tools.",
        "A launch page showing everything a product plugs into."
      ],
      "whenNotToUse": [
        "Use a logo marquee for a long list of customer logos.",
        "Use a searchable grid or table for catalogs larger than about twenty integrations."
      ],
      "accessibility": [
        "Logos are a labelled list of toggle buttons with aria-pressed; each name includes the category and whether it is connected.",
        "Only one logo is in the tab order (roving tabindex), so the orbit is a single stop between the chips and the card.",
        "The card is a labelled region with real buttons and a Docs link; opening, stepping, and filtering are announced in a polite live region.",
        "Connected status is shown as text and an icon in the card, not by the green dot alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focus shows as the lifted chip and its name tag."
      ],
      "motion": [
        "One rAF loop writes chip translations and the connector; React does not render per frame, and the loop stops when everything is at rest, off screen, or the tab is hidden.",
        "The turn eases to rest on a critically damped spring whenever the pointer or focus enters, a category is picked, or a card is open, so nothing moves while you aim.",
        "Opening a logo springs its ring the short way to the focus spot, with a touch of overshoot. When an outer logo parks, the inner ring steps aside so the connector stays clear.",
        "The card morphs out of the chip: it starts at the chip's box as a circle and springs to its own box, then its content fades in. Previous and next slide the content in the direction of travel.",
        "The category highlight is a shared layout pill on the morph spring; chips scale and dim on CSS springs.",
        "Reduced motion stops the rotation, places rings and the connector at once, and skips the morph and slides."
      ],
      "responsive": [
        "From 760px wide the orbit sits beside the detail panel and the chosen logo parks on the right; below that the card sits under the orbit and the logo parks at the bottom.",
        "Chips are 48 to 56px on desktop and 44px on phones, and the orbit fills the width up to 460px.",
        "Category chips scroll sideways in one row on narrow screens.",
        "Name tags only appear on hover or keyboard focus, never on touch."
      ],
      "performance": [
        "Positions are transform only writes from a single rAF loop that sleeps when nothing moves.",
        "Visual states (hover, dim, selected) are CSS transitions on each chip's face, not per frame work.",
        "Image marks are plain img elements, so each SVG file is fetched once and cached."
      ],
      "aiNotes": [
        "Use it as the hero of an integrations page or an in-app integrations screen, with your product mark in the center.",
        "Ten to sixteen logos read best across two rings. Give every logo a category so the filter chips appear, and a one line description.",
        "Wire onConnect to your real flow and return its promise; the card shows Connecting until it settles and Try again if it rejects.",
        "Brand marks are sample content. Show only integrations that exist and follow each brand's trademark rules."
      ],
      "related": [
        "logo-marquee",
        "hover-card",
        "orbit-menu",
        "chip-group"
      ],
      "usage": "import { OrbitLogos } from \"@/components/arc/orbit-logos/orbit-logos\";\n\n// Path data from Simple Icons (CC0) follows the text color; image URLs keep full brand color.\nconst integrations = [\n  { name: \"Stripe\", icon: \"/logos/stripe-color.svg\", category: \"Payments\", description: \"Starts trials and bills usage\", href: \"https://docs.stripe.com\" },\n  { name: \"Vercel\", icon: \"m12 1.608 12 20.784H0Z\", category: \"Dev\", description: \"Comments preview links on builds\", connected: true },\n  { name: \"Slack\", icon: \"/logos/slack-color.svg\", category: \"Comms\", description: \"Posts deploy alerts to your channels\" },\n];\n\nexport function Integrations() {\n  return <OrbitLogos\n    logos={integrations}\n    center={<ProductMark />}\n    onConnect={logo => api.connect(logo.id ?? logo.name)}\n  />;\n}",
      "exports": [
        "OrbitLogos"
      ]
    },
    {
      "name": "sand-text",
      "kind": "component",
      "title": "Sand text",
      "description": "A headline made of sand that pours into the letters, crumbles where the cursor digs, and heals itself.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "text",
        "canvas",
        "physics",
        "new"
      ],
      "docs": "https://uiarc.dev/components/sand-text",
      "markdown": "https://uiarc.dev/components/sand-text/markdown",
      "registry": null,
      "keywords": [
        "sand",
        "falling sand",
        "cellular automaton",
        "particles",
        "text effect",
        "headline",
        "crumble",
        "physics",
        "canvas",
        "hourglass"
      ],
      "whenToUse": [
        "A hero headline or section title that should be fun to touch.",
        "Short words that change on a selection, like a product name or theme."
      ],
      "whenNotToUse": [
        "Body copy or long headings; use in-view-title or text-reveal.",
        "Text that must stay readable while people interact near it, like form labels."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The real text is rendered in a visually hidden heading, so screen readers and search read it as text.",
        "The canvas is aria-hidden. Digging is decorative and never removes the text from the accessibility tree."
      ],
      "motion": [
        "Letters fill bottom first like jars. Dug grains fly with the pointer's velocity and settle with a falling sand automaton.",
        "Grains that come to rest inside a letter stick again, missing grains pour back in once the pointer rests, and loose sand outside the letters drifts away.",
        "A new word turns the old letters loose, so the old text collapses while the new one pours in.",
        "Reduced motion shows the letters formed, turns digging off, and swaps words instantly."
      ],
      "responsive": [
        "The grid is derived from the box size and the text is fitted to 94 percent of the width, so it works at 320px.",
        "Touch: dragging a finger digs; vertical scroll still works."
      ],
      "performance": [
        "One grid cell per grain, drawn as a small ImageData scaled up without smoothing. A 700px wide headline is about 50,000 cells.",
        "The loop sleeps once every grain has settled and the pointer is gone."
      ],
      "aiNotes": [
        "Use for one short headline. Size it with font-size on className; the box is 1.55em tall to leave room for piles.",
        "Pass palette for tinted sand, but the default text color matches Arc surfaces in both themes."
      ],
      "usage": "import { SandText } from \"@/components/arc/sand-text/sand-text\";\n\nexport function Hero() {\n  return <SandText as=\"h1\" text=\"Dunes\" />;\n}",
      "exports": [
        "SandText"
      ]
    },
    {
      "name": "gravity-well",
      "kind": "component",
      "title": "Gravity well",
      "description": "A grid of tiles that bends toward the cursor like spacetime, with a click that sends a shockwave through it.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "grid",
        "physics",
        "new"
      ],
      "docs": "https://uiarc.dev/components/gravity-well",
      "markdown": "https://uiarc.dev/components/gravity-well/markdown",
      "registry": null,
      "keywords": [
        "gravity",
        "spacetime",
        "grid",
        "magnetic",
        "lens",
        "shockwave",
        "ripple",
        "tiles",
        "springs",
        "physics"
      ],
      "whenToUse": [
        "A team, customer, or moodboard wall where playfulness is the point.",
        "Landing page sections that show breadth: many logos, avatars, or photos at once."
      ],
      "whenNotToUse": [
        "Grids people must scan or click precisely, like file pickers; use a plain grid.",
        "Large collections; use infinite-moodboard or photo-grid."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is a named group with aria-roledescription=\"gravity well\". Tile content keeps its own semantics and alt text.",
        "Motion is decorative: tiles keep their grid slot and only move with transform, so reading order never changes.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The well itself shows where keyboard focus is."
      ],
      "motion": [
        "The well follows the pointer on a critically damped spring, and each tile follows the field on an underdamped spring, so fast moves leave a jelly wake.",
        "Tiles deeper in the well shrink, dim, and slide under their neighbours.",
        "A click drops a shockwave that travels at 900 px/s and rings each tile once as it passes.",
        "Reduced motion leaves the grid static."
      ],
      "responsive": [
        "Columns fit the width with minTileSize, or stay fixed with columns.",
        "Touch: a tap sends a shockwave and dragging pulls the well; vertical scroll keeps working."
      ],
      "performance": [
        "One transform, opacity, and z-index write per tile per frame, with no layout reads during animation.",
        "The loop stops once every spring is at rest."
      ],
      "aiNotes": [
        "Use a uniform set of square tiles, around 30 to 60. Content is wrapped in square cells; set object-fit: cover on images.",
        "Tie onPulse to something meaningful, like shuffling or selecting, or leave it as pure delight."
      ],
      "related": [
        "dot-grid",
        "photo-grid"
      ],
      "usage": "import { GravityWell } from \"@/components/arc/gravity-well/gravity-well\";\n\nexport function Team({ people }: { people: { name: string; src: string }[] }) {\n  return (\n    <GravityWell label=\"Team\">\n      {people.map(person => <img key={person.src} src={person.src} alt={person.name} />)}\n    </GravityWell>\n  );\n}",
      "exports": [
        "GravityWell"
      ]
    },
    {
      "name": "data-flow",
      "kind": "component",
      "title": "Data flow",
      "description": "Data flow illustration. Sources (app, database, events) are on the left, a processing core with a live count sits in the middle, and destinations (warehouse, dashboard, alerts) are on the right. Records leave as hollow dots and travel curved paths. In the core they turn into solid accent squares, then fan out to each destination; alerts receives about every third record.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/data-flow",
      "markdown": "https://uiarc.dev/components/data-flow/markdown",
      "registry": null,
      "keywords": [
        "data flow",
        "pipeline illustration",
        "etl",
        "data pipeline",
        "event routing",
        "animated diagram",
        "fan out",
        "ingestion",
        "transform"
      ],
      "whenToUse": [
        "A hero or feature section explaining how data moves through a product.",
        "Docs pages introducing an ingestion, transform, or routing concept.",
        "Integration pages that show which sources feed which destinations."
      ],
      "whenNotToUse": [
        "Use sankey-flow to show real volumes between stages.",
        "Use pipeline-status for the live state of a CI or data job.",
        "Use a table or list when people need to act on individual records."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The diagram is one element with role=\"img\" and an aria-label that describes the whole flow, built from the labels unless label is passed.",
        "Nodes are not controls, so nothing enters the tab order; hover tags repeat detail that is optional.",
        "Raw and processed records differ in shape (hollow dot, solid square), not only color.",
        "Pass caption for a visible explanation under the diagram.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop writes SVG transforms, opacity, and dash offsets straight to the DOM, and only when a value changed; React does not render per frame.",
        "Records travel along real cubic paths at an even pace from an arc length table, eased in and out on each leg, with a short trail that stops at the core outline.",
        "In the core each record swells slightly as it morphs from a hollow dot into an accent square, the core outline widens and fades, and the dashed ring quickens for a moment. On the way out one copy carries the record while the others fade in as they split off, so the fan out never clumps.",
        "Six records per 6.6 second cycle are a pure function of time, so the loop repeats seamlessly; ports and node borders light up as records leave and land.",
        "Speed changes, pausing, and hover (which slows the flow a little) ease on a critically damped spring.",
        "The loop stops off screen, in hidden tabs, and when paused. Reduced motion shows a still frame: a raw record on each in path and a processed record on each out path."
      ],
      "responsive": [
        "From 600px wide the flow runs left to right with two line nodes; below that it stacks top to bottom with sources in a row above the core and destinations below.",
        "The diagram is measured in real pixels and drawn in an SVG viewBox of that size, so text and hairlines stay crisp at any width.",
        "Hover tags only appear for a mouse; touch shows the same diagram without them."
      ],
      "performance": [
        "About forty SVG elements move, from a single loop that caches every written value.",
        "Paths are sampled once per layout into a small length table; nothing is measured per frame."
      ],
      "aiNotes": [
        "Use it to explain a pipeline on a landing page or in docs: ingestion, ETL, event routing, sync.",
        "Keep labels to one or two words and put the product in meta, such as Database with Supabase.",
        "Use real brand logos only where the product matters to the story; keep generic roles on lucide icons.",
        "Use share on destinations that receive a subset, such as alerts, so the fan out tells the truth."
      ],
      "related": [
        "sankey-flow",
        "orbit-logos",
        "realtime-stream"
      ],
      "usage": "import { DataFlow } from \"@/components/arc/data-flow/data-flow\";\nimport { AppWindow, LayoutDashboard, Warehouse } from \"lucide-react\";\n\n// Image URLs keep full brand color; lucide icons follow the muted text color.\nconst sources = [\n  { label: \"App\", meta: \"Web and iOS\", icon: <AppWindow /> },\n  { label: \"Database\", meta: \"Supabase\", icon: \"/logos/supabase-color.svg\" },\n  { label: \"Events\", meta: \"Stripe webhooks\", icon: \"/logos/stripe-color.svg\" },\n];\nconst destinations = [\n  { label: \"Warehouse\", meta: \"Every record\", icon: <Warehouse /> },\n  { label: \"Dashboard\", meta: \"Live metrics\", icon: <LayoutDashboard /> },\n  { label: \"Alerts\", meta: \"Slack\", icon: \"/logos/slack-color.svg\", share: 0.34 },\n];\n\nexport function PipelineHero() {\n  return <DataFlow sources={sources} destinations={destinations} core={{ label: \"Transform\" }} />;\n}",
      "exports": [
        "DataFlow"
      ]
    },
    {
      "name": "payment-flow",
      "kind": "component",
      "title": "Payment flow",
      "description": "A card tap goes to a processor, then the network, then the bank and back with authorization, then settlement; each hop lights up with a status label and the amount travels as a chip.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/payment-flow",
      "markdown": "https://uiarc.dev/components/payment-flow/markdown",
      "registry": null,
      "keywords": [
        "payment",
        "card payment",
        "authorization",
        "settlement",
        "payout",
        "processor",
        "card network",
        "issuer",
        "fees",
        "animated diagram",
        "illustration"
      ],
      "whenToUse": [
        "A payments or checkout landing section that explains authorization and settlement.",
        "Docs pages introducing processors, card networks, issuers, fees, and payout timing."
      ],
      "whenNotToUse": [
        "Use checkout-flow or checkout-live for a real checkout form.",
        "Use webhook-fanout to explain what happens after a payment event is emitted."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The diagram is one element with role=\"img\" and an aria-label that tells the whole story: the amount, every hop, the approval, the fee, and the payout.",
        "Nodes are not controls, so nothing enters the tab order; hover tags only repeat optional detail.",
        "State never relies on color alone: every node's status line reads Checking risk, Routed to bank, Approved, Cleared, Fee $1.53, or Settled, and the approval chip carries a check and a word.",
        "Pass caption for a visible explanation under the diagram.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop writes transforms, opacity, dash values, a few attributes, and text nodes straight to the DOM, and only when a value changed; React does not render per frame.",
        "The payment is a pure function of time, so the loop repeats seamlessly: tap, authorize, approve, settle, pay out, rest. The balance keeps growing from loop to loop instead of jumping back.",
        "The amount eases from node to node and slips under each chip while it is processed, so every hop reads as enter, work, leave. The path lights in the accent behind it and turns to success as the approval travels back.",
        "At the processor waypoint the settlement chip crossfades from the gross to the net amount, which is the fee shown as cause and effect; the balance then counts up with tabular numerals.",
        "Speed changes, pausing, and hover (which slows the story a little) ease on a critically damped spring. The loop stops off screen, in hidden tabs, and when paused.",
        "Reduced motion shows a still frame just after the fee: approved at checkout, the network cleared, the bank settled, and the net amount on its way to the balance."
      ],
      "responsive": [
        "The diagram is measured in real pixels and drawn in an SVG viewBox of that size, so text and hairlines stay crisp at any width.",
        "Below 700px it turns vertical: the four hops stack on the left and the balance sits top right, with the settlement rail running up beside the chain.",
        "The step row stacks into one column below 420px. Hover tags only appear for a mouse."
      ],
      "performance": [
        "About forty small elements driven by a single loop that caches every written value.",
        "Paths are sampled once per layout into small length tables; nothing is measured per frame."
      ],
      "aiNotes": [
        "Use it to explain how a card payment works, why payouts arrive later than approvals, or where fees come from, on a landing page or in docs.",
        "Match the fee and amount to the product's real pricing so the diagram tells the truth, and swap the processor and network marks for the ones the product uses.",
        "Keep node labels to one or two words; the status lines carry the story."
      ],
      "related": [
        "data-flow"
      ],
      "usage": "import { PaymentFlow } from \"@/components/arc/payment-flow/payment-flow\";\n\nexport function PaymentsSection() {\n  return <PaymentFlow\n    amount={42.5}\n    fee={1.53}\n    balance={1284.1}\n    currency=\"USD\"\n    caption=\"Approved in about a second, paid out the next business day.\"\n  />;\n}",
      "exports": [
        "PaymentFlow"
      ]
    },
    {
      "name": "prize-wheel",
      "kind": "component",
      "title": "Prize wheel",
      "description": "A referral or promo prize wheel you flick to spin, with real angular momentum, a flexible pointer that ticks against each peg, weighted segments set by the host, a slow suspenseful settle, and the won segment lifting into a reward card with a copyable code.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "gesture",
        "physics",
        "new"
      ],
      "docs": "https://uiarc.dev/components/prize-wheel",
      "markdown": "https://uiarc.dev/components/prize-wheel/markdown",
      "registry": null,
      "keywords": [
        "prize wheel",
        "spin the wheel",
        "wheel of fortune",
        "referral",
        "promo code",
        "coupon",
        "reward",
        "giveaway",
        "gamification",
        "spinner",
        "momentum",
        "physics"
      ],
      "whenToUse": [
        "A referral or launch page that rewards a signup with a discount code.",
        "An in-product moment that grants one of several perks, such as a milestone reward."
      ],
      "whenNotToUse": [
        "Use a plain coupon card or copy button when everyone gets the same reward.",
        "Avoid it in serious flows such as billing or checkout, where chance feels out of place."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The Spin hub is a real button with a specific name. Enter or Space pressed and held winds the wheel up; releasing spins it. A screen reader activation without a press spins at a medium strength.",
        "The wheel is one element with role=\"img\" whose label lists every prize with its chance, so the odds are never visual only.",
        "A polite live region announces Spinning, the prize won with its code spelled out, and copy success or failure.",
        "While a spin runs the hub is aria-disabled instead of disabled, so focus stays on it. The hidden reward card is inert until a prize lands.",
        "The winner is never shown by color alone: the card names it, the odds row lights up, and the announcement repeats it.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop writes the wheel rotation, the pointer rotation, and the charge ring straight to the DOM, only when a value changed; React renders only when a spin starts and when it lands.",
        "The wheel rotates as a composited HTML layer, so the SVG face never repaints per frame. A static metal sheen sits above it so the light stays still while the wheel turns under it.",
        "A drag follows the pointer angle around the hub; the release velocity from the last 100ms becomes the spin. The winner is drawn by weight, then the travel is chosen as the landing spot nearest the distance that velocity would naturally carry, so the wheel leaves your hand at the speed you threw it.",
        "The spin follows 1 - (1 - t)^3: velocity decays smoothly with no seam and the last turn crawls past the final pegs.",
        "The pointer is a stiff, lightly damped spring that collides with the pegs as a tapered capsule: it only moves when a peg actually touches it, bends about 30 degrees until the peg clears its tip, then snaps back with a short wobble. The collision runs in substeps that follow the wheel, so no peg skips through it at full speed; fast spins rattle it and the settle ticks peg by peg.",
        "A spin thrown from the hub or Spin again builds its speed over a few frames instead of jumping to full speed, and winding up eases in and out. A flick keeps the exact speed of your hand.",
        "The reward card flies from the segment under the pointer to its place on a spring with a little life, one continuous movement.",
        "The loop stops when idle, off screen, and in hidden tabs. Reduced motion removes the drift, the pointer wobble, and the flight: a spin lands at once on the prize and the card appears in place."
      ],
      "responsive": [
        "The wheel is an SVG viewBox inside a square stage, so the face, pegs, and pointer scale to any width with crisp hairlines.",
        "Container queries place the reward panel beside the wheel from 736px and below it on narrower layouts, where the odds list folds into two columns.",
        "The wheel captures touch for dragging while the rest of the component scrolls normally."
      ],
      "performance": [
        "About sixty static SVG elements rotate as one composited layer; nothing is measured per frame.",
        "The pointer spring runs four small substeps per frame and stops as soon as it rests."
      ],
      "aiNotes": [
        "Use it for a referral reward, a launch promo, or a signup incentive where the moment itself is the point.",
        "For real prizes, draw the result on the server and pass pickWinner so the browser cannot choose its own reward. Pair it with once.",
        "Keep labels short and the segment count between six and ten. Mark only the rarest prize as featured.",
        "Keep showOdds on for promotions that must disclose chances."
      ],
      "related": [
        "slot-text",
        "wallet-stack",
        "copy-button"
      ],
      "usage": "import { PrizeWheel } from \"@/components/arc/prize-wheel/prize-wheel\";\n\nexport function ReferralReward({ drawnIndex }: { drawnIndex: number }) {\n  return <PrizeWheel\n    once\n    pickWinner={() => drawnIndex}\n    segments={[\n      { label: \"10% off\", detail: \"Off your first year\", code: \"REF-10OFF\", weight: 40 },\n      { label: \"Free month\", detail: \"One month on us\", code: \"REF-MONTH\", weight: 25 },\n      { label: \"20% off\", detail: \"Off your first year\", code: \"REF-20OFF\", weight: 20 },\n      { label: \"Extra seat\", detail: \"One more teammate for a year\", code: \"REF-SEAT\", weight: 14 },\n      { label: \"Lifetime\", detail: \"Pro for life\", code: \"REF-LIFE\", weight: 1, featured: true },\n    ]}\n    onResult={segment => console.log(\"won\", segment.label)}\n  />;\n}",
      "exports": [
        "PrizeWheel"
      ]
    },
    {
      "name": "date-reel",
      "kind": "component",
      "title": "Date reel",
      "description": "A 3D wheel date and time picker (like iOS reels) with momentum scrolling, snapping, curved perspective, and full keyboard support.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "input",
        "date",
        "gesture",
        "new"
      ],
      "docs": "https://uiarc.dev/components/date-reel",
      "markdown": "https://uiarc.dev/components/date-reel/markdown",
      "registry": null,
      "keywords": [
        "date picker",
        "time picker",
        "wheel picker",
        "reel",
        "drum picker",
        "ios picker",
        "scroll picker",
        "momentum",
        "snap",
        "schedule send",
        "reminder",
        "birthday",
        "3d"
      ],
      "whenToUse": [
        "Scheduling a send, reminder or post where a phone-style wheel is familiar and faster than a calendar grid.",
        "Touch-first flows and mobile web, where flicking a reel beats typing a date.",
        "Birthdays and other dates far from today, using date mode with its month, day and year reels."
      ],
      "whenNotToUse": [
        "Use date-picker or date-range-picker when people need to see a whole month or pick a range.",
        "Use time-picker for a compact field inside a dense form.",
        "Use time-dial to compare times across cities."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The picker is a named group; each reel is a role=\"spinbutton\" with its own name (Day, Hour, Minute, AM or PM) and aria-valuetext that reads the entry, such as \"Fri, Sep 25\".",
        "A polite live region announces the full moment once the reels come to rest after a change, and again when the confirm button succeeds or fails.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The focused reel shows its entry in the accent color inside the lens, and quick picks use aria-pressed.",
        "Dates that do not exist, such as February 30, are dimmed on the reel and the day reel turns back to the last real day by itself.",
        "Drawn entries and the visual summary are hidden from assistive technology so a moving reel never floods the reader."
      ],
      "motion": [
        "One requestAnimationFrame loop runs only while a reel moves and writes transforms, opacity and text straight to recycled DOM nodes; React re-renders only when a value is committed.",
        "Each reel is projected onto a cylinder: entries rise on a sine curve, compress with the cosine of their angle and fade toward the edges, and a second copy clipped to the lens shows the crisp selected text.",
        "A flick keeps its measured velocity on an exponential glide that is solved to land exactly on an entry, then hands the last half row to a critically damped spring; drags past the ends stretch like rubber and bounce back.",
        "Keyboard steps, taps, wheel scrolling and quick picks all spin on the same spring, so pressing again mid-spin adds to the motion instead of restarting it.",
        "The loop stops when the picker leaves the viewport or the tab is hidden and resumes where it left off. With reduced motion every change lands at once and there is no intro."
      ],
      "responsive": [
        "A container query sets the row height and type size, so the reels fit a 320px phone and read larger on desktop without changing proportions.",
        "Reels size to their widest entry and sit centered under one lens that spans the full width.",
        "Below about 480px the confirm button moves under the quick picks at full width and 44px tall.",
        "Reels use touch-action pan-x, so a vertical swipe turns the reel and does not scroll the page."
      ],
      "performance": [
        "Eleven recycled nodes per reel (twice, for the lens), regardless of how many days or years the range holds.",
        "Text is written only when a node shows a new entry; the loop sleeps when every reel is at rest.",
        "Wheel scrolling is attached as a non-passive listener only on the reels."
      ],
      "aiNotes": [
        "Use it where choosing a moment is the task: schedule send, reminders, booking a slot, a birthday on sign up.",
        "Pass today and defaultValue as fixed dates for server rendering, then update today after mount if you need the real clock.",
        "onChange fires when the reels rest, so it is safe to save or validate there. For a confirm flow return the promise from onConfirm."
      ],
      "related": [
        "time-picker",
        "date-picker",
        "time-dial"
      ],
      "usage": "import { DateReel } from \"@/components/arc/date-reel/date-reel\";\n\nexport function SendLater({ onSchedule }: { onSchedule: (at: Date) => Promise<void> }) {\n  return (\n    <DateReel\n      title=\"Send later\"\n      today={new Date(2026, 8, 24)}\n      defaultValue={new Date(2026, 8, 25, 9, 0)}\n      onConfirm={onSchedule}\n    />\n  );\n}",
      "exports": [
        "DateReel"
      ]
    },
    {
      "name": "dns-resolution",
      "kind": "component",
      "title": "DNS resolution",
      "description": "A browser asks for a domain and the query hops from the resolver to a root server, the TLD server and the authoritative nameserver, each pointing to the next, until an IP address comes back and the page connects. The resolver keeps the answer with a TTL ring that drains, and a second lookup is answered straight from that cache in one short hop.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/dns-resolution",
      "markdown": "https://uiarc.dev/components/dns-resolution/markdown",
      "registry": null,
      "keywords": [
        "dns",
        "dns resolution",
        "dns lookup",
        "resolver",
        "root server",
        "tld",
        "nameserver",
        "authoritative",
        "ttl",
        "cache",
        "domain",
        "ip address",
        "isometric",
        "illustration",
        "animated diagram",
        "landing page",
        "docs"
      ],
      "whenToUse": [
        "A docs or marketing section that explains how a domain name turns into an address and why the second visit is faster.",
        "Custom domain setup, DNS hosting, resolver or CDN pages that need the chain of servers and the role of TTL to click.",
        "Teaching material where viewers should be able to step back and forth through each hop."
      ],
      "whenNotToUse": [
        "Use webhook-fanout to show one event delivered to many endpoints with retries.",
        "Use data-flow for a general pipeline between services.",
        "Use a real status or table component to report actual DNS records or propagation."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The drawing is role=\"img\" with an aria-label that tells the whole story: the chain of servers, the address, the connection time and that the next lookup comes from the cache. Pass label to write your own.",
        "The labels, moving record chips and hover tags inside the drawing are part of that image, and the narrated caption is hidden from assistive technology, so screen readers hear one stable description instead of a changing loop.",
        "The scrubber is a group of real buttons named Step 1 of 6 and so on; the current step carries aria-current=\"step\", arrow keys, Home and End move between steps, and the pause button reports its state with aria-pressed.",
        "Status is never color alone: every status chip pairs its dot with words such as Resolving, Try .dev, Cache hit or Connected in 84 ms."
      ],
      "motion": [
        "One requestAnimationFrame loop writes SVG attributes, transforms, opacity and text directly, and only when a value changed; React never re-renders per frame.",
        "Packets travel along real quadratic arcs at an even pace (arc length lookup) with a sine ease, a short lit trail and a shadow that slides along the floor and shrinks as the packet rises, which gives the 2.5D depth.",
        "Every frame is a pure function of one clock, so the counters, statuses, TTL ring and scrubber stay in sync, jumps land exactly, and the loop joins seamlessly when the cached record expires and the scene fades back to its start.",
        "Pausing, speed changes and the gentle hover slowdown ease on a critically damped spring. The loop stops offscreen and in a hidden tab and resumes where it left off.",
        "With reduced motion there is no travel or loop: the stage shows a settled frame of the cached state, and the scrubber switches between settled frames of each stage."
      ],
      "responsive": [
        "The drawing is one SVG with a viewBox and non-scaling one pixel strokes. At 640px and wider it uses a landscape stage; below that a portrait stage with the servers in a column beside the browser and resolver.",
        "Labels, status chips, the cached record and the moving record chip are HTML placed by percentage, so text stays at its real size at every width.",
        "Container queries hide the step names on phones (the caption names the current step) and the meta lines in the narrowest landscape range. Scrubber buttons keep a tall touch target."
      ],
      "performance": [
        "About a hundred small SVG nodes. Each frame writes only values that changed, and nothing runs offscreen or in a background tab.",
        "Geometry and arc length tables are built once per layout; resizing only rescales the moving record chip.",
        "No canvas, images, filters or blur."
      ],
      "aiNotes": [
        "Use it as an explanatory figure on a landing page, a docs page or a blog post about DNS, custom domains, resolvers, TTLs or caching.",
        "Keep the domain and address realistic but fictional; 203.0.113.0/24 is reserved for documentation. The lookup is a simulation and takes no live data.",
        "Only add a brand mark where it is true to the story, such as a public resolver. Brands are sample content, never an endorsement."
      ],
      "related": [
        "data-flow"
      ],
      "usage": "import { DnsResolution } from \"@/components/arc/dns-resolution/dns-resolution\";\n\nexport function DnsSection() {\n  return (\n    <DnsResolution\n      domain=\"app.acme.dev\"\n      address=\"203.0.113.24\"\n      ttl={300}\n      nodes={{ resolver: { label: \"Resolver\", meta: \"1.1.1.1\" } }}\n    />\n  );\n}",
      "exports": [
        "DnsResolution"
      ]
    },
    {
      "name": "model-training",
      "kind": "component",
      "title": "Model training",
      "description": "Training data batches feed a small neural net diagram whose connections strengthen, a loss curve descends beside it, and an evaluation check turns green at the end.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/model-training",
      "markdown": "https://uiarc.dev/components/model-training/markdown",
      "registry": null,
      "keywords": [
        "model training",
        "machine learning",
        "neural network",
        "deep learning",
        "backpropagation",
        "loss curve",
        "epochs",
        "evaluation",
        "fine-tuning",
        "AI",
        "animated diagram",
        "illustration"
      ],
      "whenToUse": [
        "An AI or ML product section that explains training, fine-tuning or evaluation in one glance.",
        "Docs pages introducing batches, epochs, loss curves or held-out evaluation."
      ],
      "whenNotToUse": [
        "Use line-chart or metric-card for real, live training metrics.",
        "Use data-flow to explain how records move through a pipeline, and pipeline-status for real job status."
      ],
      "dependencies": [
        "lucide-react"
      ],
      "accessibility": [
        "The diagram is one element with role=\"img\" and an aria-label that walks through the whole run: batches, the forward pass, backprop strengthening the connections, the loss falling over the epochs, and the held-out evaluation passing its target.",
        "The step bar is an ordered list of real buttons named \"Stage 3, Backprop\" with aria-current on the playing stage, and a play and pause button with a specific name. The caption repeats the story in words.",
        "Nothing relies on color alone: every readout is a number, the evaluation shows Waiting, Running or Passed as a named tag next to the score, and the winning prediction is a probability.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop writes SVG transforms, opacity, dash offsets, widths, CSS variables and a few text nodes straight to the DOM, and only when a value changed; React does not render per frame.",
        "The whole run is a pure function of time, so the loop is seamless: at the start of each run the connections relax back to their random starting weights while the readouts crossfade from the finished run to an untrained model.",
        "Batches fly from the stack into the input layer on an eased curve while the stack advances one slot, signals ride each connection with a smooth ease and fade at both ends, and the error travels back as a short dash. Nodes fill as a signal lands and ring as the error passes.",
        "Each backward pass strengthens the connections it crossed, so weights, the loss curve, the epoch count and the output probabilities all step together, with the loss head gliding along the curve during each update.",
        "Hover slows the story on a critically damped spring. The loop stops off screen, in hidden tabs, and when paused. Reduced motion shows the finished run as a still frame: learned connections, the full loss curve with its validation point, and the passed evaluation. Picking a stage then shows that stage's still frame."
      ],
      "responsive": [
        "The network is measured in real pixels and drawn in an SVG viewBox of that size, so text and hairlines stay crisp at any width.",
        "Above 720px the loss readout sits beside the network; below it moves under. Below 480px the two data stacks move above the network and batches drop into it, the output probabilities stack under their labels, and the step bar shows numbers while the caption names the stage.",
        "Hover details only appear for a mouse; touch shows the same diagram without them."
      ],
      "performance": [
        "About 150 small SVG elements for the default 3, 4, 4, 2 network, driven by a single loop that caches every written value.",
        "Geometry and weights are computed once per width; nothing is measured per frame."
      ],
      "aiNotes": [
        "Use it to explain how a model learns on a landing page or in docs: training data, forward and backward passes, epochs, loss and evaluation.",
        "Match classes, unit and the numbers to the product's real task, such as spam and not spam for emails. Keep class names to one word so they fit beside the output layer on phones.",
        "The numbers are illustrative. Say so near the illustration if they could be read as a real benchmark."
      ],
      "related": [
        "data-flow"
      ],
      "usage": "import { ModelTraining } from \"@/components/arc/model-training/model-training\";\n\nexport function TrainingSection() {\n  return <ModelTraining\n    classes={[\"Positive\", \"Negative\"]}\n    samples={12800}\n    heldOutSamples={1600}\n    epochs={12}\n    lossFrom={0.69}\n    lossTo={0.08}\n    accuracy={94.2}\n    target={90}\n  />;\n}",
      "exports": [
        "ModelTraining"
      ]
    },
    {
      "name": "multi-region-failover",
      "kind": "component",
      "title": "Multi-region failover",
      "description": "Animated multi-region failover on a 2.5D stage: users stream requests through a global load balancer to two regions; the primary degrades, fails three health checks, traffic shifts smoothly to the healthy region, then it recovers, passes three checks and traffic ramps back to an even split.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/multi-region-failover",
      "markdown": "https://uiarc.dev/components/multi-region-failover/markdown",
      "registry": null,
      "keywords": [
        "failover",
        "multi-region",
        "region",
        "high availability",
        "disaster recovery",
        "load balancer",
        "global load balancing",
        "dns failover",
        "health check",
        "traffic shift",
        "weights",
        "rebalance",
        "outage",
        "isometric",
        "illustration",
        "animated diagram",
        "landing page",
        "docs"
      ],
      "whenToUse": [
        "A reliability, infrastructure or pricing page that needs to show why running in two regions keeps a service up during an outage.",
        "Docs for health checks, failover policies, traffic weights or slow start, where viewers should step through detection, failover, recovery and rebalance.",
        "Incident write-ups and postmortems that explain how traffic moved during a regional outage."
      ],
      "whenNotToUse": [
        "Use database-replication to show a primary database failing over to a replica.",
        "Use circuit-breaker for one service failing fast against a failing dependency.",
        "Use autoscale for instances added and removed with load in one place.",
        "Use a real status or monitoring component to report actual region health."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The drawing is role=\"img\" with an aria-label that tells the whole story: the split, the failing region, the failed checks, the shift to the healthy region and the gradual return. Pass label to write your own.",
        "Labels, counters, the moving health check chip and hover tags inside the drawing are part of that image, and the narrated caption is hidden from assistive technology, so screen readers hear one stable description instead of a changing loop.",
        "The scrubber is a group of real buttons named Step 1 of 6 and so on; the current step carries aria-current=\"step\", arrow keys, Home and End move between steps, and the pause button reports its state with aria-pressed.",
        "Status is never color alone: every chip pairs its dot with words such as Failed checks 2/3, Out of rotation, Warming up 62% or Errors 27.3%, and failing instances also change their outline."
      ],
      "motion": [
        "One requestAnimationFrame loop writes SVG attributes, transforms, opacity and text directly, and only when a value changed; React never re-renders per frame.",
        "Requests are a deterministic stream: each one is routed by the balancer weight at the moment it reaches the balancer and lands on a specific instance, which flashes. Requests to a down instance stall, turn to errors and fade.",
        "Requests travel along real arcs at an even pace with a glide ease, so they gather at the balancer and fan out, with a shadow that slides along the floor and shrinks as they rise, which gives the 2.5D depth.",
        "Every frame is a pure function of one clock, so the weights, counters, checks, statuses and scrubber stay in sync, jumps land exactly, and the loop is seamless because the last frame is the balanced first frame.",
        "Pausing, speed changes and the gentle hover slowdown ease on a critically damped spring. The loop stops offscreen and in a hidden tab and resumes where it left off.",
        "With reduced motion there is no travel or loop: the stage shows a settled frame of the failover, and the scrubber switches between settled frames of each stage."
      ],
      "responsive": [
        "The drawing is one SVG with a viewBox and non-scaling one pixel strokes. At 640px and wider it uses a landscape stage with the regions stacked on the right; below that a portrait stage with users on top, the balancer in the middle and the two regions side by side.",
        "Labels, status chips, counters, the weight split and the moving health check chip are HTML placed by percentage, so text stays at its real size at every width.",
        "Container queries hide the step names on phones (the caption names the current step) and the meta lines in the narrowest landscape range. Scrubber buttons keep a tall touch target."
      ],
      "performance": [
        "About 130 small SVG nodes, with a fixed pool of 15 request dots reused across the stream. Each frame writes only values that changed, and nothing runs offscreen or in a background tab.",
        "Geometry and arc length tables are built once per layout; resizing only rescales the moving health check chip.",
        "No canvas, images, filters or blur."
      ],
      "aiNotes": [
        "Use it as an explanatory figure on a landing page, a docs page or a blog post about high availability, disaster recovery, global load balancing, DNS failover or health checks.",
        "Keep region names and codes realistic. The outage, traffic and timings are a simulation and take no live data.",
        "Only add a brand mark where it is true to the story, such as the load balancer provider. Brands are sample content, never an endorsement."
      ],
      "related": [
        "dns-resolution"
      ],
      "usage": "import { MultiRegionFailover } from \"@/components/arc/multi-region-failover/multi-region-failover\";\n\nexport function FailoverSection() {\n  return (\n    <MultiRegionFailover\n      hostname=\"api.acme.dev\"\n      requestsPerSecond={12000}\n      nodes={{\n        primary: { label: \"US East\", meta: \"us-east-1\" },\n        secondary: { label: \"EU West\", meta: \"eu-west-1\" },\n      }}\n    />\n  );\n}",
      "exports": [
        "MultiRegionFailover"
      ]
    },
    {
      "name": "vector-search",
      "kind": "component",
      "title": "Vector search",
      "description": "Documents drop onto a tilted embedding plane and fold into points that cluster by topic; the index links neighbours, a query is typed, embedded and lowered onto the plane, a signal hops along the graph toward it, and a widening ring lights the nearest documents with connecting lines, similarity scores and a ranked list.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/vector-search",
      "markdown": "https://uiarc.dev/components/vector-search/markdown",
      "registry": null,
      "keywords": [
        "vector search",
        "embeddings",
        "semantic search",
        "nearest neighbours",
        "similarity",
        "RAG",
        "retrieval",
        "vector database",
        "HNSW",
        "animated diagram",
        "illustration"
      ],
      "whenToUse": [
        "A feature section on semantic search, AI answers, or retrieval over a team's docs.",
        "Docs pages introducing embeddings, nearest neighbour search, or approximate indexes."
      ],
      "whenNotToUse": [
        "Use search-index to explain keyword search with an inverted index.",
        "Use a real scatter or cluster chart to show an actual embedding projection."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plane and its legend form one element with role=\"img\" and an aria-label that explains the idea with the real values: how many documents cluster into which topics, the query, how many hops the search takes, and each match with its similarity score.",
        "The step bar is a group of real buttons named for each stage, with aria-current on the playing stage, plus a named play and pause button.",
        "Nothing relies on color alone: clusters carry name chips, matches are written out as a ranked list with titles, sources, and scores, and the counters are plain numbers.",
        "Points are not controls, so nothing else enters the tab order; hover tags only repeat the title, topic, and source that the list and label already give.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop writes SVG transforms, opacity, dash lengths, and a few text nodes straight to the DOM, and only when a value changed; React does not render per frame.",
        "The whole story is a pure function of time, so the loop repeats seamlessly: pages fall and fold into points, clusters and links draw in, the query is typed and lowered, the signal hops, the ring finds the matches, and the plane clears.",
        "The signal hops along the real shortest route through the index graph, and each hop's duration scales with the link's length, so the pace stays even. Hops arc slightly above the plane.",
        "The ring widens on an eased curve, and each match lights at the exact moment the ring reaches it; the time is solved from the curve, so halo, line, score chip, and ranked row stay in sync.",
        "Speed changes, pausing, and hover (which slows the story a little) ease on a critically damped spring. Readout bars and result bars ease with the standard and considered durations.",
        "The loop stops off screen, in hidden tabs, and when paused. Reduced motion shows the answer as a still frame: every point placed, the path walked, the matches linked and scored; the step bar still jumps between still frames of each stage."
      ],
      "responsive": [
        "The plane is measured in real pixels and drawn in an SVG viewBox of that size, so hairlines and text stay crisp at any width.",
        "From 720px of container width the legend sits beside the plane; below it the legend drops under the plane. The story is the same.",
        "Cluster names sit above clusters at the back and below clusters at the sides, and score chips sit on the side of each match facing away from the query, so labels stay apart even at 390px.",
        "Hover tags only appear for a mouse; touch shows the same diagram without them."
      ],
      "performance": [
        "About 150 small SVG and HTML elements, driven by a single loop that caches every written value.",
        "Positions, the index graph, the search route, and every timing are computed once per layout; nothing is measured per frame."
      ],
      "aiNotes": [
        "Use it to explain semantic search, embeddings, RAG retrieval, or a vector database on a landing page or in docs.",
        "Keep titles short, like help center article names. Three topics with five or six documents each read best.",
        "Rankings and scores are computed from the positions on the plane, so they always match the picture. To make a specific document the top match, list it first in its topic or pass queryAt near it.",
        "Sources are sample content: use real brand marks only for the tools the documents actually come from."
      ],
      "related": [
        "model-training",
        "data-flow"
      ],
      "usage": "import { VectorSearch } from \"@/components/arc/vector-search/vector-search\";\n\nexport function RetrievalSection() {\n  return <VectorSearch\n    topics={[{ name: \"Billing\" }, { name: \"Access\" }, { name: \"Deploys\" }]}\n    documents={[\n      { title: \"Refund a charge\", topic: 0 },\n      { title: \"Update a card on file\", topic: 0 },\n      { title: \"Download invoices\", topic: 0 },\n      { title: \"Rotate an API key\", topic: 1 },\n      { title: \"Scope API tokens\", topic: 1 },\n      { title: \"Revoke a session\", topic: 1 },\n      { title: \"Roll back a deploy\", topic: 2 },\n      { title: \"Preview environments\", topic: 2 },\n      { title: \"Add a custom domain\", topic: 2 },\n    ]}\n    query=\"How do I rotate a leaked API key?\"\n    k={3}\n  />;\n}",
      "exports": [
        "VectorSearch"
      ]
    },
    {
      "name": "agent-timeline",
      "kind": "component",
      "title": "Agent timeline",
      "description": "A compact timeline of one orchestrated agent run: the orchestrator and its subagents share a time axis, handoffs draw in between rows, and a playhead replays thinking, tool calls and waiting while the header counts time, tokens and cost. Hover a span for what it did, or click it to trace its handoff path.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-timeline",
      "markdown": "https://uiarc.dev/components/agent-timeline/markdown",
      "registry": null,
      "keywords": [
        "agent timeline",
        "multi-agent",
        "orchestrator",
        "subagents",
        "agent trace",
        "gantt",
        "handoff",
        "tool calls",
        "run replay",
        "playhead",
        "scrubber",
        "tokens",
        "cost",
        "parallel agents",
        "ai",
        "llm"
      ],
      "whenToUse": [
        "Showing how an orchestrator split work across subagents and how long each part took, in a run history, a trace viewer or a debugging panel.",
        "Replaying a finished agent run in a product demo or changelog, with real numbers for time, tokens and cost.",
        "Following a multi-agent run live as events stream in from your backend."
      ],
      "whenNotToUse": [
        "Use agent-orchestrator for a full screen of agent lanes with streaming text and messages; this timeline is the compact, reusable view of timing.",
        "Use observability-trace for distributed request tracing across services rather than agents.",
        "Use agent-run or research-run when people need the agent's steps, sources and answer, not the timing.",
        "Use tool-call for the detail of a single tool invocation, and agent-status for one agent's current state."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a labelled section. Each row of spans is a group named by its agent label, and every span is a button whose name reads the agent, the kind, the tool, failure, what it did, its time range and tokens; aria-pressed marks the pinned span.",
        "Spans use a roving tab index, so the chart is one tab stop and arrow keys move between spans by row and by time.",
        "The scrubber is a slider with a value text such as \"12.4s of 26.4s\" and full keyboard support; dragging anywhere on the axis moves it.",
        "Status is never color alone: a failed call shows a cross icon and the word Failed in its card and the legend, a retry shows a retry icon and its note, and the finished state shows a check with the summary.",
        "A polite live region announces replay, pause and the finished summary. The counting status line is not live, so it never floods a screen reader.",
        "The span card repeats what the span's accessible name already says, so it is hidden from assistive technology.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring on spans, the scrubber thumb and the controls."
      ],
      "motion": [
        "Bars and the playhead read one CSS variable for the current time, updated every animation frame, so spans grow smoothly without React rendering per frame; React updates counters and states about twenty times a second.",
        "An active span has a square, solid leading edge and a short shine that stops when it ends; its corner rounds off as it completes. A failed call changes to its failed look with a quick color fade when it returns.",
        "Connectors draw in with a clip reveal from the side the handoff leaves, about 360ms, and the port dot pops in at the start of a dispatch or after a result lands.",
        "The span card glides between spans on the morph spring, crossfades its content and springs to its new height; the status line and play button swap with one minimal crossfade.",
        "When the run ends the playhead fades out and the status settles into the summary.",
        "Reduced motion starts on the finished run and drops the shine, the connector draw, the card glide and the status crossfades. Playback still works when the person asks for it."
      ],
      "responsive": [
        "Fills its container up to the canvas width. Below a 520px container the label column narrows to dot and name, and the model moves into the span card.",
        "When the plot would be narrower than 300px it scrolls horizontally inside its own frame with the agent labels pinned, and the replay keeps the playhead in view unless the person has just scrolled.",
        "The status line wraps after a separator, never before, and the counters reserve the width of their final value, so the header never shifts while it counts.",
        "Hover cards are anchored inside the component and flip below the row when there is no room above."
      ],
      "performance": [
        "The run is normalized once per data change: spans sorted, connector shapes and totals memoized.",
        "Per frame the clock writes one CSS variable; widths, the playhead and connectors are pure CSS from it. React re-renders at most about twenty times a second while playing, and memoized span buttons only update when their state changes.",
        "The replay clock only runs while the timeline is in view, and stops when the run ends.",
        "Handles a few hundred spans comfortably; for long traces, pass a window of the run or aggregate tiny spans."
      ],
      "aiNotes": [
        "Choose it to show one orchestrated multi-agent run as a timeline: which agent did what, when, in parallel, with handoffs. It fits a card 360 to 760px wide.",
        "Put the orchestrator first. Order subagents so connectors pass through idle rows: the agent that returns first sits right under the orchestrator, and dispatches go to the deepest row first.",
        "Give each handoff a time that matches the spans around it: a dispatch just before the target's first span, a result just after the source's last span. The shape is inferred from those gaps.",
        "Use kind \"waiting\" for an agent that is blocked on others, so parallelism and idle time read at a glance and the summary's speedup counts only real work.",
        "Mark a failed tool call with status \"failed\" and a detail message, and the call that retried it with retry: true.",
        "Real model and tool marks are built in by name; pass mark for anything else. Brand marks are sample content, never implied endorsements."
      ],
      "related": [
        "agent-orchestrator",
        "agent-run",
        "research-run",
        "tool-call",
        "agent-status",
        "agent-plan",
        "context-window"
      ],
      "usage": "import { AgentTimeline } from \"@/components/arc/agent-timeline/agent-timeline\";\n\n// The sample run, replaying.\n<AgentTimeline />\n\n// A finished run from your traces renders complete, ready to replay.\n<AgentTimeline\n  run={{\n    title: \"Weekly churn report\",\n    agents: [\n      { id: \"lead\", name: \"Orchestrator\", model: \"Claude Opus 5.5\", price: 8 },\n      { id: \"sql\", name: \"Analyst\", model: \"Claude Haiku 5.5\", price: 0.2 },\n    ],\n    spans: [\n      { agentId: \"lead\", kind: \"thinking\", start: 0, end: 1.4, label: \"Planned the report\", tokens: 1200 },\n      { agentId: \"lead\", kind: \"waiting\", start: 1.4, end: 6.2, label: \"Waiting on the analyst\" },\n      { agentId: \"sql\", kind: \"tool\", tool: \"PostgreSQL\", start: 1.7, end: 4.1, label: \"Pulled churn by plan\", tokens: 2400 },\n      { agentId: \"sql\", kind: \"thinking\", start: 4.1, end: 5.9, label: \"Summarized the numbers\", tokens: 900 },\n    ],\n    handoffs: [\n      { from: \"lead\", to: \"sql\", at: 1.4, label: \"Churn by plan, last 8 weeks\" },\n      { from: \"sql\", to: \"lead\", at: 6.2, label: \"Churn is up on the Team plan\" },\n    ],\n  }}\n  onSpanSelect={span => console.log(span?.label)}\n/>\n\n// Live: keep appending events to the run, leave end off open spans.\n<AgentTimeline run={liveRun} live={status === \"running\"} />",
      "exports": [
        "AgentTimeline"
      ]
    },
    {
      "name": "typewriter-terminal",
      "kind": "component",
      "title": "Typewriter terminal",
      "description": "A terminal that types a scripted CLI session with a seeded human cadence, colors the shell syntax, and streams output lines, progress bars and spinners, with a scrubber, play/pause and replay controls.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "terminal",
        "new"
      ],
      "docs": "https://uiarc.dev/components/typewriter-terminal",
      "markdown": "https://uiarc.dev/components/typewriter-terminal/markdown",
      "registry": null,
      "keywords": [
        "terminal",
        "typewriter",
        "cli",
        "install",
        "command line",
        "shell",
        "typing animation",
        "npm",
        "developer tools",
        "replay",
        "scrubber"
      ],
      "whenToUse": [
        "A landing page hero or install section for a developer tool, SDK, or CLI.",
        "Docs quick starts that show what each command prints before people run it.",
        "Changelog or launch posts that show a new command in action."
      ],
      "whenNotToUse": [
        "Use code-block for commands and source that people need to read and copy without waiting.",
        "Use log-stream for real, live output from a running process.",
        "Use code-magic-move to show how a file changes between versions."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a figure named by label. The animated screen is hidden from assistive technology, and a visually hidden ordered list gives the full transcript: every command and what it printed.",
        "The scrubber is a real slider with role=\"slider\", its value in seconds, and a value text that names the current step and command. Arrow keys and Page keys jump to the previous or next command, Home and End go to the start and end, and Space or K plays and pauses.",
        "Play, pause, and replay are real buttons with specific names. Seeking, pausing, and replaying are announced in a polite live region. Autoplay is not announced, so the loop never floods a screen reader.",
        "Status never relies on color alone: success, warning, and danger lines carry an icon, tasks show a tick, and progress shows a percentage.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One rAF loop derives the whole screen from a single session time and writes attributes, text, transforms, and opacity straight to the DOM, only when a value changed. React does not render per frame.",
        "Typing follows a seeded human cadence: a short think before the first key, quick bursts inside words, small stalls on shifted keys and word breaks. The caret stays solid while keys land and blinks softly while it waits.",
        "Each output line rises a few pixels and fades in. Progress bars fill with a quick start, a stall around the middle, and a steady finish, with a tabular percentage. Spinners cycle braille frames and resolve to a tick.",
        "The screen follows the newest line on a critically damped spring, and only measures when the visible text changes. The top edge feathers lines away as they scroll off.",
        "Scrubbing is direct: the session follows the pointer exactly, text untypes and retypes, and a release within a few pixels of a command start settles onto it. Keyboard jumps and replay glide through time on an ease in out curve, so replay visibly rewinds the session.",
        "Play and pause ease the playback rate on a spring instead of stopping in one frame. The loop clears upward while the scrubber glides back, so it never jumps. The loop stops off screen and in hidden tabs. With reduced motion the finished session shows at once, the caret stops blinking, and the scrubber still jumps between steps."
      ],
      "responsive": [
        "The window fills its container and keeps a fixed height in rows. A container query drops the text to the small size and tightens padding below 560px, and hides the time readout below 400px.",
        "Long commands and output wrap character by character like a real terminal, and the screen still follows the newest line.",
        "The scrubber and buttons keep 44px touch targets at every width."
      ],
      "performance": [
        "The session is compiled once per script into timings. Each frame only compares cached values, so an idle frame writes nothing.",
        "The loop rests when playback is paused and settled, off screen, and in hidden tabs."
      ],
      "aiNotes": [
        "Use it for install instructions, CLI marketing, and quick starts where seeing the commands run sells the product.",
        "Keep commands real and copyable, since the copy button copies them verbatim. Keep each command under about fifty characters so it fits one line on wide screens; longer ones wrap.",
        "Two to four steps with two to four output lines each read best. Use tasks and progress lines for work that takes time, and one success line to end each step.",
        "Everything shown is a simulation. Say so if it could be read as a live session."
      ],
      "related": [
        "code-block",
        "hero-section"
      ],
      "usage": "import { TypewriterTerminal } from \"@/components/arc/typewriter-terminal/typewriter-terminal\";\n\nexport function InstallHero() {\n  return (\n    <TypewriterTerminal\n      title=\"Install\"\n      steps={[\n        {\n          command: \"npm install @acme/sdk\",\n          output: [\n            { kind: \"progress\", label: \"Fetching packages\", duration: 1.2 },\n            { text: \"Added 12 packages in 1.8s\", tone: \"success\" },\n          ],\n        },\n        {\n          command: \"npx acme login\",\n          output: [\n            { kind: \"task\", label: \"Opening the browser\", done: \"0.3s\" },\n            \"Signed in as dana@acme.dev\",\n          ],\n        },\n      ]}\n      rows={8}\n    />\n  );\n}",
      "exports": [
        "TypewriterTerminal"
      ]
    },
    {
      "name": "context-window",
      "kind": "component",
      "title": "Context window",
      "description": "An AI context window as a grid of 200 cells: the system prompt, tools, files, summary and messages fill it in order, a threshold row marks where compaction starts, and sending a message, attaching a file or compacting shows exactly how much still fits.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "tokens",
        "chart",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/context-window",
      "markdown": "https://uiarc.dev/components/context-window/markdown",
      "registry": null,
      "keywords": [
        "context window",
        "token usage",
        "llm context",
        "compaction",
        "summarize conversation",
        "prompt tokens",
        "mcp tools tokens",
        "output reserve",
        "ai session",
        "token budget"
      ],
      "whenToUse": [
        "Showing developers or power users what fills a model's context and when it will compact.",
        "Agent and coding tools that compact long sessions and want the effect to be understandable.",
        "Docs and onboarding that explain context windows, compaction and output reserves."
      ],
      "whenNotToUse": [
        "A simple quota or plan usage bar; use usage-meter.",
        "Choosing a model; use model-picker.",
        "Billing or spend over time; use usage-billing."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The grid is one image with a full text description: model, tokens used, every category with its tokens, and the threshold.",
        "The legend is a list with tokens and percent for each part, so nothing depends on color.",
        "Every send, attachment, model switch and compaction is announced through a polite status line with an icon and text.",
        "The model menu is a Radix dropdown with radio items; the trigger names the model and its window.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared ring."
      ],
      "motion": [
        "New tokens fill cells in a short wave from where the change happened, about 4 to 9ms per cell and never more than 360ms in total.",
        "Compaction dims the messages in one sweep from oldest to newest, then the summary lands right after the files and the freed cells turn back to free space in the same wave.",
        "Switching models re-scales every cell in one quick pass from the first cell.",
        "Token counts and percentages roll to their new values; status lines crossfade in place.",
        "Pointing at a legend row lights its cells and quiets the rest.",
        "Reduced motion changes cells and numbers at once and keeps every status line."
      ],
      "responsive": [
        "On wide containers the legend sits beside the grid; below 640px it moves under the grid and the cells tighten.",
        "Below 420px the header stacks, with the model menu under the token readout.",
        "Cells are square and fluid, so the grid always fills its column without horizontal scroll."
      ],
      "performance": [
        "200 cells are plain spans whose color changes through CSS transitions with a per-cell delay; nothing re-renders per frame except the rolling numbers.",
        "The layout is recomputed only when the items or the model change."
      ],
      "aiNotes": [
        "Feed it the real token usage of a session: system prompt, tool definitions (for example MCP servers), attached files and memory, any summary and the messages.",
        "Keep reserve equal to the max output you request, so the grid shows the space the reply needs.",
        "Use onCompact to run your own summarization when the threshold is crossed; pass summarize to report the real summary size.",
        "The 200 cell grid is fixed, so each cell is window / 200 tokens; the note under the grid says how many."
      ],
      "related": [
        "model-picker",
        "ai-composer",
        "agent-run",
        "usage-meter",
        "ai-chat"
      ],
      "usage": "import { ContextWindow } from \"@/components/arc/context-window/context-window\";\n\nexport function SessionContext({ items }) {\n  return (\n    <ContextWindow\n      items={items}\n      threshold={0.8}\n      onCompact={({ folded, summary }) => console.log(`Folded ${folded} tokens into ${summary}`)}\n    />\n  );\n}",
      "exports": [
        "ContextWindow"
      ]
    },
    {
      "name": "agent-loop",
      "kind": "component",
      "title": "Agent loop",
      "description": "An illustration of the agent loop: gather context, take action, verify, repeat, with a bright head circling three stations around the model.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-loop",
      "markdown": "https://uiarc.dev/components/agent-loop/markdown",
      "registry": null,
      "keywords": [
        "agent loop",
        "ai agent",
        "tool use",
        "tool calling",
        "gather context",
        "take action",
        "verify",
        "agentic loop",
        "react loop",
        "observe act",
        "context window",
        "claude",
        "illustration",
        "explainer",
        "ai diagram"
      ],
      "whenToUse": [
        "Docs, explainers and landing pages that teach how an AI agent works: it gathers context, calls tools, checks the result and repeats until the task is done.",
        "Showing why agents take several passes, with a failed check or a failed tool call that sends the loop round again.",
        "A visual summary of a finished agent run with the tools it used and the context it built up."
      ],
      "whenNotToUse": [
        "Use agent-run or research-run to show a real agent's live steps and streamed output.",
        "Use tool-call for a single tool call card inside a chat or a log.",
        "Use reasoning-tree to explain search over thoughts rather than tool use.",
        "Use mcp-flow to explain how a host talks to MCP servers over a transport.",
        "Use context-window to show what fills a model's context and how close it is to the limit."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is a labelled group. Station names, tool calls, results, checks and context blocks are real text, and the ring drawing itself (track, pins, head) is hidden from assistive technology.",
        "A polite status region speaks each beat when you step or pause (for example \"GitHub returned: EU cards, since Oct 18\"), and the summary at the end, but stays quiet while the loop plays on its own.",
        "Status never relies on color: ready, running, paused, done and stopped each pair an icon with a label; a tool result shows a check or a cross, a failed call says failed to screen readers, and a check shows a check icon with \"Check passed\" or a rotate icon with \"Retry\".",
        "The context list is an ordered list, read in the order blocks were gathered.",
        "When stepping reaches the end, Next step turns off and focus moves to Restart instead of falling back to the page.",
        "Pointer focus draws no ring; keyboard focus uses Arc's shared focus outline on every control."
      ],
      "motion": [
        "The head eases from station to station round the ring and drags a soft gradient trail that stretches with its speed and shrinks to nothing when it lands, so it never sits still with a tail. Small chevrons on the track show the direction.",
        "The active station's node fills, a thin ring settles around it and its label carries a calm shimmer that stops with a clear solid state.",
        "A tool call drops out of the ring: the pin under Take action draws down in about 180ms, then the chip lands at its end with a 6px drop and a slight scale on a critically damped spring (no bounce), growing from the pin. Its spinner crossfades to a check or a cross as the result fades in. A second call crossfades into the same lane after the old one fades out in 80ms, so nothing reflows. Checks drop from Verify the same way.",
        "A new context block fades in at the end of the stack with a short drop while the token count rolls up in tabular numerals; the space for every block is reserved, so the card never grows.",
        "The loop counter rolls its digit up inside a fixed width. Labels such as the status and the caption swap with one short crossfade.",
        "At the end the ring draws in clockwise in about 760ms and the head fades out. A stopped run leaves the ring neutral.",
        "Reduced motion opens on the done state. Playing and stepping still work, with no travel and no trail: the head jumps between stations and each change applies in place."
      ],
      "responsive": [
        "From a 640px container the context stack sits in a column beside the ring, with the check and the tool call side by side in two lanes, each at the end of its station's pin.",
        "Narrower, the context stack moves under the ring (two columns between 460 and 640px) and one lane shows the latest tool call or check.",
        "The ring is laid out in real pixels from the measured width, so strokes and labels stay crisp at every size. Below a 360px ring column the labels drop their icons and shrink, and on the narrowest cards the two lower stations move further down the ring so their labels fit beside them, which holds down to 320px.",
        "The speed label hides below a 400px container and the controls wrap onto two lines on the narrowest phones."
      ],
      "performance": [
        "The whole run is a pure timeline built once from the iterations, so pausing, stepping and replaying are exact.",
        "The head and the trail are written straight to the SVG from one requestAnimationFrame loop that runs only while playing or stepping; React renders once per beat, not per frame.",
        "The trail is one arc path with a gradient whose ends follow the tail and the head; everything else animates transform, opacity or a stroke dash.",
        "Autoplay waits until the card is in view."
      ],
      "aiNotes": [
        "Use it to explain how a tool using agent works (gather context, act, verify, repeat), not to stream a real agent's output. Say in the surrounding copy that the run is illustrative.",
        "Write two to four iterations. Give each a short context label (one to three words), a detail under about 30 characters, a tool call action of two to four words and a result under about 30 characters, so nothing truncates in the context column or the chips. The context stack reserves a slot per iteration.",
        "Let the last iteration pass its check; earlier ones can fail with a retry note to show why the loop goes round again. Set status: \"error\" on a call to show a failed tool call followed by a retry in the same step.",
        "Copy brand marks from agent-loop-marks.tsx or pass any ReactNode with aria-hidden. Mono marks follow the text color; set style={{ color }} for a brand color. Brand marks are sample content, never an implied endorsement.",
        "The card is a single focused component about 760px wide at most; it puts the context stack beside the ring from a 640px container and under it below that."
      ],
      "related": [
        "reasoning-tree",
        "agent-run",
        "tool-call",
        "agent-plan",
        "context-window",
        "text-shimmer"
      ],
      "usage": "import { AgentLoop } from \"@/components/arc/agent-loop/agent-loop\";\nimport { GithubMark, SlackColorMark } from \"@/components/arc/agent-loop/agent-loop-marks\";\n\nexport function DeployLoop() {\n  return (\n    <AgentLoop\n      task=\"Fix the failing deploy on main and tell #releases.\"\n      summary=\"The build cache was stale. Cleared it, the deploy passed and #releases knows.\"\n      onDone={() => console.log(\"loop finished\")}\n      iterations={[\n        {\n          gather: { label: \"Build log\", detail: \"Step 4 of 9 failed\", tokens: 3200 },\n          calls: [{ tool: \"GitHub\", mark: <GithubMark aria-hidden />, action: \"Read workflow run\", result: \"Cache key mismatch\" }],\n          verify: { passed: false, note: \"Fix not applied yet\" },\n        },\n        {\n          gather: { label: \"Workflow file\", detail: \"deploy.yml, 84 lines\", tokens: 1900, mark: <GithubMark aria-hidden /> },\n          calls: [\n            { tool: \"GitHub\", mark: <GithubMark aria-hidden />, action: \"Clear cache and rerun\", result: \"Deploy passed in 3m 12s\" },\n            { tool: \"Slack\", mark: <SlackColorMark aria-hidden />, action: \"Post update\", result: \"Posted in #releases\" },\n          ],\n          verify: { passed: true, note: \"Deploy green, team told\" },\n        },\n      ]}\n    />\n  );\n}",
      "exports": [
        "AgentLoop"
      ]
    },
    {
      "name": "mixture-of-experts",
      "kind": "component",
      "title": "Mixture of experts",
      "description": "An illustration of mixture of experts routing: each token is scored by a router, travels to its top experts, and their outputs merge into the token's result.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/mixture-of-experts",
      "markdown": "https://uiarc.dev/components/mixture-of-experts/markdown",
      "registry": null,
      "keywords": [
        "mixture of experts",
        "moe",
        "sparse moe",
        "expert routing",
        "gating network",
        "router",
        "top k",
        "top 2 routing",
        "active parameters",
        "load balancing",
        "expert capacity",
        "token dropping",
        "llm architecture",
        "transformer",
        "illustration"
      ],
      "whenToUse": [
        "Explaining why a mixture of experts model is cheap per token: only the chosen experts run, which the active parameter readout makes concrete.",
        "Showing how a gate scores experts, picks the top k and blends their outputs, with real or sample scores.",
        "Teaching load balancing and expert capacity, where an uneven router overloads some experts and drops tokens."
      ],
      "whenNotToUse": [
        "Use model-router to pick which model handles a request in a product; this card explains routing inside one model.",
        "Use attention-map to show which tokens a token attends to, rather than which experts process it.",
        "Use token-inspector to show tokenization, token ids and probabilities of generated text.",
        "Use speculative-decoding to explain draft and verify generation speedups."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is a labelled group. The stage lines, router bars and packets are decorative and hidden from assistive technology; every fact they show is also on a focusable element or in text.",
        "Each token is a button whose name lists its experts and weights, such as \"paid: Verbs 68%, Symbols 32%\", and says when an expert skipped it at capacity. Pinning sets aria-pressed.",
        "Each expert tile is a button named with its load, whether it is at capacity, and its role in the route on screen.",
        "The rolling numbers are hidden from assistive technology and paired with plain text, such as \"37B of 671B parameters active per token\" and the load balance in percent.",
        "First and second choices are told apart by order and label as well as color, and a skipped expert shows a warning icon, a dashed border and the word full, never color alone.",
        "Pausing, resuming, pinning, the top k change and replay are announced through a polite status region. Autoplay itself is not announced.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One clock drives the story: the input chip slides in, a packet with a short soft trail travels to the router, the score bars grow, packets travel to the chosen experts, the tiles light up and their counters roll, and the packets curve into the merge point and out to the output chip.",
        "Packets are drawn straight to the DOM on requestAnimationFrame; React only re-renders when a phase changes. Travel uses an in-out curve and an arc length table so dots and trails move evenly on curves.",
        "Score bars, tile tints and connector colors are CSS transitions on transform and color. Load counters, the active parameter readout and the balance roll on tabular numerals with the smooth, critically damped spring.",
        "Hover, keyboard focus, a pin, scrolling out of view and the pause button all stop the clock exactly where it is, and it resumes from there.",
        "Footer text swaps with a short crossfade: the old line fades out in about 100ms while the new one fades in from 2px below.",
        "Reduced motion drops all travel and the loop: each token's scores, experts and output land in place on a slower cadence, and the card stops on its summary."
      ],
      "responsive": [
        "From about 568px wide the card flows left to right: input, router, a column of experts with names and counters, then output.",
        "Narrower cards flow top to bottom: the router becomes a band of vertical score bars, each above its expert, and the experts become a row of icon tiles with their counters.",
        "The header wraps so the readout takes its own row, the route readout drops the token chip, and the footer stacks the load balance under it. Everything holds at 320px without horizontal scroll.",
        "Touch has no hover, so tapping a token or an expert pins it and tapping again resumes."
      ],
      "performance": [
        "Routes, loads and drops for the whole sequence are planned once per top k with plain arithmetic and memoized.",
        "Per frame work is a handful of attribute writes on six packet groups; nothing re-renders between phase changes.",
        "The clock stops while the card is offscreen, paused, inspected or finished, and the geometry is rebuilt only when the stage width changes."
      ],
      "aiNotes": [
        "Choose it to explain how a sparse mixture of experts layer routes tokens: on a model page, in docs about a model's architecture, or in a lesson on LLM internals. It is an illustration, not a profiler.",
        "Keep expert names to one word. Narrow cards show only each expert's icon, so pass an icon for custom experts or accept the first letter.",
        "Scores do not need to sum to 1. The top k are taken by score with ties kept in expert order, and their weights are renormalized for the readout.",
        "capacityFactor is what produces the warning state. Leave it at 1 to show a couple of overflows in the sample, raise it to 1.25 or more for a clean run, or pass Infinity to turn dropping off.",
        "The default parameter counts are illustrative. If the page names a real model, pass that model's own total, shared and per expert sizes and say where they come from.",
        "Use onRoute to sync something else on the page, such as highlighting the token in a transcript."
      ],
      "related": [
        "speculative-decoding",
        "agent-loop",
        "vector-search",
        "context-window"
      ],
      "usage": "import { MixtureOfExperts } from \"@/components/arc/mixture-of-experts/mixture-of-experts\";\n\nexport function RoutingExplainer() {\n  return <MixtureOfExperts />;\n}\n\n// Your own sequence and experts. Scores are gate weights in expert order.\nconst experts = [\n  { name: \"Code\", handles: \"Identifiers and keywords\" },\n  { name: \"Math\", handles: \"Numbers and operators\" },\n  { name: \"Prose\", handles: \"Everyday words\" },\n  { name: \"Names\", handles: \"People and places\" },\n];\n\nexport function CustomRouting() {\n  return (\n    <MixtureOfExperts\n      experts={experts}\n      tokens={[\n        { text: \"const\", scores: [0.7, 0.05, 0.15, 0.1] },\n        { text: \" total\", scores: [0.4, 0.3, 0.25, 0.05] },\n        { text: \" =\", scores: [0.35, 0.55, 0.05, 0.05] },\n      ]}\n      defaultTopK={1}\n      capacityFactor={Infinity}\n      parameters={{ total: 141, shared: 5, perExpert: 22 }}\n      onRoute={route => console.log(route.token, route.experts)}\n    />\n  );\n}",
      "exports": [
        "MixtureOfExperts"
      ]
    },
    {
      "name": "speculative-decoding",
      "kind": "component",
      "title": "Speculative decoding",
      "description": "An illustration of speculative decoding: a fast draft model guesses a few tokens, the target model checks them in one pass, and accepted guesses join the answer.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/speculative-decoding",
      "markdown": "https://uiarc.dev/components/speculative-decoding/markdown",
      "registry": null,
      "keywords": [
        "speculative decoding",
        "draft model",
        "target model",
        "assisted generation",
        "inference speed",
        "llm latency",
        "token acceptance",
        "tokens per pass",
        "verify",
        "ai illustration",
        "explainer",
        "decoding"
      ],
      "whenToUse": [
        "Explaining why a product answers faster: docs, model pages, inference dashboards and blog posts about latency.",
        "Showing a recorded speculative run from your own server, with acceptance and tokens per pass for a chosen draft length.",
        "Teaching the trade off between draft length, acceptance rate and speedup with a slider people can try."
      ],
      "whenNotToUse": [
        "Use text-stream to show an answer streaming to a user; this card explains the mechanism behind the speed.",
        "Use token-inspector to look at token probabilities and alternatives for one answer.",
        "Use model-router to explain how requests are sent to different models; speculative decoding runs two models on every request.",
        "Use attention-map or mixture-of-experts for what happens inside a single forward pass."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is a named group. The lanes are hidden from assistive technology and summarized in hidden text once each round has its verdicts: what the draft proposed, how many the target accepted and which token it wrote instead.",
        "Each output line carries the text written so far as its accessible name, so a screen reader reads words instead of tokens.",
        "Stats are a description list with spoken values (\"2.6 times\", \"67 percent\"); the rolling digits are decorative.",
        "Verdicts never rely on color: accepted columns show a check, the rejected guess is struck through with an x beside the target's token, and discarded guesses fade.",
        "A polite live region announces play, pause, restart, the comparison, a new draft length with its expected acceptance and speedup, and the finished result. It never announces every round.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Draft chips appear one by one, fading in from 4px to the left, with a short shimmer on the text while they wait.",
        "The target's sweep fills each cell in order from a per frame CSS variable; verdict icons and the replacement token crossfade in as the sweep reaches them, and the strike draws from the left.",
        "Accepted tokens and the target's token travel from their chips into their slot in the answer on a critically damped spring, about 20ms apart. The answer is laid out in full from the start, so the line never reflows.",
        "Lane height follows wrapping chips on the smooth spring; the target only line opens and closes on the same spring. Stats roll per digit with tabular numerals.",
        "Status text swaps with a short crossfade; no character morphs, blur or typewriter effects.",
        "Reduced motion never autoplays; when played, tokens appear in place without travel, the sweep steps per column, and the shimmer, slide and rolls are off."
      ],
      "responsive": [
        "Lane and line labels sit in a 120px column on wide cards. Below 560px the lane labels become a legend above the columns and line labels sit above their text with the pass count on the right.",
        "Draft columns wrap onto a second line when they run out of room; the lane height follows on a spring.",
        "The status line reserves two lines on narrow cards so wrapping copy never moves the lanes. Stats stay three across; the slider takes a full row above the compare switch."
      ],
      "performance": [
        "The clock runs on requestAnimationFrame in a ref. React state changes only when something on screen changes (a chip, a verdict, a landed token); the sweep is a CSS variable written each frame.",
        "Plans are memoized on a serialized copy of the inputs, so an inline tokens array does not restart the run each render.",
        "The loop stops when the run ends or is paused, and frame steps are capped so a background tab never jumps ahead."
      ],
      "aiNotes": [
        "This is an explainer, not a live decoder. With tokens it simulates a draft model from ease values and a seed; with rounds it replays a real run. Say \"illustrative\" when the numbers are simulated.",
        "Token text keeps its leading space, as tokenizers do. Punctuation without a space stays on the same line as the word before it.",
        "The speedup counts draft time: tokens divided by the sum of one pass per round plus draftCost per drafted token. Longer drafts accept a smaller share because everything after the first miss is discarded, so the speedup peaks at a middle length.",
        "Keep model names generic (Draft model, Target model) unless the run really came from named models.",
        "The simulation always leaves the final token to the target, so every round ends with a token from the target."
      ],
      "related": [
        "mixture-of-experts",
        "context-window"
      ],
      "usage": "import { SpeculativeDecoding } from \"@/components/arc/speculative-decoding/speculative-decoding\";\n\n// Simulated: pass the answer and let the seeded draft guess.\n<SpeculativeDecoding\n  tokens={[{ text: \"The\" }, { text: \" build\", ease: 0.6, miss: \" deploy\" }, { text: \" passed\" }, { text: \".\" }]}\n  defaultDraftLength={5}\n  defaultCompare\n/>\n\n// Replayed: pass rounds recorded from your own server.\n<SpeculativeDecoding\n  rounds={[\n    { draft: [\"The\", \" build\", \" failed\"], accepted: 2, target: \" passed\" },\n    { draft: [], accepted: 0, target: \".\" },\n  ]}\n  onComplete={stats => console.log(stats.speedup)}\n/>",
      "exports": [
        "SpeculativeDecoding"
      ]
    },
    {
      "name": "agent-status",
      "kind": "component",
      "title": "Agent status",
      "description": "A compact live status pill for a running AI agent: the agent's mark in a live ring, the current activity and an elapsed timer, opening into the steps so far.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-status",
      "markdown": "https://uiarc.dev/components/agent-status/markdown",
      "registry": null,
      "keywords": [
        "agent status",
        "ai agent",
        "status pill",
        "run status",
        "live indicator",
        "approval",
        "human in the loop",
        "coding agent",
        "research agent",
        "elapsed timer",
        "progress",
        "dynamic island",
        "agent run",
        "claude",
        "tool calls"
      ],
      "whenToUse": [
        "An app header, title bar or composer that needs to show at a glance that an agent is running, what it is doing and for how long.",
        "Coding, research or browser agents that pause for approval, so the person can approve or deny without opening a full run view.",
        "Background runs that people check on occasionally: the pill stays small, and the panel shows recent steps and controls on demand."
      ],
      "whenNotToUse": [
        "Use agent-run or research-run when the run itself is the page and every step, tool call and output needs room.",
        "Use agent-approvals to review a queue of pending approvals across several agents.",
        "Use agent-orchestrator or agent-tasks to compare many agents or runs at once.",
        "Use text-shimmer alone for a single inline working label without a timer, steps or controls."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The pill is a button with aria-expanded and aria-controls pointing at the panel, and an accessible name that reads the agent, the activity, the count and the elapsed time.",
        "A polite status region announces each new activity and the outcome (\"Claude Sonnet 5.5: done in 14 seconds, 6 files changed\"), not every tick of a count or the timer. Pass notice to add the result of an action.",
        "Status never relies on color: every state pairs the ring and glyph with words (Ready, Paused, Needs your approval, Done in, Failed, Stopped after), and steps use present tense while running and past tense once finished.",
        "The step progress bar is a progressbar with a value text such as \"4 of 6 steps finished\".",
        "Pause and Resume, and Approve and the Pause that follows it, share one button, so keyboard focus stays in place. If a focused control disappears with a state change, focus moves to the pill instead of the page.",
        "In narrow containers the inline Approve, Deny and Retry and the Open run button become icon buttons with accessible names. A truncated activity stays readable in full through the pill's name and the panel's step list.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, inset on the pill."
      ],
      "motion": [
        "A new activity rolls in vertically in about 180ms while the old one leaves upward; the pill's width follows on the morph spring, and the label fades at its right edge only while it is wider than its room, so nothing snaps or wraps.",
        "Opening the panel springs width and height together on the smooth spring from the pill's own footprint; the header row stays where it is and the panel content fades in after a beat. Closing reverses it with a faster fade.",
        "The live ring spins a short arc while working, freezes in place when paused, gives way to an accent dot with one soft ping when input is needed, and closes around the glyph as the mark morphs into a drawn check (done) or cross (failed).",
        "The working label carries a calm shimmer that stops in every other state. Counts and the timer change in place with tabular numerals.",
        "New steps fade in at the bottom of the panel while older ones slide up; segments of the progress bar fill from the left.",
        "Reduced motion swaps every spring for an instant change, drops the roll, the spin, the ping and the shimmer, and keeps all states, text, focus and announcements."
      ],
      "responsive": [
        "The pill sizes to its content and never exceeds its parent's width; a long activity fades out at its right edge instead of wrapping.",
        "Below 400px of available width the inline actions become icon buttons, the timer steps aside while they show, and Open run in the panel becomes an icon button.",
        "The panel opens at 384px or the pill's width, whichever is wider, capped by the parent, and floats over the page with a shadow so nothing below moves.",
        "Step labels in the panel wrap rather than truncate, so long file paths stay readable on a phone."
      ],
      "performance": [
        "The timer runs on a small per-instance clock that ticks four times a second only while the run is live and stops when paused, done or idle.",
        "Width and height animate motion values directly; natural widths come from two hidden, inert measuring rows watched by a ResizeObserver, so the pill never measures in the middle of an animation.",
        "The spinner and shimmer are CSS animations that pause or stop outside the working state."
      ],
      "aiNotes": [
        "Drive it from your agent's event stream: set status, keep activity to the latest tool call or phase in the present tense, and append steps as they finish. Keep the pill's label short; the panel carries the history.",
        "Keep startedAt fixed for the whole run; the component freezes paused and failed time itself. Pass endedAt from your backend when you have it.",
        "Use needs-input for any approval gate (shell commands, pushes, payments, emails) and phrase activity.label as the request: \"Needs your approval to run git push\".",
        "For done, put only the outcome in activity.label (\"6 files changed\"); the component writes \"Done in 1m 12s · 6 files changed\".",
        "Pass only the handlers your backend supports; each control appears only when its handler exists.",
        "Place it in a container that sets its width, such as a header slot or a flex row: the pill measures its parent to truncate cleanly and to size the panel."
      ],
      "related": [
        "agent-run",
        "agent-approvals",
        "ai-composer",
        "research-run",
        "tool-call",
        "agent-plan",
        "text-shimmer"
      ],
      "usage": "import { AgentStatus } from \"@/components/arc/agent-status/agent-status\";\nimport { FileSearch, FilePen, FlaskConical } from \"lucide-react\";\n\nexport function RunHeader({ run, agent }) {\n  return (\n    <AgentStatus\n      status={run.status}               // \"working\" | \"needs-input\" | \"paused\" | \"done\" | ...\n      startedAt={run.startedAt}\n      agent={{ name: \"Claude Sonnet 5.5\", mark: <ClaudeMark style={{ color: \"#D97757\" }} /> }}\n      activity={{ label: \"Running tests\", icon: FlaskConical, progress: { value: 8, total: 12 } }}\n      steps={[\n        { id: \"read\", label: \"Read 12 files\", icon: FileSearch, status: \"done\", durationMs: 1900 },\n        { id: \"edit\", label: \"Edited src/billing/invoice.ts\", icon: FilePen, status: \"done\", durationMs: 2500 },\n        { id: \"tests\", label: \"Running tests\", icon: FlaskConical, status: \"active\", startedAt: run.stepStartedAt },\n        { id: \"push\", label: \"Push the branch\", status: \"pending\" },\n      ]}\n      onPause={() => agent.pause(run.id)}\n      onResume={() => agent.resume(run.id)}\n      onStop={() => agent.cancel(run.id)}\n      onApprove={() => agent.approve(run.id)}\n      onDeny={() => agent.deny(run.id)}\n      onRetry={() => agent.retry(run.id)}\n      onOpenRun={() => router.push(`/runs/${run.id}`)}\n    />\n  );\n}\n\n// With no props it plays the simulated coding run.\n<AgentStatus />",
      "exports": [
        "AgentStatus",
        "useAgentRunSimulation"
      ]
    },
    {
      "name": "tool-call",
      "kind": "component",
      "title": "Tool call",
      "description": "A tool call row for agent transcripts: the tool's mark and the call in plain words, a live state while it runs, and the input and output a click away, with approval and retry when needed.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/tool-call",
      "markdown": "https://uiarc.dev/components/tool-call/markdown",
      "registry": null,
      "keywords": [
        "tool call",
        "tool use",
        "function call",
        "agent transcript",
        "ai chat",
        "used tools",
        "approval",
        "human in the loop",
        "retry",
        "tool result",
        "mcp",
        "agent",
        "llm tools",
        "inline approval"
      ],
      "whenToUse": [
        "Showing what tools an AI agent used inside a chat or agent transcript, with the arguments and results one click away.",
        "Human in the loop actions where a call must be allowed or denied inline before it runs, such as posting a message or writing to a database.",
        "Making failures recoverable in place: a timed out query with its reason and a Retry button.",
        "Debug and audit views where developers need to copy exact tool inputs and outputs."
      ],
      "whenNotToUse": [
        "Use agent-run for a full multi step run with a plan, a timeline and live logs rather than inline rows in a message.",
        "Use agent-approvals for a queue of pending approvals across many agents and runs.",
        "Use research-run when the work is mostly web research with sources and a streamed report.",
        "Use mcp-servers to manage which tools an agent can reach, not to show individual calls."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The group header and every call row are real buttons with aria-expanded and aria-controls; a row's name reads the tool, the verb phrase and the status, such as \"GitHub: Searched issues in arc-ui/web Done, 4 results, 1.2s\".",
        "Status is never color alone: each status has its own icon shape (spinner, check, alert, shield, ban), failed and waiting calls show a reason line, and the row name spells the status out.",
        "Each call announces its status changes through a polite live region, including that the preview only simulates an action when no handler is passed.",
        "Allow, Deny and Retry are labelled with the call they act on, such as \"Allow: Post a summary to #release\".",
        "The input list is a description list; tables keep table semantics with column headers. Folded rows and lines are hidden from assistive technology until shown.",
        "Copy buttons confirm success or failure in place and through a live region, without changing width.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared ring, inset inside rows so it is never clipped."
      ],
      "motion": [
        "Opening a row, opening the group and showing more lines animate height on the critically damped smooth spring, with a short opacity fade; chevrons turn on the shared CSS spring.",
        "Opening the group staggers its rows in 20ms apart with a 6px settle. Rows appended by a live run grow in from zero height instead, so nothing below them jumps.",
        "A row's height follows its content on the same spring when a reason line or wrapped actions appear, so a status change never resizes in one frame.",
        "Status icons trade places with a snappy scale and fade; the success check draws itself once. The running title uses the text shimmer and settles to solid when the call finishes.",
        "The mark stack widens on the morph spring as a new tool joins, so the header label glides instead of jumping.",
        "Reduced motion removes the stagger, spinner rotation, shimmer travel and springs. Panels, rows and statuses change immediately and every label, announcement and focus path stays."
      ],
      "responsive": [
        "Rows are container aware: below 440px the meta keeps only the duration, the reason line wraps, and Allow, Deny and Retry move under the reason so the title keeps its width.",
        "Under 440px the Input and Output panel drops its title indent and uses the full row width; wide JSON and tables scroll inside their own well, never the page.",
        "Rows are 40px tall and inline actions grow to 32px on touch screens. The whole row is the tap target for opening it."
      ],
      "performance": [
        "Folded outputs use fixed row heights, so folding and unfolding never measures the DOM.",
        "Input and Output mount only while a row is open; a closed group unmounts its rows.",
        "Height follows content through one ResizeObserver per row, and width changes from the container cut straight to the new height without animating.",
        "No dependencies beyond motion and lucide-react; brand marks are inline SVG."
      ],
      "aiNotes": [
        "Use ToolCallGroup for the tool calls of one assistant turn and ToolCall for a single call shown on its own. Render the group between the assistant's intro sentence and its answer.",
        "Append calls to the array as the agent starts them and change status in place; keep ids stable. Pass active while the agent is still working so the header does not flicker between calls.",
        "Give each call a past tense title and a present tense titles.running, and for approval calls an imperative titles.approval (\"Post a summary to #release\") plus a message saying what allowing will do.",
        "Pick the output type that reads best: list for search results and issues, table for SQL rows, json for structured API responses, text for a sent message or a short log. Use renderOutput for anything richer.",
        "Approve, deny and retry handlers should update the call's status; the component never changes status itself when a handler is passed. Without handlers it simulates locally, which suits previews only.",
        "Brand marks are sample content and never imply endorsement. For tools without a built in mark pass { name, mark } with a 16px inline SVG."
      ],
      "related": [
        "ai-chat",
        "agent-run",
        "agent-approvals",
        "agent-loop",
        "research-run",
        "mcp-servers",
        "text-shimmer"
      ],
      "usage": "import { ToolCall, ToolCallGroup } from \"@/components/arc/tool-call/tool-call\";\n\n// One call on its own\n<ToolCall\n  tool=\"github\"\n  title=\"Searched issues in arc-ui/web\"\n  titles={{ running: \"Searching issues in arc-ui/web\" }}\n  status=\"success\"\n  input={{ repo: \"arc-ui/web\", query: \"is:open label:release-blocker\" }}\n  output={{ type: \"list\", items: [{ lead: \"#1284\", title: \"Checkout button stays disabled after a coupon is removed\", tag: \"checkout\" }] }}\n  durationMs={1180}\n/>\n\n// Every call from one assistant turn, wired to your agent loop\nexport function ToolCalls({ calls, working, agent }) {\n  return (\n    <ToolCallGroup\n      calls={calls}\n      active={working}\n      onApprove={call => agent.approve(call.id)}\n      onDeny={call => agent.deny(call.id)}\n      onRetry={call => agent.retry(call.id)}\n    />\n  );\n}",
      "exports": [
        "ToolCall",
        "ToolCallGroup",
        "ToolCallData"
      ]
    },
    {
      "name": "agent-plan",
      "kind": "component",
      "title": "Agent plan",
      "description": "The plan an agent writes before it acts: edit, reorder or add steps while it is a draft, then watch it work through them as a live checklist.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-plan",
      "markdown": "https://uiarc.dev/components/agent-plan/markdown",
      "registry": null,
      "keywords": [
        "agent plan",
        "plan mode",
        "todo list",
        "task plan",
        "checklist",
        "approve plan",
        "coding agent",
        "ai agent",
        "step list",
        "reorder",
        "drag and drop",
        "progress",
        "retry",
        "human in the loop",
        "claude"
      ],
      "whenToUse": [
        "An agent should show and get approval for its plan before it edits code, runs commands or touches production.",
        "A person needs to adjust the agent's plan (rename, reorder, remove or add steps) and then watch the same plan execute.",
        "A side panel or chat message that tracks a multi-step task with a live current step, failures that need a decision, and a final summary."
      ],
      "whenNotToUse": [
        "Use agent-run for a streaming transcript of everything the agent did, with tool calls and output, rather than a checklist of steps.",
        "Use agent-approvals when the person approves individual risky actions one at a time instead of a whole plan up front.",
        "Use agent-tasks for a queue of independent tasks across agents rather than one ordered plan.",
        "Use research-run for search and reading progress that ends in a cited answer."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plan is a section named after the model and the goal; the steps are an ordered list, so a screen reader announces the count and each position.",
        "Every step announces its state in words (Pending, In progress, Done, Failed, Skipped) before its title. Each state also has its own shape, a ring, a spinning arc, two bars, a disc with a check, a disc with an exclamation mark or a dashed ring, so it never rests on color.",
        "A failed step shows the word Failed with its reason and two labelled buttons. Skipped steps say Skipped beside the title.",
        "Titles are labelled text areas described by a hidden hint that explains Alt plus the arrow keys and Escape. Grips and remove buttons are named after their step and position.",
        "Edits, approval, each finished step, an inserted step, a failure, pause and the final summary are announced through one polite status region, including that the preview is a simulation.",
        "Focus follows the action: a moved step keeps focus, a removed step hands it to its neighbour, Approve plan hands it to Pause, and Retry or Skip hand it back to Pause.",
        "The progress line is a progressbar with a text value (\"2 of 6 steps finished\"), hidden while the plan is a draft.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, inset on controls inside clipped rows."
      ],
      "motion": [
        "Reorders, inserts, removals and activity lines move on a critically damped spring (about 260ms, no bounce), so rows slide and grow without overshoot.",
        "Completing a step draws its check as a stroke over about 220ms while the disc fades up beneath it, and the title color settles from primary to secondary.",
        "The current step's title shimmers with Arc's text shimmer beside a spinning arc; pausing stops both and swaps the arc for a pause glyph.",
        "A suggested step streams its title in chunks of a few words every 34ms, each chunk only fading in. A step the agent inserts arrives with a faint accent wash that fades out over about 1.8s.",
        "Counts roll with tabular digits in a reserved width. The header label, footer actions and status icons swap with one short crossfade, and the footer's height follows on the same spring.",
        "Reduced motion keeps every state, label, focus move and announcement but drops travel, the spinner, the stroke draw, the wash and the chunk fades."
      ],
      "responsive": [
        "A container query tightens padding below 420px; the step grid keeps a fixed lead and trail column so titles wrap the same way in draft and while running and never shift on approve.",
        "Draft footer buttons share the row equally on narrow containers, and the done summary wraps above Start over.",
        "With a mouse the remove button appears on hover or focus; on touch it stays visible. Grips accept touch drags without scrolling the page.",
        "Reads well from 320px to about 760px wide; the demo caps it at 560px."
      ],
      "performance": [
        "The simulation schedules exactly one timer per state change, so pausing simply drops it and nothing runs while the plan is idle.",
        "Streaming uses one requestAnimationFrame clock with catch-up and appends spans; settled chunks never re-render or re-mount.",
        "Only transform and opacity animate on rows, except the brief height spring when a row or activity line enters or leaves.",
        "The shimmer runs only on the active step and only while it is on screen and the page is visible."
      ],
      "aiNotes": [
        "Use it for the plan step of a coding or operations agent: the agent proposes steps, a person edits and approves, then the same card tracks execution. It is not an onboarding checklist and not a run log.",
        "Write step titles as instructions (\"Add a sliding window limiter to the API middleware\"). Keep substeps to short past-tense activity starting with a verb such as Read, Edited, Created, Searched, Found, Ran or Measured; the verb picks the icon.",
        "Pass `status` when wiring a real agent. Then the component is fully controlled by `steps` and `status`: set one step to \"active\" at a time, append substeps as the agent reports them, set \"done\" with a duration in seconds, and set \"failed\" with an error to show Retry and Skip.",
        "Insert steps the agent adds mid-run with `note: \"Added by agent\"`; they slide in where you place them in the array.",
        "Keep planning calls onKeepPlanning; add the agent's suggestion to `steps` yourself. In the simulation it streams one of three sample suggestions in before the last step.",
        "The default model is Claude Opus 5.5 with its brand mark. Pass `model` with your own name and a 16px mark for another model."
      ],
      "related": [
        "agent-run",
        "agent-tasks",
        "agent-approvals",
        "agent-loop",
        "ai-chat",
        "text-shimmer"
      ],
      "usage": "import { AgentPlan, type AgentPlanStep } from \"@/components/arc/agent-plan/agent-plan\";\n\n// With no props it shows a sample plan and simulates the run.\nexport function PlanDemo() {\n  return <AgentPlan />;\n}\n\n// Driven by your agent.\nexport function AgentPlanPanel({ run }) {\n  return (\n    <AgentPlan\n      goal={run.goal}\n      steps={run.steps}           // [{ id, title, state, substeps, note, error, duration }]\n      status={run.status}         // \"draft\" | \"running\" | \"paused\" | \"done\"\n      summary={run.summary}\n      model={{ name: \"Claude Sonnet 5.5\", mark: <ClaudeMark /> }}\n      onChange={steps => run.updateDraft(steps)}\n      onKeepPlanning={() => run.suggestStep()}\n      onApprove={steps => run.start(steps)}\n      onPauseChange={paused => (paused ? run.pause() : run.resume())}\n      onStepAction={({ type, step }) => (type === \"retry\" ? run.retry(step.id) : run.skip(step.id))}\n    />\n  );\n}\n\n// A step the agent is working on, and one it inserted.\nconst steps: AgentPlanStep[] = [\n  { id: \"store\", title: \"Keep request counters in Redis with a 60 second TTL\", state: \"active\", substeps: [\"Read lib/redis.ts\", \"Ran 6 tests\"] },\n  { id: \"fallback\", title: \"Fall back to in-memory counters when Redis is down\", note: \"Added by agent\" },\n];",
      "exports": [
        "AgentPlan"
      ]
    },
    {
      "name": "prompt-cache",
      "kind": "component",
      "title": "Prompt cache",
      "description": "An animated explainer for prompt caching: the shared prefix of each request is cached once, later requests read it back, and the cost and time saved add up.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/prompt-cache",
      "markdown": "https://uiarc.dev/components/prompt-cache/markdown",
      "registry": null,
      "keywords": [
        "prompt caching",
        "prompt cache",
        "cache hit",
        "cache write",
        "cache read",
        "prefix matching",
        "time to live",
        "TTL",
        "time to first token",
        "token cost",
        "LLM cost",
        "Claude",
        "API pricing",
        "illustration",
        "animated diagram"
      ],
      "whenToUse": [
        "A docs page or blog post that explains why a stable prefix makes repeat requests cheaper and faster.",
        "A pricing or usage page that shows how cache reads, writes and the time to live affect cost.",
        "Developer onboarding for an AI product where prompt order and cache breakpoints matter."
      ],
      "whenNotToUse": [
        "Use context-window to show how much of a model's window a conversation fills and when it compacts.",
        "Use token-inspector to show how text splits into tokens and what each costs.",
        "Use model-picker or model-router when the subject is choosing a model rather than reusing a prompt.",
        "Use a real chart of cache hit rate over time for production analytics; this is a teaching illustration with a simulated clock."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is a section named for the title and marked as a simulation with sample prices.",
        "Each request is a list item with a sentence that gives its time, message, outcome, tokens read from the cache, tokens processed, input cost and time to first token; the bars and rolling numbers are hidden from assistive technology.",
        "Requests sent with the step button, the edit toggle, pausing and reset are announced in a polite live region. Autoplay stays silent so it never talks over the page.",
        "Outcomes are never color alone: every status pairs an icon with a label (Cache write, Cache hit, Partial hit), cached and processed fills differ in fill and outline, and the cache status is written out as Empty, Writing, Reading, Expires in 4:52 or Expired.",
        "The change toggle is a button with aria-pressed. Icon buttons have names and tooltips.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "One requestAnimationFrame clock drives the story and runs only while playing or stepping, on screen and in a visible tab. The ring and countdown are written straight to the DOM; React renders only on events.",
        "Processing is one linear sweep at a steady speed across the segments that need it. A cache hit fades the prefix to its cached fill while a single band of each segment's color passes once across the prefix, and the cache panel sweeps in step.",
        "A write carries the new part of the prompt from the row into the cache on a 460ms ease out, changing from processed to cached on the way; the panel's segments land in place and its token count rolls up. The ring refills on a critically damped follow and drains as simulated time passes.",
        "A changed segment raises a short warning line in the gap where matching stops; the cache drops everything after it before the rewrite lands.",
        "Numbers roll from the row above, so cost and first token visibly come down, with tabular numerals in columns sized to their content. Status labels swap with one crossfade. Pausing freezes every CSS animation and the clock together.",
        "Reduced motion opens on a finished round, drops the sweeps, the flight and the break animation, and changes values at once; play and step still work."
      ],
      "responsive": [
        "From 660px of container width the cache panel sits beside the requests; below it the panel moves above them as a strip, so the flight still travels from the row into the cache.",
        "Below 480px each request takes three lines (label and status, the bar, then its three numbers under shared column headings), the stats stack, and the controls wrap under the title when needed.",
        "Metric columns share one subgrid, so values align across rows at every width. The card holds at 320px with no horizontal scroll."
      ],
      "performance": [
        "About 120 small elements. Fills, sweeps, the break line and the flight are CSS animations on transform and opacity; the flight also animates the size of one absolutely positioned element.",
        "Outcomes are computed once per request with plain arithmetic. The loop stops off screen, in hidden tabs and while paused."
      ],
      "aiNotes": [
        "Use it to explain prompt caching on a pricing page, in API docs, or inside a usage dashboard next to real cache hit numbers.",
        "Keep segment order realistic: content that rarely changes first (tools, system prompt), then documents, then the conversation. Prefix matching stops at the first difference, which the toggle demonstrates.",
        "Pass the real prices of the model you show. The defaults are Claude Sonnet 5.5 sample prices; for other providers set inputPrice, cacheWritePrice (equal to inputPrice when writes cost nothing extra) and cacheReadPrice.",
        "Costs, savings and speedups are computed from the props, never typed in. The first request costs more than an uncached one because of the write, and the summary says so until the hits pay it back.",
        "Latency numbers are illustrative unless you pass firstToken with your own measurements."
      ],
      "related": [
        "context-window",
        "model-picker",
        "speculative-decoding",
        "agent-loop",
        "text-shimmer"
      ],
      "usage": "import { PromptCache } from \"@/components/arc/prompt-cache/prompt-cache\";\n\nexport function CachingSection() {\n  return <PromptCache\n    segments={[\n      { id: \"tools\", label: \"Tools\", tokens: 3_800 },\n      { id: \"system\", label: \"System prompt\", tokens: 2_400 },\n      { id: \"documents\", label: \"Documents\", tokens: 36_200 },\n      { id: \"conversation\", label: \"Conversation\", tokens: 6_000 },\n    ]}\n    requests={[\n      { message: \"Which refunds are still pending this week?\", tokens: 180 },\n      { message: \"Draft a reply to Northwind about their late refund\", tokens: 240, gap: 42 },\n      { message: \"Summarize ticket 48213 and the attached invoice\", tokens: 410, gap: 35 },\n      { message: \"Which customers asked about SSO in October?\", tokens: 150, gap: 51 },\n    ]}\n    onRequest={result => console.log(result.kind, result.cost)}\n  />;\n}",
      "exports": [
        "PromptCache"
      ]
    },
    {
      "name": "reasoning-tree",
      "kind": "component",
      "title": "Reasoning tree",
      "description": "An illustration of tree search over thoughts: candidates are scored, weak branches fade, the best expand, and the winning path from question to answer draws in.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "illustration",
        "new"
      ],
      "docs": "https://uiarc.dev/components/reasoning-tree",
      "markdown": "https://uiarc.dev/components/reasoning-tree/markdown",
      "registry": null,
      "keywords": [
        "tree of thoughts",
        "beam search",
        "reasoning",
        "search tree",
        "pruning",
        "chain of thought",
        "test time compute",
        "thought tree",
        "best path",
        "llm reasoning",
        "explainer",
        "illustration",
        "ai diagram"
      ],
      "whenToUse": [
        "Explainers, docs and landing pages that teach tree of thoughts, beam search or test time search over reasoning steps.",
        "Showing why a wider search can beat a greedy one, with a control the reader can change and replay.",
        "A visual summary of how an agent weighed options before it chose a plan."
      ],
      "whenNotToUse": [
        "Use agent-run or research-run to show a real agent's steps and output as they stream.",
        "Use agent-loop to explain the plan, act and observe cycle of a tool using agent.",
        "Use speculative-decoding or attention-map for token level mechanics rather than whole thoughts.",
        "Use model-router when the decision is which model handles a request."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The thoughts form a tree with treeitems that carry their level and position, and a roving tab stop, so the whole tree is a single Tab stop.",
        "Every thought's accessible name is its full text with its score and its fate: not scored yet, scored, kept, pruned, on the best path, or answer. A pruned thought fades and loses its edge, and the answer has a check icon, so state never relies on color alone.",
        "Focus and hover show the full thought in a tag next to the node; the same text is in the accessible name.",
        "A polite status region speaks each step when you step or pause, and the answer when the search ends, but stays quiet while the search plays on its own.",
        "The header status pairs a pause or check icon with a short label, and the working label carries a calm shimmer.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring around the node."
      ],
      "motion": [
        "A proposed thought fades in as its edge draws out of the parent, 20ms apart per sibling.",
        "Scores fill as a bar with digits that roll on the same 640ms curve, using tabular numerals in a fixed width.",
        "Pruning fades the thought to 40% and retracts its edge back into the parent. Kept thoughts turn their bar to the accent.",
        "At the end the best path draws from the question to the answer in about 760ms with a lighter sheen at its head, each node on it lights up as the line reaches it, and the answer settles with one short press and a check.",
        "On phones a new child glides the lines below it down on a critically damped spring before it fades in, so nothing jumps and nothing bounces.",
        "Header labels swap with a single 160ms crossfade; the play and pause icons crossfade in place.",
        "Reduced motion shows the finished search with the path highlighted. Stepping and playing still work, with every change applied at once and no shimmer."
      ],
      "responsive": [
        "From about 670px wide the tree reads left to right in four columns: the question, then depths 1 to 3, each with a header that counts how many thoughts were kept.",
        "Narrower, it becomes a vertical outline with one line per thought, indented by depth and joined by elbow edges, which holds down to 320px.",
        "The stage reserves the full tree before the search starts, so the card never changes size while it plays; a new beam width animates the height on phones.",
        "The controls wrap under each other on the narrowest widths."
      ],
      "performance": [
        "The search and the layout are pure functions memoized on the data and beam width.",
        "Positions and edges are written straight to the DOM from one requestAnimationFrame loop that runs only while something glides, so React renders once per step, not per frame.",
        "Fades, edge draws, bars and the rolling score are CSS transitions on transform, opacity, stroke-dashoffset and a registered integer property.",
        "Autoplay waits until the tree is in view."
      ],
      "aiNotes": [
        "Use it to explain how a reasoning search works, not to show a live model's real scores. Say in the surrounding copy that the scores are illustrative.",
        "Keep labels to about 2 to 4 words (under about 20 characters) and put the full sentence in text. Give the first depth about 3 thoughts and each later thought about 2 children; deeper levels are ignored.",
        "The search is plain beam search: every depth proposes the children of the survivors, keeps the best beamWidth, and the last depth keeps only the best one as the answer. Write scores so that a wider beam can find a better answer than the greedy path, which is what makes the control worth trying.",
        "Thoughts without a score get a seeded one, so a given tree and seed always produce the same outcome.",
        "Pass model={null} when no specific model is involved. Brand marks are sample content, never an implied endorsement."
      ],
      "related": [
        "agent-loop",
        "mixture-of-experts",
        "speculative-decoding",
        "vector-search",
        "research-run"
      ],
      "usage": "import { ReasoningTree } from \"@/components/arc/reasoning-tree/reasoning-tree\";\n\nexport function SearchExplainer() {\n  return (\n    <ReasoningTree\n      question=\"Which caching plan cuts p95 latency?\"\n      defaultBeamWidth={2}\n      onAnswer={({ answer }) => console.log(answer.label, answer.score)}\n      thoughts={[\n        { label: \"Cache at the edge\", score: 0.81, text: \"Serve product pages from the CDN with a 60 second TTL.\", children: [\n          { label: \"Purge on publish\", score: 0.77, children: [\n            { label: \"Tag based purge\", score: 0.9 },\n            { label: \"Purge everything\", score: 0.42 },\n          ] },\n          { label: \"Stale while revalidate\", score: 0.84 },\n        ] },\n        { label: \"Add a Redis layer\", score: 0.66 },\n        { label: \"Bigger database\", score: 0.35 },\n      ]}\n    />\n  );\n}",
      "exports": [
        "ReasoningTree"
      ]
    },
    {
      "name": "reasoning-block",
      "kind": "component",
      "title": "Reasoning block",
      "description": "The collapsible thinking block of an AI reply. While the model thinks, a shimmering label counts the seconds and the latest reasoning streams underneath. When it finishes, it folds to \"Thought for 7s\" and opens to show the full reasoning as calm steps with headings.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/reasoning-block",
      "markdown": "https://uiarc.dev/components/reasoning-block/markdown",
      "registry": null,
      "keywords": [
        "reasoning",
        "thinking",
        "thought for",
        "extended thinking",
        "chain of thought",
        "reasoning summary",
        "collapsible",
        "ai reply",
        "claude",
        "streaming",
        "llm"
      ],
      "whenToUse": [
        "Showing a reasoning model's thinking above its answer in a chat or agent transcript.",
        "Letting people audit why an assistant reached a recommendation without crowding the reply.",
        "Restoring past messages where the reasoning should stay folded with its original thinking time."
      ],
      "whenNotToUse": [
        "Tool or function calls with inputs and outputs: use ToolCall.",
        "A multi step agent plan with checkable tasks: use Agent plan.",
        "A one line busy indicator with no reasoning to show: use TextShimmer on its own."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The header is a real button with aria-expanded and aria-controls; its name is the visible label, such as \"Thought for 7s\".",
        "A polite live region announces status changes only (thinking, thought for 7 seconds, stopped, interrupted), never each streamed chunk or each second.",
        "The two line live preview is aria-hidden because the same text is in the full reasoning region, which carries aria-busy while streaming.",
        "Status is never color alone: errors pair an alert icon with \"Thinking interrupted\", and the footer pairs a check, square or alert with Done, Stopped or the reason.",
        "The no props preview says \"Simulated, nothing was sent to a model\" to assistive technology."
      ],
      "motion": [
        "Streaming is chunked: each appended chunk fades in once over 140ms. No blur, slide or per word springs.",
        "Stop and Retry overflow a fixed 20px row, so the header is 44px in every state and never shifts when an action appears.",
        "The live preview stays anchored to its newest line; when a line wraps, the text glides up with a bounce free spring (0.24s) instead of jumping.",
        "Label swaps (Thinking to Thought for 7s) are a single crossfade: out in 100ms, in over 160ms with a 2px rise. The seconds counter rolls in tabular numerals inside a reserved width.",
        "Opening, closing and the preview collapsing all animate measured height with one critically damped spring (0.26s) and one opacity fade. Only what streams in while open (a new step, paragraph or chunk) fades on its own, so nothing animates twice.",
        "The shimmer and the self-playing loop pause off screen and in background tabs. Reduced motion keeps every end state, drops the shimmer, the glide and the chunk fades, and cuts heights instantly."
      ],
      "responsive": [
        "Fills its container from 320px up; the header never wraps and long labels truncate.",
        "The live preview stays two lines at every width, so streaming never changes the block's height.",
        "Paragraphs wrap with text-wrap: pretty and break long identifiers instead of scrolling sideways.",
        "On touch, the whole header and live preview are one press target of at least 44px."
      ],
      "performance": [
        "Streamed text is appended as spans that never re-render once on screen; finished messages render plain text.",
        "Only the open block mounts its full reasoning, so long transcripts of collapsed blocks stay light.",
        "The preview glide is one transform on one element driven by a ResizeObserver, with no layout animation on text."
      ],
      "aiNotes": [
        "Use it above the answer in an assistant message whenever the model streams reasoning (extended thinking, reasoning summaries). Feed it the raw thinking text and switch status to done as soon as answer tokens start.",
        "Do not pass seconds while streaming unless your server measures it; the block counts by itself and freezes the value when status leaves thinking. Store that number with the message so history shows the same \"Thought for 7s\".",
        "Keep reasoning out of the answer text. For tool calls made during reasoning, render ToolCall rows after the block instead of inside it.",
        "Use variant=\"plain\" inside a message bubble that already has its own surface, so you never nest cards.",
        "Swap the mark for another provider by passing model={{ name, mark }} with an 18px SVG."
      ],
      "related": [
        "tool-call",
        "agent-status",
        "text-shimmer",
        "agent-plan"
      ],
      "usage": "import { ReasoningBlock } from \"@/components/arc/reasoning-block/reasoning-block\";\n\n// While streaming: pass the growing text, then flip status when the answer starts.\n<ReasoningBlock\n  reasoning={thinkingText}\n  status={done ? \"done\" : \"thinking\"}\n  onStop={() => controller.abort()}\n/>\n\n// A finished message from history.\n<ReasoningBlock reasoning={message.reasoning} status=\"done\" seconds={message.thinkingSeconds} />",
      "exports": [
        "ReasoningBlock",
        "parseReasoning",
        "ClaudeMark"
      ]
    },
    {
      "name": "source-cards",
      "kind": "component",
      "title": "Source cards",
      "description": "A row of cited sources under an AI answer where cards stream in with real site marks while the model searches, the overflow collapses into a stack of favicons, hovering lifts a preview with the passage, and an inline [n] citation highlights its card.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/source-cards",
      "markdown": "https://uiarc.dev/components/source-cards/markdown",
      "registry": null,
      "keywords": [
        "sources",
        "citations",
        "ai search",
        "perplexity",
        "references",
        "web search",
        "favicon",
        "cited answer",
        "rag",
        "search progress"
      ],
      "whenToUse": [
        "Citations under an AI search or docs assistant answer.",
        "Showing live search progress (\"Searching 3 of 8\") while an agent browses.",
        "Linking inline [n] references in generated text to the pages they came from."
      ],
      "whenNotToUse": [
        "A full search results page with many results and filters; use a list layout instead.",
        "Tool calls with arguments and outputs; use tool-call.",
        "A single link preview inside prose; use a tooltip or hover card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The cards are a labelled list (aria-label \"Sources\") marked aria-busy while searching; skeleton slots are hidden from assistive technology.",
        "A polite live region announces status changes once: searching for the query, the number found, no sources, or the failure. Counts do not announce on every card.",
        "Each card's name reads number, title and domain, and says when it opens in a new tab. The preview is a tooltip linked with aria-describedby while open.",
        "SourceCitation is a real button named \"Source 2: Request: text() method, developer.mozilla.org\"; its highlighted state is never color alone because the card also gains an outline.",
        "Status always pairs an icon with words: spinner and Searching, globe and the count, alert and Search failed, crossed search and No sources found.",
        "The demo says in a live region that the search is simulated and that card clicks open nothing."
      ],
      "motion": [
        "Cards fade in with a 4px rise in 180ms as they arrive; skeletons crossfade out in 120ms. Nothing bounces.",
        "Collapsing the stack or a failed search dropping its reserved slots fades the leaving slots in place (120ms) while the stack or card taking their cell fades in, and the row height glides once. Skeleton bars sit on the same 18px lines as the text that replaces them.",
        "Counts roll 5px with tabular numerals in a reserved width; status icons and labels crossfade (100ms out, 160ms in).",
        "The answer's height springs (bounce 0) as it streams so the cards glide down instead of stepping one line at a time; a width change cuts straight to the new height.",
        "Previews open after a 320ms hover intent, then switch instantly between cards; they fade and rise 4px with a 0.98 scale in 160ms.",
        "Reduced motion keeps every end state and highlight but drops the rise, scale and skeleton pulse. The demo pauses off screen."
      ],
      "responsive": [
        "Wide containers show max + 1 equal columns; below 540px (container query) the grid becomes two columns, with the stack as the last tile.",
        "Previews align to the card's left or right edge so they never leave the component, and drop below the card when there is no room above.",
        "On touch there is no hover preview; tapping a card opens it. Citation chips have an enlarged hit area."
      ],
      "performance": [
        "Cards are plain elements with CSS transitions; motion only drives arrivals, the preview and number rolls.",
        "One ResizeObserver per auto height box; the demo's timers stop off screen through an IntersectionObserver.",
        "Built in marks are inline SVG paths with no network requests."
      ],
      "aiNotes": [
        "Use under an assistant message that cites web or docs sources. Keep sources in citation order so [n] maps to card n.",
        "Stream results by appending to sources with status \"searching\" and a total; switch to \"done\" when the model starts answering.",
        "Put the answer inside SourceCards as children when it contains SourceCitation chips; otherwise render SourceCards alone under your own answer and drive highlighted yourself.",
        "Pass onSourceClick for in-app reading; omit it for plain new-tab links. Do not wrap cards in your own links.",
        "Known hosts draw real marks automatically; pass icon for other favicons rather than inventing a logo."
      ],
      "related": [
        "tool-call",
        "agent-status",
        "text-shimmer",
        "tooltip",
        "copy-button"
      ],
      "usage": "import { SourceCards, SourceCitation } from \"@/components/arc/source-cards/source-cards\";\n\n<SourceCards\n  sources={results}            // [{ title, url, snippet, published }]\n  status={searching ? \"searching\" : \"done\"}\n  total={8}\n  query=\"stripe webhook signature verification\"\n  duration={2100}\n  onSourceClick={(source) => openReader(source.url)}\n>\n  <p>\n    Read the raw body with <code>await request.text()</code> before parsing it <SourceCitation n={1} />\n    <SourceCitation n={2} />.\n  </p>\n</SourceCards>",
      "exports": [
        "SourceCards",
        "SourceCitation",
        "SourceCardsSource",
        "SourceCardsSimulation"
      ]
    },
    {
      "name": "model-card",
      "kind": "component",
      "title": "Model card",
      "description": "A model details card where switching models rolls every price, limit and score in place, a token slider estimates monthly cost live, and benchmark bars show each model's rank on hover. Compare crossfades to a side-by-side table against any rival with the better values marked, and the card never changes size.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/model-card",
      "markdown": "https://uiarc.dev/components/model-card/markdown",
      "registry": null,
      "keywords": [
        "model card",
        "model details",
        "model picker",
        "llm",
        "ai model",
        "compare models",
        "benchmark",
        "context window",
        "pricing per token",
        "cost estimator",
        "token cost calculator",
        "tokens",
        "claude",
        "gpt",
        "gemini",
        "grok",
        "copy model id",
        "marketplace"
      ],
      "whenToUse": [
        "The detail pane of a model picker, showing what the highlighted model can do and what it costs before someone selects it.",
        "Model pages in docs or a marketplace, with limits, prices and a quick benchmark read against close rivals.",
        "Settings where a team picks a default model and wants a side by side check against an alternative."
      ],
      "whenNotToUse": [
        "Use model-picker to choose between many models; pair it with this card for the details.",
        "Use context-window to show how full a live conversation's context is.",
        "Use a pricing table for plan pricing; this card shows per token API prices.",
        "Do not use it for official benchmark leaderboards with many models; the bar list shows three models."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The card is an article labelled by the model name. Specs and prices are description lists, so each value is read with its label.",
        "The compare view is a table with column headers for the two models and a row header per spec. The better value carries a check icon and the text \"better\" for screen readers, so the comparison never relies on color.",
        "A polite status region says what changed when you switch views, rivals or benchmarks (for example \"Comparing Claude Opus 5.5 with Gemini 4 Argon\"). Copy model id announces Copied on its own.",
        "Benchmark bars are hidden from assistive technology; each row is focusable and labelled with the full readout (score, rank and gap to the leader). Marks are decorative and hidden, since the name is always written next to them.",
        "Modality chips say supported or not supported to screen readers; unsupported chips are dashed as well as muted, so the difference never relies on color.",
        "While loading, the card is aria-busy, the skeleton is hidden, Compare is disabled and the status region says Loading model details. An error is announced with role alert. Inactive panels are inert and aria-hidden.",
        "During a text swap the outgoing value is hidden from assistive technology, so names and values are never read twice.",
        "Pointer focus draws no ring; keyboard focus uses Arc's shared focus outline."
      ],
      "motion": [
        "The card never changes size. Details, compare, loading and error are panels stacked in one grid cell, so the card is always as tall as the tallest panel; switching fades the old panel out in 100ms and the new one in over 160ms. Inside a panel every slot is reserved: two summary lines, all seven chips, every spec and price cell, a fixed number of benchmark rows and a readout that holds its lines.",
        "Switching models crossfades every text value in place and rolls prices, the estimated cost, page counts and scores to their new values in tabular numerals inside reserved widths. Bars and the context gauge slide on a critically damped spring (visualDuration 0.24, no bounce).",
        "Dragging the volume slider rolls the estimated cost live; the compare table's cost row follows the same volume.",
        "Hovering or focusing a benchmark row tints it, fades the other rows to half opacity and crossfades the readout to its score, rank and gap, all within 160ms.",
        "The bars grow from zero once, the first time the card is mostly in view, 40ms apart.",
        "Reduced motion keeps every end state and drops travel: views, bars, numbers and values change in place, and the skeleton holds still."
      ],
      "responsive": [
        "The card is fluid up to 640px and reads its own width with container queries, so it works in a narrow side pane as well as a page.",
        "Below a 480px card, specs and prices become rows of label and value under hairlines, the cost estimate drops under the slider, the benchmark switcher drops under its heading and the readout reserves three lines. Below 420px the summary reserves three lines. Below 360px each bar moves under its name.",
        "From a 520px card the compare table has a label column beside the two model columns and shows margins on the better value; narrower, each label sits above its two values and the mini bars and margins hide.",
        "The footer wraps the copy and select buttons under the model id when space runs out. It holds at 320px without horizontal scroll."
      ],
      "performance": [
        "Bars animate transform only (scaleX) and numbers are written in place during the roll, so React renders once per change, not per frame.",
        "No measuring: the stacked grid cell reserves the height in CSS, so there is no ResizeObserver and no height animation.",
        "Nothing runs while idle: no loops apart from the CSS skeleton pulse while loading. Hidden panels stay mounted (inert) so values are ready the moment a view switches."
      ],
      "aiNotes": [
        "Use real figures from your model catalog and set sample={false}; leave sample on while the numbers are placeholders. The bundled data is a plausible October 2026 sample, not official pricing or results.",
        "Keep the summary to one sentence of about 90 characters; the card reserves two lines (three on narrow cards) and clamps anything longer.",
        "Give scores for the same benchmark ids on the model and its rivals. A row without a score shows None and the compare table shows Not listed, so rows never appear or disappear.",
        "Pass one shared pool as rivals when the model changes (the card filters out the current model), so the benchmark rows and ranks stay stable while values roll.",
        "Copy marks from model-card-marks.tsx (Claude, OpenAI, Gemini, Grok) or pass any SVG with aria-hidden. Mono marks follow the text color; set style={{ color }} for a brand color, as the Claude mark does. Brand marks are sample content, never an implied endorsement.",
        "Price cells read per million tokens. Omit cachedInput to show two price cells.",
        "For a list of many models, render a compact picker and show one ModelCard for the highlighted model; the card is a detail view, not a row."
      ],
      "related": [
        "model-picker",
        "context-window",
        "copy-button",
        "segmented-control",
        "switch",
        "dropdown-menu",
        "slider"
      ],
      "usage": "import { ModelCard, type ModelCardModel } from \"@/components/arc/model-card/model-card\";\nimport { ClaudeMark, OpenaiMark } from \"@/components/arc/model-card/model-card-marks\";\n\nconst sonnet: ModelCardModel = {\n  id: \"claude-sonnet-5-5\",\n  name: \"Claude Sonnet 5.5\",\n  provider: \"Anthropic\",\n  mark: <ClaudeMark aria-hidden style={{ color: \"#D97757\" }} />,\n  summary: \"The balanced Claude for everyday coding, agents and analysis at scale.\",\n  modalities: [\"text\", \"image\", \"pdf\", \"tools\", \"reasoning\"],\n  contextWindow: 1_000_000,\n  maxOutput: 64_000,\n  knowledgeCutoff: \"2026-03\",\n  pricing: { input: 3, output: 15, cachedInput: 0.3 },\n  scores: { \"swe-bench\": 82.1, gpqa: 85.4 },\n};\n\nexport function ModelDetails({ rivals, current, onPick }: { rivals: ModelCardModel[]; current: string; onPick: (id: string) => void }) {\n  return (\n    <ModelCard\n      model={sonnet}\n      rivals={rivals}\n      benchmarks={[\n        { id: \"swe-bench\", name: \"SWE-bench Verified\", shortName: \"SWE-bench\", description: \"real GitHub issues resolved\" },\n        { id: \"gpqa\", name: \"GPQA Diamond\", shortName: \"GPQA\", description: \"graduate level science questions\" },\n      ]}\n      defaultMonthlyTokens={50_000_000}\n      sample={false}\n      selected={current === sonnet.id}\n      onSelect={model => onPick(model.id)}\n      onCopyId={id => console.log(\"copied\", id)}\n    />\n  );\n}",
      "exports": [
        "ModelCard"
      ]
    },
    {
      "name": "ai-workflow",
      "kind": "component",
      "title": "AI workflow",
      "description": "A Linear-style AI workflow card where a Gmail trigger, a Claude step, a branch and Slack and Linear actions sit on one hairline rail, and a test run glides down it, opening each step's output inline. Steps are added from a compact command menu with real app marks.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ai-workflow",
      "markdown": "https://uiarc.dev/components/ai-workflow/markdown",
      "registry": null,
      "keywords": [
        "workflow builder",
        "automation",
        "zapier",
        "n8n",
        "make",
        "ai workflow",
        "flow builder",
        "trigger action",
        "test run",
        "integration",
        "branch",
        "claude",
        "gmail",
        "slack",
        "linear"
      ],
      "whenToUse": [
        "An automation or integrations page where people assemble a short trigger, AI step and action flow and want to test it in place.",
        "Onboarding or marketing surfaces that show how an AI step slots between real apps, with a working test run.",
        "Reviewing what an existing workflow does and seeing a sample run's outputs step by step."
      ],
      "whenNotToUse": [
        "Large free-form graphs with many branches, loops or merges: use a canvas editor instead.",
        "Showing a single tool call or a live agent's steps: use tool-call or agent-run.",
        "Scheduling recurring agent jobs: use agent-routines."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The card is a labelled section with aria-busy while a run is in progress or steps are loading; steps are an ordered list, and each branch path is its own labelled list.",
        "Every step title carries its app and status for screen readers (\"Create Linear issue, Linear, Done in 700ms\"). Status is shown as an icon plus a short label, never color alone.",
        "A polite live region announces run start, each step's result, failures with their message, and the outcome, including \"Simulated, nothing was sent\" when no onRunStep is wired.",
        "The add step menu is a combobox: the search field owns focus and points at the highlighted option with aria-activedescendant; options are a listbox. Branch paths are a radio group with roving focus.",
        "Remove buttons are named per step (\"Remove Create Linear issue\") and are hidden only visually on hover devices until the row is hovered or the button is focused.",
        "Brand marks are decorative next to the visible step title; marks whose brand color is near black draw in currentColor so they keep contrast in dark mode."
      ],
      "motion": [
        "A test run moves one thing at a time: a 5px accent dot glides down the next connector in 240ms while the line behind it fills, then the step crossfades to Running (icon and label) and its detail line crossfades to a shimmering activity line.",
        "Everything is bounce-free and 100 to 240ms. Output previews and added or removed steps open and close on measured height with a critically damped spring (visualDuration 0.22, bounce 0); switching paths crossfades the lists (100ms out, 160ms in) while the container height follows, and the tab highlight slides on the same spring.",
        "Status labels crossfade in a fixed 72px slot, so titles never shift. Reasons and drafts stream five words every 34ms, each chunk only fading in, with the final layout reserved from the first frame.",
        "The add step menu fades and settles in 150 to 180ms with no spring overshoot; filtering animates the list to its new measured height.",
        "Autoplay pauses while the card is off screen and stops for good after the first press or key inside the card.",
        "Reduced motion drops the traveling dot, line growth and height animation; states, outputs and the live region stay the same."
      ],
      "responsive": [
        "Fills its container up to 640px. Above 480px each step is one 32px row with the detail inline after the title; below that rows grow to 44px and stack the detail under the title, the status slot narrows to 64px and the header wraps to two lines.",
        "The empty state's trigger rows drop their detail text below 480px of card width.",
        "Remove buttons are always visible on touch screens and appear on hover or focus with a fine pointer."
      ],
      "performance": [
        "One state update per step change; the rail and dot animate transform and opacity in CSS, and previews animate measured height only.",
        "Streaming renders the whole text once and toggles chunk opacity, so it never reflows. Autoplay timers stop off screen via IntersectionObserver.",
        "Marks are inline SVG with no network requests."
      ],
      "aiNotes": [
        "Pick this to show or edit an automation that mixes app triggers, an LLM step and app actions, such as triage, routing or enrichment flows, inside a product settings page or onboarding.",
        "Wire onRunStep to a test endpoint and return the WorkflowOutput union so previews render; throw to show the failure state with Retry. Keep autoPlay off in real products.",
        "Use only current model names in AI step titles (Claude Sonnet 5.5, GPT-6.1 Sol, Gemini 4 Argon). Built-in app ids: gmail, slack, linear, claude, notion, sheets, hubspot, zendesk, github, stripe, branch; pass { name, mark } for anything else.",
        "One branch per flow, as the last top-level step. For multi-agent graphs or a run log, choose agent-orchestrator or agent-run instead."
      ],
      "related": [
        "agent-run",
        "agent-plan",
        "tool-call",
        "agent-routines",
        "connector-setup",
        "agent-orchestrator"
      ],
      "usage": "import { useState } from \"react\";\nimport { AiWorkflow, workflowSampleSteps } from \"@/components/arc/ai-workflow/ai-workflow\";\nimport type { WorkflowStep } from \"@/components/arc/ai-workflow/ai-workflow\";\n\nexport function SupportAutomation() {\n  const [steps, setSteps] = useState<WorkflowStep[]>(workflowSampleSteps);\n  return (\n    <AiWorkflow\n      title=\"Escalate urgent support email\"\n      steps={steps}\n      onStepsChange={setSteps}\n      autoPlay={false}\n      onRunStep={async (step, { signal, previous }) => {\n        const res = await fetch(`/api/workflows/test/${step.id}`, { method: \"POST\", body: JSON.stringify({ previous }), signal });\n        if (!res.ok) throw new Error((await res.json()).message);\n        return res.json();\n      }}\n    />\n  );\n}",
      "exports": [
        "AiWorkflow",
        "WorkflowOutput"
      ]
    },
    {
      "name": "code-suggestion",
      "kind": "component",
      "title": "Code suggestion",
      "description": "An AI code edit suggestion where the model's proposed changes stream into a small editor panel as inline diff hunks, and you accept or reject each one, or all at once with Cmd+Enter. Accepted hunks fold into the file with a smooth height change.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/code-suggestion",
      "markdown": "https://uiarc.dev/components/code-suggestion/markdown",
      "registry": null,
      "keywords": [
        "code suggestion",
        "ai code edit",
        "inline diff",
        "accept reject",
        "cursor",
        "copilot",
        "code review",
        "diff hunk",
        "inline edit",
        "ai coding",
        "apply changes",
        "claude",
        "code diff",
        "suggested changes",
        "accept all"
      ],
      "whenToUse": [
        "Showing an AI's proposed edit to a file as an inline diff the person accepts or rejects hunk by hunk.",
        "Approving an agent's file change before it is written, with a clear record of what was kept.",
        "Landing pages and docs that demonstrate an AI coding feature with a realistic, self playing edit."
      ],
      "whenNotToUse": [
        "Use a dedicated diff tool for large multi file diffs or side by side comparisons; this card is for one file and a handful of hunks.",
        "Use code-magic-move to animate code from one version to another without a review step.",
        "Use code-block to show code without changes.",
        "Use tool-call for an agent step that is not a file edit.",
        "Use agent-approvals when the person approves actions in general (emails, payments, deploys) rather than code."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled group (\"Code suggestion: Add retry with backoff\") and the code is a focusable region named after the file.",
        "Each pending removed or added line starts with a visually hidden \"Removed:\" or \"Added:\", so the diff never relies on the red and green tints; the visible minus and plus signs carry it for sighted readers too.",
        "Accept and Reject name their hunk (\"Accept Retry 429 and 5xx with backoff\"). Accept all and Reject all expose their shortcuts with aria-keyshortcuts.",
        "A polite status region speaks the state (\"3 changes to review\"), each decision (\"Accepted: Add retry limits. 2 changes to review\") and the result. The simulated suggestion adds \"Simulated, nothing was written to disk\" when it finishes. Streaming is announced once, not per line.",
        "Status always pairs an icon with a label: a spinner for reading and writing, a dot for review, a check for applied, a cross for rejected and an alert for errors.",
        "Focus never falls back to the page when the pressed control leaves with its hunk or state. Pointer focus draws no ring; keyboard focus uses Arc's shared focus outline."
      ],
      "motion": [
        "Thinking lasts about 0.8s with a calm shimmer on \"Reading fetch-invoice.ts\". Then each hunk opens: its removed lines tint red in 240ms and its new lines stream in one every 38ms, each opening to its 20px row in 120ms while it fades in. The whole sample streams in about 1.6s.",
        "Review bars open under every hunk at once on a critically damped spring (0.24s, no bounce).",
        "Accept folds the removed lines and the bar to zero height on the same spring (0.26s), so the code below glides up, while the green tint and the plus signs fade out. Reject does the same for the new lines and lets the red fade. Rows have fixed heights, so nothing is measured mid animation and nothing jitters.",
        "The footer status icon, label and actions each swap with one short crossfade in place (old out in 100ms, new in 160ms, the label with a 2px rise). The icon only swaps when its kind changes, so the spinner keeps turning from Reading to Writing and a count change swaps only the text. Zero counts in the tab turn muted.",
        "Reduced motion opens on the finished suggestion with no streaming, no autoplay and instant folds; decisions still apply and the status still updates. Autoplay waits until the card is in view and its replay waits until the card is back in view."
      ],
      "responsive": [
        "The card fills its container up to 720px. Long lines scroll sideways inside the code area while the line numbers stay pinned on the left and each review bar stays pinned to the visible width, so Accept is always in reach.",
        "The folder after the file name hides below a 520px card.",
        "Below a 440px card the gutter narrows, code drops to 12px, hunk summaries hide (the buttons keep their names for screen readers) and the footer stacks the status above full width actions. It holds down to 320px without page scroll."
      ],
      "performance": [
        "Rows are fixed 20px lines animated between known heights; nothing is measured during animation.",
        "Each line is highlighted once and memoized by its text, so streaming a new line never re-highlights the others.",
        "The simulation runs on single timeouts per step, starts only when the card is in view and stops its loop when it scrolls away. Pass maxCodeHeight for long files."
      ],
      "aiNotes": [
        "Use it wherever an AI proposes edits to one file and a person approves them: inline edit in an editor, a code review assistant, a config or SQL change preview, an agent's file edit approval.",
        "Map a unified diff to hunks: start is the zero based line of the first removed line (or the insert position), remove is the removed line count and insert the added lines. Keep hunks sorted and non overlapping, and give each a summary of two to six words.",
        "To stream, set status=\"streaming\" and grow the hunks array and each hunk's insert as tokens arrive, then set status=\"ready\". Wire onStop to abort the request with an AbortController.",
        "Write the file in onResolve (or progressively in onDecision); the component never writes anything itself. Say in surrounding copy when content is a sample.",
        "Copy marks from code-suggestion-marks.tsx (Claude, OpenAI, TypeScript) or pass any ReactNode with aria-hidden. Brand marks are sample content, never an implied endorsement.",
        "The highlighter is a small TypeScript and JavaScript tokenizer. For other languages, the code still renders cleanly in the plain text color."
      ],
      "related": [
        "tool-call",
        "agent-approvals",
        "code-block",
        "text-shimmer",
        "button"
      ],
      "usage": "import { CodeSuggestion, type CodeSuggestionHunk } from \"@/components/arc/code-suggestion/code-suggestion\";\nimport { ClaudeMark } from \"@/components/arc/code-suggestion/code-suggestion-marks\";\n\nconst hunks: CodeSuggestionHunk[] = [\n  { id: \"timeout\", start: 2, remove: 1, summary: \"Add a 10s timeout\", insert: [\n    \"  const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });\",\n  ] },\n];\n\nexport function InlineEdit({ file, onSave }: { file: { name: string; text: string }; onSave: (text: string) => void }) {\n  return (\n    <CodeSuggestion\n      code={file.text}\n      hunks={hunks}\n      fileName={file.name}\n      instruction=\"Time out slow requests\"\n      model={{ name: \"Claude Opus 5.5\", mark: <ClaudeMark aria-hidden style={{ color: \"#D97757\" }} /> }}\n      onResolve={({ code }) => onSave(code)}\n    />\n  );\n}",
      "exports": [
        "CodeSuggestion"
      ]
    },
    {
      "name": "ghost-text",
      "kind": "component",
      "title": "Ghost text",
      "description": "An inline AI autocomplete where muted ghost text continues your sentence exactly at the caret and Tab turns it solid in place. Cmd+Right takes it one word at a time, Escape fades it away, and a calm chip shows the model behind it.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ghost-text",
      "markdown": "https://uiarc.dev/components/ghost-text/markdown",
      "registry": null,
      "keywords": [
        "ghost text",
        "ai autocomplete",
        "inline suggestion",
        "copilot",
        "tab to accept",
        "text completion",
        "autocomplete textarea",
        "predictive text",
        "smart compose",
        "llm",
        "claude",
        "streaming",
        "pull request",
        "email",
        "writing assistant"
      ],
      "whenToUse": [
        "Long-form fields people write in product: pull request descriptions, emails, support replies, issue bodies, release notes, CRM notes.",
        "Short single-line fields where a likely completion saves typing, such as an email subject or a commit message.",
        "Anywhere you want AI help that stays out of the way until the person pauses, and never changes their text without an explicit key press."
      ],
      "whenNotToUse": [
        "Use ai-composer for a chat-style prompt box that sends a message to an assistant.",
        "Use code-suggestion for code diffs with accept and reject per hunk.",
        "Use a combobox or command menu when the person picks from a list of known options rather than free text.",
        "Avoid it in fields with sensitive content (passwords, card numbers, health data) unless your model provider and data policy allow it."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The field is a native textarea (or input), labelled by aria-label or the label row, and described by a hidden sentence that explains Tab, Cmd or Ctrl+Right and Escape.",
        "The ghost text lives in an aria-hidden mirror behind the field, so it is never read as content. A polite status region reads the finished suggestion (\"Suggestion: date the plan changed. Press Tab to accept.\"), then Suggestion accepted, Suggestion dismissed or Couldn't get a suggestion.",
        "The chip is a button with a clear name in each state (Accept suggestion, Retry suggestion). It is reachable with Tab only in the error state, where Retry is the action; in the ready state Tab itself accepts. Pressing the chip never steals focus from the field.",
        "State is never shown by color alone: Thinking, the Tab key cap with \"to accept\", Accepted with a check and No suggestion with an alert icon all carry words.",
        "Keyboard focus draws Arc's focus ring around the whole field; pointer focus does not.",
        "On touch screens, where there is no Tab key, the chip reads Tap to accept."
      ],
      "motion": [
        "Ghost text only fades in, chunk by chunk as it streams (140ms each, opacity only, no blur or slide), so a suggestion settles without any movement. Escape and leaving the field fade it out in 110ms; typing a different character clears it at once so nothing lingers over new text.",
        "Accepting turns the ghost solid in place: for 200ms an aria-hidden mirror with the field's exact metrics draws the text while the native glyphs go transparent (the caret stays), and the accepted range eases from muted to text color. Cmd+Right does the same per word, and the rest of the ghost never re-fades.",
        "The field's height follows the text, ghost included, on a 180ms tween and never bounces. The box is clipped while it grows, so the browser cannot scroll it under the caret mid-tween.",
        "Chip states crossfade in place (old out in 100ms, new in 160ms) while the chip's width follows on a 200ms tween, anchored to its right edge. Key caps press for 160ms when their key or the matching handle method fires. Thinking carries a calm shimmer.",
        "Reduced motion removes the fades, the ink merge, the shimmer and the height tween and keeps every state, text and announcement. The preview plays once and stops instead of looping, and it pauses while off screen."
      ],
      "responsive": [
        "The field fills its container's width; the text and ghost wrap identically at any width, so the suggestion always starts at the caret.",
        "Below 520px of field width the Cmd+Right and Escape hints step aside and only the chip remains; the footer note truncates with an ellipsis.",
        "On touch screens the chip reads Tap to accept and accepts on tap.",
        "The single-line variant scrolls sideways with the caret and keeps the chip inline at its end."
      ],
      "performance": [
        "One debounced request at a time: each keystroke clears the timer and aborts the in-flight request, so a fast typist triggers nothing until they pause.",
        "Typing through a matching suggestion trims it locally without a new request.",
        "The ghost renders as a few spans in a mirror behind a native textarea, so typing stays native-fast and works with spellcheck, IME, undo and autofill.",
        "The height and the chip width animate motion values directly; ResizeObserver measures, never a layout read mid-animation."
      ],
      "aiNotes": [
        "Use a fast, cheap model for suggest (Claude Haiku 5.5, Gemini 3.8 Flash or DeepSeek V4.1 Flash): people expect ghost text within about half a second of pausing. Cap max tokens to a sentence or a short line.",
        "Prompt for a continuation only: no preamble, no repetition of the input, and include a leading space when the text ends mid-sentence without one. The component strips a leading space if the text already ends with whitespace.",
        "Always pass signal to fetch. The component aborts on every keystroke, caret move, blur and accept, and ignores late chunks.",
        "Return null synchronously for contexts you never want to complete (inside a code block, after a URL), so no Thinking state flashes.",
        "Log onAccept and onDismiss to measure acceptance rate per model and prompt."
      ],
      "related": [
        "textarea",
        "input",
        "ai-composer",
        "code-suggestion",
        "text-shimmer",
        "ai-chat"
      ],
      "usage": "import { GhostText, ClaudeMark } from \"@/components/arc/ghost-text/ghost-text\";\n\n// Stream a continuation from your API route (any model works).\nasync function* suggest(text: string, signal: AbortSignal) {\n  const response = await fetch(\"/api/autocomplete\", { method: \"POST\", body: JSON.stringify({ text }), signal });\n  if (!response.ok || !response.body) throw new Error(\"Autocomplete is unavailable\");\n  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();\n  for (;;) {\n    const { value, done } = await reader.read();\n    if (done) return;\n    yield value;\n  }\n}\n\nexport function PullRequestDescription() {\n  const [body, setBody] = useState(\"\");\n  return (\n    <GhostText\n      value={body}\n      onValueChange={setBody}\n      suggest={suggest}\n      rows={6}\n      placeholder=\"Describe what changed and why\"\n      label=\"Description\"\n      model={{ name: \"Claude Haiku 5.5\", mark: <ClaudeMark style={{ color: \"#D97757\" }} /> }}\n      onAccept={(text, mode) => track(\"autocomplete_accept\", { mode, chars: text.length })}\n    />\n  );\n}\n\n// Single-line, uncontrolled, whole suggestions from a promise.\n<GhostText multiline={false} defaultValue=\"\" aria-label=\"Subject\" suggest={(text, signal) => api.complete(text, { signal })} />\n\n// Flush inside your own card: the card owns the border and focus style.\n<div className=\"compose-card\">\n  <ComposeHeader />\n  <GhostText variant=\"plain\" value={body} onValueChange={setBody} suggest={suggest} aria-label=\"Message\" />\n</div>\n\n// With no props it plays the simulated pull request card.\n<GhostText />",
      "exports": [
        "GhostText",
        "createDemoSuggest"
      ]
    },
    {
      "name": "image-generation",
      "kind": "component",
      "title": "Image generation",
      "description": "An image studio where each image forms on a dot matrix: a soft halftone wave breathes across the grid, the dots take the colours of the coming photo and grow into a coloured halftone, then blur away into the sharp photo. One large canvas with a mono size label, a filmstrip of four variations, a prompt bar with model and ratio, and upscale with a sweeping dot band, vary or download, with real provider marks and a built-in simulation.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/image-generation",
      "markdown": "https://uiarc.dev/components/image-generation/markdown",
      "registry": null,
      "keywords": [
        "image generation",
        "text to image",
        "ai image",
        "diffusion",
        "prompt",
        "upscale",
        "variations",
        "filmstrip",
        "generate images",
        "aspect ratio",
        "dot matrix",
        "halftone",
        "loading state",
        "dot spinner",
        "blur to sharp"
      ],
      "whenToUse": [
        "A text to image feature inside a product: marketing asset tools, listing photo helpers, slide or doc illustration.",
        "Any flow where people compare a few candidates and continue with one (upscale, vary, download).",
        "Prototyping an image model integration before the backend exists, using the built in simulation."
      ],
      "whenNotToUse": [
        "A full canvas editor with layers, masks and inpainting; build on a canvas library instead.",
        "Drawing a sketch to turn into an image; use sketch-to-image.",
        "Showing a single image with a loading state and no generation controls; use blur-image."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "Model and aspect chips and the filmstrip are radio groups with a roving tab stop; collapsed model chips keep their name as the accessible label and in a tooltip.",
        "The canvas is labelled with the image description once done, or with the variation's state while it generates, fails or stops; each thumbnail names its number and state.",
        "A polite live region reports starts, finishes, failures, picks, upscales and downloads, and says when results are simulated.",
        "Status is never color alone: failures pair a danger icon with \"Failed\" and the error, stops a square icon with \"Stopped\", the chosen thumbnail an accent ring with aria-checked, progress a spinner with a percentage.",
        "Icon only actions at narrow widths keep Upscale, Vary and Download as their accessible names."
      ],
      "motion": [
        "Each image loads on a canvas of small neutral dots, drawn at device pixel ratio. A soft radial halftone wave breathes and wanders across the grid: dots under it grow and darken, and a blurred twin of them glows like a lens.",
        "From 20% the dots take the colour of the coming photo at their cell (the preview downscaled to the grid, cover fitted), showing first under the wave, and grow by the photo's darkness, so by 78% the image reads as a coloured halftone.",
        "Over the last fifth the dots blur and fade while the photo sharpens out of them from 12px blur and 4% zoom. The final frame is the photo, and the real <img> lands on it without a cut; an image that differs from the preview resolves from soft to sharp in 480ms.",
        "The filmstrip thumbnails run their own small dot fields, so all four variations show their progress at once. Picking another variation crossfades the canvas in 200ms.",
        "Upscale sweeps a vertical band of halftone dots in the photo's colours left to right: soft ahead of it, sharp behind it, the dots and their blurred twin in it, all from one motion value. The size label crossfades to the new size.",
        "The status pill carries a 3×3 dot spinner whose dots light in turn around the ring, with a mono percentage; pills crossfade by key so a label never resizes in a frame.",
        "The canvas springs to a new aspect ratio; a container resize cuts straight to the new size. Labels that change inside buttons (Generate and Stop, Upscale, Upscaling, Upscaled, Download and Saved) are stacked in one cell and crossfade, so no button changes width.",
        "Reduced motion draws a still dot grid with the halftone glow at rest, crossfades the photo in by progress, keeps the spinner still and drops the upscale sweep."
      ],
      "responsive": [
        "The card fills its container up to 600px. The canvas keeps the batch's ratio and caps its height at 440px, so portrait shapes stay compact.",
        "Below 500px of card width the actions drop their labels to icons, so the four thumbnails always fit beside them; thumbnails shrink and stay square.",
        "Below 440px the model chips and Generate share the first row of the prompt bar and the ratio chips move to a second."
      ],
      "performance": [
        "Progress is written straight to motion values, the canvas and styles; React re-renders only on state changes (preview arrives, image finishes).",
        "The large canvas draws about four thousand dots per frame with one 2D context plus a blurred twin for the wave; thumbnails draw under two hundred.",
        "The preview is sampled once per grid size and cached; a cross origin image without CORS keeps the neutral dots.",
        "A loop runs only while its image generates and is on screen; finished, failed and stopped fields draw once and stop, and redraw on theme change."
      ],
      "aiNotes": [
        "Wire onGenerate to your image API; it runs once per variation, so a provider that returns four images per call can be wrapped by caching one request and resolving each index from it.",
        "Use request.variationOf to pass a seed or reference image for Vary. Return width and height so the footer and file names show the real size.",
        "Keep model names to what your backend actually serves; the defaults are generic product names with provider marks.",
        "Pair with a gallery or asset picker by listening to onSelect; the component holds only the current batch."
      ],
      "related": [
        "model-picker",
        "ai-composer",
        "text-shimmer"
      ],
      "usage": "import { ImageGeneration } from \"@/components/arc/image-generation/image-generation\";\n\nexport function Studio() {\n  return <ImageGeneration\n    onGenerate={async (request, { signal, onProgress }) => {\n      const job = await fetch(\"/api/images\", {\n        method: \"POST\",\n        signal,\n        body: JSON.stringify({ prompt: request.prompt, model: request.model.id, size: `${request.aspect.width}x${request.aspect.height}`, seedFrom: request.variationOf?.id }),\n      }).then(response => response.json());\n      onProgress(1);\n      return { id: job.id, src: job.url, alt: request.prompt, width: request.aspect.width, height: request.aspect.height };\n    }}\n    onDownload={image => window.open(image.src)}\n  />;\n}",
      "exports": [
        "ImageGeneration",
        "simulateImageGeneration"
      ]
    },
    {
      "name": "dictation-button",
      "kind": "component",
      "title": "Dictation button",
      "description": "A chat composer with voice input, where the mic springs into a recording pill with a smooth live waveform, a timer, and cancel and insert buttons. Words stream into the field as you speak, muted until the recognizer settles them.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/dictation-button",
      "markdown": "https://uiarc.dev/components/dictation-button/markdown",
      "registry": null,
      "keywords": [
        "dictation",
        "voice input",
        "speech to text",
        "mic button",
        "microphone",
        "voice typing",
        "transcription",
        "web speech api",
        "speechrecognition",
        "waveform",
        "recording pill",
        "composer",
        "chat input",
        "interim results",
        "AI composer"
      ],
      "whenToUse": [
        "Chat and agent composers where people would rather talk than type: mobile, long prompts, accessibility.",
        "Notes, comments and support replies where dictation saves time and the person still edits before sending.",
        "Any field that already has a send action and needs a voice entry point with clear listening state."
      ],
      "whenNotToUse": [
        "Full duplex voice conversations with spoken replies; use a voice agent pattern with call controls instead.",
        "Recording audio files to keep (voice memos); this inserts text and discards audio.",
        "Command palettes where a single spoken command should run immediately; confirm hands-free from onSpeechEnd only if you are sure."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "A polite live region announces listening (and that the default recognizer is simulated, nothing is recorded), transcribing, the inserted word count, cancel, send and every error with how to fix it. Streaming words are not announced, to keep screen readers quiet.",
        "Status is never color alone: denied shows a mic-off icon and \"Microphone blocked in site settings\"; errors show an alert icon and a short reason; Try again keeps the same icon.",
        "Only the visible face of the pill is focusable (the other faces are inert), and focus follows the action: mic to Insert, Insert back to the field at the end of the text, cancel back to the mic, a failed start to Try again.",
        "The elapsed time is a role=\"timer\" with tabular numerals in a reserved width; the waveform is decorative and hidden from assistive tech.",
        "Mic, cancel and Insert are icon buttons with accessible names and tooltips (Dictate, Cancel, Insert, Transcribing).",
        "Interim words use a muted tone between --text-muted and --text-secondary and settle to --foreground, so the difference reads in both themes without relying on hue."
      ],
      "motion": [
        "The mic springs leftward into the recording pill (up to 216px, measured from the toolbar) with a critically damped spring (0.24s visual duration, no bounce); Try again springs to its measured label width. The mic's spot becomes Insert, so the next action sits where the pointer already is.",
        "Faces crossfade (out 100ms, in 160ms). The toolbar label swaps with a single 2px crossfade. Insert crossfades its check into a spinner while transcribing.",
        "Words only fade in (140ms). A corrected interim guess refades just the words that changed. The field springs to each new line.",
        "The waveform fits as many 3px rounded bars as the track holds with an even 2px gap, so no bar is ever clipped. A level sample enters on the right every 60ms and each bar eases toward it every frame, so heights glide without steps or jitter, written straight to the DOM with no React renders. When listening stops, the bars settle in place to a row of dots.",
        "Reduced motion: no size springs, no word fades, the waveform stops travelling and pulses in place with the voice. Loops pause off screen and in background tabs."
      ],
      "responsive": [
        "Fluid to 640px. The pill keeps a fixed 216px so nothing reflows while recording; at 360px the toolbar label gives way while recording.",
        "Container queries, not viewport: keyboard hints hide under 460px of card width and error copy shortens under 400px.",
        "All targets are 36px in the toolbar; the pill's cancel and insert are 28px inside a 36px pill with 4px padding.",
        "The field grows to six lines, then scrolls, and keeps the newest words in view while dictating."
      ],
      "performance": [
        "Levels live in a ref and the waveform and timer write to the DOM directly, so a session renders React only when the transcript changes.",
        "The waveform frame loop stops once the bars have settled and pauses while off screen or in a background tab.",
        "The Web Speech adapter releases the microphone tracks and closes the AudioContext on stop, cancel, error and unmount.",
        "No audio is buffered in memory; text arrives as strings from the recognizer."
      ],
      "aiNotes": [
        "Use DictationButton when you need the whole composer; use DictationControl inside an existing composer and render onTranscript yourself with interim text muted.",
        "The default recognizer is simulated. For production pass createWebSpeechRecognizer() (free, browser built-in, Chrome sends audio to Google) or write a DictationRecognizer for Deepgram, AssemblyAI, OpenAI or Whisper over a websocket; the contract is five callbacks and an AbortSignal.",
        "Handle status \"denied\" in product copy: the component already says where to fix it, but you may want to link to your help article.",
        "Do not pass autoPlay in production; it is for docs and marketing demos and defaults to off with a real recognizer.",
        "Keep the model label honest: pass the model the composer actually sends to, or null."
      ],
      "related": [
        "chat-thread",
        "text-shimmer",
        "tooltip",
        "tool-call",
        "agent-status"
      ],
      "usage": "import { DictationButton, createWebSpeechRecognizer } from \"@/components/arc/dictation-button/dictation-button\";\n\nconst recognizer = createWebSpeechRecognizer({ lang: \"en-US\" });\n\nexport function Composer({ onSubmit }: { onSubmit: (text: string) => Promise<void> }) {\n  return <DictationButton\n    recognizer={recognizer}\n    placeholder=\"Message Claude\"\n    onSend={onSubmit}\n    onStatusChange={(status, error) => { if (status === \"denied\") console.info(\"Microphone blocked\", error); }}\n  />;\n}",
      "examples": [
        {
          "title": "Only the control, in your own composer",
          "code": "import { useState } from \"react\";\nimport { DictationControl } from \"@/components/arc/dictation-button/dictation-button\";\n\nexport function Field() {\n  const [text, setText] = useState(\"\");\n  const [live, setLive] = useState({ final: \"\", interim: \"\" });\n  return <div className=\"composer\">\n    <p>{text} {live.final} <span className=\"muted\">{live.interim}</span></p>\n    <DictationControl\n      onTranscript={setLive}\n      onCommit={final => { setText(t => (t ? t + \" \" : \"\") + final); setLive({ final: \"\", interim: \"\" }); }}\n      onCancel={() => setLive({ final: \"\", interim: \"\" })}\n    />\n  </div>;\n}"
        },
        {
          "title": "Your own speech service",
          "code": "import { DictationError, type DictationRecognizer } from \"@/components/arc/dictation-button/dictation-button\";\n\nexport const deepgram: DictationRecognizer = async (handlers, signal) => {\n  const stream = await navigator.mediaDevices.getUserMedia({ audio: true }).catch(() => { throw new DictationError(\"not-allowed\"); });\n  const socket = new WebSocket(\"wss://your-proxy.example.com/listen\");\n  socket.onmessage = event => {\n    const { final, interim, level } = JSON.parse(event.data);\n    handlers.onTranscript({ final, interim });\n    handlers.onLevel(level);\n  };\n  signal.addEventListener(\"abort\", () => { socket.close(); stream.getTracks().forEach(t => t.stop()); });\n  // ...pipe stream audio into the socket...\n  return { stop: () => new Promise(resolve => { socket.onclose = () => resolve(lastFinal); socket.send(\"finalize\"); }) };\n};"
        }
      ],
      "exports": [
        "DictationButton",
        "DictationControl",
        "useDictation",
        "DictationRecognizer"
      ]
    },
    {
      "name": "speech-player",
      "kind": "component",
      "title": "Speech player",
      "description": "A read-aloud player for AI replies. The Read aloud button in the message's action row turns into a compact player with play, a scrubbable progress bar, speed and a voice picker, and a highlight follows the spoken word through the reply, karaoke style.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/speech-player",
      "markdown": "https://uiarc.dev/components/speech-player/markdown",
      "registry": null,
      "keywords": [
        "text to speech",
        "tts",
        "read aloud",
        "speech player",
        "karaoke",
        "word highlight",
        "audio player",
        "voice picker",
        "playback speed",
        "ai reply",
        "accessibility",
        "elevenlabs",
        "openai tts",
        "claude",
        "message actions"
      ],
      "whenToUse": [
        "A chat or answer surface where people want a reply read out, with the spoken word followed in the text.",
        "Accessibility and hands free reading for long AI answers, summaries or briefings.",
        "Previewing TTS voices and speeds against real product copy."
      ],
      "whenNotToUse": [
        "Use dictation-button to turn speech into text in a composer.",
        "Use a plain audio player for podcasts or long recordings without a transcript to highlight."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The player is a labelled group. The play button's name follows its state: Play, Pause, Play again, Retry, or Preparing audio (aria-disabled while a request is in flight).",
        "The progress bar is a slider with a value text such as \"0:04 of 0:22\", updated once a second rather than every frame.",
        "A polite status region announces what changed: reading with Ava at 1× (simulated, no audio plays), paused at 0:12, switching to Leo, finished, the failure message, speed changes and feedback.",
        "Status never relies on color: the error pairs an alert icon with \"Couldn't generate audio\" and the reason; dimmed upcoming words keep body contrast and are only a progress cue.",
        "Read aloud is described by the reply text. The voice menu uses menuitemradio items with aria-checked and opens with focus on the selected voice.",
        "The hidden layer of the dock is inert, so Tab never lands on a control that is not visible. Pointer focus draws no ring; keyboard focus uses Arc's shared focus ring."
      ],
      "motion": [
        "Read aloud and Stop reading crossfade the action row and the player in the same 32px slot (out in 100ms, in 160ms with a 2px rise), so the card never changes size.",
        "The word highlight is one rect that glides between words on a line in 150ms at 1× (about 100ms at 1.5× and 75ms at 2×, so it always lands before the next word) and cuts to the next line instead of sweeping across it. Spoken words fade from dimmed to full color.",
        "The progress bar fill and thumb are written straight to transforms on each animation frame; the track thickens and the thumb scales in on hover, focus and drag.",
        "Play, pause, replay, retry and the spinner swap in place with a bounce free scale and fade. Speed and voice labels crossfade over hidden copies of every option, so their buttons never resize.",
        "The preparing label carries a calm shimmer. Reduced motion keeps every state and announcement, drops the glide, the shimmer and the sample loop, and moves the highlight instantly."
      ],
      "responsive": [
        "The card fills its container up to 640px; the reply wraps and the highlight re-measures on resize without animating.",
        "Below 440px of width the total time steps aside so the progress bar keeps room; elapsed time, speed, voice and Stop reading stay.",
        "On touch screens the row grows to 36px so every control meets the touch target size."
      ],
      "performance": [
        "One requestAnimationFrame loop runs only while playing. It writes the bar to the DOM and commits React state only when the spoken word or the whole second changes.",
        "Highlight positions come from offsetLeft and offsetTop of the current word, read once per word, never per frame.",
        "Fetched audio is kept per voice, so replaying after Stop reading starts instantly; switching voices aborts the previous request."
      ],
      "aiNotes": [
        "Pass synthesize for real audio. Most TTS providers can return word or character alignment; convert it to one { start, end } per whitespace separated word of text, in milliseconds. If yours cannot, return src alone and the timings are estimated and fitted to the audio length.",
        "Keep text exactly what was spoken: the word count of text must match timings. Strip markdown before passing it.",
        "Throw an Error with a short, human message from synthesize to show it next to \"Couldn't generate audio\"; the play button becomes Retry.",
        "Use variant=\"plain\" inside a chat transcript and pass your own actions so the row matches the rest of the message actions.",
        "Without synthesize the player is a timed simulation that plays no sound; keep the simulated note so nobody thinks audio is broken."
      ],
      "related": [
        "ai-chat",
        "ai-answer",
        "dictation-button",
        "copy-button",
        "text-shimmer",
        "meeting-summary"
      ],
      "usage": "import { SpeechPlayer } from \"@/components/arc/speech-player/speech-player\";\n\nexport function AssistantMessage({ message }) {\n  return (\n    <SpeechPlayer\n      variant=\"plain\"\n      text={message.text}\n      author={{ name: \"Claude Sonnet 5.5\", mark: <ClaudeMark style={{ color: \"#D97757\" }} /> }}\n      voices={[\n        { id: \"ava\", name: \"Ava\", description: \"Warm and conversational\" },\n        { id: \"leo\", name: \"Leo\", description: \"Calm, lower register\" },\n      ]}\n      synthesize={async ({ text, voice, signal }) => {\n        const res = await fetch(\"/api/tts\", { method: \"POST\", body: JSON.stringify({ text, voice: voice.id }), signal });\n        if (!res.ok) throw new Error(\"The voice service is busy. Try again in a moment.\");\n        const { audioUrl, words } = await res.json(); // words: [{ start, end }] in ms\n        return { src: audioUrl, timings: words };\n      }}\n      onPlay={e => track(\"tts_play\", { voice: e.voice.id, speed: e.speed })}\n      actions={<><CopyButton value={message.text} iconOnly variant=\"plain\" /><RegenerateButton /></>}\n    />\n  );\n}\n\n// With no props it reads the sample reply, simulated, with no audio.\n<SpeechPlayer />",
      "exports": [
        "SpeechPlayer",
        "simulateSpeech"
      ]
    },
    {
      "name": "meeting-summary",
      "kind": "component",
      "title": "Meeting summary",
      "description": "An AI meeting notes card: the header shows the Google Meet or Zoom mark, the attendees and the duration. Summary, Decisions and Action items stream in after a short summarizing pass. Hover any line to see the transcript quote it came from, then check off action items or send them to Linear or Notion.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/meeting-summary",
      "markdown": "https://uiarc.dev/components/meeting-summary/markdown",
      "registry": null,
      "keywords": [
        "meeting summary",
        "meeting notes",
        "AI notes",
        "action items",
        "call recap",
        "transcript summary",
        "Google Meet",
        "Zoom",
        "Linear",
        "Notion",
        "decisions",
        "AI meeting assistant"
      ],
      "whenToUse": [
        "Showing AI notes after a call in a CRM, a recruiting tool, a meeting recorder or a team workspace.",
        "Turning a transcript into decisions and owned tasks that people can push to their tracker.",
        "Any summary where readers should be able to check a point against what was actually said."
      ],
      "whenNotToUse": [
        "Live captions or a running transcript during the call; use text-stream or a transcript view.",
        "A general task list with no meeting behind it; use checkbox rows in your own list.",
        "Long documents with many sections; render them as a document page instead of a card."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is an article named after the meeting, with h2 for the title and h3 for each section.",
        "Timestamps are buttons with aria-expanded; the open quote is a role=\"tooltip\" referenced by aria-describedby.",
        "Each checkbox is a native button with role=\"checkbox\" and aria-checked, and each checkbox and send button is labeled with the action item text, so a list of them still reads clearly.",
        "The 16px checkbox has a 32px hit area, and the keyboard focus ring hugs the square itself.",
        "A polite live region announces summarizing, the finished summary, checked items and sends, and says \"Simulated, nothing was sent\" when no onSend is wired.",
        "Status never relies on color: a failed send shows an alert icon with a retry label, a sent item shows the target mark, its reference and a check.",
        "The platform name is read before the date, and owners shown by first name on narrow cards keep their full name for screen readers."
      ],
      "motion": [
        "Summarizing lasts about one second with a shimmer on the label, then notes stream 3 to 8 words every 34ms; each chunk only fades in.",
        "The body springs to its new height between loading, notes, error and empty (0.24s, no bounce). Owner, date and send controls reserve their space while text streams, so nothing shifts when they fade in.",
        "Checking an item grows the accent fill in (160ms) and draws the check on (200ms ease-out), while a 1px strikethrough sweeps left to right across every wrapped line and the text eases to muted. Unchecking reverses it faster. A press scales the square to 0.9, never the row.",
        "Send buttons crossfade mark to spinner to a sent chip while their slot springs to the new width.",
        "Quotes fade in over 160ms with a 3px offset after a short hover intent, then switch instantly between lines.",
        "Reduced motion skips the stream, size springs, check drawing and strikethrough sweep and keeps every end state and announcement. The pass waits until the card is on screen. The sample sends one item to Linear and ticks off another once, unless someone has already used the card."
      ],
      "responsive": [
        "Holds from 360px to its 640px max width. Below 460px the attendee stack moves under the date; below 520px owners show their first name so meta stays on one line.",
        "Quotes open below their line and flip above when the viewport runs out of room.",
        "Hover quotes need a fine pointer; on touch, tapping a timestamp pins the quote and tapping elsewhere closes it."
      ],
      "performance": [
        "Streaming re-renders only the card for about 1.5 seconds, using one interval. Chunk animations are CSS keyframes.",
        "One ResizeObserver for the body height and one per send slot; all are released on unmount and in-flight sends abort.",
        "Marks are inline SVG, no network requests besides attendee portraits."
      ],
      "aiNotes": [
        "Use for post-meeting notes from a recorder or transcript pipeline. Map your model output to MeetingSummaryData; keep quote.at in seconds from the start of the recording.",
        "Drive status yourself when the summary comes from a server: summarizing while the request runs, ready when it resolves, error with a message on failure.",
        "Wire onSend to the Linear issueCreate mutation or a Notion pages.create call and resolve with the issue key or page title; pass the AbortSignal through to fetch.",
        "Dates are naive local strings printed the same on server and client. Format time zones before passing them in."
      ],
      "related": [
        "tool-call",
        "agent-status",
        "checkbox",
        "avatar",
        "copy-button"
      ],
      "usage": "import { MeetingSummary } from \"@/components/arc/meeting-summary/meeting-summary\";\n\nexport function CallNotes({ meeting, status, createIssue }) {\n  return (\n    <MeetingSummary\n      meeting={meeting}\n      status={status}\n      onSend={(item, target, signal) => createIssue({ title: item.text, target, signal })}\n      onActionItemChange={(item, done) => console.log(item.id, done)}\n    />\n  );\n}",
      "exports": [
        "MeetingSummary"
      ]
    },
    {
      "name": "reply-draft",
      "kind": "component",
      "title": "Reply draft",
      "description": "An AI email reply drafter where Claude streams a reply under a received Gmail message. Quick replies steer a fresh draft, tone and length rewrite it with a single crossfade, and you can edit the finished draft before you insert it.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/reply-draft",
      "markdown": "https://uiarc.dev/components/reply-draft/markdown",
      "registry": null,
      "keywords": [
        "ai reply",
        "email reply",
        "smart reply",
        "draft reply",
        "help me write",
        "gmail ai",
        "tone",
        "rewrite",
        "quick replies",
        "streaming text",
        "claude",
        "inbox ai",
        "support reply"
      ],
      "whenToUse": [
        "Drafting a reply to an email, ticket or message with an AI model, where the reader reviews and edits before sending.",
        "Offering quick reply intents plus tone and length controls for generated text.",
        "Showing an AI writing feature on a landing page with a realistic, self-playing example."
      ],
      "whenNotToUse": [
        "Free-form chat with a model: use a chat or composer component instead.",
        "Inline autocomplete while typing: use ghost-text.",
        "Sending the email itself: this component inserts a draft; your composer owns sending."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is an article labelled with the sender; the reply area is a labelled section.",
        "Quick replies are toggle buttons with aria-pressed; tone and length are radio groups with roving tabindex.",
        "The finished draft is a plain-text editable textbox (contenteditable plaintext-only), so readers can fix a word before Insert. Edits are what onInsert receives.",
        "A polite live region announces drafting, rewriting, ready, inserted and errors. Simulated actions say nothing was sent.",
        "The body is aria-busy while drafting. Status uses an icon plus a short label, never color alone."
      ],
      "motion": [
        "Drafts stream in chunks of 3 to 8 words every 34ms; each chunk only fades in over 140ms.",
        "Tone and length rewrites crossfade once: the old draft dims during the short rewrite, fades out in 100ms and the new one fades in over 160ms with a 2px rise.",
        "The draft box springs to its measured height with no bounce; during a regenerate its height is held until the new draft finishes, so the card never jumps.",
        "The selected tone and length pills glide with a bounce-free spring. Status labels crossfade; working labels use a calm shimmer.",
        "Reduced motion: drafts appear whole, crossfades and the height spring are instant, chunk fades and the press scale are dropped."
      ],
      "responsive": [
        "Fills its container up to 600px. Below 480px the card padding drops to 16px and the email body loses its avatar indent.",
        "Quick replies and the tone and length groups wrap; under 460px the divider between the groups hides and Length moves to its own row.",
        "Touch targets grow to 32px for options and 36px for quick replies on coarse pointers. Long emails and names truncate or wrap without horizontal scroll."
      ],
      "performance": [
        "One state update per streamed chunk; chunks are plain spans with a CSS fade, swapped for a single text node when the draft finishes.",
        "The autoplay loop and auto start use an IntersectionObserver and stay idle off screen.",
        "No network calls by default; the simulation lives in reply-draft-data.ts."
      ],
      "aiNotes": [
        "Use for AI-assisted replies in inboxes, support desks, CRMs and chat tools. Pass the real message and wire generate to your model with the AbortSignal; every new action aborts the previous request.",
        "generate can return a Promise<string> (it is chunk-streamed locally) or an async iterable of text deltas from a real stream. mode \"rewrite\" means restyle the previous text for the new tone or length.",
        "Map suggestions to intents your prompt understands (ids are stable, labels are what readers see). Keep labels to two or three words.",
        "Leave autoPlay off in products; it is for marketing and docs demos."
      ],
      "related": [
        "ai-composer",
        "ghost-text",
        "tool-call",
        "agent-status",
        "text-shimmer",
        "segmented-control"
      ],
      "usage": "import { ReplyDraft } from \"@/components/arc/reply-draft/reply-draft\";\n\nexport function Inbox({ email }) {\n  return (\n    <ReplyDraft\n      message={email}\n      suggestions={[{ id: \"yes\", label: \"Sounds good\" }, { id: \"later\", label: \"Next week\" }]}\n      model={{ name: \"Claude Sonnet 5.5\" }}\n      generate={async function* ({ message, tone, length, suggestion, signal }) {\n        const res = await fetch(\"/api/reply\", { method: \"POST\", body: JSON.stringify({ id: email.id, tone, length, hint: suggestion?.label }), signal });\n        const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();\n        for (;;) { const { value, done } = await reader.read(); if (done) return; yield value; }\n      }}\n      onInsert={draft => composer.insert(draft)}\n    />\n  );\n}",
      "exports": [
        "ReplyDraft"
      ]
    },
    {
      "name": "agent-handoff",
      "kind": "component",
      "title": "Agent handoff",
      "description": "A support conversation where an AI assistant hands off to a person. When it transfers, a card shows the summary it passes to the team and the typical wait. The person then joins and takes over, and a thin timeline on the left marks each step.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-handoff",
      "markdown": "https://uiarc.dev/components/agent-handoff/markdown",
      "registry": null,
      "keywords": [
        "agent handoff",
        "human handoff",
        "AI escalation",
        "transfer to human",
        "support chat",
        "live chat",
        "Intercom",
        "Zendesk",
        "AI support agent",
        "customer support widget",
        "escalation summary"
      ],
      "whenToUse": [
        "A support widget where the AI answers first and escalates to a human agent.",
        "Showing customers what the assistant shared with the team, so they do not repeat themselves.",
        "Marketing or docs pages that explain an AI to human handoff flow."
      ],
      "whenNotToUse": [
        "A general chat with one assistant and no handoff: use chat-thread.",
        "Showing an agent's internal steps or tool calls: use agent-timeline or tool-call.",
        "A full help desk inbox for agents: build it from list and detail layouts instead."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The thread is a focusable log so keyboard users can scroll it; it does not announce every streamed chunk.",
        "Stage changes and action results are announced once through a polite status region, including that the demo is simulated.",
        "Every status pairs an icon with a label: Transferring, With Jordan, Resolved, No one available. Color is never the only signal.",
        "The summary toggle uses aria-expanded and aria-controls. Typing dots carry a label such as Jordan is typing.",
        "Marks and avatars on the timeline are decorative next to their visible labels."
      ],
      "motion": [
        "Assistant replies stream in chunks of 3 to 8 words every ~34ms; each chunk only fades in over 140ms.",
        "The thread animates to its measured height with a bounce-free spring (0.24s), so new rows and streamed lines push it up without a snap.",
        "While transferring, a small accent dot travels down the timeline beside the handoff card.",
        "Status labels crossfade (100ms out, 160ms in, 2px rise). Loops pause off screen.",
        "Reduced motion: replies appear whole, no travel dot, typing dots and spinner hold still, no height animation, the demo plays once."
      ],
      "responsive": [
        "Fills its container up to 560px wide; holds up at 360px with tighter padding and wider bubbles.",
        "Fixed height with an internal scroll pinned to the newest message, so the page never shifts as the thread grows.",
        "Long names and meta lines truncate with an ellipsis; message text wraps."
      ],
      "performance": [
        "One ResizeObserver and one IntersectionObserver per card. Streaming updates re-render only the message being streamed and its chunks.",
        "CSS keyframes for the dots, spinner and travel dot; all pause off screen.",
        "For very long threads, pass only the recent messages; the card is meant for a live conversation, not an archive."
      ],
      "aiNotes": [
        "Use for support or sales chat where an AI assistant escalates to a person. Keep the assistant's messages in `messages` and everything after the handoff in `agentMessages`.",
        "Drive `stage` from your backend: transferring when you create the ticket, connected when a person accepts, unavailable when the queue times out.",
        "Stream assistant text by updating the message's `text` with `streaming: true`; the component fades each appended chunk.",
        "Pass `summary` with the same facts you send to the help desk so the customer sees what was shared.",
        "Do not use `scenario` outside demos; it only affects the built in simulation."
      ],
      "related": [
        "chat-thread",
        "agent-status",
        "agent-timeline",
        "tool-call",
        "avatar"
      ],
      "usage": "import { AgentHandoff } from \"@/components/arc/agent-handoff/agent-handoff\";\n\n// Demo: plays a simulated support chat\n<AgentHandoff />\n\n// Wired to your support backend\n<AgentHandoff\n  stage={conversation.stage}\n  title=\"Order NW-48213\"\n  subtitle=\"Northwind Home support\"\n  messages={conversation.aiMessages}\n  agentMessages={conversation.agentMessages}\n  typing={conversation.typing}\n  summary={{\n    issue: \"Desk arrived with a cracked leg. Customer wants a full refund.\",\n    fields: [{ label: \"Order\", value: \"NW-48213\" }, { label: \"Asks for\", value: \"Full refund\" }],\n  }}\n  agent={{ name: \"Jordan Reyes\", role: \"Support specialist\", avatar: \"/jordan.jpg\", joinedAt: \"14:05\" }}\n  provider=\"intercom\"\n  onSend={text => sendMessage(text)}\n  onRequestHuman={() => escalate()}\n  onCancelTransfer={() => cancelEscalation()}\n  onRetry={() => escalate()}\n/>",
      "exports": [
        "AgentHandoff",
        "useHandoffSimulation"
      ]
    },
    {
      "name": "pii-redaction",
      "kind": "component",
      "title": "PII redaction",
      "description": "A guardrail card where personal data is redacted before the prompt reaches the model. Scan marks each name, email, phone number and card with its type and swaps it for a placeholder like [EMAIL_1]. The safe prompt travels to Claude, and the reply comes back with the original values put back for the person.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/pii-redaction",
      "markdown": "https://uiarc.dev/components/pii-redaction/markdown",
      "registry": null,
      "keywords": [
        "pii",
        "redaction",
        "guardrail",
        "privacy",
        "data loss prevention",
        "dlp",
        "anonymize",
        "mask",
        "placeholder",
        "llm safety",
        "prompt",
        "claude",
        "personal data",
        "gdpr",
        "support assistant"
      ],
      "whenToUse": [
        "Explaining or proving a privacy guardrail in an AI product: support assistants, health or finance copilots, enterprise chat.",
        "A settings or onboarding screen where admins see what the PII filter does before they turn it on.",
        "A playground where people paste text and see exactly what would reach the model."
      ],
      "whenNotToUse": [
        "Use ai-chat or ai-composer for the conversation itself; redact behind the scenes and show this card only where the guardrail needs explaining.",
        "Do not rely on the built-in detector for compliance; pass a real DLP service through detect.",
        "Use tool-call to show an arbitrary tool step in a transcript."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A polite status region announces each phase (\"Scanning for personal data\", \"4 items redacted\", \"Reply restored\") and, when the reply is simulated, adds \"Simulated, nothing was sent\".",
        "Status never relies on color: every phase pairs an icon with a short label, each type cell says Name, Email, Phone or Card with its token or None found, and marks in the original message carry labels such as \"Email, redacted as EMAIL_1\".",
        "Scan, Stop, Send, Run again and Retry are one button whose label changes, so keyboard focus stays put through the whole flow. Reset returns focus to the message field.",
        "Only the visible text layer is exposed to assistive tech; the hidden layers that keep the height steady are aria-hidden.",
        "The flow strip is decorative and hidden from screen readers, since the status region already narrates each hop.",
        "The Show original label is a real label for the switch, and the switch is disabled with a muted label until there is a safe prompt to compare."
      ],
      "motion": [
        "Scan highlights each entity in turn, 120ms apart, with a 160ms tint in its type color while the matching type cell crossfades from Checking to its token.",
        "The swap to placeholders is a single crossfade between two stacked text layers (old out in 100ms, new in over 160ms), so no words slide or rewrap mid animation and the height never changes.",
        "Each hop of the flow is one dot traveling the connector in 460ms on an ease in out curve; the active node's label shimmers while it works.",
        "The reply streams three to six words every 34ms, each chunk only fading in, then crossfades once to the restored text.",
        "Labels, statuses and the Reset button crossfade in place; nothing bounces and nothing changes size between states.",
        "Reduced motion drops the travel, the stagger, the streaming and the autoplay loop, and keeps every state, label and announcement."
      ],
      "responsive": [
        "The card fills its container up to 640px. Below 520px the four type cells fall into two rows of two.",
        "Below 400px the card tightens its gaps and hides the Simulated tag (the status region still announces it); the model name truncates with an ellipsis in the flow.",
        "Text layers stack in one grid cell and the reply reserves the sample reply's height, so nothing jumps at 360px, 390px or desktop widths."
      ],
      "performance": [
        "Detection is a handful of regular expressions and a Luhn check, linear in the message length.",
        "The loop runs only while the card is at least a quarter on screen (IntersectionObserver) and stops for good on the first interaction.",
        "Highlights and layer swaps are CSS transitions on opacity and background color; the dot animates a transform; every pending step aborts through one AbortController."
      ],
      "aiNotes": [
        "Redact on the server in production: run detectPii and redactPii (or your DLP) before the prompt leaves your backend, keep entities in the request scope, and call restorePii on the reply. Use this card to show people what happens, or as the UI over that pipeline through detect and send.",
        "The built-in detector is a baseline. Names beyond the names list and simple introductions need an NER model or a DLP service passed through detect.",
        "Keep the same token for a repeated value within one conversation so the model can refer back to it; redactPii does this per call, so persist the entity map across turns if you need it.",
        "Instruct the model to keep placeholders verbatim (for example \"Refer to [EMAIL_1] exactly as written\") so restorePii can put the values back.",
        "Set autoPlay={false} in product UI and pass defaultMessage=\"\" for a blank field."
      ],
      "related": [
        "ai-chat",
        "ai-composer",
        "tool-call",
        "agent-approvals",
        "prompt-studio",
        "text-shimmer",
        "switch"
      ],
      "usage": "import { PiiRedaction, detectPii, redactPii, restorePii } from \"@/components/arc/pii-redaction/pii-redaction\";\n\nexport function SupportGuard({ user }) {\n  return (\n    <PiiRedaction\n      autoPlay={false}\n      defaultMessage=\"\"\n      names={[user.name]}\n      model={{ name: \"Claude Sonnet 5.5\" }}\n      detect={async (text, signal) => {\n        const res = await fetch(\"/api/dlp/scan\", { method: \"POST\", body: text, signal });\n        return res.json(); // [{ type: \"email\", start: 74, end: 100 }, ...]\n      }}\n      send={async (prompt, signal) => {\n        const res = await fetch(\"/api/chat\", { method: \"POST\", body: JSON.stringify({ prompt }), signal });\n        const { text } = await res.json();\n        return text; // may contain [NAME_1], [EMAIL_1], restored by the component\n      }}\n      onRedact={({ entities }) => audit.log(\"pii.redacted\", entities.map(e => e.type))}\n    />\n  );\n}\n\n// Server side, without the card:\nconst { prompt, entities } = redactPii(message, detectPii(message, { names: [user.name] }));\nconst reply = restorePii(await model.complete(prompt), entities);\n\n// With no props it plays the simulated support conversation.\n<PiiRedaction />",
      "exports": [
        "PiiRedaction",
        "detectPii, redactPii, restorePii"
      ]
    },
    {
      "name": "response-feedback",
      "kind": "component",
      "title": "Response feedback",
      "description": "A feedback control for AI replies where thumbs down opens an inline panel with reason chips and an optional note, thumbs up fills and asks what was good, and a submit shows a short thank-you before folding back into the row.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/response-feedback",
      "markdown": "https://uiarc.dev/components/response-feedback/markdown",
      "registry": null,
      "keywords": [
        "thumbs up",
        "thumbs down",
        "feedback",
        "ai feedback",
        "response rating",
        "message actions",
        "rlhf",
        "helpful",
        "chat feedback",
        "reasons",
        "llm evaluation"
      ],
      "whenToUse": [
        "Collecting thumbs up and down on AI replies in a chat, copilot or search answer.",
        "Gathering structured reasons (inaccurate, unsafe) for model evaluation or support triage.",
        "Adding a message action row with copy and regenerate next to feedback."
      ],
      "whenNotToUse": [
        "Star or numeric ratings of a product or session: use rating.",
        "General product feedback not tied to one reply: use feedback-morph.",
        "Long surveys with many questions: use a form or dialog."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "Thumbs are toggle buttons with aria-pressed and labels Good response and Bad response; aria-expanded and aria-controls point at the panel while it is open.",
        "Reason chips are toggle buttons with aria-pressed inside a labelled group. Selected chips invert to a solid fill, so the state never relies on hue.",
        "A keyboard press on thumbs down moves focus to the first reason; a pointer press leaves focus where it is. After a keyboard submit, focus lands on the thank-you line and returns to the thumb when the panel folds.",
        "A polite live region announces rating changes, sending, the thank-you state, errors, and says when nothing was sent because the send is simulated.",
        "Errors show an icon and text next to the actions, and Submit becomes Try again."
      ],
      "motion": [
        "The panel opens and closes on a bounce-free spring (0.24s visual duration) that animates measured height, with a 160ms opacity fade.",
        "Switching between the form and the thank-you line is a single crossfade (out 100ms, in 160ms, 2px rise) while the panel height springs to the new content.",
        "The filled thumb fades in over 160ms on top of the outline, so nothing moves or resizes. Presses scale to .94 instantly and release on a spring.",
        "The success tick draws once in 280ms.",
        "Reduced motion: height changes cut instantly, the panel only fades, and press scales are dropped. All end states and announcements stay."
      ],
      "responsive": [
        "The card pads 16px below 480px of container width and 24px above; the panel and chips wrap, so it holds at 360px without horizontal scroll.",
        "Below 400px of container width the privacy note and Cancel give way (Close covers Cancel), so the error line and Try again always fit on one row.",
        "Chips grow to 32px tall on coarse pointers."
      ],
      "performance": [
        "Two small motion trees (panel height and content crossfade) mount only while the panel is open.",
        "One ResizeObserver per open panel animates content height changes; it disconnects when the panel closes.",
        "No timers run while idle; the thank-you timer and in-flight sends are cleared on unmount."
      ],
      "aiNotes": [
        "Use ResponseFeedback for a single assistant reply with its actions; use FeedbackActions when you already render your own messages.",
        "Persist both stages: store the rating on stage \"rating\" (null means cleared) and attach reasons and the note on stage \"details\". Always pass messageId.",
        "Throw or reject from onFeedback to show the error state; the component handles Try again and aborts stale calls through the signal.",
        "Set disabled while the reply is still streaming, so people rate the finished answer.",
        "Keep reason labels short (one or two words) so the chips stay on one or two lines at 360px."
      ],
      "related": [
        "chat-thread",
        "copy-button",
        "tool-call",
        "agent-status",
        "rating"
      ],
      "usage": "import { FeedbackActions, ResponseFeedback } from \"@/components/arc/response-feedback/response-feedback\";\n\n// The full reply card\n<ResponseFeedback\n  model={{ name: \"Claude Opus 5.5\" }}\n  prompt={question}\n  copyText={answerText}\n  messageId={message.id}\n  onFeedback={async (event, { signal }) => {\n    await fetch(\"/api/feedback\", { method: \"POST\", body: JSON.stringify(event), signal });\n  }}\n>\n  <Markdown>{answerText}</Markdown>\n</ResponseFeedback>\n\n// Just the control, inside your own message row\n<FeedbackActions\n  messageId={message.id}\n  defaultRating={message.rating}\n  leading={<CopyButton value={answerText} iconOnly variant=\"plain\" label=\"Copy reply\" />}\n  onFeedback={saveFeedback}\n/>",
      "exports": [
        "ResponseFeedback",
        "FeedbackActions"
      ]
    },
    {
      "name": "llm-cost-calculator",
      "kind": "component",
      "title": "LLM cost calculator",
      "description": "A pricing calculator for LLM workloads. Set tokens per request, requests per day and prompt caching, and eight current models re-rank by monthly cost. Bars glide and prices roll as you drag.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/llm-cost-calculator",
      "markdown": "https://uiarc.dev/components/llm-cost-calculator/markdown",
      "registry": null,
      "keywords": [
        "llm cost calculator",
        "token pricing",
        "ai pricing",
        "model cost",
        "api cost estimate",
        "prompt caching",
        "cached input",
        "price comparison",
        "claude",
        "gpt",
        "gemini",
        "deepseek",
        "monthly cost",
        "budget",
        "slider"
      ],
      "whenToUse": [
        "A pricing or settings page needs an honest monthly estimate for an LLM workload across several models.",
        "People pick a model for a feature and should see how cost moves with prompt size, output length, traffic and caching.",
        "An internal tool compares provider prices before a migration or a budget review."
      ],
      "whenNotToUse": [
        "Use model-picker to choose a model in a chat or composer without a cost comparison.",
        "Use context-window to show the live token usage of one conversation against a model's context window.",
        "Use usage-meter or usage-pricing for actual metered spend against a plan; this estimates, it does not report."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The panel is a section named by its heading. The model list is a radio group named \"Models ranked by monthly cost, lowest first\" with a single tab stop on the selected model.",
        "Rows are in DOM order by rank, so a screen reader reads them in the order shown. Each row's name says the model, the provider, its rank and its monthly cost.",
        "Sliders are Arc sliders with real values spoken as token and request counts, not stop indexes.",
        "One polite status region announces the selected model's monthly cost and rank once the numbers settle (not on every drag frame) and says the prices are samples calculated in the browser.",
        "Selection is shown by a tinted row, an accent bar and aria-checked, never by color alone in the accessible tree. The error state pairs an icon with the words Prices did not load.",
        "The large total and the rolling numbers are hidden from assistive tech where the status region and row labels already carry the value.",
        "Pointer focus draws no ring; keyboard focus uses Arc's shared focus ring."
      ],
      "motion": [
        "Rows are placed by transform only and move to their new rank on a critically damped spring (about 240ms, no bounce), so a re-rank is one clean glide with no reflow.",
        "Bars follow the same spring to their new share of the most expensive model.",
        "Every price counts to its new value over 240ms on an ease-out with tabular numerals, written straight to the node so eight rows roll during a drag without re-rendering.",
        "The model name under the total and the price line crossfade (100ms out, 160ms in) when the selection changes. State changes between loading, list, error and empty crossfade the list area at a fixed height.",
        "Reduced motion jumps rows, bars and numbers to their end values and keeps every state and announcement. The loading pulse stops."
      ],
      "responsive": [
        "A container query stacks the sliders below 600px and, below 520px, moves the total under the heading, puts each bar on its own line under the model name (rows grow from 44 to 58px), and wraps the breakdown into two columns.",
        "Monthly costs switch to compact figures ($420K, $4.2M) above $100,000, so the cost column never overflows; the total keeps every digit.",
        "The presets scroll inside their own track if they ever outgrow the panel. Reads well from 320px to 720px; the panel caps at 720px."
      ],
      "performance": [
        "Costs are pure arithmetic memoized per workload; a slider drag re-renders one small list.",
        "Rolling numbers update text nodes from a motion value, not React state, and only transform, width and opacity animate.",
        "The status announcement is debounced to 450ms so screen readers are not flooded during a drag. Nothing runs while the panel is idle."
      ],
      "aiNotes": [
        "Use it on a pricing page, a model settings screen or a budgeting step where people need to see what a workload costs across models. Prices are inputs: pass your own models with current list prices and say where they came from in description.",
        "The default prices are plausible samples for illustration, labeled as samples in the header and fine print. Do not present them as real quotes.",
        "Prices are US dollars per 1M tokens. cachedInput is the cache read price; cacheHitRate decides what share of input bills at it while Cached input is on.",
        "Keep marks 16px. Mono marks should use currentColor; the sample Claude mark is tinted with its brand color #D97757 and works in both themes.",
        "Use computeLlmCost on the server for the same numbers the panel shows, for example to store an estimate with a plan.",
        "Slider stops are fixed (250 to 200,000 input tokens, 100 to 16,000 output tokens, 100 to 1,000,000 requests a day). Controlled usage outside a stop still computes exactly."
      ],
      "related": [
        "model-picker",
        "model-card",
        "context-window",
        "usage-meter",
        "usage-pricing",
        "slider"
      ],
      "usage": "import { LlmCostCalculator, computeLlmCost, type LlmModelPrice } from \"@/components/arc/llm-cost-calculator/llm-cost-calculator\";\n\n// With no props: eight current models at sample prices, RAG workload.\nexport function PricingSection() {\n  return <LlmCostCalculator />;\n}\n\n// Your own price list, wired to your plan picker.\nconst models: LlmModelPrice[] = [\n  { id: \"claude-sonnet-5-5\", name: \"Claude Sonnet 5.5\", provider: \"Anthropic\", mark: <ClaudeMark />, input: 3, cachedInput: 0.3, output: 15 },\n  { id: \"gpt-6-luna\", name: \"GPT-6 Luna\", provider: \"OpenAI\", mark: <OpenaiMark />, input: 0.25, cachedInput: 0.025, output: 3 },\n];\n\nexport function ModelBudget({ prices, onPick }) {\n  const [usage, setUsage] = useState({ inputTokens: 8000, outputTokens: 500, requestsPerDay: 20000, cached: true });\n  return (\n    <LlmCostCalculator\n      models={prices.data ?? models}\n      loading={prices.isLoading}\n      error={prices.error?.message}\n      onRetry={prices.refetch}\n      usage={usage}\n      onUsageChange={setUsage}\n      cacheHitRate={0.7}\n      description=\"List prices per 1M tokens, checked October 24, 2026\"\n      onSelectedChange={(id, model) => onPick(model, computeLlmCost(model, usage, { cacheHitRate: 0.7 }))}\n    />\n  );\n}",
      "exports": [
        "LlmCostCalculator",
        "computeLlmCost"
      ]
    },
    {
      "name": "provider-keys",
      "kind": "component",
      "title": "Provider keys",
      "description": "A settings card for bring-your-own-key AI products. Each provider row shows its real mark, the masked key, a status and when the key was last used. You can reveal a key for five seconds, copy, rotate or remove it with undo. Adding or rotating a key opens an inline form that checks the format as you type, then tests the connection before saving.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/provider-keys",
      "markdown": "https://uiarc.dev/components/provider-keys/markdown",
      "registry": null,
      "keywords": [
        "api keys",
        "provider keys",
        "byok",
        "bring your own key",
        "anthropic api key",
        "openai api key",
        "secret",
        "settings",
        "rotate key",
        "masked key",
        "connection test"
      ],
      "whenToUse": [
        "A settings page where users add their own Anthropic, OpenAI, Google AI, Mistral or xAI keys.",
        "Showing which providers are connected, which keys were rejected, and when each key was last used.",
        "Letting users rotate a leaked key or remove one with a short undo window."
      ],
      "whenNotToUse": [
        "Choosing a model for a request; use model-picker.",
        "Managing your own product's API keys for customers with scopes and expiry; use secret-rotation or a table.",
        "Showing spend or rate limits; use usage-meter."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "A section labelled by its title, with the rows in a list. Every icon button has a provider-specific label, such as \"Copy the Anthropic key\".",
        "Status is never color alone: an icon plus Connected, Invalid, Not set or Testing. The status button's label includes the rejection reason.",
        "Reveal is a toggle with aria-pressed; rotate and Add key report aria-expanded for the inline form.",
        "A polite live region announces reveals, copies, test results, removals and undo. Without testKey it adds \"Simulated, nothing was sent.\"",
        "The form's input has a label and points at the helper line with aria-describedby; format and rejection errors set aria-invalid.",
        "Focus is managed: the form focuses its input, Cancel returns focus to its trigger, Remove moves focus to Undo, and Undo returns it to Remove."
      ],
      "motion": [
        "Status, key, meta and helper text swap with one crossfade (out 100ms, in 160ms with a 2px rise) in a shared grid cell, so nothing resizes.",
        "The inline form opens and closes on a measured height spring (visualDuration .24, no bounce) with a short fade.",
        "The reveal countdown is a hairline ring that empties linearly over revealDuration. Icon swaps (eye, copy check) scale and fade in 160ms.",
        "Reduced motion keeps every state and announcement, drops the countdown ring, the rise and the press scale, and slows the spinner."
      ],
      "responsive": [
        "At 560px and wider each row is one line of columns: mark, name over key, status over last used, actions.",
        "Below 560px the status moves beside the name, the key gets its own line, and last used shares a line with the actions.",
        "The form puts input, Cancel and Save on one line on wide cards and stacks them under 560px. Holds at 360px with no horizontal scroll."
      ],
      "performance": [
        "Plain DOM and CSS; Motion only animates the form height and short crossfades. The clock re-renders the card every 30 seconds for relative times.",
        "Timers and in-flight tests are cleared and aborted on unmount and when a form closes."
      ],
      "aiNotes": [
        "Use for bring your own key settings in AI products: a workspace or account settings page, an onboarding step, or a gateway admin.",
        "Never ship full keys to the browser in production: pass last4 only and leave value out, which disables reveal and copy for that row.",
        "testKey should call the provider from your server (for example a models list request) and return { ok: false, error: \"401 Unauthorized\" } on rejection.",
        "Rotate means paste a new key: the row keeps the old key until the new one passes its test, then swaps.",
        "For a single generic secret with expiry and rotation history use secret-rotation; for spend per provider pair it with usage-meter."
      ],
      "related": [
        "usage-meter",
        "copy-button",
        "input",
        "model-picker"
      ],
      "usage": "import { ProviderKeys } from \"@/components/arc/provider-keys/provider-keys\";\n\n// Sample workspace, every test simulated\n<ProviderKeys />\n\n// Wired to your API\n<ProviderKeys\n  keys={keys}\n  onKeysChange={setKeys}\n  testKey={({ provider, key, signal }) =>\n    fetch(\"/api/provider-keys/test\", { method: \"POST\", body: JSON.stringify({ provider, key }), signal }).then(r => r.json())}\n  onSave={(provider, key) => saveKey(provider, key)}\n  onRemove={provider => deleteKey(provider)}\n/>",
      "exports": [
        "ProviderKeys"
      ]
    },
    {
      "name": "generation-stats",
      "kind": "component",
      "title": "Generation stats",
      "description": "A live stats strip for a streaming LLM response where time to first token, speed with a sparkline, output tokens and cost count up in tabular numerals beside the model's real mark, then settle and open into a latency and token cost breakdown.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/generation-stats",
      "markdown": "https://uiarc.dev/components/generation-stats/markdown",
      "registry": null,
      "keywords": [
        "generation stats",
        "llm stats",
        "tokens per second",
        "time to first token",
        "ttft",
        "latency",
        "token usage",
        "cost",
        "streaming",
        "sparkline",
        "prompt cache",
        "cached tokens",
        "reasoning tokens",
        "claude",
        "gpt",
        "gemini",
        "dev panel",
        "usage meter"
      ],
      "whenToUse": [
        "Under assistant messages in a chat product that shows people what a response cost or how fast it was.",
        "Playgrounds, eval runners and internal dev panels comparing models on latency, throughput and cost.",
        "Admin or debugging views that inspect a single request's token usage and cache hit rate."
      ],
      "whenNotToUse": [
        "Use context-window to show how much of a model's context a conversation has used.",
        "Use a chart or table for spend across many requests or days; this strip describes one response.",
        "Use agent-status for a long running agent with steps; this measures one model call."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The strip is a group named after the model (\"Claude Sonnet 5.5 stats\"). The four stats are a description list, so each value is read with its label: \"First token, 412ms\".",
        "Details is a button with aria-expanded and aria-controls; at narrow widths it becomes an icon button that keeps the name Show details or Hide details.",
        "A polite status region announces settled outcomes (\"Claude Sonnet 5.5 done in 4.18s, 284 output tokens at 88 tokens per second, $0.0077\"), failures and stops, never the live counts. The self-playing preview says once that it is simulated and stays quiet until someone interacts with it.",
        "Status never relies on color: a spinner, drawn check, alert or square always sits next to a word. The latency bar is decorative; its values are listed beside it in text.",
        "The breakdown is a real table with column and row headers and a caption, so screen readers can navigate tokens, rates and cost by cell.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Numbers glide toward each new value in about 100ms per step with tabular numerals in reserved cells, so chunks of tokens read as one smooth count and nothing beside them moves. A drop, like a new request, cuts straight to the new value instead of counting down.",
        "While streaming, speed is measured when each chunk lands, so it holds still between chunks instead of sagging and jumping. The sparkline adds a smoothed reading every 160ms and compresses to keep the whole run's shape.",
        "Status labels swap with a single crossfade (out in 100ms, in over 160ms with a 2px rise); the done check draws itself once.",
        "The breakdown and the error row open on a 260ms critically damped spring for height with a 160ms fade. Nothing bounces.",
        "The preview loop pauses off screen and holds on a settled run while someone has the breakdown open.",
        "Reduced motion shows final values at once, drops the spinner, the check drawing and the height springs, and stops the preview on a settled run."
      ],
      "responsive": [
        "The card's four cells sit in one row above 600px of width and in a two by two grid below it, so the speed cell keeps room for its sparkline.",
        "Below 440px Details becomes an icon button and the Rate column hides; the model name truncates with an ellipsis rather than wrapping.",
        "The inline variant wraps its stats onto a second line on phones instead of scrolling; the breakdown spans the full width under it."
      ],
      "performance": [
        "The live clock runs on requestAnimationFrame only while waiting or streaming, and each counting number re-renders only itself.",
        "The speed series is sampled on a 160ms interval only while streaming and capped at 48 points.",
        "The preview's simulation ticks every 40ms in virtual time and only while the card is on screen."
      ],
      "aiNotes": [
        "Wire it to the stream you already have: set startedAt when the request leaves, status \"waiting\" until the first chunk, then \"streaming\" while you add to outputTokens, then \"done\" with durationMs. Speed, latency and cost follow.",
        "Map provider usage fields: Anthropic input_tokens plus cache_read_input_tokens is inputTokens and cache_read_input_tokens is cachedTokens; OpenAI prompt_tokens and prompt_tokens_details.cached_tokens map the same way, and completion_tokens_details.reasoning_tokens is reasoningTokens.",
        "Pass pricing from your own config. If your provider already reports cost, pass usage.costUsd and leave pricing out; the Rate column then hides.",
        "Use variant=\"inline\" under chat messages and the card in eval views, playgrounds and admin panels. Keep the model name as people know it (\"Claude Sonnet 5.5\"), and put the API id in model.id.",
        "When the person stops a response, set status \"stopped\" with durationMs; when the request fails, set status \"error\" with a short, specific reason and onRetry."
      ],
      "related": [
        "context-window",
        "tool-call",
        "agent-status",
        "ai-chat",
        "model-picker",
        "text-shimmer",
        "copy-button"
      ],
      "usage": "import { GenerationStats } from \"@/components/arc/generation-stats/generation-stats\";\n\n// Under an assistant message, fed by your streaming handler.\nexport function MessageStats({ run }) {\n  return (\n    <GenerationStats\n      variant=\"inline\"\n      model={{ name: \"Claude Sonnet 5.5\", provider: \"anthropic\", id: \"claude-sonnet-5-5\" }}\n      status={run.status}                 // \"waiting\" | \"streaming\" | \"done\" | \"stopped\" | \"error\"\n      startedAt={run.startedAt}           // Date.now() when the request went out\n      usage={{\n        inputTokens: run.usage.input_tokens + run.usage.cache_read_input_tokens,\n        cachedTokens: run.usage.cache_read_input_tokens,\n        outputTokens: run.outputTokens,   // update on every chunk\n        ttftMs: run.ttftMs,               // set on the first token\n        durationMs: run.durationMs,       // set on the last\n      }}\n      pricing={{ input: 3, cachedInput: 0.3, output: 15 }}\n      error={run.error}\n      onRetry={() => run.retry()}\n    />\n  );\n}\n\n// A dev panel card with the breakdown open.\n<GenerationStats model={model} status=\"done\" usage={usage} pricing={pricing} defaultExpanded />\n\n// With no props it plays the sample runs.\n<GenerationStats />",
      "exports": [
        "GenerationStats",
        "useGenerationSimulation"
      ]
    },
    {
      "name": "ai-translate",
      "kind": "component",
      "title": "AI translate",
      "description": "A message card where picking a language streams the AI translation in place, sentence by sentence over the dimmed original, with a detected-language chip, a Show original toggle and a status line credited to the model's mark.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ai-translate",
      "markdown": "https://uiarc.dev/components/ai-translate/markdown",
      "registry": null,
      "keywords": [
        "ai translate",
        "translation",
        "inline translation",
        "show original",
        "language picker",
        "detected language",
        "streaming text",
        "multilingual",
        "localization",
        "message translation",
        "claude",
        "llm",
        "i18n"
      ],
      "whenToUse": [
        "Messages, comments, reviews or support tickets written in another language, translated on demand inside the product.",
        "Release notes or announcements that readers in several regions open in their own language.",
        "Any place you want an AI translation to feel inline and reversible, with the original one click away."
      ],
      "whenNotToUse": [
        "Translating a whole page or app interface; use your i18n framework.",
        "Long documents with headings and lists; this renders one plain paragraph.",
        "Live captions or speech; use a realtime stream component."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The card is a section named after the author. The paragraph carries the lang attribute of the language on screen, and dimmed old sentences keep their own lang, so screen readers pronounce both correctly.",
        "Start, completion, the original and failures are announced in a polite status region; the paragraph is aria-busy while it streams. The simulation says that nothing was sent.",
        "The language menu is a radio group with a checkmark on the current language. The Show original toggle is a pressed button whose label switches to Show translation.",
        "Status is never color alone: the model mark plus a label (Translating to German, Translated from French, Showing original), or a warning icon plus Couldn't translate.",
        "Every control is at least 32px tall, and labels reserve the width of their longest version, so nothing moves under the pointer."
      ],
      "motion": [
        "Streaming: chunks of 3 to 6 words (about 10 characters in Japanese) land every 34ms and only fade in over 140ms. No blur, slide or per word spring.",
        "The text on screen dims to 45% over 200ms as translating starts, and each sentence that starts in the new language removes one dimmed sentence, so the translation replaces the paragraph sentence by sentence.",
        "Cached translations and the original swap with one crossfade: the old text leaves in 100ms, the new one fades in over 160ms with a 2px rise.",
        "The paragraph's height follows its text with a critically damped spring (visualDuration 0.24, no bounce); window resizes cut straight to the new height.",
        "Status labels crossfade in one shared cell; the picker and toggle labels crossfade inside their reserved width. The menu fades and scales from 98% in 150ms.",
        "Reduced motion keeps every end state and announcement but drops chunk fades, crossfades and the height spring, and autoplay runs once."
      ],
      "responsive": [
        "The card fills its container up to 620px and pads 16px on phones.",
        "Below a 470px card width the status line drops under the controls, left-aligned with the text; below 380px the chip shows just the language name.",
        "Names and meta truncate with an ellipsis; the paragraph wraps with text-wrap: pretty. No horizontal scroll at 360px."
      ],
      "performance": [
        "Chunks append as spans with a CSS fade, so streaming re-renders one paragraph and never animates layout.",
        "One ResizeObserver drives the height spring; autoplay pauses through an IntersectionObserver while the card is off screen.",
        "Every request owns an AbortController; switching language or unmounting cancels the network call and timers."
      ],
      "aiNotes": [
        "Wire translate to your model. Return an async generator of deltas to stream real tokens, or a Promise<string> and the component streams it in chunks itself. Always pass the signal to fetch.",
        "Cache lives per instance and per text; a new text prop clears it. Persist translations yourself (onTranslate) if you want them across sessions, then pass them back in translations.",
        "Use the source language as value to show the original. Show original remembers the last translated language for Show translation.",
        "Keep one paragraph or message per instance. For a whole thread, render one AiTranslate per message and control value from a shared preferred language.",
        "Model names in samples must be current: Claude Haiku 5.5 is the default credit; swap the mark and name for your provider.",
        "Sentence splitting keeps decimals like 4.2 whole and handles 。！？ for Japanese."
      ],
      "related": [
        "chat-thread",
        "text-shimmer",
        "dropdown-menu",
        "response-feedback",
        "meeting-summary"
      ],
      "usage": "import { AiTranslate } from \"@/components/arc/ai-translate/ai-translate\";\n\nexport function TranslatableMessage({ message }) {\n  return (\n    <AiTranslate\n      text={message.body}\n      sourceLanguage={message.language}\n      author={{ name: message.author.name, role: message.author.title, avatar: message.author.avatarUrl }}\n      timestamp={message.sentAtLabel}\n      model={{ name: \"Claude Haiku 5.5\" }}\n      translate={async function* ({ text, from, to, signal }) {\n        const response = await fetch(\"/api/translate\", { method: \"POST\", body: JSON.stringify({ text, from, to }), signal });\n        if (!response.ok || !response.body) throw new Error(\"The translation service is unavailable.\");\n        const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();\n        while (true) {\n          const { value, done } = await reader.read();\n          if (done) return;\n          yield value;\n        }\n      }}\n      onValueChange={language => savePreferredLanguage(language)}\n    />\n  );\n}\n\n// Pre-translated content, no network\n<AiTranslate text={post.fr} sourceLanguage=\"fr\" translations={{ en: post.en, de: post.de }} author={null} />",
      "exports": [
        "AiTranslate"
      ]
    },
    {
      "name": "tldr-toggle",
      "kind": "component",
      "title": "TL;DR toggle",
      "description": "An article card with a Full, Summary and Key points switch and a length slider. Switching streams an AI summary in place of the article as the card's height springs to fit, and each key point links back to the paragraph it came from, which gets scrolled to and highlighted.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/tldr-toggle",
      "markdown": "https://uiarc.dev/components/tldr-toggle/markdown",
      "registry": null,
      "keywords": [
        "tldr",
        "summarize",
        "summary toggle",
        "ai summary",
        "key points",
        "article summary",
        "read mode",
        "length slider",
        "streaming summary",
        "source links",
        "citations"
      ],
      "whenToUse": [
        "Offering a quick summary or key points above or in place of a long article, doc or report.",
        "Letting readers verify AI claims against the source, through key points that link back to paragraphs.",
        "Reading surfaces where people choose how much detail they want with a length control."
      ],
      "whenNotToUse": [
        "Open-ended questions about content: use an answer or chat component such as ai-answer.",
        "Short content under a couple of paragraphs, where a summary saves no time.",
        "Summaries of many documents at once: use a list or a digest layout instead of one card per item."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is an article with the title as a heading. The mode switch is a labelled group of pressed buttons, the length is a slider whose spoken value is the target (\"~60 words\", \"4 points\"), disabled in Full mode.",
        "A polite live region announces each run once (\"Summarizing with Claude Haiku 5.5\", \"Summary ready, 58 words\", \"4 key points ready\", \"Stopped\"), never the streamed chunks. With the simulation it adds \"Simulated, nothing was sent\".",
        "The card is aria-busy while a result streams. Failures render as an alert with the message and a Try again button.",
        "Jump links are named after their point (\"Jump to paragraph 3: Partial bootstrapping loads...\"), and the jump moves focus to the highlighted paragraph so screen reader and keyboard users land where the eye does.",
        "Status is never color alone: done, stopped and failed each pair an icon with a word, and key points tick from a dashed circle to a check."
      ],
      "motion": [
        "The body's height follows its content on one critically damped spring (visualDuration .26, no bounce), whether a view swaps or text streams in. A container resize cuts straight to the new height.",
        "Views swap with a single crossfade: old out in 100ms, new in over 160ms. The footer's state label and actions crossfade the same way.",
        "Streamed chunks of 3 to 8 words arrive every 30 to 40ms and only fade in over 140ms. A cached result appears whole, with no fade.",
        "Thinking lasts about 0.7s with a calm shimmer on its label; the footer's Writing label shimmers while text arrives, so only one shimmer runs at a time.",
        "A jump waits for the height to settle, scrolls smoothly to the paragraph and fades a highlight in over 160ms and out over 600ms.",
        "Reduced motion drops the height spring, the chunk fades, smooth scrolling and the icon transitions; every end state and the highlight remain."
      ],
      "responsive": [
        "The card fills its container up to 680px. Below 520px the length slider moves under the mode switch at full width; below 440px the padding steps from 24px to 16px.",
        "Footer labels stay on one line, the model name truncates first, and actions keep 32px targets.",
        "Works at 360px with no horizontal scroll; long titles wrap and balance."
      ],
      "performance": [
        "Each chunk is a small span with a CSS fade; a long summary renders a few dozen nodes.",
        "One ResizeObserver drives the height spring, and results are cached per mode and length, so switching back makes no request.",
        "Changing the slider aborts the previous request through its AbortSignal before the next starts."
      ],
      "aiNotes": [
        "Use it where people read long content inside a product: blog posts, docs pages, release notes, tickets, research reports or support articles.",
        "Give paragraphs stable ids and have the model return the id each key point came from, so jump links land on the right paragraph.",
        "Pass length to the model as targetWords or pointCount rather than a vague word like \"short\".",
        "Stream when the provider streams; return a promise when it does not, and the card paces it.",
        "Without summarize the card runs a simulation and says so in its announcements; never ship that to production."
      ],
      "related": [
        "ai-answer",
        "text-shimmer",
        "segmented-control",
        "slider",
        "copy-button",
        "tool-call"
      ],
      "usage": "import { TldrToggle, type TldrSummarize } from \"@/components/arc/tldr-toggle/tldr-toggle\";\n\n// Stream from your route: newline-delimited JSON events, { type: \"text\", text } or { type: \"point\", source }.\nconst summarize: TldrSummarize = async function* ({ mode, length, targetWords, pointCount, article, signal }) {\n  const response = await fetch(\"/api/tldr\", {\n    method: \"POST\",\n    body: JSON.stringify({ mode, length, targetWords, pointCount, paragraphs: article.paragraphs }),\n    signal,\n  });\n  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();\n  let buffer = \"\";\n  for (;;) {\n    const { value, done } = await reader.read();\n    if (done) break;\n    buffer += value;\n    const lines = buffer.split(\"\\n\");\n    buffer = lines.pop() ?? \"\";\n    for (const line of lines) if (line) yield JSON.parse(line);\n  }\n};\n\nexport function PostSummary({ post }) {\n  return (\n    <TldrToggle\n      article={{\n        title: post.title,\n        source: { name: \"Engineering blog\" },\n        author: { name: post.author.name, avatar: post.author.avatar },\n        date: post.date,\n        paragraphs: post.paragraphs, // [{ id: \"intro\", text: \"...\" }, ...]\n      }}\n      summarize={summarize}\n      model={{ name: \"Claude Haiku 5.5\", mark: <ClaudeMark style={{ color: \"#D97757\" }} /> }}\n      onJump={id => track(\"tldr_jump\", { id })}\n    />\n  );\n}",
      "exports": [
        "TldrToggle",
        "simulateTldr",
        "streamResult"
      ]
    },
    {
      "name": "connector-setup",
      "kind": "component",
      "title": "Connector setup",
      "description": "A \"Connect your tools\" card for an AI assistant. Each app connects inline: it redirects, authorizes and runs a first sync with a progress hairline, then lists the scopes it was granted and offers Disconnect with an inline confirm.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/connector-setup",
      "markdown": "https://uiarc.dev/components/connector-setup/markdown",
      "registry": null,
      "keywords": [
        "connectors",
        "integrations",
        "connect apps",
        "oauth",
        "connect your tools",
        "ai assistant settings",
        "mcp connectors",
        "slack integration",
        "github integration",
        "scopes",
        "permissions",
        "disconnect"
      ],
      "whenToUse": [
        "An assistant's settings or onboarding step where people connect workspace apps.",
        "A first-run checklist that needs to show the OAuth handshake and first sync inline, without leaving the page.",
        "Reviewing which scopes an assistant holds per app and revoking them."
      ],
      "whenNotToUse": [
        "Browsing dozens of integrations by category: use the integrations-grid block.",
        "Approving a single action the agent wants to take: use agent-approvals.",
        "Showing a tool the agent is calling right now: use tool-call."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled group with a real heading; the list is a ul with one li per app.",
        "A polite live region announces each step: connecting, connected with the sync count, failed with the reason, cancelled and disconnected. With the built-in simulation it adds \"Simulated, nothing was sent.\"",
        "Status is never color alone: errors pair an alert icon with the message, connected apps show a check with the word Connected.",
        "The Connected button carries aria-expanded and aria-controls for the details region; Disconnect asks for confirmation and focuses the destructive button only after a deliberate press.",
        "Content that is fading out leaves the accessibility tree and stops taking clicks immediately."
      ],
      "motion": [
        "Status lines and actions swap with one crossfade (out 100ms, in 160ms with a 2px rise) in a shared grid cell, so the row never reflows.",
        "Progress is a 2px hairline that eases between steps and tracks the sync count linearly; the count rolls in tabular numerals inside a width reserved for its final value.",
        "Details open with a bounce-free spring on measured height (visualDuration 0.24).",
        "The scripted run starts only once the card is on screen and stops at the first interaction. Reduced motion keeps every state and announcement but cuts the crossfades and height springs."
      ],
      "responsive": [
        "Fluid up to 520px and holds at 360px: names and status lines truncate with an ellipsis while marks and actions keep their size.",
        "Actions stay 32px tall with full-width rows of 60px, comfortable for touch.",
        "The details column aligns to the row's text at every width; the disconnect confirmation truncates its question before it wraps."
      ],
      "performance": [
        "Pure React and CSS with a few Motion components; marks are inline SVG with no image requests.",
        "Only the row that is syncing re-renders per frame while its count rolls; other rows stay static.",
        "Search filters in memory; for hundreds of apps, filter on your side and pass the result as connectors."
      ],
      "aiNotes": [
        "Use for onboarding or settings screens where a person grants an assistant access to third-party apps. Pass your own connectors; built-in marks cover Slack, GitHub, Notion, Google Drive, Linear, Jira, Salesforce and HubSpot.",
        "Wire connect to your OAuth popup or redirect handshake and call report() at each step so the row shows real progress; throw an Error with a short human message (\"Needs admin approval\") to show Retry.",
        "Write scopes as plain-language capabilities (\"Post replies as Claude\"), not raw OAuth scope strings.",
        "Keep it one card in a settings column. For a full marketplace grid of integrations use the integrations-grid block instead."
      ],
      "related": [
        "tool-call",
        "agent-status",
        "agent-approvals"
      ],
      "usage": "import { ConnectorSetup } from \"@/components/arc/connector-setup/connector-setup\";\n\nexport function Integrations({ connectedIds }: { connectedIds: string[] }) {\n  return (\n    <ConnectorSetup\n      defaultConnected={connectedIds}\n      connect={async (app, { signal, report }) => {\n        const popup = await openOAuthPopup(app.id, { signal });\n        report({ phase: \"authorizing\" });\n        const grant = await popup.result;\n        report({ phase: \"syncing\", synced: 0 });\n        const { total } = await startFirstSync(app.id, grant, { signal, onProgress: synced => report({ synced }) });\n        return { account: grant.workspace, scopes: grant.scopes, synced: total };\n      }}\n      disconnect={(app, { signal }) => revokeGrant(app.id, { signal })}\n      onConnectedChange={ids => saveConnectedApps(ids)}\n    />\n  );\n}",
      "exports": [
        "ConnectorSetup",
        "ConnectorMark"
      ]
    },
    {
      "name": "smart-form-fill",
      "kind": "component",
      "title": "Smart form fill",
      "description": "A paste-to-fill form card where AI reads an email signature or messy notes and fills a contact field by field. Each value flashes in with a confidence dot, its source lights up in the pasted text, low-confidence guesses ask for review, and Undo puts the old values back.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/smart-form-fill",
      "markdown": "https://uiarc.dev/components/smart-form-fill/markdown",
      "registry": null,
      "keywords": [
        "ai form fill",
        "autofill",
        "smart paste",
        "extract contact",
        "email signature parser",
        "ai form",
        "confidence",
        "crm contact",
        "structured extraction",
        "paste to fill"
      ],
      "whenToUse": [
        "People paste an email signature, a forwarded message or notes and expect the record to fill itself.",
        "You need visible confidence and an easy review path before AI-filled data is saved.",
        "Onboarding or CRM flows where typing six fields by hand is the main friction."
      ],
      "whenNotToUse": [
        "Plain data entry with no unstructured source; use form-field and input.",
        "Long multi-page applications; use multi-step-form and add a paste step to one page.",
        "Bulk imports of many records; use a table import flow instead of one card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A polite live region announces each step: reading, how many fields were filled, which need review, checks, undo and save. With the default extractor it adds that the run was simulated.",
        "Each filled input carries an aria-description with its confidence and, for low-confidence fields, the reason. Fields that need review are aria-invalid until checked or edited.",
        "Status is never color alone: confidence shows a dot plus a percentage or a word (Review, Checked, Edited); the footer pairs an icon with text.",
        "The check button inside a review field has a label naming the field. Save focuses the first field that still needs review.",
        "The pasted text view is a focusable, labelled region so keyboard users can scroll it."
      ],
      "motion": [
        "Fields fill in form order 140ms apart. Each value fades in over 200ms and the input breathes an accent tint for 900ms; no movement, no size change.",
        "The source span for the field being filled, hovered or focused takes a stronger tint (160ms), so cause and effect read across the card.",
        "Labels, badges, the logo and status swap with one crossfade: out in 100ms, in over 160ms with a 2px rise. The reading label uses a calm shimmer.",
        "Reduced motion fills every field at once, drops the tint animation and the autoplay loop, and keeps all end states. Autoplay pauses while the card is off screen."
      ],
      "responsive": [
        "Side by side from a 720px container: paste well left, form right. Below that the well stacks on top at a fixed height.",
        "Under 440px every field takes the full row and the footer stacks its status above the actions.",
        "Container queries, so it adapts to the column it sits in, not the viewport. Holds at 360px without horizontal scroll."
      ],
      "performance": [
        "Only CSS animations for the fill highlight; motion is used for small crossfades. No layout animation.",
        "Autoplay timers stop when the card leaves the viewport and are cleared on unmount; reads are aborted on unmount.",
        "The built-in marks are inline SVG, about 6KB together; resolveLogo can supply your own."
      ],
      "aiNotes": [
        "Use for creating a record from unstructured text: CRM contacts, vendor or company profiles, shipping addresses, support tickets. Pass custom fields for anything beyond a contact.",
        "Wire extract to your model with structured output that returns value, confidence 0 to 1 and the exact source substring; source powers the highlight, note explains low confidence.",
        "Keep the human in the loop: leave reviewThreshold around 0.8 so guesses (inferred states, paraphrased titles, one of several phone numbers) are flagged rather than saved silently.",
        "Set autoPlay={false} in product screens; the default loop is for docs and marketing."
      ],
      "related": [
        "input",
        "textarea",
        "multi-step-form",
        "ai-command"
      ],
      "usage": "import { SmartFormFill } from \"@/components/arc/smart-form-fill/smart-form-fill\";\n\nexport function NewContact() {\n  return (\n    <SmartFormFill\n      extract={async (text, signal) => {\n        const response = await fetch(\"/api/extract-contact\", { method: \"POST\", body: text, signal });\n        return response.json(); // { name: { value, confidence, source }, ... }\n      }}\n      onSubmit={(values) => saveContact(values)}\n    />\n  );\n}",
      "exports": [
        "SmartFormFill"
      ]
    },
    {
      "name": "enrich-column",
      "kind": "component",
      "title": "Enrich column",
      "description": "An AI enrichment column for a data table, where one prompt runs down every row and each cell shimmers while it works, then settles into a value with its source or a failure you can retry.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/enrich-column",
      "markdown": "https://uiarc.dev/components/enrich-column/markdown",
      "registry": null,
      "keywords": [
        "enrich column",
        "ai column",
        "clay",
        "data enrichment",
        "lead enrichment",
        "ai table",
        "spreadsheet ai",
        "company data",
        "headcount",
        "funding round",
        "retry",
        "shimmer",
        "agent"
      ],
      "whenToUse": [
        "Enriching a list of companies or leads with facts an agent can look up (headcount, funding, tech stack).",
        "Classifying or summarizing rows with one prompt, with a visible source per answer.",
        "Showing per-row AI progress with stop, re-run and retry in a CRM, sales tool or internal admin."
      ],
      "whenNotToUse": [
        "Large spreadsheets with many columns and editing; use Data grid and add an AI column there.",
        "A single question over a dataset; use Ask your data.",
        "A log of one agent's tool calls; use Tool call."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A real table: the company is a row header and the AI column's name and prompt are its column header.",
        "Every cell state pairs an icon with text (Queued, a shimmering activity, the value, Failed with a reason), never color alone.",
        "A polite live region announces runs, stops and the final count, and says when values are simulated and nothing was sent.",
        "Icon buttons carry labels that name the row (Run Linear, Retry Retool); source links say they open a new tab."
      ],
      "motion": [
        "Cell swaps are one crossfade in a shared grid cell: old content out in 100ms, new in 160ms with a 2px rise. Nothing changes size.",
        "Status icons crossfade with a bounce-free scale; the done tick draws in 240ms; the running label uses a calm text shimmer.",
        "The editor opens with a critically damped height spring (240ms, no bounce).",
        "Reduced motion keeps every state and cuts travel and the tick draw; the spinner slows down."
      ],
      "responsive": [
        "Fluid up to 760px. Below 560px the domain, source text and model detail fold away; the source keeps its arrow link.",
        "Below 440px the gutters tighten to 16px, values wrap to two lines in 60px rows, and failure reasons hide behind the Failed label.",
        "On touch screens the row run controls stay visible since there is no hover."
      ],
      "performance": [
        "Only changed cells re-render visibly; runs are capped by concurrency and every request is abortable.",
        "Workers read the latest rows and prompt through refs, so editing mid-run never acts on stale data.",
        "Built for tens of rows; for hundreds, virtualize the table body and keep concurrency low."
      ],
      "aiNotes": [
        "Use for AI-filled columns in tables of accounts, leads, documents or tickets, where one prompt runs per row.",
        "Pass a real enrich function and honor the AbortSignal; throw an Error with a short message to show a failed cell with retry.",
        "Persist values with onCellChange and feed them back through initialCells on the next render.",
        "Keep values to one short line; the cell truncates with an ellipsis and shows the full value on hover."
      ],
      "related": [
        "tool-call",
        "agent-status",
        "text-shimmer",
        "sortable-data-table",
        "data-grid",
        "ask-your-data"
      ],
      "usage": "import { EnrichColumn } from \"@/components/arc/enrich-column/enrich-column\";\n\n<EnrichColumn\n  title=\"Pipeline\"\n  rows={accounts.map(account => ({ id: account.id, name: account.name, domain: account.domain }))}\n  defaultColumn={{ name: \"Headcount and funding\", prompt: \"Find the headcount and latest funding round\" }}\n  enrich={async (row, { prompt, signal, setActivity }) => {\n    setActivity(`Searching for ${row.name}`);\n    const response = await fetch(\"/api/enrich\", { method: \"POST\", body: JSON.stringify({ domain: row.domain, prompt }), signal });\n    if (!response.ok) throw new Error(\"No source found\");\n    return response.json(); // { value, source: { label, href } }\n  }}\n  onCellChange={(row, cell) => cell.status === \"done\" && save(row.id, cell)}\n/>",
      "exports": [
        "EnrichColumn"
      ]
    },
    {
      "name": "ticket-router",
      "kind": "component",
      "title": "Ticket router",
      "description": "A support ticket router where messages from Gmail, Intercom, Slack and Zendesk come in one at a time. An AI classifier labels each one with intent, sentiment and confidence, and the ticket moves into a team lane with an assignee. Low confidence tickets wait in a review lane, and one click there routes them to the suggested team.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ticket-router",
      "markdown": "https://uiarc.dev/components/ticket-router/markdown",
      "registry": null,
      "keywords": [
        "ticket routing",
        "support triage",
        "ai classifier",
        "helpdesk",
        "intent detection",
        "sentiment",
        "confidence",
        "queue",
        "assignment",
        "intercom",
        "zendesk",
        "gmail",
        "slack",
        "customer support"
      ],
      "whenToUse": [
        "Showing how an AI classifier triages inbound support messages into team queues.",
        "A live triage widget in a helpdesk or ops dashboard, with a human review lane for low confidence.",
        "Landing page or onboarding demos of AI routing for support products."
      ],
      "whenNotToUse": [
        "A full ticket inbox with search and bulk actions; use a table or list layout.",
        "A single tool step in a chat transcript; use tool-call.",
        "Generic multi step agent progress; use agent-status or agent-plan."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a section labelled by its title. Each lane is a list labelled with its name and ticket count.",
        "A polite live region announces each routing (\"routed to Billing, assigned to Andre Williams\"), failures, pauses and review confirmations, and says when the run is simulated and nothing was sent.",
        "Intent, sentiment and review status always pair an icon with a text label; confidence reads as a percentage, never color alone.",
        "Review tickets are real buttons with a label naming the subject, confidence and target lane."
      ],
      "motion": [
        "About two seconds per ticket: arrive 220ms, classify, labels fade in with a 50ms stagger, then a pulse draws along the connector to the target lane in 240ms and the row lands.",
        "Rows enter and leave by animating their measured height with a bounce free spring (visualDuration .24). Counters roll in reserved tabular width.",
        "Ticket swaps are a single crossfade: out in 100ms, in over 160ms with a small rise. No blur, no per letter motion.",
        "Reduced motion keeps every end state and the landing highlight, and drops travel, rises and the connector draw. The flow pauses off screen."
      ],
      "responsive": [
        "Fills its container up to 840px. Four lanes sit in a row with a connector above 600px of content width; below that they form a 2 by 2 grid with two rows each.",
        "Below 600px the classifier labels stack above the route target; below 400px the confidence meter and model name hide, keeping the percentage.",
        "Every text line truncates with an ellipsis, so long subjects and names never change the card height."
      ],
      "performance": [
        "One timer or one classifier call runs at a time; both cancel on pause, off screen (IntersectionObserver) and unmount.",
        "Routed history is capped at 80 tickets and each lane renders only its visible rows.",
        "Marks are inline SVG with no network requests; avatars use the shared image component."
      ],
      "aiNotes": [
        "Use for support triage, helpdesk automation and AI routing previews. Wire classify to your model call and onRoute to your helpdesk API.",
        "Lane ids returned by route must match lanes; an unknown id falls back to the first non review lane.",
        "Pass loop={false} with your own tickets to get the empty and All caught up states. Append to tickets to keep the flow going.",
        "Keep it inside a content column (360 to 880px). Below 600px the lanes fold into a 2 by 2 grid and the connector hides."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "agent-approvals",
        "agent-handoff",
        "ai-workflow"
      ],
      "usage": "import { TicketRouter } from \"@/components/arc/ticket-router/ticket-router\";\n\nexport function SupportInbox({ tickets }) {\n  return (\n    <TicketRouter\n      tickets={tickets}\n      reviewThreshold={0.75}\n      classify={async (ticket, { signal }) => {\n        const res = await fetch(\"/api/classify\", { method: \"POST\", body: JSON.stringify(ticket), signal });\n        return res.json(); // { intent, sentiment, confidence }\n      }}\n      onRoute={ticket => assignInHelpdesk(ticket.id, ticket.lane, ticket.assignee)}\n    />\n  );\n}",
      "exports": [
        "TicketRouter"
      ]
    },
    {
      "name": "ask-your-data",
      "kind": "component",
      "title": "Ask your data",
      "description": "A natural language to chart card: you ask a question, PostgreSQL streams into a collapsible, syntax highlighted panel, the query runs with a rolling row count, and the answer builds as stacked bars, lines or a table, with a one line insight streamed in underneath.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ask-your-data",
      "markdown": "https://uiarc.dev/components/ask-your-data/markdown",
      "registry": null,
      "keywords": [
        "ask your data",
        "text to sql",
        "natural language query",
        "nl2sql",
        "ai analytics",
        "chart",
        "postgresql",
        "sql generation",
        "stacked bar chart",
        "line chart",
        "data table",
        "insight",
        "bi",
        "self serve analytics"
      ],
      "whenToUse": [
        "Letting people ask product or business questions in plain words and see a chart, with the SQL one click away to verify or copy.",
        "Embedding an analytics answer in a dashboard card, an admin page or an AI assistant's result view.",
        "Teaching or auditing text to SQL, where showing the generated query builds trust."
      ],
      "whenNotToUse": [
        "Use bar-chart or line-chart when the query is fixed and there is no question to ask.",
        "Use tool-call to show a database query as one step inside a longer agent transcript.",
        "Use sql-editor when people write and tune queries by hand."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A polite live region narrates the run: writing SQL, running the query, then the chart title, row count and insight. Without generateSql and runQuery it adds \"Simulated with sample data, nothing was queried.\"",
        "Status is never color alone: a spinner, a drawn check, an alert or a stop glyph sits beside a text label in the SQL row and the stage header.",
        "The SQL row is a real button with aria-expanded and aria-controls, labelled with its title and meta, such as \"Generated SQL, PostgreSQL · 48 rows · 214ms\". Text not yet streamed is hidden from assistive technology.",
        "The chart is a focusable group: arrow keys move a readout that is announced, such as \"Aug 10: Free 61, Pro 26, Team 10, Enterprise 2, total 99\". The table view keeps real table semantics with column and row headers.",
        "The stage sets aria-busy while working. Text fields get no focus ring by design; the field border shifts to the accent on focus, and every other control uses Arc's shared keyboard ring."
      ],
      "motion": [
        "SQL streams four tokens every 34ms, each chunk only fading in over 140ms. The full query is laid out invisibly first, so the panel holds its final size and nothing below moves.",
        "The SQL panel opens and folds on a 300ms bounce free spring; it folds once the query starts, before the chart builds, so only one region moves at a time.",
        "Bars rise from the baseline in 320ms with at most 180ms of stagger; lines draw in 460ms. Switching views crossfades (100ms out, 160ms in) in a fixed area.",
        "Header text and the SQL meta crossfade as single lines with a 2px settle. The row count rolls in tabular numerals in reserved width.",
        "The insight streams four words a tick with its two lines reserved. Reduced motion shows SQL and insight whole, drops the bar rise, line draw, spinner and breathing skeleton, and keeps every state and announcement. Loops pause off screen."
      ],
      "responsive": [
        "Container aware: below 520px the source keeps only its icon, the chart switch moves into the result above the legend at full width, and the stage grows to fit wrapped legend and insight lines.",
        "X axis labels thin out to keep 46px apart, wide SQL lines and tables scroll inside their own wells, and the tooltip flips to whichever side of the column has room.",
        "Touch: tap or drag across the chart to read values; vertical scrolling still passes through."
      ],
      "performance": [
        "Charts are plain SVG measured with one ResizeObserver, no chart library. Text and hairlines are drawn at device pixels, so they stay crisp at any width.",
        "The SQL tokenizer is a single regular expression pass, memoized per query. The SQL panel and every view mount only while shown.",
        "Rows scanned updates come through onProgress; batch them to a few per second for very fast queries."
      ],
      "aiNotes": [
        "Use it for self serve analytics inside a product: a question box on a dashboard, a metrics page, or an admin console. Wire generateSql to your model with the schema in the prompt and runQuery to a read only role with a statement timeout.",
        "Return the SQL as an async iterable of model chunks for true streaming, or a whole string for the paced reveal. Always honour signal so Stop really cancels both steps.",
        "Shape rows into categories and series in runQuery. Keep series to four or fewer for the default palette; set stacked false for rates or averages so totals are not shown.",
        "Let the model write the insight from the returned rows, one or two sentences with concrete numbers. Do not invent numbers that are not in the result.",
        "Throw from runQuery with the database message to show it verbatim in the error state with Try again."
      ],
      "related": [
        "tool-call",
        "ai-chat",
        "agent-run",
        "bar-chart",
        "line-chart",
        "text-shimmer"
      ],
      "usage": "import { AskYourData } from \"@/components/arc/ask-your-data/ask-your-data\";\n\n// Self playing preview with sample data\n<AskYourData />\n\n// Wired to your model and a read only database role\nexport function Explore() {\n  return (\n    <AskYourData\n      source={{ name: \"analytics\", dialect: \"PostgreSQL\" }}\n      generateSql={(question, { signal }) => streamSql(question, { signal })} // AsyncIterable<string>\n      runQuery={async (sql, { signal, onProgress }) => {\n        const res = await fetch(\"/api/query\", { method: \"POST\", body: JSON.stringify({ sql }), signal });\n        if (!res.ok) throw new Error(await res.text()); // shows the database error\n        const { rows, ms } = await res.json();\n        onProgress(rows.length, rows.length);\n        return {\n          title: \"Weekly signups by plan\",\n          categories: weeksOf(rows),\n          series: seriesByPlan(rows),\n          rowCount: rows.length,\n          durationMs: ms,\n        };\n      }}\n      onResult={(result, { sql }) => saveToHistory(result.title, sql)}\n    />\n  );\n}",
      "exports": [
        "AskYourData",
        "AskYourDataResult"
      ]
    },
    {
      "name": "agent-cursor",
      "kind": "component",
      "title": "Agent cursor",
      "description": "A live document where Claude edits like a collaborator. Its labeled cursor comes out of its avatar, selects a paragraph and streams in a rewrite, adds a heading and fixes a typo, while a teammate's cursor reads along. Changes stay tinted until someone accepts or undoes them from a floating review bar.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/agent-cursor",
      "markdown": "https://uiarc.dev/components/agent-cursor/markdown",
      "registry": null,
      "keywords": [
        "ai cursor",
        "agent cursor",
        "multiplayer cursor",
        "collaborative editing",
        "ai writing assistant",
        "live document edit",
        "accept changes",
        "suggested edits",
        "claude",
        "notion ai",
        "presence"
      ],
      "whenToUse": [
        "Showing an AI writing assistant that edits a doc alongside people, with accept and undo.",
        "Landing or onboarding sections that demo a collaborative AI editor in motion.",
        "A product surface that streams structured edits from an agent and needs a human review step."
      ],
      "whenNotToUse": [
        "Free form chat or Q&A: use a chat transcript component.",
        "A real rich text editor: wire your editor's own collaboration cursors; this card renders read-only blocks.",
        "Long documents with many sections: the card is built for a short excerpt of five to ten blocks."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A polite live region announces each step (reading, the current edit, edits ready, accepted, undone, errors), and adds \"Simulated, nothing was saved\" for simulated runs.",
        "Cursors, selections and the visual status line are decorative and hidden from assistive tech; the document stays real headings and paragraphs, marked aria-busy while the agent edits.",
        "Status never relies on color: every terminal state pairs an icon with a label, and pending changes are tinted and listed in the review bar with a count.",
        "Focus inside the card or a hovering pointer holds the looping demo in review, so nobody loses the bar mid reach."
      ],
      "motion": [
        "Cursors glide on a gentle quadratic arc with an ease-in-out curve, 280 to 600ms by distance, driven by motion values so nothing re-renders per frame.",
        "Selections fill line by line in step with the drag. Rewrites fade the old text out in 110ms, then stream 3 to 6 words every 34ms, each chunk fading in over 140ms with no blur or slide.",
        "Block heights follow measured content with a bounce-free spring (0.24s); inserted blocks grow from zero, then their text fades in.",
        "Status labels crossfade (100ms out, 160ms in). The review bar rises 8px and fades in 200ms.",
        "Reduced motion: cursors jump to each target, text appears whole, the loop and idle drift stop, and every end state stays.",
        "Playback pauses while the card is off screen."
      ],
      "responsive": [
        "Fluid up to 680px wide; under 480px the doc padding tightens and the review bar drops \"by Claude\" to stay on one line.",
        "Cursor tags flip to the left of the arrow near the right edge, and resting cursors re-anchor when the card resizes.",
        "Works at 360px with no horizontal scroll; text wraps and the selection and cursor targets are measured from live line boxes."
      ],
      "performance": [
        "Cursor positions are motion values, so travel never re-renders React. Streaming re-renders one block per tick.",
        "Selection and target geometry come from Range.getClientRects on demand, not on every frame.",
        "Loops, idle drift and timers stop when the card is off screen or unmounted; Stop aborts the run's signal."
      ],
      "aiNotes": [
        "Pick this to show an AI agent editing a shared document in place, Notion or Google Docs style, with a review step. For a chat transcript use ai-chat; for a single text diff use a diff view.",
        "Feed real edits through run (async, abortable) or edits (static). Edits address blocks by id; fix edits need the exact word to replace inside the block text.",
        "Persist accepted text from onAccept and pass it back as blocks; the doc only resets to blocks when no run is using it.",
        "Set agent to another provider with its own mark and brand color; the cursor, selection and tint all follow agent.color."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "text-shimmer",
        "avatar",
        "button"
      ],
      "usage": "import { AgentCursor, type AgentEdit, type DocBlock } from \"@/components/arc/agent-cursor/agent-cursor\";\n\nconst [doc, setDoc] = useState<DocBlock[]>(initialDoc);\n\n<AgentCursor\n  blocks={doc}\n  run={async signal => {\n    const res = await fetch(\"/api/agent/edits\", { method: \"POST\", body: JSON.stringify(doc), signal });\n    return (await res.json()) as AgentEdit[];\n  }}\n  collaborator={{ name: \"Emma Collins\", avatar: \"/media/people/emma-collins.svg\" }}\n  onAccept={setDoc}\n/>",
      "exports": [
        "AgentCursor"
      ]
    },
    {
      "name": "provider-status",
      "kind": "component",
      "title": "Provider status",
      "description": "A status card for the AI providers your product depends on. Each row shows the provider's real mark, its health as an icon and label, p50 latency and a 30-day uptime bar with a tooltip for each day. An incident row expands into its timeline of updates, and the card checks again on a visible countdown.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/provider-status",
      "markdown": "https://uiarc.dev/components/provider-status/markdown",
      "registry": null,
      "keywords": [
        "provider status",
        "AI status",
        "LLM uptime",
        "status page",
        "incident",
        "uptime bar",
        "latency",
        "health check",
        "OpenAI status",
        "Anthropic status",
        "model provider",
        "monitoring"
      ],
      "whenToUse": [
        "Showing the live health, latency and recent uptime of the model providers your product routes to.",
        "Explaining degraded AI features to users or admins with the provider's incident updates in place.",
        "A routing or fallback settings page where people need to see which providers are healthy right now."
      ],
      "whenNotToUse": [
        "A full public status page with components, subscriptions and history: build a page, not a card.",
        "A single provider's state inside a chat header: use agent-status or a small badge.",
        "Detailed latency charts over time: use a chart component."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section. Rows are a list; an incident row has a full-row button with aria-expanded and aria-controls, named with the provider, its health and the incident title.",
        "Health is never color alone: every status shows an icon and a label, and day ticks carry their health in the tooltip and in the bar's label.",
        "Each uptime bar is one tab stop with a label that sums up the 30 days. Arrow keys move through the days and a polite live region reads each one, so 180 ticks never flood the tab order.",
        "A polite live region announces checks, refresh errors and simulated actions (\"Simulated, no requests were sent\").",
        "Focus rings come from the shared :focus-visible styles; the row button's ring sits just inside the row."
      ],
      "motion": [
        "Health labels, latency, uptime and the summary change with a single crossfade (out in 100ms, in over 160ms with a 2px rise) inside a reserved grid cell, so nothing reflows.",
        "The incident panel springs to its measured height with no bounce (visual duration 240ms); new updates fade in while the panel grows.",
        "Ticks change color over 240ms. The countdown ring is a linear CSS drain that pauses with the countdown off screen; Checking swaps it for a short spinner and a shimmer label.",
        "Reduced motion keeps every end state and drops the rise, the height spring, the spinner and the skeleton pulse."
      ],
      "responsive": [
        "From 560px wide each row is one line with columns for status, p50 latency, the uptime bar and the 30 day percentage.",
        "Below 560px (a container query, so it follows the card, not the window) rows take two lines: name and status above, a full width uptime bar and percentage below, with latency under the name.",
        "Holds up at 360px with no horizontal scroll; long names and details truncate with an ellipsis.",
        "Touch: tapping a tick shows its day, dragging sideways scrubs, and vertical scrolling still works."
      ],
      "performance": [
        "One shared tooltip for the whole card, positioned on demand, instead of one per tick.",
        "The countdown re-renders once per second at most; the ring animates in CSS. Both pause off screen and in background tabs.",
        "Refreshes are abortable and never overlap."
      ],
      "aiNotes": [
        "Use it for a provider health view in an AI product: a settings page, an admin dashboard tile or a status popover. Wire onRefresh to your own aggregation endpoint; do not poll provider status pages from the browser.",
        "Pass real marks at 16px. Mono marks should use currentColor so they follow the theme.",
        "Keep latency and uptime as plain numbers; the component formats them (412 ms, 1.92 s, 99.61%).",
        "Label sample or synthetic numbers with the footnote prop."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "tooltip",
        "text-shimmer",
        "button"
      ],
      "usage": "import { ProviderStatus } from \"@/components/arc/provider-status/provider-status\";\nimport { AnthropicMark, OpenaiMark } from \"@/components/arc/provider-status/provider-status\";\n\nexport function Health() {\n  const [providers, setProviders] = useState<ProviderStatusItem[]>(initial);\n  return (\n    <ProviderStatus\n      providers={providers}\n      refreshInterval={60_000}\n      onRefresh={async signal => {\n        const res = await fetch(\"/api/provider-status\", { signal });\n        if (!res.ok) throw new Error(\"Couldn’t reach the status feeds. Showing the last results.\");\n        return (await res.json()) as ProviderStatusItem[];\n      }}\n      onOpenIncident={(provider, incident) => window.open(incident.url, \"_blank\")}\n      footnote=\"Latency is the median over the last 5 minutes.\"\n    />\n  );\n}",
      "exports": [
        "ProviderStatus",
        "sampleProviders",
        "AnthropicMark, OpenaiMark, GeminiColorMark, MistralColorMark, XaiMark, AwsMark"
      ]
    },
    {
      "name": "training-run",
      "kind": "component",
      "title": "Training run",
      "description": "A fine-tuning job card where a live train and validation loss curve draws across a fixed step axis that doubles as the epoch progress bar. It shows the learning rate, elapsed time, ETA and the latest evals and checkpoints, has Pause and Cancel while it runs, and ends in results with a Deploy button.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/training-run",
      "markdown": "https://uiarc.dev/components/training-run/markdown",
      "registry": null,
      "keywords": [
        "fine-tuning",
        "training run",
        "loss curve",
        "LoRA",
        "epochs",
        "checkpoint",
        "eval",
        "learning rate",
        "ETA",
        "deploy model",
        "ML job",
        "Qwen",
        "Hugging Face"
      ],
      "whenToUse": [
        "A fine-tuning or training job page in an AI platform, where people watch loss, pause, cancel and deploy.",
        "An eval or distillation run that reports a loss curve, checkpoints and a final score.",
        "A product onboarding step that trains a custom model on the customer's data and ends in a deploy."
      ],
      "whenNotToUse": [
        "A generic multi-series chart with no job lifecycle: use line-chart.",
        "A short agent task with steps rather than a loss curve: use agent-status.",
        "Explaining how training works conceptually: use model-training."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A polite live region announces status changes (Queued, Training, Paused, Completed, Failed, Deployed) and any notice, never every step.",
        "Status is an icon plus a label, never color alone; the failure reason is text in the footer and in the event log.",
        "The chart is a labelled group whose name carries the current step and both losses; the arrow keys inspect samples and the legend reads them out visually.",
        "Cancel is a two step action so a stray press never stops a long run. Deploy keeps focus while loading.",
        "Stats are a description list; event steps and numbers use tabular numerals."
      ],
      "motion": [
        "The curve and epoch segments ease toward each new sample every frame (exponential, 120ms), so data arriving in bursts still draws at an even pace with no jumps. A rollback or reset jumps instantly.",
        "The y axis is fixed from the first samples and the x axis spans the whole run, so nothing rescales mid-run.",
        "New events slide the list down with one bounce-free spring (0.24s); status, stats and footer text crossfade (100ms out, 160ms in, 2px rise).",
        "Reduced motion: the curve jumps to each sample, the spinner and shimmer stop, and crossfades become instant.",
        "The demo simulation freezes while the card is off screen."
      ],
      "responsive": [
        "Fills its container up to 640px. Under 440px the card padding drops to 16px, stats go to two columns, the legend shortens Validation to Val and the footer stacks context over actions.",
        "The chart measures its width and redraws at true pixel size, so strokes stay 2px crisp at any width. Touch drag scrubs the chart horizontally while vertical swipes still scroll."
      ],
      "performance": [
        "Curve paths are written straight to the SVG on animation frames, without React renders; the smoother stops when it reaches the latest sample.",
        "Comfortable up to a few thousand samples. For longer runs, downsample to about 500 points before passing loss."
      ],
      "aiNotes": [
        "Use for one fine-tuning or training job: model, dataset, live loss, controls and deploy. For a list of jobs, render one compact row per job and open this card for the selected one.",
        "Pass totalSteps and epochs from the job config so the axis is right from the first sample. Send loss as it streams; there is no need to throttle, the card smooths it.",
        "Each control only appears when its callback is passed, so a read-only monitor needs only data props.",
        "Real marks: import QwenColorMark, MetaColorMark or HuggingFaceMark from the component, or pass your own SVG as model.mark.",
        "Primary actions (Resume, Retry, Deploy) use the accent; only Confirm cancel uses danger."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "model-training",
        "line-chart",
        "progress",
        "copy-button"
      ],
      "usage": "import { TrainingRun, QwenColorMark, HuggingFaceMark } from \"@/components/arc/training-run/training-run\";\n\nexport function FineTuneJob({ job }: { job: Job }) {\n  return (\n    <TrainingRun\n      status={job.status}\n      name={job.outputName}\n      model={{ name: \"Qwen3.8 Max\", mark: <QwenColorMark /> }}\n      details={[\"LoRA r16\", \"8× H200\"]}\n      dataset={{ name: \"arcwork/support-tickets-oct\", examples: 48210, mark: <HuggingFaceMark /> }}\n      step={job.step}\n      totalSteps={job.totalSteps}\n      epochs={job.epochs}\n      loss={job.loss}\n      learningRate={job.learningRate}\n      elapsed={job.elapsedSeconds}\n      eta={job.etaSeconds}\n      events={job.events}\n      error={job.error}\n      onPause={() => api.pause(job.id)}\n      onResume={() => api.resume(job.id)}\n      onCancel={() => api.cancel(job.id)}\n      onRetry={() => api.retry(job.id)}\n      onDeploy={signal => api.deploy(job.id, { signal })}\n    />\n  );\n}",
      "exports": [
        "TrainingRun",
        "useTrainingRunSimulation"
      ]
    },
    {
      "name": "ai-command",
      "kind": "component",
      "title": "AI command",
      "description": "A Cmd+K command palette with an AI mode. Search pages and actions, or press Tab to ask a question: the answer streams inside the palette with numbered sources, and the next steps it suggests, like filing a Linear issue or opening a Notion doc, run straight from the list.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ai-command",
      "markdown": "https://uiarc.dev/components/ai-command/markdown",
      "registry": null,
      "keywords": [
        "command palette",
        "cmd k",
        "command menu",
        "ask ai",
        "ai search",
        "spotlight",
        "raycast",
        "quick actions",
        "streaming answer",
        "citations",
        "sources",
        "keyboard"
      ],
      "whenToUse": [
        "A global Cmd+K in a SaaS app where people both jump to pages and ask questions about their workspace.",
        "Search that should fall back to an AI answer when nothing matches.",
        "Answers that lead straight to an action in another tool, like filing a Linear issue or posting to Slack."
      ],
      "whenNotToUse": [
        "A long, multi-turn conversation: use a chat thread.",
        "Plain navigation with no AI: use command-palette.",
        "Inline field suggestions while typing in a form: use an autocomplete input."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The input is a combobox that owns a listbox; the active row is announced through aria-activedescendant, so focus never leaves the input.",
        "A polite live region announces questions, finished answers, action results and simulated runs (\"Simulated, nothing was sent\").",
        "Status is never color alone: running, done and failed actions change both the icon and the label.",
        "Citations and source chips are buttons with full descriptions as their accessible names; hovering or focusing one highlights its partner.",
        "The mode toggle uses aria-pressed buttons and keeps input focus."
      ],
      "motion": [
        "The palette never changes size: the results area has a fixed height and views crossfade in place (100ms out, 160ms in, 2px rise).",
        "One highlight glides between rows in 130ms and jumps without travel when the list itself changes, so typing never animates.",
        "Answers stream 3 to 7 words every 34ms, each chunk fading in over 140ms; the view follows the text down unless you scroll up.",
        "Labels, status icons and footer hints swap with a single crossfade. The done check draws once.",
        "Reduced motion: no walkthrough, no chunk fades or travel; the answer reveals in large steps and every state stays.",
        "The walkthrough pauses off screen through IntersectionObserver and restarts from a clean state."
      ],
      "responsive": [
        "Fills its container up to 680px and holds up at 360px; long labels and meta truncate with an ellipsis, meta first.",
        "Below 540px the navigate hint hides from the footer; source chips wrap.",
        "Rows are 40px tall and work with touch; hover only moves the highlight on fine pointers that actually move."
      ],
      "performance": [
        "Search is a synchronous word match over the items array, fine for a few hundred rows; filter on the server beyond that.",
        "Streaming appends one span per chunk; the highlight is positioned directly in the DOM without extra renders.",
        "The walkthrough and its timers stop off screen and on unmount; in-flight answers and actions abort through their signals."
      ],
      "aiNotes": [
        "Use for a product-wide Cmd+K that can also answer questions; put it inside a dialog with elevated and onClose.",
        "Return sources before text with setSources so the chips land first, and cite them in the text as [1], [2].",
        "Give each suggested action a run that takes the AbortSignal; return the created object's name (\"Created ENG-2481\") as the done label.",
        "Keep item labels short and put context in meta; use group to section results.",
        "For a plain command menu without AI, use command-palette instead."
      ],
      "related": [
        "command-palette",
        "source-cards",
        "tool-call",
        "starter-prompts"
      ],
      "usage": "import { AiCommand } from \"@/components/arc/ai-command/ai-command\";\n\n// Sample walkthrough\n<AiCommand />\n\n// Wired to your app and model\n<AiCommand\n  items={commands}\n  onSelect={item => { router.push(item.id); setOpen(false); }}\n  onAsk={async (question, { signal, append, setSources }) => {\n    const res = await fetch(\"/api/ask\", { method: \"POST\", body: JSON.stringify({ question }), signal });\n    const { sources, stream } = await readAnswer(res);\n    setSources(sources);\n    for await (const delta of stream) append(delta);\n    return { actions: [{ id: \"issue\", label: \"Create issue in Linear\", run: s => createIssue(question, s) }] };\n  }}\n  onClose={() => setOpen(false)}\n  elevated\n/>",
      "exports": [
        "AiCommand",
        "AiCommandAction",
        "AiCommandSource"
      ]
    },
    {
      "name": "starter-prompts",
      "kind": "component",
      "title": "Starter prompts",
      "description": "A greeting and a grid of starter cards for the empty state of an AI app. The cards are grouped into Write, Code, Analyze and Plan tabs, and a shuffle swaps them with a short staggered crossfade. Picking a card fills the composer, and cards that use a connected app show its mark (Gmail, GitHub, Notion, Linear, Google Calendar).",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "new"
      ],
      "docs": "https://uiarc.dev/components/starter-prompts",
      "markdown": "https://uiarc.dev/components/starter-prompts/markdown",
      "registry": null,
      "keywords": [
        "starter prompts",
        "prompt suggestions",
        "empty state",
        "ai chat",
        "new chat",
        "conversation starters",
        "prompt cards",
        "composer",
        "shuffle",
        "integrations"
      ],
      "whenToUse": [
        "The empty state of a chat, copilot or agent, before the first message.",
        "Teaching what an assistant can do with connected tools, such as Gmail, GitHub or Notion.",
        "Onboarding a new workspace with a few tailored, ready to send prompts."
      ],
      "whenNotToUse": [
        "Inside a running conversation; use follow-up suggestion chips in the chat thread instead.",
        "A command launcher with search; use command-palette or ai-command.",
        "A single inline suggestion in a text field; use ghost-text."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The component is a labelled section; the greeting is its heading.",
        "Each card is a button whose name includes the title, the connected app and the category.",
        "A polite live region announces picks, shuffles, sending, done and errors, and says when a send is simulated.",
        "Status is never color alone: done shows a check and label, errors an alert icon and the message.",
        "The send button is labelled Send message, and Stop sending while a message is in flight."
      ],
      "motion": [
        "Shuffle and tab changes crossfade each card's content inside a fixed frame: out in 100ms, in over 160ms with a 2px rise, staggered 35ms per card (about 100ms in total).",
        "Picking a card focuses the composer and fades the new text in over 160ms.",
        "The footer status and the send or stop icon crossfade; nothing moves or resizes.",
        "Reduced motion keeps every end state and cuts the fades and the skeleton breathing."
      ],
      "responsive": [
        "Two columns of cards above about 560px of container width, one column below.",
        "Tabs scroll inside themselves with edge fades; Shuffle becomes an icon button under about 440px.",
        "Padding and the outer radius step down under 480px of viewport width. Holds at 360px with no horizontal scroll."
      ],
      "performance": [
        "Renders only the cards on screen; the pool can hold hundreds of starters.",
        "No timers or loops while idle; the skeleton uses one CSS animation.",
        "Marks are inline SVG, no images to load."
      ],
      "aiNotes": [
        "Use it as the first screen of a new chat or agent thread, then swap it for the conversation once a message is sent.",
        "Keep titles short and verb first, and put the full, specific instruction in prompt; the composer shows prompt, not title.",
        "Set app only when the prompt really uses that integration, so the mark means something.",
        "Wire onSubmit to your chat endpoint and honor the AbortSignal for Stop."
      ],
      "related": [
        "chat-thread",
        "tool-call",
        "ghost-text",
        "empty-state",
        "ai-command",
        "segmented-control"
      ],
      "usage": "import { StarterPrompts } from \"@/components/arc/starter-prompts/starter-prompts\";\n\nexport function NewChat() {\n  return (\n    <StarterPrompts\n      greeting=\"Good morning, Maya\"\n      onSelect={prompt => track(\"starter_picked\", { id: prompt.id })}\n      onSubmit={async (message, { signal }) => {\n        const res = await fetch(\"/api/chat\", { method: \"POST\", body: JSON.stringify({ message }), signal });\n        if (!res.ok) throw new Error(\"Couldn't send. Try again.\");\n      }}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Your own starters and tabs",
          "code": "<StarterPrompts\n  categories={[{ id: \"support\", label: \"Support\" }, { id: \"sales\", label: \"Sales\" }]}\n  prompts={[\n    { id: \"refund\", category: \"support\", app: \"gmail\", title: \"Answer refund requests\", description: \"Draft replies for the 6 open threads\", prompt: \"Draft replies to the 6 open refund threads in Gmail...\" },\n    { id: \"pipeline\", category: \"sales\", title: \"Review the pipeline\", description: \"Deals that slipped this month\", prompt: \"List the deals that slipped in October and why...\" },\n  ]}\n  model={{ name: \"GPT-6.1 Sol\", mark: <OpenAIMark width={14} height={14} /> }}\n/>"
        }
      ],
      "exports": [
        "StarterPrompts"
      ]
    },
    {
      "name": "ai-mascot",
      "kind": "component",
      "title": "AI mascot",
      "description": "A small assistant character, a soft pebble with pill eyes and a companion dot, that shows what the AI is doing. It breathes and glances when idle, leans in to listen, looks up while the dot orbits its head, reads along the line it writes, squints with a sparkle when done and shakes once with worried brows on an error.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ai-mascot",
      "markdown": "https://uiarc.dev/components/ai-mascot/markdown",
      "registry": null,
      "keywords": [
        "ai mascot",
        "assistant character",
        "ai avatar",
        "thinking animation",
        "generating",
        "loading character",
        "agent face",
        "blob",
        "listening",
        "writing",
        "done sparkle",
        "error shake",
        "empty state",
        "eyes follow cursor",
        "svg animation"
      ],
      "whenToUse": [
        "Give an assistant a face that shows what it is doing: in a reply header, beside a composer, on an image canvas while it generates, or in an empty state.",
        "Products that want generation to feel alive and friendly without a spinner, with states that read at 24px."
      ],
      "whenNotToUse": [
        "Use agent-status when people need the steps, tools and elapsed time of a run, not just a mood.",
        "Use text-shimmer alone for a plain \"Thinking\" line in dense interfaces where a character would distract.",
        "Do not use it as a progress indicator with a known percentage; use a progress bar."
      ],
      "accessibility": [
        "The SVG has role img and a label built from name and status, for example \"Atlas, thinking\", which updates with every state.",
        "With announce, status changes are read in a polite live region outside the image. Use it on one mascot per view so screen readers do not hear duplicates.",
        "Status is never carried by motion alone: each state changes the face (wide eyes, eyes up, eyes down, happy squint, worried brows) and the label. Show a text status next to it where the state matters.",
        "Pointer following ignores touch and is off under reduced motion."
      ],
      "motion": [
        "Every state is a continuous function of time (sums of slow sines, an orbit angle, a writing cycle whose start and end meet at zero length), so loops never restart and have no seam.",
        "A status change blends the old pose into the new one over 420ms with an ease in and out; a change during a blend starts from the frame on screen. The first paint (and the server render) is the still pose, and the loop eases out of it over the same 420ms, so nothing jumps on mount. Eyes ride a critically damped spring: quick saccades when idle, a soft follow while thinking, tight tracking while writing. Nothing bounces.",
        "Done passes the eyes through a closed line into the happy squint while the dot shrinks and the sparkle grows in its place. Error shakes once, decaying to rest in about .7s.",
        "The dot is drawn twice, behind and in front of the body, and switches layers only at the ends of its orbit where it is clear of the body. While it crosses the crown in front it fades into the eye color, so it stays visible on the body.",
        "Blinks come every 2.4 to 5.2s, sometimes twice, and take 160ms.",
        "Reduced motion shows a still, representative pose for each state (eyes up with the dot over the head, half a written line, the squint and sparkle, the brows), switches in place and drops every loop, glance, blink and shake."
      ],
      "responsive": [
        "A fixed square of size px that never reflows its parent. The drawing scales crisply as vector art at any device pixel ratio.",
        "Below 56px the compact drawing frames the body tighter and enlarges the eyes, dot and sparkle so they stay legible at 24px."
      ],
      "performance": [
        "One requestAnimationFrame loop per mascot writes SVG attributes directly through refs, so React does not re-render per frame; only the self-playing loop sets state, once per state.",
        "The loop stops off screen (IntersectionObserver) and in hidden tabs, and its clock only runs while visible, so it resumes on the same frame instead of jumping.",
        "Under reduced motion the loop repaints only when the status changes.",
        "About fifteen small SVG nodes, with no filters, masks or gradients."
      ],
      "aiNotes": [
        "Map your assistant's real phases to status: microphone open is listening, waiting for the first token is thinking, streaming is writing, finished is done, failed is error, otherwise idle.",
        "Keep thinking short in real use; switch to writing on the first token so the mascot reads what it writes.",
        "Use size 24 to 32 inline in a message header or composer, 40 to 64 in a side panel header, 96 to 160 in an empty state. Below 56 the drawing simplifies on its own.",
        "Set name to the assistant's name so the label reads well. Turn announce on for one mascot only.",
        "Leave status unset only for showcases; in a product always pass it."
      ],
      "related": [
        "agent-status",
        "text-shimmer",
        "ai-spark",
        "image-reveal",
        "starter-prompts",
        "dictation-button",
        "ai-chat",
        "reasoning-block"
      ],
      "usage": "import { AiMascot, type MascotStatus } from \"@/components/arc/ai-mascot/ai-mascot\";\n\n// Inline next to a streaming reply\nexport function ReplyHeader({ phase }: { phase: \"thinking\" | \"streaming\" | \"complete\" | \"failed\" }) {\n  const status: MascotStatus = phase === \"streaming\" ? \"writing\" : phase === \"complete\" ? \"done\" : phase === \"failed\" ? \"error\" : \"thinking\";\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n      <AiMascot status={status} size={24} name=\"Atlas\" followPointer={false} />\n      <span>Atlas</span>\n    </div>\n  );\n}\n\n// Empty state: large, watching the pointer anywhere on the page\n<AiMascot status=\"idle\" size={160} followPointer=\"page\" name=\"Atlas\" />\n\n// In a voice composer while the microphone is open\n<AiMascot status={recording ? \"listening\" : \"idle\"} size={40} tone=\"ink\" announce />\n\n// Brand color body with light eyes\n<AiMascot status=\"thinking\" size={96} color=\"#7747ff\" eyeColor=\"white\" />",
      "exports": [
        "AiMascot"
      ]
    },
    {
      "name": "image-reveal",
      "kind": "component",
      "title": "Image reveal",
      "description": "An image generation canvas where a soft, blurred preview made from the image's own colors sharpens top to bottom behind a soft-edged band. It works the way ChatGPT, Midjourney and Gemini show images forming, with fine film grain, a translucent progress pill and calm queued and error states.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/image-reveal",
      "markdown": "https://uiarc.dev/components/image-reveal/markdown",
      "registry": null,
      "keywords": [
        "image generation",
        "AI image",
        "generating",
        "progressive reveal",
        "diffusion",
        "denoising",
        "latent noise",
        "sampler steps",
        "vae decode",
        "progress",
        "canvas",
        "image placeholder",
        "midjourney style",
        "chatgpt image"
      ],
      "whenToUse": [
        "Showing an image model's progress on a canvas or in a chat reply.",
        "A batch of image variations sampling side by side.",
        "A queued image job that should feel alive before it starts."
      ],
      "whenNotToUse": [
        "Plain image loading from a CDN; use a blur-up placeholder or a skeleton.",
        "Text generation or tool calls; use text-shimmer or tool-call.",
        "Determinate file uploads; use progress."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The canvas has role img; its label names the state while generating and becomes the alt text once done.",
        "A polite live region announces Queued, Generating image, Image ready and the error message, not every step. Auto mode appends simulated.",
        "While generating, the pill is a progressbar with aria-valuenow and an aria-valuetext of Step n of total.",
        "Status is never color alone: the pill pairs a ring or check with a short label, and the error state pairs an alert icon with its message."
      ],
      "motion": [
        "Queued is the calm surface tone with one broad, soft shimmer that starts and ends fully off canvas, so its 2.6s loop has no seam.",
        "Generating starts from pure latent noise: per-cell gaussian color at about 1/8 of the output resolution, drawn as crisp blocks, with a new seeded frame every step crossfaded in 35ms.",
        "Under the noise is the model's current prediction, built from the image itself (6 to 96 px wide copies, smoothly upscaled). It moves along that chain per step, so color blobs come first, then composition, then shapes.",
        "Noise strength follows a schedule that drops fast early, so the composition reads by about a third of the steps and the last steps only refine. Steps run at a constant rate, like a real sampler.",
        "After the last step a tiled decode lands the sharp image tile by tile in raster order, each tile crossfading in, and the pill switches to Decoding.",
        "One soft-light sheen sweeps across once at completion (1.1s), the pill shows the elapsed time, then fades out.",
        "Starting over or swapping the photo fades the decoded image straight back to the surface before anything resets underneath, and a photo swap crossfades from a snapshot, so there is never a hard cut.",
        "Pill labels crossfade (100ms out, 160ms in after a 60ms gap) and the pill width springs with no bounce.",
        "Reduced motion keeps one static noise frame, decodes as a single crossfade and drops the shimmer travel, spinner and sheen.",
        "The frame loop stops off screen (IntersectionObserver) and in background tabs, and draws nothing once the image is resolved."
      ],
      "responsive": [
        "Fills its container width (up to 560px by default) and keeps the given aspect ratio.",
        "The canvas renders at device pixel ratio (capped at 2), so it stays crisp from a 120px thumbnail to a large canvas.",
        "Latent noise is sized in CSS pixels (about 6 px cells), so it reads as the same latent grid at every size."
      ],
      "performance": [
        "Canvas 2D only, no WebGL and no per-pixel work per frame: a few drawImage calls. Noise is generated once per step at latent resolution (a few thousand cells).",
        "The prediction chain is built once per image and size; decode tiles are direct copies from the sharp image.",
        "Frames stop drawing when the image is resolved and nothing changes, and the loop stops entirely off screen.",
        "Images are decoded off the main thread before use."
      ],
      "aiNotes": [
        "Use for image generation surfaces: an image canvas, an inline image in a chat reply, a variations grid, or an empty state while a first image renders.",
        "Wire a real job by passing status and progress (step / total), plus the job's seed. Pass decoding while the VAE decodes if the backend reports it. With no progress events, pass status only and an estimated duration.",
        "Use size=\"sm\" in grids. A batch samples in lockstep, so give every tile the same duration and its own seed, and override max-inline-size through className or style.",
        "The preview is sampled from the final image, so pass src as soon as a preview or final URL exists; until it loads the tile stays in the calm queued look.",
        "For text or agent work in progress, pick a text shimmer or an agent status component instead."
      ],
      "related": [
        "text-shimmer",
        "agent-status",
        "tool-call",
        "progress"
      ],
      "usage": "import { ImageReveal } from \"@/components/arc/image-reveal/image-reveal\";\n\nexport function GeneratedImage({ job }: { job: { status: \"queued\" | \"generating\" | \"done\" | \"error\"; step: number; url?: string } }) {\n  return (\n    <ImageReveal\n      src={job.url}\n      alt=\"A calm alpine lake at golden hour\"\n      status={job.status}\n      progress={job.step / 30}\n      steps={30}\n      aspectRatio=\"4 / 5\"\n      onRetry={() => retry(job)}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Estimated duration, no progress events",
          "code": "<ImageReveal src={url} status={url ? \"done\" : \"generating\"} duration={8000} />"
        },
        {
          "title": "Batch of four with a Generate again button",
          "code": "const [run, setRun] = useState(0);\n\n<div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8 }}>\n  {urls.map((url, index) => (\n    <ImageReveal key={index} src={url} size=\"sm\" seed={index + 1}\n      runKey={run} loop={false} duration={4200} style={{ maxInlineSize: \"none\" }} />\n  ))}\n</div>\n<Button onClick={() => setRun(run + 1)}>Generate again</Button>"
        }
      ],
      "exports": [
        "ImageReveal"
      ]
    },
    {
      "name": "ai-spark",
      "kind": "component",
      "title": "AI spark",
      "description": "A brand-quality animated spark mark. Four tapered rays and four lighter diagonals breathe at rest, ripple while thinking, pulse like a heartbeat while writing, then gather and settle when done. Every status change blends from the pose the mark is already in.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ai-spark",
      "markdown": "https://uiarc.dev/components/ai-spark/markdown",
      "registry": null,
      "keywords": [
        "ai spark",
        "ai sparkle",
        "asterisk",
        "thinking indicator",
        "generating indicator",
        "ai loader",
        "ai logo animation",
        "working indicator",
        "ai status mark",
        "spinner alternative",
        "ai avatar",
        "sparkle animation"
      ],
      "whenToUse": [
        "Showing that an assistant is thinking or writing, inline above a streaming reply.",
        "A send or generate button that turns into a working indicator inside a composer.",
        "An image or canvas tool that needs a calm, branded working mark over the canvas while a render runs.",
        "Empty states and onboarding where a 64 to 96px hero mark gives the AI feature an identity."
      ],
      "whenNotToUse": [
        "Determinate progress with a known percentage; use a progress bar or the generation stats component instead.",
        "Multi step agent runs where each step needs its own state and detail; use agent-status or agent-run.",
        "Plain text loading lines with no mark; use text-shimmer."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The root is a polite live region (role=\"status\"): a status change is announced once, as \"AI is thinking\" for the mark or as the label text for the pill.",
        "aria-busy is set while thinking or generating.",
        "Status is never color alone: each state has its own shape and motion, error shortens the rays and dims the diagonals, and the pill adds a text label.",
        "Set announce={false} on decorative copies (lists, size ladders) so screen readers hear one voice. They then render as role=\"img\" with an accessible name.",
        "The SVG is aria-hidden; the name comes from the label or a visually hidden phrase."
      ],
      "motion": [
        "One requestAnimationFrame loop per spark writes eight small ray paths, a core radius and two group transforms. React never re-renders per frame and nothing triggers layout.",
        "Every status is a periodic function of time and of position around the ring, so idle, thinking and generating loop with no seam.",
        "Idle: a 6.4s breath of the whole mark while one diagonal at a time softly reaches out. Thinking: one crest ripples around the ring every 1.9s, with small per-ray phase offsets, while the mark turns at 26 degrees per second. Generating: a lub dub heartbeat every 1.04s, axes firing in sequence and diagonals answering, 46 degrees per second, with two particles orbiting just outside the rays (40px and up).",
        "Done: the rays gather toward the center in 170ms and ease back to rest over 510ms with no overshoot, while the spin glides into the home angle on an ease-out whose speed only falls.",
        "Error: one small decaying shake over 460ms, shorter rays, dimmer diagonals and a muted danger tone over 320ms.",
        "Status changes blend over 420 to 560ms from wherever the mark is; an interrupted blend becomes the new starting point, and the gather and shake gestures always finish smoothly even if the status changes again. Pill labels crossfade (100ms out, 160ms in, 2px rise) while the pill eases to its new width.",
        "Reduced motion: no spin, loops, gather, shake or particles. Each status shows a still pose with a 200ms blend between poses and the color change.",
        "The loop pauses when the spark is off screen or the tab is hidden, and stops entirely once a resting state has settled."
      ],
      "responsive": [
        "The mark has a fixed footprint at any status, so it never shifts surrounding layout.",
        "Crisp from 12px to 120px and beyond: geometry lives in a 100 unit viewBox, ray weight is hinted on a log scale by rendered size, and CSS lengths are measured so hinting still applies.",
        "The pill keeps a 28px height and only animates its label width."
      ],
      "performance": [
        "Eight SVG paths, three circles and one rAF loop per spark; resting states stop the loop entirely, and off screen sparks do not tick.",
        "No canvas, no filters, no blur, no gradients; depth comes from two tones of one color.",
        "The pill label shimmer runs only while thinking or generating."
      ],
      "aiNotes": [
        "Map your stream lifecycle to status: request sent or tool running -> thinking, first token until the last -> generating, finished -> done, failed or aborted -> error. Return to idle when the composer is ready again.",
        "Use variant=\"pill\" with label for specific progress (\"Searching 4 sources\", \"Drawing\"); keep labels to one or two words.",
        "Use size=\"1em\" and tone=\"inherit\" to drop the mark inline into a sentence or a button so it matches the text.",
        "Use one spark per active generation. For many rows, render a static mark (status=\"idle\", twinkle={false}) and animate only the active row.",
        "Use variant=\"avatar\" next to assistant messages or inside an icon button; size sets the tile and the mark is 56% of it.",
        "Leaving status unset makes it self-play, which is only meant for demos and marketing pages."
      ],
      "related": [
        "text-shimmer",
        "agent-status",
        "reasoning-block",
        "ai-mascot",
        "ai-chat",
        "ai-composer"
      ],
      "usage": "import { AiSpark } from \"@/components/arc/ai-spark/ai-spark\";\n\nexport function ReplyStatus({ phase }: { phase: \"thinking\" | \"generating\" | \"done\" }) {\n  return <AiSpark variant=\"pill\" status={phase} />;\n}",
      "examples": [
        {
          "title": "Assistant avatar",
          "code": "<AiSpark variant=\"avatar\" size={32} status={phase} announce={false} />"
        },
        {
          "title": "Inline in a sentence",
          "code": "<p>\n  <AiSpark size=\"1em\" tone=\"inherit\" status=\"generating\" announce={false} /> Drafting a reply to Maya Chen\n</p>"
        },
        {
          "title": "Custom progress label",
          "code": "<AiSpark variant=\"pill\" status=\"thinking\" label=\"Searching 4 sources\" />"
        },
        {
          "title": "Hero mark in an empty state",
          "code": "<AiSpark size={96} status=\"idle\" />"
        }
      ],
      "exports": [
        "AiSpark"
      ]
    },
    {
      "name": "pixel-mascot",
      "kind": "component",
      "title": "Pixel mascot",
      "description": "A pixel-art robot that shows what an AI assistant is doing. It idles, thinks, types, celebrates and errors in frame-stepped sprite animations, and stays crisp at every integer size from 24px to 192px.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/pixel-mascot",
      "markdown": "https://uiarc.dev/components/pixel-mascot/markdown",
      "registry": null,
      "keywords": [
        "pixel art",
        "8-bit",
        "16-bit",
        "sprite",
        "mascot",
        "robot",
        "ai loading",
        "thinking indicator",
        "typing indicator",
        "generating",
        "assistant avatar",
        "retro",
        "loader",
        "empty state"
      ],
      "whenToUse": [
        "A friendly generating indicator next to an assistant reply or in a chat composer.",
        "An empty state or onboarding panel where a small character makes an AI feature feel approachable.",
        "Image or canvas generation, where a mascot in the corner shows thinking, drawing and done.",
        "Products with a playful or retro visual voice that still want precise, calm motion."
      ],
      "whenNotToUse": [
        "Detailed agent progress with steps and timings; use agent-status or agent-timeline.",
        "Dense tables or toolbars where a 24px character is noise; use status-dot or a spinner.",
        "Serious or regulated contexts (finance, health, legal) where a character feels flippant; use text-shimmer.",
        "Showing model reasoning content; use reasoning-block."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The sprite is one SVG with role=\"img\" and a label such as \"Arc is thinking\" that follows the status.",
        "A visually hidden polite live region announces status changes when the status is controlled; set announce to false when the surrounding UI already reports progress, to avoid double announcements.",
        "Status is never color alone: each state has its own shape (thought bubble, keyboard, happy eyes, crossed eyes) and the label carries the words.",
        "The error state uses the danger token for the crossed eyes on a tinted screen, so it reads in light and dark and with any accent."
      ],
      "motion": [
        "Stepped on purpose: the sprite advances on a fixed 40ms clock and moves in whole pixels. Timings are authored as ease curves in steps, for example the done jump rises fast, holds at the top for 120ms and settles without a rebound.",
        "Status changes never cut: lids close and open, eyes slide one pixel per step, arms pass through neighbouring poses, the thought bubble and keyboard step in and out from wherever they are. A change mid animation reverses from the current frame.",
        "Loops are seamless by construction: idle bobs on a 2s cycle with blinks and a glance on an 8s cycle, thinking cycles three dots every 1.12s, writing types an eight stroke bar every 1.6s while the eyes scan and return.",
        "Only SVG attributes change (transform, x, y, visibility, opacity) from a requestAnimationFrame loop outside React, so there is no layout work or re-render per frame.",
        "Pauses off screen with an IntersectionObserver and when paused is set. With reduced motion the mascot holds a clear resting pose for each status: open eyes, bubble with three dots, keyboard, happy eyes or crossed eyes."
      ],
      "responsive": [
        "The sprite is a fixed square of 24 times size CSS pixels and never reflows; pick a smaller size on narrow layouts.",
        "Inline-flex with vertical-align middle, so it sits on a text line at sizes 1 and 2.",
        "Crisp at every integer size thanks to shape-rendering crispEdges and whole pixel transforms."
      ],
      "performance": [
        "One SVG with about 40 nodes and no images. The loop writes only changed attributes, about 25 times a second at most, and skips writes on unchanged steps.",
        "No React re-render per frame; the loop stops off screen, when paused and under reduced motion.",
        "Many instances on one page are fine; each runs its own lightweight loop only while visible."
      ],
      "aiNotes": [
        "Choose PixelMascot for a characterful \"AI is working\" indicator with retro charm: inline in a reply at size 2, in a composer at size 2 or 3, on an image canvas or empty state at size 4 to 8.",
        "Map your own lifecycle to the five statuses: request sent is thinking, first token is writing, finished is done, failures are error, and settle back to idle a couple of seconds after done.",
        "Keep size an integer; the art is drawn for whole pixel multiples and stays crisp at every one of them.",
        "The palette follows --accent and the theme neutrals. Set --accent on a parent to brand it; it looks right with Arc's near neutral grey default too.",
        "Pair it with text, such as a TextShimmer label, when users need to know exactly what is happening; the mascot carries the mood, the text carries the detail."
      ],
      "related": [
        "ai-mascot",
        "cube-mascot",
        "ghost-mascot",
        "agent-status",
        "text-shimmer",
        "reasoning-block",
        "chat-thread",
        "empty-state"
      ],
      "usage": "import { PixelMascot } from \"@/components/arc/pixel-mascot/pixel-mascot\";\n\n// Inline next to a streaming reply\n<PixelMascot status={isStreaming ? \"writing\" : \"thinking\"} size={2} name=\"Claude Sonnet 5.5\" announce={false} />\n\n// Empty state or composer, self playing\n<PixelMascot size={6} />",
      "exports": [
        "PixelMascot",
        "usePixelMascotLoop"
      ]
    },
    {
      "name": "ghost-mascot",
      "kind": "component",
      "title": "Ghost mascot",
      "description": "A floating ghost mascot whose soft sheet ripples at the hem while it hovers. It blinks when idle, turns toward you to listen, fades a little translucent while thinking, draws a tiny pen line while writing, smiles with a sparkle when done and wobbles once on an error.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/ghost-mascot",
      "markdown": "https://uiarc.dev/components/ghost-mascot/markdown",
      "registry": null,
      "keywords": [
        "ghost mascot",
        "ai mascot",
        "assistant character",
        "ai avatar",
        "generating animation",
        "thinking animation",
        "loading character",
        "ghost",
        "listening",
        "writing",
        "done sparkle",
        "error wobble",
        "empty state",
        "svg path morph",
        "wavy hem"
      ],
      "whenToUse": [
        "Give an assistant a light, friendly face that shows what it is doing: in a reply header, in a composer, over an image canvas while it generates, or in an empty state.",
        "Products that want generation to feel alive without a spinner, with states that still read at 24px."
      ],
      "whenNotToUse": [
        "Use agent-status when people need the steps, tools and elapsed time of a run, not just a mood.",
        "Use text-shimmer alone for a plain \"Thinking\" line in dense interfaces where a character would distract.",
        "Do not use it as a progress indicator with a known percentage; use a progress bar."
      ],
      "accessibility": [
        "The SVG has role img and a label built from name and status, for example \"Atlas, thinking\", which updates with every state.",
        "With announce, status changes are read in a polite live region outside the image. Use it on one ghost per view so screen readers do not hear duplicates.",
        "Status is never carried by color or motion alone: each state changes the face and marks (wide eyes, eyes up with a swirl, a pen and line, happy eyes with a sparkle, droopy eyes) and the label. Show a text status next to it where the state matters."
      ],
      "motion": [
        "Every loop is a function of a phase that only grows: the float, the two hem waves, the swirl and the writing cycle. Rates blend by integrating into those phases, so speed changes never jump and nothing restarts.",
        "The hem is two traveling sine waves drawn as exact Hermite curves through rounded corners, rebuilt every frame. It trails the float: the sheet stretches on the way up, and the hem lifts and flares on the way down.",
        "A status change blends every pose value from the frame on screen to the new pose over 400ms with an ease in and out. Eyes ride a critically damped spring: quick saccades when idle, a slow drift up while thinking, tight tracking of the pen tip while writing. Nothing bounces.",
        "The swirl draws in from its outer end and erases behind itself; the writing line draws word by word, the pen lifting between words, then erases while the pen glides back. Both are empty at the moment their cycle wraps, so there is no seam.",
        "Done grows the sparkle in 320ms with an ease out and a quarter turn, then a slow twinkle. Error wobbles once, decaying to rest in about 1.2s, softened at the start so it never kicks.",
        "Reduced motion shows a still, representative pose for each state (a half drawn swirl, half a written line, the sparkle, the droop), switches in place and drops every loop, glance, blink and wobble."
      ],
      "responsive": [
        "A fixed square of size px that never reflows its parent. The drawing is vector art and stays crisp at any device pixel ratio.",
        "Below 56px the compact drawing frames the ghost tighter and enlarges the eyes and marks so they stay legible at 24px."
      ],
      "performance": [
        "One requestAnimationFrame loop per ghost writes SVG attributes directly, so React does not re-render per frame; only the self-playing loop sets state, once per state.",
        "The loop stops off screen (IntersectionObserver) and in hidden tabs, and its clock only runs while visible, so it resumes on the same frame instead of jumping.",
        "Under reduced motion the loop repaints only when the status changes.",
        "About a dozen SVG nodes, one clip path, no filters, masks or gradients."
      ],
      "aiNotes": [
        "Map the assistant's real phases to status: microphone open is listening, waiting for the first token is thinking, streaming is writing, finished is done, failed is error, otherwise idle.",
        "Keep thinking short in real use; switch to writing on the first token.",
        "Use size 24 to 32 inline in a reply header or composer, 40 to 64 in a panel or on an image canvas, 96 to 160 in an empty state.",
        "The default accent tone follows the theme accent, including Arc's near-neutral grey. Use tone=\"sheet\" for the classic pale ghost.",
        "Set name to the assistant's name so the label reads well. Turn announce on for one ghost only. Leave status unset only for showcases."
      ],
      "related": [
        "ai-mascot",
        "agent-status",
        "text-shimmer",
        "ai-spark",
        "image-generation",
        "ai-chat",
        "ai-composer",
        "dictation-button"
      ],
      "usage": "import { GhostMascot, type GhostStatus } from \"@/components/arc/ghost-mascot/ghost-mascot\";\n\n// Inline next to a streaming reply\nexport function ReplyHeader({ phase }: { phase: \"waiting\" | \"streaming\" | \"complete\" | \"failed\" }) {\n  const status: GhostStatus = phase === \"streaming\" ? \"writing\" : phase === \"complete\" ? \"done\" : phase === \"failed\" ? \"error\" : \"thinking\";\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n      <GhostMascot status={status} size={24} name=\"Atlas\" />\n      <span>Atlas</span>\n    </div>\n  );\n}\n\n// Empty state: the classic pale sheet\n<GhostMascot status=\"idle\" size={160} tone=\"sheet\" name=\"Atlas\" />\n\n// On an image canvas while it generates\n<GhostMascot status={generating ? \"thinking\" : \"done\"} size={64} announce />\n\n// Brand color sheet with light eyes\n<GhostMascot status=\"listening\" size={96} color=\"#7747ff\" eyeColor=\"white\" />",
      "exports": [
        "GhostMascot"
      ]
    },
    {
      "name": "cube-mascot",
      "kind": "component",
      "title": "Cube mascot",
      "description": "An isometric cube mascot where three shaded faces and a painted-on face show what the assistant is doing. It turns a quarter turn in eased steps while thinking, prints lines onto the floor while writing, settles with a sparkle when done, and shakes once on error.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "ai",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/cube-mascot",
      "markdown": "https://uiarc.dev/components/cube-mascot/markdown",
      "registry": null,
      "keywords": [
        "cube mascot",
        "isometric cube",
        "ai mascot",
        "assistant character",
        "3d cube",
        "thinking animation",
        "generating",
        "loading character",
        "agent face",
        "writing",
        "done sparkle",
        "error shake",
        "empty state",
        "svg animation"
      ],
      "whenToUse": [
        "Give an assistant a recognisable, product-like character that shows it is generating: in a reply header, in a composer, on an image canvas or in an empty state.",
        "Brands with a geometric, technical feel where a soft blob would feel off."
      ],
      "whenNotToUse": [
        "Use ai-mascot for a softer, rounder character, or when you need a listening state.",
        "Use agent-status when people need the steps, tools and elapsed time of a run.",
        "Do not use it as a progress indicator with a known percentage; use a progress bar."
      ],
      "accessibility": [
        "The SVG has role img and a label built from name and status, for example \"Atlas, writing\", which updates with every state.",
        "With announce, status changes are read in a polite live region outside the image. Use it on one mascot per view.",
        "Status is never motion alone: each state changes the face (eyes up, eyes down, happy squint, worried brows and frown) and the label. Show a text status next to it where the state matters.",
        "Under reduced motion every state is a still pose that switches in place."
      ],
      "motion": [
        "The cube is real 3D: eight corners are rotated (yaw, roll, pitch, then a fixed 32 degree camera) and projected each frame. The silhouette is their convex hull with rounded corners, used as a clip, so the outline stays soft while edges between faces stay exact.",
        "Each face is shaded by its normal against a light from the upper left, interpolating between three anchors (top, front, side), so a face turning away darkens continuously.",
        "Thinking holds for .5s, then turns a quarter turn over .8s with a cubic ease in and out, rising slightly and leaning into the turn. A cube looks identical after 90 degrees, so the end of each step is the start of the next and the loop has no seam. The face on the leaving side fades as it turns edge on; the face on the arriving side fades in and its eyes open as it reaches the front.",
        "Writing prints a line every 360ms: each line slides out from under the face along the ground for 1.44s and only fades in and out, and its length changes while it is invisible. Eyes sweep slowly and the cube nods once per four lines.",
        "Idle is a sum of slow sines with incommensurate periods (tilt, sway, float, glance), so it never repeats or restarts. Blinks come every 2.6 to 4.9s, sometimes twice, and take 170ms.",
        "Done dips once and settles while the eyes pass through a closed line into the squint and the sparkle grows and turns in, then twinkles slowly. Error shakes 3.5 times in .56s under a squared envelope that reaches rest with zero velocity.",
        "A status change blends every pose value from the frame on screen to the new state over 420ms (240ms into error) with an ease in and out. A change caught mid turn keeps turning forward instead of unwinding. Nothing bounces."
      ],
      "responsive": [
        "A fixed square of size px that never reflows its parent. Vector art stays crisp at any device pixel ratio.",
        "Below 56px the compact drawing frames the cube tighter and enlarges the eyes so they read at 24px."
      ],
      "performance": [
        "One requestAnimationFrame loop per cube writes SVG attributes directly, and only when they change, so React does not re-render per frame; the self-playing loop sets state once per state.",
        "The loop stops off screen (IntersectionObserver) and in hidden tabs, and its clock only runs while visible, so it resumes on the same frame.",
        "Under reduced motion nothing loops; the drawing repaints only on status changes.",
        "About thirty small SVG nodes and one clip path, no filters, masks or gradients."
      ],
      "aiNotes": [
        "Map your assistant's real phases to status: waiting for the first token is thinking, streaming is writing, finished is done, failed is error, otherwise idle.",
        "Keep thinking short in real use and switch to writing on the first token.",
        "Use size 24 to 32 inline in a message header or composer, 40 to 64 in a panel header, 96 to 160 in an empty state or on a generating canvas.",
        "Set name to the assistant's name. Turn announce on for one mascot only. Leave status unset only for showcases.",
        "The default body is the accent, which is near neutral grey in Arc; the shading anchors come from it, so any accent or color prop works in light and dark."
      ],
      "related": [
        "ai-mascot",
        "agent-status",
        "text-shimmer",
        "ai-spark",
        "image-reveal",
        "ai-chat",
        "reasoning-block"
      ],
      "usage": "import { CubeMascot, type CubeStatus } from \"@/components/arc/cube-mascot/cube-mascot\";\n\n// Inline next to a streaming reply\nexport function ReplyHeader({ phase }: { phase: \"waiting\" | \"streaming\" | \"complete\" | \"failed\" }) {\n  const status: CubeStatus = phase === \"streaming\" ? \"writing\" : phase === \"complete\" ? \"done\" : phase === \"failed\" ? \"error\" : \"thinking\";\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n      <CubeMascot status={status} size={24} name=\"Atlas\" />\n      <span>Atlas</span>\n    </div>\n  );\n}\n\n// Empty state on an image canvas while it generates\n<CubeMascot status={generating ? \"thinking\" : \"idle\"} size={144} name=\"Atlas\" announce />\n\n// Brand colored body with light features\n<CubeMascot status=\"writing\" size={64} color=\"#4f6bff\" faceColor=\"white\" />",
      "exports": [
        "CubeMascot"
      ]
    },
    {
      "name": "tone-tuner",
      "kind": "component",
      "title": "Tone tuner",
      "description": "A brand voice card where two sliders (formality and detail) and Friendly, Confident and Empathetic chips rewrite a customer reply. Each changed sentence crossfades in place, new words can be highlighted against the original, and Copy takes the current version.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/tone-tuner",
      "markdown": "https://uiarc.dev/components/tone-tuner/markdown",
      "registry": null,
      "keywords": [
        "tone",
        "brand voice",
        "rewrite",
        "ai writing",
        "formality",
        "tone slider",
        "copy editing",
        "email reply",
        "diff highlight",
        "support reply"
      ],
      "whenToUse": [
        "A composer or help desk reply that offers AI tone controls before sending.",
        "Brand voice tooling where marketers compare versions of the same copy.",
        "Any rewrite where people want to steer with a few dials rather than write a prompt."
      ],
      "whenNotToUse": [
        "Free form AI editing with a prompt box: use a chat or composer instead.",
        "Long documents: the card is sized for a message of a few paragraphs.",
        "Exact line by line code or contract diffs: use a dedicated diff view."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "Voice chips are toggle buttons with aria-pressed inside a group named Voice.",
        "Each slider is named (Formality, Detail) and speaks its level name, not a number.",
        "A polite live region announces each landed rewrite with its settings, says when the sample is simulated (nothing was sent), and reports failures.",
        "Highlighted words use a mark element and the header counts the changed words, so changes are not shown by color alone.",
        "Status pairs an icon with a short label: Original draft, Rewriting, Rewritten or Couldn't rewrite with a Retry button."
      ],
      "motion": [
        "Each changed sentence fades out in 100ms, swaps while invisible and fades in over 160ms; all changed sentences swap on the same frame, so the paragraph reflows once, out of sight. Unchanged sentences never animate.",
        "The text area reserves the height of the longest voice at the current formality and detail, so switching voice never resizes the card; a slider move eases it to the new height on a bounce free spring.",
        "The status flips to Rewriting on the same frame as the input, with a calm shimmer, then crossfades to Rewritten.",
        "The self playing tour pauses off screen and stops at the first interaction. Reduced motion swaps text instantly and skips the tour."
      ],
      "responsive": [
        "The card is fluid up to 640px. Under 520px the sliders stack, the chips share one row in equal columns and the status hides its settings.",
        "Under 400px the chip icons and changed count drop so labels never clip. Holds at 360px without horizontal scroll.",
        "Sliders take touch drags; vertical pans still scroll the page."
      ],
      "performance": [
        "Rewrites are debounced and superseded requests are aborted, so dragging a slider sends at most one request after it settles.",
        "The word diff is an O(n×m) table over the message words, fine for a few hundred words.",
        "Only changed sentences animate opacity; no layout animation on text."
      ],
      "aiNotes": [
        "Use for tone and length controls on AI rewrites: support replies, sales emails, release notes, in-app copy.",
        "Pass `text` and `rewrite` together. Send `instructions` to the model as the system or user instruction; it already asks to keep facts, numbers and names.",
        "Return plain text from `rewrite` with the same paragraph breaks so sentences line up and only changed sentences animate.",
        "The diff is word level against the original draft; keep the original stable while the person tunes."
      ],
      "related": [
        "slider",
        "switch",
        "copy-button",
        "text-shimmer",
        "segmented-control",
        "agent-status"
      ],
      "usage": "import { ToneTuner } from \"@/components/arc/tone-tuner/tone-tuner\";\n\nexport function ReplyTone({ draft }: { draft: string }) {\n  return (\n    <ToneTuner\n      text={draft}\n      recipient={{ name: \"Sofia Ramirez\", subject: \"Re: Duplicate charge on INV-20481\" }}\n      rewrite={async ({ text, instructions }, signal) => {\n        const res = await fetch(\"/api/rewrite\", { method: \"POST\", body: JSON.stringify({ text, instructions }), signal });\n        if (!res.ok) throw new Error(\"The model did not respond.\");\n        return (await res.json()).text;\n      }}\n      onRewrite={text => console.log(\"New version\", text)}\n    />\n  );\n}",
      "exports": [
        "ToneTuner"
      ]
    },
    {
      "name": "upgrade-prompt",
      "kind": "component",
      "title": "Upgrade prompt",
      "description": "An upgrade nudge for the moment someone hits a limit. It shows a usage meter at 50 of 50 AI credits, a rolling price with a monthly and yearly toggle, three concrete unlocks with real model marks, and Upgrade or Maybe later. Upgrading leads into a success state with the new limits, and the card never changes size between states.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/upgrade-prompt",
      "markdown": "https://uiarc.dev/components/upgrade-prompt/markdown",
      "registry": null,
      "keywords": [
        "upgrade",
        "paywall",
        "upsell",
        "limit reached",
        "usage limit",
        "credits",
        "plan upgrade",
        "pricing toggle",
        "monthly yearly",
        "conversion",
        "nudge",
        "out of credits",
        "pro plan",
        "billing"
      ],
      "whenToUse": [
        "An AI product that hits a usage limit (credits, generations, seats) and needs a tasteful, conversion focused nudge.",
        "A paywall moment inside an app, in a dialog, popover or inline in place of the blocked result.",
        "Offering one next plan with a monthly and yearly price."
      ],
      "whenNotToUse": [
        "Comparing several plans side by side: use a pricing table or plan picker.",
        "Buying prepaid credits without a plan change: use credit-topup.",
        "Passive usage reporting on a billing page: use a usage meter."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section (\"Upgrade to Pro\") with a heading, so it reads well inside a dialog or popover.",
        "The usage bar is role=\"meter\" with a value text such as \"50 of 50 AI credits used\".",
        "Status is an icon plus a label (Limit reached, 1,950 AI credits left), never color alone.",
        "A polite live region announces upgrading, success and errors. Without onUpgrade it says the upgrade is simulated and nothing was charged.",
        "The rolling price is aria-hidden with a plain text copy for screen readers. Inactive states are inert and hidden, so focus never lands on a faded panel.",
        "Upgrade stays focusable while processing (aria-busy) so keyboard focus is not lost."
      ],
      "motion": [
        "The usage bar fills once when it scrolls into view and settles to the new limit after the upgrade on a 0.5s bounce-free spring, moved with transform so its rounded end never stretches.",
        "The price rolls digit by digit between monthly and yearly on a 0.28s spring, in tabular numerals so nothing shifts.",
        "Title, description, meter label and counts crossfade: 100ms out, 160ms in with a 2px rise. Panels fade the same way with a 4px rise.",
        "Every variant and panel shares one grid cell, so the card never changes size between offer, processing, error, success and reminder, and the buttons stay in place.",
        "Reduced motion: the bar and price jump to their values; crossfades become plain fades."
      ],
      "responsive": [
        "Fluid up to 440px. Below 420px viewport width padding drops to 20px.",
        "At very narrow widths the billing toggle wraps under the price; feature lines stay single line.",
        "Buttons sit side by side at every width: Maybe later at its natural width, Upgrade filling the rest."
      ],
      "performance": [
        "No timers or loops while idle; the only timeout is the simulated upgrade.",
        "The meter and price animate transforms only, through motion values, so there are no re-renders while they move.",
        "Two inline SVG marks, about 4 KB."
      ],
      "aiNotes": [
        "Show it at the moment of value: when an action is blocked by the limit, not on page load. Put it in a dialog or popover anchored to the blocked control.",
        "Keep the three features concrete and specific to what the person was just doing (more credits, the better model, no waiting).",
        "The price is display only. Charge the plan and interval on the server; amount in the selection is for confirmation copy.",
        "Pass onContinue to close the prompt and retry the blocked action, so upgrading feels like it unblocked them.",
        "Snooze the nudge in onDismiss so it does not reappear on every blocked action."
      ],
      "related": [
        "credit-topup",
        "plan-picker",
        "usage-meter",
        "checkout-live",
        "segmented-control",
        "button"
      ],
      "usage": "import { UpgradePrompt } from \"@/components/arc/upgrade-prompt/upgrade-prompt\";\n\nexport function OutOfCredits({ workspace, onClose, retry }) {\n  return (\n    <UpgradePrompt\n      used={workspace.credits.used}\n      limit={workspace.credits.limit}\n      resetsOn=\"Nov 1\"\n      onUpgrade={async (selection, signal) => {\n        const res = await fetch(\"/api/billing/upgrade\", { method: \"POST\", body: JSON.stringify(selection), signal });\n        if (!res.ok) throw new Error((await res.json()).message ?? \"Your card was declined. Try another card.\");\n      }}\n      onUpgraded={() => workspace.refresh()}\n      onContinue={() => { onClose(); retry(); }}\n      onDismiss={() => { fetch(\"/api/nudges/upgrade/snooze\", { method: \"POST\" }); onClose(); }}\n    />\n  );\n}",
      "exports": [
        "UpgradePrompt"
      ]
    },
    {
      "name": "referral-card",
      "kind": "component",
      "title": "Referral card",
      "description": "A give-and-get referral card where people copy or share their invite link, invite friends by email as chips, and watch invites move from invited to signed up to earned, with the credit total counting up and a milestone reward unlocking.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/referral-card",
      "markdown": "https://uiarc.dev/components/referral-card/markdown",
      "registry": null,
      "keywords": [
        "referral",
        "refer a friend",
        "invite",
        "give get",
        "affiliate",
        "credits",
        "share link",
        "growth",
        "milestone",
        "reward"
      ],
      "whenToUse": [
        "A referral or invite program in account settings, billing or an onboarding checklist.",
        "Showing a person what their invites have earned and how close they are to a bonus reward."
      ],
      "whenNotToUse": [
        "Team member invites with roles and permissions; use a members table.",
        "Public leaderboards or affiliate payouts with many columns; use a table."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "avatar",
        "button",
        "copy-button",
        "tooltip"
      ],
      "accessibility": [
        "Share buttons are icon buttons with aria-labels (Post on X, Share on LinkedIn, Share by email) and tooltips; a polite live region reports what happened, including Simulated, nothing was posted.",
        "The hint under the email field is a polite status tied to the input with aria-describedby; errors set aria-invalid and show an alert icon plus text, never color alone.",
        "Stages are an icon plus a word. The milestone is a progressbar with aria-valuetext such as 4 of 5 paid referrals.",
        "The funnel is a definition list, the people a labelled list.",
        "While sending, the field turns read only instead of disabled, so keyboard focus stays put."
      ],
      "motion": [
        "Numbers roll to their new value in 600ms with tabular numerals, so columns never shift.",
        "Stage labels, the milestone line, share icons and the hint crossfade once: old out in 100ms, new in over 160ms with a 2px rise, stacked in one grid cell.",
        "Milestone segments fill from the left in 240ms and turn to the success color when unlocked.",
        "New invites open at the top of the list with a bounce-free height spring (0.24s) inside a fixed-height list, so the card never grows.",
        "Reduced motion keeps every end state and drops the roll, rise and height travel; the sample program plays twice as fast."
      ],
      "responsive": [
        "The card is fluid up to 560px. Below a 470px card width the share buttons take their own full row and the milestone line keeps room for two lines.",
        "Long emails truncate with an ellipsis while the date stays visible; chips scroll sideways inside a fixed-height field."
      ],
      "performance": [
        "Rolling numbers are written by motion values, not React state, so a roll causes no re-renders.",
        "The sample program starts when the card is 40% on screen and runs once."
      ],
      "aiNotes": [
        "Derive everything from referrals: pass the list from your backend and update it when webhooks report sign-ups and paid conversions.",
        "Keep reward, currency and copy consistent with your actual program terms; the heading is built from reward.",
        "Use referralShareUrl when you need the same share links elsewhere.",
        "Only the first address of each simulated send moves through the stages, so the demo stays believable."
      ],
      "related": [
        "copy-button",
        "avatar",
        "button",
        "tooltip",
        "agent-status"
      ],
      "usage": "import { ReferralCard } from \"@/components/arc/referral-card/referral-card\";\n\nexport function Referrals({ me, referrals }) {\n  return (\n    <ReferralCard\n      link={`https://northwind.app/r/${me.code}`}\n      reward={20}\n      referrals={referrals}\n      milestone={{ goal: 5, reward: \"3 months free\", detail: \"Applied to your next invoice\" }}\n      onShare={(channel, url) => window.open(url, \"_blank\", \"noopener,width=600,height=640\")}\n      onInvite={(emails, { signal }) =>\n        fetch(\"/api/referrals/invite\", { method: \"POST\", body: JSON.stringify({ emails }), signal }).then(response => {\n          if (!response.ok) throw new Error(\"Could not send the invites. Nothing was sent.\");\n        })}\n      onMilestone={() => track(\"referral_milestone_unlocked\")}\n    />\n  );\n}",
      "exports": [
        "ReferralCard",
        "referralShareUrl"
      ]
    },
    {
      "name": "trial-banner",
      "kind": "component",
      "title": "Trial banner",
      "description": "A trial status banner where a ring counts down the days left, the copy names what the workspace loses, and the upgrade path is one click away. It comes as a slim banner, a sidebar card or a header pill. It shifts to a warning tone in the last days, has expired and upgraded states, and folds into the pill when dismissed or snoozed.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/trial-banner",
      "markdown": "https://uiarc.dev/components/trial-banner/markdown",
      "registry": null,
      "keywords": [
        "trial banner",
        "free trial",
        "trial countdown",
        "upgrade banner",
        "billing nudge",
        "trial expired",
        "SaaS trial",
        "paywall",
        "add payment method",
        "snooze banner"
      ],
      "whenToUse": [
        "A SaaS app with a free trial that needs a quiet, always visible upgrade path.",
        "Telling a workspace what it loses when the trial ends and nudging harder in the last days.",
        "Showing an expired trial without blocking the app."
      ],
      "whenNotToUse": [
        "A hard paywall that blocks a feature; use a modal or an upgrade screen instead.",
        "Generic announcements or release notes; use a plain banner or toast.",
        "Usage limits (credits, seats); use a usage meter."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The banner and card are labelled regions (\"Pro trial\"); the ring is decorative and the days are always in the text.",
        "A polite live region announces billing, snooze and dismiss results, and says when an action was simulated.",
        "Status never relies on color: the warning tone also changes the copy to name the end day, expired shows a lock, upgraded a check.",
        "After dismiss or snooze, focus moves to the trial pill; reopening it moves focus to the primary action."
      ],
      "motion": [
        "The ring arc springs to its new length with no bounce (0.24s); its color follows the tone in 160ms.",
        "Copy, days and icons swap with one crossfade (old out in 100ms, new in over 160ms with a 2px rise) in a shared grid cell, so nothing shifts.",
        "Button labels morph their width without bounce; the pill label width springs to the measured text.",
        "Reduced motion keeps every end state and drops the crossfades and springs."
      ],
      "responsive": [
        "The banner uses a container query: under 600px the buttons move under the copy, aligned with its left edge, and the sub line may take two lines (space reserved).",
        "The card fills its container up to 340px. The pill's popover keeps 12px from the viewport edge.",
        "Holds up at 360px, 390px and desktop widths without horizontal scroll."
      ],
      "performance": [
        "No timers or loops while idle; only the simulated billing call and the short simulated note use a timeout.",
        "Marks are inline SVG; no images or fonts are loaded."
      ],
      "aiNotes": [
        "Pass daysLeft and endsAt from your billing provider (for example Stripe's subscription trial_end); the component never reads the clock itself, so it renders the same on server and client.",
        "Use variant=\"banner\" above the app content, \"card\" at the bottom of a sidebar, \"pill\" in a dense header.",
        "Keep lossSummary to a few words and features to three or four rows; these are the reasons to pay, not a feature table.",
        "Persist onDismiss and onSnooze yourself, then stop rendering the component until the snooze ends."
      ],
      "related": [
        "agent-status",
        "button",
        "dropdown-menu",
        "popover",
        "segmented-control"
      ],
      "usage": "import { TrialBanner } from \"@/components/arc/trial-banner/trial-banner\";\n\nexport function AppTrialBanner({ trial }: { trial: { daysLeft: number; endsAt: string } }) {\n  return (\n    <TrialBanner\n      daysLeft={trial.daysLeft}\n      endsAt={trial.endsAt}\n      onAddPayment={async signal => {\n        const res = await fetch(\"/api/billing/checkout\", { method: \"POST\", signal });\n        if (!res.ok) throw new Error(\"Checkout failed\");\n        window.location.assign((await res.json()).url);\n      }}\n      onComparePlans={() => router.push(\"/pricing\")}\n      onSnooze={({ until }) => saveSnooze(until)}\n      onDismiss={() => saveDismissed()}\n    />\n  );\n}",
      "exports": [
        "TrialBanner"
      ]
    },
    {
      "name": "team-switcher",
      "kind": "component",
      "title": "Team switcher",
      "description": "A workspace switcher like Linear, Vercel or Slack. The trigger shows the current workspace's logo and plan. The menu has search, your role in each workspace and a Cmd+1 to 9 shortcut for each, and lets you create or join a workspace inline. A highlight glides between rows, and switching crossfades the trigger through a short loading state.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/team-switcher",
      "markdown": "https://uiarc.dev/components/team-switcher/markdown",
      "registry": null,
      "keywords": [
        "team switcher",
        "workspace switcher",
        "organization switcher",
        "org picker",
        "account switcher",
        "tenant switcher",
        "project switcher",
        "multi-tenant",
        "linear",
        "vercel",
        "slack",
        "dropdown"
      ],
      "whenToUse": [
        "Switching between organizations, teams or workspaces in a multi-tenant SaaS app.",
        "An account or project picker that needs search, roles and keyboard shortcuts.",
        "Letting people create a new workspace or accept an invite without leaving the current page."
      ],
      "whenNotToUse": [
        "Picking from a short static list of options in a form: use a select or segmented control.",
        "Global navigation across pages and commands: use the command palette block.",
        "Switching between two or three views of the same data: use tabs."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger is a button with aria-haspopup=\"dialog\", aria-expanded and aria-busy while switching; its label reads the workspace and its plan.",
        "The search field is a combobox that owns a listbox and points at the highlighted row with aria-activedescendant, so focus never leaves the field while navigating.",
        "The current workspace is aria-selected and marked with a check icon, not color alone. Errors pair an icon with text.",
        "A polite live region announces switching, the result, and when an action was simulated (\"Simulated, nothing was sent\").",
        "Closing with Escape or by choosing returns focus to the trigger. Tabbing out or clicking outside closes the menu."
      ],
      "motion": [
        "The menu fades and scales in from 98% over 180ms and leaves in 120ms.",
        "One highlight glides between rows with a bounce-free spring (visualDuration 0.24) and appears in place instead of sliding in from the top.",
        "Switching crossfades the trigger's logo, name and plan line (100ms out, 160ms in, 2px y) through a shimmer label and a spinner. Nothing resizes.",
        "The create and join views crossfade inside a fixed-height panel with a 6px horizontal shift that follows the navigation direction.",
        "Reduced motion keeps the crossfades and feedback as plain fades and drops the glide, shifts and scale."
      ],
      "responsive": [
        "The root is up to 360px wide and fills narrower containers, so it holds at 360px and 390px viewports.",
        "Names and plan lines truncate with an ellipsis; roles and shortcuts keep their width.",
        "The list shows six rows and scrolls beyond that without a visible scrollbar; the panel height never changes.",
        "Rows are 48px tall for touch, and pointer and keyboard drive the same highlight."
      ],
      "performance": [
        "One motion value pair drives the highlight; rows do not animate individually.",
        "Filtering is a plain in-memory match on name, plan and role, fine for hundreds of workspaces.",
        "Marks are inline SVG; no images load."
      ],
      "aiNotes": [
        "Use it at the top of an app sidebar or in a header for multi-tenant SaaS: organizations, teams, projects or stores.",
        "Pass real workspaces with logo and tile when the tenant has a brand mark; otherwise the tile falls back to a letter avatar.",
        "Wire onSwitch to whatever loads the tenant (route prefetch, session switch); keep the promise short, the trigger shows a loading state meanwhile.",
        "Return the created or joined workspace from onCreateWorkspace or onJoinWorkspace and also add it to the workspaces prop; the switcher dedupes by id.",
        "The menu is absolutely positioned below the trigger. Leave about 450px below it or place it where the panel can overflow."
      ],
      "related": [
        "dropdown-menu",
        "popover",
        "combobox",
        "avatar"
      ],
      "usage": "import { useState } from \"react\";\nimport { TeamSwitcher } from \"@/components/arc/team-switcher/team-switcher\";\n\nconst workspaces = [\n  { id: \"acme\", name: \"Acme\", plan: \"Enterprise\", members: 214, role: \"Admin\" },\n  { id: \"acme-labs\", name: \"Acme Labs\", plan: \"Pro\", members: 12, role: \"Owner\" },\n];\n\nexport function AppHeader() {\n  const [workspaceId, setWorkspaceId] = useState(\"acme\");\n  return (\n    <TeamSwitcher\n      workspaces={workspaces}\n      value={workspaceId}\n      onValueChange={setWorkspaceId}\n      onSwitch={(workspace, signal) => api.session.switch(workspace.id, { signal })}\n      onCreateWorkspace={async (name, signal) => api.workspaces.create({ name }, { signal })}\n      onJoinWorkspace={async (invite, signal) => api.invites.accept(invite, { signal })}\n    />\n  );\n}",
      "exports": [
        "TeamSwitcher",
        "WorkspaceTile"
      ]
    },
    {
      "name": "live-visitors",
      "kind": "component",
      "title": "Live visitors",
      "description": "A realtime visitors card where the live count rolls digit by digit, new arrivals ping in on a dotted world map, the top pages reorder with bars, and signups and upgrades tick in with country flags.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/live-visitors",
      "markdown": "https://uiarc.dev/components/live-visitors/markdown",
      "registry": null,
      "keywords": [
        "live visitors",
        "realtime analytics",
        "active users",
        "visitors now",
        "world map",
        "dotted map",
        "top pages",
        "signups",
        "upgrades",
        "conversion ticker",
        "rolling number",
        "odometer",
        "country flags",
        "live dashboard"
      ],
      "whenToUse": [
        "Realtime visitors on a marketing site or app, next to signups and upgrades as they happen.",
        "Launch days and campaigns where a team watches traffic and conversions live.",
        "Admin and growth dashboards that need one glanceable tile for right now."
      ],
      "whenNotToUse": [
        "Historical trends over days or weeks; use a chart with axes.",
        "Precise geography or per-country breakdowns; use a table or a proper map.",
        "A single number with no context; use a stat card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section; the count is read once as plain text (\"128 people on the site now\"), and the rolling digits are hidden from screen readers.",
        "The pause button is a toggle with aria-pressed and a label that names the action; its tooltip says the same.",
        "Status is never color alone: the dot always sits next to Live, Paused, Connecting or Reconnecting.",
        "Each event row reads as one sentence with the country spelled out and the time in words (\"Signed up, Berlin, Germany, 12 seconds ago\"). Flags are decorative.",
        "The map is decorative and hidden; the same information is in the lists.",
        "Count changes are not announced, so screen readers are not flooded. A polite live region reports pause and resume, and says when the stream is simulated."
      ],
      "motion": [
        "Digits roll on their own strips (280ms ease-out) with tabular numerals, so the number never changes width; a new leading digit (99 to 100) grows in.",
        "New visitors fade in on the map with one ring that expands and fades (900ms); events get a stronger ring and a small city label for 2s. Expired dots fade out over 400ms.",
        "Page rows and event rows move as one step: rows slide by one row height (280ms), the new row fades in (160ms) and the last one fades out below the clip. Bars ease to their new width.",
        "The live dot pulses every 1.6s only while live and on screen. The state label crossfades (out 100ms, in 160ms) in a reserved width.",
        "Reduced motion keeps every update but drops the rolls, rings, slides and pulse."
      ],
      "responsive": [
        "Holds 640px at most and fills narrower containers through a container query.",
        "Below 520px card width the two lists stack and padding drops to 16px; the map scales with the card.",
        "Paths, cities and labels truncate with an ellipsis; counts and times keep fixed right-aligned columns."
      ],
      "performance": [
        "The land is one SVG path of round caps computed once; pings are a handful of circles.",
        "Rows, bars and digits move with CSS transforms and widths on absolutely positioned elements, so nothing reflows the card.",
        "The pulse, clock and simulation stop while the card is off screen (IntersectionObserver) or paused."
      ],
      "aiNotes": [
        "Pick this for a realtime analytics view, an admin home or a launch day dashboard tile that answers who is here right now.",
        "Without count it is a self-playing demo. As soon as you pass count, pass pages, visitors and events too.",
        "Send arrivals as a rolling window (ids that stay for 5 to 8 seconds); the component handles enter and exit animations from id changes.",
        "Keep event labels short and past tense (Signed up, Upgraded to Pro) and never show personal data such as emails in the ticker.",
        "Use connection to reflect the socket state instead of hiding the card."
      ],
      "related": [
        "account-health",
        "generation-stats",
        "goal-tracker",
        "agent-status",
        "credit-topup"
      ],
      "usage": "import { LiveVisitors } from \"@/components/arc/live-visitors/live-visitors\";\n\nexport function RealtimeCard({ count, pages, arrivals, conversions, status }) {\n  // Feed it from your realtime source (a websocket, SSE or a 2s poll of your analytics API).\n  return (\n    <LiveVisitors\n      count={count}\n      pages={pages}\n      visitors={arrivals}\n      events={conversions}\n      connection={status}\n      onPausedChange={paused => (paused ? socket.pause() : socket.resume())}\n    />\n  );\n}",
      "exports": [
        "LiveVisitors",
        "useLiveVisitorsSimulation"
      ]
    },
    {
      "name": "social-proof-toast",
      "kind": "component",
      "title": "Social proof toast",
      "description": "A stack of social proof notifications for landing and pricing pages, where recent trials, upgrades and verified purchases from people and companies arrive every few seconds. The stack pauses and fans out on hover, and visitors can dismiss any toast.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/social-proof-toast",
      "markdown": "https://uiarc.dev/components/social-proof-toast/markdown",
      "registry": null,
      "keywords": [
        "social proof",
        "fomo notification",
        "recent purchase popup",
        "sales notification",
        "live activity toast",
        "trial signup toast",
        "conversion",
        "landing page",
        "verified badge",
        "toast stack"
      ],
      "whenToUse": [
        "Showing recent trials, purchases or upgrades on a landing or pricing page.",
        "A live activity feed for a launch or waitlist page (people joining, teams upgrading).",
        "Highlighting verified purchases from known companies near a checkout CTA."
      ],
      "whenNotToUse": [
        "System feedback after a user action (saved, copied, failed): use toast or undo-toast.",
        "Inside a signed in product where activity is not marketing: use an activity feed list.",
        "When you do not have real events to show; fabricated social proof erodes trust."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stack is a labelled region (Recent activity). New toasts are not announced, so screen reader users are not interrupted every few seconds; dismissals are confirmed in a polite status.",
        "Each dismiss button names the toast it closes, such as Dismiss: Sofia started a Pro trial.",
        "Toasts behind the stack limit are aria-hidden and inert. Hover and keyboard focus both pause the cycle, so nothing moves while someone reads or acts.",
        "The verified state is an icon plus a text label, never color alone. Portraits use empty alt text because the name is in the sentence."
      ],
      "motion": [
        "A new toast rises 16px and fades in at the front while the others step back 10px and scale down 5% each. Every move uses a bounce free spring (visual duration 0.24s); content of covered toasts fades out in 160ms so text never overlaps.",
        "Hover or focus fans the stack out with the same spring; dismissing fades and slightly shrinks the toast in 160ms and the rest close the gap.",
        "Toasts have a fixed 64px height, so nothing resizes between states.",
        "Reduced motion keeps the stack and the cycle but drops travel: toasts only fade.",
        "The cycle pauses off screen (IntersectionObserver) and in background tabs."
      ],
      "responsive": [
        "360px wide in a corner, 24px from the edges. Below 480px the stack spans the width with 16px gutters.",
        "Text truncates with an ellipsis on one line per row, so toast height never changes.",
        "On touch screens the dismiss button is always visible; on pointer devices it appears on hover or keyboard focus."
      ],
      "performance": [
        "At most maxVisible + 1 toasts are mounted. Animations are transform and opacity only.",
        "One timer drives the cycle and stops while paused, off screen, or in a hidden tab."
      ],
      "aiNotes": [
        "Use for social proof on marketing, pricing and checkout pages. Feed it real, recent events from your billing or analytics backend; do not invent activity.",
        "Keep it calm: interval 4000 to 8000ms, limit around 5 to 8 per session, and pause it while modals or cookie banners are open via paused.",
        "For one-off app notifications use toast or toast-stack instead. Marks should be mono and use currentColor so they follow the theme.",
        "position=\"inline\" is for docs and previews; on a real page use a corner position."
      ],
      "related": [
        "toast",
        "toast-stack",
        "undo-toast",
        "avatar",
        "avatar-group"
      ],
      "usage": "import { SocialProofToast } from \"@/components/arc/social-proof-toast/social-proof-toast\";\nimport type { SocialProofEvent } from \"@/components/arc/social-proof-toast/social-proof-toast\";\nimport { LinearMark } from \"@/components/arc/social-proof-toast/social-proof-toast-marks\";\n\nconst events: SocialProofEvent[] = [\n  { id: \"1\", name: \"Sofia\", action: \"started a Pro trial\", meta: \"Madrid\", time: new Date(Date.now() - 2 * 60_000), avatar: \"/people/sofia.jpg\" },\n  { id: \"2\", name: \"Linear\", action: \"upgraded to Business\", meta: \"48 seats\", time: Date.now() - 4 * 60_000, logo: <LinearMark />, verified: \"Verified by Stripe\" },\n];\n\nexport function Landing() {\n  return <SocialProofToast events={events} position=\"bottom-left\" limit={6} onShow={event => console.log(\"shown\", event.id)} />;\n}",
      "exports": [
        "SocialProofToast",
        "SocialProofEvent"
      ]
    },
    {
      "name": "install-snippet",
      "kind": "component",
      "title": "Install snippet",
      "description": "A \"get started in 30 seconds\" card where framework tabs, three numbered steps and highlighted snippets take a developer from install to first event. Generate key wipes a fresh API key into the placeholder in place.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "saas",
        "new"
      ],
      "docs": "https://uiarc.dev/components/install-snippet",
      "markdown": "https://uiarc.dev/components/install-snippet/markdown",
      "registry": null,
      "keywords": [
        "install snippet",
        "quickstart",
        "getting started",
        "sdk install",
        "npm install",
        "api key",
        "code tabs",
        "framework tabs",
        "developer onboarding",
        "copy code"
      ],
      "whenToUse": [
        "A quickstart that has to work for several frameworks without three pages of docs.",
        "Onboarding where creating an API key is part of getting started.",
        "Marketing pages for developer tools that want to show how little setup it takes."
      ],
      "whenNotToUse": [
        "A single code sample with no steps: use code-block.",
        "Trying requests against a live API: use api-playground.",
        "Long setup guides with many branches: write docs pages and link to them."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Framework and package manager tabs are real tablists with roving tabindex; the steps list is the framework tab panel.",
        "Steps are an ordered list with h4 titles; a finished step announces \", done\" and swaps its number for a check, so progress is never color alone.",
        "A polite live region announces framework changes, key generation, and that the default key is simulated. Copy buttons announce Copied on their own.",
        "Errors show an alert icon plus text, and the button changes to Try again.",
        "Marks are decorative (aria-hidden); every tab has a text label, and package manager tabs keep an aria-label when the narrow layout hides their names."
      ],
      "motion": [
        "Tab highlights slide to the measured tab on a bounce-free spring (visualDuration .24).",
        "Snippets, file names and step descriptions crossfade: the old one fades out in 100ms, the new one fades in over 160ms with a 2px rise. Old and new share one grid cell and every code block reserves the height of its longest snippet, so nothing moves.",
        "A new key eases the slot from the placeholder width to the key width (exact, in ch) while the key wipes in from the left over 340ms, then its tint fades out.",
        "Reduced motion keeps every end state and drops the wipe, rise and slide."
      ],
      "responsive": [
        "Fills its container up to 640px. Below 460px the padding tightens and package manager tabs show marks only; below 380px framework tabs drop their marks and keep their names, and the version label hides.",
        "Code scrolls horizontally inside its block, never the page."
      ],
      "performance": [
        "Highlighting is a small regex tokenizer, memoized per snippet. No highlighter dependency.",
        "Only transforms, opacity, clip-path and one measured width animate. No loops, nothing runs when idle."
      ],
      "aiNotes": [
        "Use for SDK, API or CLI onboarding: the first screen after sign-up, a docs quickstart, or an empty dashboard waiting for its first event.",
        "Wire generateKey to your key endpoint and pass apiKey for readers who already have one. Keep snippets short (under ten lines) so the card stays scannable.",
        "Replace the sample frameworks with your own; any id other than next, react, vue or html shows a text-only tab.",
        "Do not use it to display existing secret keys in full; mask them in an API keys page instead."
      ],
      "related": [
        "code-block",
        "copy-button",
        "connector-setup",
        "onboarding-checklist",
        "tabs"
      ],
      "usage": "import { InstallSnippet } from \"@/components/arc/install-snippet/install-snippet\";\n\nexport function Onboarding() {\n  return (\n    <InstallSnippet\n      generateKey={async signal => {\n        const res = await fetch(\"/api/keys\", { method: \"POST\", signal });\n        if (!res.ok) throw new Error(\"Could not create a key\");\n        return (await res.json()).key;\n      }}\n      onCopy={(step) => track(\"onboarding_copy\", { step })}\n    />\n  );\n}",
      "exports": [
        "InstallSnippet"
      ]
    },
    {
      "name": "branch-graph",
      "kind": "component",
      "title": "Branch graph",
      "description": "A git commit graph where branches draw in as colored lanes, merge commits join lines, a rebase lifts commits and replays them onto main, a squash folds them into one, and hovering a commit shows its message, author and diff stats.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "git",
        "graph",
        "developer",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/branch-graph",
      "markdown": "https://uiarc.dev/components/branch-graph/markdown",
      "registry": null,
      "keywords": [
        "git",
        "commit graph",
        "branch",
        "merge",
        "rebase",
        "squash",
        "pull request",
        "history",
        "version control",
        "developer tools",
        "diff stats"
      ],
      "whenToUse": [
        "A merge strategy picker in a code review or pull request screen, showing exactly what main will look like afterwards.",
        "A commit history panel in a developer tool, with hover details and keyboard browsing.",
        "Docs and landing pages for version control or CI products that explain merge, rebase, and squash."
      ],
      "whenNotToUse": [
        "Use git-branches for a looping, non-interactive illustration of a branch, pull request, and release.",
        "Use a timeline or changelog for releases people need to read rather than a commit graph.",
        "Use a virtualized table for long real histories; this graph is built for a focused slice of about five to fifteen commits."
      ],
      "dependencies": [
        "lucide-react"
      ],
      "accessibility": [
        "The root is a labelled section. The strategy control is a radiogroup of real radio buttons with a roving tab stop; arrow keys, Home, and End pick a strategy.",
        "Commits form a listbox in visual order, newest first. Up and Down, Home, and End move through commits with aria-activedescendant; Escape closes the commit card. Each option is named with its message, hash, author, date, diff stats, and the branches that point to it.",
        "A polite live region announces the result of every strategy picked by the user, such as how many commits were replayed and that they got new hashes. Autoplay changes are not announced.",
        "The graph and the commit card are aria-hidden because the listbox carries the same information. Branch color is always paired with a named tag, merge commits carry an icon and a hollow dot, and additions and deletions carry + and − signs, so nothing relies on color alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The keyboard commit takes the hover fill and shows its card."
      ],
      "motion": [
        "One rAF loop writes SVG path data, transforms, and opacity straight to the DOM and skips every value that did not change. React renders only when the strategy or the inspected commit changes.",
        "Edges are computed each frame from the live positions of their two commits, so lines stretch and bend with them. A branch bends near its parent and a merge bends near its child, like a real git client.",
        "The entrance draws each line up from the oldest commit at a constant pace, and every commit lands as its line reaches it.",
        "A rebase lifts the branch's rows into floating cards, carries them one after another along an eased arc into the base lane, and sets them down with a soft ring while their hashes rewrite in place. The old branch point retracts first and the new link to the base tip draws in after the commits land.",
        "A merge makes room at the top, then the merge commit lands and both lines draw into it. A squash flies the commits into one point where the squashed commit appears; undoing it replays them back out. Every transition starts from wherever things are, so picking a new strategy mid-flight never jumps.",
        "The loop runs only while something moves or autoplay is waiting, and stops off screen and in hidden tabs. With reduced motion the graph shows each state immediately with no entrance, flights, or autoplay."
      ],
      "responsive": [
        "The graph is drawn in real pixels in an SVG beside the rows, so hairlines and dots stay crisp at any width.",
        "Container queries drop the date below 720px, the author below 560px, and the hash below 400px; the full details stay in the hover card and each option's accessible name. The strategy control takes a full row on phones.",
        "On touch, a tap on a commit pins its card and a tap elsewhere closes it."
      ],
      "performance": [
        "About sixty SVG and HTML elements are updated from one loop with a write cache, so a quiet frame touches nothing.",
        "Every layout is computed once per data set. Nothing is measured inside the loop."
      ],
      "aiNotes": [
        "Use it where people choose how a pull request lands: a merge button, a code review tool, or docs that teach merge, rebase, and squash.",
        "Pass commits newest first with real parent ids. Keep the pull request branch to two to five commits and use two or three lanes; more lanes read as noise.",
        "Keep branch names short, like feat/search. The hover card holds the longer story in body.",
        "The merge, rebase, and squash states are previews computed from the data. Say so if the page could be read as the live state of a repository."
      ],
      "related": [
        "timeline"
      ],
      "usage": "import { BranchGraph } from \"@/components/arc/branch-graph/branch-graph\";\n\nconst ada = { name: \"Ada Park\", avatar: \"/people/ada.jpg\" };\n\nconst commits = [\n  { id: \"c3\", hash: \"b81f2e0\", message: \"Rank results by recency\", branch: \"feat/search\", parents: [\"c2\"], author: ada, date: \"1 hour ago\", additions: 64, deletions: 9, files: 3 },\n  { id: \"m2\", hash: \"4a0c9d1\", message: \"Upgrade to Node 22\", branch: \"main\", parents: [\"m1\"], author: ada, date: \"3 hours ago\", additions: 8, deletions: 8, files: 2 },\n  { id: \"c2\", hash: \"e27d4b3\", message: \"Index docs on publish\", branch: \"feat/search\", parents: [\"m1\"], author: ada, date: \"Yesterday\", additions: 210, deletions: 12, files: 7 },\n  { id: \"m1\", hash: \"93c5a07\", message: \"Launch docs site\", branch: \"main\", parents: [], author: ada, date: \"2 days ago\", additions: 540, deletions: 0, files: 31 },\n];\n\nexport function MergePreview() {\n  return (\n    <BranchGraph\n      commits={commits}\n      branches={[{ name: \"main\" }, { name: \"feat/search\" }]}\n      pullRequest={{ number: 1207, title: \"Instant search\", branch: \"feat/search\" }}\n      autoplay={false}\n    />\n  );\n}",
      "exports": [
        "BranchGraph"
      ]
    },
    {
      "name": "link-unfurl",
      "kind": "component",
      "title": "Link unfurl",
      "description": "A composer where pasting a URL shows a loading shimmer on the link, then unfurls it into a rich preview card with title, image and favicon in one morph, which can be collapsed back to the inline link or removed.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "composer",
        "link-preview",
        "morph",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/link-unfurl",
      "markdown": "https://uiarc.dev/components/link-unfurl/markdown",
      "registry": null,
      "keywords": [
        "link preview",
        "unfurl",
        "open graph",
        "og image",
        "rich link",
        "embed",
        "url preview",
        "composer",
        "chat input",
        "slack",
        "paste link",
        "morph"
      ],
      "whenToUse": [
        "A message or comment composer where pasted links should show what they point to before sending.",
        "Product marketing that shows how a chat or notes app handles links."
      ],
      "whenNotToUse": [
        "Use hover-card to preview a link someone reads, not writes.",
        "Use mention-input when the tokens are people or records rather than web links, and rich-text-editor for long formatted documents."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The message is a contenteditable element with role=\"textbox\", aria-multiline, a label, and a description of its keys. Pasting inserts plain text, and a pasted or typed address (followed by a space) becomes an atomic link that deletes as one unit.",
        "Keyboard: Enter sends, Shift+Enter adds a line, and Alt+Enter shows or collapses the preview of the link before the caret. Every card has a named collapse button and a named remove button, and its title is a real link.",
        "A folded card is inert and hidden from assistive technology, so only open previews are in the tab order.",
        "A polite live region announces a link being added and loading, the preview opening with its site and title, collapsing, removal, a failed preview, and the sent message. Send errors use role=\"alert\".",
        "Loading is shown by the sweep and by muted text and a globe instead of a favicon, and announced, never by color alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focused controls take a muted fill."
      ],
      "motion": [
        "One rAF loop drives every card from a critically damped spring, writing only transforms, clip paths, opacity, and the plate size; React renders once per state change, never per frame.",
        "The card is clipped to the exact rectangle of its inline link, with the favicons aligned, and translated onto it. As the spring runs the clip widens first and deepens after, while the translation eases home, so the link reads as unrolling into the card. A plate under the card carries its border and a shadow that lifts only mid flight.",
        "The lane grows with the card's height on the same progress, so the toolbar below glides instead of jumping. The body fades in after the header, and the image settles from a slight zoom.",
        "When a preview resolves, the link's width morphs to fit the title while the title rolls up and the favicon pops in.",
        "Collapsing runs the same path backwards on a quicker spring. Dragging a card up folds it with the pointer; releasing past about 40% or with an upward flick folds it, otherwise it springs back.",
        "The loop sleeps once cards settle, and cards land instantly when the composer is offscreen or the tab is hidden; the loading sweep pauses with them. Reduced motion and paused open and fold cards instantly and show a still tint instead of the sweep."
      ],
      "responsive": [
        "The composer sizes itself from its own width with container queries: padding steps up from 560px, and the Try label hides below 360px so the sample buttons and send button stay on one row.",
        "Cards are at most 440px wide and fill narrower composers; images keep the 1.91 : 1 Open Graph ratio. Links truncate at 16em with the full title in the card and the accessible name.",
        "On touch only the card header drags, so the page still scrolls over the card body and image."
      ],
      "performance": [
        "Each frame reads all geometry first and then writes, so the loop costs one layout pass. Nothing is created or removed while animating.",
        "Preview images are decoded before the card opens, so the morph never reveals an empty frame."
      ],
      "aiNotes": [
        "Use it in chat, comments, and notes where links are shared and a preview helps people decide whether to open them.",
        "Unfurl on your server (Open Graph and oEmbed) and return site, title, description, image, and favicon through resolve. Honor the abort signal; it fires when the link is removed or the message is sent.",
        "The favicon slot in the link and the card are aligned on purpose; keep favicons square.",
        "Brand marks in the samples are sample content, not endorsements."
      ],
      "related": [
        "mention-input",
        "rich-text-editor",
        "hover-card",
        "chat-thread"
      ],
      "usage": "import { LinkUnfurl, type LinkUnfurlPreview } from \"@/components/arc/link-unfurl/link-unfurl\";\n\nexport function ChannelComposer({ channel, post }: { channel: string; post: (text: string) => Promise<void> }) {\n  return <LinkUnfurl\n    label={`Message #${channel}`}\n    placeholder={`Message #${channel}`}\n    defaultValue=\"\"\n    samples={[]}\n    autoPlay={false}\n    resolve={async (url, signal) => {\n      const res = await fetch(`/api/unfurl?url=${encodeURIComponent(url)}`, { signal });\n      return res.ok ? (await res.json()) as LinkUnfurlPreview : null;\n    }}\n    onSend={message => post(message.text)}\n  />;\n}",
      "exports": [
        "LinkUnfurl"
      ]
    },
    {
      "name": "charge-session",
      "kind": "component",
      "title": "Charge session",
      "description": "An EV or device charging card. Energy packets flow from a charger along the cable into the battery pack, which fills toward a draggable charge limit. The charge curve tapers near full, and power, energy, time to limit and cost update live. Stop charging drains the flow.",
      "tier": "pro",
      "category": "Special",
      "tags": [
        "special",
        "motion",
        "progress",
        "energy",
        "new"
      ],
      "docs": "https://uiarc.dev/components/charge-session",
      "markdown": "https://uiarc.dev/components/charge-session/markdown",
      "registry": null,
      "keywords": [
        "ev charging",
        "charging",
        "battery",
        "charge session",
        "electric vehicle",
        "charge limit",
        "charge curve",
        "time to full",
        "kwh",
        "cost",
        "energy",
        "fleet",
        "charger",
        "power"
      ],
      "whenToUse": [
        "An EV or fleet app that shows a live charging session with a charge limit.",
        "A charge point operator console where staff watch power, energy and cost for one bay.",
        "A product page that demonstrates how your charging network reports time to full and cost."
      ],
      "whenNotToUse": [
        "Use usage-meter or progress for a static quota or a simple percentage.",
        "Use gauge for a single live reading without a session, curve or cost.",
        "Use a table or a chart block to compare many sessions at once."
      ],
      "dependencies": [
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled group with a real heading. Power, energy added, time to limit and cost are a description list, so each value is read with its name.",
        "The limit marker is a focusable role=\"slider\" named Charge limit, with aria-valuetext such as Charge limit 80%, about 23 minutes to go. Arrows move it by 1%, Shift with an arrow or Page Up and Page Down by 10%, Home and End jump to the minimum and to 100%. The drag hint is linked with aria-describedby.",
        "Stopping, resuming, reaching the limit, starting a new session and a settled limit change are announced in a polite live region with the charge, energy and cost at that moment.",
        "The action button keeps one accessible label at a time (Stop charging, Resume charging, Start new session) and its width never changes.",
        "The scene and the curve plot are hidden from assistive technology; every value they show is also in the stats, the header or the slider.",
        "State never relies on color: the header pairs an icon with Charging, Stopped or Charged to limit, and time to limit reads Paused or Done."
      ],
      "motion": [
        "One requestAnimationFrame loop advances the session and writes SVG attributes, CSS variables and text directly; React renders only when the session state changes.",
        "Packets move along an arc length table of the cable curve, so they travel at an even pace, ease out of the charger, glide through the nub and slow as they meet the fill front, where each landing briefly brightens the front.",
        "Power follows the charge curve times an availability level that ramps up on start and drains exponentially on stop, so the numbers, the LED, the cable core, the packet pace and the live point on the curve all fall away together.",
        "Dragging the limit is one to one with the pointer; the pill lifts slightly while held. Raising the limit after the pack is full resumes charging, lowering it below the charge completes the session.",
        "The loop stops offscreen, in background tabs, when paused, and once the flow has fully drained.",
        "With reduced motion the session holds still: packets sit at even spacing along the path to show direction, values are current, and stop, resume and the limit all update at once."
      ],
      "responsive": [
        "The scene is one SVG with a fixed aspect ratio that scales to any width, so the charger, cable and pack keep their proportions; the limit pill is HTML placed in container units, so its text and 44px touch target keep their real size.",
        "The curve stretches to its container with non scaling strokes; its labels and live point are HTML so they stay crisp and round.",
        "Above 640px the curve sits beside a two by two stats grid and the percentage grows to the display size. Below 420px padding tightens, the stats shrink one step and the action button spans the card.",
        "Dragging uses touch-action pan-y on the scene and the curve, so vertical page scroll still works on phones."
      ],
      "performance": [
        "A frame evaluates the power curve, a 48 step time to limit integral and 18 packets, well under a millisecond.",
        "Every write goes through a cache, so unchanged attributes and text are never touched; pointer math reads the target rectangle once per press.",
        "No canvas, no layout reads in the loop, and the loop idles completely once the flow has drained."
      ],
      "aiNotes": [
        "Use it for EV charging apps, fleet and depot dashboards, charge point operator consoles, or any device charging flow where time to full and cost matter.",
        "The session is simulated from capacity, maxPower, taperAt and rate. For a real session, map your live state of charge onto startCharge and keep speed low, or say clearly that the view is an estimate.",
        "Keep the title to the charger or vehicle name and put the site and peak power in location.",
        "Stopping in the demo is local; wire onStateChange to your backend before calling it a real stop."
      ],
      "related": [
        "usage-meter",
        "progress",
        "gauge",
        "slider"
      ],
      "usage": "import { ChargeSession } from \"@/components/arc/charge-session/charge-session\";\n\nexport function DepotCharger() {\n  return (\n    <ChargeSession\n      title=\"Bay 12, depot charger\"\n      location=\"North yard, 350 kW\"\n      capacity={98}\n      maxPower={350}\n      startCharge={22}\n      defaultLimit={85}\n      rate={0.38}\n      currency=\"EUR\"\n      locale=\"de-DE\"\n      onLimitChange={limit => api.setChargeLimit(\"bay-12\", limit)}\n      onStateChange={(state, detail) => state === \"stopped\" && api.stopSession(\"bay-12\", detail)}\n    />\n  );\n}",
      "exports": [
        "ChargeSession"
      ]
    },
    {
      "name": "mega-menu-morph",
      "kind": "component",
      "title": "Mega menu morph",
      "description": "A site header whose menus share one floating panel. Moving between sections morphs its size, slides the next menu in from that side and glides the caret under the new trigger, with hover intent, a safe triangle and an accordion sheet on narrow screens.",
      "tier": "pro",
      "category": "Disclosure",
      "tags": [
        "navigation",
        "menu",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/mega-menu-morph",
      "markdown": "https://uiarc.dev/components/mega-menu-morph/markdown",
      "registry": null,
      "keywords": [
        "mega menu",
        "site header",
        "navigation bar",
        "navbar",
        "dropdown",
        "morphing dropdown",
        "shared layout",
        "stripe menu",
        "hover intent",
        "safe triangle",
        "menu aim",
        "caret",
        "accordion menu",
        "mobile menu",
        "marketing header"
      ],
      "whenToUse": [
        "A marketing site header with product, solution, developer and company sections that each need a rich menu.",
        "A documentation or platform site whose top sections deserve descriptions, a featured story or a status line.",
        "Anywhere visitors browse several menus in a row and repeated open and close animations would feel busy."
      ],
      "whenNotToUse": [
        "Use morph-nav for a compact floating pill navigation with search built in.",
        "Use dropdown-menu for a single list of actions or links behind one button.",
        "Use tabs or a docs-sidebar style navigation for moving between views inside a product."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The header is a nav landmark named by label. Triggers are buttons with aria-expanded and aria-controls pointing at the open menu; plain sections are links, with aria-current=\"page\" when they hold the current page.",
        "Menus follow the disclosure navigation pattern rather than role=\"menu\": links stay links, Tab order runs from a trigger into its menu, and focus leaving the header closes it.",
        "The current section is marked with aria-current or its label, never only by the accent mark.",
        "The folded header uses a menu button with aria-expanded and aria-controls, named Open menu or Close menu. Sections in the sheet are accordion buttons with aria-expanded controlling labelled regions.",
        "The measuring copy of the row, the highlight, the caret, the band and the safe area overlay are hidden from assistive technology; the measuring copy is also inert.",
        "Outside presses and Escape close the menu and the sheet from anywhere on the page. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focused links show the same quiet fill as hover."
      ],
      "motion": [
        "One surface serves every menu. Its position, width and height are springs restated as stiffness and damping, so retargeting mid flight keeps the velocity it has: growing carries a trace of overshoot, shrinking never overshoots, and the caret glides with the position to stay under the trigger.",
        "Menus hang from the top left of the surface at their natural size. The next menu slides in 36px from the side of the trigger you moved to and the previous one leaves the other way, both with a short opacity and 2px blur crossfade.",
        "A footer is drawn on a muted band anchored to the bottom edge. The band's height springs with the panel, so a status row grows out of it instead of fading in, and a menu without a footer folds it away.",
        "Opening from closed lands the surface in place and fades it in with a short drop and a scale from the caret; closing fades it up and away. Reopening while it closes picks up where it is.",
        "The trigger highlight glides between triggers on a spring with a little life; it appears in place when the pointer enters the row and fades where it is when the pointer leaves. Chevrons turn on the shared spring curve.",
        "The folded sheet grows its height on the smooth spring and staggers its rows in; accordion sections open with height and opacity together.",
        "With reduced motion every size and position change lands at once, menus crossfade in 140ms without travel or blur, the highlight jumps and the sheet fades."
      ],
      "responsive": [
        "The root is an inline size container. The full row is measured from an invisible copy at its natural width, and the header folds exactly when it stops fitting, into a bar with the brand, the primary action and a menu button.",
        "The folded sheet spans the header, never runs past the viewport (or --mm-sheet-max), scrolls inside itself and opens one accordion section at a time.",
        "Menus clamp to the header with an 8px inset; their groups wrap onto new rows when the header is narrower than a menu's natural width, and the caret stays over the trigger.",
        "Hover behavior only runs for mouse and pen. On touch, triggers toggle on tap and a tap outside closes the menu."
      ],
      "performance": [
        "Only the open menu and, during a switch, the leaving menu are mounted. Menus are measured with a ResizeObserver, and the surface animates width, height and a transform on one absolutely positioned element, so no page layout is touched.",
        "The highlight and caret are transforms driven by motion values; React renders only when the open section, the hovered trigger or the fold changes.",
        "Pointer positions are kept in a short ring of samples, and the safe triangle is a single point in triangle test on pointer entry to another trigger."
      ],
      "aiNotes": [
        "Use it as the top navigation of a marketing or product site with three to six sections, where each section holds several destinations worth describing. For a single dropdown use dropdown-menu; for app navigation use a sidebar.",
        "Keep menus different in shape on purpose: a wide menu with two groups and a feature next to a narrow single list is what makes the morph read. Keep descriptions to one line of five or six words.",
        "Put only live, meaningful information in footer, such as system status or an announcement; the band's growth draws attention to it.",
        "Pass onNavigate in a client router so links do not reload the page, and pass current so the accent mark follows the route.",
        "Give the header room: it folds automatically when the row stops fitting. Use collapseBelow with a number only when the fold must match other breakpoints on the page.",
        "Place it at the top of a relatively positioned page area; menus and the sheet float over the content below and never push it down."
      ],
      "related": [
        "morph-nav",
        "dropdown-menu",
        "site-header",
        "tabs"
      ],
      "usage": "import { MegaMenuMorph } from \"@/components/arc/mega-menu-morph/mega-menu-morph\";\nimport type { MegaMenuItem } from \"@/components/arc/mega-menu-morph/mega-menu-morph\";\nimport { BookOpen, CreditCard, Repeat, Zap } from \"lucide-react\";\n\nconst items: MegaMenuItem[] = [\n  {\n    value: \"products\",\n    label: \"Products\",\n    groups: [{ title: \"Payments\", links: [\n      { label: \"Checkout\", description: \"Hosted pages in 46 currencies\", icon: <CreditCard />, href: \"/checkout\" },\n      { label: \"Billing\", description: \"Subscriptions, usage and invoices\", icon: <Repeat />, href: \"/billing\" },\n    ] }],\n    feature: { title: \"Instant payouts in the EU\", description: \"Sellers now get paid in seconds.\", cta: \"Read the announcement\", href: \"/blog/instant-payouts\" },\n  },\n  {\n    value: \"developers\",\n    label: \"Developers\",\n    groups: [{ title: \"Documentation\", links: [\n      { label: \"Quickstart\", icon: <Zap />, href: \"/docs/quickstart\" },\n      { label: \"API reference\", icon: <BookOpen />, href: \"/docs/api\" },\n    ] }],\n    footer: <span>All systems normal</span>,\n  },\n  { value: \"pricing\", label: \"Pricing\", href: \"/pricing\" },\n];\n\nexport function SiteHeader() {\n  const router = useRouter();\n  const pathname = usePathname();\n  return (\n    <MegaMenuMorph\n      items={items}\n      brand={{ name: \"Tally\", mark: <TallyMark />, href: \"/\" }}\n      actions={[{ label: \"Sign in\", href: \"/login\" }, { label: \"Start now\", href: \"/signup\", variant: \"primary\" }]}\n      current={pathname.startsWith(\"/docs\") ? \"developers\" : undefined}\n      onNavigate={destination => destination.href && router.push(destination.href)}\n    />\n  );\n}",
      "exports": [
        "MegaMenuMorph"
      ]
    },
    {
      "name": "toc-progress",
      "kind": "component",
      "title": "Table of contents progress",
      "description": "A table of contents that reads along with you: a line bends through the nested headings, fills as you scroll, and carries a marker to the current section, with smooth jumps and a time-left countdown.",
      "tier": "pro",
      "category": "Disclosure",
      "tags": [
        "navigation",
        "scroll",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/toc-progress",
      "markdown": "https://uiarc.dev/components/toc-progress/markdown",
      "registry": null,
      "keywords": [
        "table of contents",
        "toc",
        "on this page",
        "scroll spy",
        "reading progress",
        "scrollspy",
        "article navigation",
        "docs navigation",
        "anchor links",
        "section indicator",
        "progress line",
        "time left",
        "reading time"
      ],
      "whenToUse": [
        "A documentation page, guide or blog post long enough that readers jump between sections.",
        "An engineering write up or policy where knowing how much is left helps readers commit to it.",
        "Any long article in its own scroll container, such as a help panel or a reader view."
      ],
      "whenNotToUse": [
        "Use tabs when sections are alternatives rather than parts of one document.",
        "Use scroll-progress-ring for a compact overall progress indicator without navigation.",
        "Skip it for pages with fewer than four headings; a plain list of anchors is enough."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The list is a nav landmark named by the title, with an ordered list of links whose href points at each heading, so it works as plain anchors before hydration.",
        "The current heading carries aria-current=\"location\"; read, current and upcoming headings also differ in text contrast, never by the line alone.",
        "Choosing a heading gives the target heading tabindex=\"-1\" if it has none and focuses it with preventScroll, so screen reader and keyboard users land where the article scrolled.",
        "The line, the fill and the marker are one SVG hidden from assistive technology. The time left is plain text beside the title and is not announced on every change.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. A focused heading shows a quiet fill."
      ],
      "motion": [
        "The reading position is a section index plus how far through that section you are, from where its heading crosses the offset line to where the next one does. It follows the scroll on a critically damped spring, so a wheel step glides instead of jumping and the marker never points past the section you are in.",
        "Each section fills the stretch of line from its heading to the next, bends included, so the fill always meets a heading exactly when that heading is reached. The marker rides the tip of the fill along the same points the line is drawn from.",
        "Near the end of the article the reading line eases from the offset down to the bottom edge of the viewport, so short last sections are still reached when the scroll bottoms out.",
        "A chosen heading scrolls smoothly and the line traces there through every section in between. If the heading cannot reach the top because the article ends, it stays current until the reader scrolls on.",
        "Heading colors change over 240ms. The time left rolls up to its new value with a short blur, on the snappy spring, and ends on Finished.",
        "With reduced motion the fill and marker follow the scroll directly, chosen headings jump into place, colors change at once and the time left crossfades."
      ],
      "responsive": [
        "The list takes the width of its column and labels wrap; the line follows each row's measured height, so wrapped labels keep the bends between rows.",
        "Rows, the line and the heading positions are re-measured with ResizeObserver whenever the list, the article or the scroll container resizes, including font loading and images.",
        "Works with the page scroll or any scroll container, including one inside a preview frame or a dialog.",
        "Touch targets are the full row width; on narrow screens place the component above the article."
      ],
      "performance": [
        "Scroll handling is passive and does arithmetic on cached heading positions; layout is read only when something resizes.",
        "The line is a single polyline path and its fill is the same path with a stroke dash, animated through motion values; the marker is one transform. React renders only when the current heading or the rounded time left changes.",
        "Keep items to a few dozen headings; the line is rebuilt in one pass when the list resizes."
      ],
      "aiNotes": [
        "Use it beside long form content with real headings: documentation pages, engineering write ups, guides, policies and changelogs. Pass the heading ids that already exist in the article.",
        "Keep to two levels for most articles, three at most; the line bends at every level change, so alternating levels on every row makes it zigzag.",
        "Set offset to the height of any sticky header plus 16 to 32px, and use the same value as scroll-margin-top on the headings for anchor links.",
        "When the article scrolls inside its own container, pass scrollContainer; otherwise it finds the nearest scrolling ancestor of the first heading, or the page.",
        "Pass readingTime when you already know it; the default estimate counts every word in the scroll container, including code.",
        "Put it in a sticky aside on wide screens. On phones, show it above the article or in a sheet rather than beside it."
      ],
      "related": [
        "docs-sidebar",
        "tabs"
      ],
      "usage": "import { TocProgress } from \"@/components/arc/toc-progress/toc-progress\";\n\nconst headings = [\n  { id: \"overview\", label: \"Overview\" },\n  { id: \"install\", label: \"Installation\" },\n  { id: \"install-cli\", label: \"With the CLI\", level: 2 },\n  { id: \"install-manual\", label: \"By hand\", level: 2 },\n  { id: \"configure\", label: \"Configuration\" },\n] as const;\n\nexport function DocsPage({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"page\">\n      <article>{children}</article>\n      <aside className=\"toc\">\n        {/* Sticky header is 64px tall: headings count as reached just below it. */}\n        <TocProgress items={[...headings]} offset={88} onNavigate={id => history.replaceState(null, \"\", `#${id}`)} />\n      </aside>\n    </div>\n  );\n}",
      "exports": [
        "TocProgress"
      ]
    },
    {
      "name": "presence-stack",
      "kind": "component",
      "title": "Presence stack",
      "description": "Avatars of the people here now: joins pop in as the stack reflows, a counter rolls past the limit, dots show typing and idle, and hover fans it out with a gliding name label.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "avatar",
        "realtime",
        "new"
      ],
      "docs": "https://uiarc.dev/components/presence-stack",
      "markdown": "https://uiarc.dev/components/presence-stack/markdown",
      "registry": null,
      "keywords": [
        "presence",
        "avatars",
        "avatar stack",
        "facepile",
        "who is here",
        "online",
        "realtime",
        "collaboration",
        "multiplayer",
        "typing indicator",
        "idle",
        "status",
        "join",
        "leave",
        "overflow",
        "roster"
      ],
      "whenToUse": [
        "A document, board or design file header that shows who is here right now.",
        "A call or meeting roster where people come and go and some are talking or idle.",
        "Any realtime surface where joins, leaves and typing should be felt, not just counted."
      ],
      "whenNotToUse": [
        "Use avatar-group for a static list of owners, assignees or members that does not change while you look at it.",
        "Use live-cursors when you also need cursors and selections on a canvas; it includes its own small stack.",
        "Use a plain count for very large rooms, such as a livestream with thousands of viewers."
      ],
      "dependencies": [
        "motion",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The stack is one button named with the label and the count, such as People here now, 6 people, with aria-haspopup and aria-expanded from the popover.",
        "The faces, status marks and hover label are decorative; the list repeats every name with their status or detail in text, so nothing depends on color or on the typing animation.",
        "Joins and leaves are announced in a polite live region. Status changes are not announced to avoid chatter; they are in the list.",
        "The list is a labelled dialog from Radix Popover: focus moves in on open, Escape and a click outside close it, and focus returns to the stack.",
        "Rows are plain content unless onPersonSelect is set, then each is a button named with the person and what they are doing.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard focus shows through the name label, which arrow keys move between faces."
      ],
      "motion": [
        "Faces are positioned with transforms only. Reflow after a join or leave rides the smooth spring, so a burst of changes blends into one movement instead of queueing.",
        "A joining face pops in from 40 percent scale on the morph spring, with a hint of overshoot; a leaving face shrinks and fades in 160ms while its neighbours already close the gap.",
        "The +N counter rolls the way it moved: up for more people, down for fewer. Its number is nudged into the visible part of the chip.",
        "Hover keeps the 68 percent overlap and never spreads the stack; the hovered face lifts 2px, and one name label glides between faces on the morph spring while its width follows the new name. The label takes its first width at once so it never grows from nothing.",
        "The status dot scales into a small typing pill whose three dots rise in turn; idle dims the portrait with a fade.",
        "The list opens from the stack with a short scale and fade; rows expand and collapse in height as people come and go, and the count and detail lines roll.",
        "With reduced motion every change applies at once: no pop, slide, roll or bouncing dots, and the list simply appears."
      ],
      "responsive": [
        "The stack reserves its rest width, so it fits beside a title even at 320px. Hover never changes its width, so it never pushes content.",
        "On touch there is no hover; a tap opens the list, which carries the same names and statuses.",
        "The list is 288px wide and stays inside the viewport with collision padding; it scrolls past about six rows."
      ],
      "performance": [
        "Only transforms and opacity animate for the stack. Photos render at the exact size, as plain img elements unless ArcProvider sets an image component such as next/image.",
        "Motion springs are interruptible and stop when settled; nothing runs at rest except the three typing dots, which are a compositor animation and stop with reduced motion.",
        "Diffing joins and leaves for the live region is a single pass over the ids per update."
      ],
      "aiNotes": [
        "Use it wherever a realtime app shows who else is here: a document or board header, a call roster, a shared dashboard.",
        "Map your presence data to people on every change and pass the whole array; do not animate anything yourself. Keep ids stable across updates.",
        "Put the current user first and pass selfId so the label and list say (you). Put the most recently active people next so they stay visible before the +N counter.",
        "Use align=\"end\" in a right aligned header. The component reserves the width of max faces plus the counter so neighbours never shift as people join.",
        "Use detail for the specific place, such as Editing Pricing; it falls back to Active now, Idle or Typing.",
        "Set --presence-ring on the component to the surface color behind it so the rings around faces and dots cut out cleanly."
      ],
      "related": [
        "avatar-group",
        "avatar",
        "popover"
      ],
      "usage": "import { PresenceStack, type PresencePerson } from \"@/components/arc/presence-stack/presence-stack\";\n\nexport function DocumentHeader({ doc, me }: { doc: Doc; me: User }) {\n  // From your realtime layer: Liveblocks, PartyKit, Supabase Realtime or your own socket.\n  const others = useOthers();\n  const people: PresencePerson[] = [\n    { id: me.id, name: me.name, src: me.avatar, status: \"active\" },\n    ...others.map(other => ({\n      id: other.id,\n      name: other.name,\n      src: other.avatar,\n      status: other.typing ? \"typing\" : other.idle ? \"idle\" : \"active\",\n      detail: other.section ? `Editing ${other.section}` : undefined,\n    })),\n  ];\n\n  return (\n    <header className=\"flex items-center justify-between\">\n      <h1>{doc.title}</h1>\n      <PresenceStack people={people} selfId={me.id} align=\"end\" onPersonSelect={person => followCursor(person.id)} />\n    </header>\n  );\n}",
      "exports": [
        "PresenceStack"
      ]
    },
    {
      "name": "share-access",
      "kind": "component",
      "title": "Share access",
      "description": "A share panel: invite people as validated email chips with suggestions, change roles from a compact menu, remove with undo, switch link access and copy the link, with every change landing in place.",
      "tier": "pro",
      "category": "Disclosure",
      "tags": [
        "overlay",
        "sharing",
        "permissions",
        "new"
      ],
      "docs": "https://uiarc.dev/components/share-access",
      "markdown": "https://uiarc.dev/components/share-access/markdown",
      "registry": null,
      "keywords": [
        "share",
        "sharing",
        "permissions",
        "access",
        "invite",
        "email chips",
        "roles",
        "can edit",
        "can view",
        "link sharing",
        "copy link",
        "collaborators",
        "google docs share",
        "popover"
      ],
      "whenToUse": [
        "A Share button on a document, board or project that controls who can open it and what they can do.",
        "Inviting teammates by email with suggestions from a workspace directory.",
        "Managing link sharing next to per person access in one place."
      ],
      "whenNotToUse": [
        "Use the invite-people block for inviting people to a whole workspace with seats and plans.",
        "Use copy-button alone when there is only a public link to share.",
        "Use a full settings page with a data table for organisations with hundreds of members and groups."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The panel is a section named Share “title”. The field is a combobox with a listbox of suggestions, aria-activedescendant for the highlighted one and aria-expanded.",
        "An invalid address sets aria-invalid, is marked with an icon as well as color, and its message is linked with aria-describedby.",
        "Each chip has a Remove button named with the person. Each role trigger is named with the person and the current role, such as Role for Nathan Cole: Can edit.",
        "Role and access menus are Radix dropdown menus with radio items, so the current choice is announced as checked and focus returns to the trigger.",
        "Adding and removing chips, invites, role changes, removals, undo, access changes and copying are announced in a polite live region.",
        "Copy link keeps its width across Copy link, Link copied and Couldn’t copy, and the failure state pairs an icon with text.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Hovered and focused triggers and rows take a muted background."
      ],
      "motion": [
        "Chips pop in from 80 percent scale on the snappy spring and the field reflows around them with layout transforms, so the caret slides to its new spot.",
        "Suggestions open under the field with a short scale and fade, and the highlight slides between rows as a shared layout element.",
        "The error line and the invite row expand and collapse in height on the smooth spring, which never overshoots.",
        "New people expand into the list with a brief accent tint that fades over a second. A removed row folds its height to zero while the rows below slide up, and the undo bar slides in beneath.",
        "Role labels crossfade vertically while the trigger's width follows the new label on the morph spring, so the chevron glides instead of jumping. The count of people rolls.",
        "The lock turns into a globe with a small rotation, the description rolls to its new line and the link role menu slides in from the right without moving anything else.",
        "Copy link swaps to a check and Link copied in place, then back after 1.8 seconds.",
        "With reduced motion all of it applies at once: no pops, rolls, slides or height animation."
      ],
      "responsive": [
        "The panel is up to 440px wide and fills narrower containers. At 420px and below padding tightens and the link role moves under the general access line.",
        "Long names and emails truncate with an ellipsis in rows and chips; the full value is in the accessible name.",
        "Chips wrap onto new lines inside the field as it grows; menus stay inside the viewport with collision padding."
      ],
      "performance": [
        "Everything that moves is a transform or opacity except row and message heights, which animate only when they appear or leave.",
        "Suggestions filter a directory array in memory; for very large directories, pass a pre filtered directory from your search.",
        "Menus mount only while open."
      ],
      "aiNotes": [
        "Use it as the content of a share popover or dialog for any object with per person access: documents, boards, projects, folders.",
        "Control members and persist every change; the component emits the whole next list. Use optimistic updates so rows land instantly.",
        "Return a promise from onInvite for the server call. Reject it to keep the chips so the person can retry.",
        "Pass directory from your workspace so people are suggested with photos. Anyone else can still be invited by email.",
        "Owner rows cannot be changed or removed. Map any other permission scheme to the three roles or fork ROLES in the source.",
        "The panel draws its own floating surface; when placing it inside a popover primitive, keep that popover's content unstyled."
      ],
      "related": [
        "popover",
        "tag-input",
        "dropdown-menu",
        "copy-button",
        "presence-stack",
        "combobox"
      ],
      "usage": "import * as Popover from \"@radix-ui/react-popover\";\nimport { ShareAccess } from \"@/components/arc/share-access/share-access\";\n\nexport function ShareButton({ doc }: { doc: Doc }) {\n  const [open, setOpen] = useState(false);\n  const { data: members, mutate } = useMembers(doc.id);\n\n  return (\n    <Popover.Root open={open} onOpenChange={setOpen}>\n      <Popover.Trigger>Share</Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Content align=\"end\" sideOffset={8}>\n          <ShareAccess\n            title={doc.title}\n            members={members}\n            onMembersChange={next => mutate(api.setMembers(doc.id, next), { optimisticData: next })}\n            directory={workspace.people}\n            link={doc.url}\n            selfId={me.id}\n            onInvite={(people, role) => api.invite(doc.id, people, role)}\n            onDone={() => setOpen(false)}\n          />\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}",
      "exports": [
        "ShareAccess"
      ]
    },
    {
      "name": "metric-ticker",
      "kind": "component",
      "title": "Metric ticker",
      "description": "A strip of live KPIs that roll to new values digit by digit and flash their change; rest on one to pause the strip and open its recent trend as a sparkline.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "metric",
        "realtime",
        "new"
      ],
      "docs": "https://uiarc.dev/components/metric-ticker",
      "markdown": "https://uiarc.dev/components/metric-ticker/markdown",
      "registry": null,
      "keywords": [
        "ticker",
        "kpi",
        "metrics",
        "live",
        "realtime",
        "stock ticker",
        "marquee",
        "strip",
        "odometer",
        "rolling digits",
        "delta",
        "sparkline",
        "dashboard header",
        "status",
        "trend"
      ],
      "whenToUse": [
        "A compact live readout of several KPIs above a dashboard, a status page or a trading view.",
        "Wall screens and operations views where values change on their own and people glance at them.",
        "A header strip that must fit anywhere from a phone to a wide monitor without redesign."
      ],
      "whenNotToUse": [
        "Use metric-card or stat-card for one or two headline numbers with room for context.",
        "Use line-chart or sparkline when the trend over time is the main content, not a detail.",
        "Avoid it for values that change once a day; the live treatment would promise more than it delivers."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The strip is a labelled region with a list of metrics. Each metric is a button named with its label, formatted value and change, such as API p95, 214 ms, up 2 ms, with aria-expanded and aria-controls pointing at the panel.",
        "Only one metric is a tab stop (roving tab index), so the strip costs one Tab however many metrics it holds.",
        "Changes pair an arrow and a signed amount with color, so good and bad news never rely on color alone.",
        "Values are not announced on every tick, which would flood a screen reader on a live strip; they are read when a metric is focused.",
        "The repeated copy that makes the marquee seamless is hidden from assistive technology and out of the tab order.",
        "The sparkline is decorative; the panel states the trend in words and the low and high as text."
      ],
      "motion": [
        "Digits roll like an odometer on the smooth spring, turning the way the value moved and wrapping 9 to 0. The ones settle first and each higher place follows 25ms later, so a change ripples right to left. A new digit column or a wider delta opens on a spring, never in one frame.",
        "A change flashes for flashDuration: the delta's tint deepens, its arrow slides in from the direction of travel and a thin ring expands and fades once. Then the tint drains and the delta rests in a quieter tone until the next change.",
        "The marquee eases its speed rather than stopping dead: resting on the strip glides it to a halt in about half a second, leaving lets it pick up again gently. Drag to scrub it; release hands your flick back to the loop, which decays into the steady drift.",
        "The panel grows out of the metric that opened it on a morph spring, scaling from the metric's center. It follows its metric every frame, and moving to another metric glides it across while its content crossfades.",
        "The sparkline draws itself in, the area fades up under it and the last point pops in once the line arrives.",
        "Focusing a metric inside a moving marquee pulls it into view on the shortest path around the loop.",
        "With reduced motion there is no marquee (overflow falls back to horizontal scroll), digits change in place, the panel fades without scaling and the sparkline appears drawn."
      ],
      "responsive": [
        "Metrics that fit share the width equally. When they no longer fit, the strip becomes a marquee (or a snapping horizontal scroll with overflow=\"scroll\"); the switch is measured with a ResizeObserver, not a breakpoint.",
        "The marquee feathers its ends with a mask while the border stays crisp.",
        "Below 420px values drop to the base size and metrics tighten their padding.",
        "The panel is at most 248px wide and is kept inside the component's box; leave room under the strip or let it float over the content below.",
        "Touch: a tap pins a panel and pauses the strip; a press anywhere outside closes it. Horizontal drags scrub the marquee while vertical page scrolling still works."
      ],
      "performance": [
        "The marquee is one requestAnimationFrame loop writing a single transform. It stops entirely when the strip is offscreen, when the tab is hidden and when the marquee is not needed.",
        "The panel follows its metric with a frame loop that runs only while a panel is open.",
        "Digit wheels, the flash and the panel are transforms and opacity only. The sparkline is one small SVG path redrawn when its data changes."
      ],
      "aiNotes": [
        "Use it as the live headline of a dashboard, status page or trading style view: four to eight metrics people glance at while doing something else.",
        "Set positive: \"down\" on any metric where lower is better (latency, errors, churn, cost). Getting this wrong flashes good news red.",
        "Use delta: \"absolute\" for rates and durations, where 0.3 points or 8 ms means more than a percent of a percent; keep the default percent for money and counts.",
        "Pass history when you already have a series from your backend; otherwise the ticker builds one from what it sees since mount.",
        "Keep labels to two or three words. The strip is for scanning; the panel caption can carry the context.",
        "Batch updates. One to a few changes a second reads as live; a new array every frame just flickers."
      ],
      "related": [
        "metric-card",
        "sparkline",
        "animated-counter",
        "line-chart"
      ],
      "usage": "import { MetricTicker, type TickerMetric } from \"@/components/arc/metric-ticker/metric-ticker\";\n\nexport function HealthStrip({ stats }: { stats: LiveStats }) {\n  const metrics: TickerMetric[] = [\n    { id: \"mrr\", label: \"Monthly recurring revenue\", value: stats.mrr, format: { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 } },\n    { id: \"active\", label: \"Active now\", value: stats.activeUsers },\n    { id: \"conversion\", label: \"Trial conversion\", value: stats.conversion, format: { style: \"percent\", maximumFractionDigits: 1 }, delta: \"absolute\", caption: \"Rolling 7 days\" },\n    { id: \"latency\", label: \"API p95\", value: stats.p95, format: { style: \"unit\", unit: \"millisecond\" }, positive: \"down\", delta: \"absolute\", history: stats.p95History },\n  ];\n\n  return <MetricTicker metrics={metrics} label=\"Product health\" />;\n}",
      "exports": [
        "MetricTicker"
      ]
    },
    {
      "name": "split-bar",
      "kind": "component",
      "title": "Split bar",
      "description": "A single 100% bar split into shares: rest on a segment to widen it and read its exact figures, toggle segments from the legend and the rest reflow, with labels inside when they fit and on leader lines when they don't.",
      "tier": "pro",
      "category": "Data",
      "tags": [
        "data",
        "chart",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/split-bar",
      "markdown": "https://uiarc.dev/components/split-bar/markdown",
      "registry": null,
      "keywords": [
        "split bar",
        "stacked bar",
        "100% bar",
        "part to whole",
        "share",
        "proportion",
        "percent",
        "traffic sources",
        "budget",
        "storage bar",
        "legend toggle",
        "leader lines",
        "segment",
        "breakdown"
      ],
      "whenToUse": [
        "A compact part to whole view that fits in a card row or a dashboard header.",
        "Comparing shares where people also want exact figures on demand and the option to leave a category out.",
        "Showing how a mix changes between periods or filters with one continuous reflow."
      ],
      "whenNotToUse": [
        "Use bar-chart to compare absolute values across many categories.",
        "Use donut-chart when the total in the middle is the headline, or waffle-chart for a count of units.",
        "Use a stacked chart over time when the trend of each share matters more than today's split."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The bar is a focusable group with the role description split bar, named with every visible share (Organic search 41.6%, Direct 24.8% and so on) and how to read each one.",
        "Moving through segments with the arrow keys announces the segment's name, share and value in a polite live region; switching a segment off or on is announced too.",
        "Legend items are toggle buttons with aria-pressed, named with the segment and its share or hidden. An off segment is also shown without color: a hollow swatch, struck through name and the word Off.",
        "The last visible segment cannot be switched off; the legend item gives a small shake and the reason is announced.",
        "Inside labels, outside labels, leader lines and rolling digits are hidden from assistive technology; the group name and readout carry the same numbers as text.",
        "Segments use the shared chart palette, validated for color vision deficiency in both themes, and every segment is named in the legend, so color is never the only key."
      ],
      "motion": [
        "Each segment is a full width layer cut to its share with an animated clip-path, and its edges ride the smooth spring. Reflow never touches layout, and a new target mid flight retargets from where the edges are.",
        "Hovering or arrowing onto a segment widens it by about 4.5% of the bar, or enough to fit its name and share when it is small, while the others give way proportionally and dim, so the bar always stays at 100%.",
        "Labels live inside the clipped layer, so they slide with their segment and are cut cleanly at its edge instead of spilling into a neighbour. When a segment grows or shrinks, its name opens or closes on a spring beside the percentage.",
        "Outside labels glide to their new places and fade in and out; their leader lines are S curves that morph with them. The label lane opens and closes smoothly when the first label moves outside or the last one comes back.",
        "Percentages, values and the total roll digit by digit like an odometer, in the direction they changed.",
        "The readout above the bar crossfades between the total and the highlighted segment with a short rise and a little blur.",
        "With reduced motion segments and labels move at once, digits change in place, readouts swap without travel and the refusal shake is skipped."
      ],
      "responsive": [
        "The bar spans its container. Its width is measured with a ResizeObserver and label fitting is recalculated, so the same data shows more labels inside on a wide card and moves more of them outside on a phone.",
        "The readout wraps under the title on narrow screens; the legend wraps to as many rows as it needs.",
        "On touch, a tap on a segment highlights it and a tap elsewhere clears it; vertical page scrolling is never captured."
      ],
      "performance": [
        "Label fitting uses canvas text metrics cached per font, so it never reads layout while animating.",
        "Segment edges, caps and labels are driven by motion values; React renders only when the data, the hidden set or the highlighted segment changes.",
        "Segments animate clip-path and transform only."
      ],
      "aiNotes": [
        "Use it for a part to whole split of two to eight categories where people compare shares at a glance: traffic sources, budget lines, storage by file type, survey answers, device mix.",
        "Order segments by size or by a meaningful sequence, and put catch-all shares such as Other last so they take the neutral colors.",
        "Set unit or format so the readout gives the exact figure, not only the percentage.",
        "Keep labels short. Long names only show inside wide segments; everything else moves to the outside lane.",
        "Pass new segments with the same ids to animate between periods or filters; the bar reflows instead of redrawing."
      ],
      "related": [
        "donut-chart",
        "waffle-chart",
        "treemap",
        "bar-chart",
        "usage-meter"
      ],
      "usage": "import { SplitBar, type SplitSegment } from \"@/components/arc/split-bar/split-bar\";\n\nconst budget: SplitSegment[] = [\n  { id: \"payroll\", label: \"Payroll\", value: 412_000 },\n  { id: \"cloud\", label: \"Cloud\", value: 96_500 },\n  { id: \"marketing\", label: \"Marketing\", value: 74_200 },\n  { id: \"tools\", label: \"Software\", value: 18_900 },\n  { id: \"other\", label: \"Other\", value: 9_300 },\n];\n\nexport function BudgetSplit() {\n  const [hidden, setHidden] = useState<string[]>([]);\n  return (\n    <SplitBar\n      title=\"Q4 budget\"\n      segments={budget}\n      format={{ style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 }}\n      hidden={hidden}\n      onHiddenChange={setHidden}\n    />\n  );\n}",
      "exports": [
        "SplitBar"
      ]
    },
    {
      "name": "undo-toast",
      "kind": "component",
      "title": "Undo toast",
      "description": "Archive or delete rows with a second chance: the gap closes, a countdown toast merges repeat actions, and Undo or \\u2318Z puts every row back exactly where it was.",
      "tier": "pro",
      "category": "Feedback",
      "tags": [
        "feedback",
        "toast",
        "undo",
        "new"
      ],
      "docs": "https://uiarc.dev/components/undo-toast",
      "markdown": "https://uiarc.dev/components/undo-toast/markdown",
      "registry": null,
      "keywords": [
        "undo",
        "toast",
        "snackbar",
        "archive",
        "delete",
        "trash",
        "inbox",
        "list",
        "remove",
        "restore",
        "countdown",
        "cmd z",
        "ctrl z",
        "optimistic",
        "batch",
        "gmail undo"
      ],
      "whenToUse": [
        "An inbox, notification list, task list or file list where archiving or deleting should be one click with a way back.",
        "Bulk triage, where people remove several items quickly and one toast should summarise them all.",
        "Anywhere a confirmation dialog would slow down a routine, reversible action."
      ],
      "whenNotToUse": [
        "Irreversible destruction such as deleting a project or revoking keys; use confirm-popover so the consequence is stated first.",
        "General app notifications that are not tied to removing list items; use toast-stack.",
        "Lists of hundreds of visible rows animating at once; virtualise the list and remove through the ref handle instead."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The list is a labelled ul. A folded row is aria-hidden and inert, so it leaves the tab order and the accessibility tree the moment it is removed and returns when it is restored.",
        "Removing a focused row hands focus to the same control on the next row, or the previous one; when the list is empty focus moves to Undo. Undoing from the toast moves focus to the first restored row.",
        "A polite live region announces each change in words: Conversation archived. Press Command Z or the Undo button to undo; 3 conversations restored; Couldn't archive conversation.",
        "The toast is a labelled group that describes itself with the live text. The Undo button carries aria-keyshortcuts. The countdown ring and seconds are decorative; hovering or focusing the toast pauses them so there is time to act.",
        "State never relies on color: the toast pairs a countdown, check or alert icon with a sentence."
      ],
      "motion": [
        "Remove: the row content fades and slips 28px aside with a small scale and blur in 180ms while the row height closes on the smooth spring, so rows below glide up to fill the gap.",
        "The toast rises 18px from a slightly smaller, blurred state on the morph spring, from its bottom edge. It leaves by dropping and fading on the exit curve.",
        "Merge: the message crossfades vertically when its shape changes, the count rolls digit by digit, the ring springs back to full and the toast gets a velocity nudge on its scale.",
        "The countdown ring is an SVG path whose length follows a linear motion value; the seconds inside roll on each whole second. Hover, focus and a hidden tab pause the same animation, so the ring stops exactly where it was.",
        "Undo: restored rows reopen top to bottom with a 35ms stagger, their content slides back in on the snappy spring after the height starts to open, and a faint accent glow marks where they returned. The ring turns into a check with a small rotation.",
        "Everything is interruptible: removing again mid collapse, undoing mid collapse or removing during an undo all retarget from the current values.",
        "With reduced motion rows open and close at once, the toast only fades, digits swap without rolling and the ring refills instantly; the countdown itself still drains so the time limit stays visible."
      ],
      "responsive": [
        "Rows truncate their title and description with an ellipsis at any width; the time and the actions share one cell so nothing reflows on hover.",
        "On touch screens the row actions are always visible and the shortcut hint on Undo is hidden.",
        "The toast is at most 372px wide and keeps 12px from the component edges, so it fits a 320px phone; the message truncates before the Undo button shrinks."
      ],
      "performance": [
        "Only removed and restored rows animate. Row height is the one layout property that moves, because closing the gap is the information; the content moves with transforms and opacity.",
        "The countdown is one motion value drained by a linear animation, not a timer tick; React re-renders once per whole second for the digit.",
        "Rows render through a small child component, so a toast update does not re-render every row's content."
      ],
      "aiNotes": [
        "Use UndoToast instead of a confirmation dialog for reversible removals: archive, delete to trash, remove from a list, mark done. It lets people act fast and recover.",
        "Choose one pattern for the server: deferred (do nothing until onCommit, then send the batch) or optimistic (send in onRemove, reverse in onUndo). Deferred is simpler and needs no reversal endpoint.",
        "Keep items controlled when the list lives in your store. Removed rows stay in your array until onItemsChange fires on commit, which is what lets undo put them back at the same index.",
        "Reserve space under the list for the toast in container placement (about 76px), or use toastPlacement=\"viewport\" for full page lists.",
        "Name the noun so the toast reads naturally: noun={[\"file\", \"files\"]}. Give each action a label and past tense if you add kinds beyond archive and delete.",
        "Use your own row markup freely; UndoToastRow is a convenience. Call ctx.remove from buttons, swipe actions or keyboard shortcuts."
      ],
      "related": [
        "toast-stack",
        "toast",
        "swipe-actions",
        "confirm-morph",
        "hold-to-confirm"
      ],
      "usage": "import { Archive, Trash2 } from \"lucide-react\";\nimport { UndoToast, UndoToastRow, UndoToastRowAction } from \"@/components/arc/undo-toast/undo-toast\";\n\nexport function Inbox({ threads, setThreads }: { threads: Thread[]; setThreads: (t: Thread[]) => void }) {\n  return (\n    <UndoToast\n      label=\"Conversations\"\n      items={threads}\n      onItemsChange={setThreads}\n      getKey={thread => thread.id}\n      noun={[\"conversation\", \"conversations\"]}\n      onCommit={entries => api.apply(entries.map(e => ({ id: e.key, action: e.kind })))}\n      style={{ paddingBottom: 76 }}\n      renderItem={(thread, { remove }) => (\n        <UndoToastRow\n          label={`${thread.from}, ${thread.subject}`}\n          leading={<Avatar src={thread.avatar} />}\n          title={thread.from}\n          description={thread.subject}\n          meta={thread.time}\n          shortcuts={{ e: () => remove(\"archive\"), Backspace: () => remove(\"delete\") }}\n          actions={<>\n            <UndoToastRowAction label=\"Archive\" icon={<Archive size={16} />} onClick={() => remove(\"archive\")} />\n            <UndoToastRowAction label=\"Delete\" icon={<Trash2 size={16} />} onClick={() => remove(\"delete\")} />\n          </>}\n        />\n      )}\n    />\n  );\n}",
      "exports": [
        "UndoToast",
        "UndoToastRow",
        "UndoToastRowAction"
      ]
    },
    {
      "name": "confirm-popover",
      "kind": "component",
      "title": "Confirm popover",
      "description": "A destructive button that grows into a small confirmation card, with the consequence, optional type-to-confirm, progress on the Confirm button, and a check or error before folding back.",
      "tier": "pro",
      "category": "Disclosure",
      "tags": [
        "overlay",
        "confirm",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/confirm-popover",
      "markdown": "https://uiarc.dev/components/confirm-popover/markdown",
      "registry": null,
      "keywords": [
        "confirm",
        "confirmation",
        "popover",
        "delete",
        "destructive",
        "danger zone",
        "are you sure",
        "type to confirm",
        "progress",
        "inline confirm",
        "morph",
        "revoke",
        "remove",
        "alertdialog"
      ],
      "whenToUse": [
        "A danger zone in settings: delete project, transfer ownership, revoke a member.",
        "A row action in a table where a modal would lose the row's context.",
        "Any destructive action that needs a sentence of consequence and a visible result without leaving the page."
      ],
      "whenNotToUse": [
        "Reversible removals such as archiving; use undo-toast so the action is instant and recoverable.",
        "A one word inline confirmation in a toolbar; use confirm-morph, which morphs horizontally inside the button's row.",
        "Long forms or several decisions; use dialog."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The trigger has aria-haspopup=\"dialog\" and aria-expanded. The open card is a non modal alertdialog named by the title and described by the consequence.",
        "The phrase field has a visible label that includes the phrase; it is marked aria-invalid while the typed text does not match.",
        "While the action runs the card is aria-busy and holds focus; a polite live region reads the pending label and the percentage, then the success label or the failure. The failure is also a role=\"alert\" paragraph and focus moves to Try again.",
        "Confirm stays in the tab order while locked and says so with aria-disabled, so people can reach it and hear why it does nothing yet.",
        "The trigger is kept in place, invisible, while the card covers it, and takes focus back only after the card has folded into it, so focus is never lost to the page."
      ],
      "motion": [
        "One surface: at rest it is drawn exactly over the button, then its width, height and corner radius spring to the card on the morph spring (a hint of bounce) and back on the smooth spring. The edge nearest the button stays still, and the content is pinned to that edge, so the card is revealed in place rather than sliding.",
        "The button face fades out in 100ms as the card content fades up 6px from a 4px blur, 60ms later. Tint, border and floating shadow crossfade from the button's look to the card's.",
        "Typing the phrase fills Confirm on the snappy spring to the share of the phrase matched so far; a second copy of the label in the fill's ink is clipped to the fill, so the text inverts exactly at its edge.",
        "Confirming folds Cancel away on the smooth spring while Confirm widens to the full row; the fill drops to zero and then follows reported progress, or a soft sweep runs when there is none.",
        "Success fills the bar, then the card folds back to the button's size with the check rotating in. Failure refills the bar as Try again, opens the error on a height spring and shakes the card once with a damped keyframe.",
        "With reduced motion the size changes are instant and only the faces crossfade; there is no shake and the sweep becomes a static tint."
      ],
      "responsive": [
        "The card is 320px wide by default and never wider than the window less 32px; it shifts horizontally to stay 16px from either edge, so it works from a 320px phone upward.",
        "It grows downward unless there is not enough room below the button, then upward, keeping the button's edge still.",
        "The phrase field disables autocorrect, autocapitalise and spellcheck, so phones do not change what is typed."
      ],
      "performance": [
        "Only one surface is mounted while the question is open, and nothing is mounted at rest besides the button.",
        "The fill and its inverted label use a clip-path driven by a motion value, so typing and progress updates do not re-render the component.",
        "Width and height animate on a single absolutely positioned layer, which does not reflow the page around it."
      ],
      "aiNotes": [
        "Use it for irreversible or serious actions where the consequence should be read before confirming: deleting a project, revoking access, rotating keys, cancelling a subscription.",
        "Write the description as the consequence, with numbers when you have them: This deletes 14 deployments and every log. It can't be undone.",
        "Add confirmText only for the most destructive actions; typing the resource name is friction by design.",
        "Report progress from onConfirm with ctx.setProgress when the action has steps; otherwise the bar sweeps until the promise settles. Throw an Error with a human message to show it under the error title.",
        "Keep successLabel short, such as Deleted or Removed, so the button does not change width at rest.",
        "The card floats over the page with position absolute inside the component. Do not place it in an ancestor with overflow hidden that is smaller than the card."
      ],
      "related": [
        "confirm-morph",
        "hold-to-confirm",
        "dialog",
        "undo-toast"
      ],
      "usage": "import { Trash2 } from \"lucide-react\";\nimport { ConfirmPopover } from \"@/components/arc/confirm-popover/confirm-popover\";\n\nexport function DeleteProject({ project }: { project: Project }) {\n  return (\n    <ConfirmPopover\n      icon={<Trash2 size={16} />}\n      align=\"end\"\n      title={`Delete ${project.name}?`}\n      description={`This permanently deletes ${project.deployments} deployments and every log. It can't be undone.`}\n      confirmText={project.slug}\n      confirmLabel=\"Delete project\"\n      errorTitle={`Couldn't delete ${project.name}`}\n      onConfirm={async ({ setProgress, signal }) => {\n        for await (const step of api.deleteProject(project.id, { signal })) setProgress(step.done / step.total);\n      }}\n      onDone={() => router.push(\"/projects\")}\n    >\n      Delete\n    </ConfirmPopover>\n  );\n}",
      "exports": [
        "ConfirmPopover"
      ]
    },
    {
      "name": "toggle-group",
      "kind": "component",
      "title": "Toggle group",
      "description": "A row of icon or text toggles, single or multiple: one pressed fill glides between options, or each fades in on its own and neighbours join, as in a formatting toolbar.",
      "tier": "free",
      "category": "Inputs",
      "tags": [
        "input",
        "toggle",
        "toolbar",
        "new"
      ],
      "docs": "https://uiarc.dev/components/toggle-group",
      "markdown": "https://uiarc.dev/components/toggle-group/markdown",
      "registry": "https://uiarc.dev/r/toggle-group.json",
      "keywords": [
        "toggle group",
        "toggle buttons",
        "toolbar",
        "formatting",
        "bold italic underline",
        "text alignment",
        "icon buttons",
        "pressed",
        "multi select",
        "button group"
      ],
      "whenToUse": [
        "Formatting toolbars: bold, italic and underline as multiple, alignment as single.",
        "Compact view options on a canvas, map or editor, such as grid or list, or which layers to show.",
        "Any short row of icon toggles that apply immediately."
      ],
      "whenNotToUse": [
        "Use segmented-control for switching between views, ranges or tabs of content.",
        "Use checkbox or switch for settings in a form that are saved together.",
        "Use chip-group for filters with long text labels or many options that wrap."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Single mode renders role=\"radiogroup\" with role=\"radio\" items and aria-checked; multiple mode renders role=\"group\" with aria-pressed buttons. Both are labelled by label.",
        "Roving tabindex: only one item is a tab stop, so a toolbar with several groups stays quick to cross with Tab.",
        "Icon only items take their accessible name and tooltip from label. Pressed state is shown by fill and text contrast, and exposed through aria-checked or aria-pressed.",
        "The fills are aria-hidden decoration."
      ],
      "motion": [
        "Single: one fill with a shared layoutId moves to the chosen item on the morph spring, scoped per group by a LayoutGroup; its radius is scale corrected so it keeps its shape between items of different widths.",
        "Multiple: every item owns a fill that fades in over 180ms while it settles from 86% scale on the spring curve. When two neighbours are on, the fills close the gap and square off their inner corners, so a run of pressed items reads as one shape.",
        "Pressing an item gives its icon and label a small 92% press and spring back; the item box never scales, so neighbours never shift.",
        "Reduced motion moves the single fill instantly and replaces fades and presses with instant color changes."
      ],
      "responsive": [
        "The strip stays on one line and sizes to its items; place groups in a wrapping flex row so they stack at 320px.",
        "Items are at least 36px square at md; use md on touch screens and sm in dense desktop toolbars."
      ],
      "performance": [
        "Single mode runs one Motion layout animation per change; multiple mode is pure CSS transitions on opacity, transform and inset.",
        "Item order for the roving tab stop is read from the DOM once per render of the group."
      ],
      "aiNotes": [
        "Choose ToggleGroup for tool-like options that act on something else, often icon only: text formatting, view options, filters on a canvas. Choose segmented-control for a tab-like choice between views or ranges.",
        "Use type=\"multiple\" for independent on/off options (bold, italic) and the default single type for mutually exclusive ones (alignment).",
        "Give every icon only item a label. Keep items as direct children; wrapping each in another element stops neighbouring fills from joining.",
        "Place several groups side by side with an 8px gap to build a toolbar; each group stays one tab stop."
      ],
      "related": [
        "segmented-control",
        "radio-group",
        "checkbox",
        "switch",
        "chip-group",
        "rich-text-editor"
      ],
      "usage": "import { ToggleGroup, ToggleGroupItem } from \"@/components/arc/toggle-group/toggle-group\";\nimport { AlignCenter, AlignLeft, AlignRight, Bold, Italic, Underline } from \"lucide-react\";\n\nexport function FormatBar() {\n  const [marks, setMarks] = useState<string[]>([]);\n  const [align, setAlign] = useState(\"left\");\n  return (\n    <div style={{ display: \"flex\", gap: 8 }}>\n      <ToggleGroup type=\"multiple\" label=\"Text style\" value={marks} onValueChange={setMarks}>\n        <ToggleGroupItem value=\"bold\" label=\"Bold\"><Bold /></ToggleGroupItem>\n        <ToggleGroupItem value=\"italic\" label=\"Italic\"><Italic /></ToggleGroupItem>\n        <ToggleGroupItem value=\"underline\" label=\"Underline\"><Underline /></ToggleGroupItem>\n      </ToggleGroup>\n      <ToggleGroup label=\"Alignment\" value={align} onValueChange={setAlign}>\n        <ToggleGroupItem value=\"left\" label=\"Align left\"><AlignLeft /></ToggleGroupItem>\n        <ToggleGroupItem value=\"center\" label=\"Align center\"><AlignCenter /></ToggleGroupItem>\n        <ToggleGroupItem value=\"right\" label=\"Align right\"><AlignRight /></ToggleGroupItem>\n      </ToggleGroup>\n    </div>\n  );\n}",
      "exports": [
        "ToggleGroup",
        "ToggleGroupItem"
      ]
    },
    {
      "name": "avatar-upload",
      "kind": "component",
      "title": "Avatar upload",
      "description": "Click or drop a photo onto the avatar to crop it in place with drag, pinch and zoom. It uploads through a progress ring and settles, with clear errors and retry.",
      "tier": "pro",
      "category": "Inputs",
      "tags": [
        "input",
        "upload",
        "image",
        "new"
      ],
      "docs": "https://uiarc.dev/components/avatar-upload",
      "markdown": "https://uiarc.dev/components/avatar-upload/markdown",
      "registry": null,
      "keywords": [
        "avatar upload",
        "profile photo",
        "profile picture",
        "photo upload",
        "image upload",
        "crop",
        "circular crop",
        "zoom",
        "pinch",
        "drag and drop",
        "progress ring",
        "upload progress",
        "initials",
        "account settings",
        "change photo"
      ],
      "whenToUse": [
        "Profile, account and team member settings where a person sets their own photo.",
        "Workspace or organization logos that display in a circle.",
        "Onboarding steps that ask for a photo and should show upload progress in place."
      ],
      "whenNotToUse": [
        "Several files or documents at once: use file-dropzone.",
        "Free form crops with aspect ratios, rotation or a large canvas: use image-cropper.",
        "Showing an avatar without editing: use avatar."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The component is a group named by its label. The avatar is a button named Change profile photo or Upload profile photo.",
        "Opening the cropper moves focus to the crop area, a focusable group whose name gives the zoom and every key. Saving, canceling and removing return focus to the upload button.",
        "The zoom slider is a native range with aria-valuetext in percent, flanked by Zoom out and Zoom in buttons.",
        "Errors, the uploading state, Photo updated and Photo removed appear in a polite live region; progress is announced in 25% steps. Errors pair an icon with a sentence that says what to do next.",
        "The photo, the dimmed overflow and the rings are decorative; the state they show is always in text as well.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard focus on the avatar shows the same camera overlay as hover."
      ],
      "motion": [
        "Opening the cropper grows the avatar itself into the 184px crop circle with a layout animation on the smooth spring, while a tinted stage fades in behind it and the new photo fades in. The layout reflows once and the circle travels to its new place; the block's height follows on the same spring.",
        "Dragging tracks the pointer one to one. Past an edge the photo follows at a falling rate and never more than a quarter of the circle, then springs back inside the bounds on release. The dimmed overflow brightens while you drag, zoom or scrub the slider.",
        "Wheel, trackpad pinch and two finger pinch zoom toward the pointer or between the fingers, so the point you aim at stays still. The slider zooms around the centre.",
        "Saving shrinks the circle back, insets the cropped photo to 90% under a dim scrim and draws a 2px ring outside it. The ring turns while waiting, then fills with reported progress on the smooth spring.",
        "On success the ring closes, then a second full ring widens by 12% and fades over 480ms while the photo springs back to full size on the snappy spring and the scrim lifts.",
        "A failed upload stops the ring where it was and turns it to the danger color, keeping the pending photo inset until you retry or discard.",
        "With reduced motion the circle resizes at once, the photo snaps to its bounds, the settle ring is skipped and the waiting ring turns slowly."
      ],
      "responsive": [
        "At rest the avatar sits beside its label, message and buttons at every width down to 320px; below 400px the spacing tightens so Replace photo and Remove stay on one row.",
        "While cropping, the 248px stage sits beside the controls on wide containers and stacks above them below 480px, measured with a container query so it adapts to narrow cards as well as phones.",
        "The crop stage uses touch-action: none so dragging and pinching never scroll the page; the wheel is captured only over the stage."
      ],
      "performance": [
        "The cropper moves two copies of the photo with a single transform each, in percentages of their own size, so the photo stays correct while the circle resizes and no layout runs per frame.",
        "The crop is drawn once into a canvas at outputSize when you save; the original file is never re-encoded before that.",
        "Size and type are checked before decoding, so a 12 MB file is rejected without being read into an image."
      ],
      "aiNotes": [
        "Wire onUpload to your storage and call report from your upload progress events; the ring is only as honest as the progress you report.",
        "Resolve with the stored URL so the avatar shows the canonical file; object URLs made by the component are revoked when replaced and on unmount.",
        "Reject with an Error whose message says what happened and what is unchanged; it is shown as is.",
        "Keep accept and maxSize in line with your backend. The default hint is generated from them.",
        "Use the ref's selectFile to accept pasted images or photos from your own camera capture; they pass the same checks as a drop.",
        "Real portraits in the gallery demo come from public/media/people/portrait; the demo uploads nothing."
      ],
      "related": [
        "avatar",
        "file-dropzone",
        "avatar-group"
      ],
      "usage": "import { AvatarUpload } from \"@/components/arc/avatar-upload/avatar-upload\";\n\nexport function ProfilePhoto({ user }: { user: User }) {\n  return (\n    <AvatarUpload\n      name={user.name}\n      src={user.avatarUrl}\n      onUpload={async (image, { report, signal }) => {\n        const { uploadUrl, publicUrl } = await api.createAvatarUpload();\n        await putWithProgress(uploadUrl, image, { onProgress: report, signal });\n        await api.saveAvatar(publicUrl);\n        return publicUrl;\n      }}\n      onRemove={() => api.removeAvatar()}\n    />\n  );\n}",
      "exports": [
        "AvatarUpload"
      ]
    },
    {
      "name": "expandable-text",
      "kind": "component",
      "title": "Expandable text",
      "description": "Long text clamped to a few lines with a soft fade that glides open with Read more and closes with Show less, shown only when the text overflows.",
      "tier": "free",
      "category": "Disclosure",
      "tags": [
        "disclosure",
        "text",
        "motion",
        "new"
      ],
      "docs": "https://uiarc.dev/components/expandable-text",
      "markdown": "https://uiarc.dev/components/expandable-text/markdown",
      "registry": "https://uiarc.dev/r/expandable-text.json",
      "keywords": [
        "read more",
        "show more",
        "show less",
        "truncate",
        "line clamp",
        "clamp",
        "collapse text",
        "expand text",
        "see more",
        "review",
        "description",
        "fade out text"
      ],
      "whenToUse": [
        "A list of user reviews or comments where some entries are long.",
        "A product, listing, or profile description that should not push the rest of the page down.",
        "Any card where long text would make the cards uneven."
      ],
      "whenNotToUse": [
        "Use accordion for a set of titled sections or an FAQ.",
        "Use expandable-card when the hidden part is structured content rather than running text.",
        "Use scroll-area when the full text must stay in a fixed height box.",
        "Do not clamp content people must read, such as legal terms or error details."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The clamp is visual only: the full text stays in the accessibility tree, so screen readers read all of it without pressing the toggle.",
        "The toggle is a native button with aria-expanded and aria-controls pointing at the text.",
        "The two labels crossfade in one grid cell; the hidden one is aria-hidden, so the button's name is always just the current label.",
        "Links or buttons inside the clamped part open the text when they receive keyboard focus, so focus is never hidden."
      ],
      "motion": [
        "Height, the fade mask, and the chevron all follow one progress value, so they always move in step and a click mid-way reverses from the current height.",
        "Opening takes 500 to 600ms and closing 400 to 450ms, growing gently with the distance, on a soft ease out (0.32, 0.5, 0.2, 1) that answers at once and spends most of its time settling. It never overshoots.",
        "The fade over the last line dissolves as the text opens and only returns as it nears the clamp. While moving, newly revealed lines rise through the soft edge instead of being uncovered by a hard line.",
        "Read more and Show less crossfade with a short drift and a brief blur, without changing the button's width.",
        "Closing from far down the page scrolls the start of the text back into view together with the collapse.",
        "At rest the clamp is plain CSS (max-height in lh units), so width and font changes need no JavaScript. With reduced motion the text opens and closes at once."
      ],
      "responsive": [
        "The clamp is measured in lines, so narrower screens show the same number of lines with fewer words; the toggle appears or disappears as wrapping changes.",
        "The toggle sits under the text, left aligned, with a 28px tall hit area that works on touch."
      ],
      "performance": [
        "One ResizeObserver per instance, watching the text and a zero width probe that is exactly the clamp height.",
        "Only height and a mask gradient change while moving; at rest there is no inline height and, once open, no mask.",
        "The server renders the toggle, assuming long text; short text drops it on the first measurement."
      ],
      "aiNotes": [
        "Use it for reviews, product descriptions, bios, comments, and release notes where the first lines are enough for most readers.",
        "Pick lines by context: 2 in dense lists such as reviews, 3 to 4 for a product or profile description.",
        "Keep one line height inside the text. Headings or large images inside break the line based clamp; use expandable-card or accordion for structured content.",
        "Nothing needs measuring by the caller: a ResizeObserver decides whether the toggle is shown and keeps that correct as the layout changes."
      ],
      "related": [
        "expandable-card",
        "accordion",
        "scroll-area"
      ],
      "usage": "import { ExpandableText } from \"@/components/arc/expandable-text/expandable-text\";\n\nexport function ReviewBody({ review }: { review: Review }) {\n  return (\n    <ExpandableText lines={2}>\n      {review.paragraphs.map(paragraph => <p key={paragraph}>{paragraph}</p>)}\n    </ExpandableText>\n  );\n}",
      "exports": [
        "ExpandableText"
      ]
    },
    {
      "name": "emoji-picker",
      "kind": "component",
      "title": "Emoji picker",
      "description": "A full emoji picker with instant search, scroll-synced categories, skin tones, recently used emoji, and a virtualized, keyboard-navigable grid.",
      "tier": "pro",
      "category": "Inputs",
      "tags": [
        "input",
        "picker",
        "emoji",
        "new"
      ],
      "docs": "https://uiarc.dev/components/emoji-picker",
      "markdown": "https://uiarc.dev/components/emoji-picker/markdown",
      "registry": null,
      "keywords": [
        "emoji",
        "emoji picker",
        "emoji keyboard",
        "smiley",
        "emoticon",
        "skin tone",
        "recent emoji",
        "frequently used",
        "chat",
        "composer",
        "insert emoji",
        "reaction picker"
      ],
      "whenToUse": [
        "Message composers, comment boxes, and chat inputs.",
        "Choosing an emoji for a status, a project, a page icon, or a label.",
        "Any text field where people expect the full emoji set with search."
      ],
      "whenNotToUse": [
        "Use reactions for a small, fixed set of quick responses on a post or comment.",
        "Use combobox or a select when the choices are a short, known list.",
        "Do not use it as an icon picker; emoji render differently on every platform."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The grid is role=\"grid\" with rows and gridcells named after each emoji; section names are column headers. The active cell is exposed with aria-activedescendant from the search field while searching, and from the grid while it has focus, so the active emoji is announced as it moves.",
        "Only the rows in view are rendered, plus the active row, so aria-activedescendant always points at a real element. Rows carry aria-rowindex and the grid aria-rowcount.",
        "The number of results is announced in a polite live region while searching, including when nothing matches.",
        "The category bar is a toolbar with one tab stop; the category in view is marked with aria-current. The tone control is a button with aria-expanded that opens a radiogroup of six named tones.",
        "Escape is handled on window in the capture phase and marked as handled only when it closes the tone menu or clears the search, so the first Escape never closes a surrounding Radix popover by accident.",
        "The preview row repeats the active emoji's name for sighted users and is hidden from assistive technology."
      ],
      "motion": [
        "The category indicator slides to the category under the top edge on the morph spring as you scroll. When a tab is pressed it goes straight there and waits while the list scrolls smoothly to the category, instead of stepping through every category in between.",
        "The skin tone menu unrolls leftward out of the tone button on a clip-path inset, and rolls back into it; the inset is a CSS transition, so a quick close reverses mid-way.",
        "The preview emoji settles in from a slight scale on each change; a pressed emoji dips and springs back.",
        "Search results appear instantly, with no animation, so typing always feels immediate.",
        "With reduced motion the indicator and tone menu move at once, scrolling to a category jumps, and the preview does not scale."
      ],
      "responsive": [
        "The grid picks its column count from the width, keeping cells at least 36px, so it goes from 9 columns at 352px to 7 in a narrow phone popover.",
        "The default size is 352 by 408px and it never exceeds its container; set --emoji-picker-width and --emoji-picker-height to change it.",
        "Hover previews only follow a mouse pointer; on touch a tap picks the emoji directly."
      ],
      "performance": [
        "The dataset is about 73 kB of compact strings (1,870 emoji with names, keywords, and skin tone positions) parsed once on first use, then shared by every picker on the page.",
        "The grid is virtualized: only the rows in view plus a small overscan are rendered, so scrolling the whole set stays light.",
        "Search scans precomputed lowercase strings and is memoized per query.",
        "Skin tones are applied when a cell renders, so there is no second dataset for toned variants."
      ],
      "aiNotes": [
        "Put it in a popover next to the input it fills. Remember the input's selection and insert at it, then move the caret after the emoji; see the composer demo.",
        "The picker has no border, background, or padding of its own. Give the popover padding: 0 so the grid runs edge to edge.",
        "Keep the popover open when Shift is held for several picks in a row, and close it otherwise.",
        "The dataset covers Emoji 15.0, which renders on current Apple, Google, and Microsoft platforms. Older systems may show a few newer emoji as boxes.",
        "Use onSkinToneChange with skinTone to sync the tone with a user profile; otherwise it is remembered in localStorage with the recent emoji."
      ],
      "related": [
        "reactions",
        "popover",
        "combobox",
        "textarea",
        "mention-input",
        "rich-text-editor"
      ],
      "usage": "import { EmojiPicker } from \"@/components/arc/emoji-picker/emoji-picker\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/arc/popover/popover\";\n\nexport function EmojiButton({ onInsert }: { onInsert: (emoji: string) => void }) {\n  const [open, setOpen] = useState(false);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger aria-label=\"Add emoji\"><SmilePlus /></PopoverTrigger>\n      <PopoverContent side=\"top\" style={{ padding: 0, width: \"min(22rem, calc(100vw - 20px))\" }}>\n        <EmojiPicker\n          onSelect={(selection, event) => {\n            onInsert(selection.emoji);\n            if (!event.shiftKey) setOpen(false);\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}",
      "exports": [
        "EmojiPicker"
      ]
    },
    {
      "name": "release-readiness",
      "kind": "block",
      "title": "Release readiness",
      "description": "A launch checklist that keeps prerequisites, progress, and the final action together.",
      "tier": "pro",
      "builtFrom": [
        "Checkbox",
        "Progress",
        "Switch",
        "Button",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/release-readiness",
      "registry": null,
      "keywords": [
        "react release checklist",
        "launch readiness",
        "pre release checklist",
        "deploy gate",
        "checklist with progress bar",
        "handoff checklist"
      ],
      "whenToUse": [
        "A pre-launch or handoff gate where several checks must be done before one action unlocks.",
        "Release or launch checklists with a readiness bar and owners.",
        "Handoffs that should optionally notify people when the action runs."
      ],
      "whenNotToUse": [
        "Use onboarding-checklist for a user's own getting-started tasks."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The completed count lives in an aria-live=\"polite\" region.",
        "Success is announced with role=\"status\" and failures with role=\"alert\".",
        "Checks use the library Checkbox, so each has a real label."
      ],
      "motion": [
        "The completed count swaps with a 5px rise and fade.",
        "Reduced motion sets the transition duration to zero."
      ],
      "responsive": [
        "Below 700px the summary narrows to a tighter two-column header.",
        "Below 520px the summary and the two-column task grid collapse to one column, and actions stack."
      ],
      "performance": [
        "Four checkboxes and one progress bar; state updates are local and cheap.",
        "The task list is a plain array with no virtualization, which is fine for a checklist of this size."
      ],
      "aiNotes": [
        "Use for a pre-launch or handoff gate where several conditions must be true before one action.",
        "Replace the tasks array, initialChecks, and the hard-coded release name and avatars.",
        "Wire onRelease to your deploy or handoff call.",
        "Composes Arc Avatar, Button, Checkbox, Progress, and Switch."
      ],
      "related": [
        "onboarding-checklist",
        "progress"
      ],
      "usage": "import { ReleaseReadiness } from \"@/components/arc/blocks/release-readiness/release-readiness\";\n\nexport function ReleasePanel() {\n  return <ReleaseReadiness onRelease={() => startRelease(\"2.4\")} />;\n}",
      "exports": [
        "ReleaseReadiness"
      ]
    },
    {
      "name": "signup-form",
      "kind": "block",
      "title": "Sign up form",
      "description": "An account creation flow with field validation, password strength, and a clear completion state.",
      "tier": "free",
      "builtFrom": [
        "Input",
        "Password field",
        "Checkbox",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/signup-form",
      "registry": "https://uiarc.dev/r/signup-form.json",
      "keywords": [
        "react signup form",
        "sign up page",
        "registration form",
        "create account form",
        "password strength form",
        "email password signup"
      ],
      "whenToUse": [
        "The client side of an email and password sign-up.",
        "Sign-up pages that should show password strength and a product updates opt-in.",
        "Flows that want an in-place success panel after the account is created."
      ],
      "whenNotToUse": [
        "Use sign-in or login-split for returning users.",
        "Use password-field alone when the form is already built.",
        "Use otp-input for passwordless or code-based sign-up."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Invalid fields set aria-invalid and point aria-describedby at their message.",
        "Field and submit errors use role=\"alert\"; the success panel uses role=\"status\".",
        "Provider buttons have labels like \"Sign up with Google\"; while one is busy the others are aria-disabled.",
        "Password strength is shown as text, not only color."
      ],
      "motion": [
        "The card height springs between the form and the completion panel, which slides in while the form exits.",
        "The completion ring and check draw themselves in with pathLength.",
        "Reduced motion drops the offsets and blur and uses zero durations."
      ],
      "responsive": [
        "Container query: below 380px of card width the padding, heading, and provider button text shrink so fields keep their width.",
        "Below 480px viewport width the card uses the smaller panel radius."
      ],
      "performance": [
        "Validation is local and synchronous; the card height is a motion value measured with a layout effect, so swapping steps does not reflow the page.",
        "Strength bars animate with CSS transitions, turned off under reduced motion."
      ],
      "aiNotes": [
        "Use as the client side of an email and password sign-up with optional social providers.",
        "Wire onProvider to your auth library's OAuth sign-in; the built-in provider flow is a simulation.",
        "Validation is local; keep server checks in onSubmit and pass their message through serverError.",
        "Composes Arc Button, Checkbox, Input, and PasswordField."
      ],
      "related": [
        "sign-in",
        "login-split",
        "password-field",
        "password-strength",
        "otp-input"
      ],
      "usage": "import { SignupForm, type SignupDetails } from \"@/components/arc/blocks/signup-form/signup-form\";\n\nexport function SignUpPage() {\n  const [error, setError] = useState<string | null>(null);\n  async function create(details: SignupDetails) {\n    const res = await fetch(\"/api/sign-up\", { method: \"POST\", body: JSON.stringify(details) });\n    if (!res.ok) setError(\"That email is already registered.\");\n  }\n  return <SignupForm onSubmit={create} serverError={error} onProvider={provider => signIn(provider.toLowerCase())} />;\n}",
      "exports": [
        "SignupForm",
        "SignupDetails"
      ]
    },
    {
      "name": "wallet-card",
      "kind": "block",
      "title": "Wallet card",
      "description": "A compact account view with balance, recent activity, and focused money actions.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Input",
        "Number field"
      ],
      "docs": "https://uiarc.dev/components/blocks/wallet-card",
      "registry": null,
      "keywords": [
        "react wallet card",
        "crypto wallet ui",
        "balance card",
        "fintech dashboard card",
        "send money ui",
        "account balance chart",
        "banking app component"
      ],
      "whenToUse": [
        "A personal or business balance summary with quick send, deposit, swap, and buy actions.",
        "Dashboards that pair a balance with a small history chart and recent activity.",
        "Apps with a main and a reserve account shown in one card."
      ],
      "whenNotToUse": [
        "Use billing-overview for subscription billing and invoices.",
        "Use wallet-stack for a stack of payment cards.",
        "Use checkout-flow for paying for an order."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The balance is an aria-live region; hidden balances read as \"Balance hidden\".",
        "Action buttons use aria-pressed and aria-controls; focus returns to the action after a submit.",
        "Amount errors use role=\"alert\" and aria-describedby on the input."
      ],
      "motion": [
        "Balances count to the new value over about 0.75s, and the card resizes on a gentle spring.",
        "Panels swap with a short rise and blur; new activity rows expand in place.",
        "Reduced motion sets values instantly and disables layout animation."
      ],
      "responsive": [
        "The card is a container query root capped at 660px, so it adapts to its slot, not the viewport.",
        "At 420px container width padding tightens and action arrows hide; at 280px the wallet switcher drops its label and the footer text shrinks.",
        "Chart hover scrubbing is mouse only; touch and keyboard use the range input over the chart."
      ],
      "performance": [
        "Balances tween through a motion value over about 0.75s instead of re-rendering per frame.",
        "Activity search filters in memory and shows three rows until expanded; the list is not virtualized."
      ],
      "aiNotes": [
        "Use for a personal or business balance summary with quick money actions.",
        "Every action is simulated. Replace initialEntries, balances, and submit() with calls to your ledger, and never trust client amounts.",
        "Composes Arc Button, DropdownMenu, Popover, and SegmentedControl."
      ],
      "related": [
        "billing-overview",
        "wallet-stack",
        "animated-counter"
      ],
      "usage": "import { WalletCard } from \"@/components/arc/blocks/wallet-card/wallet-card\";\n\nexport function AccountSummary() {\n  return <WalletCard initialBalanceCents={482_150} />;\n}",
      "exports": [
        "WalletCard"
      ]
    },
    {
      "name": "logo-marquee",
      "kind": "block",
      "title": "Logo marquee",
      "description": "A quiet, continuously moving row of brand marks with a pause control.",
      "tier": "free",
      "builtFrom": [
        "Motion",
        "Button",
        "Brand marks"
      ],
      "docs": "https://uiarc.dev/components/blocks/logo-marquee",
      "registry": "https://uiarc.dev/r/logo-marquee.json",
      "keywords": [
        "react logo marquee",
        "logo cloud",
        "infinite logo scroll",
        "trusted by logos",
        "customer logos strip",
        "css marquee"
      ],
      "whenToUse": [
        "A customer or integration logo strip on a landing page.",
        "Social proof sections with a heading and a short supporting line."
      ],
      "whenNotToUse": [
        "Use integrations when each logo needs a description or a connect action.",
        "Use testimonial-stage when quotes matter more than logos.",
        "Use carousel when people should browse items by hand."
      ],
      "dependencies": [
        "lucide-react"
      ],
      "accessibility": [
        "The duplicated loop copy is aria-hidden, so each brand is read once.",
        "The pause button uses aria-pressed and a live region announces the motion state."
      ],
      "motion": [
        "A CSS keyframe loop scrolls two copies of the list for a seamless track.",
        "Under prefers-reduced-motion the animation stops and the duplicate list is hidden."
      ],
      "responsive": [
        "Below 700px the heading and description stack and the marks shrink.",
        "The track fades at both edges with a mask, so logos never hard-cut at narrow widths."
      ],
      "performance": [
        "The loop is a single CSS transform keyframe on one track, with no JavaScript animation.",
        "Two copies of the list are rendered for a seamless loop; under reduced motion the animation stops and the copy is hidden.",
        "Logos are CSS masks from SVG URLs, so use small single-color SVGs."
      ],
      "aiNotes": [
        "Use for a customer or integration logo strip on a landing page.",
        "Only pass brands you have permission to show; the defaults are illustrative.",
        "Composes Arc Button for the pause control."
      ],
      "related": [
        "testimonial-stage",
        "integrations",
        "carousel"
      ],
      "usage": "import { LogoMarquee } from \"@/components/arc/blocks/logo-marquee/logo-marquee\";\n\nexport function Customers() {\n  return (\n    <LogoMarquee\n      title=\"Trusted by product teams\"\n      description=\"Teams of every size ship with us.\"\n      brands={[{ name: \"Acme\", icon: \"/logos/acme.svg\" }, { name: \"Globex\", icon: \"/logos/globex.svg\" }]}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Integration logos",
          "code": "<LogoMarquee\n  title=\"Works with your stack\"\n  description=\"Connect the tools your team already uses.\"\n  brands={[\n    { name: \"Linear\", icon: \"/logos/linear.svg\" },\n    { name: \"Figma\", icon: \"/logos/figma.svg\" },\n    { name: \"Notion\", icon: \"/logos/notion.svg\" },\n  ]}\n/>"
        }
      ],
      "exports": [
        "LogoMarquee",
        "LogoMarqueeBrand"
      ]
    },
    {
      "name": "team-directory",
      "kind": "block",
      "title": "Team directory",
      "description": "Searchable people, team filters, and a focused profile panel.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Avatar",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/team-directory",
      "registry": null,
      "keywords": [
        "react team directory",
        "employee directory",
        "people search",
        "staff directory ui",
        "company directory",
        "team list with profile"
      ],
      "whenToUse": [
        "An internal directory where people search colleagues and copy contact details.",
        "Team pages with department filters and a profile pane."
      ],
      "whenNotToUse": [
        "Use team-members to manage roles and invitations.",
        "Use team-showcase for a public, image-led team section.",
        "Use customers-table for large tabular people data."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Filter buttons are a labelled group with aria-pressed; person rows are buttons with aria-pressed.",
        "Escape in the search field clears the query.",
        "The profile pane is polite live content, so selection changes are announced.",
        "Copy success and failure are announced through role=\"status\"."
      ],
      "motion": [
        "The filter highlight and the row selection highlight glide with shared layoutIds on the morph spring.",
        "Filtered rows close their gaps with position layout animation and fade in or out.",
        "The profile pane fades up 4px when the person changes.",
        "Reduced motion removes layout animation and offsets and turns off CSS transitions."
      ],
      "responsive": [
        "Container queries on the block: below 640px the copy button becomes icon-only; below 500px the profile pane moves under the list; below 400px the header stacks, search fills the width, and filter counts hide."
      ],
      "performance": [
        "Search and filters run in memory on every keystroke, and rows are not virtualized.",
        "For hundreds of people, filter on the server and paginate before passing data in."
      ],
      "aiNotes": [
        "Use for an internal directory or team page where people need contact details.",
        "Replace the people array, the Team union, and portrait paths with your directory data.",
        "Composes Arc Avatar."
      ],
      "related": [
        "team-members",
        "team-showcase",
        "search-field",
        "avatar"
      ],
      "usage": "import { TeamDirectory } from \"@/components/arc/blocks/team-directory/team-directory\";\n\nexport function PeoplePage() {\n  const router = useRouter();\n  return <TeamDirectory onPersonSelect={(person) => router.push(\"/people/\" + person.id)} />;\n}",
      "exports": [
        "TeamDirectory"
      ]
    },
    {
      "name": "project-board",
      "kind": "block",
      "title": "Project board",
      "description": "A living workflow board for moving work between stages.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Avatar",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/project-board",
      "registry": null,
      "keywords": [
        "react kanban board",
        "project board",
        "task board",
        "kanban component",
        "accessible kanban",
        "trello style board"
      ],
      "whenToUse": [
        "A compact kanban with a handful of tasks across fixed stages.",
        "Boards where moving tasks should be accessible through explicit buttons, not drag only."
      ],
      "whenNotToUse": [
        "Use reorderable-list when order within one list matters more than stages.",
        "Use timeline for a dated sequence of events.",
        "Add drag and drop and virtualization, or use a dedicated board library, for large boards."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each card has explicit buttons labeled \"Move <task> to <stage>\" instead of drag only.",
        "Moves are announced in a visually hidden role=\"status\" region.",
        "Each column is a section labelled \"<stage>, <n> tasks\"; the filter buttons use aria-pressed."
      ],
      "motion": [
        "Cards travel between columns with a per-card layoutId on the gentle spring; new cards scale in from 0.98.",
        "The filter highlight glides between All work and My work, and the progress bar grows with scaleX.",
        "Reduced motion drops the layoutId and springs, so cards and bars change in place."
      ],
      "responsive": [
        "The four columns need 820px; narrower boards scroll horizontally with proximity snap to each column.",
        "Below 700px each column is up to 78vw wide, team avatars hide, and padding tightens; below 400px the add button becomes icon only.",
        "Moves use buttons, so touch never needs a drag gesture."
      ],
      "performance": [
        "Cards move between columns with a per-card layoutId, which measures layout on each move.",
        "No virtualization; keep boards to dozens of cards rather than hundreds."
      ],
      "aiNotes": [
        "Use for a compact kanban of a few tasks. For large boards add virtualization and drag and drop.",
        "Replace initialTasks, the stages list, and the owner portraits; addTask cycles through sample draft titles.",
        "Owner portraits render with next/image from lib/media.",
        "Wire onTaskMove to persist the stage change."
      ],
      "related": [
        "reorderable-list",
        "workspace-sidebar",
        "avatar-group",
        "filter-toolbar"
      ],
      "usage": "import { ProjectBoard } from \"@/components/arc/blocks/project-board/project-board\";\n\nexport function Board() {\n  return (\n    <ProjectBoard\n      onTaskMove={(task) => updateTask(task.id, { stage: task.stage })}\n    />\n  );\n}",
      "exports": [
        "ProjectBoard"
      ]
    },
    {
      "name": "invoice-studio",
      "kind": "block",
      "title": "Invoice studio",
      "description": "An editable invoice with live totals and a clear save flow.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/invoice-studio",
      "registry": null,
      "keywords": [
        "react invoice editor",
        "invoice generator ui",
        "line items form",
        "quote builder",
        "invoice form with totals",
        "billing invoice component"
      ],
      "whenToUse": [
        "Creating or editing an invoice or quote with line items.",
        "Forms that need a live subtotal and total as quantities and rates change."
      ],
      "whenNotToUse": [
        "Use billing-overview to show past invoices and plan status.",
        "Use checkout-flow when the customer is paying.",
        "Use sortable-data-table for read-only lists of invoices."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The total is an output element with aria-live and aria-atomic.",
        "Save progress and validation messages are announced via role=\"status\".",
        "Row inputs and remove buttons carry per-line aria-labels."
      ],
      "motion": [
        "Totals roll with a short vertical slide and blur when they change.",
        "Added and removed lines animate height via layout.",
        "Reduced motion uses opacity-only swaps with zero duration."
      ],
      "responsive": [
        "Below 760px line columns narrow; below 620px the column header hides and each line becomes a stacked card with its own labels.",
        "Below 380px the from and to parties stack into one column."
      ],
      "performance": [
        "Totals recompute on each edit and roll with a short slide; line add and remove animate height with layout.",
        "Lines are not virtualized, which suits invoices with a few dozen lines."
      ],
      "aiNotes": [
        "Use for creating or editing an invoice or quote.",
        "Replace startingLines and the client header; totals are USD only.",
        "Recompute totals on the server; treat onSave data as a draft."
      ],
      "related": [
        "billing-overview",
        "number-field",
        "sortable-data-table"
      ],
      "usage": "import { InvoiceStudio, type InvoiceLine } from \"@/components/arc/blocks/invoice-studio/invoice-studio\";\n\nexport function NewInvoice() {\n  async function save(lines: InvoiceLine[]) {\n    await fetch(\"/api/invoices/draft\", { method: \"POST\", body: JSON.stringify({ lines }) });\n  }\n  return <InvoiceStudio onSave={save} />;\n}",
      "exports": [
        "InvoiceStudio",
        "InvoiceLine"
      ]
    },
    {
      "name": "plan-comparison",
      "kind": "block",
      "title": "Plan comparison",
      "description": "Compare meaningful differences between plans and billing periods.",
      "tier": "free",
      "builtFrom": [
        "Segmented control",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/plan-comparison",
      "registry": "https://uiarc.dev/r/plan-comparison.json",
      "keywords": [
        "react pricing comparison",
        "plan comparison table",
        "pricing table",
        "monthly yearly toggle",
        "feature matrix",
        "saas pricing page"
      ],
      "whenToUse": [
        "Comparing two tiers side by side with a feature matrix.",
        "Pricing pages that switch between monthly and yearly prices.",
        "Long feature lists where people want to see only the differences."
      ],
      "whenNotToUse": [
        "Use usage-pricing when price depends on seats or volume.",
        "Use segmented-control alone for a simple billing toggle.",
        "Use cancel-flow for downgrade or cancellation decisions."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The matrix uses table, row, columnheader, rowheader, and cell roles.",
        "Prices are aria-live with aria-atomic so the full value is read.",
        "The chosen plan is announced in a role=\"status\" footer."
      ],
      "motion": [
        "Prices count between billing periods and the selection highlight moves with layoutId.",
        "Extra feature rows expand in height with a light blur.",
        "Reduced motion jumps to the final values and disables height animation."
      ],
      "responsive": [
        "Below 640px the feature header column hides and each feature name spans the row above its two values.",
        "Below 365px plan and value padding tighten so both plans still fit side by side."
      ],
      "performance": [
        "Prices tween with a single animate call per value; the selection highlight uses one layoutId.",
        "Showing extra rows animates height, so keep the matrix to a few dozen features."
      ],
      "aiNotes": [
        "Use when people compare two tiers side by side.",
        "Edit the pricing and features constants; there is no selection callback, so wire the Button onClick yourself.",
        "Composes Arc Button, SegmentedControl, and Switch."
      ],
      "related": [
        "usage-pricing",
        "segmented-control",
        "animated-counter",
        "cancel-flow"
      ],
      "usage": "import { PlanComparison } from \"@/components/arc/blocks/plan-comparison/plan-comparison\";\n\nexport function PricingPage() {\n  return <PlanComparison />;\n}",
      "exports": [
        "PlanComparison"
      ]
    },
    {
      "name": "availability-picker",
      "kind": "block",
      "title": "Availability picker",
      "description": "Find a meeting time across people and see the choice come together.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Calendar",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/availability-picker",
      "registry": null,
      "keywords": [
        "react availability picker",
        "meeting time picker",
        "group scheduling",
        "calendly style picker",
        "time slot picker",
        "book a meeting ui"
      ],
      "whenToUse": [
        "Picking a meeting time that works for several attendees.",
        "Booking flows that show a few days and open slots with a confirmation step."
      ],
      "whenNotToUse": [
        "Use calendar or date-picker to pick a single date.",
        "Use time-picker for a free time input.",
        "Use event-invite to show and respond to an invite."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Days and times are labeled groups of buttons with aria-pressed.",
        "The selected day and the final selection are aria-live regions."
      ],
      "motion": [
        "Time slots slide in from the direction of the day change.",
        "The selected slot highlight moves with a shared layoutId on a responsive spring.",
        "Reduced motion removes the horizontal offset and uses zero durations."
      ],
      "responsive": [
        "The five days sit in a fixed five-column grid and time slots in two columns at every width.",
        "Below 700px attendee names hide; below 490px day cards shrink and the heading icon and date header hide."
      ],
      "performance": [
        "Slot lists swap with AnimatePresence on day change; there are no timers or observers.",
        "Load availability before render, since the block does no fetching."
      ],
      "aiNotes": [
        "Use for picking a meeting time across several people.",
        "Replace the days, slots, and attendees with availability from your calendar service; times are labeled Zürich time.",
        "onConfirm receives display strings, so map them back to real timestamps."
      ],
      "related": [
        "calendar",
        "time-picker",
        "date-picker",
        "booking-pill"
      ],
      "usage": "import { AvailabilityPicker } from \"@/components/arc/blocks/availability-picker/availability-picker\";\n\nexport function Schedule() {\n  return (\n    <AvailabilityPicker\n      onConfirm={({ date, time }) => bookMeeting({ date, time })}\n    />\n  );\n}",
      "exports": [
        "AvailabilityPicker"
      ]
    },
    {
      "name": "metrics-dashboard",
      "kind": "block",
      "title": "Metrics dashboard",
      "description": "A dense analytics view with KPI tabs, one inspectable chart, and top pages and sources.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Charts",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/metrics-dashboard",
      "registry": null,
      "keywords": [
        "react metrics dashboard",
        "analytics dashboard",
        "kpi dashboard",
        "dashboard with chart",
        "saas analytics ui",
        "traffic dashboard"
      ],
      "whenToUse": [
        "A small, explorable KPI overview with a range toggle and one chart.",
        "Product or marketing dashboards with four headline metrics, a 7, 30, or 90 day range, and top pages and sources."
      ],
      "whenNotToUse": [
        "Use metric-explorer or revenue-explorer for larger or drill-down data.",
        "Use stat-card for a single number.",
        "Use sparkline for inline trends in a table."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Range buttons use aria-pressed; the KPI tiles are tabs with aria-selected.",
        "The chart is role=\"img\" with a label; the inspected value is announced through a status tooltip.",
        "Deltas carry a text direction, not only color."
      ],
      "motion": [
        "KPI values and list counts count to their new amounts with tabular numerals.",
        "The range thumb and KPI underline slide on a spring; the chart crossfades between metrics and ranges.",
        "Reduced motion swaps values and views immediately."
      ],
      "responsive": [
        "Layout follows the block's own width through a ResizeObserver: below 640px the KPI tabs form a two by two grid and the top pages and sources lists stack.",
        "Below 460px padding tightens, the range control and actions fill the width, KPI values shrink, and the chart is 168px tall.",
        "Chart hover is mouse based; touch and keyboard select points through focus and the arrow keys."
      ],
      "performance": [
        "The chart is a hand-built SVG with a monotone cubic path: up to 30 daily points, or 13 weekly points for 90 days. No charting library.",
        "All aggregation runs over an in-memory daily series on each change; for larger data or drill-down use metric-explorer."
      ],
      "aiNotes": [
        "Use for a compact product or marketing analytics overview.",
        "Replace the generated daily series with your analytics source; Export only shows a local confirmation.",
        "The layout follows the block's own width (measured), so it works in narrow panels as well as full pages.",
        "For larger sets, prefer metric-explorer or revenue-explorer."
      ],
      "related": [
        "metric-explorer",
        "revenue-explorer",
        "sparkline",
        "bar-chart"
      ],
      "usage": "import { MetricsDashboard } from \"@/components/arc/blocks/metrics-dashboard/metrics-dashboard\";\n\nexport function Overview() {\n  return <MetricsDashboard />;\n}",
      "exports": [
        "MetricsDashboard"
      ]
    },
    {
      "name": "media-player",
      "kind": "block",
      "title": "Media player",
      "description": "An audio player with a living playlist and complete playback controls.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Progress",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/media-player",
      "registry": null,
      "keywords": [
        "react audio player",
        "music player component",
        "podcast player",
        "playlist player",
        "custom audio controls",
        "media player ui"
      ],
      "whenToUse": [
        "A small audio collection such as a podcast, sample pack, or playlist.",
        "Pages that need a full player with seek, skip, volume, and a track list."
      ],
      "whenNotToUse": [
        "Use now-playing for a compact now playing widget.",
        "Use transcript-studio when the audio needs a synced transcript.",
        "Use voice-recorder for recording audio."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Uses a native audio element with range inputs for seek and volume.",
        "Transport and playlist buttons have labels such as \"Play <title>\" and \"Next track\"; the current track has aria-current.",
        "Seek and volume ranges carry aria-valuetext such as \"0:12 of 0:24\" and \"70%\". The mute button's label switches between Mute and Unmute audio."
      ],
      "motion": [
        "The selection rail glides to the current track with a shared layoutId on the gentle spring; playing bars pulse beside it.",
        "The cover crossfades and the title rises in when the track changes; the play icon swaps with a short scale and blur.",
        "Reduced motion removes the glides, swaps, and bar animation."
      ],
      "responsive": [
        "Below 420px viewport width the padding tightens, the cover takes a 1.7 ratio, transport buttons shrink, and the volume slider narrows to 64px.",
        "Seek and volume are native range inputs, so touch and keyboard work without extra handling."
      ],
      "performance": [
        "Uses a native audio element; position updates come from timeupdate, not a frame loop.",
        "The demo tracks are generated as WAV blobs in the browser; replace them with real audio URLs in production."
      ],
      "aiNotes": [
        "Use for a small audio collection such as a podcast, a sample pack, or a playlist.",
        "Replace the tracks array and renderTrack() with real audio URLs set on the audio element, and the cover ids with your images.",
        "The default export is MediaPlayerBlock."
      ],
      "related": [
        "now-playing",
        "slider",
        "voice-recorder"
      ],
      "usage": "import { MediaPlayerBlock } from \"@/components/arc/blocks/media-player/media-player\";\n\nexport function Listen() {\n  return <MediaPlayerBlock />;\n}",
      "exports": [
        "MediaPlayerBlock"
      ]
    },
    {
      "name": "support-conversation",
      "kind": "block",
      "title": "Support conversation",
      "description": "A responsive message thread with useful quick replies and live feedback.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Button",
        "Avatar",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/support-conversation",
      "registry": null,
      "keywords": [
        "react support chat",
        "chat thread ui",
        "customer support conversation",
        "help desk chat",
        "chat with typing indicator",
        "ticket conversation"
      ],
      "whenToUse": [
        "A support chat or ticket thread with quick replies and attachments.",
        "Help flows that should show a typing indicator and a resolve toggle."
      ],
      "whenNotToUse": [
        "Use support-widget for a floating help launcher.",
        "Use ai-composer for a prompt input without a thread.",
        "Use inbox-triage to handle many conversations at once."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "New messages are announced through a polite live region.",
        "The send, attach, and typing indicator elements have aria-labels.",
        "The file input only accepts PDF."
      ],
      "motion": [
        "Messages rise into the thread on a gentle spring and existing messages shift via layout.",
        "Reduced motion removes the offsets and uses instant transitions."
      ],
      "responsive": [
        "The thread scrolls within a 350 to 450px height and centers messages up to 590px wide.",
        "Below 650px and again below 390px the header and composer padding tighten and the portrait shrinks."
      ],
      "performance": [
        "Every message stays mounted and the thread is not virtualized, so load long histories in pages.",
        "New messages smooth-scroll the thread to the bottom; replies are simulated with timers you should replace."
      ],
      "aiNotes": [
        "Use as a chat or ticket thread shell.",
        "Replace replyFor and scheduleReply with messages from your support backend, and initialMessages with the real history.",
        "onSend fires before the simulated reply; the component still keeps its own message list."
      ],
      "related": [
        "support-widget",
        "ai-composer",
        "inbox-triage",
        "textarea"
      ],
      "usage": "import { SupportConversation } from \"@/components/arc/blocks/support-conversation/support-conversation\";\n\nexport function Ticket({ ticketId }: { ticketId: string }) {\n  return (\n    <SupportConversation\n      onSend={(message) => postMessage(ticketId, message)}\n      onResolve={(resolved) => setTicketResolved(ticketId, resolved)}\n    />\n  );\n}",
      "exports": [
        "SupportConversation"
      ]
    },
    {
      "name": "inbox-triage",
      "kind": "block",
      "title": "Inbox triage",
      "description": "Process a focused inbox with archive, snooze, and restore actions.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/inbox-triage",
      "registry": null,
      "keywords": [
        "react inbox",
        "email triage ui",
        "inbox component",
        "archive snooze inbox",
        "message list",
        "mail app ui"
      ],
      "whenToUse": [
        "A queue of messages or requests that need archive, snooze, or restore decisions.",
        "Inboxes where rows expand in place to show details and actions."
      ],
      "whenNotToUse": [
        "Use inbox-sidebar for mail navigation with folders and counts.",
        "Use notification-center for system notifications.",
        "Use swipe-actions for touch-first lists with gesture actions."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Folders are a tablist with arrow key navigation; the list is its tabpanel.",
        "Message rows toggle with aria-expanded and aria-controls and declare their shortcuts with aria-keyshortcuts.",
        "After a message leaves, focus moves to the next row, or to the heading when the list is empty; undo returns focus to the restored row.",
        "Moves are announced in a polite role=\"status\" region; the Unread filter uses aria-pressed."
      ],
      "motion": [
        "Rows and message details open and collapse with height and opacity on the smooth spring.",
        "The folder indicator glides between tabs with a shared layoutId; the status line swaps with a short rise.",
        "Reduced motion removes height and travel and keeps plain fades."
      ],
      "responsive": [
        "Container queries: below 560px rows switch to a two-line layout with a 32px avatar, subject and preview under the sender, and a short hint in the footer.",
        "Below 360px the unread count hides, tabs tighten, and detail text drops its avatar indent."
      ],
      "performance": [
        "Rows collapse with height animation; the list is not virtualized.",
        "Focus moves to the next row after an action on the next frame, without extra timers."
      ],
      "aiNotes": [
        "Use when a queue of messages or requests needs clear next actions.",
        "No callbacks are exposed; replace initialMessages and the move() and undo() functions with calls to your message service.",
        "Composes Arc Avatar and Button."
      ],
      "related": [
        "inbox-sidebar",
        "notification-center",
        "swipe-actions",
        "support-conversation"
      ],
      "usage": "import { InboxTriage } from \"@/components/arc/blocks/inbox-triage/inbox-triage\";\n\nexport function Messages() {\n  return <InboxTriage />;\n}",
      "exports": [
        "InboxTriage"
      ]
    },
    {
      "name": "command-palette",
      "kind": "block",
      "title": "Command palette",
      "description": "A complete keyboard driven action surface with search, grouped results, and shortcuts.",
      "tier": "free",
      "builtFrom": [
        "Search",
        "Keyboard navigation",
        "Action list"
      ],
      "docs": "https://uiarc.dev/components/blocks/command-palette",
      "registry": "https://uiarc.dev/r/command-palette.json",
      "keywords": [
        "react command palette",
        "cmd k menu",
        "command menu",
        "spotlight search",
        "cmdk alternative",
        "keyboard command palette",
        "quick actions"
      ],
      "whenToUse": [
        "App-wide command search opened with Cmd or Ctrl plus K.",
        "Jumping to pages, creating items, and running actions from one input.",
        "Inline search panels that group results with shortcuts."
      ],
      "whenNotToUse": [
        "Use combobox to pick a value inside a form.",
        "Use dropdown-menu for a short list of actions.",
        "Use expanding-search or search-field to search page content."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The input is a combobox with aria-controls and aria-activedescendant pointing at the listbox option, so focus never leaves the input.",
        "Results are role=option buttons with aria-selected, grouped in labelled role=group containers.",
        "A polite live region announces the result count, or No results, as the query changes.",
        "It renders inline, not in a modal. Put it in a dialog yourself if it should trap focus and close on outside click."
      ],
      "motion": [
        "The panel rises in on a spring; the results frame springs to the list height as the query filters.",
        "Remaining rows glide into place when the shift is small and snap when it is a long jump; the highlight follows the active row on the snappy spring for pointer and arrow keys.",
        "Reduced motion removes the entrance, glides, and highlight travel."
      ],
      "responsive": [
        "It fills its container width; below 480px shortcut hints are hidden and padding tightens.",
        "Below 340px the footer drops its last hint.",
        "Results scroll inside a 342px maximum height, and long labels ellipsize."
      ],
      "performance": [
        "Filtering is a simple substring match over label, description, group, and keywords on every keystroke.",
        "Rows are not virtualized and animate with layout; keep lists to a few hundred items or filter on the server."
      ],
      "aiNotes": [
        "Use for app-wide command search. For picking a value inside a form use combobox; for a small list of actions use dropdown-menu.",
        "Wrap it in dialog and pass onClose to close the dialog; map onSelect to your router or action handlers by item.id."
      ],
      "related": [
        "dialog",
        "combobox",
        "dropdown-menu",
        "expanding-search",
        "search-field"
      ],
      "usage": "import { CommandPalette, type CommandItem } from \"@/components/arc/command-palette/command-palette\";\n\nconst items: CommandItem[] = [\n  { id: \"new\", label: \"New project\", group: \"Create\", shortcut: \"N\" },\n  { id: \"invite\", label: \"Invite teammate\", group: \"Team\", keywords: [\"member\"] },\n  { id: \"theme\", label: \"Toggle theme\", description: \"Switch light and dark\" },\n];\n\nexport function Palette({ onClose }: { onClose: () => void }) {\n  return <CommandPalette items={items} onSelect={item => run(item.id)} onClose={onClose} />;\n}",
      "examples": [
        {
          "title": "Inside a dialog",
          "code": "<Dialog open={open} onOpenChange={setOpen}>\n  <DialogContent title=\"Command palette\">\n    <CommandPalette\n      items={items}\n      onSelect={item => { run(item.id); setOpen(false); }}\n      onClose={() => setOpen(false)}\n    />\n  </DialogContent>\n</Dialog>"
        }
      ],
      "exports": [
        "CommandPalette",
        "CommandItem"
      ]
    },
    {
      "name": "notification-center",
      "kind": "block",
      "title": "Notification center",
      "description": "A home for updates with read state, grouped information, and animated disclosure.",
      "tier": "free",
      "builtFrom": [
        "Popover",
        "Badge",
        "Action list"
      ],
      "docs": "https://uiarc.dev/components/blocks/notification-center",
      "registry": "https://uiarc.dev/r/notification-center.json",
      "keywords": [
        "react notification center",
        "notification bell",
        "notification dropdown",
        "inbox popover",
        "unread notifications",
        "activity feed popover"
      ],
      "whenToUse": [
        "A bell in the app header with an unread badge and an inbox of recent updates.",
        "Notifications people can mark read, expand, and dismiss in bulk."
      ],
      "whenNotToUse": [
        "Use toast-stack for transient messages that should disappear.",
        "Use inbox-triage for a full-page inbox.",
        "Use badge for a count without a panel."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The trigger's aria-label includes the unread count; the visual badge is aria-hidden.",
        "The summary line is aria-live polite, and outgoing text copies are aria-hidden while they fade.",
        "Row toggles use aria-expanded and announce unread state; view toggles use aria-pressed.",
        "Focus moves to the next row, or a view toggle, after an item is read, dismissed, or cleared."
      ],
      "motion": [
        "The bell tilts when open; counts roll like an odometer in the direction of change.",
        "Rows open and collapse their own height on a spring, and bulk actions cascade down the list in under a quarter second.",
        "Reduced motion replaces height, roll, and tilt with instant fades."
      ],
      "responsive": [
        "The panel is min(424px, 100vw minus 24px) wide and at most min(590px, 100vh minus 24px) tall, with the list scrolling inside.",
        "Below 380px padding tightens, the details indent shrinks, and the Mark all read label becomes icon only.",
        "Titles and previews ellipsize on one line."
      ],
      "performance": [
        "The list is not virtualized and keeps its own copy of the notifications; cap what you pass in.",
        "The panel mounts in a portal only while open."
      ],
      "aiNotes": [
        "Use in an app header for an inbox of updates. Use toast or toast-stack for transient messages and inbox-triage for a full-page inbox.",
        "notifications seeds internal state; sync changes back to your server through onReadChange and onDismiss rather than re-passing the array."
      ],
      "related": [
        "popover",
        "toast-stack",
        "badge",
        "avatar",
        "inbox-triage"
      ],
      "usage": "import { NotificationCenter } from \"@/components/arc/notification-center/notification-center\";\n\nexport function HeaderBell() {\n  return (\n    <NotificationCenter\n      notifications={[\n        { id: \"1\", title: \"Deploy finished\", time: \"2m\", tone: \"success\" },\n        { id: \"2\", title: \"Ana commented\", time: \"1h\", actor: { name: \"Ana\", photo: \"/ana.jpg\" } },\n      ]}\n      onReadChange={(item, read) => markRead(item.id, read)}\n      onDismiss={item => dismiss(item.id)}\n    />\n  );\n}",
      "exports": [
        "NotificationCenter",
        "NotificationItem"
      ]
    },
    {
      "name": "selection-toolbar",
      "kind": "block",
      "title": "Selection toolbar",
      "description": "A contextual batch action surface that appears when items are selected.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Selection state",
        "Action list"
      ],
      "docs": "https://uiarc.dev/components/blocks/selection-toolbar",
      "registry": null,
      "keywords": [
        "react selection toolbar",
        "bulk actions bar",
        "floating action bar",
        "selected items toolbar",
        "table bulk actions",
        "multi select actions"
      ],
      "whenToUse": [
        "Bulk actions for checkbox-selectable tables or grids.",
        "Floating bars that appear only while items are selected."
      ],
      "whenNotToUse": [
        "Use dropdown-menu for per-row actions."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The bar is role=toolbar labelled Selection actions.",
        "A visually hidden aria-live polite line announces the count, such as 3 projects selected; the animated count is aria-hidden.",
        "The clear button has aria-label Clear selection.",
        "The bar unmounts at count 0, so move focus back to the list yourself if it was inside the bar."
      ],
      "motion": [
        "The bar rises and scales in on a spring and sinks back faster when the selection clears.",
        "Digits roll in the direction of the change, the plural s fades in place, and the count's width springs so actions do not jump.",
        "Reduced motion uses plain fades and no width spring."
      ],
      "responsive": [
        "Below 520px the bar goes full width and actions wrap onto a second row under the count.",
        "You position it yourself, for example fixed at the bottom center."
      ],
      "performance": [
        "The bar unmounts at count 0; a ResizeObserver springs the count width so actions do not jump."
      ],
      "aiNotes": [
        "Use with checkbox-selectable tables and grids for bulk actions.",
        "Position it yourself (for example fixed at the bottom center); it has no default export, so import it by name."
      ],
      "related": [
        "sortable-data-table",
        "checkbox"
      ],
      "usage": "import { SelectionToolbar } from \"@/components/arc/selection-toolbar/selection-toolbar\";\n\nexport function ProjectsBar({ selected, clear }: { selected: string[]; clear: () => void }) {\n  return (\n    <SelectionToolbar\n      count={selected.length}\n      noun=\"project\"\n      onClear={clear}\n      actions={[\n        { label: \"Archive\", onClick: () => archive(selected) },\n        { label: \"Delete\", onClick: () => remove(selected), destructive: true },\n      ]}\n    />\n  );\n}",
      "exports": [
        "SelectionToolbar",
        "SelectionAction"
      ]
    },
    {
      "name": "file-upload",
      "kind": "block",
      "title": "File upload",
      "description": "A complete file selection flow with constraints, progress, and error feedback.",
      "tier": "free",
      "builtFrom": [
        "File picker",
        "Progress",
        "Feedback"
      ],
      "docs": "https://uiarc.dev/components/blocks/file-upload",
      "registry": "https://uiarc.dev/r/file-upload.json",
      "keywords": [
        "react file upload",
        "drag and drop upload",
        "upload progress",
        "file dropzone",
        "multiple file upload",
        "upload with retry"
      ],
      "whenToUse": [
        "Uploading files with per-file progress, retry, and remove.",
        "Attachments that must be validated by type and size before upload.",
        "Uploads that should abort when a file is removed, via the AbortSignal."
      ],
      "whenNotToUse": [
        "Use file-dropzone when you only need to pick files without upload state.",
        "Use import-mapper when uploaded data needs column mapping."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The dropzone is role=button with aria-disabled and aria-describedby for the helper text.",
        "An aria-live polite status line announces added, rejected, uploaded, failed, and removed files.",
        "Retry and remove buttons are labelled with the file name.",
        "After removing a file, focus moves to the neighbouring remove button or back to the dropzone."
      ],
      "motion": [
        "The dropzone label swaps to Drop to add files while dragging; rows open and close their height on a spring.",
        "One spring drives the progress bar and the counted percentage, the bar folds away once the file lands, and a check draws in.",
        "Reduced motion jumps progress and replaces height and blur swaps with fades."
      ],
      "responsive": [
        "The dropzone and list fill their container; file names ellipsize on one line.",
        "On touch devices the dropzone opens the native file picker, since drag and drop is desktop only."
      ],
      "performance": [
        "Uploads run through your onUpload, one call per valid file; there is no concurrency limit, so queue large batches yourself.",
        "The file list is not virtualized; keep it to a reasonable number of files."
      ],
      "aiNotes": [
        "Use when files are uploaded with progress. Use file-dropzone when you only need to pick files without upload state.",
        "Wire onUpload to fetch or XHR and honour the AbortSignal; without onUpload files are only listed and validated."
      ],
      "related": [
        "file-dropzone",
        "progress"
      ],
      "usage": "import { FileUpload } from \"@/components/arc/file-upload/file-upload\";\n\nexport function Attachments() {\n  return (\n    <FileUpload\n      accept=\"image/*,.pdf\"\n      maxSize={10 * 1024 * 1024}\n      onUpload={async (file, { onProgress, signal }) => {\n        await uploadWithProgress(file, onProgress, signal);\n      }}\n    />\n  );\n}",
      "exports": [
        "FileUpload",
        "FileUploadItem"
      ]
    },
    {
      "name": "otp-input",
      "kind": "block",
      "title": "OTP input",
      "description": "A six digit verification flow with paste support and keyboard navigation.",
      "tier": "free",
      "builtFrom": [
        "Input",
        "Keyboard navigation",
        "Validation"
      ],
      "docs": "https://uiarc.dev/components/blocks/otp-input",
      "registry": "https://uiarc.dev/r/otp-input.json",
      "keywords": [
        "react otp input",
        "verification code input",
        "2fa code input",
        "pin input",
        "one time code",
        "sms code input"
      ],
      "whenToUse": [
        "Verification codes from email or SMS.",
        "Two-factor authentication code entry."
      ],
      "whenNotToUse": [
        "Use password-field for secrets people type from memory.",
        "Use input for any other text."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Slots sit in a labelled role=group, each with an aria-label like Verification code, digit 1 of 6.",
        "Helper and error text are linked with aria-describedby; the error uses role=alert and sets aria-invalid.",
        "The first slot uses autocomplete one-time-code so browsers can offer SMS codes."
      ],
      "motion": [
        "Typed characters rise into the slot and unblur; a paste lands as a short left-to-right wave.",
        "One focus ring glides between slots, and a new error nudges the row side to side once.",
        "Reduced motion removes the glide, shake, and blur and uses instant fades."
      ],
      "responsive": [
        "Slots are 42px wide and can shrink with max-width 100%; below 360px gaps tighten and slots drop to 44px tall.",
        "The first slot uses autocomplete one-time-code, so iOS and Android can offer SMS codes.",
        "numeric inputMode brings up the number pad on phones."
      ],
      "performance": [
        "Only a handful of inputs plus one gliding ring; a ResizeObserver springs the message height."
      ],
      "aiNotes": [
        "Use for verification and two-factor codes. Use input or password-field for anything else.",
        "It is controlled: keep value in state and submit when value.length equals length. There is no default export, so import it by name."
      ],
      "related": [
        "input",
        "password-field",
        "sign-in",
        "signup-form",
        "security-settings"
      ],
      "usage": "import { useState } from \"react\";\nimport { OtpInput } from \"@/components/arc/otp-input/otp-input\";\n\nexport function VerifyCode() {\n  const [code, setCode] = useState(\"\");\n  return (\n    <OtpInput\n      label=\"Verification code\"\n      description=\"We sent a 6-digit code to your email.\"\n      value={code}\n      onChange={next => { setCode(next); if (next.length === 6) verify(next); }}\n      autoFocus\n    />\n  );\n}",
      "exports": [
        "OtpInput"
      ]
    },
    {
      "name": "multi-step-form",
      "kind": "block",
      "title": "Multi-step form",
      "description": "A guided form that presents one decision at a time and preserves progress.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Progress",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/multi-step-form",
      "registry": null,
      "keywords": [
        "react multi step form",
        "form wizard",
        "step form",
        "onboarding form",
        "wizard component",
        "multi page form"
      ],
      "whenToUse": [
        "Onboarding or intake flows split into a few short steps.",
        "Wizards that need Back and Continue, progress, and a success state out of the box."
      ],
      "whenNotToUse": [
        "Use stepper when you only need the progress indicator.",
        "Use checkout-flow for full page flows.",
        "Use onboarding-checklist for tasks done outside a form."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each step is a fieldset with a legend, and focus moves to the new legend (or the success heading) after navigation.",
        "Progress is a labelled nav with aria-current=step, plus a visually hidden aria-live line announcing Step 2 of 3.",
        "The success state is role=status. Native required and pattern constraints in step content block submit before the step advances."
      ],
      "motion": [
        "Steps slide 20px in the direction of travel while the shell springs to the new step's height.",
        "Completed markers draw a check, the step count rolls, and the submit button's width springs when the label changes.",
        "Reduced motion swaps steps, heights, and labels instantly."
      ],
      "responsive": [
        "The progress list scrolls horizontally when steps overflow.",
        "Below 420px the progress header stacks with the step count on top and padding tightens."
      ],
      "performance": [
        "Only the active step is rendered, so keep field values in parent state.",
        "The shell springs to each step's height; step content is otherwise untouched."
      ],
      "aiNotes": [
        "Use for onboarding or intake flows split into a few short steps. Use stepper for a standalone progress indicator and checkout-flow for full page flows.",
        "Step content is rendered only while active, so keep field values in parent state; validation beyond native constraints is up to you."
      ],
      "related": [
        "stepper",
        "signup-form"
      ],
      "usage": "import { MultiStepForm } from \"@/components/arc/multi-step-form/multi-step-form\";\n\nexport function Onboarding() {\n  return (\n    <MultiStepForm\n      steps={[\n        { id: \"account\", title: \"Account\", content: <AccountFields /> },\n        { id: \"team\", title: \"Team\", description: \"Who will you work with?\", content: <TeamFields /> },\n        { id: \"plan\", title: \"Plan\", content: <PlanFields /> },\n      ]}\n      onComplete={saveOnboarding}\n    />\n  );\n}",
      "exports": [
        "MultiStepForm",
        "FormStep"
      ]
    },
    {
      "name": "team-showcase",
      "kind": "block",
      "title": "Team showcase",
      "description": "An image led team introduction with profiles that open in place.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/team-showcase",
      "registry": null,
      "keywords": [
        "react team section",
        "meet the team",
        "team page",
        "about us team grid",
        "team profiles",
        "staff showcase"
      ],
      "whenToUse": [
        "About or company pages that introduce the team with portraits.",
        "Sections that need one of four layouts: gallery, filmstrip, directory, or spotlight."
      ],
      "whenNotToUse": [
        "Use team-directory for an internal, searchable people list.",
        "Use team-members for managing members and roles.",
        "Use testimonial-stage for quotes from customers."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Selection buttons use aria-pressed; the directory rows use aria-expanded with aria-controls.",
        "Profile changes are announced in aria-live regions.",
        "Copy buttons are labeled per person, such as \"Copy Maya's email\"."
      ],
      "motion": [
        "Profiles crossfade and expand in place when selection changes.",
        "Reduced motion removes offsets and height animation."
      ],
      "responsive": [
        "It is a container query root, so layouts adapt to the section width, not the viewport.",
        "At 760px container width the header stacks and details move under the photos.",
        "At 440px the gallery and filmstrip become horizontal snap scrollers and spotlight stacks the photo under the copy."
      ],
      "performance": [
        "Portraits load through next/image with fill and per-layout sizes.",
        "The selected person is scrolled into view with a smooth scroll that becomes instant under reduced motion."
      ],
      "aiNotes": [
        "Use on an about or company page to introduce the team.",
        "Replace the people array and portraits; remove the demo contact details if they should not be public.",
        "Render TeamShowcase with a fixed variant; TeamShowcaseReview exists for the docs preview.",
        "Composes Arc CopyButton and SegmentedControl."
      ],
      "related": [
        "team-directory",
        "testimonial-stage",
        "team-members",
        "avatar"
      ],
      "usage": "import { TeamShowcase } from \"@/components/arc/blocks/team-showcase/team-showcase\";\n\nexport function About() {\n  return (\n    <TeamShowcase\n      variant=\"spotlight\"\n      onPersonSelect={(person) => router.push(\"/team/\" + person.id)}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Directory layout",
          "code": "<TeamShowcase\n  variant=\"directory\"\n  onPersonSelect={(person) => track(\"team_profile_open\", { id: person.id })}\n/>"
        }
      ],
      "exports": [
        "TeamShowcase",
        "TeamShowcaseReview"
      ]
    },
    {
      "name": "testimonial-stage",
      "kind": "block",
      "title": "Studio perspectives",
      "description": "Portrait led editorial stories with a focused reading state.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/testimonial-stage",
      "registry": null,
      "keywords": [
        "react testimonial section",
        "testimonial slider",
        "customer quotes",
        "testimonials with photos",
        "review showcase",
        "quote carousel"
      ],
      "whenToUse": [
        "Testimonials or editorial stories where the person's portrait matters.",
        "Landing sections that rotate through a few quotes on demand."
      ],
      "whenNotToUse": [
        "Use logo-marquee when logos are enough social proof.",
        "Use carousel for many short testimonials.",
        "Use team-showcase to introduce your own team."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "People form a role=\"tablist\" with role=\"tab\" and aria-selected.",
        "The quote panel is a polite live region."
      ],
      "motion": [
        "Portrait and quote crossfade with a small directional shift.",
        "Reduced motion removes the shift and uses instant transitions."
      ],
      "responsive": [
        "Below 800px the columns narrow; below 680px the portrait stacks above the quote.",
        "Below 350px padding and quote size shrink to keep the quote readable."
      ],
      "performance": [
        "Only the active portrait is mounted, via next/image, and the first one gets priority loading.",
        "There is no autoplay timer; changes happen only on tab, arrow, or key input."
      ],
      "aiNotes": [
        "Use for testimonials or editorial stories about people.",
        "Replace the perspectives array with verified quotes and portraits you have permission to use."
      ],
      "related": [
        "team-showcase",
        "logo-marquee",
        "carousel",
        "tabs"
      ],
      "usage": "import { TestimonialStage } from \"@/components/arc/blocks/testimonial-stage/testimonial-stage\";\n\nexport function Stories() {\n  return (\n    <TestimonialStage\n      className=\"my-24\"\n      onPerspectiveChange={(p) => track(\"testimonial_view\", p)}\n    />\n  );\n}",
      "exports": [
        "TestimonialStage"
      ]
    },
    {
      "name": "changelog-feed",
      "kind": "block",
      "title": "Changelog feed",
      "description": "Release notes you can filter, open in place, and scroll through month by month.",
      "tier": "free",
      "builtFrom": [
        "Copy button"
      ],
      "docs": "https://uiarc.dev/components/blocks/changelog-feed",
      "registry": "https://uiarc.dev/r/changelog-feed.json",
      "keywords": [
        "react changelog",
        "release notes page",
        "changelog component",
        "product updates feed",
        "what's new page",
        "changelog with filters"
      ],
      "whenToUse": [
        "A product changelog or release notes page grouped by month.",
        "Release notes with type filters such as features, fixes, and improvements.",
        "Changelogs that should offer an email subscribe action in the header."
      ],
      "whenNotToUse": [
        "Use timeline for a short vertical list of events without filters.",
        "Use accordion for plain expandable content that is not dated."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each entry row is a button with aria-expanded and aria-controls pointing at its detail panel.",
        "Filter chips are toggle buttons with aria-pressed, and the visible count is announced in a polite status region.",
        "The month nav marks the current month with aria-current, and the scroll area is focusable with an aria-label."
      ],
      "motion": [
        "Entries open in place on a spring while the chevron rotates; the month label slides in the scroll direction.",
        "The subscribe control morphs from button to form to a drawn check with a short blur crossfade.",
        "Reduced motion makes these changes instant."
      ],
      "responsive": [
        "Below 640px the header stacks, rows move date and type above the text, and the scroller drops from 560px to 520px.",
        "Below 480px month counts and the shown count are hidden so the month nav fits on one line.",
        "Hover styles apply only on hover-capable fine pointers."
      ],
      "performance": [
        "The feed scrolls inside a fixed-height area; entries are not virtualized, so paginate or load older months on demand.",
        "Scroll tracking for the month bar is batched to one requestAnimationFrame, and a ResizeObserver resyncs it on resize."
      ],
      "aiNotes": [
        "Use for a product changelog or release notes page.",
        "Replace the entries array and the imported sample images with data from your release notes source or CMS.",
        "The subscribe submit only changes local state; post the email to your mailing list provider."
      ],
      "related": [
        "timeline",
        "accordion",
        "copy-button",
        "chip-group"
      ],
      "usage": "import { ChangelogFeed } from \"@/components/arc/blocks/changelog-feed/changelog-feed\";\n\nexport default function ChangelogPage() {\n  return (\n    <main>\n      <ChangelogFeed />\n    </main>\n  );\n}",
      "exports": [
        "ChangelogFeed"
      ]
    },
    {
      "name": "usage-pricing",
      "kind": "block",
      "title": "Usage pricing",
      "description": "A pricing calculator that finds your plan as you drag seats and traffic.",
      "tier": "pro",
      "builtFrom": [
        "Slider",
        "Segmented control",
        "Animated counter",
        "Text morph",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/usage-pricing",
      "registry": null,
      "keywords": [
        "react pricing calculator",
        "usage based pricing",
        "seat pricing slider",
        "pricing page component",
        "plan recommender",
        "monthly yearly toggle",
        "saas pricing"
      ],
      "whenToUse": [
        "Pricing pages for plans priced by seats plus usage.",
        "Calculators that should recommend a plan as people move sliders.",
        "Monthly and yearly toggles where the saving should be visible in the breakdown."
      ],
      "whenNotToUse": [
        "Use plan-comparison for a side by side table of fixed tiers.",
        "Use usage-forecast to project spend for an existing customer.",
        "Use billing-overview for the settings page of someone already subscribed."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plan card and breakdown are labelled sections tied to their headings with aria-labelledby.",
        "Plan changes are announced in a polite live region with the plan name and reason.",
        "The decorative meter is aria-hidden; the numbers are real text."
      ],
      "motion": [
        "Prices roll with the animated counter and plan names morph with text-morph as sliders move.",
        "The feature list reflows as items are added or removed for the recommended plan.",
        "Reduced motion drops the layout and entrance animations."
      ],
      "responsive": [
        "At 780px container width and above the plan card sits in its own column beside the sliders; below that everything stacks.",
        "The block caps at 1060px and clips its own overflow, so it fits any page column."
      ],
      "performance": [
        "Prices roll with the animated counter and the feature list reflows with layout animation on each slider change.",
        "A ResizeObserver drives the height morph of the plan card."
      ],
      "aiNotes": [
        "Use on a pricing page for seat plus usage based plans.",
        "Replace the plan table, rates, and limits with your own; the trial button only sets local state, so route it to signup.",
        "Composes Arc slider, segmented-control, animated-counter, text-morph, and button."
      ],
      "related": [
        "plan-comparison",
        "slider",
        "animated-counter",
        "segmented-control",
        "usage-forecast"
      ],
      "usage": "import { UsagePricing } from \"@/components/arc/blocks/usage-pricing/usage-pricing\";\n\nexport default function PricingPage() {\n  return (\n    <main>\n      <UsagePricing />\n    </main>\n  );\n}",
      "exports": [
        "UsagePricing"
      ]
    },
    {
      "name": "delivery-tracker",
      "kind": "block",
      "title": "Delivery tracker",
      "description": "A live order tracker whose marker glides between stops while the arrival time rolls.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Text morph"
      ],
      "docs": "https://uiarc.dev/components/blocks/delivery-tracker",
      "registry": null,
      "keywords": [
        "react delivery tracker",
        "order tracking page",
        "shipment status",
        "courier tracking ui",
        "package tracker",
        "eta tracker"
      ],
      "whenToUse": [
        "Order status pages that follow a delivery from dispatch to door.",
        "Tracking screens with a live arrival time and an event history.",
        "Deliveries where people can message or call the courier."
      ],
      "whenNotToUse": [
        "Use timeline for a static list of past events.",
        "Use progress when a single percentage is enough.",
        "Use support-conversation for a full chat with support."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Stage changes and arrival updates are announced through a polite live region.",
        "Quick replies are a labelled group, and the courier typing indicator is a role=\"status\" with a label.",
        "Call status text updates in its own polite live region."
      ],
      "motion": [
        "The marker glides between stops while the arrival time rolls.",
        "The contact button morphs into the contact panel through a shared layoutId.",
        "Reduced motion makes the marker and panel morph instant."
      ],
      "responsive": [
        "Below 760px container width the courier, updates, and order stack in one column.",
        "Below 560px the hero stacks and only the current stop keeps its label on the route bar."
      ],
      "performance": [
        "The call timer interval runs only during a call, and a ResizeObserver positions the route marker.",
        "Shared layoutIds morph the contact button into its panel; nothing animates while idle."
      ],
      "aiNotes": [
        "Use on an order status page.",
        "Replace the simulated timeline with updates from your carrier or dispatch webhooks, and swap the sample product photos.",
        "Connect message and call actions to your messaging and masked calling service; composes Arc avatar, button, and text-morph."
      ],
      "related": [
        "timeline",
        "avatar",
        "support-conversation"
      ],
      "usage": "import { DeliveryTracker } from \"@/components/arc/blocks/delivery-tracker/delivery-tracker\";\n\nexport default function OrderPage() {\n  return (\n    <main>\n      <DeliveryTracker className=\"order-status\" />\n    </main>\n  );\n}",
      "exports": [
        "DeliveryTracker"
      ]
    },
    {
      "name": "ai-composer",
      "kind": "block",
      "title": "AI composer",
      "description": "The prompt box at the bottom of an AI app: files upload in place, / opens commands, @ inserts mentions, a model and effort menu sits in the toolbar, and the send button turns into a stop control ringed by the reply's progress.",
      "tier": "pro",
      "builtFrom": [
        "Tooltip",
        "Text shimmer",
        "Copy button",
        "Dropdown menu"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-composer",
      "registry": null,
      "keywords": [
        "ai composer",
        "chat input",
        "prompt box",
        "chat composer",
        "message input",
        "send button",
        "stop generating",
        "attachments",
        "file upload",
        "slash commands",
        "mentions",
        "model picker",
        "effort",
        "dictation",
        "streaming",
        "claude",
        "assistant"
      ],
      "whenToUse": [
        "The prompt box of an assistant, a coding agent or an in-product AI feature where people attach files, mention docs and pick a model.",
        "A new chat screen that should open on a greeting and suggestions, then show the first exchange.",
        "Any place that needs a streaming send and stop control with honest progress and keyboard first commands."
      ],
      "whenNotToUse": [
        "Use ai-chat for a full conversation with history, message actions and a sidebar of chats.",
        "Use ai-inline-edit to rewrite a selection inside a document instead of chatting about it.",
        "Use voice-agent when speech is the main input and the reply is spoken.",
        "Use model-picker when the job is comparing and choosing models rather than sending a prompt."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The textarea is labeled Message and described by a hidden hint for Enter, Shift Enter, / and @. While a list is open it points at it with aria-controls and aria-activedescendant, and the list is a listbox of options grouped into Files and Docs.",
        "Tokens are drawn by a mirror under a transparent textarea, so the field stays a real textarea for screen readers, spellcheck, undo and IME input.",
        "The send button changes its accessible name with its state: Send message, Send after uploads finish, Send with the message over the limit, and Stop generating. It stays focusable while it cannot send and reports aria-disabled.",
        "Web search, Extended thinking and the mic are toggle buttons with aria-pressed; on state also shows in the accent fill and a tooltip, never color alone.",
        "Attachment chips name their remove control after the file, and a running upload's control reads Cancel upload. A failed file shows an icon and a short reason.",
        "A polite status region reports what happened: uploads finished, a long paste added as an attachment, the model and effort chosen, sent with a simulated reply, reply finished or stopped, and that dictation and the Drive, GitHub and screenshot actions are simulated.",
        "Menus come from Radix with focus moved in and returned on close; after picking an attach action focus returns to the textarea.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "The send arrow lifts out and a stop square scales in on the morph spring; a thin accent arc around the button fills with the reply, then fades as the button turns back into an arrow the moment the stream ends.",
        "Replies stream in chunks of whole words every 34ms on an animation frame clock, about 80 words a second, with catch-up when a live stream runs ahead. Each new chunk only fades in over 160ms; settled text is static, and the reply grows line by line on a short ease-out.",
        "A short Thinking shimmer covers the moment before the first token; with Extended thinking or Web search on, those steps shimmer instead and settle into Thought for 1s or Searched 6 sites.",
        "The sent message lifts into its bubble on the snappy spring while the composer glides from the center to the bottom on the smooth spring.",
        "Attachment chips scale in, their ring fills with the upload, and a check pops in before handing over to the remove control. The model chip springs to the width of its new label.",
        "Reduced motion keeps every state and announcement but drops travel: chunks appear without a fade, the waveform holds still, the ring does not spin, and panels change at once."
      ],
      "responsive": [
        "The block is a container: below 680px the tool chips fold into icon buttons with tooltips, below 560px the effort label leaves the model chip, and below 400px the model chip keeps only its mark and the counter keeps only its ring.",
        "At 320px every control stays in one toolbar row, attachment chips scroll sideways in their own row, and suggestions stack to full width.",
        "The command and mention list sits above the line being typed and stays inside the composer's width."
      ],
      "performance": [
        "The reveal clock runs on requestAnimationFrame only while a reply is writing and skips renders when nothing changed; older chunks merge into one text node so the DOM stays small for long replies.",
        "The field height follows the mirror through a ResizeObserver instead of measuring scrollHeight on every keystroke.",
        "The dictation waveform writes bar transforms directly on animation frames without re-rendering React.",
        "Object URLs for image thumbnails are revoked when a chip is removed or the block unmounts."
      ],
      "aiNotes": [
        "Wire `onSend` to your model endpoint and return an async iterable of text deltas; the block handles the reveal clock, Stop (through the AbortSignal), Retry and the progress ring. Throwing inside the iterable shows an error line under the reply.",
        "The message carries `tokens` (the commands and mentions in the text) and `attachments` (with the real File for picked, dropped or pasted files, and the pasted text for pasted chips), so the server can resolve context without parsing the text.",
        "Uploads, Google Drive, GitHub, screenshots and dictation are simulated in the preview. Replace the actions in composer.tsx with your own upload and picker calls; the chip model (status uploading, done, ready or error with progress) stays the same.",
        "Use only current models in `models`. The sample lists Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, GPT-6.1 Sol and Gemini 4 Argon with their real marks.",
        "Keep the stage minimal. For a full thread with history, branching and message actions, use ai-chat and reuse this composer inside it."
      ],
      "related": [
        "ai-chat",
        "ai-answer",
        "ai-side-panel",
        "model-picker",
        "context-window",
        "text-shimmer"
      ],
      "usage": "import { AiComposer, type ComposerMessage } from \"@/components/arc/blocks/ai-composer/ai-composer\";\n\nasync function* streamReply(message: ComposerMessage, signal: AbortSignal) {\n  const response = await fetch(\"/api/chat\", {\n    method: \"POST\",\n    body: JSON.stringify({ text: message.text, model: message.model, effort: message.effort, webSearch: message.webSearch, thinking: message.thinking }),\n    signal,\n  });\n  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();\n  for (;;) {\n    const { value, done } = await reader.read();\n    if (done) return;\n    yield value;\n  }\n}\n\nexport function Assistant() {\n  return <AiComposer greeting=\"How can I help with checkout?\" onSend={(message, { signal }) => streamReply(message, signal)} />;\n}",
      "exports": [
        "AiComposer"
      ]
    },
    {
      "name": "sign-in",
      "kind": "block",
      "title": "Sign in",
      "description": "A sign in card that morphs from email to a six digit code to your account.",
      "tier": "free",
      "builtFrom": [
        "Input",
        "OTP input",
        "Button",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/sign-in",
      "registry": "https://uiarc.dev/r/sign-in.json",
      "keywords": [
        "react sign in",
        "passwordless login",
        "email code login",
        "magic code sign in",
        "passkey login",
        "sso buttons",
        "otp login"
      ],
      "whenToUse": [
        "The entry screen of an app with email code sign in.",
        "Sign in cards that also offer passkeys and single sign-on.",
        "Passwordless flows that should confirm the account before continuing."
      ],
      "whenNotToUse": [
        "Use login-centered, login-split, or login-immersive for password based layouts.",
        "Use signup-form to create an account.",
        "Use otp-input on its own when the code step lives in your own form."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Single sign-on providers are a labelled group of buttons with visible names.",
        "Step changes and errors are announced through a visually hidden role=\"status\" region.",
        "Provider marks are aria-hidden SVGs next to text labels."
      ],
      "motion": [
        "The card morphs its height between the email, code, and done steps, sliding content in the direction of travel.",
        "Reduced motion swaps the slide for a plain fade."
      ],
      "responsive": [
        "The card is a container; below 380px its padding and heading shrink, and the provider buttons tighten.",
        "Below a 480px viewport the card corners soften to the panel radius."
      ],
      "performance": [
        "A ResizeObserver drives the height morph between steps.",
        "The signed in avatar reuses the same image through a layoutId, so the morph does not wait on a second image."
      ],
      "aiNotes": [
        "Use as the entry screen of an app.",
        "Send and verify codes through your auth provider instead of demoCode, connect the passkey button to WebAuthn, and the provider buttons to OAuth.",
        "Composes Arc input, otp-input, button, and avatar; exports SignInAccount and SignInMethod types."
      ],
      "related": [
        "otp-input",
        "login-centered",
        "login-split",
        "signup-form",
        "password-field"
      ],
      "usage": "import { SignIn } from \"@/components/arc/blocks/sign-in/sign-in\";\nimport { useRouter } from \"next/navigation\";\n\nexport function SignInPage() {\n  const router = useRouter();\n\n  return <SignIn onSignIn={() => router.push(\"/dashboard\")} />;\n}",
      "exports": [
        "SignIn"
      ]
    },
    {
      "name": "workspace-sidebar",
      "kind": "block",
      "title": "Workspace sidebar",
      "description": "A product sidebar with a workspace switcher, live search, draggable favorites, and inline projects.",
      "tier": "pro",
      "builtFrom": [
        "Search field",
        "Avatar",
        "Animated counter",
        "Text morph",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/workspace-sidebar",
      "registry": null,
      "keywords": [
        "react sidebar",
        "workspace switcher",
        "collapsible sidebar",
        "app sidebar navigation",
        "icon rail sidebar",
        "linear style sidebar",
        "draggable favorites"
      ],
      "whenToUse": [
        "The main navigation of a multi-workspace app.",
        "Sidebars with draggable favorites and inline project creation.",
        "Navigation that should collapse to an icon rail with the [ shortcut."
      ],
      "whenNotToUse": [
        "Use sidebar-rail when you need the whole app shell with a mobile sheet.",
        "Use docs-sidebar for documentation trees.",
        "Use inbox-sidebar for mail folders and labels."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The workspace switcher is a Radix dropdown menu with full menu keyboard support.",
        "Favorites can be reordered by keyboard as well as by drag.",
        "The rail shortcut ignores inputs, menus, and dialogs so typing is never hijacked."
      ],
      "motion": [
        "The sidebar width springs between full and rail, and favorites reorder with layout animation.",
        "New projects slide in and counts roll with the animated counter.",
        "Reduced motion is applied after hydration and makes width and reorder changes instant."
      ],
      "responsive": [
        "The open width is the --full custom property, 272px by default, and the rail is 64px.",
        "It has no mobile sheet; below 380px it only shortens to 660px, so pair it with drawer on phones.",
        "Favorites drag on the y axis with Reorder, and Alt+Arrow keys reorder without a pointer."
      ],
      "performance": [
        "Width springs through one motion value, so collapsing does not re-render the lists.",
        "Lists are not virtualized; keep favorites and projects to a sidebar's worth of rows."
      ],
      "aiNotes": [
        "Use as the main navigation of a multi-workspace app.",
        "Load workspaces, favorites, and projects from your API, route through onNavigate, and persist favorite order and new projects yourself.",
        "Composes Arc search-field, avatar, animated-counter, text-morph, and button."
      ],
      "related": [
        "sidebar-rail",
        "inbox-sidebar",
        "docs-sidebar",
        "search-field",
        "reorderable-list"
      ],
      "usage": "import { WorkspaceSidebar } from \"@/components/arc/blocks/workspace-sidebar/workspace-sidebar\";\nimport { useRouter } from \"next/navigation\";\n\nexport function AppNav() {\n  const router = useRouter();\n\n  return (\n    <WorkspaceSidebar\n      onNavigate={({ workspace, id }) => router.push(`/${workspace}/${id}`)}\n      onCollapsedChange={(collapsed) => localStorage.setItem(\"rail\", String(collapsed))}\n    />\n  );\n}",
      "exports": [
        "WorkspaceSidebar"
      ]
    },
    {
      "name": "settings-page",
      "kind": "block",
      "title": "Settings page",
      "description": "Account settings with a gliding section nav and a save bar that morphs in as you edit.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Select",
        "Switch",
        "Segmented control",
        "Avatar",
        "Animated counter",
        "Text morph",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/settings-page",
      "registry": null,
      "keywords": [
        "react settings page",
        "account settings",
        "settings form",
        "unsaved changes bar",
        "profile settings ui",
        "save bar"
      ],
      "whenToUse": [
        "Account or workspace settings with several sections.",
        "Forms that should show a save bar only when something changed.",
        "Settings where Cmd or Ctrl+S should save."
      ],
      "whenNotToUse": [
        "Use security-settings for sessions, two factor, and passwords.",
        "Use billing-overview for plan and invoice management.",
        "Use team-members for inviting and managing people."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a labelled section, and the section nav supports roving arrow key focus.",
        "Form fields use Arc input, select, and switch with visible labels.",
        "The save bar counts changed fields and announces saves and reverts."
      ],
      "motion": [
        "The active section indicator glides between nav items.",
        "The save bar morphs in as you edit and its changed count rolls.",
        "Reduced motion removes the glide and morph."
      ],
      "responsive": [
        "Below 720px container width the section nav becomes a horizontal scrolling row above the form and fields drop to one column.",
        "Below 460px and 360px the save bar hides secondary text and the profile header stacks."
      ],
      "performance": [
        "The form scrolls inside a fixed-height pane; a ResizeObserver keeps the nav indicator aligned.",
        "Only the save bar count and indicator animate; fields are plain controlled inputs."
      ],
      "aiNotes": [
        "Use for account or workspace settings.",
        "Replace the initial values with data from your user API, send only changed fields on save, and upload the photo file to your storage.",
        "Composes Arc input, select, switch, segmented-control, avatar, animated-counter, text-morph, and button."
      ],
      "related": [
        "security-settings",
        "billing-overview",
        "team-members",
        "api-keys",
        "switch"
      ],
      "usage": "import { SettingsPage } from \"@/components/arc/blocks/settings-page/settings-page\";\n\nexport default function AccountSettings() {\n  return (\n    <main>\n      <SettingsPage />\n    </main>\n  );\n}",
      "exports": [
        "SettingsPage"
      ]
    },
    {
      "name": "sidebar-rail",
      "kind": "block",
      "title": "Sidebar rail",
      "description": "An app shell whose sidebar folds into an icon rail and slides in as a sheet on phones.",
      "tier": "pro",
      "builtFrom": [
        "Tooltip",
        "Avatar",
        "Avatar group",
        "Badge",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/sidebar-rail",
      "registry": null,
      "keywords": [
        "react app shell",
        "sidebar layout",
        "collapsible sidebar",
        "icon rail navigation",
        "mobile nav sheet",
        "dashboard layout",
        "nested sidebar"
      ],
      "whenToUse": [
        "The frame of a product app with a sidebar, breadcrumbs, and content area.",
        "Apps with nested nav groups that should fold into an icon rail.",
        "Shells that need a navigation sheet on narrow widths."
      ],
      "whenNotToUse": [
        "Use workspace-sidebar when workspace switching and favorites matter more than the shell.",
        "Use docs-sidebar for documentation.",
        "Use breadcrumb on its own when the page already has navigation."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dialog",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "Nav rows set aria-current=\"page\" on the active item and aria-expanded on groups.",
        "The narrow layout uses a Radix dialog sheet that moves focus to the current item when it opens.",
        "Rail icons show Radix tooltips with their labels, and section changes are announced in a status region."
      ],
      "motion": [
        "The sidebar folds to a rail on a spring and nested groups expand by height.",
        "The breadcrumb rises into the new section name.",
        "Reduced motion makes folding and group changes instant."
      ],
      "responsive": [
        "Below 640px container width the sidebar is replaced by a menu button that opens a Radix dialog sheet, up to 288px wide.",
        "The sheet drags closed to the left, and a ResizeObserver closes it when the shell widens past 640px.",
        "Content rows drop the timestamp below 420px of content width."
      ],
      "performance": [
        "The shell has a fixed 640px height and scrolls its content inside, so it does not reflow the page.",
        "Rail tooltips mount only while shown through Radix."
      ],
      "aiNotes": [
        "Use as the frame of a product app.",
        "Map the sections to your routes, send selection to your router, and persist the collapsed state per person.",
        "Composes Arc tooltip, avatar, avatar-group, badge, and animated-counter."
      ],
      "related": [
        "workspace-sidebar",
        "docs-sidebar",
        "inbox-sidebar",
        "breadcrumb",
        "tooltip"
      ],
      "usage": "import { SidebarRail } from \"@/components/arc/blocks/sidebar-rail/sidebar-rail\";\n\nexport default function AppShell() {\n  return (\n    <main>\n      <SidebarRail />\n    </main>\n  );\n}",
      "exports": [
        "SidebarRail"
      ]
    },
    {
      "name": "onboarding-illustration",
      "kind": "block",
      "title": "Onboarding illustration",
      "description": "A compact onboarding card whose line illustration morphs from folder to files, team, and badge.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/onboarding-illustration",
      "registry": null,
      "keywords": [
        "react onboarding",
        "onboarding modal",
        "welcome dialog",
        "first run experience",
        "onboarding steps illustration",
        "animated svg onboarding"
      ],
      "whenToUse": [
        "A short first-run dialog in a document or workspace product.",
        "Onboarding with three or four steps that each need a simple picture."
      ],
      "whenNotToUse": [
        "Use onboarding-checklist for tasks people complete over time.",
        "Use feature-illustration for a feature section on a marketing page."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The illustration is role=\"img\" with a label describing the current scene.",
        "Step copy sits in a polite live region, so step changes are read out.",
        "Step dots are labelled buttons with aria-current on the active step."
      ],
      "motion": [
        "The same line shapes morph between folder, files, team, and badge scenes on springs.",
        "Reduced motion jumps to each scene without the morph."
      ],
      "responsive": [
        "Below 440px padding tightens, the Back label becomes an icon, and the copy keeps a fixed minimum height so buttons do not jump.",
        "The illustration's pointer parallax runs only for a mouse on hover-capable devices."
      ],
      "performance": [
        "The scenes are one inline SVG whose shapes morph on springs, with no images to load."
      ],
      "aiNotes": [
        "Use as a first-run dialog in a document or workspace product.",
        "Swap the step copy and scene labels for your own, and route Get started to your workspace creation flow.",
        "Has no Arc component dependencies."
      ],
      "related": [
        "onboarding-checklist",
        "stepper",
        "empty-states"
      ],
      "usage": "import { OnboardingIllustration } from \"@/components/arc/blocks/onboarding-illustration/onboarding-illustration\";\n\nexport function FirstRun() {\n  return (\n    <div role=\"dialog\" aria-label=\"Welcome\">\n      <OnboardingIllustration />\n    </div>\n  );\n}",
      "exports": [
        "OnboardingIllustration"
      ]
    },
    {
      "name": "docs-sidebar",
      "kind": "block",
      "title": "Docs sidebar",
      "description": "A docs layout with a live filtering nav tree, version switcher, and a gliding on this page outline.",
      "tier": "pro",
      "builtFrom": [
        "Search field",
        "Dropdown menu",
        "Code block",
        "Copy button",
        "Badge",
        "Avatar",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/docs-sidebar",
      "registry": null,
      "keywords": [
        "react docs layout",
        "documentation sidebar",
        "docs navigation",
        "table of contents scroll spy",
        "on this page outline",
        "api docs template"
      ],
      "whenToUse": [
        "Product or API documentation with a nav tree and page outline.",
        "Docs that need a version switcher and a filterable tree.",
        "Pages where the on this page outline should follow scroll."
      ],
      "whenNotToUse": [
        "Use tree-view for a plain hierarchy outside a docs layout.",
        "Use sidebar-rail for app navigation.",
        "Use command-palette for global search across docs."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The rail toggle exposes aria-expanded and aria-keyshortcuts=\"[\".",
        "Filter results are announced as a count in a polite status region.",
        "The nav tree supports arrow key movement from the search field into the list."
      ],
      "motion": [
        "The outline indicator glides to the heading in view, and the sidebar folds to a rail on a spring.",
        "Nav groups expand by height as the filter opens matching branches.",
        "Reduced motion makes the glide and folds instant."
      ],
      "responsive": [
        "At 1000px container width and above the outline gets its own 200px column; below 740px it becomes a collapsible bar above the article.",
        "Below 620px the rail is dropped and the nav tree folds behind a menu button above the content."
      ],
      "performance": [
        "Scroll spy is batched to one requestAnimationFrame per scroll, and ResizeObservers remeasure on width changes.",
        "The nav tree is not virtualized; very large trees rely on the filter."
      ],
      "aiNotes": [
        "Use for product or API documentation.",
        "Load the tree and pages from your content source, route page changes through your router, and point the version switcher at your versioned builds.",
        "Composes Arc search-field, dropdown-menu, code-block, copy-button, badge, avatar, and button."
      ],
      "related": [
        "tree-view",
        "sidebar-rail",
        "code-block",
        "search-field",
        "command-palette"
      ],
      "usage": "import { DocsSidebar } from \"@/components/arc/blocks/docs-sidebar/docs-sidebar\";\n\nexport default function DocsPage() {\n  return (\n    <DocsSidebar\n      defaultCollapsed={false}\n      onCollapsedChange={(collapsed) => localStorage.setItem(\"docs-rail\", String(collapsed))}\n    />\n  );\n}",
      "exports": [
        "DocsSidebar"
      ]
    },
    {
      "name": "integrations",
      "kind": "block",
      "title": "Integrations",
      "description": "An integrations directory where Connect morphs to Connected and the filters recount as you go.",
      "tier": "pro",
      "builtFrom": [
        "Search field",
        "Drawer",
        "Select",
        "Hold to confirm",
        "Avatar",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/integrations",
      "registry": null,
      "keywords": [
        "react integrations page",
        "app marketplace",
        "connect apps ui",
        "integrations directory",
        "oauth connect button",
        "third party integrations"
      ],
      "whenToUse": [
        "Settings or marketplace pages for third party connections.",
        "Directories with search, category filters, and Connect buttons.",
        "Connections that need sync settings and a safe disconnect."
      ],
      "whenNotToUse": [
        "Use webhooks for outgoing event endpoints.",
        "Use api-keys for developer credentials."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Filters are a role=\"radiogroup\" of role=\"radio\" buttons with aria-checked and roving arrow keys.",
        "Filter results and connection changes are announced in a polite status region.",
        "Disconnect uses hold to confirm, so it cannot fire from a stray click."
      ],
      "motion": [
        "Connect morphs into a Connected state and filter counts recount as you go.",
        "Cards reflow with layout animation when filtering.",
        "Reduced motion swaps morphs and reflow for instant changes."
      ],
      "responsive": [
        "Below 720px container width the search goes full width under the title.",
        "Below 480px the filters become one horizontally scrolling row instead of wrapping.",
        "Settings open in a drawer, which goes nearly full width on phones."
      ],
      "performance": [
        "Cards reflow with layout animation when filtering; the grid is not virtualized.",
        "ResizeObservers drive the filter indicator and height morphs."
      ],
      "aiNotes": [
        "Use on a settings or marketplace page for third party connections.",
        "Load integrations and connection state from your API, start each provider's OAuth flow from Connect, and save sync frequency and Disconnect through it.",
        "Composes Arc search-field, drawer, select, hold-to-confirm, avatar, and button."
      ],
      "related": [
        "webhooks",
        "api-keys",
        "drawer",
        "hold-to-confirm",
        "search-field"
      ],
      "usage": "import { Integrations } from \"@/components/arc/blocks/integrations/integrations\";\n\nexport default function IntegrationsPage() {\n  return (\n    <main>\n      <Integrations />\n    </main>\n  );\n}",
      "exports": [
        "Integrations"
      ]
    },
    {
      "name": "billing-overview",
      "kind": "block",
      "title": "Billing overview",
      "description": "A billing page where the plan card morphs into a picker and usage, invoices, and card follow.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Badge",
        "Input",
        "Segmented control",
        "Text morph",
        "Animated counter",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/billing-overview",
      "registry": null,
      "keywords": [
        "react billing page",
        "subscription management",
        "billing settings",
        "invoice list",
        "usage meters",
        "change plan ui",
        "saas billing"
      ],
      "whenToUse": [
        "The billing settings page of a SaaS app.",
        "Pages that show plan, usage, payment method, and invoices together.",
        "Plan changes that should happen in place without leaving billing."
      ],
      "whenNotToUse": [
        "Use plan-comparison or usage-pricing on a public pricing page.",
        "Use cancel-flow for the cancellation journey.",
        "Use usage-meter to show a single quota elsewhere."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plan picker is a role=\"radiogroup\" of role=\"radio\" buttons; unavailable plans are disabled.",
        "The price quote updates in a polite live region, and actions are announced in a status region."
      ],
      "motion": [
        "The plan card morphs into the picker and back inside a region whose height springs; the selected plan highlight glides with a shared layoutId.",
        "Changing copy rises into place while the old line lifts away; totals roll with AnimatedCounter and card digits turn one column at a time.",
        "Usage bars fill when they scroll into view.",
        "Reduced motion makes the morph and fills instant."
      ],
      "responsive": [
        "Below 780px container width the plan and payment cards stack; below 700px meters go single column and invoice rows wrap.",
        "Below 480px quote buttons stretch full width, and below 340px the card form fields stack."
      ],
      "performance": [
        "Usage bars animate once when 30% in view.",
        "The invoice list is not virtualized; paginate long billing histories."
      ],
      "aiNotes": [
        "Use as the billing settings page of a SaaS app.",
        "Load plan, usage, card, and invoices from your billing provider and connect confirmPlan, saveCard, pay, and download to it.",
        "Quotes prorate a mid-term change for the days left; switching billing period starts a new period today and credits unused time. Replace this with your provider's preview invoice.",
        "Composes Arc button, badge, input, segmented-control, text-morph, animated-counter, and avatar."
      ],
      "related": [
        "usage-meter",
        "plan-comparison",
        "cancel-flow",
        "invoice-studio",
        "settings-page"
      ],
      "usage": "import { BillingOverview } from \"@/components/arc/blocks/billing-overview/billing-overview\";\n\nexport default function BillingPage() {\n  return (\n    <main>\n      <BillingOverview />\n    </main>\n  );\n}",
      "exports": [
        "BillingOverview"
      ]
    },
    {
      "name": "api-keys",
      "kind": "block",
      "title": "API keys",
      "description": "API key management where Create key unfolds into its form and each secret is shown once.",
      "tier": "pro",
      "builtFrom": [
        "Hold to confirm",
        "Copy button",
        "Input",
        "Segmented control",
        "Checkbox",
        "Badge",
        "Avatar",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/api-keys",
      "registry": null,
      "keywords": [
        "react api keys",
        "api key management",
        "developer settings",
        "create api key",
        "revoke api key",
        "secret reveal",
        "token management"
      ],
      "whenToUse": [
        "The developer settings page of an API product.",
        "Key management with scopes, environments, and a one-time secret reveal.",
        "Revoking keys where a stray click must not fire."
      ],
      "whenNotToUse": [
        "Use webhooks for event endpoints and signing secrets.",
        "Use integrations for OAuth connections to other apps.",
        "Use roles-permissions for user access."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each key opens a labelled role=\"region\" detail panel.",
        "Revoke uses hold to confirm, so it cannot fire from a stray click.",
        "Creation, copy, and revoke results are announced in a polite status region."
      ],
      "motion": [
        "The Create key button unfolds into its form, and new keys slide into the list.",
        "Reduced motion replaces the unfold with an instant swap."
      ],
      "responsive": [
        "Below 700px container width key rows stack scopes and dates under the name; below 540px detail facts go single column.",
        "The create form drops to one column below 560px of its own width."
      ],
      "performance": [
        "Key rows are not virtualized, which suits the handful of keys most accounts have.",
        "A ResizeObserver drives the create button's unfold into the form."
      ],
      "aiNotes": [
        "Use on the developer settings page of an API product.",
        "Create keys on your server and pass the one-time secret to the reveal step; never store it client side.",
        "Connect hold to revoke to your revocation endpoint. Composes Arc hold-to-confirm, copy-button, input, segmented-control, checkbox, badge, avatar, and button."
      ],
      "related": [
        "webhooks",
        "integrations",
        "hold-to-confirm",
        "copy-button",
        "roles-permissions"
      ],
      "usage": "import { ApiKeys } from \"@/components/arc/blocks/api-keys/api-keys\";\n\nexport default function DeveloperSettings() {\n  return (\n    <main>\n      <ApiKeys />\n    </main>\n  );\n}",
      "exports": [
        "ApiKeys"
      ]
    },
    {
      "name": "inbox-sidebar",
      "kind": "block",
      "title": "Inbox sidebar",
      "description": "A mail sidebar whose Compose button grows into a composer and whose folders take dropped mail.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Animated counter",
        "Tooltip",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/inbox-sidebar",
      "registry": null,
      "keywords": [
        "react email client",
        "inbox ui",
        "mail sidebar",
        "gmail clone",
        "drag to folder",
        "compose window",
        "mail app layout"
      ],
      "whenToUse": [
        "The shell of a mail or messaging app.",
        "Inboxes where messages are dragged onto folders.",
        "Mail clients with row quick actions and a compose window."
      ],
      "whenNotToUse": [
        "Use inbox-triage for a focused one-at-a-time triage flow.",
        "Use notification-center for app notifications.",
        "Use workspace-sidebar for general app navigation."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The composer is a role=\"dialog\" labelled by its title.",
        "Moves, stars, and sends are announced through a polite status region, and rail icons have tooltips.",
        "Every drag action has a keyboard or menu alternative."
      ],
      "motion": [
        "Compose grows into the composer through a shared layoutId, and unread counts roll.",
        "Dropped messages leave the list with layout animation while the target folder pulses.",
        "Reduced motion makes the morph and list changes instant."
      ],
      "responsive": [
        "Below 720px container width the sidebar is locked to its 64px icon rail; below 420px row labels are hidden.",
        "Dragging to folders works with a mouse only; touch uses the row menu and quick actions."
      ],
      "performance": [
        "The shell is a fixed 660px tall and the message list is not virtualized; page long mailboxes.",
        "Sidebar width springs through one motion value, and a ResizeObserver detects the narrow layout."
      ],
      "aiNotes": [
        "Use as the shell of a mail or messaging app.",
        "Load folders, labels, and messages from your mail API, persist moves and stars through it, and connect Send and drafts to your outbox.",
        "Composes Arc avatar, animated-counter, tooltip, and button."
      ],
      "related": [
        "inbox-triage",
        "workspace-sidebar",
        "sidebar-rail",
        "notification-center",
        "swipe-actions"
      ],
      "usage": "import { InboxSidebar } from \"@/components/arc/blocks/inbox-sidebar/inbox-sidebar\";\n\nexport default function MailPage() {\n  return (\n    <InboxSidebar\n      onCollapsedChange={(collapsed) => localStorage.setItem(\"mail-rail\", String(collapsed))}\n    />\n  );\n}",
      "exports": [
        "InboxSidebar"
      ]
    },
    {
      "name": "team-members",
      "kind": "block",
      "title": "Team members",
      "description": "A members list where typed emails turn into chips and fly into pending invites as seats roll.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Search field",
        "Text morph",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/team-members",
      "registry": null,
      "keywords": [
        "react team members",
        "invite team members",
        "member management",
        "user roles table",
        "invite by email",
        "seat management",
        "workspace members"
      ],
      "whenToUse": [
        "The team or workspace settings page of a SaaS app.",
        "Inviting several people at once by typing or pasting emails.",
        "Member lists with roles, pending invites, and seat counts."
      ],
      "whenNotToUse": [
        "Use team-directory for a browsable people directory.",
        "Use roles-permissions to define what each role can do.",
        "Use tag-input for a general chip input outside invites."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-select"
      ],
      "accessibility": [
        "Role selects use Radix select with full keyboard support.",
        "Invite validation messages sit in a polite live region, and actions are announced in a status region."
      ],
      "motion": [
        "Email chips fly into the pending invites list and the seat count rolls.",
        "Rows enter and leave with layout animation.",
        "Reduced motion makes the flight and reflow instant."
      ],
      "responsive": [
        "Below 600px container width the header and invite row stack and the search goes full width.",
        "Below 420px each member row splits into the identity on top and last active, role, and remove underneath."
      ],
      "performance": [
        "Member rows are not virtualized; add paging for large organizations.",
        "Rows enter and leave with layout animation, and a ResizeObserver drives the height morph."
      ],
      "aiNotes": [
        "Use on the team or workspace settings page of a SaaS app.",
        "Load members, invites, and seat limits from your API and connect role changes, Send invite, Resend, Revoke, and Remove to it.",
        "Composes Arc avatar, button, search-field, text-morph, and animated-counter."
      ],
      "related": [
        "team-directory",
        "roles-permissions",
        "tag-input",
        "settings-page",
        "avatar"
      ],
      "usage": "import { TeamMembers } from \"@/components/arc/blocks/team-members/team-members\";\n\nexport default function TeamSettings() {\n  return (\n    <main>\n      <TeamMembers />\n    </main>\n  );\n}",
      "exports": [
        "TeamMembers"
      ]
    },
    {
      "name": "page-header",
      "kind": "block",
      "title": "Page header",
      "description": "A project page header that folds into a compact bar as you scroll, with tabs whose counts roll.",
      "tier": "free",
      "builtFrom": [
        "Button",
        "Badge",
        "Animated counter",
        "Avatar group",
        "Avatar",
        "Progress"
      ],
      "docs": "https://uiarc.dev/components/blocks/page-header",
      "registry": "https://uiarc.dev/r/page-header.json",
      "keywords": [
        "react page header",
        "project header",
        "sticky header on scroll",
        "condensing header",
        "record header with tabs",
        "breadcrumb header",
        "page title with actions"
      ],
      "whenToUse": [
        "The top of a project, repository, or record page with breadcrumbs, status, people, and tabs.",
        "Detail pages with long content where the header should fold into a compact bar as people scroll.",
        "Headers that need a few primary actions plus an overflow menu for the rest."
      ],
      "whenNotToUse": [
        "Use tabs and breadcrumb on their own when the page only needs navigation, not status and people.",
        "Use workspace-sidebar for app-level navigation between sections.",
        "Use settings-page for settings screens that need a section nav rather than a record header."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tabs",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "Tabs use Radix roles (tab, tabpanel) and the current breadcrumb carries aria-current.",
        "The icon-only actions trigger has aria-label=\"More actions\"; disabled items use aria-disabled.",
        "A polite role=\"status\" region announces results of Follow, Share, Copy link and Archive."
      ],
      "motion": [
        "Past a short scroll threshold the header folds into a compact bar; the tab indicator glides with a shared layoutId and counts roll with Animated counter.",
        "Reduced motion swaps springs for fades, uses instant scroll instead of smooth, and keeps widths static."
      ],
      "responsive": [
        "A ResizeObserver moves secondary actions into the menu when the action row runs out of room.",
        "Below a 560px container the root breadcrumb hides, the tab row scrolls sideways with a faded edge, and below 460px the create button drops to an icon.",
        "The header condenses from the scroll of its own inner scroller, not the window, so page content must live inside that scroller."
      ],
      "performance": [
        "Scroll handling only compares scrollTop to a 16px threshold, so it re-renders when crossing it, not on every frame.",
        "Tab counts roll with Animated counter and the tab indicator uses a shared layoutId; both are cheap at a handful of tabs."
      ],
      "aiNotes": [
        "Use at the top of a project, repository or record page that scrolls beneath it.",
        "The block takes no props: replace the in-file project, people and tab data with your own, or lift them into props.",
        "Follow, Share update, Copy link, New issue and Archive are simulated; wire them to your API and the breadcrumbs to your router.",
        "Composes Arc Button, Badge, Animated counter, Avatar, Avatar group and Progress."
      ],
      "related": [
        "tabs",
        "breadcrumb",
        "avatar-group",
        "dropdown-menu",
        "workspace-sidebar"
      ],
      "usage": "import { PageHeader } from \"@/components/arc/blocks/page-header/page-header\";\n\nexport default function ProjectPage() {\n  return (\n    <main>\n      <PageHeader />\n    </main>\n  );\n}",
      "exports": [
        "PageHeader"
      ]
    },
    {
      "name": "revenue-explorer",
      "kind": "block",
      "title": "Revenue explorer",
      "description": "A SaaS revenue chart you zoom with a brush while paths, axes and figures glide to the range.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/revenue-explorer",
      "registry": null,
      "keywords": [
        "react revenue chart",
        "mrr chart",
        "chart with brush zoom",
        "range selector chart",
        "saas analytics chart",
        "comparison line chart",
        "interactive area chart"
      ],
      "whenToUse": [
        "Monthly MRR or revenue broken into new, expansion, and churn on an analytics page.",
        "Charts where people need to zoom a date range with an overview brush and compare against a prior period."
      ],
      "whenNotToUse": [
        "Use mrr-waterfall to explain how MRR moved between two points in time.",
        "Use metric-explorer for a row of KPI cards that open into a chart.",
        "Use bar-chart or sparkline for a small chart inside a card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The brush window and both handles are role=\"slider\" with aria-valuenow and month-name aria-valuetext.",
        "Series toggles use aria-pressed and the comparison toggle is a role=\"switch\" with aria-checked.",
        "The plot is a focusable role=\"img\" with aria-describedby pointing at the live summary."
      ],
      "motion": [
        "Paths, axes and ticks glide to the new range on springs while totals roll with Animated counter.",
        "Reduced motion jumps values instantly and replaces movement with opacity changes."
      ],
      "responsive": [
        "Chart width follows its container through a ResizeObserver, so axes and paths recompute at any width.",
        "Below a 560px container padding tightens and the comparison context wraps under the KPIs.",
        "The plot uses touch-action pan-y so vertical page scroll still works, while the brush uses touch-action none and pointer capture for dragging."
      ],
      "performance": [
        "Geometry is computed from 24 months of data; paths and ticks animate on springs rather than a per-frame loop.",
        "It is SVG with no downsampling, so long daily series should be aggregated to months before passing them in."
      ],
      "aiNotes": [
        "Use for MRR or other monthly flow metrics (new, expansion, churn) on an analytics page.",
        "Replace DATA and the month calendar in revenue-data.ts with your billing series; geometry, summaries and y domains are computed from it.",
        "No props or callbacks; lift the data into props if you load it at runtime."
      ],
      "related": [
        "metric-explorer",
        "mrr-waterfall",
        "usage-forecast",
        "cohort-retention",
        "animated-counter"
      ],
      "usage": "import { RevenueExplorer } from \"@/components/arc/blocks/revenue-explorer/revenue-explorer\";\n\nexport default function AnalyticsPage() {\n  return (\n    <section>\n      <RevenueExplorer />\n    </section>\n  );\n}",
      "exports": [
        "RevenueExplorer"
      ]
    },
    {
      "name": "journey-flow",
      "kind": "block",
      "title": "Journey flow",
      "description": "A Sankey of user journeys whose flows morph by channel and trace a cohort on hover.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/journey-flow",
      "registry": null,
      "keywords": [
        "react sankey diagram",
        "user journey flow",
        "funnel flow chart",
        "sankey chart",
        "conversion paths",
        "user flow visualization"
      ],
      "whenToUse": [
        "Showing where users go between funnel stages, such as visit to signup to activation.",
        "Comparing flows across acquisition channels with a segment switch.",
        "Tracing one step to see which paths lead into and out of it."
      ],
      "whenNotToUse": [
        "Use cohort-retention to show how groups retain over time.",
        "Use bar-chart for a simple linear funnel without branching paths.",
        "Use metrics-dashboard for a general KPI overview."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Segments form a role=\"radiogroup\"; steps sit in a role=\"toolbar\" with roving tabIndex and aria-pressed.",
        "The diagram is role=\"img\" with a text summary; each step button's label includes its users and share of visitors.",
        "A polite live region announces visits and paid plans when the channel changes.",
        "The breakdown panel is a labelled aside, and the data table toggle exposes aria-expanded for a non-visual reading of the chart."
      ],
      "motion": [
        "Ribbons morph their widths on a spring slightly slower than a control when the segment changes; counts follow a beat later and never overshoot.",
        "Tracing dims everything outside the cohort; panel text settles with a short rise and digits roll in place.",
        "Reduced motion swaps the morph for an instant change with an opacity fade."
      ],
      "responsive": [
        "From an 860px container the detail panel sits beside the chart at 280px; below that it moves under the chart.",
        "Below 560px the segment switch fills the width and padding tightens, and below 480px step labels shrink to 10 to 11px.",
        "A ResizeObserver feeds the chart width, so ribbons re-lay out on resize rather than scaling."
      ],
      "performance": [
        "Ribbons are SVG paths computed from 13 sample nodes; very large graphs with many stages will get crowded and slower to morph.",
        "Segment changes morph ribbon widths on springs, with no loop running at rest."
      ],
      "aiNotes": [
        "Use to show where users go between funnel steps on an analytics page.",
        "Replace NODES, STAGES, LINKS and SEGMENTS in journey-flow-geometry.ts with your counts; layout, tracing and percentages are computed.",
        "No callbacks; selection is internal."
      ],
      "related": [
        "metrics-dashboard",
        "cohort-retention",
        "revenue-explorer",
        "segmented-control"
      ],
      "usage": "import { JourneyFlow } from \"@/components/arc/blocks/journey-flow/journey-flow\";\n\nexport default function FunnelPage() {\n  return <JourneyFlow className=\"funnel\" />;\n}",
      "exports": [
        "JourneyFlow"
      ]
    },
    {
      "name": "empty-states",
      "kind": "block",
      "title": "Empty states",
      "description": "Four empty states in one illustration whose shapes morph between scenes as you switch tabs.",
      "tier": "free",
      "docs": "https://uiarc.dev/components/blocks/empty-states",
      "registry": "https://uiarc.dev/r/empty-states.json",
      "keywords": [
        "react empty state",
        "empty state illustration",
        "no results state",
        "offline state",
        "404 page illustration",
        "inbox zero",
        "morphing svg illustration"
      ],
      "whenToUse": [
        "A reference for no results, offline, inbox zero, and 404 moments with one shared illustration style.",
        "Extracting one scene as a richer empty state for a key screen."
      ],
      "whenNotToUse": [
        "Use empty-state for a simple empty state with an icon, text, and action.",
        "Use onboarding-illustration for first-run screens that teach the product.",
        "Use skeleton while content is still loading."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Scenes use role=\"tablist\", \"tab\" and \"tabpanel\" with aria-selected and aria-controls.",
        "The illustration is a role=\"img\" labelled by the scene; a polite live region reports action results."
      ],
      "motion": [
        "Each shape morphs its path to the next scene with slight per-shape delays, and the tab indicator glides with a layoutId.",
        "Reduced motion swaps scenes with a short fade and no morph."
      ],
      "responsive": [
        "The block is a container query root; from a 560px container the title steps up a size.",
        "Pointer parallax only turns on for hover-capable fine pointers, so touch devices get a still illustration."
      ],
      "performance": [
        "Shape morphs run on a small requestAnimationFrame loop that stops as soon as every shape settles.",
        "A ResizeObserver springs the copy height when scenes change."
      ],
      "aiNotes": [
        "Use as a reference for no results, offline, inbox zero or 404 moments; usually you extract one scene rather than ship the tabbed gallery.",
        "Actions (clear filters, retry, refresh, search) are simulated; wire them to your real handlers.",
        "For a single simple empty state, prefer the empty-state component."
      ],
      "related": [
        "empty-state",
        "onboarding-illustration",
        "tabs"
      ],
      "usage": "import { EmptyStates } from \"@/components/arc/blocks/empty-states/empty-states\";\n\nexport default function EmptyGallery() {\n  return (\n    <main>\n      <EmptyStates />\n    </main>\n  );\n}",
      "exports": [
        "EmptyStates"
      ]
    },
    {
      "name": "login-centered",
      "kind": "block",
      "title": "Centered login",
      "description": "A passkey-first login card on a quiet ring backdrop that morphs through email and code.",
      "tier": "free",
      "builtFrom": [
        "Button",
        "Input",
        "OTP input"
      ],
      "docs": "https://uiarc.dev/components/blocks/login-centered",
      "registry": "https://uiarc.dev/r/login-centered.json",
      "keywords": [
        "react login page",
        "passkey login",
        "sign in page",
        "otp email login",
        "webauthn sign in ui",
        "sign up page",
        "auth card"
      ],
      "whenToUse": [
        "A full screen sign in or sign up page where passkeys are the main method.",
        "Auth flows that fall back to a six digit email code.",
        "Products that want one card for both sign in and sign up."
      ],
      "whenNotToUse": [
        "Use login-split for password sign in beside a brand photo.",
        "Use login-immersive for magic link auth over a full-bleed photo.",
        "Use sign-in for a simpler embedded sign in form."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The email field uses autoComplete=\"email\" and the code step uses the OTP input group.",
        "Busy buttons use aria-busy and aria-disabled so focus stays put during simulated requests.",
        "A polite role=\"status\" region announces step changes and errors."
      ],
      "motion": [
        "Steps slide and blur between passkey, email, code and done while the card height follows; the resend label rolls.",
        "Reduced motion swaps slides for fades and shows the passkey check without drawing its stroke."
      ],
      "responsive": [
        "Below a 520px container the stage and card padding shrink, and below 400px the SSO buttons tighten to small text.",
        "Pass fullScreen to fill 100dvh; without it the block sits in a framed card.",
        "The ring backdrop drifts with the pointer only while it moves over the screen, and stays still on touch and under reduced motion."
      ],
      "performance": [
        "The backdrop is a light SVG ring set moved by CSS variables from motion values, with no re-render per pointer move.",
        "Only the active step is mounted; the card height animates as steps change."
      ],
      "aiNotes": [
        "Use as a full screen sign in page (pass fullScreen) when passkeys are the primary method.",
        "Connect the passkey button to WebAuthn, the email step to your code sender, the code step to verification and SSO buttons to OAuth routes; demoCode and demoEmail exist only for the simulation.",
        "onSignIn receives the email, method and mode; redirect from there.",
        "Composes Arc Button, Input and OTP input."
      ],
      "related": [
        "sign-in",
        "login-split",
        "login-immersive",
        "signup-form",
        "otp-input"
      ],
      "usage": "import { LoginCentered } from \"@/components/arc/blocks/login-centered/login-centered\";\nimport { useRouter } from \"next/navigation\";\n\nexport default function SignInPage() {\n  const router = useRouter();\n  return (\n    <LoginCentered\n      fullScreen\n      termsHref=\"/terms\"\n      privacyHref=\"/privacy\"\n      onSignIn={() => router.push(\"/app\")}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Route sign ups to onboarding",
          "code": "<LoginCentered\n  fullScreen\n  onSignIn={(email, method, mode) =>\n    router.push(mode === \"sign-up\" ? \"/welcome\" : \"/app\")\n  }\n/>"
        }
      ],
      "exports": [
        "LoginCentered"
      ]
    },
    {
      "name": "login-immersive",
      "kind": "block",
      "title": "Immersive login",
      "description": "A glass magic link card over dusk photography that folds its envelope shut once sent.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Input"
      ],
      "docs": "https://uiarc.dev/components/blocks/login-immersive",
      "registry": null,
      "keywords": [
        "react magic link login",
        "passwordless sign in",
        "login page with background image",
        "glass login card",
        "email link sign in",
        "full screen login"
      ],
      "whenToUse": [
        "A magic link sign in page with a strong brand photo behind a glass card.",
        "Consumer or creative products where the sign in screen carries the brand mood."
      ],
      "whenNotToUse": [
        "Use login-centered for passkey-first auth with an email code.",
        "Use login-split for password sign in with a quieter layout.",
        "Use sign-in when the form sits inside an existing page."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The email field uses autoComplete=\"email\" and SSO buttons sit in a labelled role=\"group\".",
        "Busy actions use aria-disabled rather than disabled so focus is kept.",
        "A polite role=\"status\" region announces when the link is sent and resend availability."
      ],
      "motion": [
        "After sending, the form gives way to an envelope that folds shut and the resend countdown rolls.",
        "Reduced motion replaces the fold and slides with fades."
      ],
      "responsive": [
        "The card is min(100%, 420px) wide; below a 380px card width its padding and heading shrink and the SSO buttons tighten.",
        "Below a 520px container the top bar and footer padding shrink.",
        "The photo uses next/image with fill and sizes 100vw, so it covers any viewport."
      ],
      "performance": [
        "The photo loads eagerly with a blur placeholder; use a well compressed image since it is the largest asset.",
        "The card uses backdrop blur, which falls back to a solid surface where backdrop-filter is not supported."
      ],
      "aiNotes": [
        "Use as a full screen sign in or sign up page for magic link auth with a strong brand photo.",
        "Replace the imported photo with your own, connect the email form to your magic link sender, Open mail app to a mailto or deep link, and SSO to OAuth routes.",
        "Uses next/image; composes Arc Button and Input."
      ],
      "related": [
        "login-centered",
        "login-split",
        "sign-in",
        "signup-form"
      ],
      "usage": "import { LoginImmersive } from \"@/components/arc/blocks/login-immersive/login-immersive\";\n\nexport default function SignInPage() {\n  return (\n    <LoginImmersive\n      fullScreen\n      termsHref=\"/terms\"\n      privacyHref=\"/privacy\"\n      onSignIn={(email, method) => console.log(email, method)}\n    />\n  );\n}",
      "exports": [
        "LoginImmersive"
      ]
    },
    {
      "name": "security-settings",
      "kind": "block",
      "title": "Security settings",
      "description": "An account security page with guided two-factor setup, password change and active sessions.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Badge",
        "Checkbox",
        "Copy button",
        "Hold to confirm",
        "OTP input",
        "Password field",
        "Password strength"
      ],
      "docs": "https://uiarc.dev/components/blocks/security-settings",
      "registry": null,
      "keywords": [
        "react security settings",
        "two factor setup",
        "2fa setup flow",
        "totp qr code setup",
        "active sessions list",
        "change password form",
        "backup codes"
      ],
      "whenToUse": [
        "The security tab of account settings with two-factor setup, password change, and sessions.",
        "Guided TOTP setup that walks through scan, verify, and backup codes."
      ],
      "whenNotToUse": [
        "Use settings-page for general account and workspace settings.",
        "Use api-keys for managing developer tokens.",
        "Use audit-log for the history of security events."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Password errors render in a role=\"alert\" linked with aria-describedby and aria-invalid; the field uses autoComplete=\"current-password\".",
        "After revoking a session, focus moves to the neighbouring Revoke button or the sessions heading.",
        "A polite live region announces saves, verification and session changes."
      ],
      "motion": [
        "Two-factor steps advance along a rail with a gliding marker, and sections expand with a height spring.",
        "Reduced motion uses instant transitions and skips height animation."
      ],
      "responsive": [
        "Below a 640px container backup codes drop to two columns.",
        "Below 560px the QR code stacks above the setup key and section padding tightens."
      ],
      "performance": [
        "Sections are plain DOM with height springs when expanding; there is no loop or observer cost.",
        "The session list is not virtualized, which is fine for the handful of sessions an account normally has."
      ],
      "aiNotes": [
        "Use as the security tab of account settings.",
        "Verification, backup code download, password update and session revoke are simulated with timers; replace them with your TOTP check, code generator, password endpoint and session API.",
        "Replace the in-file ACCOUNT, SETUP_KEY, QR pattern and initialSessions with real data from props or a fetch.",
        "Composes Arc Button, Badge, Checkbox, Copy button, Hold to confirm, OTP input, Password field and Password strength."
      ],
      "related": [
        "settings-page",
        "api-keys",
        "password-strength",
        "otp-input"
      ],
      "usage": "import { SecuritySettings } from \"@/components/arc/blocks/security-settings/security-settings\";\n\nexport default function SecurityTab() {\n  return (\n    <section>\n      <SecuritySettings />\n    </section>\n  );\n}",
      "exports": [
        "SecuritySettings"
      ]
    },
    {
      "name": "cancel-flow",
      "kind": "block",
      "title": "Cancel flow",
      "description": "A cancellation flow that answers each reason with a tailored offer and a calm hold-to-cancel.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Badge",
        "Avatar",
        "Textarea",
        "Animated counter",
        "Hold to confirm"
      ],
      "docs": "https://uiarc.dev/components/blocks/cancel-flow",
      "registry": null,
      "keywords": [
        "react cancel subscription flow",
        "churn prevention flow",
        "cancellation survey",
        "retention offer",
        "pause subscription",
        "cancel flow saas"
      ],
      "whenToUse": [
        "Subscription cancellation in billing settings with a reason step and a retention offer.",
        "Flows that offer a discount, pause, or downgrade before confirming the cancel."
      ],
      "whenNotToUse": [
        "Use dialog with hold-to-confirm for a one step destructive action without offers.",
        "Use plan-comparison when people are choosing a plan rather than leaving.",
        "Use billing-overview for the billing page itself."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Reasons form a role=\"radiogroup\" with aria-checked cards.",
        "The pause length is a role=\"spinbutton\" whose aria-valuetext includes the resume date.",
        "A polite role=\"status\" region announces offer changes and the outcome."
      ],
      "motion": [
        "Steps slide between reason, offer and confirm, the selection highlight glides with a layoutId, and prices roll with Animated counter.",
        "Reduced motion swaps slides for fades and figures change without rolling."
      ],
      "responsive": [
        "Below a 520px container the reason cards drop to one column and the action buttons fill the width.",
        "The final hold works with mouse, touch, and keyboard through hold-to-confirm."
      ],
      "performance": [
        "Only the current step is mounted; prices roll with Animated counter.",
        "No observers or loops; the cost is a few spring transitions per step change."
      ],
      "aiNotes": [
        "Use in billing settings when someone cancels a subscription.",
        "Connect each offer's accept to your discount, pause or plan change API, the hold to your cancel endpoint and the note to your feedback store; all are simulated.",
        "Edit REASONS, PRICE, RESUME and IMPACTS in cancel-flow.tsx to match your plans.",
        "Composes Arc Button, Badge, Avatar, Textarea, Animated counter and Hold to confirm."
      ],
      "related": [
        "billing-overview",
        "plan-comparison",
        "hold-to-confirm",
        "stepper"
      ],
      "usage": "import { CancelFlow } from \"@/components/arc/blocks/cancel-flow/cancel-flow\";\n\nexport default function CancelPage() {\n  return (\n    <main>\n      <CancelFlow />\n    </main>\n  );\n}",
      "exports": [
        "CancelFlow"
      ]
    },
    {
      "name": "login-split",
      "kind": "block",
      "title": "Login split",
      "description": "A full screen split login: an email-first form that morphs into a password step beside a photo.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Input",
        "Password field",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/login-split",
      "registry": null,
      "keywords": [
        "react split login page",
        "login with image",
        "email first login",
        "password sign in page",
        "two column login",
        "sign in screen"
      ],
      "whenToUse": [
        "Password-based sign in with an email-first step beside a brand photo.",
        "Sign in pages that should show the recognised account before asking for a password."
      ],
      "whenNotToUse": [
        "Use login-centered for passkey-first auth.",
        "Use login-immersive for magic link auth.",
        "Use signup-form for account creation."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Fields use autoComplete=\"username\" and \"current-password\"; errors render in a role=\"alert\" tied with aria-describedby and aria-invalid.",
        "SSO buttons sit in a labelled role=\"group\".",
        "A polite role=\"status\" region announces step changes and simulated link actions."
      ],
      "motion": [
        "Continue morphs the email form into the password step with the account avatar, and labels roll between states.",
        "Reduced motion uses fades and shortens the simulated delays."
      ],
      "responsive": [
        "Below a 760px container the photo stacks under the form at a 460px minimum height; from 760px they sit side by side.",
        "From 1100px the columns split 5 to 6 and the quote moves to the photo's lower left.",
        "The photo uses sizes that request 100vw below 760px and 50vw above."
      ],
      "performance": [
        "The photo drifts on a 36 second CSS animation that people can pause, and it is off under reduced motion.",
        "The photo loads through next/image with a blur placeholder; there are no scripts running while idle."
      ],
      "aiNotes": [
        "Use as a password-based sign in page with fullScreen.",
        "Connect Continue to your account lookup, Sign in and SSO to your auth provider, and Forgot password to your reset email; replace the sample people list and the imported photo.",
        "Pass links for sign up, terms and privacy; onSignIn receives the account and method.",
        "Uses next/image; composes Arc Button, Input, Password field and Avatar."
      ],
      "related": [
        "login-centered",
        "login-immersive",
        "sign-in",
        "signup-form",
        "password-field"
      ],
      "usage": "import { LoginSplit } from \"@/components/arc/blocks/login-split/login-split\";\n\nexport default function SignInPage() {\n  return (\n    <LoginSplit\n      fullScreen\n      links={{ signUp: \"/sign-up\", terms: \"/terms\", privacy: \"/privacy\" }}\n      onSignIn={(account, method) => console.log(account.email, method)}\n    />\n  );\n}",
      "examples": [
        {
          "title": "Real links",
          "code": "<LoginSplit\n  fullScreen\n  links={{ signUp: \"/sign-up\", terms: \"/terms\", privacy: \"/privacy\" }}\n  onSignIn={account => router.push(\"/app\")}\n/>"
        }
      ],
      "exports": [
        "LoginSplit"
      ]
    },
    {
      "name": "cohort-retention",
      "kind": "block",
      "title": "Cohort retention",
      "description": "A cohort retention triangle that morphs into retention curves and rewashes by segment.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/cohort-retention",
      "registry": null,
      "keywords": [
        "react cohort retention chart",
        "retention triangle",
        "cohort analysis table",
        "retention curves",
        "churn cohort heatmap",
        "saas retention chart"
      ],
      "whenToUse": [
        "Growth or customer success dashboards comparing how signup cohorts retain month over month.",
        "Views that need both the cohort triangle and retention curves from the same data."
      ],
      "whenNotToUse": [
        "Use journey-flow for paths between funnel steps.",
        "Use activity-heatmap for activity by day rather than by cohort.",
        "Use revenue-explorer for revenue over time."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The chart is a focusable role=\"group\" with aria-describedby, and a polite live region reads the focused cohort, month and rate.",
        "Decorative cells and curves are aria-hidden; the legend is a labelled role=\"img\"."
      ],
      "motion": [
        "Cells morph into curves on the view switch and rewash their colour when the segment changes; headline figures roll with Animated counter.",
        "Reduced motion jumps values instantly and follows the pointer without springs."
      ],
      "responsive": [
        "Chart geometry follows container width through a ResizeObserver.",
        "Below a 560px container padding tightens and the KPI context wraps.",
        "The chart uses touch-action pan-y, so touch scrubbing does not block vertical page scroll."
      ],
      "performance": [
        "The triangle is 12 cohorts by 12 months of SVG cells; much larger grids mean many more animated cells.",
        "Cell colour changes use CSS transitions with a stagger instead of JavaScript per cell."
      ],
      "aiNotes": [
        "Use on a growth or customer success dashboard to compare how signup cohorts retain.",
        "Replace simulate() in cohort-data.ts with your cohort sizes and monthly retained counts; rates, averages and deltas are derived.",
        "Edit SEGMENTS in cohort-data.ts to match your segments. No props or callbacks.",
        "Composes Arc Segmented control and Animated counter."
      ],
      "related": [
        "revenue-explorer",
        "journey-flow",
        "metrics-dashboard",
        "activity-heatmap",
        "segmented-control"
      ],
      "usage": "import { CohortRetention } from \"@/components/arc/blocks/cohort-retention/cohort-retention\";\n\nexport default function GrowthPage() {\n  return (\n    <section>\n      <CohortRetention />\n    </section>\n  );\n}",
      "exports": [
        "CohortRetention"
      ]
    },
    {
      "name": "support-widget",
      "kind": "block",
      "title": "Support widget",
      "description": "An in-app help launcher that grows into live article search, reading, and a chat with your team.",
      "tier": "pro",
      "builtFrom": [
        "Search field",
        "Avatar",
        "Button",
        "Text morph",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/support-widget",
      "registry": null,
      "keywords": [
        "react support widget",
        "help center widget",
        "intercom style chat",
        "in-app help launcher",
        "customer support chat",
        "help article search"
      ],
      "whenToUse": [
        "An in-app help launcher with article search, an article reader, and chat.",
        "SaaS products that want one help entry point mounted near the app root."
      ],
      "whenNotToUse": [
        "Use support-conversation for a full page support chat.",
        "Use ai-composer for an AI prompt input without help articles.",
        "Use popover for a small anchored tip."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The open panel is a role=\"dialog\" whose label switches between Help and the conversation.",
        "The chat thread is a role=\"log\" with a screen reader only typing indicator, and a polite live region announces view changes.",
        "The launcher uses aria-haspopup=\"dialog\" and includes the unread reply count in its label."
      ],
      "motion": [
        "The launcher springs open into the panel, and views slide in the direction of travel.",
        "Unread badges and the launcher icon pop in on a snappy spring; changing text crossfades with a short rise.",
        "Reduced motion replaces scale, slide and rotate with plain fades."
      ],
      "responsive": [
        "The dock is positioned absolute inside the block's 680px demo stage; move it to position fixed when you mount it in a real app.",
        "Below a 519px container the stage is 640px tall, the dock sits closer to the corner, and the sample app nav hides.",
        "On coarse pointers opening chat focuses the panel instead of the text field, so the keyboard does not pop up on its own."
      ],
      "performance": [
        "Views mount one at a time and ResizeObservers animate the panel size between them.",
        "Article search filters an in-memory list; move it to your search API for a large help center."
      ],
      "aiNotes": [
        "Use as the in-app help entry point of a SaaS product, mounted once near the app root.",
        "Replace ARTICLES, TEAM, SUGGESTIONS and replyTo in support-widget-data.ts with your help center and real replies.",
        "Wire onSend to your inbox or chat provider; the sample app page behind the launcher is decoration to remove."
      ],
      "related": [
        "search-field",
        "avatar",
        "text-morph",
        "dialog",
        "ai-composer"
      ],
      "usage": "import { SupportWidget } from \"@/components/arc/blocks/support-widget/support-widget\";\n\nexport function AppShell() {\n  return (\n    <SupportWidget\n      onSend={message => sendToInbox(message)}\n      onOpenChange={open => track(open ? \"help_opened\" : \"help_closed\")}\n    />\n  );\n}",
      "exports": [
        "SupportWidget"
      ]
    },
    {
      "name": "uptime-status",
      "kind": "block",
      "title": "Uptime status",
      "description": "A status page with 90 day uptime bars, a morphing latency chart and an incident timeline.",
      "tier": "pro",
      "builtFrom": [
        "Badge",
        "Button",
        "Avatar",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/uptime-status",
      "registry": null,
      "keywords": [
        "react status page",
        "uptime monitor",
        "service status page",
        "uptime bars",
        "incident timeline",
        "90 day uptime"
      ],
      "whenToUse": [
        "A public or internal status page with per-service uptime history.",
        "Pages that combine 90 day uptime strips, response time, and an incident timeline."
      ],
      "whenNotToUse": [
        "Use activity-heatmap for activity counts rather than availability.",
        "Use timeline for an incident history on its own.",
        "Use sparkline for a small latency trend inside a card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each strip is a focusable role=\"group\" labelled with the service and described by its uptime summary.",
        "The latency chart has a text summary as its label, and polite live regions read out the inspected day or point.",
        "Incident headers are disclosure buttons with aria-expanded and aria-controls, and service rows use aria-pressed."
      ],
      "motion": [
        "The selected service highlight moves between rows with a shared layout spring, and the tooltip follows the pointer on a spring.",
        "The latency line morphs between services, and status text swaps with a short rise.",
        "Reduced motion removes the springs and moves the tooltip directly."
      ],
      "responsive": [
        "Below an 860px container the response time chart and incidents stack into one column.",
        "Below 560px row details and uptime labels hide and the status button moves under the summary.",
        "Strips and the latency chart use touch-action pan-y, so touch scrubbing keeps vertical scroll."
      ],
      "performance": [
        "Each service draws 90 SVG bars; many services on one page means hundreds of rects, which is fine but not free.",
        "Chart widths follow ResizeObservers; there is no animation loop."
      ],
      "aiNotes": [
        "Use for a public or internal status page.",
        "Replace services, daily history, latency samples and incidents in uptime-data.ts; bars, percentages and scales derive from them.",
        "Remove or rewire the simulated incident trigger to your monitoring feed."
      ],
      "related": [
        "activity-heatmap",
        "badge",
        "timeline",
        "sparkline"
      ],
      "usage": "import { UptimeStatus } from \"@/components/arc/blocks/uptime-status/uptime-status\";\n\nexport default function StatusPage() {\n  return (\n    <main>\n      <UptimeStatus />\n    </main>\n  );\n}",
      "exports": [
        "UptimeStatus"
      ]
    },
    {
      "name": "usage-forecast",
      "kind": "block",
      "title": "Usage forecast",
      "description": "A billing period usage chart with a forecast cone and a draggable budget that dates the crossing.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Animated counter",
        "Text morph"
      ],
      "docs": "https://uiarc.dev/components/blocks/usage-forecast",
      "registry": null,
      "keywords": [
        "react usage chart",
        "usage forecast",
        "budget alert chart",
        "billing usage graph",
        "metered billing dashboard",
        "spend forecast"
      ],
      "whenToUse": [
        "Billing or usage pages that warn customers before they hit a budget.",
        "Metered products that show month to date usage with a forecast range."
      ],
      "whenNotToUse": [
        "Use usage-meter for a simple progress bar against a limit.",
        "Use usage-pricing to estimate price from usage before buying.",
        "Use metric-explorer for general KPIs without a budget."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The plot and the budget handle are role=\"slider\" elements with aria-valuetext in plain words.",
        "The chart SVG has a full sentence label and is described by a data table; a polite live region reads the forecast summary.",
        "Alert rules are toggle buttons with aria-pressed and labels that include their current status."
      ],
      "motion": [
        "The budget line follows drags directly and settles on a snappy spring when released or moved by keyboard.",
        "The crossing marker and inspection panel follow the scrubbed day on springs; numbers roll with the animated counter.",
        "Reduced motion drops the springs and applies changes immediately."
      ],
      "responsive": [
        "Chart width follows its container through a ResizeObserver, and below 560px KPIs and the legend tighten.",
        "The plot uses touch-action pan-y for scrubbing, while the budget handle uses touch-action none so it can be dragged on touch."
      ],
      "performance": [
        "SVG over daily readings for one billing period; the forecast cone is computed once per change, not per frame.",
        "Dragging the budget updates one line and label; there is no loop at rest."
      ],
      "aiNotes": [
        "Use on a billing or usage page to warn customers before they hit a budget.",
        "Replace METRICS and daily readings in usage-data.ts with your metering data.",
        "Persist budget and alert rule changes to your billing settings; they are local state in the preview."
      ],
      "related": [
        "usage-meter",
        "usage-pricing",
        "billing-overview",
        "segmented-control",
        "metric-explorer"
      ],
      "usage": "import { UsageForecast } from \"@/components/arc/blocks/usage-forecast/usage-forecast\";\n\nexport default function BillingUsagePage() {\n  return (\n    <section>\n      <UsageForecast />\n    </section>\n  );\n}",
      "exports": [
        "UsageForecast"
      ]
    },
    {
      "name": "mrr-waterfall",
      "kind": "block",
      "title": "MRR waterfall",
      "description": "An MRR bridge whose floating bars morph by period and open the accounts behind each move.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/mrr-waterfall",
      "registry": null,
      "keywords": [
        "react waterfall chart",
        "mrr bridge",
        "revenue waterfall",
        "mrr movements chart",
        "saas revenue bridge",
        "floating bar chart"
      ],
      "whenToUse": [
        "Revenue or board reporting that explains how MRR moved from start to end of a period.",
        "Finance views where each movement should list the accounts behind it."
      ],
      "whenNotToUse": [
        "Use revenue-explorer for MRR over many months.",
        "Use bar-chart for plain comparisons that do not build from a start value.",
        "Use cohort-retention for retention by cohort."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Bars are a role=\"radiogroup\" of radio buttons with roving tabindex; each label reads the movement, amount and account count.",
        "The chart SVG carries a text summary, and the accounts panel is labelled by the selected movement.",
        "A polite live region announces each newly selected bar."
      ],
      "motion": [
        "Bars morph to new levels when the period changes, and the account list reorders with layout animation.",
        "Headings and figures swap with a short rise, and totals roll with the animated counter.",
        "Reduced motion swaps values without movement."
      ],
      "responsive": [
        "Below an 800px container the accounts panel moves under the chart.",
        "Below 560px the summary stats become a three column row with smaller figures.",
        "Hover previews apply only to mouse pointers; touch taps select a bar."
      ],
      "performance": [
        "A handful of SVG bars per period, sized by ResizeObserver; cheap at any size.",
        "Account lists are not virtualized, so cap or paginate large movements."
      ],
      "aiNotes": [
        "Use on a revenue or board reporting page to explain how MRR moved between periods.",
        "Replace PERIODS in mrr-data.ts with your billing movements; levels, axis and lists are computed from them."
      ],
      "related": [
        "revenue-explorer",
        "cohort-retention",
        "bar-chart",
        "segmented-control",
        "metric-explorer"
      ],
      "usage": "import { MrrWaterfall } from \"@/components/arc/blocks/mrr-waterfall/mrr-waterfall\";\n\nexport default function RevenuePage() {\n  return (\n    <section>\n      <MrrWaterfall />\n    </section>\n  );\n}",
      "exports": [
        "MrrWaterfall"
      ]
    },
    {
      "name": "webhooks",
      "kind": "block",
      "title": "Webhooks",
      "description": "A webhooks console with delivery history, a payload inspector and retries that roll the rate.",
      "tier": "pro",
      "builtFrom": [
        "Badge",
        "Button",
        "Checkbox",
        "Copy button",
        "Input",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/webhooks",
      "registry": null,
      "keywords": [
        "react webhooks dashboard",
        "webhook endpoints ui",
        "webhook delivery logs",
        "retry webhook",
        "payload inspector",
        "developer settings webhooks"
      ],
      "whenToUse": [
        "A developer settings page for webhook endpoints, delivery history, and retries.",
        "Consoles that need a payload and headers inspector next to the delivery list."
      ],
      "whenNotToUse": [
        "Use api-keys for managing tokens.",
        "Use audit-log for account activity rather than outgoing events.",
        "Use integrations for a catalogue of third-party connections."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Payload and headers use a tablist with roving tabindex, and the code panel is a focusable tabpanel.",
        "Endpoint and delivery rows mark the current item with aria-current and point at the detail region with aria-controls.",
        "Form errors render with role=\"alert\", and the add toggle exposes aria-expanded."
      ],
      "motion": [
        "Selection highlights and the active tab slide with shared layout springs; new rows grow in and flash once.",
        "A successful retry swaps to Resolved and the delivery rate rolls to its new value.",
        "Reduced motion replaces growth and rolls with instant or opacity-only changes."
      ],
      "responsive": [
        "Below an 880px container the delivery list and inspector stack, and below 760px endpoint rows wrap onto two lines.",
        "Below 540px padding tightens and the footer stacks; inner container queries also reflow the add form, delivery rows, and headers on their own widths."
      ],
      "performance": [
        "Delivery rows are not virtualized; paginate from your webhook service for busy endpoints.",
        "ResizeObservers size the volume chart; there is no animation loop."
      ],
      "aiNotes": [
        "Use on a developer settings page for managing webhook endpoints.",
        "Load endpoints, event volume and delivery attempts from your webhook service into webhooks-data.ts.",
        "Connect retry, send test event and endpoint creation to your API, and return the real signing secret once."
      ],
      "related": [
        "api-keys",
        "code-block",
        "copy-button",
        "integrations"
      ],
      "usage": "import { Webhooks } from \"@/components/arc/blocks/webhooks/webhooks\";\n\nexport default function DeveloperSettings() {\n  return (\n    <main>\n      <Webhooks />\n    </main>\n  );\n}",
      "exports": [
        "Webhooks"
      ]
    },
    {
      "name": "roles-permissions",
      "kind": "block",
      "title": "Roles and permissions",
      "description": "A permissions matrix with group toggles that ripple, role duplication and a review bar.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Animated counter",
        "Text morph",
        "Avatar group"
      ],
      "docs": "https://uiarc.dev/components/blocks/roles-permissions",
      "registry": null,
      "keywords": [
        "react permissions matrix",
        "roles and permissions ui",
        "rbac settings",
        "access control table",
        "role editor",
        "permission toggles"
      ],
      "whenToUse": [
        "Workspace settings where admins edit what each role can do.",
        "Permission matrices with grouped permissions and a review step before saving."
      ],
      "whenNotToUse": [
        "Use team-members to assign roles to people.",
        "Use switch rows in settings-page for a few on and off settings.",
        "Use security-settings for personal account security."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The matrix uses grid, row, rowheader and columnheader roles with a single tab stop.",
        "Cells are role=\"switch\" or role=\"checkbox\" buttons, group toggles report aria-checked=\"mixed\", and locked owner cells use aria-disabled.",
        "A polite status region announces toggles and save progress."
      ],
      "motion": [
        "Group toggles ripple across their permissions with a short stagger, and a column band follows the hovered role.",
        "Unsaved dots pop in and the review bar slides up while changes exist.",
        "Reduced motion removes the ripple and springs."
      ],
      "responsive": [
        "The matrix scrolls sideways with sticky permission labels, so role columns stay aligned on narrow screens.",
        "Below a 760px container the label column is 208px and role columns 88px; below 540px they shrink to 160px and 84px and group counts hide.",
        "The review bar stays sticky to the viewport while there are unsaved changes."
      ],
      "performance": [
        "Every cell is a real control and rows are not virtualized; very large permission sets should stay collapsed by group."
      ],
      "aiNotes": [
        "Use on a workspace settings page for editing what each role can do.",
        "Replace ROLES, GROUPS, GRANTED and MEMBERS in roles-permissions.tsx with data from your authorization API.",
        "Connect Save changes to your API; it only shows a simulated saved state."
      ],
      "related": [
        "team-members",
        "security-settings",
        "settings-page",
        "switch",
        "avatar-group"
      ],
      "usage": "import { RolesPermissions } from \"@/components/arc/blocks/roles-permissions/roles-permissions\";\n\nexport default function WorkspaceRoles() {\n  return (\n    <section>\n      <RolesPermissions />\n    </section>\n  );\n}",
      "exports": [
        "RolesPermissions"
      ]
    },
    {
      "name": "metric-explorer",
      "kind": "block",
      "title": "Metric explorer",
      "description": "KPI cards that open into a scrubbable chart and morph between metrics and ranges.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/metric-explorer",
      "registry": null,
      "keywords": [
        "react kpi cards",
        "metric explorer",
        "analytics dashboard chart",
        "expandable stat card",
        "kpi to chart",
        "saas metrics"
      ],
      "whenToUse": [
        "The top of a SaaS analytics or finance page with KPI cards that open into a chart.",
        "Dashboards where people move between a few metrics and date ranges."
      ],
      "whenNotToUse": [
        "Use stat-card or metric-card for static KPI tiles.",
        "Use revenue-explorer for a revenue breakdown with a range brush.",
        "Use metrics-dashboard for a full dashboard layout."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each card is a button whose label reads the value, change and range.",
        "The open chart uses a tablist and tabpanel, and the scrubber is a role=\"slider\" with a dated aria-valuetext.",
        "The close button advertises Escape with aria-keyshortcuts, and a polite live region announces changes."
      ],
      "motion": [
        "A card's surface morphs into the chart panel with a shared layout transition and back on close.",
        "The line morphs between metrics and ranges, and the tab highlight slides with the selection.",
        "Reduced motion replaces the morph and slides with instant changes."
      ],
      "responsive": [
        "Four cards per row drop to two below a 760px container.",
        "Below 640px tab values hide and the readout stacks; below 440px card padding and values shrink.",
        "The chart uses touch-action pan-y, so touch scrubbing does not block page scroll."
      ],
      "performance": [
        "Opening a card morphs one shared surface with a layoutId, and ResizeObservers fit the chart and tab indicator.",
        "SVG charts over daily series; aggregate long histories before passing them in."
      ],
      "aiNotes": [
        "Use at the top of a SaaS analytics or finance page.",
        "Replace the seeded series in metric-data.ts with daily values from your warehouse; range and comparison math stay the same."
      ],
      "related": [
        "metric-card",
        "sparkline",
        "revenue-explorer",
        "segmented-control"
      ],
      "usage": "import { MetricExplorer } from \"@/components/arc/blocks/metric-explorer/metric-explorer\";\n\nexport default function AnalyticsPage() {\n  return (\n    <main>\n      <MetricExplorer />\n    </main>\n  );\n}",
      "exports": [
        "MetricExplorer"
      ]
    },
    {
      "name": "activity-terrain",
      "kind": "block",
      "title": "Activity terrain",
      "description": "A 3D terrain of hourly activity you can orbit, zoom, fly into and flatten into a heatmap.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/activity-terrain",
      "registry": null,
      "keywords": [
        "react 3d heatmap",
        "activity terrain",
        "3d bar chart canvas",
        "hourly activity chart",
        "week by hour heatmap",
        "canvas data visualization"
      ],
      "whenToUse": [
        "Analytics or operations pages showing when a product is busiest across a week.",
        "Showcase dashboards where a 3D view of a 7 by 24 grid adds impact, with a flat heatmap fallback."
      ],
      "whenNotToUse": [
        "Use activity-heatmap for a plain 2D grid that is easier to read.",
        "Use bar-chart for totals by day or hour."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stage is a focusable group with a keyboard hint, and the canvas has a text summary as its label.",
        "Peak hours are aria-pressed buttons that fly to or leave the hour.",
        "A polite live region reads the highlighted hour's value."
      ],
      "motion": [
        "Columns rise in a wave on load and when the metric changes; camera moves use spring physics with momentum on drag.",
        "Flattening to a heatmap springs the camera to a top view.",
        "The engine watches prefers-reduced-motion and snaps the camera and heights with no wave."
      ],
      "responsive": [
        "Below an 880px container the stats move under the plot; below 560px the plot is 280px tall and the camera starts from a steeper angle.",
        "Touch devices swipe sideways to orbit while vertical swipes scroll the page, and the hint text switches for coarse pointers.",
        "Two-finger pinch zooms on touch."
      ],
      "performance": [
        "Draws on a 2D canvas with devicePixelRatio capped at 2.",
        "The requestAnimationFrame loop pauses offscreen via IntersectionObserver and in hidden tabs, and idles when nothing moves."
      ],
      "aiNotes": [
        "Use on an analytics or operations page to show when a product is busiest across a week.",
        "Replace METRICS in terrain-data.ts with hourly counts from your events warehouse; keep the 7 by 24 shape."
      ],
      "related": [
        "activity-heatmap",
        "revenue-globe",
        "customer-galaxy",
        "segmented-control"
      ],
      "usage": "import { ActivityTerrain } from \"@/components/arc/blocks/activity-terrain/activity-terrain\";\n\nexport default function UsagePatterns() {\n  return (\n    <section>\n      <ActivityTerrain />\n    </section>\n  );\n}",
      "exports": [
        "ActivityTerrain"
      ]
    },
    {
      "name": "revenue-globe",
      "kind": "block",
      "title": "Revenue globe",
      "description": "A dotted globe where live payments arc home to headquarters, with a replayable day.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/revenue-globe",
      "registry": null,
      "keywords": [
        "react globe",
        "revenue globe",
        "payments map animation",
        "3d globe canvas",
        "live transactions map",
        "dotted globe"
      ],
      "whenToUse": [
        "Revenue or operations overviews showing where payments come from right now.",
        "Showcase pages where a live globe with arcs and a day timeline adds impact."
      ],
      "whenNotToUse": [
        "Use a table or bar-chart when exact regional numbers matter more than the view.",
        "Use customer-galaxy to explore accounts rather than transactions.",
        "Use revenue-explorer for revenue over time."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The globe is a focusable region with a roledescription, instructions in its label and a text summary of revenue.",
        "The timeline is a role=\"slider\" whose value text reads the UTC time.",
        "Region and city rows are aria-pressed buttons, and a polite live region announces changes."
      ],
      "motion": [
        "Payments draw as arcs to headquarters while the globe drifts; drags rubber-band past the latitude limit.",
        "The tooltip follows the pointer on a spring, and totals roll with the animated counter.",
        "Reduced motion stops the auto rotation, hides replay and play controls, and shows the current state as Now."
      ],
      "responsive": [
        "Below an 880px container the panel moves under the globe and the globe height follows width with clamp.",
        "Below 600px the panel drops to one column; below 400px the clock takes its own row.",
        "The timeline slider uses touch-action none and pointer capture, so scrubbing works on touch."
      ],
      "performance": [
        "Draws on a 2D canvas with devicePixelRatio capped at 2.",
        "The animation loop pauses offscreen via IntersectionObserver and in hidden tabs."
      ],
      "aiNotes": [
        "Use on a revenue or operations overview to show where money comes from right now.",
        "Swap minuteTx in globe-data.ts for a query on your payments table, and set HQ, CITIES and REGIONS to your own."
      ],
      "related": [
        "customer-galaxy",
        "activity-terrain",
        "animated-counter",
        "revenue-explorer"
      ],
      "usage": "import { RevenueGlobe } from \"@/components/arc/blocks/revenue-globe/revenue-globe\";\n\nexport default function RevenueOverview() {\n  return (\n    <section>\n      <RevenueGlobe />\n    </section>\n  );\n}",
      "exports": [
        "RevenueGlobe"
      ]
    },
    {
      "name": "customer-galaxy",
      "kind": "block",
      "title": "Customer galaxy",
      "description": "2,400 accounts as a drifting galaxy that spirals into clusters by plan, region or health.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/customer-galaxy",
      "registry": null,
      "keywords": [
        "react data galaxy",
        "customer visualization",
        "canvas particle clusters",
        "zoomable scatter",
        "account explorer",
        "mrr visualization"
      ],
      "whenToUse": [
        "Customer success or revenue pages to explore where MRR and risk concentrate.",
        "Showcase views that cluster thousands of accounts by plan, region, or health."
      ],
      "whenNotToUse": [
        "Use customers-table when people need to find and act on specific accounts.",
        "Use semantic-zoom for hierarchical data like teams and projects.",
        "Use bar-chart for simple totals by segment."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stage is role=\"application\" with a roledescription, a text summary label and a keyboard hint.",
        "Grouping uses a tablist and tabpanel; cluster rows are aria-pressed buttons with MRR and risk in their labels.",
        "Zoom buttons have labels, and a polite live region announces selections."
      ],
      "motion": [
        "Accounts drift, then spiral into clusters with spring physics when grouping changes; flying to a cluster eases the camera.",
        "The tab pill slides with a shared layout spring, and panel rows reorder with layout animation.",
        "Reduced motion is passed to the engine, which snaps to layouts, and UI transitions become instant."
      ],
      "responsive": [
        "From an 880px container the totals panel sits beside the stage at 296px; below that it moves under it, and below 620px the stage is 440px tall.",
        "Touch uses pinch to zoom and double tap; the stage allows vertical page scroll until zoomed in.",
        "Mouse wheel zoom only takes over for ctrl or pinch gestures and mouse-like wheels, so trackpad scrolling still moves the page."
      ],
      "performance": [
        "Renders about 2,400 accounts on a 2D canvas with devicePixelRatio capped at 2.",
        "The loop pauses offscreen via IntersectionObserver and in hidden tabs."
      ],
      "aiNotes": [
        "Use on a customer success or revenue page to explore where MRR and risk concentrate.",
        "Replace the seeded accounts in galaxy-data.ts with your book, keeping plan, region, health and MRR fields."
      ],
      "related": [
        "revenue-globe",
        "semantic-zoom",
        "cohort-retention"
      ],
      "usage": "import { CustomerGalaxy } from \"@/components/arc/blocks/customer-galaxy/customer-galaxy\";\n\nexport default function CustomerSuccessPage() {\n  return (\n    <section>\n      <CustomerGalaxy />\n    </section>\n  );\n}",
      "exports": [
        "CustomerGalaxy"
      ]
    },
    {
      "name": "semantic-zoom",
      "kind": "block",
      "title": "Semantic zoom",
      "description": "An infinite-zoom map of a company that reveals teams, projects, tasks and people as you fly in.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/semantic-zoom",
      "registry": null,
      "keywords": [
        "react semantic zoom",
        "infinite zoom canvas",
        "zoomable org chart",
        "zoom ui",
        "portfolio map",
        "canvas zoom and pan"
      ],
      "whenToUse": [
        "Workspace or portfolio overviews where people zoom from the whole company to one task.",
        "Hierarchical data where each zoom level should show different detail."
      ],
      "whenNotToUse": [
        "Use tree-view for a plain hierarchy people scan and expand.",
        "Use project-board for working on tasks.",
        "Use customer-galaxy for flat data clustered by attributes."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The stage is role=\"application\" with a zoomable map roledescription, a text summary label and a keyboard hint.",
        "Breadcrumbs are a nav with aria-current=\"location\" on the current level.",
        "The tour toggle uses aria-pressed, the side panel is labelled, and a polite live region announces moves."
      ],
      "motion": [
        "The camera flies between levels on eased zooms, and detail fades in as each level gets large enough.",
        "Breadcrumbs, panel stats and people avatars enter with a blur rise or spring.",
        "Reduced motion is passed to the engine for instant camera moves, and UI transitions become instant."
      ],
      "responsive": [
        "From a 900px container the side panel sits beside the stage at 268px; below that it moves underneath, and below 600px the stage is 480px tall.",
        "Touch uses pinch to zoom and tap to fly in; the stage allows vertical page scroll until zoomed in.",
        "The mouse wheel always zooms over the stage, so leave page space around it for scrolling."
      ],
      "performance": [
        "Draws on 2D canvases sized by ResizeObserver with devicePixelRatio capped at 2.",
        "The loop pauses offscreen via IntersectionObserver and in hidden tabs."
      ],
      "aiNotes": [
        "Use on a workspace or portfolio overview so people can move from the whole company to a single task.",
        "Replace TEAMS, PROJECTS, TASKS and PEOPLE in zoom-data.ts; layout, rollups and labels derive from them."
      ],
      "related": [
        "customer-galaxy",
        "tree-view",
        "breadcrumb",
        "layout-morph"
      ],
      "usage": "import { SemanticZoom } from \"@/components/arc/blocks/semantic-zoom/semantic-zoom\";\n\nexport default function WorkspaceMap() {\n  return (\n    <section>\n      <SemanticZoom />\n    </section>\n  );\n}",
      "exports": [
        "SemanticZoom"
      ]
    },
    {
      "name": "layout-morph",
      "kind": "block",
      "title": "Layout morph",
      "description": "A 24 card gallery that flies between grid, masonry, list, fan, carousel and a 3D helix.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/layout-morph",
      "registry": null,
      "keywords": [
        "react layout animation",
        "gallery layout switcher",
        "masonry to grid animation",
        "3d helix gallery",
        "view mode toggle",
        "morphing gallery"
      ],
      "whenToUse": [
        "Catalogue, portfolio, or team pages where people browse the same set in different layouts.",
        "Showcase galleries that fly between grid, masonry, list, fan, carousel, and a 3D helix."
      ],
      "whenNotToUse": [
        "Use photo-grid for a simple gallery.",
        "Use carousel or cover-flow for one horizontal browsing style.",
        "Use lightbox-gallery when opening a full size image is the main job."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The view switch is a role=\"radiogroup\" with roving tabindex and labelled icon options.",
        "The stage is a group with a gallery roledescription whose label explains the keys.",
        "The open card is a role=\"dialog\"; the gallery behind it is hidden and the closed dialog is inert."
      ],
      "motion": [
        "Cards fly between layouts on springs, staggered outward from the chosen origin.",
        "Opening a card morphs it into the detail view.",
        "Reduced motion is passed to the engine and MotionConfig uses the user setting."
      ],
      "responsive": [
        "Below a 720px container the layout tabs become icon-only and the stage is 540px tall, shrinking to 500px and 460px below 480px and 360px.",
        "In scrolling layouts the stage takes touch-action none; wheel only rotates the ring for horizontal or shift scrolls."
      ],
      "performance": [
        "Cards are DOM elements moved with transforms, and the ring layouts spin one camera transform per frame instead of every card.",
        "The loop pauses offscreen via IntersectionObserver and in hidden tabs; keep the set near the 24 sample cards."
      ],
      "aiNotes": [
        "Use for a catalogue, portfolio or team page where people browse the same set in different ways.",
        "Replace ITEMS in morph-data.ts with your own photos and profiles; layouts, captions and totals derive from it.",
        "The save action in the open view is local state; connect it to your favourites API if needed."
      ],
      "related": [
        "photo-grid",
        "carousel",
        "cover-flow",
        "card-stack",
        "segmented-control"
      ],
      "usage": "import { LayoutMorph } from \"@/components/arc/blocks/layout-morph/layout-morph\";\n\nexport default function CataloguePage() {\n  return (\n    <main>\n      <LayoutMorph />\n    </main>\n  );\n}",
      "exports": [
        "LayoutMorph"
      ]
    },
    {
      "name": "agent-run",
      "kind": "block",
      "title": "Agent run",
      "description": "A single card that follows one AI coding agent through a task: steps stream in with the live one shimmering, tool calls open in place, and the agent stops on a two-file diff for you to approve or send back before it writes. When the run ends, the steps fold into one line and a result shows the files changed, the checks that passed and a pull request button.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Input",
        "Text shimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-run",
      "registry": null,
      "keywords": [
        "ai agent",
        "coding agent",
        "agent run",
        "tool calls",
        "approval",
        "human in the loop",
        "diff",
        "unified diff",
        "pull request",
        "steps",
        "timeline",
        "claude",
        "streaming",
        "autonomous agent",
        "agent trace"
      ],
      "whenToUse": [
        "Coding agents and background jobs that read, search, edit and test, where people watch progress and approve the change before it is written.",
        "Product surfaces that hand an agent a ticket and return a pull request, such as an issue tracker, a review tool or an IDE side panel.",
        "Demos and onboarding that explain how an agent works step by step, with a real approval gate."
      ],
      "whenNotToUse": [
        "Use research-run for a research agent that reads sources and writes a cited report.",
        "Use agent-approvals for an inbox of pending approvals across many agents and tools.",
        "Use agent-orchestrator when several agents work in parallel on parts of one job.",
        "Use code-review for reviewing a finished pull request line by line with comments."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card is a section named by the task. Steps are an ordered list named Steps; each expandable step is a button with aria-expanded and aria-controls.",
        "A polite live region announces each step as it starts, the wait for approval, the end of the run with the test count, and stopping. A second region reports the pull request, including that the preview only simulates it.",
        "Status is never color alone: the status word sits beside an icon, finished steps change to past tense with a check, and a sent back step reads Sent back with the note.",
        "Diff lines carry hidden Added and Removed prefixes; the + and − markers and the tints are decorative. The file tabs are a tablist with tabpanels, and the hidden panel is inert.",
        "Approving or rejecting moves focus to that step's row, opening the note moves focus into it, and cancelling returns it to Reject.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "New steps open their height on a critically damped spring and fade in; nothing bounces.",
        "The live verb shimmers with Text shimmer. When a step finishes, the verb crossfades to past tense (100ms out, 160ms in from 2px) while its slot follows the measured width, and the check draws its stroke once.",
        "The header status word swaps with the same crossfade; the timer uses tabular numerals in a reserved width.",
        "Every diff file shares one grid cell, so switching tabs only crossfades and the card never resizes. The reject note swaps in place while the footer height follows on the smooth spring.",
        "At the end the steps fold to one line and the result opens below; the closing message streams in chunks of a few words every 34ms, laid out in advance so nothing reflows.",
        "The pull request button keeps every label in one cell, so Open pull request, Opening and PR #482 opened crossfade without changing width.",
        "Reduced motion drops travel, fades and the shimmer and keeps every state, label and announcement."
      ],
      "responsive": [
        "The card is up to 720px wide and works from 320px. Below 520px the step durations hide, the step details lose their indent, the diffstat blocks hide and the approval buttons take a full row.",
        "Long step labels and file paths truncate on one line; the full path stays in the tab title and the panel label.",
        "Diff and tool output scroll sideways inside their own frame, so the card never scrolls horizontally."
      ],
      "performance": [
        "The simulation commits about 25 times a second from one requestAnimationFrame loop and pauses off screen, in a hidden tab and while waiting.",
        "Finished steps keep the same object and the rows are memoized, so only the live row re-renders on each beat.",
        "Streaming text renders its chunks once and only flips their opacity; there is no per-word animation."
      ],
      "aiNotes": [
        "Use it for one agent on one task with a human approval before writes. For a fleet of agents use agent-orchestrator; for a queue of approvals use agent-approvals.",
        "Pass run for a live agent. Each step needs id, mark, verb, doneVerb and status; add target, doneTarget, progress, io, text or change as they apply. The card derives labels: running shows verb + target, waiting shows Review + doneTarget, done shows doneVerb + doneTarget.",
        "An approval step carries change: { id, summary, files: [{ path, status, lines }], outcome }. Diff lines are { type: \"add\" | \"del\" | \"ctx\", old?, new?, text }.",
        "Marks: think, read, search, edit, terminal and review are lucide icons; npm, typescript and github are real brand marks. Pass icon on a step for your own.",
        "Set result when status is done: summary, files with additions and deletions, tests and checks. elapsedMs should count only time the agent worked.",
        "Do not show it as executing real code in a demo; the default script is simulated and the pull request is simulated unless onOpenPullRequest is wired."
      ],
      "related": [
        "research-run",
        "agent-approvals",
        "agent-orchestrator",
        "agent-tasks",
        "text-shimmer"
      ],
      "usage": "import { AgentRun } from \"@/components/arc/blocks/agent-run/agent-run\";\n\n// Simulated demo\n<AgentRun />\n\n// Live: build the state from your agent runtime's events\nexport function AgentPanel({ run, task }) {\n  return (\n    <AgentRun\n      task={task}\n      run={run} // { status: \"working\" | \"waiting\" | \"done\", elapsedMs, steps, result }\n      onApprove={change => agent.approve(change.id)}\n      onReject={(change, reason) => agent.reject(change.id, reason)}\n      onOpenPullRequest={() => agent.openPullRequest()} // resolves to { number, url }\n      onRestart={() => agent.restart()}\n    />\n  );\n}\n\n// A step while it runs, and an approval step waiting for a decision\nconst steps = [\n  { id: \"tests\", mark: \"npm\", verb: \"Running tests\", doneVerb: \"Ran tests\", doneTarget: \"12 passed\", status: \"running\", progress: { value: 8, total: 12 } },\n  { id: \"review\", mark: \"review\", verb: \"Preparing\", doneVerb: \"Approved\", doneTarget: \"changes to 2 files\", status: \"waiting\", change },\n];",
      "exports": [
        "AgentRun",
        "useSimulatedRun"
      ]
    },
    {
      "name": "week-calendar",
      "kind": "block",
      "title": "Week calendar",
      "description": "A week calendar where you drag to create, move, and resize events, with color coded calendars, day and agenda views, and swipes on phones.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/week-calendar",
      "registry": null,
      "keywords": [
        "react week calendar",
        "drag and drop calendar",
        "google calendar clone",
        "scheduler component",
        "time blocking calendar",
        "event scheduler",
        "react calendar with time zones"
      ],
      "whenToUse": [
        "Scheduling and planning screens where people create, move and resize time blocks across a week.",
        "Team or personal calendars that need several color coded calendars, all day events and an agenda on phones."
      ],
      "whenNotToUse": [
        "Use month-calendar for a month overview with events per day.",
        "Use calendar or date-picker to pick a date, and time-picker for a single time.",
        "Use availability-picker to collect open slots from someone."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each range is a role=\"grid\" labelled with its dates and described by screen reader instructions; every day is a labelled grid cell with its date and event count, and keyboard focus moves between them with the arrow keys.",
        "Events are buttons whose labels read the title, full date, start and end time, calendar and location; all day events and agenda rows follow the same pattern.",
        "The quick create form and event details are labelled dialogs that take focus, close on Escape or a press outside, and return focus to the event.",
        "A polite live region announces range changes, creates, moves, resizes, deletes and undos. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Focus shows the same fill as hover."
      ],
      "motion": [
        "Ranges slide in the direction of travel on a spring without overshoot, the header and all day row move with the grid, and a swipe on touch drags the neighbors along so letting go continues without a seam.",
        "Events animate to new positions with transforms only: a dragged event hops between 15 minute slots, and neighbors settle into their new columns after a drop or an edit.",
        "Popovers scale in from the edge that faces their event; the undo toast rises from the bottom.",
        "Reduced motion replaces slides and position springs with instant moves and short fades."
      ],
      "responsive": [
        "From a 1100px container a sidebar adds a mini month and calendar toggles; narrower, the toggles move into a Calendars popover.",
        "Between 640px and 900px the gutter and dates tighten and event blocks drop details that no longer fit, using container queries on each event.",
        "Below 640px the week becomes three days with Day and Agenda views, horizontal swipes change days, and a long press starts creating or moving so vertical swipes still scroll.",
        "The grid scrolls inside a height of clamp(480px, 72vh, 760px); set --calendar-height on the root to change it."
      ],
      "performance": [
        "Event blocks are memoized on primitive props, so a drag re-renders only the event that moves; position changes animate with transforms, never layout.",
        "Time zone conversion runs once per events change; the now line updates on each minute boundary.",
        "All events are plain DOM; for thousands of events per week, pass only the visible range."
      ],
      "aiNotes": [
        "Use for scheduling and planning screens where people create and rearrange time blocks; pass events and persist in onCreate, onUpdate and onDelete.",
        "Event times are Date instants. The grid shows them in timeZone (or the browser's zone), so store UTC and let the calendar format them.",
        "Map your calendars to calendars with a color each; hidden calendars start toggled off in the calendar list.",
        "sampleEvents() and SAMPLE_CALENDARS in week-calendar-data.ts are demo content to remove."
      ],
      "related": [
        "calendar",
        "date-picker",
        "time-picker",
        "popover",
        "availability-picker"
      ],
      "usage": "import { useState } from \"react\";\nimport { WeekCalendar, type CalendarEvent } from \"@/components/arc/blocks/week-calendar/week-calendar\";\n\nconst calendars = [\n  { id: \"work\", name: \"Work\", color: \"blue\" },\n  { id: \"personal\", name: \"Personal\", color: \"green\" },\n];\n\nexport default function SchedulePage({ initial }: { initial: CalendarEvent[] }) {\n  const [events, setEvents] = useState(initial);\n  return (\n    <WeekCalendar\n      events={events}\n      onEventsChange={setEvents}\n      calendars={calendars}\n      onCreate={event => api.create(event)}\n      onUpdate={event => api.update(event)}\n      onDelete={event => api.remove(event.id)}\n      startHour={6}\n      timeZone=\"America/Los_Angeles\"\n    />\n  );\n}",
      "exports": [
        "WeekCalendar"
      ]
    },
    {
      "name": "scroll-story",
      "kind": "block",
      "title": "Scroll story",
      "description": "A feature section where scrolling the copy drives a pinned product view through each step.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/scroll-story",
      "registry": null,
      "keywords": [
        "react scroll story",
        "scrollytelling",
        "sticky scroll section",
        "scroll driven animation",
        "product walkthrough on scroll",
        "apple style scroll section"
      ],
      "whenToUse": [
        "Marketing pages that walk through a product in a few steps as people scroll.",
        "Feature sections where a pinned product view should change with the copy."
      ],
      "whenNotToUse": [
        "Use scroll-highlight for a single paragraph that reveals with scroll.",
        "Use chaos-to-order for one before and after switch.",
        "Use feature-illustration for a static feature visual."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The scrolling viewport is a focusable, labelled region, so it scrolls with the keyboard.",
        "Step tabs use a tablist with roving tabindex, and the active step in the copy carries aria-current=\"step\".",
        "The product status and copy link and send results are announced through polite live regions."
      ],
      "motion": [
        "Scroll position drives a progress value that fills the step tabs, sorts messages on springs and draws the trend line.",
        "Springs are restated as stiffness and damping, so retargeting mid-scroll keeps velocity.",
        "Reduced motion jumps to steps instead of smooth scrolling and replaces the springs with short fades."
      ],
      "responsive": [
        "The story scrolls inside its own viewport of clamp(480px, 74vh, 600px), not the page.",
        "Below a 700px container the product view stacks above the copy and stays pinned while the steps scroll under it."
      ],
      "performance": [
        "Scroll is read in a passive listener and measured once per animation frame.",
        "Product states are memoized per step, so they do not re-render on every scroll frame."
      ],
      "aiNotes": [
        "Use on a marketing page to walk through a product in a few steps.",
        "Replace STEPS copy and the product states (MESSAGES, THEMES, TREND, TEAM, LINK) in scroll-story.tsx with your own.",
        "Below 700px the product view stacks above the copy and stays pinned; no extra layout work is needed."
      ],
      "related": [
        "text-reveal",
        "in-view-title"
      ],
      "usage": "import { ScrollStory } from \"@/components/arc/blocks/scroll-story/scroll-story\";\n\nexport default function FeaturesSection() {\n  return (\n    <section>\n      <ScrollStory />\n    </section>\n  );\n}",
      "exports": [
        "ScrollStory"
      ]
    },
    {
      "name": "spotlight-grid",
      "kind": "block",
      "title": "Spotlight grid",
      "description": "A feature grid where light follows the pointer, cards play live demos, and a click morphs a card into its detail.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/spotlight-grid",
      "registry": null,
      "keywords": [
        "react spotlight grid",
        "feature grid",
        "bento grid",
        "interactive feature section",
        "landing page features",
        "card spotlight hover",
        "expanding feature cards",
        "shared layout card morph"
      ],
      "whenToUse": [
        "A product landing page section that shows four features with live, hands-on previews.",
        "Developer tool marketing where a small working demo explains a feature better than copy.",
        "Pages that want a bento grid with one wide card per row and a detail view that opens in place."
      ],
      "whenNotToUse": [
        "Use use-case-gallery for a browsable set of customer or use case stories.",
        "Use expandable-card when you need a single card that expands, not a whole feature grid.",
        "Use feature-illustration for one static feature explained with an illustration."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The section is labelled by its heading. Card buttons carry aria-expanded and aria-controls pointing at the open detail region.",
        "The open detail is a role=\"region\" labelled by its title, and the grid behind it is made inert.",
        "Segmented controls are radiogroups with roving tabindex; the flag toggle is a role=\"switch\"; Approve uses aria-pressed.",
        "The latency chart is a role=\"img\" SVG whose label states the range and p95. Deploy and approval results are announced through a polite live region.",
        "Mini previews are aria-hidden; the card title and blurb carry the meaning."
      ],
      "motion": [
        "The card surface and title share layoutIds with the detail view, so opening grows the card on a spring with a little bounce and closing settles without overshoot.",
        "The border light tracks the pointer through springs and fades in and out; it only runs for mouse pointers.",
        "Hover previews: the latency sparkline redraws every 1.1s, the deploy counter ticks every 0.9s, the flag switch and comment pin spring in.",
        "Charts morph their path between time ranges; changed labels crossfade with a short blur.",
        "Reduced motion drops the layout morph, the pointer light, and the preview timers. Content fades or switches instantly and previews show their end state."
      ],
      "responsive": [
        "The root is a size container named spotlight. Below 520px the grid is one column, from 520px two columns with wide cards spanning both, from 820px three columns.",
        "Below 520px the detail view opens at the card's own position instead of the top of the grid, and cards, padding, and the chart get smaller.",
        "The pointer light and hover previews only run for a mouse; on touch, cards simply open on tap."
      ],
      "performance": [
        "The pointer light is driven by motion values and CSS variables, so pointer moves do not re-render React.",
        "Each card reads its offset with a ResizeObserver. Preview intervals only run while a card is hovered or focused.",
        "useInView starts the first sparkline and tick animations once, when the grid is 30% visible."
      ],
      "aiNotes": [
        "Use for a landing page feature section where each feature can be tried, not just described.",
        "All content lives in constants at the top of the file (CARDS, RANGES, SHIPS, COMMENTS). Edit those and the matching Mini and Detail components to present your own features.",
        "There are no props or callbacks; the deploy, flag, and approval interactions are simulated locally.",
        "Uses next/image with shared demo portraits from @/components/arc/lib/media, and Arc AnimatedCounter for numbers."
      ],
      "related": [
        "expandable-card",
        "scroll-story"
      ],
      "usage": "import { SpotlightGrid } from \"@/components/arc/blocks/spotlight-grid/spotlight-grid\";\n\nexport function FeaturesSection() {\n  return (\n    <section style={{ padding: \"96px 24px\" }}>\n      <SpotlightGrid />\n    </section>\n  );\n}",
      "exports": [
        "SpotlightGrid"
      ]
    },
    {
      "name": "invite-people",
      "kind": "block",
      "title": "Invite people",
      "description": "Invite teammates with roles, bulk paste validation, a seat meter, and avatars that fly into a pending list.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/invite-people",
      "registry": null,
      "keywords": [
        "react invite people",
        "invite teammates form",
        "email chip input",
        "workspace invite",
        "seat limit meter",
        "invite by email",
        "team invite with roles",
        "paste email list"
      ],
      "whenToUse": [
        "Inviting several people at once by name or email, with a role for each.",
        "Plans with a seat limit where people should see the effect of an invite before sending.",
        "Flows where users paste a messy list from a spreadsheet and need clear per-entry errors."
      ],
      "whenNotToUse": [
        "Use team-members to manage people who already joined.",
        "Use roles-permissions to edit what each role can do.",
        "Use tag-input for a plain chip field without a directory, roles, or seats."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The field is a combobox with aria-expanded, aria-controls, and aria-activedescendant pointing into a role=\"listbox\" of options; blocked options are aria-disabled.",
        "Role menus are role=\"menu\" with menuitemradio items and aria-checked.",
        "The seat bar is a role=\"meter\" with aria-valuetext such as \"12 of 10 seats, 2 over the limit\".",
        "Send stays focusable with aria-disabled while blocked and points aria-describedby at the error list or the seat summary.",
        "Every add, remove, send, join, and revoke is announced in a polite live region."
      ],
      "motion": [
        "Avatars share layoutIds, so a person flies from the suggestion into a chip, from the chip into the pending list, and from the row into the team stack when they join.",
        "Invalid chips shake when you try to send; the over-limit notice shakes on the same attempt.",
        "Labels that change length resize on a spring and crossfade their text; the field height springs as chips wrap.",
        "Reduced motion (MotionConfig reducedMotion=\"user\" plus a hydrated flag) removes shakes, offsets, and layout travel and sets durations to zero."
      ],
      "responsive": [
        "The card is at most 600px wide and is a size container. Below 440px pending rows move the status under the name and suggestions hide their title column.",
        "Below 480px viewport width the padding drops to 16px and the corner radius shrinks.",
        "The role menu is positioned inside the block and clamped 8px from its edges."
      ],
      "performance": [
        "AutoWidth and AutoHeight use one ResizeObserver each to spring sizes; there is no per-frame loop.",
        "Suggestions filter the in-memory directory on every keystroke and show at most five. For a large directory, search on the server and debounce.",
        "All simulated timers are tracked and cleared on unmount and on reset."
      ],
      "aiNotes": [
        "Use for inviting teammates to a workspace with per-person roles and a seat limit.",
        "Replace DIRECTORY, TEAM, INVITED, PLANS, and ROLES in invite-people-data.ts with your own data. parseToken and splitList handle names, bare emails, and \"Name <email>\" from pasted lists.",
        "There are no props or callbacks. Wire the send, resend, revoke, and upgrade functions in the component to your API; the timers there only simulate delivery and one accepted invite.",
        "Error chips explain each bad entry in plain words; clicking one moves its text back into the field for editing."
      ],
      "related": [
        "team-members",
        "roles-permissions",
        "tag-input",
        "multi-select",
        "avatar-group"
      ],
      "usage": "import { InvitePeople } from \"@/components/arc/blocks/invite-people/invite-people\";\n\nexport function InviteDialogBody() {\n  return <InvitePeople />;\n}",
      "exports": [
        "InvitePeople"
      ]
    },
    {
      "name": "cart-drawer",
      "kind": "block",
      "title": "Cart drawer",
      "description": "Products fly into the cart, counts roll, and a drawer handles quantities, shipping progress, and checkout.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/cart-drawer",
      "registry": null,
      "keywords": [
        "react cart drawer",
        "shopping cart drawer",
        "add to cart animation",
        "slide out cart",
        "ecommerce cart",
        "free shipping progress bar",
        "swipe to remove cart item",
        "promo code field"
      ],
      "whenToUse": [
        "Product listings where people add several items and review them in a side cart.",
        "Stores that show a free shipping threshold and promo codes before checkout.",
        "Demos of an add to cart interaction with clear feedback on where the item went."
      ],
      "whenNotToUse": [
        "Use checkout-flow for a full multi-step checkout with address and payment.",
        "Use checkout-summary when you only need the order summary and totals.",
        "Use drawer for a generic side panel that is not a cart."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The cart button has aria-haspopup=\"dialog\", aria-expanded, and a label with the item count. The drawer is role=\"dialog\" with aria-modal and moves focus to its close button.",
        "The shop behind an open drawer is inert, and the drawer body is inert while an order is placing.",
        "Quantity steppers are labelled groups; the minus button becomes \"Remove\" at quantity one. Swipe to remove has button equivalents.",
        "The shipping bar is a role=\"progressbar\"; an invalid promo sets aria-invalid and swaps the note under the field.",
        "Adds, removals, promo changes, and the placed order are announced in a polite live region."
      ],
      "motion": [
        "The product photo flies along a quadratic arc into the cart over 780ms, shrinking and tilting; the badge count only changes when it lands, then the cart bumps and rings.",
        "The drawer slides in on a critically damped spring. Rows collapse in height when removed and the totals roll with AnimatedCounter.",
        "Swipe a row left past 110px, or flick it, to remove; short drags snap back with the release velocity.",
        "The checkout button shares a layoutId with the confirmation disc, then a checkmark draws in.",
        "Reduced motion skips the flight and the bump, disables row dragging, fades the drawer instead of sliding it, and zeroes the remaining springs."
      ],
      "responsive": [
        "The root is a size container. Above 640px the grid has three columns; at 640px and below it has two, with tighter padding.",
        "At 380px and below the cart label hides, prices stack under names, and the drawer fills the block with a 4px inset.",
        "Rows accept horizontal drag with direction lock, so vertical scrolling in the drawer still works on touch."
      ],
      "performance": [
        "Each flight is driven by one motion value and transforms, not React state per frame; several flights can overlap.",
        "The drawer and scrim only mount while open; timers are cleared on unmount.",
        "Product images use next/image with sizes for 50vw on small screens and 260px otherwise."
      ],
      "aiNotes": [
        "Use for an ecommerce page where adding to cart and reviewing the cart happen without leaving the grid.",
        "Replace PRODUCTS (with PhotoId images from @/components/arc/lib/media) and the FREE_SHIPPING, SHIPPING, and PROMO constants; swap the image source for your own URLs if needed.",
        "There are no props. Connect the add, setQty, remove, applyPromo, and checkout functions to your cart store and payment flow; checkout currently waits 1.3s and shows a sample receipt.",
        "Composes Arc Button and AnimatedCounter."
      ],
      "related": [
        "checkout-summary",
        "drawer",
        "swipe-actions",
        "number-field"
      ],
      "usage": "import { CartDrawer } from \"@/components/arc/blocks/cart-drawer/cart-drawer\";\n\nexport function ShopPage() {\n  return <CartDrawer />;\n}",
      "exports": [
        "CartDrawer"
      ]
    },
    {
      "name": "pricing-calculator",
      "kind": "block",
      "title": "Pricing calculator",
      "description": "A seat slider and billing toggle drive plan cards with rolling prices and a moving recommended badge.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/pricing-calculator",
      "registry": null,
      "keywords": [
        "react pricing calculator",
        "seat pricing slider",
        "per seat pricing",
        "pricing page",
        "monthly yearly toggle",
        "saas pricing plans",
        "team size pricing",
        "volume discount pricing"
      ],
      "whenToUse": [
        "Per-seat pricing where showing the exact total for a team size helps people choose.",
        "Pricing pages with a monthly and yearly toggle and a visible yearly saving.",
        "Plans with seat caps or volume discounts that are hard to explain in a static table."
      ],
      "whenNotToUse": [
        "Use usage-pricing when price depends on metered usage rather than seats.",
        "Use plan-comparison for a feature by feature table across plans.",
        "Use billing-toggle when you only need the monthly and yearly switch."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The slider is a native range input labelled \"Team size\" with aria-valuetext such as \"20 seats\".",
        "Billing is a radiogroup with roving tabindex; plan buttons use aria-pressed for the selected plan.",
        "Each plan card is labelled by its heading, and each feature list is labelled with the plan name.",
        "Billing changes, plan picks, and the started trial are announced in a polite live region."
      ],
      "motion": [
        "The selection frame and the Recommended badge are shared layout elements that travel between cards on a spring.",
        "Prices and totals roll with AnimatedCounter; changed copy crossfades with a short blur.",
        "Features that appear or disappear with the seat count open and close in height.",
        "Reduced motion removes the offsets and blur and sets layout and height transitions to zero."
      ],
      "responsive": [
        "The root is a size container. At 860px and below the three plan cards stack in one column.",
        "At 560px and below padding and the title shrink, and the controls tighten.",
        "At 360px and below the savings line and switch button wrap onto several lines."
      ],
      "performance": [
        "State is a seat index, billing period, and picked plan; everything else is derived on render.",
        "Twenty three seat stops and three cards, so no memoization or virtualization is needed."
      ],
      "aiNotes": [
        "Use for per-seat SaaS pricing where the right plan depends on team size and billing period.",
        "Edit STOPS, PLANS, recommend, and seatPrice to match your pricing. Starter has maxSeats 10; picking more shows a limit note and a button to drop back to 10 seats.",
        "Volume pricing takes $2 off each seat from 25 seats and $4 from 100 on Team and Business.",
        "There are no props. Wire startTrial to your signup or checkout; it currently waits 1.1s. Composes Arc Button and AnimatedCounter."
      ],
      "related": [
        "usage-pricing",
        "plan-comparison",
        "billing-toggle",
        "slider"
      ],
      "usage": "import { PricingCalculator } from \"@/components/arc/blocks/pricing-calculator/pricing-calculator\";\n\nexport function PricingPage() {\n  return <PricingCalculator />;\n}",
      "exports": [
        "PricingCalculator"
      ]
    },
    {
      "name": "gantt-timeline",
      "kind": "block",
      "title": "Gantt timeline",
      "description": "A project timeline with draggable, resizable bars, dependency arrows that follow them, and animated zoom.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/gantt-timeline",
      "registry": null,
      "keywords": [
        "react gantt chart",
        "gantt timeline",
        "project timeline",
        "drag to reschedule",
        "task dependencies",
        "roadmap timeline",
        "project planning chart",
        "resizable task bars"
      ],
      "whenToUse": [
        "Planning work across weeks or months where tasks depend on each other.",
        "Roadmaps that people reschedule by dragging, with undo and conflict warnings.",
        "Phase based plans where finished phases can be folded into a summary bar."
      ],
      "whenNotToUse": [
        "Use project-board for status columns without dates.",
        "Use week-calendar for time slots within days.",
        "Use timeline for a read-only list of dated events."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Bars are role=\"button\" with aria-roledescription \"Task bar\" or \"Milestone\", aria-pressed for selection, and a label with name, dates, percent done, and a warning when a dependency is violated.",
        "Every bar is described by hidden text that explains the Alt and Alt Shift shortcuts.",
        "Folded rows are aria-hidden and inert; group toggles carry aria-expanded.",
        "The zoom control is a group of aria-pressed buttons. The footer status is role=\"status\" and reports each change and conflict.",
        "The calendar header, grid lines, arrows, and summary bars are aria-hidden."
      ],
      "motion": [
        "Bars, arrows, group summaries, and task rows read the same motion values, so they move together without drifting.",
        "After a drag, the bar springs to the snapped day from its release velocity; a dashed snap target and a date label show while dragging.",
        "Shortening past one day resists with a soft rubber band instead of stopping.",
        "Zoom animates the day scale on a spring while keeping the middle of the view fixed; the header tiers crossfade.",
        "Folding a group slides its rows up into the group row and fades them.",
        "Reduced motion jumps bars, rows, scale, and scroll to their targets and removes the crossfades."
      ],
      "responsive": [
        "When the scroller is under 560px wide, the sticky task column narrows from 248px to 132px.",
        "Bars under 96px wide hide their name and bars under 36px hide their percentage, through container queries on each bar.",
        "The chart scrolls horizontally, and dragging near either edge scrolls the view along with the bar.",
        "Below 480px viewport width the root padding and task row indent shrink."
      ],
      "performance": [
        "Drag, zoom, and row folding update motion values; React re-renders only on commit and when the snapped day changes.",
        "Each dependency arrow recomputes its path from motion values every frame while something moves; keep plans in the tens of tasks.",
        "The header and grid lines render one element per day at day zoom, so very long ranges add DOM weight."
      ],
      "aiNotes": [
        "Use for project plans with phases, dependencies, and drag to reschedule.",
        "The calendar range is computed once on mount from defaultTasks and today, starting on a Monday with a week before and three weeks after. In controlled mode pass defaultTasks as well, or the calendar spans the sample data.",
        "onTasksChange reports each move or resize as ISO start and end dates; persist change.id with those dates. Undo keeps the last 20 changes.",
        "Assignee avatars come from @/components/arc/lib/media PersonId portraits; tasks without one show a plain marker.",
        "Dependency conflicts are shown, not prevented. Validate on the server if a violation must be blocked."
      ],
      "related": [
        "project-board",
        "week-calendar",
        "timeline",
        "workflow-canvas",
        "tree-table"
      ],
      "usage": "\"use client\";\n\nimport { useState } from \"react\";\nimport { GanttTimeline } from \"@/components/arc/blocks/gantt-timeline/gantt-timeline\";\nimport type { GanttGroup, GanttTask } from \"@/components/arc/blocks/gantt-timeline/gantt-timeline\";\n\nconst groups: GanttGroup[] = [\n  { id: \"design\", name: \"Design\" },\n  { id: \"build\", name: \"Build\" },\n];\n\nconst initial: GanttTask[] = [\n  { id: \"wires\", name: \"Wireframes\", group: \"design\", start: \"2026-10-05\", end: \"2026-10-09\", progress: 1 },\n  { id: \"ui\", name: \"Visual design\", group: \"design\", start: \"2026-10-12\", end: \"2026-10-23\", progress: 0.4, dependsOn: [\"wires\"] },\n  { id: \"api\", name: \"API\", group: \"build\", start: \"2026-10-12\", end: \"2026-10-30\", progress: 0.2 },\n  { id: \"beta\", name: \"Beta\", group: \"build\", start: \"2026-11-02\", end: \"2026-11-02\", milestone: true, dependsOn: [\"ui\", \"api\"] },\n];\n\nexport function Roadmap() {\n  const [tasks, setTasks] = useState(initial);\n  return (\n    <GanttTimeline\n      title=\"Q4 roadmap\"\n      groups={groups}\n      defaultTasks={initial}\n      tasks={tasks}\n      onTasksChange={(next, change) => {\n        setTasks(next);\n        if (change.kind === \"move\" || change.kind === \"resize\") {\n          void fetch(\"/api/tasks/\" + change.id, { method: \"PATCH\", body: JSON.stringify({ start: change.start, end: change.end }) });\n        }\n      }}\n      today=\"2026-10-14\"\n      defaultCollapsed={[]}\n    />\n  );\n}",
      "exports": [
        "GanttTimeline",
        "GanttTask",
        "GanttGroup",
        "GanttChange"
      ]
    },
    {
      "name": "ai-chat",
      "kind": "block",
      "title": "AI chat",
      "description": "A full-screen AI chat where answers stream in as markdown with folding reasoning, tool chips and cited sources. People can switch chats and models, edit or retry a message into versions, rate replies and stop a reply mid-stream.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Copy button",
        "Hover card",
        "Tooltip",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-chat",
      "registry": null,
      "keywords": [
        "ai chat",
        "chatbot",
        "llm chat",
        "assistant",
        "streaming",
        "markdown",
        "reasoning",
        "thinking",
        "tool calls",
        "citations",
        "model switcher",
        "regenerate",
        "message versions",
        "chat history",
        "stop generating",
        "claude",
        "gpt",
        "gemini"
      ],
      "whenToUse": [
        "A multi turn assistant inside a product page, drawer or dashboard card, with streaming answers, reasoning, tools and citations.",
        "Any place users need to retry, edit, rate or copy answers and switch models mid conversation.",
        "Prototyping an LLM feature: with no props it plays a realistic scripted conversation and answers new messages with simulated replies."
      ],
      "whenNotToUse": [
        "Use ai-composer when you only need the prompt box with attachments, mentions and commands.",
        "Use ai-answer for a single question and a long, article like answer with sources.",
        "Use agent-run or research-run when the work is a multi step agent run with a plan and progress rather than a conversation.",
        "Use model-picker for a standalone model selector with pricing and context details."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The card is a labelled section. Each turn is an article; a reply that is still thinking or streaming sets aria-busy.",
        "A polite status region announces when a reply starts, finishes, stops or fails, model and chat changes, copy results and feedback. In the preview every scripted reply and rating is announced as simulated.",
        "The reasoning line and tool chips are buttons with aria-expanded and aria-controls. Citations are links named \"Source 2: title, domain\" and open a read only preview on hover or keyboard focus.",
        "Icon buttons carry specific names (Copy, Retry, Good response, Bad response, Edit, Attach files, Send message or Stop reply), and the thumbs expose aria-pressed.",
        "Status is never color alone: errors pair an icon with \"Reply failed\" and the message, a stopped reply says Stopped, and a failed tool shows an alert icon with its summary.",
        "The version switcher is a labelled group (\"Version 2 of 2\") with Previous and Next buttons.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Answers stream in chunks of three or four whole words every 34ms on a requestAnimationFrame clock (about 90 to 120 words per second). Each chunk is its own span that fades in once over 150ms; nothing already on screen re-renders, moves or blurs, and list markers and code fences never appear before their first word.",
        "Thinking and running tools use a calm text shimmer that stops when they finish. Labels such as Thinking to \"Thought for 4s\", tool states, the model chip and the title swap with one crossfade (100ms out, 160ms in from 2px) while the box follows the measured width on a critically damped spring.",
        "Reasoning, tool details and attachments fold open by height on a spring with no bounce. Switching versions or opening the editor glides the turn to its new height and fades the new content in.",
        "While stuck to the bottom the thread follows new lines on a soft lerp, so text glides up instead of jumping a line at a time. Scrolling up releases it and shows a jump to latest button.",
        "Switching chats crossfades the thread (out in 100ms, in over 160ms from 4px), restores that chat's scroll position and never replays finished replies. Menus fade and scale from their trigger.",
        "Reduced motion drops the chunk fade, travel and smooth scrolling and keeps every state, label and announcement."
      ],
      "responsive": [
        "It fills the screen: the header spans the full width while the conversation and composer stay in one centered column (760px by default, set --chat-column). Set --chat-height to embed it in a smaller area. It works from 320px wide.",
        "Padding, bubble width and the source list adapt to the card's own width through container queries, so it fits a sidebar as well as a page.",
        "The header title truncates before the model chip and New chat button move; code blocks scroll sideways inside themselves.",
        "On touch screens message actions are always visible instead of appearing on hover."
      ],
      "performance": [
        "Turns are memoized; while a reply streams only that turn re-renders, once per 34ms tick.",
        "Streaming text is parsed from the revealed slice each tick with a small linear parser, and settles into plain strings 240ms after the last chunk.",
        "The stream is aborted on Stop, on a new message and on unmount, and the reveal clock only runs while a reply is streaming."
      ],
      "aiNotes": [
        "Use it when the conversation itself is the product surface: support, internal assistants, docs Q&A or a copilot card. Wire `respond` to your API and map its stream to events; yield text deltas in whatever size they arrive, because the reveal clock paces them.",
        "Markdown support is deliberately small: paragraphs, ordered and bulleted lists, short headings, **bold**, `inline code`, fenced code blocks and [n] citations that index into the sources event (1 based). Keep answers in that subset.",
        "Send sources before the text that cites them. A citation with no matching source renders as a plain number.",
        "Tools are upserted by id: yield the same id with status \"running\", then \"done\" or \"error\", plus a short summary such as \"4 sources\" or \"40,213,880 rows\". Give a tool `sources` for a search or `input` and `output` for a query.",
        "Messages passed to `respond` follow the visible branch only. An edit or retry adds a version, and later turns belong to the version they followed.",
        "Real model names and marks are sample content. Use only models you actually offer."
      ],
      "related": [
        "ai-composer",
        "ai-answer",
        "model-picker",
        "research-run",
        "agent-run",
        "text-shimmer",
        "context-window"
      ],
      "usage": "import { AiChat } from \"@/components/arc/blocks/ai-chat/ai-chat\";\nimport type { ChatRespond } from \"@/components/arc/blocks/ai-chat/ai-chat\";\n\n// Map your API's stream onto chat events. Text can arrive in any chunk size; the chat paces it.\nconst respond: ChatRespond = async function* (messages, { signal, model }) {\n  const response = await fetch(\"/api/chat\", {\n    method: \"POST\",\n    body: JSON.stringify({ model: model.id, messages: messages.map(({ role, content }) => ({ role, content })) }),\n    signal,\n  });\n  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();\n  while (true) {\n    const { value, done } = await reader.read();\n    if (done) break;\n    yield { type: \"text\", delta: value };\n  }\n};\n\nexport function SupportChat() {\n  return <AiChat respond={respond} onFeedback={(message, value) => track(\"reply_feedback\", { id: message.id, value })} />;\n}",
      "exports": [
        "AiChat"
      ]
    },
    {
      "name": "checkout-summary",
      "kind": "block",
      "title": "Checkout summary",
      "description": "An order summary with a promo code that becomes a chip, discount and tax lines, and pay button states.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/checkout-summary",
      "registry": null,
      "keywords": [
        "react checkout summary",
        "order summary",
        "promo code field",
        "coupon input",
        "pay button states",
        "checkout total",
        "stripe checkout summary"
      ],
      "whenToUse": [
        "The last step before a hosted checkout such as a Stripe session redirect.",
        "A single product purchase with launch discounts or promo codes.",
        "A pricing page side panel that confirms the final total."
      ],
      "whenNotToUse": [
        "Use checkout-flow when you collect shipping and card details in the page.",
        "Use cart-drawer for a cart with several items and quantities.",
        "Use billing-overview for an existing subscription's invoices and plan."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The block is a section labelled by title with an h3 heading.",
        "The promo field has a visually hidden label. Errors set aria-invalid, aria-describedby, and a role=\"alert\" message.",
        "The applied code chip has a remove button labelled Remove code followed by the code.",
        "The pay button is aria-live=\"polite\", so Processing, Paid, and Try again are announced. A payment error adds a role=\"alert\" line.",
        "The total has visually hidden formatted text, while the rolling digits are aria-hidden."
      ],
      "motion": [
        "Adjustment and coupon lines open with a height spring and fade.",
        "The promo form swaps to the applied chip with a small scale and blur. An invalid code shakes the field horizontally for 360ms.",
        "The total rolls through AnimatedCounter. The pay button's label and icon swap with a rise and blur on each state change, and a spinner turns while processing.",
        "Reduced motion skips the shake, makes lines appear instantly, reduces swaps to fades, and slows the spinner to 1.6s per turn."
      ],
      "responsive": [
        "The card is up to 440px wide and fills narrower containers. Padding scales between 20px and 28px with the viewport.",
        "The product name truncates on one line; the price column stays unwrapped.",
        "Hover styles apply only on fine pointers. Buttons scale down slightly on press."
      ],
      "performance": [
        "One pageshow listener per instance. No timers except those in the sample handlers.",
        "Only the total uses AnimatedCounter. Line and button animations run on state changes only."
      ],
      "aiNotes": [
        "Choose it for a one-product order summary before payment. Use checkout-flow for a multi-step checkout with address and payment fields, and cart-drawer for many items.",
        "Pass either onPay for a client-side payment or form for a server route that redirects to a hosted checkout. With form, the button shows Processing on submit and resets on back-forward cache restore.",
        "Always pass item in production. Without it the block uses a sample order, a WELCOME10 test code, and a simulated payment.",
        "Coupon amounts are positive in CheckoutCouponResult and are subtracted. The subtotal never goes below zero; decimals switch to two when any amount is fractional."
      ],
      "related": [
        "cart-drawer",
        "billing-toggle",
        "action-button",
        "money-input"
      ],
      "usage": "import { CheckoutSummary } from \"@/components/arc/blocks/checkout-summary/checkout-summary\";\n\nexport function Checkout() {\n  return (\n    <CheckoutSummary\n      item={{ name: \"Arc Pro\", description: \"All components and blocks\", price: 199, period: \"one payment\" }}\n      adjustments={[{ id: \"launch\", label: \"Launch price\", amount: -50, note: \"38 of 50 left\" }]}\n      coupon={{\n        onApply: async code => {\n          const res = await fetch(\"/api/coupon\", { method: \"POST\", body: JSON.stringify({ code }) });\n          return res.json();\n        },\n      }}\n      tax={{ label: \"Tax\", note: \"Calculated at checkout\" }}\n      form={{ action: \"/api/checkout\", fields: { plan: \"pro\" } }}\n      payLabel={total => `Continue to payment, ${total}`}\n    />\n  );\n}",
      "exports": [
        "CheckoutSummary",
        "CheckoutSummaryItem",
        "CheckoutSummaryLine"
      ]
    },
    {
      "name": "site-header",
      "kind": "block",
      "title": "Site header",
      "description": "A sticky website header that turns solid on scroll, with a gliding active link, mega menu panels, and a mobile sheet.",
      "tier": "free",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/site-header",
      "registry": "https://uiarc.dev/r/site-header.json",
      "keywords": [
        "react site header",
        "marketing navbar",
        "mega menu",
        "responsive navbar",
        "sticky header",
        "mobile menu sheet",
        "landing page header"
      ],
      "whenToUse": [
        "The top bar of a marketing site, landing page, or docs site.",
        "Product sites with grouped destinations that deserve a mega menu with a feature card.",
        "Pages that need one clear sign-up action in the header."
      ],
      "whenNotToUse": [
        "Use workspace-sidebar or sidebar-rail inside a signed-in app.",
        "Use morph-nav for a single-surface animated navigation with built-in search.",
        "Use menubar for application commands."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A labelled nav landmark; the current item is marked with aria-current.",
        "Panel triggers expose aria-expanded; outside presses and Escape close whichever layer is open.",
        "The mobile sheet locks page scroll while open and returns focus to the menu button when closed."
      ],
      "motion": [
        "Mega panels open on hover after a short intent delay and close after a leave grace; switching items morphs the panel to the new content.",
        "The header background turns solid past scrollThreshold.",
        "Reduced motion removes panel travel and uses fades."
      ],
      "responsive": [
        "It is a container-query component: below 760px of its own width the links, panels, and wide actions hide and a menu button opens a sheet.",
        "From 760px the sheet and scrim are removed; widening the container closes an open sheet.",
        "Hover-to-open panels only apply to fine pointers."
      ],
      "performance": [
        "Panels mount only while open; the scroll listener updates a single solid-state attribute.",
        "Feature images use next/image with sizes set, so they load only when a panel opens."
      ],
      "aiNotes": [
        "Choose it for marketing and docs sites. For in-app navigation use workspace-sidebar or sidebar-rail.",
        "The defaults are Arc sample content; always pass brand, items, and actions for real sites.",
        "Links render as anchors when an href is set; wire onNavigate for client-side routing or analytics.",
        "The mega variant uses next/image for feature images; allow their host in your image config."
      ],
      "related": [
        "morph-nav",
        "site-footer",
        "hero-section",
        "breadcrumb"
      ],
      "usage": "import { SiteHeader } from \"@/components/arc/blocks/site-header/site-header\";\n\nexport function MarketingHeader() {\n  return (\n    <SiteHeader\n      variant=\"simple\"\n      brand={{ name: \"Acme\", href: \"/\", mark: <Logo /> }}\n      items={[\n        { value: \"product\", label: \"Product\", href: \"/product\" },\n        { value: \"pricing\", label: \"Pricing\", href: \"/pricing\" },\n        { value: \"docs\", label: \"Docs\", href: \"/docs\" },\n      ]}\n      current=\"pricing\"\n      secondaryAction={{ label: \"Sign in\", href: \"/sign-in\" }}\n      primaryAction={{ label: \"Start free\", href: \"/sign-up\" }}\n    />\n  );\n}",
      "exports": [
        "SiteHeader",
        "SiteHeaderBlock"
      ]
    },
    {
      "name": "site-footer",
      "kind": "block",
      "title": "Site footer",
      "description": "A website footer with link columns and newsletter, a minimal layout, and a large fading Arc mark.",
      "tier": "free",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/site-footer",
      "registry": "https://uiarc.dev/r/site-footer.json",
      "keywords": [
        "react footer",
        "site footer",
        "footer with newsletter",
        "footer link columns",
        "minimal footer",
        "marketing footer"
      ],
      "whenToUse": [
        "The closing section of a marketing, docs, or product site.",
        "Footers that need a newsletter signup and a status link.",
        "Minimal one-row footers for app shells and docs."
      ],
      "whenNotToUse": [
        "Use a plain row with breadcrumb or links inside signed-in app screens.",
        "Use contact-section when the main goal is to start a conversation."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Renders a footer landmark with labelled link groups.",
        "The newsletter validates in place with a message tied to the field, and confirms without a toast.",
        "External links show an outward arrow and open in a new tab.",
        "Status tone is paired with its label, not shown by color alone."
      ],
      "motion": [
        "The subscribe button morphs to its confirmed state while the form keeps its width.",
        "The logo variant's large mark fades toward the bottom edge.",
        "Reduced motion swaps states without travel."
      ],
      "responsive": [
        "Container queries: below 760px the newsletter stacks above the columns; below 520px columns become two per row and the bottom row stacks.",
        "Link rows wrap rather than truncate."
      ],
      "performance": [
        "Static markup apart from the newsletter form; no observers or timers."
      ],
      "aiNotes": [
        "Defaults are Arc sample content; pass brand, columns, legal, socials, and newsletter for real sites.",
        "Pass newsletter={null} and status={null} when you have no backend for them.",
        "Composes Arc Button and SegmentedControl."
      ],
      "related": [
        "site-header",
        "hero-section",
        "faq-section",
        "contact-section"
      ],
      "usage": "import { SiteFooter } from \"@/components/arc/blocks/site-footer/site-footer\";\n\nexport function Footer() {\n  return (\n    <SiteFooter\n      brand={{ name: \"Acme\", href: \"/\" }}\n      tagline=\"Scheduling for small clinics.\"\n      columns={[\n        { title: \"Product\", links: [{ label: \"Pricing\", href: \"/pricing\" }, { label: \"Changelog\", href: \"/changelog\" }] },\n        { title: \"Company\", links: [{ label: \"About\", href: \"/about\" }, { label: \"Careers\", href: \"/careers\" }] },\n      ]}\n      legal={[{ label: \"Privacy\", href: \"/privacy\" }, { label: \"Terms\", href: \"/terms\" }]}\n      newsletter={{ onSubscribe: email => subscribe(email) }}\n      status={{ label: \"All systems normal\", tone: \"success\", href: \"https://status.example.com\" }}\n    />\n  );\n}",
      "exports": [
        "SiteFooter",
        "SiteFooterLink",
        "SiteFooterBlock"
      ]
    },
    {
      "name": "hero-section",
      "kind": "block",
      "title": "Hero section",
      "description": "Three full screen SaaS heroes: a live dashboard rising from the bottom edge over a drifting mesh, a workflow graph that routes sample events node by node, and editorial type over a mesh gradient.",
      "tier": "free",
      "builtFrom": [
        "Button",
        "Line chart",
        "Sparkline",
        "Segmented control",
        "Avatar",
        "Gradient mesh"
      ],
      "docs": "https://uiarc.dev/components/blocks/hero-section",
      "registry": "https://uiarc.dev/r/hero-section.json",
      "keywords": [
        "react hero section",
        "saas hero",
        "landing page hero",
        "hero with dashboard screenshot",
        "mesh gradient hero",
        "workflow hero",
        "perspective screenshot hero"
      ],
      "whenToUse": [
        "The first section of a SaaS landing page, when the product screen is the pitch (Screenshot).",
        "Developer tools, automation, and infrastructure products, where showing the product working is the pitch (Workflow).",
        "A launch or download page that wants one confident line and a sense of atmosphere (Mesh)."
      ],
      "whenNotToUse": [
        "Use page-header for titles inside app screens.",
        "Use hero-signup when the goal is collecting an email in the first section."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each design has one h1, and the calls to action are real links or buttons with visible text.",
        "The dashboard is a labelled group of sample data; its range control is a segmented control and the chart keeps its screen reader table.",
        "The workflow graph is a labelled group; each step's state has a text label (waiting, running, done, skipped) and its result is written out, never shown by color alone.",
        "The mesh and glow are decorative and hidden from assistive technology. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Screenshot: the copy rises in reading order, then the window rises in behind it and settles flat from a tilt.",
        "Workflow: each step turns to the accent while it runs, the data draws along the edge to the next one, and the result and time swap in with a small spring. The run pauses while the hero is off screen or the tab is hidden.",
        "Mesh: the gradient drifts on the Gradient mesh editor's seamless 20 second loop.",
        "Reduced motion removes every transform: no rise or tilt, the mesh draws one still frame, content fades in briefly, and the workflow shows each event as its finished run without playing; Send test event moves to the next one at once."
      ],
      "responsive": [
        "Every design is one full screen (100svh, full width) on desktop and phones. Container queries: the split layout stacks below 900px; below 560px padding tightens and actions fill the width.",
        "The dashboard and the workflow graph are drawn at a fixed design size and scaled like a screenshot. The dashboard switches to a compact layout without the sidebar under 720px and runs off the bottom edge into a fade; the graph switches to compact action nodes on phones.",
        "On phones the logo row of the Mesh design becomes a grid."
      ],
      "performance": [
        "One WebGL canvas at most per hero, drawn at one pixel per CSS pixel and thirty frames a second, only while on screen and the tab is visible. A static CSS render of the same mesh shows first.",
        "The workflow advances one React state step every few hundred milliseconds only while the hero is on screen and the tab is visible; nothing else loops at rest."
      ],
      "aiNotes": [
        "The three designs carry sample products (Lumen, Relay, Cadence). Replace the copy in hero-lumen.tsx, hero-relay.tsx, and hero-cadence.tsx, the dashboard data in hero-lumen-data.ts, and the sample events at the top of hero-relay.tsx, or pass title and friends for a plain content hero.",
        "Mesh colors live in CSS per theme (hero-lumen.module.css and hero-cadence.module.css), in the variables --mesh-base and --mesh-1 onward.",
        "Brand marks are sample customers from public/block-logos, not endorsements.",
        "Composes Arc Button, LineChart, Sparkline, SegmentedControl, and Avatar."
      ],
      "related": [
        "site-header",
        "logo-marquee",
        "hero-signup"
      ],
      "usage": "import { HeroSection } from \"@/components/arc/blocks/hero-section/hero-section\";\n\nexport function Hero() {\n  return (\n    <HeroSection\n      variant=\"centered\"\n      primaryAction={{ label: \"Start free trial\", href: \"/sign-up\" }}\n      secondaryAction={{ label: \"Book a demo\", href: \"/demo\" }}\n    />\n  );\n}",
      "exports": [
        "HeroSection",
        "HeroLumen",
        "HeroRelay",
        "HeroCadence",
        "HeroMesh",
        "HeroSectionBlock"
      ]
    },
    {
      "name": "faq-section",
      "kind": "block",
      "title": "FAQ section",
      "description": "FAQs as an accordion, a topic rail, or a searchable list that highlights matches.",
      "tier": "free",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/faq-section",
      "registry": "https://uiarc.dev/r/faq-section.json",
      "keywords": [
        "react faq section",
        "faq accordion",
        "searchable faq",
        "faq with categories",
        "frequently asked questions component"
      ],
      "whenToUse": [
        "FAQ sections on pricing, product, and support pages.",
        "Long FAQs that benefit from categories or live search."
      ],
      "whenNotToUse": [
        "Use accordion for generic collapsible content that is not a question list.",
        "Use a docs page when answers need code, images, or long structure."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Questions are buttons with aria-expanded controlling their answer region.",
        "Search matches are wrapped in mark elements; closed items that match only in the answer show a short excerpt.",
        "The search variant uses Arc SearchField with a real label."
      ],
      "motion": [
        "Answers open with height and opacity together and the plus icon turns.",
        "Filtering and category changes move remaining items with layout animation.",
        "Reduced motion opens and filters instantly."
      ],
      "responsive": [
        "Container queries: below 760px the category rail becomes a horizontal scrolling row above the questions; below 480px padding tightens."
      ],
      "performance": [
        "Search filters in memory on each keystroke; fine for dozens of items, not thousands."
      ],
      "aiNotes": [
        "Keep answers plain text; the search variant matches and highlights inside them.",
        "Pass stable ids when questions may be reworded, so controlled open state survives edits.",
        "Composes Arc SearchField and SegmentedControl."
      ],
      "related": [
        "accordion",
        "search-field",
        "contact-section",
        "hero-section"
      ],
      "usage": "import { FaqSection } from \"@/components/arc/blocks/faq-section/faq-section\";\n\nexport function PricingFaq() {\n  return (\n    <FaqSection\n      variant=\"search\"\n      title=\"Questions about billing\"\n      items={[\n        { question: \"Can I cancel anytime?\", answer: \"Yes. Your plan stays active until the end of the billing period.\" },\n        { question: \"Do you offer refunds?\", answer: \"We refund annual plans within 14 days of purchase.\" },\n      ]}\n      contact={{ label: \"Talk to billing\", href: \"mailto:billing@example.com\" }}\n    />\n  );\n}",
      "exports": [
        "FaqSection",
        "FaqSectionBlock"
      ]
    },
    {
      "name": "contact-section",
      "kind": "block",
      "title": "Contact section",
      "description": "A validated contact form that morphs into a confirmation, support channels, and office cards with local times.",
      "tier": "free",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/contact-section",
      "registry": "https://uiarc.dev/r/contact-section.json",
      "keywords": [
        "react contact form",
        "contact section",
        "contact page",
        "office hours time zones",
        "support channels",
        "validated contact form"
      ],
      "whenToUse": [
        "Contact pages and sections on marketing sites.",
        "Sales or support pages that list several channels with response times.",
        "Companies with offices in several time zones."
      ],
      "whenNotToUse": [
        "Use support-widget for an in-app help launcher.",
        "Use signup-form or login blocks for account flows."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Fields use labelled Arc Input and Textarea; errors appear after the first submit and update as you type.",
        "A failed send shows a role=\"alert\" message and keeps the form.",
        "Office open status is written out, not shown by color alone."
      ],
      "motion": [
        "The form card springs into the confirmation; faces slide along one axis in the direction of travel out of a soft blur.",
        "The topic highlight glides between chips.",
        "Reduced motion swaps faces without travel."
      ],
      "responsive": [
        "Container queries: below 820px the form layout stacks and the channel list becomes a horizontal scrolling row; below 520px field rows stack and the submit button fills the width."
      ],
      "performance": [
        "The offices variant re-renders every 15 seconds to keep local times live; other variants have no timers."
      ],
      "aiNotes": [
        "Without onSubmit the form only simulates sending; always wire it in production.",
        "Offices compute local time and open status with Intl.DateTimeFormat in each timeZone.",
        "Composes Arc Input, Textarea, Button, CopyButton, SegmentedControl, and Avatar."
      ],
      "related": [
        "signup-form",
        "support-widget",
        "faq-section",
        "site-footer"
      ],
      "usage": "import { ContactSection } from \"@/components/arc/blocks/contact-section/contact-section\";\n\nexport function Contact() {\n  return (\n    <ContactSection\n      title=\"Talk to us\"\n      topics={[\"Sales\", \"Support\"]}\n      onSubmit={async message => {\n        const response = await fetch(\"/api/contact\", { method: \"POST\", body: JSON.stringify(message) });\n        if (!response.ok) throw new Error(\"Could not send\");\n      }}\n    />\n  );\n}",
      "exports": [
        "ContactSection",
        "ContactSectionBlock"
      ]
    },
    {
      "name": "settings-command",
      "kind": "block",
      "title": "Settings command",
      "description": "Workspace settings where a search field grows into a palette that flies you to any setting.",
      "tier": "pro",
      "builtFrom": [
        "Switch",
        "Segmented control",
        "Animated counter",
        "Text morph",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/settings-command",
      "registry": null,
      "keywords": [
        "settings",
        "preferences",
        "search settings",
        "command palette",
        "jump to setting",
        "unsaved changes",
        "save bar",
        "sticky nav",
        "scroll spy",
        "admin settings"
      ],
      "whenToUse": [
        "Workspace or account settings with several sections and many options.",
        "Admin consoles where support links people to a specific setting.",
        "Any settings page where people ask where something lives."
      ],
      "whenNotToUse": [
        "Use settings-page for a short, paged profile form with a few fields per section.",
        "Use command-palette for app wide navigation and actions, not just settings.",
        "Avoid it for a single form; a plain form with a submit button is clearer."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The palette is a modal dialog with a combobox input and a listbox of options; the scrim closes it on press.",
        "Each row labels its control with the setting label and describes it with the hint and any validation error.",
        "Edited rows and sections carry a dot plus visually hidden Edited text, so the state never relies on color.",
        "A polite status region announces jumps, saves, discards, and validation problems.",
        "Invalid fields block saving; Save jumps to the first invalid row the same way search does."
      ],
      "motion": [
        "The search field grows into the palette: position, size, and corner radius ride one spring, and results are revealed by the growing edge instead of reflowing.",
        "Picking a result flies the palette into the target row while the page scrolls, then the row's wash swells and settles twice.",
        "The section nav highlight glides to whichever section is in view as you scroll.",
        "The unsaved changes bar grows out of a small puck, rolls its count, and shrinks into Settings saved.",
        "Reduced motion swaps flights for fades, scrolls instantly, and keeps a single soft pulse."
      ],
      "responsive": [
        "Below 720px the section nav becomes a scrolling row above the content.",
        "Below 480px select and segmented rows stack their control under the label; switches stay inline.",
        "The palette always fits the frame with a 12px margin and drops its hint row on phones."
      ],
      "performance": [
        "Search is a synchronous filter over the settings array; it stays instant for a few hundred settings.",
        "Scroll spy is throttled to one read per animation frame.",
        "The palette and save bar animate transform, size, and opacity of a single element each."
      ],
      "aiNotes": [
        "Choose it when a product has more than about 15 settings and people search for them by name.",
        "Add keywords for the words people actually type, such as 2fa, sso, dark mode, or invoice.",
        "Keep the data file as the single schema: the palette, nav, and rows all read from it.",
        "Send only the changed ids to your API through onSave and reject on failure so the bar offers Retry."
      ],
      "related": [
        "settings-page",
        "command-palette",
        "security-settings",
        "switch",
        "segmented-control"
      ],
      "usage": "import { SettingsCommand } from \"@/components/arc/blocks/settings-command/settings-command\";\nimport { settingDefinitions, settingsSections } from \"@/components/arc/blocks/settings-command/settings-command-data\";\n\nexport function WorkspaceSettings({ saved }: { saved: Record<string, string | boolean> }) {\n  return (\n    <SettingsCommand\n      sections={settingsSections}\n      settings={settingDefinitions}\n      defaultValues={saved}\n      onSave={async (values, changed) => {\n        await fetch(\"/api/workspace/settings\", { method: \"PATCH\", body: JSON.stringify(Object.fromEntries(changed.map(id => [id, values[id]]))) });\n      }}\n    />\n  );\n}",
      "exports": [
        "SettingsCommand"
      ]
    },
    {
      "name": "checkout-live",
      "kind": "block",
      "title": "Live checkout",
      "description": "A subscription checkout whose summary recomputes as you edit, then glides to the centre and unfolds into a receipt.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Checkbox",
        "Input",
        "Button",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/checkout-live",
      "registry": null,
      "keywords": [
        "checkout",
        "subscription",
        "billing",
        "payment form",
        "order summary",
        "coupon",
        "receipt",
        "seats",
        "saas upgrade"
      ],
      "whenToUse": [
        "Upgrade from a free plan to a paid team plan.",
        "Paid signup where seats and add-ons change the price.",
        "A self serve checkout that needs coupons and tax by country."
      ],
      "whenNotToUse": [
        "A physical goods cart with shipping; use checkout-flow.",
        "Only showing totals without payment; use checkout-summary.",
        "Comparing plans before choosing; use plan-comparison."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Plans and billing period are radio groups with aria-checked; add-ons are real checkboxes.",
        "Errors are tied to their inputs with aria-describedby and aria-invalid, and a failed submit focuses the first invalid field.",
        "Rolling amounts keep the full value in visually hidden text, so screen readers never hear single digits.",
        "On success focus moves to the receipt heading; payment errors use role alert."
      ],
      "motion": [
        "Every amount rolls digit by digit on the snappy spring; add-on and discount lines open and close their height on the smooth spring.",
        "On success the form fades aside and the summary glides to the centre with one position spring, while the coupon folds away and receipt details unfold.",
        "The selected plan ring and billing pill travel between options as shared layout highlights.",
        "Reduced motion keeps every state and swaps travel for instant changes."
      ],
      "responsive": [
        "Two columns with a sticky summary from 760px of frame width.",
        "Below that the summary sits on top as a collapsible total; plans stack below 520px.",
        "The receipt is a single centred column at every width."
      ],
      "performance": [
        "Only the changed digits animate; lines are plain DOM.",
        "Validation runs on each change for a handful of fields, so it stays cheap."
      ],
      "aiNotes": [
        "Choose this for a SaaS upgrade or paid signup where plan, seats and add-ons are chosen on the same page as payment.",
        "Prices are cents per month; yearly totals are derived with yearlyDiscount. Keep tax and coupon math on your server and treat the client numbers as a preview.",
        "Replace the card inputs with your payment provider's hosted fields in production; keep the layout and validation feedback.",
        "Below 760px of frame width the summary moves above the form and collapses to a total you can expand."
      ],
      "related": [
        "checkout-summary",
        "pricing-calculator",
        "plan-comparison"
      ],
      "usage": "import { CheckoutLive } from \"@/components/arc/blocks/checkout-live/checkout-live\";\n\nexport function UpgradePage() {\n  return (\n    <CheckoutLive\n      defaultPlan=\"team\"\n      defaultSeats={12}\n      validateCoupon={code => fetch(`/api/coupons/${code}`).then(r => r.json())}\n      onPay={async order => {\n        const res = await fetch(\"/api/subscribe\", { method: \"POST\", body: JSON.stringify(order) });\n        if (!res.ok) throw new Error(\"Your card was declined. Try another card.\");\n      }}\n    />\n  );\n}",
      "exports": [
        "CheckoutLive"
      ]
    },
    {
      "name": "kanban-board",
      "kind": "block",
      "title": "Kanban board",
      "description": "A kanban board where cards lift, tilt and spring into the slot that opens for them, with swimlanes and live WIP limits.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Checkbox",
        "Segmented control",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/kanban-board",
      "registry": null,
      "keywords": [
        "kanban",
        "board",
        "drag and drop",
        "swimlanes",
        "wip limit",
        "task board",
        "pipeline",
        "issue tracker"
      ],
      "whenToUse": [
        "A team board where cards move between statuses.",
        "Pipelines where ordering within a stage matters.",
        "Boards that need a per person view."
      ],
      "whenNotToUse": [
        "Scheduling on dates; use gantt-timeline or week-calendar.",
        "Read only status summaries; use a table.",
        "Hundreds of cards per column; virtualize or use customers-table."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Cards are focusable buttons with a draggable role description and a label that includes title, id and assignee.",
        "Every pick up, move and drop is announced in an assertive live region with the column and position.",
        "The drawer is a modal dialog: focus moves in, Tab is trapped, Escape closes it and focus returns to the card.",
        "WIP limits are shown with text as well as color."
      ],
      "motion": [
        "The lifted card rides pointer motion values, lifts slightly and tilts on a spring from horizontal velocity.",
        "Neighbours glide aside with layout springs as the slot moves; on release a small spring chases the slot's live position and carries the throw velocity.",
        "Switching to swimlanes flies every card to its lane with shared layout ids.",
        "Reduced motion drops tilt and travel; drops land at once."
      ],
      "responsive": [
        "Columns keep a readable minimum width and the board scrolls sideways with snap on phones.",
        "The drawer covers the board on narrow frames.",
        "Auto scroll works on both axes when dragging near an edge."
      ],
      "performance": [
        "Only the dragged card moves every frame, through transforms; hit testing reads a few rects per move.",
        "Cards are memoized and use position only layout animation.",
        "Comfortable up to a few hundred cards."
      ],
      "aiNotes": [
        "Choose this for issue boards, sales pipelines, hiring stages or any status workflow with manual ordering.",
        "Persist onCardsChange: the array order is the order within each column.",
        "Touch starts a drag after a short press so swipes still scroll the board.",
        "Use limit on columns for WIP limits; counts update live while dragging."
      ],
      "related": [
        "project-board",
        "gantt-timeline",
        "week-calendar",
        "reorderable-list"
      ],
      "usage": "import { KanbanBoard } from \"@/components/arc/blocks/kanban-board/kanban-board\";\n\nexport function Sprint({ issues, team }) {\n  const [cards, setCards] = useState(issues);\n  return (\n    <KanbanBoard\n      title=\"Sprint 42\"\n      columns={[{ id: \"todo\", name: \"To do\" }, { id: \"doing\", name: \"Doing\", limit: 4 }, { id: \"done\", name: \"Done\" }]}\n      people={team}\n      cards={cards}\n      onCardsChange={next => { setCards(next); saveOrder(next); }}\n    />\n  );\n}",
      "exports": [
        "KanbanBoard"
      ]
    },
    {
      "name": "usage-billing",
      "kind": "block",
      "title": "Usage billing",
      "description": "A usage based billing dashboard with a projected spend line, meters against limits, alerts, and an upgrade that morphs into a confirm sheet.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Text morph",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/usage-billing",
      "registry": null,
      "keywords": [
        "usage billing",
        "metered billing",
        "spend dashboard",
        "projected spend",
        "usage limits",
        "spend alerts",
        "budget alerts",
        "invoice history",
        "upgrade plan",
        "proration"
      ],
      "whenToUse": [
        "A billing page for usage priced SaaS or infrastructure products.",
        "Admin dashboards where finance needs spend alerts and invoice history in one place.",
        "Upgrade moments driven by limits, where the upgrade should explain what changes."
      ],
      "whenNotToUse": [
        "Use billing-overview for flat seat based plans without meters.",
        "Use plan-comparison or usage-pricing on a public pricing page.",
        "Avoid it for a single quota; a usage meter component is enough."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The chart is an image with a spoken summary of spend, projection, and budget; its cursor also works from the keyboard.",
        "Each meter is a meter role with a value text that says when it is near or projected over the limit.",
        "Warnings pair an icon and words with color. Invoice status pairs a dot with a label.",
        "The upgrade sheet is a modal dialog that traps Tab, focuses its heading, and returns focus to the plan card.",
        "The sheet states that the preview takes no payment."
      ],
      "motion": [
        "The spend line draws in once, the projection and alert rules follow; alert rules glide to their new height while you type an amount.",
        "Meter bars fill with a short stagger. After an upgrade every limit grows, so the bars shrink and warnings clear in place.",
        "The plan card grows into the confirm sheet on one spring and folds back into the card once the upgrade lands.",
        "Invoice downloads swap their icon for a spinner, then a check, without changing width.",
        "Reduced motion removes the draw in, stagger, and flight, and keeps short fades."
      ],
      "responsive": [
        "From 700px the chart and plan card share a row, and meters sit beside alerts.",
        "Below 520px the invoice issue date and overage prices hide; everything else stacks.",
        "The confirm sheet keeps a 12px margin and stays clear of sticky page chrome."
      ],
      "performance": [
        "The chart is one SVG sized from a ResizeObserver; hover updates only the cursor and tooltip.",
        "Bars animate transform scale only.",
        "Sheet flight animates one element's transform and size."
      ],
      "aiNotes": [
        "Choose it for products billed on usage: API calls, compute, storage, bandwidth, seats.",
        "Feed daily spend from your billing provider; the projection is the average of the last seven days carried to the end of the period.",
        "Set plan limits per meter id so the upgrade can recompute every bar.",
        "Call your checkout or subscription update API in onUpgrade and reject on a declined card."
      ],
      "related": [
        "billing-overview",
        "usage-forecast",
        "usage-pricing",
        "plan-comparison",
        "usage-meter"
      ],
      "usage": "import { UsageBilling } from \"@/components/arc/blocks/usage-billing/usage-billing\";\n\nexport function BillingPage({ usage, plans }: { usage: BillingSnapshot; plans: PlanCatalog }) {\n  return (\n    <UsageBilling\n      period={usage.period}\n      daily={usage.dailySpend}\n      meters={usage.meters}\n      plan={plans.current}\n      upgrade={plans.next}\n      defaultAlerts={usage.alerts}\n      onAlertsChange={alerts => saveAlerts(alerts)}\n      onUpgrade={plan => startUpgrade(plan.id)}\n      onDownloadInvoice={invoice => downloadPdf(invoice.id)}\n    />\n  );\n}",
      "exports": [
        "UsageBilling"
      ]
    },
    {
      "name": "activity-feed",
      "kind": "block",
      "title": "Activity feed",
      "description": "A team activity feed grouped by day on a quiet avatar timeline, with a type filter, a live new updates button, expandable details, and a mark all read sweep.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Animated counter",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/activity-feed",
      "registry": null,
      "keywords": [
        "activity feed",
        "team activity",
        "timeline",
        "recent activity",
        "mark all read",
        "new updates",
        "live feed",
        "diff",
        "filters",
        "grouped by day"
      ],
      "whenToUse": [
        "A workspace or project activity page for a team product.",
        "A dashboard panel that shows what changed since someone last looked."
      ],
      "whenNotToUse": [
        "Use audit-log for compliance history that needs search, export, and exact timestamps.",
        "Use notification-center for a personal inbox opened from a bell.",
        "Use changelog-feed for public release notes."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "next/image"
      ],
      "accessibility": [
        "Each day is a labeled section with a heading; events are an ordered list.",
        "Expandable events are buttons with aria-expanded and aria-controls.",
        "Unread state is spoken as part of the event and shown with a dot and a stronger name, not color alone.",
        "The type filter is a labeled group of toggle buttons with aria-pressed and arrow key navigation.",
        "A polite status region announces new updates shown and how many were marked read."
      ],
      "motion": [
        "New events queue behind a button that opens its own row above the list, so no event is covered; showing them collapses that row and slides the new events in at the top.",
        "Mark all read sends a hairline down the list; each row lets go of its dot exactly as the line passes it.",
        "Changing the type filter crossfades the list, so rows never slide over each other.",
        "Details open with a height spring; the one line preview gives way to the full detail.",
        "Reduced motion removes the sweep delay and travel and keeps instant state changes."
      ],
      "responsive": [
        "Type filters scroll sideways on narrow screens.",
        "Below 440px the time moves into the meta line under each event.",
        "Day headings stick with a translucent material while their events scroll under them."
      ],
      "performance": [
        "Filtering is memoized over the item list.",
        "The sweep uses CSS transition delays and one Web Animations API line, so it costs no React renders per row.",
        "Photos load through next/image at 96px."
      ],
      "aiNotes": [
        "Map your events to action and target so each row reads as one sentence.",
        "Push websocket events into incoming; the new updates button counts them and merges them on click.",
        "Keep details short: a comment, a small diff, a deploy summary, or up to four photos.",
        "Persist read state through onRead."
      ],
      "related": [
        "notification-center",
        "changelog-feed",
        "timeline",
        "inbox-triage"
      ],
      "usage": "import { ActivityFeed } from \"@/components/arc/blocks/activity-feed/activity-feed\";\n\nexport function ProjectActivity({ events, live }: { events: Activity[]; live: Activity[] }) {\n  return <ActivityFeed items={events} incoming={live} onRead={ids => markRead(ids)} />;\n}",
      "exports": [
        "ActivityFeed"
      ]
    },
    {
      "name": "ai-side-panel",
      "kind": "block",
      "title": "AI side panel",
      "description": "An assistant docked beside a page that reads it and edits it: ask about the page or a selected passage, review each edit as a word diff, and apply it with a tint sweep on the page and one-click undo.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Text shimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-side-panel",
      "registry": null,
      "keywords": [
        "ai assistant",
        "copilot",
        "side panel",
        "document assistant",
        "suggested edits",
        "word diff",
        "apply and undo",
        "context chips",
        "ask about selection",
        "streaming",
        "bottom sheet",
        "claude"
      ],
      "whenToUse": [
        "An assistant next to a document or record that can summarize it and propose edits.",
        "Copilot style help where edits must be approved, applied visibly and undone.",
        "Asking about a selected passage or table without leaving the page."
      ],
      "whenNotToUse": [
        "A standalone conversation; use ai-chat.",
        "Rewriting a passage in place with no side panel; use ai-inline-edit.",
        "A single prompt box; use ai-composer.",
        "Showing an autonomous agent's steps; use agent-run."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The panel is a labelled complementary region; the close and reopen buttons report aria-expanded and aria-controls.",
        "A polite status region announces each step: reading the page, answer ready (and which part a change touches), applied with undo available, undone, discarded, stopped and errors.",
        "After Apply focus moves to the Undo pill; after Undo it returns to the card's Apply button; after Discard it moves to the composer.",
        "Diffs use real ins and del elements, so removed and added words are distinguishable without color.",
        "Every status pairs an icon with a label (Applied to the page, Discarded, Undone, Stopped, did not respond).",
        "In sheet mode the page behind is inert while the sheet is open."
      ],
      "motion": [
        "Closing the panel narrows the dock on a critically damped spring (no bounce) while the panel slides out; the page column widens and the document glides to center.",
        "Answers stream in chunks of 3 to 8 words every 34ms on a requestAnimationFrame clock; each chunk fades in over 150ms and settled text never re-renders.",
        "Apply sweeps an accent tint across the changed block once, while the old text fades out in 100ms and the new text fades in over 180ms; the block height follows on a spring with no overshoot.",
        "Status swaps in the change card footer, chips and the Undo pill are single opacity crossfades in a fixed size slot, so nothing shifts.",
        "Reduced motion keeps every state, drops the chunk fade, the slide and the sweep travel, and only fades the tint."
      ],
      "responsive": [
        "From 640px the panel docks at 360px wide beside the page; the page keeps a readable 520px measure.",
        "Below 640px of container width the panel becomes a bottom sheet over the page with a soft scrim and a close button.",
        "Chips truncate the selection label; the full selected text is in the chip's title."
      ],
      "performance": [
        "Only the dock width (or sheet position) animates during open and close; the page reflows once per frame.",
        "Streaming updates one answer; earlier chunks and turns do not re-render.",
        "The word diff is a small LCS over one block, computed on render."
      ],
      "aiNotes": [
        "Choose this to add an assistant to a document, spec or record page where answers should become reviewable edits.",
        "Return edits as data ({ blockId, after }) rather than prose so people approve each change; every applied change can be undone.",
        "Pass the selection to your model; it carries the block id, its label and the selected text.",
        "Stream with an async iterable of text deltas; the panel paces the reveal and drains bursts quickly.",
        "The preview's scripted responder fails the first free form question it cannot answer once, to show the error state; real responders throw to show it."
      ],
      "related": [
        "ai-chat",
        "ai-composer",
        "ai-inline-edit",
        "ai-answer",
        "context-window"
      ],
      "usage": "import { AiSidePanel } from \"@/components/arc/blocks/ai-side-panel/ai-side-panel\";\n\nexport function SpecPage({ spec }) {\n  return (\n    <AiSidePanel\n      page={spec}\n      respond={async ({ prompt, page, selection, signal }) => {\n        const res = await fetch(\"/api/assistant\", {\n          method: \"POST\",\n          body: JSON.stringify({ prompt, page, selection }),\n          signal,\n        });\n        const { text, change } = await res.json(); // change: { blockId, after: PageBlock }\n        return { text, change };\n      }}\n      onApply={(change, page) => saveSpec(page)}\n      onUndo={(change, page) => saveSpec(page)}\n    />\n  );\n}\n\n// Stream real tokens: return text as an async iterable of deltas.\nasync function* deltas(stream) {\n  for await (const event of stream) if (event.type === \"text\") yield event.text;\n}",
      "exports": [
        "AiSidePanel"
      ]
    },
    {
      "name": "research-run",
      "kind": "block",
      "title": "Research run",
      "description": "Deep research that plans searches, reads sources with their marks, and streams a cited answer where every chip lights up the sentence it supports.",
      "tier": "pro",
      "builtFrom": [
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/research-run",
      "registry": null,
      "keywords": [
        "deep research",
        "answer engine",
        "citations",
        "sources",
        "streaming",
        "perplexity",
        "search",
        "llm",
        "ai answer",
        "follow up",
        "research agent"
      ],
      "whenToUse": [
        "An answer engine or deep research feature that searches, reads and cites.",
        "Internal knowledge search where people must verify each claim.",
        "Any long streamed answer that benefits from visible sources and follow ups."
      ],
      "whenNotToUse": [
        "Open ended conversation without sources; use ai-chat.",
        "An agent that edits files or runs tools; use agent-run.",
        "A single answer with sources inside another page; use ai-answer."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The block is a labelled region; each turn is an article named by its question, and the sources rail is a labelled complementary region.",
        "The answer is marked aria-busy while it streams; a polite live region announces once when it is ready or stopped, with the source count, instead of every word.",
        "Citation chips are buttons named with the source number and title. Reading progress is exposed as text on each source number, such as Reading, 60 percent.",
        "The steps toggle and source rows report aria-expanded; collapsed panels are inert.",
        "Copy, Stop and Rerun keep their width while their labels change, and state never relies on color alone."
      ],
      "motion": [
        "Streamed text is buffered and drained on a requestAnimationFrame loop at a rate that follows the measured arrival rate, scaled by how full the buffer is and eased so bursts bend the pace instead of kinking it. Reveal snaps to whole words.",
        "Each commit becomes its own chunk span that develops in once, opacity with a 1.5px blur that clears, and never renders again, so earlier lines never re-split or replay. Lists, headings and chips appear already formatted; a half received citation or marker waits for its first word.",
        "Before the first word a quiet shimmer reads Writing the answer in the exact place the first line lands. A small dot appears at the end only when the stream stalls, and leaves with the last word.",
        "The thread eases toward the newest line while you are at the bottom. Scrolling up releases it and a Jump to latest pill springs in above the composer.",
        "Sub-queries stagger in, their spinners turn into checks on the snappy spring, and source marks pop into each query row. Source rows open with a height spring; the research steps collapse on the smooth spring as the answer starts.",
        "The action row is reserved from the start: Stop and a live word count cross fade into Copy, Rerun and the time and token readout, so nothing below moves.",
        "Reduced motion reveals whole word chunks without fades or blur, stops shimmer and spinners, and snaps panels and scrolling."
      ],
      "responsive": [
        "From 880px container width the sources sit in their own scrolling rail beside the thread. Below that they fold into a strip of marks under the question that opens into the list, so the streaming text stays the last thing on screen.",
        "Below 520px sub-query rows drop their counts and marks, the question steps down a size, and gutters tighten.",
        "Layout follows the block's container, not the viewport."
      ],
      "performance": [
        "Only the chunk being revealed renders; earlier chunks are memoized, so long answers stay cheap. Hover state reaches chips and sentences through one small style rule instead of re-rendering the answer.",
        "The pacing loop runs only while buffered text remains and commits to React only when the word boundary moves.",
        "Source marks are CSS masks over small SVGs, tinted by the text color."
      ],
      "aiNotes": [
        "Choose this for search or deep research answers where people need to see what was read and check claims against sources.",
        "Emit sources before the text that cites them, numbered in arrival order, and send `[n]` right after the sentence it supports; the block attaches each chip to that sentence.",
        "Keep the answer to the supported markdown subset: paragraphs, ### headings, - and 1. lists, **bold**, `code`, fenced code and [n] citations.",
        "Stop keeps exactly the words already on screen and drops the buffered rest; rerun replaces the last turn in place.",
        "Pass a logo URL per source for a monochrome mark; brand marks in the preview are sample content from Simple Icons, not endorsements."
      ],
      "related": [
        "ai-chat",
        "agent-run",
        "ai-side-panel",
        "ai-answer"
      ],
      "usage": "import { ResearchRun, type ResearchEvent } from \"@/components/arc/blocks/research-run/research-run\";\n\nexport function Research() {\n  return (\n    <ResearchRun\n      autoStart={false}\n      onAsk={async function* (question, history, signal) {\n        const res = await fetch(\"/api/research\", { method: \"POST\", body: JSON.stringify({ question, history }), signal });\n        // Your server streams newline delimited ResearchEvent JSON.\n        for await (const event of readJsonLines<ResearchEvent>(res.body!)) yield event;\n      }}\n    />\n  );\n}",
      "exports": [
        "ResearchRun",
        "ResearchSource"
      ]
    },
    {
      "name": "ai-inline-edit",
      "kind": "block",
      "title": "AI inline edit",
      "description": "A document you edit with AI in place: \\u2318K on a selection, watch the rewrite stream in as a diff, accept each change, and Tab through ghost text.",
      "tier": "pro",
      "builtFrom": [
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-inline-edit",
      "registry": null,
      "keywords": [
        "ai editor",
        "inline edit",
        "rewrite",
        "diff",
        "suggestions",
        "autocomplete",
        "ghost text",
        "copilot",
        "cmd k",
        "notion ai",
        "tab to accept",
        "accept reject"
      ],
      "whenToUse": [
        "Rewriting a selection: shorten, clarify, change tone or fix grammar in place.",
        "Inline autocomplete while writing documents or messages.",
        "Any AI suggestion that should be reviewed as a diff before it lands."
      ],
      "whenNotToUse": [
        "Rich formatting such as headings, lists or links; the block edits plain paragraphs.",
        "Asking questions about a page; use ai-side-panel.",
        "A single prompt box without a document; use ai-composer."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The editor is a labelled region; each paragraph is a native contenteditable, so caret movement, selection and screen reader editing work as usual.",
        "The prompt is a labelled dialog with a combobox input and a listbox of actions using aria-activedescendant.",
        "Each change in review is a focusable group named with what it does, such as Change \"very careful\" to \"carefully\", with Enter and Backspace to decide.",
        "Ghost text is aria-hidden and never part of the paragraph's text; the footer names the keys to accept or dismiss it.",
        "Streaming, review and applied states are announced with role status; failures use role alert with Try again. Deletions and insertions pair color with strike through and highlight.",
        "The block claims Cmd/Ctrl + K only while focus is inside it, so an app wide command menu still works elsewhere."
      ],
      "motion": [
        "One floating layer carries the whole edit: it opens under the selection, morphs its size on the smooth spring from prompt to a Stop status bar to the review bar to an Applied receipt, and follows the diff as it grows.",
        "The rewrite is paced like any stream: buffered, drained on a frame loop that tracks the arrival rate, snapped to whole words. Inserted words develop in with opacity and a small blur that clears; deleted words have their strike drawn across as the stream passes them.",
        "While streaming, a short word matched far ahead is not trusted yet, so the diff only grows at the stream edge and never flickers between alignments.",
        "The active quick action is marked by a highlight that slides between rows on the snappy spring. Hunk toolbars fade up on hover or focus; a decided change settles with a brief tint.",
        "Accepted ghost text glows for a moment using the CSS highlight API, without touching the text.",
        "Reduced motion shows diffs, ghost text and the layer without fades, sweeps or morphs."
      ],
      "responsive": [
        "The page column stays at a comfortable reading width and tightens its gutters below 560px container width, where the title steps down.",
        "The prompt keeps to 320px or the page width, whichever is smaller, and is clamped inside the page; review bars wrap onto two lines when narrow.",
        "On touch, selecting text shows the Edit bubble, which opens the same prompt."
      ],
      "performance": [
        "Paragraphs are uncontrolled contenteditables that render once, so typing never re-renders them and the caret never jumps.",
        "The word diff is a small dynamic program over the selection, recomputed only when the revealed suggestion moves by a word.",
        "Ghost text is a single non editable span appended at the caret; nothing else in the paragraph is touched."
      ],
      "aiNotes": [
        "Choose this to add AI rewriting and autocomplete to a notes, docs or email editor where people must stay in control of every change.",
        "Return only the replacement for the selected text, not the whole paragraph; the block diffs it word by word against the selection.",
        "Stream deltas when you can; stopping keeps the words already shown as reviewable changes and leaves the rest of the original untouched.",
        "Keep completions short, one clause or sentence, and return null when unsure; they only appear with the caret at the end of a paragraph.",
        "An edit applies to one paragraph; a selection that spans paragraphs is clamped to the first one."
      ],
      "related": [
        "ai-side-panel",
        "ai-composer",
        "ai-chat",
        "rich-text-editor"
      ],
      "usage": "import { AiInlineEdit } from \"@/components/arc/blocks/ai-inline-edit/ai-inline-edit\";\n\nexport function Notes({ doc }: { doc: EditorDocument }) {\n  return (\n    <AiInlineEdit\n      defaultDocument={doc}\n      onRequestEdit={async function* (request, signal) {\n        const res = await fetch(\"/api/rewrite\", { method: \"POST\", body: JSON.stringify(request), signal });\n        const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();\n        for (;;) { const { value, done } = await reader.read(); if (done) return; yield value; }\n      }}\n      onSuggest={(request, signal) => fetch(\"/api/complete\", { method: \"POST\", body: JSON.stringify(request), signal }).then(r => r.json())}\n      onChange={saveDocument}\n    />\n  );\n}",
      "exports": [
        "AiInlineEdit"
      ]
    },
    {
      "name": "kpi-drilldown",
      "kind": "block",
      "title": "KPI drilldown",
      "description": "KPI cards that expand into a full detail chart with period comparison and a breakdown table, then fold back.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/kpi-drilldown",
      "registry": null,
      "keywords": [
        "kpi",
        "dashboard",
        "drilldown",
        "metric card",
        "shared layout",
        "expand card",
        "period comparison",
        "breakdown table",
        "sparkline",
        "analytics"
      ],
      "whenToUse": [
        "An analytics or finance overview where each number deserves a closer look.",
        "Executive dashboards that compare the current period with the previous one."
      ],
      "whenNotToUse": [
        "Use metric-explorer to compare many metrics on one chart.",
        "Use metrics-dashboard for a static summary without drill down.",
        "Avoid it for real time monitoring where values change every second."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Cards are buttons whose name includes the value and the change.",
        "The detail is a labeled region; the grid behind it is hidden from assistive technology while it is open.",
        "The chart has a spoken summary and a keyboard cursor; changes pair an arrow and sign with color.",
        "Breakdowns are real tables with row headers."
      ],
      "motion": [
        "The card grows into the detail on one spring, and the detail folds back into the same card.",
        "Every range is resampled to the same number of points, so sparklines and the detail line morph instead of redrawing.",
        "Values roll digit by digit when the range changes.",
        "The previous period line fades in and out with Compare; breakdown rows crossfade and their share bars fill with a small stagger.",
        "Reduced motion jumps between grid and detail and keeps short fades."
      ],
      "responsive": [
        "Three columns above 760px, two down to 520px, then one.",
        "Below 520px the share column hides and Compare becomes an icon button.",
        "The detail scrolls inside its panel when it is taller than the grid."
      ],
      "performance": [
        "Sparklines use a fixed 100 by 32 viewBox with non scaling strokes, so resizing never re measures them.",
        "The detail chart measures its width once per resize and animates only path data."
      ],
      "aiNotes": [
        "Give each metric at least twice the longest range of daily values so the comparison period exists.",
        "Use kind rate for percentages so changes read in points, and set lowerIsBetter for churn or latency.",
        "Keep selected in the URL with onSelectedChange so a detail can be shared.",
        "Six metrics fit best; more belong in metric-explorer."
      ],
      "related": [
        "metrics-dashboard",
        "metric-explorer",
        "revenue-explorer",
        "mrr-waterfall",
        "bar-chart"
      ],
      "usage": "import { KpiDrilldown } from \"@/components/arc/blocks/kpi-drilldown/kpi-drilldown\";\n\nexport function Overview({ metrics }: { metrics: Kpi[] }) {\n  const [metric, setMetric] = useQueryState(\"metric\");\n  return <KpiDrilldown kpis={metrics} selected={metric} onSelectedChange={setMetric} />;\n}",
      "exports": [
        "KpiDrilldown"
      ]
    },
    {
      "name": "blog-grid",
      "kind": "block",
      "title": "Blog grid",
      "description": "A blog index with a featured post, category filter, post cards, pagination and an in-place reader.",
      "tier": "free",
      "builtFrom": [
        "Avatar",
        "Pagination"
      ],
      "docs": "https://uiarc.dev/components/blocks/blog-grid",
      "registry": "https://uiarc.dev/r/blog-grid.json",
      "keywords": [
        "blog grid",
        "blog index",
        "article list",
        "news page",
        "blog cards",
        "featured post",
        "category filter",
        "react blog template"
      ],
      "whenToUse": [
        "A marketing site blog or journal index.",
        "Resource hubs with a few categories and a featured story."
      ],
      "whenNotToUse": [
        "Use changelog-page for release notes with versions.",
        "Use a data table for large archives that need sorting and search."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Filters are buttons with aria-pressed; pagination is a nav with aria-current on the active page.",
        "Each card is one link with the title, excerpt and byline as its content, so screen readers announce it once.",
        "The reader moves focus to its back button, and closing returns focus to the card you opened.",
        "Images carry real alt text; author avatars are decorative."
      ],
      "motion": [
        "The filter and page highlights glide on the morph spring.",
        "Filtering crossfades the grid with a short rise; paging slides it in the direction of travel.",
        "Opening a post morphs the card image into the reader hero, and back on close.",
        "Card images zoom 3% on hover (fine pointers only).",
        "Reduced motion swaps travel and morphs for plain fades."
      ],
      "responsive": [
        "Three columns above 900px, two to 540px, then one.",
        "The featured post stacks its image above the text below 680px.",
        "Filters scroll sideways on phones; Previous and Next collapse to arrows."
      ],
      "performance": [
        "Only one page of cards renders at a time; images below the fold load lazily.",
        "Transitions animate transform and opacity only."
      ],
      "aiNotes": [
        "Choose this for a blog, journal, news or resources index.",
        "Pass getHref for real article routes; leave it out for a self-contained demo or a small site that reads posts in place.",
        "Drive category and page from the URL with the controlled props so the index is linkable."
      ],
      "related": [
        "changelog-feed",
        "hero-section",
        "pagination"
      ],
      "usage": "import { BlogGrid } from \"@/components/arc/blocks/blog-grid/blog-grid\";\n\nexport function BlogIndex({ posts }: { posts: BlogPost[] }) {\n  return (\n    <BlogGrid\n      posts={posts}\n      categories={[\"Product\", \"Design\", \"Engineering\", \"Company\"]}\n      getHref={post => `/blog/${post.id}`}\n    />\n  );\n}",
      "exports": [
        "BlogGrid",
        "BlogPost"
      ]
    },
    {
      "name": "comparison-table",
      "kind": "block",
      "title": "Comparison table",
      "description": "An us versus them table with a sticky header, a highlighted column, and a stacked phone view.",
      "tier": "free",
      "builtFrom": [
        "Switch",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/comparison-table",
      "registry": "https://uiarc.dev/r/comparison-table.json",
      "keywords": [
        "comparison table",
        "us vs them",
        "competitor comparison",
        "feature comparison",
        "alternatives page",
        "sticky header table",
        "react comparison table"
      ],
      "whenToUse": [
        "Showing how your product stacks up against named alternatives.",
        "Feature matrices with three to five columns and grouped rows."
      ],
      "whenNotToUse": [
        "Use plan-comparison to compare your own pricing tiers.",
        "Use a sortable data table for large datasets."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Built with table, row, columnheader, rowheader and cell roles, so screen readers navigate it as a table.",
        "Marks have text alternatives: Included, Partial (with its note), Not included.",
        "The switch is a real checkbox with role switch; a polite live region says when rows are filtered.",
        "Meaning never relies on color: checks, half circles and dashes differ in shape, with a legend."
      ],
      "motion": [
        "Checks in your column draw in once, staggered down the table, when they scroll into view.",
        "Differences only collapses matching rows on the smooth spring and closes the gap.",
        "Picking a competitor on phones swaps its header and marks with a small pop.",
        "The call to action morphs its label to the done state.",
        "Reduced motion shows every mark at once and swaps collapses for instant changes."
      ],
      "responsive": [
        "Below 640px (stackBelow) it shows your column beside one competitor with a picker above.",
        "Column widths are fractions, so it never scrolls sideways."
      ],
      "performance": [
        "A width observer switches layouts; no per frame work while scrolling.",
        "Row collapses animate height only on rows that change."
      ],
      "aiNotes": [
        "Use this for a /compare or /alternatives page or under pricing.",
        "Keep competitor names generic or factual; values should be verifiable.",
        "Set stickyTop to your fixed header height; use maxHeight only when embedding the table in a panel."
      ],
      "related": [
        "plan-comparison",
        "pricing-calculator",
        "usage-pricing",
        "faq-section"
      ],
      "usage": "import { ComparisonTable } from \"@/components/arc/blocks/comparison-table/comparison-table\";\n\nexport function Alternatives() {\n  return (\n    <ComparisonTable\n      columns={[\n        { id: \"us\", name: \"Relay\", caption: \"$10 per seat\", highlight: true },\n        { id: \"them\", name: \"Legacy suite\", caption: \"$24 per seat\" },\n      ]}\n      sections={[{ id: \"core\", title: \"Core\", rows: [\n        { id: \"offline\", feature: \"Offline mode\", values: { us: true, them: false } },\n        { id: \"sso\", feature: \"SSO\", values: { us: true, them: { value: \"partial\", note: \"Enterprise plan\" } } },\n      ] }]}\n      cta={{ label: \"Start free trial\", href: \"/signup\" }}\n      stickyTop={64}\n    />\n  );\n}",
      "exports": [
        "ComparisonTable"
      ]
    },
    {
      "name": "feature-bento",
      "kind": "block",
      "title": "Feature bento",
      "description": "A six cell feature bento where small, real product UI acts each feature out on hover.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/feature-bento",
      "registry": null,
      "keywords": [
        "bento",
        "bento grid",
        "feature grid",
        "features section",
        "landing page",
        "product features",
        "marketing section"
      ],
      "whenToUse": [
        "A landing page features section that should show the product rather than list it.",
        "Launch pages where each capability has one clear, demonstrable behavior."
      ],
      "whenNotToUse": [
        "Long feature lists or comparison tables. Use Plan comparison.",
        "A guided walkthrough with order. Use Scroll story.",
        "Cards that open into detail. Use Spotlight grid."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The section is labelled by its heading; each cell is a list item with a real h3 and paragraph.",
        "Illustrations are aria-hidden: they repeat what the title and sentence already say.",
        "With href, the title is the only link and its hit area stretches over the cell, so there is one tab stop per cell.",
        "Focusing a linked cell plays its illustration, the same as hovering it."
      ],
      "motion": [
        "Cells rise 14px and fade in once as they enter view, staggered by column.",
        "With a mouse, hovering a cell acts its feature out: a cursor drags a selection while another types, a palette highlight glides to its match, the latest bar lifts and takes the accent, steps check off along a filling line, a switch flips and saves, a scrubber slides back and the text swaps to that version.",
        "Hover in uses the shared spring curve with small sequenced delays; hover out returns quickly with no delays.",
        "On touch screens each cell plays once when it scrolls into view and stays in its end state.",
        "Only transform and opacity animate, plus two tiny typing reveals. Reduced motion jumps straight to each state."
      ],
      "responsive": [
        "Six columns from 880px container width, two columns from 560px, one column below.",
        "The tall cell spans two rows in both multi column layouts, so the grid stays gap free.",
        "Illustrations cap their width and center, so they read at 320px.",
        "Hover effects are gated to fine pointers; touch plays each cell on view instead."
      ],
      "performance": [
        "Illustrations are plain DOM and CSS transitions keyed on data attributes; no animation loop runs.",
        "One IntersectionObserver per cell, disconnected after the first view.",
        "Three 22px avatars are the only images."
      ],
      "aiNotes": [
        "Use as the main features section of a landing page, directly under the hero.",
        "Keep titles to one line and descriptions to one sentence; the illustrations carry the detail.",
        "Replace sample strings inside the illustrations through bentoSamples in feature-bento-data.ts, or swap a whole cell's visual with the visuals prop.",
        "Keep six cells: wide, tall, two regular, two half. Other counts work but lose the rhythm."
      ],
      "related": [
        "spotlight-grid",
        "scroll-story",
        "hero-section"
      ],
      "usage": "import { FeatureBento } from \"@/components/arc/blocks/feature-bento/feature-bento\";\n\nexport function Features() {\n  return (\n    <FeatureBento\n      title=\"Everything your team needs\"\n      features={[\n        { id: \"sync\", title: \"Edit together\", description: \"See who is here and what they change.\", visual: \"sync\", size: \"wide\" },\n        { id: \"search\", title: \"Find anything\", description: \"One menu for every page and person.\", visual: \"command\", size: \"tall\" },\n        { id: \"insights\", title: \"Clear numbers\", description: \"Trends next to the work that moved them.\", visual: \"chart\" },\n        { id: \"rules\", title: \"Automations\", description: \"Route, enrich, and notify on their own.\", visual: \"flow\" },\n        { id: \"access\", title: \"Access\", description: \"Roles for every workspace.\", visual: \"permissions\", size: \"half\" },\n        { id: \"versions\", title: \"History\", description: \"Rewind to any moment.\", visual: \"history\", size: \"half\" },\n      ]}\n    />\n  );\n}",
      "exports": [
        "FeatureBento"
      ]
    },
    {
      "name": "stats-band",
      "kind": "block",
      "title": "Stats band",
      "description": "Headline numbers that count up in view, each with a tiny visual that proves it and a context line on hover, plain or in a hairline grid.",
      "tier": "free",
      "builtFrom": [
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/stats-band",
      "registry": "https://uiarc.dev/r/stats-band.json",
      "keywords": [
        "stats",
        "statistics",
        "numbers",
        "count up",
        "counter",
        "metrics band",
        "social proof",
        "kpi"
      ],
      "whenToUse": [
        "Marketing pages that back a claim with a few numbers.",
        "About and investor pages with headline metrics."
      ],
      "whenNotToUse": [
        "Live product metrics with trends. Use Metric card or Metrics dashboard.",
        "More than four numbers. Split them or use a table."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "Stats are a description list: the label is the term, the number, detail, and context are its descriptions. Screen readers get both the detail and the context line.",
        "Stats with a context line are focusable, so keyboard and touch users reveal it too. Visuals are decorative and aria-hidden; the copy carries the meaning.",
        "Screen readers get the final formatted value once; the counting digits are aria-hidden.",
        "The section is labelled by the title or, without one, by \"Key numbers\"."
      ],
      "motion": [
        "Each number counts from zero with a strong ease out over 1.6s, staggered 90ms per stat, the first time the band is half in view.",
        "Digits are tabular and the final value reserves the width underneath, so nothing shifts while counting.",
        "Stats rise 8px and fade in; in the divided layout the top and bottom rules draw across and the hairlines grow between stats.",
        "Once a number has mostly landed, its visual draws in: the sparkline and map wipe in from the left, bars grow from the baseline in a short sweep, and the trend end dot settles last.",
        "Hover or focus cross-fades the detail line to the context line and gives that stat's visual the accent; the others stay neutral.",
        "Reduced motion shows final values, lines, and visuals at once."
      ],
      "responsive": [
        "One row of up to four stats, two columns below 760px container width, one column below 420px.",
        "Dividers reflow with the grid: vertical between neighbours, horizontal between rows.",
        "Number size scales with container width between 36px and 72px."
      ],
      "performance": [
        "Counting writes text straight to the DOM from one Motion tween per stat; React does not rerender per frame.",
        "One IntersectionObserver for the band, disconnected after the first view."
      ],
      "aiNotes": [
        "Use three or four stats with specific, verifiable numbers. Round numbers read as made up.",
        "Put units in suffix (%, ms, +, /5) so they render smaller and muted beside the number. Use notation: \"compact\" for large counts; it counts within its final unit.",
        "Give each stat a visual that proves the number, and a context line that compares it (up from, down from, p95). Leave both out for a plain numbers band.",
        "Place it under a hero or between a features section and testimonials.",
        "Edit stats-band-data.ts to replace the sample numbers."
      ],
      "related": [
        "metric-card",
        "animated-counter",
        "logo-marquee"
      ],
      "usage": "import { StatsBand } from \"@/components/arc/blocks/stats-band/stats-band\";\n\nexport function Numbers() {\n  return (\n    <StatsBand\n      layout=\"divided\"\n      stats={[\n        {\n          value: 2_334_000, notation: \"compact\", decimals: 1, label: \"Deploys in the last 12 months\",\n          detail: \"Across 12,400 teams\", context: \"Up from 1.4M the year before\",\n          visual: { kind: \"trend\", values: [141, 148, 139, 162, 171, 184, 196, 203, 221, 238, 257, 274] },\n        },\n        { value: 38, suffix: \"ms\", label: \"Median response time\", detail: \"At the edge, worldwide\" },\n        { value: 35, label: \"Edge regions on six continents\", visual: { kind: \"map\", points: [[-122.4, 37.8], [-0.1, 51.5], [139.7, 35.7]] } },\n      ]}\n    />\n  );\n}",
      "exports": [
        "StatsBand"
      ]
    },
    {
      "name": "cta-section",
      "kind": "block",
      "title": "CTA section",
      "description": "A call to action as a centered closing section, a split beside a setup card that completes itself, or a dismissible banner.",
      "tier": "free",
      "builtFrom": [
        "Button",
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/cta-section",
      "registry": "https://uiarc.dev/r/cta-section.json",
      "keywords": [
        "cta",
        "call to action",
        "cta section",
        "banner",
        "closing section",
        "signup prompt",
        "conversion",
        "landing page"
      ],
      "whenToUse": [
        "The last section of a landing page, asking for a trial or demo.",
        "An in-page prompt beside a product visual that shows the outcome.",
        "A slim promotional banner above content."
      ],
      "whenNotToUse": [
        "The first screen of a page. Use Hero section.",
        "Collecting an email. Use Newsletter signup.",
        "A persistent site-wide notice. Use Announcement bar."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each variant is a section labelled by its heading.",
        "Actions with href are real links; others are buttons. Simulated confirmations announce politely.",
        "The dismiss button has the name \"Dismiss\"; the banner closes its gap so focus order stays clean.",
        "The setup card is aria-hidden: the copy beside it carries the message."
      ],
      "motion": [
        "Primary actions lean their arrow forward on hover; buttons press to .97 and morph their width when the label changes.",
        "Split: the stage rises into view once, then the setup card ticks its three steps, fills its progress line, and slides in teammates' faces.",
        "Banner: dismissing fades it, scales it to .98, and springs its height closed so the page below moves up smoothly.",
        "Reduced motion shows the finished setup card and removes travel."
      ],
      "responsive": [
        "Split stacks copy above the visual below 860px container width.",
        "Below 560px actions go full width and stack, and the banner puts its text above the action and dismiss button.",
        "Headings scale with the container between 30px and 52px."
      ],
      "performance": [
        "The setup card runs three timers once after it enters view; everything else is CSS transitions.",
        "No images except up to four small avatars."
      ],
      "aiNotes": [
        "Use centered at the end of a landing page, split mid page next to a relevant product state, and banner at the top of an app or docs page.",
        "Pass href for real navigation. Leave both href and onClick out only for previews.",
        "One primary and at most one secondary action. Keep labels to two or three words with a verb.",
        "Edit cta-section-data.ts for the sample copy and the setup card content."
      ],
      "related": [
        "hero-section",
        "newsletter-signup",
        "announcement-bar",
        "pricing-calculator"
      ],
      "usage": "import { CtaSection } from \"@/components/arc/blocks/cta-section/cta-section\";\n\nexport function Closing() {\n  return (\n    <CtaSection\n      variant=\"split\"\n      title=\"Your team, set up before lunch\"\n      primaryAction={{ label: \"Create a workspace\", href: \"/signup\" }}\n      secondaryAction={{ label: \"Book a demo\", href: \"/demo\" }}\n      points={[\"Import from any tracker\", \"SSO included\", \"Cancel anytime\"]}\n    />\n  );\n}",
      "exports": [
        "CtaSection"
      ]
    },
    {
      "name": "newsletter-signup",
      "kind": "block",
      "title": "Newsletter signup",
      "description": "An email signup framed by a stack of past issues; subscribing drops the next issue, addressed to you, onto the front.",
      "tier": "free",
      "builtFrom": [
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/newsletter-signup",
      "registry": "https://uiarc.dev/r/newsletter-signup.json",
      "keywords": [
        "newsletter",
        "email signup",
        "subscribe",
        "waitlist",
        "email capture",
        "mailing list",
        "form",
        "digest",
        "issue"
      ],
      "whenToUse": [
        "Collecting emails for a newsletter, digest, or changelog that has real issues to show.",
        "The end of a blog post or docs page."
      ],
      "whenNotToUse": [
        "Account creation with a password. Use Signup form.",
        "Longer forms with several fields. Use Contact section.",
        "A launch waitlist at the top of a page. Use Hero signup."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The email field has a visually hidden label, type email, inputmode email, autocomplete email, and aria-invalid when the address is wrong.",
        "The line under the field is its description: privacy note, error, or confirmation. Errors use role alert; the line is a polite live region and names the address the link went to.",
        "Validation starts when the field loses focus with text in it, then updates as you type. On an invalid submit, focus stays in the field.",
        "While sending, the button sets aria-busy and aria-disabled and ignores repeat submits. On success, focus moves to \"Use a different email\", which restores the form and refocuses the field.",
        "Only the front issue is exposed to assistive technology; the issues behind it and the thumbnails are hidden as decoration."
      ],
      "motion": [
        "On success the next issue, addressed to you, rises onto the front of the stack while the others step back and the oldest drops away. It is one no-overshoot spring, and opacity resolves fast so two issues never read through each other.",
        "The reader count rolls up by one, and \"including you\" fades in after it.",
        "An invalid or failed submit shakes the field a few pixels and tints its border; the message swaps in place of the note.",
        "Every message, button label, and issue has its space reserved, so nothing below the form moves between states.",
        "Reduced motion places the issues at once with a short fade, removes the shake and the roll, and keeps the check and labels as quick fades."
      ],
      "responsive": [
        "Inline puts the stack under the copy and form below 860px container width.",
        "Below 400px read times hide and an addressed issue shows the recipient instead of the date.",
        "When the form is narrower than 300px the field and button separate and stack, both full width.",
        "The card is at most 460px wide; its tray crops the stack and fades it out at the edge."
      ],
      "performance": [
        "Issue thumbnails and faces use next/image at their rendered size.",
        "Motion is transform and opacity only; no layout animation.",
        "No network code: bring your own onSubscribe."
      ],
      "aiNotes": [
        "Pass onSubscribe and throw on failure; the component handles validation, busy, error, retry, and success states.",
        "Give it your own publication so the stack shows real past issues. Use short subjects and story titles; titles clamp at two lines.",
        "Keep the privacy note honest and specific.",
        "Use inline between page sections; use card in a sidebar, at the end of a post, or centered in a footer band.",
        "Edit newsletter-signup-data.ts for copy, issues, and reader faces."
      ],
      "related": [
        "cta-section",
        "site-footer",
        "contact-section",
        "signup-form",
        "hero-signup"
      ],
      "usage": "import { NewsletterSignup } from \"@/components/arc/blocks/newsletter-signup/newsletter-signup\";\n\nexport function Newsletter() {\n  return (\n    <NewsletterSignup\n      variant=\"card\"\n      onSubscribe={async email => {\n        const response = await fetch(\"/api/subscribe\", { method: \"POST\", body: JSON.stringify({ email }) });\n        if (!response.ok) throw new Error(\"Subscribe failed\");\n      }}\n    />\n  );\n}",
      "exports": [
        "NewsletterSignup"
      ]
    },
    {
      "name": "product-listing",
      "kind": "block",
      "title": "Product listing",
      "description": "A storefront grid with price, color and size filters, a sort menu, hover photo swaps, and quick add with a size row.",
      "tier": "pro",
      "builtFrom": [
        "Slider",
        "Switch",
        "Dropdown menu",
        "Button",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/product-listing",
      "registry": null,
      "keywords": [
        "product listing",
        "product grid",
        "collection page",
        "category page",
        "ecommerce filters",
        "price range",
        "quick add",
        "shop"
      ],
      "whenToUse": [
        "A shop's collection or category page with a few dozen to a few hundred products.",
        "Catalogs where color and size are the main filters and a quick add saves a trip to the product page."
      ],
      "whenNotToUse": [
        "One product with many options; use product-detail or product-configurator.",
        "Huge catalogs with faceted server search; keep the card and filter styles but fetch results from your search service."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Price handles are native range inputs with labels and currency value text.",
        "Swatches and size chips are toggle buttons with aria-pressed and color names; the colors under each card are listed in an accessible label.",
        "The sort menu is a listbox popup with aria-activedescendant and returns focus to its button.",
        "Quick add names its product, the size row is a labelled group, and each addition is announced through a live region.",
        "On narrow widths the filters open in a modal sheet that takes focus and closes with Escape."
      ],
      "motion": [
        "Cards reflow on a spring when filters or sort change; removed cards fade out and new ones fade in, all at the same size.",
        "Hover crossfades to the second photo inside the same fixed 4:5 box, with no zoom. Quick add rises into view on hover.",
        "Quick add, the size row, and Added swap in place inside one glass pill with a short blur.",
        "The bag icon tilts and the count rolls when something is added.",
        "Reduced motion drops the travel and layout animation and keeps opacity fades."
      ],
      "responsive": [
        "Above 900px a 232px filter sidebar sits beside a grid of cards at least 196px wide; from 640px to 900px the sidebar narrows to 208px.",
        "Below 640px the sidebar becomes a Filters button with an active count that opens a sheet from the left.",
        "Below 420px the sort button stretches to fill the toolbar. Quick add is always visible on touch screens."
      ],
      "performance": [
        "Images use next/image with responsive sizes; the hover photo only loads its small size until shown.",
        "Layout animation runs on position only, with popLayout exits, so filtering stays smooth with dozens of cards."
      ],
      "aiNotes": [
        "Use for category, collection, and search result pages. Pass products already scoped to the page; the block filters and sorts on the client.",
        "Put filter state in the URL if people share links; lift sort and filters into props when you do.",
        "Keep hoverImage a lifestyle or alternate angle shot at the same aspect ratio as the main image."
      ],
      "related": [
        "product-detail",
        "cart-drawer"
      ],
      "usage": "import { ProductListing } from \"@/components/arc/blocks/product-listing/product-listing\";\n\nexport function CollectionPage({ products }) {\n  return (\n    <ProductListing\n      title=\"Lighting\"\n      products={products}\n      onAddToCart={(product, size) => cart.add(product.id, size)}\n    />\n  );\n}",
      "exports": [
        "ProductListing",
        "ListingProduct"
      ]
    },
    {
      "name": "product-detail",
      "kind": "block",
      "title": "Product detail",
      "description": "A product page with a swipeable gallery, finish and size variants, add to bag that confirms in place, shipping, a reviews summary, and details.",
      "tier": "pro",
      "builtFrom": [
        "Carousel",
        "Radio group",
        "Number field",
        "Accordion",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/product-detail",
      "registry": null,
      "keywords": [
        "product detail",
        "product page",
        "pdp",
        "ecommerce",
        "gallery",
        "variants",
        "add to cart",
        "reviews summary",
        "shop"
      ],
      "whenToUse": [
        "Product pages with a few variants, a gallery, and reviews.",
        "Stores that want stock aware variants with a restock alert instead of a dead button."
      ],
      "whenNotToUse": [
        "Products built from many options with live pricing; use product-configurator.",
        "Browsing many products; use product-listing."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The gallery is a labelled carousel region with slide groups; hidden slides are aria-hidden and the counter is live.",
        "Finish and size are radiogroups with roving tab stops, like native radios. Sold out sizes stay selectable and say Sold out in text.",
        "Add to bag reports aria-busy while adding and announces the result; failures show Try again on the button itself.",
        "Accordion buttons sit in headings with aria-expanded and aria-controls.",
        "Review bars are hidden from assistive technology and each row carries a text label with its count."
      ],
      "motion": [
        "The gallery follows the pointer 1:1, rubber-bands at the ends, projects the flick with momentum, and hands the release velocity to the spring.",
        "Changing finish crossfades each photo in place; the thumbnail ring slides between thumbnails.",
        "The price rolls when size changes. Add to bag morphs through Adding and Added to bag without changing width while the bag icon tilts and its count rolls.",
        "Review bars fill once when they scroll into view; accordion sections open on a critically damped height spring.",
        "Reduced motion jumps the gallery, removes travel and blur, and shows final bar widths."
      ],
      "responsive": [
        "Above 980px the gallery sticks beside the info column; from 680px to 980px both columns share the width equally.",
        "Below 680px everything stacks: a square gallery with a four up thumbnail row, then the info.",
        "Under 420px the reviews summary stacks above the bars. Gallery arrows appear on hover only; touch uses swipe."
      ],
      "performance": [
        "The gallery drags one track with a motion value; only transform changes during a swipe.",
        "The first photo uses priority loading; the rest load lazily at the gallery's size."
      ],
      "aiNotes": [
        "Use as the main product page. Put finish specific photos on each finish so the gallery always matches the chosen swatch.",
        "Return your cart request from onAddToCart so the button shows real pending and error states.",
        "Keep sections to three or four; long policies belong on their own page."
      ],
      "related": [
        "product-listing",
        "cart-drawer"
      ],
      "usage": "import { ProductDetail } from \"@/components/arc/blocks/product-detail/product-detail\";\n\nexport function ProductPage({ product }) {\n  return (\n    <ProductDetail\n      product={product}\n      onAddToCart={selection => fetch(\"/api/cart\", { method: \"POST\", body: JSON.stringify(selection) })}\n      onNotify={selection => subscribeRestock(selection)}\n    />\n  );\n}",
      "exports": [
        "ProductDetail",
        "DetailProduct"
      ]
    },
    {
      "name": "feedback-board",
      "kind": "block",
      "title": "Feedback board",
      "description": "A feature request board with a submit form that finds similar requests, upvotes, top and new sorting, status filters, and comments.",
      "tier": "pro",
      "builtFrom": [
        "Input",
        "Textarea",
        "Segmented control",
        "Avatar",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/feedback-board",
      "registry": null,
      "keywords": [
        "feedback board",
        "feature requests",
        "upvote",
        "voting",
        "ideas",
        "product feedback",
        "canny",
        "suggestions"
      ],
      "whenToUse": [
        "A public or in app board where customers suggest and vote on features.",
        "Beta programs that need a lightweight place for requests and team replies."
      ],
      "whenNotToUse": [
        "One to one support conversations; use support-conversation or support-widget."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The form validates on submit with an inline error tied to the title through aria-describedby and aria-invalid.",
        "Votes are toggle buttons with aria-pressed and a label that names the request and count; changes and posts are announced.",
        "Request titles are buttons with aria-expanded that reveal details and comments in place.",
        "Status always pairs a dot with its name; filters and sort are radiogroups."
      ],
      "motion": [
        "Voting hops the arrow and rolls the count. Rows keep their place while you vote and only re-rank when you change sort or filter, so nothing moves under the pointer.",
        "Sort and filter changes glide rows to their new order. A posted request lands at the top with a brief tint.",
        "The form, similar requests, errors, and expanded details open on a critically damped height spring.",
        "Reduced motion removes travel and height springs and keeps short fades."
      ],
      "responsive": [
        "Content sits in a centered 820px column under a frosted sticky header.",
        "Below 640px the toolbar stacks: status filters scroll sideways above the sort switch, and the form's category chips sit above a full width Post button.",
        "Vote boxes shrink slightly and the relative time hides on phones."
      ],
      "performance": [
        "Sorting and filtering are memoized per change; only rows that move animate.",
        "The header blur is one backdrop filter; it falls back to a solid surface when transparency is reduced."
      ],
      "aiNotes": [
        "Use to collect and rank feature requests from customers. Keep the admin side (status changes, merging) in your own tools and pass the result in.",
        "The similar requests check is a simple word match on the client. Swap it for server search when the board grows.",
        "Record votes per signed in user; the board tracks only the current visitor's vote."
      ],
      "related": [
        "changelog-feed",
        "support-widget",
        "activity-feed"
      ],
      "usage": "import { FeedbackBoard } from \"@/components/arc/blocks/feedback-board/feedback-board\";\n\nexport function FeedbackPage({ requests, user }) {\n  return (\n    <FeedbackBoard\n      defaultRequests={requests}\n      currentUser={{ name: user.name, avatar: user.avatar }}\n      onSubmit={draft => createRequest(draft)}\n      onVote={(id, voted) => vote(id, voted)}\n      onComment={(id, text) => comment(id, text)}\n    />\n  );\n}",
      "exports": [
        "FeedbackBoard",
        "FeedbackRequest"
      ]
    },
    {
      "name": "profile-page",
      "kind": "block",
      "title": "Profile page",
      "description": "A user profile with cover, bio, stats, activity and project tabs, and an edit sheet that writes back into the page.",
      "tier": "pro",
      "builtFrom": [
        "Tabs",
        "Avatar",
        "Drawer",
        "Input",
        "Textarea",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/profile-page",
      "registry": null,
      "keywords": [
        "profile",
        "user profile",
        "account page",
        "edit profile",
        "cover photo",
        "avatar",
        "bio",
        "activity tab",
        "projects"
      ],
      "whenToUse": [
        "A person's own profile with an edit flow",
        "Public creator or member pages inside an app",
        "Team member pages that show recent work"
      ],
      "whenNotToUse": [
        "Account security or billing settings, use settings-page",
        "A directory of many people, use team-directory",
        "A marketing team section, use team-showcase"
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Tabs use tablist, tab, and tabpanel roles with roving tab index and aria-controls.",
        "The edit sheet is a modal dialog with a labelled title; focus moves to the first field, stays inside while open, and returns to Edit profile on close.",
        "Validation errors are tied to their inputs with aria-describedby and aria-invalid; save errors use role alert.",
        "Star buttons expose aria-pressed and name the project and count. Share keeps its width while its label changes to Copied or Failed."
      ],
      "motion": [
        "The tab underline travels between tabs on the morph spring; tab content fades up 6px.",
        "The edit sheet slides from the right edge on the smooth spring over a fading scrim, and leaves the same way faster.",
        "Saved fields glow once with the accent tint, and a new star pops with a small spring.",
        "Reduced motion swaps the slide for a fade, keeps the highlight, and removes travel."
      ],
      "responsive": [
        "Below 760px the about column stacks above the tabs; below 560px project cards go to one column.",
        "The cover shortens and the avatar shrinks on narrow screens; the edit sheet becomes full width.",
        "The screen height is min(760px, 100dvh) and the page scrolls inside it."
      ],
      "performance": [
        "Only transform and opacity animate; images are lazy except the cover.",
        "The edit sheet mounts only while open.",
        "Activity and projects render as plain lists; paginate beyond a few hundred items."
      ],
      "aiNotes": [
        "Choose this for a member, creator, or teammate profile inside a product. It is a full screen: the page scrolls inside the block so the sheet always covers the visible area.",
        "Load all data from your API and pass it in; the block owns only the edit draft and local star state.",
        "Set editable={false} for other people's profiles and replace the actions with Follow or Message in your own fork.",
        "Covers and avatars render with next/image; pass paths your image config allows."
      ],
      "related": [
        "settings-page",
        "team-directory",
        "activity-feed",
        "team-members"
      ],
      "usage": "import { ProfilePage } from \"@/components/arc/blocks/profile-page/profile-page\";\n\nexport function Profile({ user, activity, projects }) {\n  return (\n    <ProfilePage\n      profile={user}\n      activity={activity}\n      projects={projects}\n      onSave={async next => {\n        const res = await fetch(\"/api/me\", { method: \"PATCH\", body: JSON.stringify(next) });\n        if (!res.ok) throw new Error(\"Could not save your profile.\");\n      }}\n    />\n  );\n}",
      "exports": [
        "ProfilePage"
      ]
    },
    {
      "name": "help-center",
      "kind": "block",
      "title": "Help center",
      "description": "A help center with instant search, topic cards, and popular articles that open into a readable article with an outline and feedback.",
      "tier": "pro",
      "builtFrom": [
        "Search field",
        "Command palette",
        "Card",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/help-center",
      "registry": null,
      "keywords": [
        "help center",
        "knowledge base",
        "support",
        "faq",
        "search",
        "instant search",
        "articles",
        "table of contents",
        "was this helpful",
        "docs"
      ],
      "whenToUse": [
        "A public help center or knowledge base",
        "In-app help with searchable articles",
        "Support portals that deflect tickets with good answers"
      ],
      "whenNotToUse": [
        "Developer API reference with a nav tree, use docs-sidebar",
        "A short FAQ on a marketing page, use faq-section",
        "Live chat with support, use support-conversation"
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Search is a combobox with a listbox of options and aria-activedescendant; the result count is announced politely.",
        "Articles use real headings; outline links move focus to their section heading and mark the current one with aria-current.",
        "Breadcrumbs are a labelled navigation; the feedback area announces its thank you politely.",
        "Topic icons are decorative; every card is a button named by its text."
      ],
      "motion": [
        "Results drop in from the field with a short fade and scale; they are never animated while you arrow through them.",
        "Views slide 16px in the direction of travel, deeper to the right and back to the left.",
        "The outline bar follows the section you are reading on the snappy spring; clicking a section scrolls smoothly.",
        "Feedback states crossfade and the card resizes on the smooth spring. Reduced motion uses fades and instant scrolling."
      ],
      "responsive": [
        "Topic cards go from one to two to three columns.",
        "The compact search moves into the top bar once you leave the home screen.",
        "The screen height is min(760px, 100dvh) and the page scrolls inside it."
      ],
      "performance": [
        "Search runs in memory on every keystroke; switch to a server search past a few thousand articles.",
        "Scroll tracking uses one passive listener and reads section positions only while scrolling."
      ],
      "aiNotes": [
        "Choose this for a self serve support site, a docs style help area, or an in-app help screen.",
        "Wire a server search through search when you have many articles; keep the combobox and highlight behavior.",
        "Article sections are plain text paragraphs and steps; render your CMS rich text inside the prose container if you need links or images.",
        "The outline appears from 860px of width; below that the article reads as one column."
      ],
      "related": [
        "docs-sidebar",
        "faq-section",
        "support-widget",
        "support-conversation"
      ],
      "usage": "import { HelpCenter } from \"@/components/arc/blocks/help-center/help-center\";\n\nexport function Help({ articles, categories }) {\n  return (\n    <HelpCenter\n      brand=\"Acme\"\n      articles={articles}\n      categories={categories}\n      onArticleOpen={article => router.replace(`/help/${article.id}`)}\n      onFeedback={(id, helpful, comment) => fetch(\"/api/help/feedback\", { method: \"POST\", body: JSON.stringify({ id, helpful, comment }) })}\n    />\n  );\n}",
      "exports": [
        "HelpCenter"
      ]
    },
    {
      "name": "hero-signup",
      "kind": "block",
      "title": "Hero signup",
      "description": "A full screen waitlist hero on a soft, drifting mesh gradient: the email field validates as you type and its button grows into the confirmation with your place in line.",
      "tier": "pro",
      "builtFrom": [
        "Gradient mesh",
        "Input",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/hero-signup",
      "registry": null,
      "keywords": [
        "hero",
        "waitlist",
        "email signup",
        "mesh gradient",
        "early access",
        "landing page",
        "email capture",
        "launch"
      ],
      "whenToUse": [
        "Waitlists, early access, and launch pages where the email is the only ask.",
        "Pages that want a calm but alive first impression without product screenshots."
      ],
      "whenNotToUse": [
        "Signup that needs a password, SSO choice, or consent up front; use Sign up form.",
        "A newsletter box further down the page; use Newsletter signup."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "A real form with a labeled type=email field, autocomplete=email, inputmode=email and enterkeyhint=send.",
        "Validation messages are specific (a missing @, an unfinished domain) and tied to the field with aria-describedby and aria-invalid; they appear after blur or submit, never while the first address is still being typed.",
        "Joining, success, and failure are announced through a polite status region; focus moves to the confirmation on success and back to the field on Change email.",
        "The button stays focusable while busy and ignores repeat submits, so a double press never joins twice.",
        "The field font is 16px on phones so iOS never zooms, and every target is at least 44px tall."
      ],
      "motion": [
        "The headline rises in word by word once on load, followed by the supporting line, the field, and the social proof.",
        "Submitting crossfades the label into a spinner inside the same button, so nothing changes size.",
        "Success is one spatial transition: the button's shape grows across the whole field on a critically damped spring and becomes the confirmation, while the place in line rolls in digit by digit.",
        "The mesh drifts on a slow, seamless loop and blends into the new colors when the theme or accent changes. Reduced motion draws one still frame, removes the rise, the shake and the rolls, and swaps states instantly."
      ],
      "responsive": [
        "Fills exactly one screen: min-height is 100svh (100vh where unsupported), so mobile browser bars never hide the bottom, and the content is centered vertically.",
        "Works from 320px: the headline scales with the container, the button label shortens on very narrow widths, and the social proof stacks.",
        "The line under the field reserves its height, so a validation message or the confirmation never moves the content below."
      ],
      "performance": [
        "The background is the Gradient mesh component in background mode: one WebGL triangle with a light shader grain, no editor chrome and no pointer handling.",
        "The loop stops while the hero is off screen or the tab is hidden. Without WebGL the same palette renders as CSS radial gradients."
      ],
      "aiNotes": [
        "Choose this when the first section of a page exists to collect an email: a waitlist, early access, or a launch list.",
        "Wire onSubscribe to your API and return the real position; throw Errors with short human messages and the block handles busy, retry, and focus.",
        "In the preview, an address that contains fail shows the failure path once, so Try again can be tested."
      ],
      "related": [
        "hero-section",
        "newsletter-signup",
        "cta-section"
      ],
      "usage": "import { HeroSignup } from \"@/components/arc/blocks/hero-signup/hero-signup\";\n\nexport function Hero() {\n  return (\n    <HeroSignup\n      count={2140}\n      onSubscribe={async email => {\n        const res = await fetch(\"/api/waitlist\", { method: \"POST\", body: JSON.stringify({ email }) });\n        if (!res.ok) throw new Error(\"We couldn't save your spot\");\n        const { position } = await res.json();\n        return position;\n      }}\n    />\n  );\n}",
      "exports": [
        "HeroSignup"
      ]
    },
    {
      "name": "pivot-builder",
      "kind": "block",
      "title": "Pivot builder",
      "description": "A pivot table builder where you drag dimensions and measures into Rows, Columns, Values and Filters wells, with a live preview while you hover, per value aggregation, subtotals, a grand total, heat shading, collapsible row groups, swap and CSV export.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Empty state"
      ],
      "docs": "https://uiarc.dev/components/blocks/pivot-builder",
      "registry": null,
      "keywords": [
        "pivot table",
        "pivot",
        "crosstab",
        "olap",
        "aggregation",
        "group by",
        "subtotal",
        "grand total",
        "heatmap",
        "drag and drop",
        "report builder",
        "analytics",
        "csv export",
        "spreadsheet"
      ],
      "whenToUse": [
        "Self-serve analysis where the viewer picks the rows, columns and measures.",
        "Internal reporting screens that replace a spreadsheet pivot table.",
        "Exploring a flat export before deciding which fixed chart or report to build."
      ],
      "whenNotToUse": [
        "Use data-grid or sortable-data-table to browse, edit or select individual records; the pivot only shows aggregates.",
        "Use tree-table for a hierarchy that already exists in the data, such as folders or an org chart.",
        "Use cohort-retention or metric-explorer when the question is fixed and a chart answers it faster."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "The result is a table with role=\"table\", row, rowheader, columnheader and cell roles, and aria-colspan on grouped column headers, so screen readers read each value with its row and column.",
        "Every drag has a menu path: fields and chips are real buttons with menus, so the whole pivot can be built without a pointer.",
        "Row groups are buttons with aria-expanded. The scroll area is focusable and named, so keyboard users can scroll wide pivots.",
        "Changes are announced in a polite live region, such as \"Country moved to columns\" or \"Exported ridgeline-sales-pivot.csv\".",
        "Heat shading is never the only signal: every shaded cell prints its value, and the footer names the shaded measure with a low to high key.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as a filled chip, field or row header."
      ],
      "motion": [
        "The dragged chip follows the pointer on motion values, with no React render per frame. React only hears when the target well or insert position changes.",
        "Chips in the wells glide aside to open a dashed slot where the chip will land, on the smooth spring with no overshoot. On release the chip flies into that slot, or home if nothing changed, or fades out when dropped outside to remove.",
        "The table previews the drop while you hover: a new structure crossfades in, rows that appear or leave slide the rest into place, and a changed number rises into its cell.",
        "Group chevrons rotate on the spring token, heat shading fades by opacity, and the swap icon turns half a turn per swap.",
        "Reduced motion removes the fly, lift and slide; chips and rows land at once and numbers change in place."
      ],
      "responsive": [
        "From 880px the field list sits beside the wells as a column; below it becomes wrapped field pills above the wells.",
        "Wells sit in a two by two grid from 640px and stack below it, with the well name above its chips on phones.",
        "The table scrolls inside its card with the header row and first column pinned, and the grand total pinned to the bottom.",
        "On touch screens chips, fields, row groups and menu items grow to 44px. A tap opens the menu; a drag moves the chip."
      ],
      "performance": [
        "The pivot recomputes in one pass per layout change and memoizes on layout and filters; 1,200 records with nested rows and columns compute in well under a frame.",
        "Drag hit testing reads layout offsets, not animated rects, so the insert position never oscillates while neighbours move.",
        "Row and cell animations use transform and opacity only. Very large pivots (hundreds of rows) stay correct but should collapse groups by default."
      ],
      "aiNotes": [
        "Choose it when people need to slice one flat dataset their own way: sales by region and quarter, hours by team and month, tickets by queue and priority.",
        "Feed records as flat objects keyed by field id. Give dimensions an explicit values order when the natural order is not alphabetical (quarters, months, sizes).",
        "Start with a meaningful defaultLayout so the first view already answers a question. Use the empty layout only where building from scratch is the point.",
        "Persist views with onLayoutChange and pass the saved layout back as defaultLayout with a key to restore it.",
        "The engine is synchronous and client side. Aggregate on the server first for datasets beyond tens of thousands of records."
      ],
      "related": [
        "data-grid",
        "tree-table",
        "cohort-retention",
        "metric-explorer",
        "sortable-data-table"
      ],
      "usage": "import { PivotBuilder, type PivotField } from \"@/components/arc/blocks/pivot-builder/pivot-builder\";\n\nconst fields: PivotField[] = [\n  { id: \"team\", label: \"Team\", kind: \"dimension\" },\n  { id: \"month\", label: \"Month\", kind: \"dimension\", values: [\"Jan\", \"Feb\", \"Mar\"] },\n  { id: \"hours\", label: \"Hours\", kind: \"measure\", format: \"number\", aggs: [\"sum\", \"average\", \"percent\"] },\n];\n\nexport function TimeReport({ rows }: { rows: { team: string; month: string; hours: number }[] }) {\n  return (\n    <PivotBuilder\n      title=\"Logged hours\"\n      description=\"Q1 2026, all teams\"\n      records={rows}\n      fields={fields}\n      defaultLayout={{\n        filters: [],\n        columns: [{ id: \"c-month\", field: \"month\" }],\n        rows: [{ id: \"r-team\", field: \"team\" }],\n        values: [{ id: \"v-hours\", field: \"hours\", agg: \"sum\", heat: true }],\n      }}\n      fileName=\"logged-hours.csv\"\n      onLayoutChange={layout => saveView(layout)}\n    />\n  );\n}",
      "exports": [
        "PivotBuilder",
        "Aggregations"
      ]
    },
    {
      "name": "search-results",
      "kind": "block",
      "title": "Search results",
      "description": "A faceted workspace search page with suggestions, facet counts that follow the query, filter chips, mixed doc, person and file results with highlighted matches, a gliding keyboard cursor and a preview pane.",
      "tier": "pro",
      "builtFrom": [
        "Checkbox",
        "Slider",
        "Bottom sheet",
        "Filter toolbar",
        "Segmented control",
        "Empty state",
        "Button",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/search-results",
      "registry": null,
      "keywords": [
        "search",
        "search results",
        "faceted search",
        "filters",
        "facets",
        "highlight",
        "keyboard navigation",
        "skeleton",
        "no results",
        "workspace search",
        "app shell"
      ],
      "whenToUse": [
        "A full search page for a workspace or knowledge base that returns several kinds of things at once.",
        "Search where people narrow by type, team, and date, and want to read a match before opening it."
      ],
      "whenNotToUse": [
        "Use command-palette for quick jump to navigation from any screen.",
        "Use product-listing for a storefront grid filtered by price, color, and size.",
        "Use customers-table or sortable-data-table when results are records to compare in columns."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The query is a combobox with aria-activedescendant into a grouped listbox of recent and suggested searches.",
        "Results are one listbox with aria-activedescendant, so a screen reader hears each result's title, meta, and snippet as the cursor moves.",
        "Result counts and loading are announced in a polite status region; the results region sets aria-busy while skeletons show.",
        "Facets are fieldsets with legends; each option is a labeled checkbox, and options with no matches are disabled rather than hidden.",
        "The last edited range is a two thumb slider with named thumbs and month value text.",
        "Keyboard position shows through the gliding fill, never a focus ring. Highlights use mark elements, and matches are never shown by color alone."
      ],
      "motion": [
        "One neutral fill glides between results on a no overshoot spring as the cursor moves, by keyboard or pointer.",
        "The result count rolls up when results grow and down when a filter narrows them, in tabular numerals.",
        "New result sets crossfade with a short rise; the preview pane crossfades per result. Skeletons share the result grid, so nothing shifts when results land.",
        "The suggestions menu scales in from the query bar; the chip row opens and closes its height on a smooth spring.",
        "Reduced motion removes travel and the skeleton pulse and keeps plain fades."
      ],
      "responsive": [
        "From 1120px wide the layout is facets, results, and a preview pane; below that the preview opens in a bottom sheet.",
        "Below 680px the facets move into a filters sheet with a sticky Clear all and Show results footer, and the query bar gains a Filters button with a count.",
        "Rows and sheet controls keep 44px touch targets; titles wrap to two lines and snippets clamp to two.",
        "Layout decisions use a container query, so the block adapts to its column, not the window."
      ],
      "performance": [
        "Matching, sorting, and facet counts are memoized and run on the client for a few hundred items; move them to the server for larger indexes.",
        "Only transform and opacity animate; the preview pane width is measured once with a ResizeObserver."
      ],
      "aiNotes": [
        "Map your index to the three result shapes; keep snippets to the sentence that contains the match, since highlighting reads the query terms.",
        "Facet counts are disjunctive within a group: each option counts what you would see if you added it. Keep that behavior when counts come from a server.",
        "Pass latency={0} and drive loading from your own fetch by swapping results when a query resolves.",
        "Photos in file results come from lib/media; replace them with real thumbnails."
      ],
      "related": [
        "command-palette",
        "filter-toolbar",
        "help-center",
        "product-listing"
      ],
      "usage": "import { SearchResults } from \"@/components/arc/blocks/search-results/search-results\";\n\nexport function WorkspaceSearch({ items }: { items: SearchResult[] }) {\n  return (\n    <SearchResults\n      results={items}\n      workspace=\"Acme\"\n      defaultQuery=\"\"\n      latency={0}\n      onSearch={query => track(\"search\", { query })}\n      onOpen={result => router.push(`/${result.kind}/${result.id}`)}\n    />\n  );\n}",
      "exports": [
        "SearchResults"
      ]
    },
    {
      "name": "env-variables",
      "kind": "block",
      "title": "Environment variables",
      "description": "An environment variables and secrets manager with masked values, per-environment scope toggles, .env paste import with duplicate and invalid key warnings, write-only sensitive values, an environment diff by fingerprint, and a redeploy banner.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Checkbox",
        "Copy button",
        "Input",
        "Search field",
        "Segmented control",
        "Select",
        "Switch",
        "Textarea"
      ],
      "docs": "https://uiarc.dev/components/blocks/env-variables",
      "registry": null,
      "keywords": [
        "environment variables",
        "env",
        "dotenv",
        ".env",
        "secrets",
        "config",
        "configuration",
        "vercel",
        "netlify",
        "deploy",
        "redeploy",
        "preview",
        "production",
        "development",
        "sensitive",
        "masked",
        "diff",
        "compare environments",
        "developer settings"
      ],
      "whenToUse": [
        "A settings screen where developers manage configuration and secrets across development, preview, and production.",
        "Onboarding a project by pasting an existing .env file and scoping it to environments in one step.",
        "Auditing drift between environments before a release, such as a key set in Preview but missing in Production."
      ],
      "whenNotToUse": [
        "Use api-keys for creating, scoping, and revoking access tokens that your product issues to its users.",
        "Use a plain form for a handful of fixed settings that are not per environment."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-select",
        "@radix-ui/react-switch",
        "@radix-ui/react-checkbox"
      ],
      "accessibility": [
        "Masked values render as bullets hidden from assistive technology plus a visually hidden \"Hidden value\" or \"Sensitive value, write-only\" label; the real value is only rendered after Reveal.",
        "Reveal and environment toggles are buttons with aria-pressed and names that include the key, such as \"Production for DATABASE_URL\".",
        "Each row's toggles sit in a labelled group; the wide matrix column heads are decorative and every toggle carries its own name.",
        "Inline errors (invalid key, key already set in an environment, empty value) are tied to their fields; scope conflicts appear as an alert under the row.",
        "A polite live region announces every change, import, removal, and redeploy result, and the .env parse summary updates live as you type.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as a fill on toggles and icon buttons, matching hover."
      ],
      "motion": [
        "The redeploy banner, add panel, inline editor, and conflict notes open with height and opacity on the smooth spring and close with a quicker exit ease.",
        "Environment toggles fill with a snappy scale and draw their check with pathLength, both without overshoot.",
        "Revealing a value crossfades the mask and the value with a short rise in a fixed height slot, so the row never jumps.",
        "Added and removed rows grow and collapse their height so the list closes its gap; saved or imported rows glow once in the accent and fade.",
        "The Variables and Compare views crossfade with a small rise. Reduced motion removes all travel and lands every change at once."
      ],
      "responsive": [
        "From a 760px container, rows become a matrix: key, value, one toggle column per environment with a count of values set, and an edit action.",
        "Below that, each row stacks the key and edit action, then the value, then three labelled environment pills in an even grid.",
        "Below 560px header actions, search, form actions, and the redeploy button go full width; the .env preview drops the masked value column.",
        "Toggles, icon buttons, and the swap control grow to 44px targets on touch screens. The block is a container query, so it adapts to the space it is placed in."
      ],
      "performance": [
        "The .env text is parsed in a memo keyed on the text, rows, and chosen environments; there is no per-frame work.",
        "Only revealed rows render their value and copy button, and only the open row renders an editor.",
        "Timers for notes, the saved glow, and the redeploy confirmation are cleared on change and unmount."
      ],
      "aiNotes": [
        "Choose it for a project settings page in a hosting, deployment, or backend platform. Wire onChange to your store and onRedeploy to your deploy API.",
        "Search matches key names only, never values. Compare shows a six character fingerprint of each value, so two environments can be checked without exposing either.",
        "Sensitive rows are write-only: the editor starts empty and saving replaces the value; the flag cannot be turned off without removing and re-adding the variable.",
        "The .env parser handles comments, blank lines, export prefixes, quotes, and inline comments. Later duplicates win the key and earlier ones are skipped; keys that start with a number or contain other characters are rejected."
      ],
      "related": [
        "api-keys",
        "security-settings"
      ],
      "usage": "import { EnvVariables } from \"@/components/arc/blocks/env-variables/env-variables\";\n\nexport function ProjectEnvironment({ project, variables }) {\n  return (\n    <EnvVariables\n      project={project.slug}\n      variables={variables}\n      sampleDotenv=\"\"\n      onChange={rows => saveVariables(project.id, rows)}\n      onRedeploy={environments => triggerRedeploy(project.id, environments)}\n    />\n  );\n}",
      "exports": [
        "EnvVariables"
      ]
    },
    {
      "name": "market-terminal",
      "kind": "block",
      "title": "Market terminal",
      "description": "A trading terminal with a canvas candlestick chart, volume, moving averages, crosshair OHLC readout, wheel, drag and pinch zoom, a live watchlist with sparklines and flashing prices, and a market and limit order ticket.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Button",
        "Number field",
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/market-terminal",
      "registry": null,
      "keywords": [
        "trading",
        "stocks",
        "market",
        "candlestick",
        "ohlc",
        "volume",
        "moving average",
        "watchlist",
        "order ticket",
        "brokerage",
        "paper trading",
        "limit order",
        "crosshair",
        "zoom",
        "pan",
        "live prices",
        "finance",
        "terminal"
      ],
      "whenToUse": [
        "A trading or investing product that needs a full chart, a watchlist and an order ticket on one screen.",
        "A paper trading or education app where people practice placing market and limit orders against moving prices.",
        "A markets dashboard where candles, volume and moving averages must be inspected bar by bar."
      ],
      "whenNotToUse": [
        "Use order-book-depth to show bids and asks at each price level.",
        "Use metric-explorer or a line chart for product or business metrics without open, high, low and close.",
        "Use options-payoff to model an options position's profit at expiry.",
        "Use wallet-card for a simple balance and recent transactions without a chart."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The chart is a focusable image with a role description, a label naming the symbol, range and candle interval, and a description of its keys.",
        "Moving the crosshair with the keyboard announces the candle's time, open, high, low, close, change and volume through a polite live region.",
        "Watchlist rows are buttons with a full label (ticker, name, price and change today); the charted symbol carries aria-current.",
        "Direction is never color alone: changes carry a sign and an arrow, and order status is written as text.",
        "Order problems (too few shares, not enough buying power, not enough shares to sell) and failures are written in a status line under the button.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as a fill on rows, toggles and icon buttons, and as the crosshair on the chart."
      ],
      "motion": [
        "The chart is one canvas drawn in a requestAnimationFrame loop that runs only while something moves; React never re-renders per frame.",
        "Zoom, pan and range changes ease the view and the price scale with an exponential approach timed from the smooth spring, so nothing overshoots and every gesture can be reversed mid-way.",
        "A drag flick carries momentum that decays like a thrown card. Pinch zooms around the midpoint of two fingers.",
        "Live ticks glide the last candle, its price tag, the price line and the moving averages to the new price; the header price rolls with the animated counter.",
        "Each watchlist price flashes a green or red tint in the direction it moved, as a single opacity fade. The selected row's fill glides between rows.",
        "The limit field opens with height and opacity together; new orders grow into the list. The live dot's pulse is the only loop, because it reports live status.",
        "Reduced motion makes the view, prices and panels change at once, drops momentum and the pulse, and holds the flash as a brief static tint."
      ],
      "responsive": [
        "From 1080px the watchlist and ticket stack in a 340px column beside the chart; from 720px they sit side by side under it; below that everything stacks.",
        "On narrow charts the OHLC readout moves above the canvas, the chart shortens to 340px, and the default window thins so candles stay at least five pixels apart.",
        "Below 640px the stats row spans four equal columns, the range control takes its own row, and every button in the toolbar grows to 44px.",
        "Touch drags pan the chart horizontally while vertical swipes still scroll the page; a tap pins the crosshair and a second tap clears it."
      ],
      "performance": [
        "Candles, volume, averages and the crosshair draw on one canvas at up to 2x device pixel ratio, only for the visible candles.",
        "The readout, axis tags and crosshair labels are written straight into their text nodes by the engine, and only when their text changes.",
        "Series are generated once per symbol and range and then updated in place by ticks; moving averages are one running sum.",
        "Ticks pause while the tab is hidden, and theme or accent changes recolor the canvas without rebuilding it."
      ],
      "aiNotes": [
        "Choose it for the main screen of a brokerage, a paper trading app or an internal markets dashboard where one symbol is charted and traded at a time.",
        "Everything is simulated: candles are generated from each symbol's price, previous close and volatility, ticks are a random walk, and orders fill against the live price locally. Pass the callbacks and your own data for real use.",
        "Today's session is identical on every range: coarser candles are folded from the five minute bars, and ticks update the live candle of every range together.",
        "Working limit orders fill on their own when the live price crosses them, and a marketable limit fills at the better of the limit and the market.",
        "Keep sample prices believable: the header, watchlist and ticket all read the same live price, so a mismatch shows immediately."
      ],
      "related": [
        "metric-explorer",
        "wallet-card"
      ],
      "usage": "import { MarketTerminal } from \"@/components/arc/blocks/market-terminal/market-terminal\";\n\nexport function Trade() {\n  return (\n    <MarketTerminal\n      defaultTicker=\"AAPL\"\n      defaultRange=\"1D\"\n      onPlaceOrder={order => api.orders.create(order)}\n      onCancelOrder={order => api.orders.cancel(order.id)}\n    />\n  );\n}",
      "exports": [
        "MarketTerminal"
      ]
    },
    {
      "name": "budget-variance",
      "kind": "block",
      "title": "Budget variance",
      "description": "Budget against actual operating spend by department as aligned bullet charts, with favorable and unfavorable variance, a month, quarter and year to date switch that springs every bar, in-place drill-down into budget lines, and a total row.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/budget-variance",
      "registry": null,
      "keywords": [
        "budget",
        "budget vs actual",
        "variance",
        "bullet chart",
        "finance",
        "fp&a",
        "operating expenses",
        "opex",
        "cost center",
        "department spend",
        "favorable",
        "unfavorable",
        "plan vs actual",
        "month close"
      ],
      "whenToUse": [
        "Monthly close and budget review screens that compare planned and actual operating spend by department or cost center.",
        "Finance dashboards where a controller needs to see at a glance which teams are over, which are under, and by how much.",
        "Any plan against actual comparison with a meaningful tolerance band, such as headcount cost or cloud spend by team."
      ],
      "whenNotToUse": [
        "Use mrr-waterfall to explain how a single total moved from one value to another.",
        "Use usage-forecast to project spend against a cap over time.",
        "Use a line or bar chart to show a trend across many periods; this block compares one window at a time.",
        "Use a sortable data table when there are dozens of cost centers and sorting matters more than the visual comparison."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each department is a heading with a disclosure button (aria-expanded, aria-controls) whose name is a full sentence: spent, budget, variance and whether it is favorable.",
        "The total and every budget line carry the same sentence in visually hidden text; the charts and the counting numbers are hidden from assistive technology so nothing is read twice or mid-count.",
        "Changing the window announces the new window and the total through a polite live region.",
        "Variance never relies on color: amounts carry a sign and every share ends in over, under or on plan.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The focused department shows a filled row."
      ],
      "motion": [
        "Switching the window springs every bar to its new length on one critically damped spring and counts every amount and share to the new value, all written to the DOM from motion values.",
        "The overspend is the same bar clipped to the space past budget, so the danger color appears exactly as the bar crosses the tick, with no second animation to keep in sync.",
        "Opening a department grows its lines in height and opacity together without overshoot; closing is faster. The chevron turns on the shared spring curve.",
        "Every animation is interruptible: a new window or toggle starts from wherever the bars and numbers are.",
        "Reduced motion jumps bars and numbers to their final values and opens lines at full height."
      ],
      "responsive": [
        "From a 760px container the block is a table: name and owner, the bullet chart, actual, budget, variance and the disclosure chevron in aligned columns.",
        "Below that each row puts the name, its actual of budget and the variance on top and the bullet chart across the full width below, so the chart never gets squeezed.",
        "Long names wrap instead of truncating. The period switch fills the width on phones.",
        "Rows are at least 60px tall and the switch grows to 44px on touch screens."
      ],
      "performance": [
        "Bars animate transform only (scaleX from the left edge); bands and ticks are static percentage positions, so a window change never measures layout.",
        "Numbers count on motion values that write text content directly, so a window change renders React once, not once per frame.",
        "Department and total figures are memoized per window; the shared scale is computed once per data set."
      ],
      "aiNotes": [
        "Choose it for cost centers where spending under budget is favorable. Every department and line is read as an expense.",
        "Pass one amount per period id for both budget and actual on every line; departments and the total are summed, so they always reconcile.",
        "The scale is shared by every row and every period (at least 120 percent of budget, rounded up to fit the largest line), so the budget tick never moves when the window changes.",
        "Bands mix the text color and the overspend uses the danger token, so the block stays neutral in any accent. The accent marks only the open department's chevron and the selected window.",
        "The first render formats every number from props, so server and client match."
      ],
      "related": [
        "mrr-waterfall",
        "usage-forecast",
        "revenue-explorer",
        "metrics-dashboard"
      ],
      "usage": "import { BudgetVariance } from \"@/components/arc/blocks/budget-variance/budget-variance\";\n\nexport function FinanceOverview({ departments }) {\n  return (\n    <BudgetVariance\n      departments={departments}\n      defaultPeriod=\"quarter\"\n      ranges={[0.9, 1.05]}\n      onDepartmentToggle={(id, open) => open && prefetchLines(id)}\n    />\n  );\n}",
      "exports": [
        "BudgetVariance"
      ]
    },
    {
      "name": "choropleth-explorer",
      "kind": "block",
      "title": "Choropleth explorer",
      "description": "A filled-region map that shades every state or region by revenue, growth or penetration on a stepped scale, with a country switch (United States, Canada, Brazil, United Kingdom, France, Germany, Italy, Australia), a hoverable legend, a synced ranked list and a camera zoom into local detail.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/choropleth-explorer",
      "registry": null,
      "keywords": [
        "choropleth",
        "map",
        "filled map",
        "heat map",
        "geography",
        "states",
        "counties",
        "regions",
        "drilldown",
        "zoom",
        "sales territories",
        "penetration",
        "legend",
        "analytics"
      ],
      "whenToUse": [
        "Sales, revenue or usage dashboards that break a metric down by state, country or territory.",
        "Market coverage and penetration reviews where a region's own subareas explain its number.",
        "Any analytics view where readers compare neighbouring areas and need the ranked numbers beside the map."
      ],
      "whenNotToUse": [
        "Use offices-map to place pins at specific locations such as offices or stores.",
        "Use revenue-globe for flows and totals on a spinning globe when geography is ambience rather than the comparison.",
        "Use a bar chart or sortable table when the regions are few, or when area size would mislead (a large, sparse region draws more attention than a small, dense one)."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The map is an image with a label naming the area and the metric; the ranked list next to it is the accessible path to every value and every zoom.",
        "The list is one tab stop with roving focus. Each row names its rank, region, value and whether it opens counties.",
        "Zooming, zooming out, switching the metric and pinning a county are announced through one polite live region, including the highest region for the new metric.",
        "Legend steps are toggle buttons with aria-pressed and a label that counts their regions and states their range. Values carry a sign and a true minus, so direction never depends on color alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as a filled row and an outline on the map."
      ],
      "motion": [
        "Switching the metric fades each region to its new fill with a delay set by its position, so the change sweeps across the map from west to east in under 800ms.",
        "Selecting a state moves one camera on a critically damped spring: translate and scale travel together, so the state glides to the center in a straight line and can be reversed mid-flight. Counties fade in while the camera is still arriving; other states recede to context.",
        "The headline total counts to each new value on a motion value, list rows glide to their new rank with a layout spring, and rank bars resize on a transform.",
        "The tooltip follows the pointer on motion values without React renders; the hovered row scrolls into view inside the list, never the page.",
        "Reduced motion makes every fill, zoom, count and reorder instant."
      ],
      "responsive": [
        "From an 820px container the map and the ranked list sit side by side and the list matches the map column's height; below that the list stacks under the map and scrolls inside a fixed height.",
        "The metric switch takes the full width on phones; the legend wraps its no data step below the ramp.",
        "Rows are 44px tall, and switches, legend steps and the back button grow to 44px on touch. Tapping a state zooms in directly; tooltips are limited to fine pointers.",
        "Nothing scrolls sideways: the map scales with its container through the view box."
      ],
      "performance": [
        "About 51 state paths plus the counties of one state at a time, with one delegated pointer listener for the whole map.",
        "The camera writes a single transform attribute from three motion values, and strokes use non-scaling-stroke so the zoom never re-rasterises line widths in JavaScript.",
        "Fills change through CSS transitions of fill and fill-opacity; ranking and binning are memoized per metric and level.",
        "The sample data file is about 85KB of simplified path data; supply your own paths at the precision your map needs."
      ],
      "aiNotes": [
        "Choose it when the question is how a metric is distributed across areas that people recognise by shape, and when a region's subareas are worth a closer look.",
        "Bins are recomputed for whatever is in view: quantile breaks rounded to two significant digits, split at zero with a second hue on diverging metrics. Pass raw values and let the block choose the steps.",
        "Paths are straight segment SVG data. Top level regions share one view box; subregions use a local grid placed with origin [x, y, unit], which keeps small states sharp when zoomed. The sample data is simplified US Census cartographic boundaries in Albers USA.",
        "Only ten states carry county data in the sample; the others zoom in and explain there is no county detail, which is also how the block handles a region without subregions.",
        "The positive side uses the first chart series color and the negative side the third, both theme tokens. No data renders as a flat neutral with its own legend step."
      ],
      "related": [
        "revenue-globe",
        "revenue-explorer",
        "kpi-drilldown",
        "metric-explorer"
      ],
      "usage": "import { ChoroplethExplorer } from \"@/components/arc/blocks/choropleth-explorer/choropleth-explorer\";\n\nexport function SalesByRegion() {\n  return (\n    <ChoroplethExplorer\n      defaultMetric=\"growth\"\n      onRegionOpen={id => router.replace(id ? `?state=${id}` : \"?\")}\n    />\n  );\n}",
      "exports": [
        "ChoroplethExplorer"
      ]
    },
    {
      "name": "small-multiples",
      "kind": "block",
      "title": "Small multiples",
      "description": "A trellis of small line charts that reads as one chart, with a synced crosshair, a shared or independent y scale that springs the axes, sorting with a FLIP reorder, a ghost of the overall total behind every panel and dimming for flat trends.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Switch"
      ],
      "docs": "https://uiarc.dev/components/blocks/small-multiples",
      "registry": null,
      "keywords": [
        "small multiples",
        "trellis",
        "facet",
        "sparklines",
        "line chart grid",
        "synced crosshair",
        "shared scale",
        "independent scale",
        "compare regions",
        "compare products",
        "trend",
        "analytics",
        "dashboard"
      ],
      "whenToUse": [
        "Comparing revenue, signups or usage across regions, products, plans or channels over the same months.",
        "Showing which segments grow, shrink or stall at a glance, with one shared crosshair to read every value for the same period.",
        "Dashboards where one multi-line chart would be a tangle of overlapping series."
      ],
      "whenNotToUse": [
        "Use line-chart when two or three series need to be read against each other on one plot.",
        "Use metric-explorer or kpi-drilldown when a person picks one metric and drills into its detail.",
        "Use sparkline inside a table row when each series is a small supporting detail beside other columns.",
        "Use a plain table when the exact values matter more than the shape of each trend."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The panels are one focusable group with a keyboard hint, so the whole trellis is a single tab stop instead of eight.",
        "Moving the crosshair by keyboard announces the period, the total and the highest series through one polite live region; sorting, scaling and dimming announce what changed.",
        "A visually hidden table lists every series with its value at the current period, its growth and its trend in words, in the current sort order.",
        "Trend direction is carried by an arrow icon and a signed percentage with a true minus sign, and flat trends are labelled Flat, so dimming never relies on opacity alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as the crosshair and the filled selection in the controls."
      ],
      "motion": [
        "Switching the y scale springs each panel's domain on one critically damped spring. The line, the area, the end dot and the axis ticks all derive from the same two motion values, so the axes slide and the line reshapes as one movement; ticks for the other scale fade in as they arrive.",
        "Sorting moves every panel to its new slot with a FLIP layout animation on a no-overshoot spring, and the hairline dividers travel with the panels.",
        "The crosshair appears in place, then glides between periods on a short spring written straight to motion values; the dot in each panel rides its line between points.",
        "Dimming flat trends fades the panel content only, so the grid dividers stay crisp.",
        "Reduced motion makes the rescale, the reorder and the crosshair instant."
      ],
      "responsive": [
        "Four columns from a 640px container, two below it, with plot heights of 104px, 92px and 76px as the container narrows.",
        "On phones the controls wrap to full rows, the panel value moves under the name, and the tick gutter narrows.",
        "Hovering a panel drives the crosshair on fine pointers; on touch, a tap or horizontal drag on a chart places it while vertical swipes still scroll the page.",
        "Controls grow to 44px on coarse pointers."
      ],
      "performance": [
        "Paths are SVG in a fixed viewBox stretched to the panel, with non-scaling strokes, so resizing never recomputes geometry.",
        "During a rescale, paths and ticks are rebuilt from motion values each frame without React renders; the crosshair moves by transforms of full-size layers.",
        "React renders only when the crosshair crosses into a new period, and the FLIP layout measurement runs only when the order changes."
      ],
      "aiNotes": [
        "Choose it when the question is how several series of the same measure compare over one period, and every panel should be read at once rather than opened.",
        "Pass series with the same number of values as periods. Four to twelve panels work best; the grid is two columns on phones and four from a 640px container.",
        "The default formatters read values as thousands of US dollars; pass formatValue and formatTick for any other unit.",
        "The ghost line is the sum of all series drawn in its own scale, so it shows the overall shape, not a size to compare against.",
        "The line uses the first chart series color, which follows the accent; keep one color so the panels read as one chart."
      ],
      "related": [
        "metric-explorer",
        "kpi-drilldown",
        "revenue-explorer",
        "line-chart",
        "sparkline"
      ],
      "usage": "import { SmallMultiples } from \"@/components/arc/blocks/small-multiples/small-multiples\";\n\nexport function SignupsByPlan() {\n  return (\n    <SmallMultiples\n      title=\"Signups by plan\"\n      description=\"Weekly signups, last 26 weeks\"\n      series={plans.map(plan => ({ id: plan.id, name: plan.name, values: plan.weekly }))}\n      periods={weeks.map(week => ({ short: week.short, long: week.long }))}\n      growthLag={4}\n      formatValue={value => value.toLocaleString(\"en-US\")}\n      formatTick={value => value.toLocaleString(\"en-US\")}\n      defaultScale=\"shared\"\n    />\n  );\n}",
      "exports": [
        "SmallMultiples"
      ]
    },
    {
      "name": "connected-scatter",
      "kind": "block",
      "title": "Connected scatter",
      "description": "A connected scatterplot of monthly CAC against LTV where the path draws itself in, a scrubber moves a head along the curve, and a channel switch morphs the path against a shaded 3x target line.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/connected-scatter",
      "registry": null,
      "keywords": [
        "connected scatter",
        "connected scatterplot",
        "scatter plot",
        "cac",
        "ltv",
        "ltv to cac",
        "unit economics",
        "time series",
        "trajectory",
        "path chart",
        "scrubber",
        "morph",
        "growth",
        "acquisition channels",
        "price volume"
      ],
      "whenToUse": [
        "A growth or finance screen that shows whether unit economics are moving toward or away from a target ratio.",
        "Comparing how a few segments or channels travelled through the same two metrics over the same months.",
        "A report where specific months (a pricing change, a campaign) explain a turn and deserve a label on the path."
      ],
      "whenNotToUse": [
        "Use line-chart or metric-explorer when time itself is the axis and one metric is enough.",
        "Use efficient-frontier for risk against return of a portfolio you can change.",
        "Use a plain scatter or beeswarm when points have no time order to connect."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The scrubber is role=\"slider\" over the months, with a value text that reads the date, both values, the ratio and whether it is above or below the target.",
        "A polite live region announces the month when the head settles and the new values when the channel changes. Playback announces only where it stops.",
        "The readout is a description list, so the selected month, both values and the ratio are always available as text.",
        "The chart drawing is hidden from assistive technology; a visually hidden table lists every month with both values and the ratio for the current channel.",
        "Above or below target is written out beside a status dot, never shown by color alone.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The focused scrubber fills and enlarges its thumb."
      ],
      "motion": [
        "The first time the block scrolls into view, the head travels every month and the path draws in behind it; the dotted future path, arrows and labels fade in once it lands.",
        "Scrubbing moves the head along the real curve, not a straight line between months. A press glides the head to the pointer once, then it follows one to one, and a release settles on the nearest month with the smooth spring, which never overshoots.",
        "Switching channel morphs every point, arrow and the path itself on the smooth spring; the readout numbers count through the in between values and month labels crossfade.",
        "Every moving part (path, trail length, head, guides, dots, arrows, scrubber thumb and readout values) is a motion value written once per frame, so none of it re-renders React.",
        "Reduced motion shows the finished path at once, jumps the head to each month, steps playback month by month and swaps channels without a morph."
      ],
      "responsive": [
        "The block reflows against its own width with container queries: four readout cells in a row from 720px, two by two below it.",
        "The chart draws in real pixels at every size, so text and strokes never stretch; the plot is 380px tall on wide layouts, 330px at tablet widths and 300px on phones.",
        "Below 480px of plot width the notes, the target line label and the zone label hide (the key names both), x ticks thin to every other gridline, and edge labels flip inward.",
        "The scrubber is 44px tall for touch and the play button is a full size control. On phones the play button sits above the scrubber, and quarter labels under the scrubber hide below 560px.",
        "Tapping a point on a touch screen shows its values and moves the head there."
      ],
      "performance": [
        "The path is traced as Catmull-Rom Béziers and sampled once per resize or per morph frame (24 samples per month), so moving the head is a lookup, not a curve solve.",
        "Only month changes, hover changes and the play state re-render React; every frame writes motion values directly.",
        "Comfortable up to a few dozen points per series; beyond that, thin the direction arrows or aggregate to quarters."
      ],
      "aiNotes": [
        "Choose it when the story is how two metrics moved together over time, such as CAC against LTV, price against volume, or spend against conversions.",
        "The sample data is monthly CAC and 12 month LTV for three acquisition channels, January 2025 to June 2026. Replace SERIES, X_AXIS and Y_AXIS with your own; keep the same number of points in every series.",
        "Use notes sparingly (one or two per series) for events that explain a turn in the path; the first and last month are labelled automatically and narrow plots hide the notes.",
        "The target line always passes through the origin, so it suits ratio targets. Set min on both axes to zoom into where the data lives; the shaded region is clipped to the plot.",
        "The first render is server safe: it draws at a default size with the head on the first month, then measures and animates on the client."
      ],
      "related": [
        "metric-explorer",
        "segmented-control",
        "line-chart"
      ],
      "usage": "import { ConnectedScatter } from \"@/components/arc/blocks/connected-scatter/connected-scatter\";\n\nexport function UnitEconomics() {\n  return (\n    <ConnectedScatter\n      defaultSeries=\"partners\"\n      onMonthChange={index => setSelectedMonth(index)}\n    />\n  );\n}",
      "exports": [
        "ConnectedScatter"
      ]
    },
    {
      "name": "horizon-chart",
      "kind": "block",
      "title": "Horizon chart",
      "description": "A horizon chart that stacks fourteen daily series as compact folded rows, with a synced crosshair, a sort by name, peak or latest, a band count setting that folds every row on one spring, and a legend that explains how to read the bands.",
      "tier": "pro",
      "builtFrom": [
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/horizon-chart",
      "registry": null,
      "keywords": [
        "horizon chart",
        "horizon graph",
        "time series",
        "many series",
        "deviation",
        "baseline",
        "anomaly",
        "monitoring",
        "compact chart",
        "bands",
        "crosshair",
        "analytics"
      ],
      "whenToUse": [
        "Operations or growth dashboards comparing many markets, services or products against their own baseline over weeks or months.",
        "Monitoring views where the reader scans for which rows broke from normal and when, then reads exact values on one day.",
        "Any time series comparison where small multiples of line charts would need too much height."
      ],
      "whenNotToUse": [
        "Use small-multiples when there are only a few series and the reader needs axes and exact levels in each panel.",
        "Use ridgeline to compare distributions rather than values over time.",
        "Use streamgraph to show how parts add up to a total over time.",
        "Use line-chart for one to four series that should be read precisely against a y axis."
      ],
      "dependencies": [
        "motion"
      ],
      "accessibility": [
        "The chart is a focusable group with a keyboard hint; focusing it places the crosshair on the latest day.",
        "A visually hidden table lists every row with its latest value, its highest and its lowest value with their dates, in the current order.",
        "Moving the crosshair by keyboard announces the day with the highest and lowest row through one polite live region; sort and band changes are announced there too.",
        "Values carry a sign and the legend names both sides (below and above), so direction never depends on color alone. The color scale is an image with a full text label.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Keyboard position shows as the crosshair, the readout and the selected fills of the switches."
      ],
      "motion": [
        "Changing the band count animates one motion value that drives two custom properties on the root, so every layer in every row folds or unfolds together on a no-overshoot spring, on transform and opacity alone.",
        "Sorting moves each row to its new place with a layout spring that never overshoots.",
        "The crosshair is a full-width track translated by a share of its own width, following the pointer on a short critically damped spring; it appears in place instead of sliding in from its last day.",
        "Row values and the date are written straight to the DOM while scrubbing, so the pointer never renders React.",
        "Reduced motion makes the fold, the reorder and the crosshair instant."
      ],
      "responsive": [
        "Rows share one plot column, so the crosshair spans every row at any width.",
        "Below a 560px container the name and value columns narrow, text steps down to the small size and the switches wrap under the title at full width.",
        "Switches grow to 44px on touch. Tapping or dragging across the chart sets the crosshair and keeps it after the finger lifts; vertical scrolling is never captured.",
        "The legend moves under the readout on narrow screens and keeps its fixed scale so band changes never shift the layout."
      ],
      "performance": [
        "Each row draws its above and below areas once as paths in a unit box; the bands are CSS transforms of those paths, so changing the band count never rebuilds geometry.",
        "Scrubbing writes the date and one text node per row without React renders; hit testing uses one rectangle over the plot column.",
        "Comfortable up to about 40 rows of a few hundred days; beyond that, aggregate days into weeks."
      ],
      "aiNotes": [
        "Choose it for 8 to 30 series on one time axis where each value is a deviation from a baseline: sessions against a seasonal average, latency against a target, demand against a forecast, returns against a benchmark.",
        "Pass signed values against the baseline, not raw levels. Convert levels first (value / baseline - 1) so rows of different sizes compare on one scale.",
        "The above side uses the first chart series color and the below side the third, both theme tokens that follow the accent; keep them if you restyle.",
        "Every row always renders four layers per side; a layer above the band count covers values past the extent and stays empty, which is what lets the band count animate continuously."
      ],
      "related": [
        "small-multiples",
        "ridgeline",
        "streamgraph",
        "activity-heatmap"
      ],
      "usage": "import { HorizonChart } from \"@/components/arc/blocks/horizon-chart/horizon-chart\";\n\nexport function MarketHealth({ markets, days }) {\n  return (\n    <HorizonChart\n      title=\"Sessions against baseline\"\n      series={markets}\n      days={days}\n      baselineLabel=\"spring average\"\n      defaultBands={3}\n      onDayChange={index => setInspectedDay(index)}\n    />\n  );\n}",
      "exports": [
        "HorizonChart"
      ]
    },
    {
      "name": "line-replay",
      "kind": "block",
      "title": "Line replay",
      "description": "A time replay line chart that draws a day of traffic with a moving playhead, a value label on the tip, a scrubber, speed control and key moments annotated as the playhead passes them.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Segmented control"
      ],
      "docs": "https://uiarc.dev/components/blocks/line-replay",
      "registry": null,
      "keywords": [
        "replay",
        "time replay",
        "playback chart",
        "animated line chart",
        "playhead",
        "scrubber",
        "timeline",
        "annotations",
        "key moments",
        "launch day",
        "incident timeline",
        "story chart",
        "speed control"
      ],
      "whenToUse": [
        "Post-launch or campaign reviews where the team wants to watch traffic respond to each announcement.",
        "Incident retrospectives that step through a metric with the alert, the deploy and the recovery marked.",
        "Storytelling pages and reports where a single series over one session or day is best understood in sequence."
      ],
      "whenNotToUse": [
        "Use annotated-trend when people need to add and edit their own notes on a static trend.",
        "Use connected-scatter to replay how two measures moved together over time.",
        "Use metric-explorer or a line chart to compare several series or ranges at a glance without playback."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The scrubber is a role=\"slider\" measured in minutes; its value text reads the time and the value (\"9:40 am, 2,316 on site\") and is updated whenever playback is paused, so a screen reader is not flooded while it plays.",
        "The readout above the chart repeats the time, the value, the ratio to the comparison and the peak so far as text, so nothing is available only as a line position. The drawing itself is hidden from assistive technology.",
        "Passing a moment during playback, pausing, jumping and finishing are announced through a polite live region (\"9:40 am. Hacker News front page.\", \"Replay finished. Peak 6,041 at 10:25 am.\").",
        "Key moments are an ordered list of buttons; the latest reached one has aria-current. A moment's effect line stays hidden from screen readers until the replay has played through it.",
        "The play button has a specific name for each state: Play replay, Pause replay, Replay from the start.",
        "Incidents carry a warning icon and their own title, never color alone. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. The scrubber and the moment rows show keyboard position with a fill."
      ],
      "motion": [
        "Playback advances a single motion value from requestAnimationFrame; each frame writes the line and area paths, the playhead, the tip label, the axis clock, the scrubber and the readout straight to the DOM. React renders only when a moment is passed, playback starts or stops, or the size changes.",
        "Replay and jumping to a moment glide the playhead with the smooth spring, which never overshoots, so the line retracts or extends to the target and a new jump mid-way picks up from where it is.",
        "Passing a moment settles its marker onto the line (opacity and scale), fades in its stem, and rises its callout into the lane above the plot on the smooth spring; the previous callout fades out faster than the new one enters.",
        "Time axis labels fade out of the way of the playhead clock instead of overlapping it.",
        "Reduced motion keeps playback (it is started by the viewer and can be paused at any time) but makes rewinds, jumps, markers and callouts instant and turns off autoplay."
      ],
      "responsive": [
        "From a block width of 1000px the key moments sit in a divided column beside the chart; below that they move under it in two columns, and in one column below 560px.",
        "Below 560px the readout becomes a three-column grid with the peak on its own row, the time axis labels every six hours, the chart is 300px tall, and the scrubber takes a full row above the play button and the speed control.",
        "The play button and every moment row are at least 44px tall, and the scrubber's hit area is 44px tall. The chart keeps vertical page scrolling; a horizontal drag or a tap scrubs."
      ],
      "performance": [
        "One path string is built per frame from precomputed point positions (288 points in the sample), and text nodes are written only when their text changes.",
        "Running peaks, moment positions and effects are computed once per data set; positions are recomputed only on resize through a ResizeObserver.",
        "No React render happens per frame; state changes only a handful of times per replay."
      ],
      "aiNotes": [
        "Choose it when the order in which something happened is the story: a launch day, an incident, an election night, a match, a trading session. The replay makes cause and effect visible by revealing the line and the moments together.",
        "Pass points on one clock in minutes from the start, and moments on the same clock. Points do not need to be evenly spaced; moments between points are placed by interpolation.",
        "The value axis is fixed to the whole series, so the line never rescales while it plays.",
        "The sample is an illustrative launch day for a fictional product with typical traffic for comparison; it is not real traffic."
      ],
      "related": [
        "connected-scatter",
        "metric-explorer"
      ],
      "usage": "import { LineReplay, type ReplayMoment, type ReplayPoint } from \"@/components/arc/blocks/line-replay/line-replay\";\n\nexport function IncidentReplay({ points, moments }: { points: ReplayPoint[]; moments: ReplayMoment[] }) {\n  return (\n    <LineReplay\n      title=\"Checkout errors, 14 May\"\n      description=\"Failed payments per minute across the incident window, UTC.\"\n      data={points}\n      moments={moments}\n      seriesLabel=\"14 May\"\n      baselineLabel=\"Previous Tuesday\"\n      valueLabel=\"Errors\"\n      duration={20}\n      onMomentReached={moment => analytics.track(\"replay_moment\", { id: moment.id })}\n    />\n  );\n}",
      "exports": [
        "LineReplay"
      ]
    },
    {
      "name": "exploded-view",
      "kind": "block",
      "title": "Exploded view",
      "description": "A scroll driven exploded view where a live phone screen built from five CSS 3D layers lies back, separates in depth with labels that follow each layer, takes each layer into focus, and then reassembles.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/exploded-view",
      "registry": null,
      "keywords": [
        "exploded view",
        "layers",
        "3d",
        "css 3d",
        "perspective",
        "isometric",
        "scroll driven",
        "scrollytelling",
        "product screen",
        "phone mockup",
        "design system",
        "showpiece",
        "sticky section",
        "depth"
      ],
      "whenToUse": [
        "A showpiece section that shows a product is built with care, by literally taking its screen apart.",
        "Explaining the layers of a design system or UI kit: tokens, grid, content, controls and overlays.",
        "A launch page moment between the hero and the feature list, where one strong scroll interaction earns its place."
      ],
      "whenNotToUse": [
        "Use zoom-into-ui to fly into parts of a flat product screen and explain features in place.",
        "Use scroll-story when the story is a sequence of product states rather than the structure of one screen.",
        "Use a static screenshot or feature-illustration where scroll driven motion would compete with nearby sections."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The section is labelled by its heading, and in container mode the scrolling frame is a focusable region named after it.",
        "The 3D stage and its floating labels are decorative and hidden from assistive tech; an ordered list outside the stage names every layer with its caption, bottom to top.",
        "The caption for the current stop sits in a polite live region, so each layer's name and description is announced as it takes focus.",
        "Each stop is a real button with the stop's name as its label and aria-current=\"step\" on the current one.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. A focused or hovered stop button fills with the muted surface, and the current stop's tick turns the accent and thickens, so position never depends on color alone."
      ],
      "motion": [
        "One scroll timeline drives everything: the screen tilts back and turns, the layers spread apart in depth, the labels draw their leader lines and fade in bottom to top, then the stack closes and stands up again.",
        "Scroll progress is smoothed by a critically damped spring with no overshoot, so the stack follows the wheel softly, never passes its true position, and reverses mid way without a jump.",
        "The layer in focus keeps full strength while the others step back in opacity, and its outline and label take the accent; the caption crossfades with a short rise on the enter curve and leaves faster than it arrives.",
        "Labels live in flat stage space and are positioned each frame by projecting the layer's anchor point through the same rotation and perspective as the CSS, so text never tilts and the leader line always meets its layer.",
        "Per frame work writes transforms and opacity straight to the DOM from the motion value; React renders only when the stop changes.",
        "Reduced motion removes all travel: the screen shows flat at the start and end and fully separated for the five layer stops, switching instantly, and caption changes are instant."
      ],
      "responsive": [
        "From 880px wide the heading, caption and stops share a column on the left and the 3D stage fills the right.",
        "Below that the heading sits above the stage and the caption with its stops below it; below 560px the stops span the full width above the caption and the floating labels keep their names only.",
        "On a tall, narrow stage the stack turns less and spreads further, so it uses the height instead of shrinking; the scale is fitted from the stage size so the separated stack and its labels never overflow.",
        "Every stop button is 44px tall, and at phone widths the seven buttons share the full row."
      ],
      "performance": [
        "Only transform and opacity change per frame, on a small number of elements: one group rotation, five layer depths and five label positions.",
        "Fit is measured once and again only when the stage resizes, through a ResizeObserver; the projection per frame is a few lines of trigonometry per layer.",
        "The first render is the flat screen on the server and the client alike, so there is no hydration mismatch; reduced motion is read after hydration."
      ],
      "aiNotes": [
        "Choose it for a marketing or launch page that explains how a product is put together: design systems, UI kits, hardware with a screen, or an app whose craft is the selling point.",
        "The layer art is the built in Tally budgeting screen. Change the copy through layers, title and description; to show your own product, replace LayerArt in the source with your screen split into five layers at 280 by 620 design px.",
        "Use scroll=\"page\" on a real landing page so the section pins to the viewport; container mode is for previews, docs and embeds where page scroll is not available.",
        "Tally, the people and the wine bar are sample content. Portraits and photos are local assets from public/media."
      ],
      "related": [
        "scroll-story"
      ],
      "usage": "import { ExplodedView } from \"@/components/arc/blocks/exploded-view/exploded-view\";\n\nexport function HowItIsBuilt() {\n  return (\n    <ExplodedView\n      scroll=\"page\"\n      title=\"Built in layers\"\n      description=\"Take the home screen apart. Each layer has one job, and together they read as one calm surface.\"\n      onStopChange={stop => analytics.track(\"exploded_view_stop\", { stop })}\n    />\n  );\n}",
      "exports": [
        "ExplodedView"
      ]
    },
    {
      "name": "seat-map",
      "kind": "block",
      "title": "Seat map",
      "description": "An event seat picker with a curved concert hall map, price tiers, zoom and pan, sold, held and wheelchair seats, and a live summary with fees and a hold countdown.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Animated counter"
      ],
      "docs": "https://uiarc.dev/components/blocks/seat-map",
      "registry": null,
      "keywords": [
        "seat map",
        "seat picker",
        "seat selection",
        "venue map",
        "event tickets",
        "ticketing",
        "theatre seating",
        "concert tickets",
        "price tiers",
        "zoom and pan",
        "pinch zoom",
        "wheelchair seats",
        "accessible seating",
        "hold timer",
        "checkout"
      ],
      "whenToUse": [
        "Reserved seating for concerts, theatre, sports and conferences where the buyer picks exact seats.",
        "Any spatial picker with priced places: tables in a restaurant plan, desks in an office, cabins on a deck.",
        "Checkout flows that need a live total and a short hold before payment."
      ],
      "whenNotToUse": [
        "Use a quantity stepper with best available when seats are general admission or assigned by the box office.",
        "Use availability-picker for choosing times rather than places.",
        "Use choropleth-explorer to compare values across regions without selecting them."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Every seat is a checkbox with a full name, such as \"Row G, seat 7, Stalls centre. Stalls, $112. Aisle seat\", and sold or held seats are marked disabled.",
        "Seats are grouped by section, and the map carries instructions for its keys through aria-describedby.",
        "Keyboard position shows as a soft fill behind the seat and a tooltip with the seat details, never a focus ring.",
        "Selections, removals, the seat limit, holds and expiry are announced in a polite live region.",
        "Tier, status and access never rely on color alone: sold seats shrink, held seats are outlined with a dot, selected seats carry a check, and wheelchair spaces are square with a glyph. The key under the map names each mark.",
        "Zoom buttons, the tier filter and each summary row are real buttons with specific names; remove buttons name the seat they remove."
      ],
      "motion": [
        "The camera moves on one critically damped spring, so zooming, choosing a tier and following the keyboard glide without overshoot and can be interrupted by a drag at any point.",
        "Choosing a price tier dims the other tiers and flies the camera to where those seats sit; choosing it again returns to the whole hall.",
        "A picked seat fills and its check scales in; summary rows open and close their height on the smooth spring, and totals roll to their new values with tabular digits.",
        "Pan, pinch and ctrl or cmd scroll write straight to motion values and a single transform per frame, with no React state per frame.",
        "Reduced motion jumps the camera and swaps every transition for an instant change while keeping focus and announcements."
      ],
      "responsive": [
        "From 860px the map and the summary sit side by side with a 320px summary; below that the summary stacks under the map.",
        "Below 560px the map is 340px tall, the price filter scrolls sideways, and section labels hide until there is room for them.",
        "On touch, a tap on the whole hall zooms to that spot first, so seats are 44px apart before one can be chosen; one finger pans and two fingers pinch once zoomed, while the page still scrolls at the full view.",
        "Zoom buttons, price chips and remove buttons grow to 44px on coarse pointers."
      ],
      "performance": [
        "Seats are plain SVG groups in a memoized component, so a selection repaints the changed seats rather than the hall. The demo draws about 500 seats.",
        "Camera motion is one transform on a single group, batched to one write per frame with Motion's frame loop.",
        "Tooltip position is written directly to its style when the camera or seat changes, without re-rendering.",
        "For halls of many thousands of seats, split sections into separate layers or render seats to canvas and keep the same props."
      ],
      "aiNotes": [
        "Generate seat positions from your venue plan in any unit system; the camera fits whatever bounds the seats cover. Rotation turns each seat toward the stage.",
        "Map your inventory to status: available, held (in another buyer's cart) or sold. Only available seats can be chosen.",
        "Tier colors are plain CSS values. Keep them to the chart series or other semantic tokens so both themes stay legible.",
        "Wire onHold to your reservation API and return its promise. The countdown only reflects the local hold length; take the real expiry from your server if it differs.",
        "The first render uses the venue view box and no camera transform, then measures the viewport after mount, so the markup matches on server and client."
      ],
      "related": [
        "availability-picker",
        "choropleth-explorer"
      ],
      "usage": "import { SeatMap } from \"@/components/arc/blocks/seat-map/seat-map\";\n\nexport function ChooseSeats({ performance, hall, prices }) {\n  return (\n    <SeatMap\n      event={performance}\n      venue={hall}\n      tiers={prices}\n      maxSeats={8}\n      onHold={seats => fetch(\"/api/holds\", { method: \"POST\", body: JSON.stringify(seats.map(seat => seat.id)) }).then(response => {\n        if (!response.ok) throw new Error(\"Hold failed\");\n      })}\n    />\n  );\n}",
      "exports": [
        "SeatMap"
      ]
    },
    {
      "name": "ai-answer",
      "kind": "block",
      "title": "AI answer",
      "description": "One AI answer set like a clean article: it thinks, shows its sources as small cards with colored marks, and streams a formatted answer where every citation lights the sentence it supports. Copy, regenerate with options, rate, share, page through versions and ask a related question in place.",
      "tier": "pro",
      "builtFrom": [
        "Text shimmer",
        "Hover card",
        "Copy button",
        "Tooltip",
        "Popover"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-answer",
      "registry": null,
      "keywords": [
        "ai answer",
        "answer engine",
        "cited answer",
        "citations",
        "sources",
        "streaming",
        "reasoning",
        "thinking",
        "regenerate",
        "feedback",
        "follow-up questions",
        "perplexity style",
        "llm",
        "markdown",
        "answer page"
      ],
      "whenToUse": [
        "An answer page where one question gets one well formatted, cited answer that people read, copy and share.",
        "Docs and help center assistants that must show where each claim comes from.",
        "Internal knowledge search where the answer mixes prose, steps, a comparison table and code."
      ],
      "whenNotToUse": [
        "Use ai-chat for a back and forth conversation with many turns kept on screen.",
        "Use research-run for long multi step research that plans searches and reads many sources before writing.",
        "Use ai-side-panel when the assistant sits next to a document or app the person is working in.",
        "Use ai-inline-edit to rewrite a selection in place rather than answer a question."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-popover",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The block is a labelled region named by the question, with aria-busy while an answer streams. A polite status region announces Thinking, Writing the answer, Answer ready, Answer stopped or the error.",
        "Action results (copied, link copied, feedback sent, version shown) go to a second polite live region; the share links say when the preview copies a sample link.",
        "The reasoning and sources rows are buttons with aria-expanded and aria-controls; closed panels are inert, so their content leaves the tab order.",
        "Citations are links named \"Source n: title, site\"; the source card opens on keyboard focus as well as hover, and a lit sentence is a visual aid only, never the only cue.",
        "The comparison table keeps header cells with scope, and the table and code scroll inside focusable, labelled regions on narrow screens.",
        "Status is never color alone: copied shows a drawn check, errors an icon and text, feedback a filled glyph with aria-pressed, and stopped or failed answers a note with a Try again button.",
        "Asking a related question moves focus to the new question heading; New thread moves focus to the composer.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "\"Thinking\" uses the Arc text shimmer for about a second and settles to solid text as \"Thought for 9s\" when the first words arrive.",
        "The answer streams in chunks on a requestAnimationFrame clock: about every 34ms, three to eight words commit at once and fade in over 160ms on a plain ease-out, with no blur, travel or stagger. A steady pace of about a hundred words a second plus catch-up drains bursts, so a full answer lands in two to four seconds. Settled text never re-renders or reflows.",
        "Headings, list items, table rows and code lines appear as their text arrives; a line or row waits for its newline, so containers never pop in half built.",
        "The reasoning and sources panels open on the smooth spring; source cards fade in 20ms apart, at most six steps. Hovering the sources row fans the mark chips apart on the CSS spring.",
        "Regenerating or switching versions crossfades the answer while its height springs to the new length, and the version number rolls up or down.",
        "Copy and Share morph their icon (a drawn check) and label in place without changing width; Send turns into Stop in the same spot.",
        "Reduced motion keeps the chunked arrival but drops every fade, roll, fan and spring, and keeps all states and announcements."
      ],
      "responsive": [
        "The reading column caps at about 704px with even gutters that shrink to 16px on phones; the question scales between 22px and 28px with the container.",
        "Source cards form a two column grid in a wide column and a snap scrolling row on phones, faded at the trailing edge with a mask; a citation that lights an off screen card scrolls it into view.",
        "The table and code scroll horizontally inside their frames; the action bar wraps its model line under the actions.",
        "The top bar truncates the title and keeps Share; the composer stays docked at the bottom and grows to six lines."
      ],
      "performance": [
        "Streamed markdown is parsed into numbered units that stay stable as text arrives, so each commit only appends new spans and earlier text never re-renders.",
        "One requestAnimationFrame loop per answer paces the reveal and commits at most every 34ms; it stops as soon as the answer is on screen.",
        "Finished versions render whole without the reveal, and the answer body is memoized against hover changes outside its sentences."
      ],
      "aiNotes": [
        "Choose it for a single answer page with sources: help centers, internal search, docs assistants, answer engines. It shows one question at a time; asking another replaces it.",
        "Stream markdown in `text` events: paragraphs, ### headings, 1. and - lists, pipe tables and fenced code (```js filename.js). Inline **bold**, `code` and [n] citations work everywhere; [n] points at sources[n - 1].",
        "Send `sources` early, during reasoning, so the stack fills while the model thinks. Send `thought` with the thinking duration when the first text is ready; without it the time to first text is used.",
        "Brand marks are sample content. Pass your own `icon` per source; keep color brands in their colors and mono brands on currentColor.",
        "Regenerate keeps every version in memory for the session and calls `respond` with mode and the previous answer as markdown, so your prompt can ask for a shorter, longer or different answer.",
        "The demo compresses time: thinking plays in about a second and the reported thinking time and duration are what the simulated model would have taken."
      ],
      "related": [
        "ai-chat",
        "research-run",
        "ai-composer",
        "ai-side-panel",
        "model-picker",
        "text-shimmer"
      ],
      "usage": "import { AiAnswer, type AnswerEvent } from \"@/components/arc/blocks/ai-answer/ai-answer\";\n\n// Wire a real model: forward your server's stream as AnswerEvents.\nasync function* respond({ question, mode, model, signal }) {\n  const response = await fetch(\"/api/answer\", { method: \"POST\", body: JSON.stringify({ question, mode, model: model.id }), signal });\n  if (!response.ok) throw new Error(\"The model is busy. Try again in a moment.\");\n  for await (const event of readNdjson<AnswerEvent>(response.body!)) yield event;\n}\n\nexport function AnswerPage() {\n  return (\n    <AiAnswer\n      question=\"How do we roll out passkeys without locking anyone out?\"\n      respond={respond}\n      onFeedback={feedback => track(\"answer_feedback\", feedback)}\n      onFollowUp={question => router.push(`/ask?q=${encodeURIComponent(question)}`)}\n      onShare={({ version }) => `https://acme.dev/answers/42${version ? `?v=${version + 1}` : \"\"}`}\n    />\n  );\n}\n\n// Or show a stored answer without streaming.\n<AiAnswer\n  question=\"What should we measure before each rollout stage?\"\n  answer={[\n    { type: \"paragraph\", text: \"Measure whether people get in, not just whether they enroll. [1]\" },\n    { type: \"list\", ordered: true, items: [\"Enrollment\", \"Passkey success\", \"Fallbacks\"] },\n  ]}\n  sources={[{ id: \"webdev\", title: \"Passkey form autofill\", site: \"web.dev\", domain: \"web.dev\", url: \"https://web.dev/articles/passkey-form-autofill\" }]}\n  usage={{ tokens: 1240, thoughtSeconds: 7, seconds: 18.4 }}\n/>",
      "exports": [
        "AiAnswer"
      ]
    },
    {
      "name": "model-picker",
      "kind": "block",
      "title": "Model picker",
      "description": "A model picker in one quiet pill: open it to choose from featured models with a one line note each, set the reasoning effort, or browse every other model by provider, and the pill swaps to your choice in place.",
      "tier": "pro",
      "docs": "https://uiarc.dev/components/blocks/model-picker",
      "registry": null,
      "keywords": [
        "model picker",
        "model selector",
        "llm model switcher",
        "ai model dropdown",
        "reasoning effort",
        "claude",
        "gpt",
        "gemini",
        "choose model",
        "ai settings"
      ],
      "whenToUse": [
        "Choosing the model and effort for a chat, an agent or a playground.",
        "A default model setting with a short review of what the chosen model costs and how much context it takes."
      ],
      "whenNotToUse": [
        "Comparing two models' answers side by side; use model-arena.",
        "A full prompt box with attachments and tools; use ai-composer, which has its own model chip.",
        "Choosing from a plain list of options with no descriptions; use the select or dropdown-menu component."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu"
      ],
      "accessibility": [
        "Built on the Radix dropdown menu: the featured models, the effort levels and the other models are radio groups, so a screen reader announces each as checked or not.",
        "The pill's name includes the current model and effort, for example \"Model: Claude Opus 5.5, medium effort. Change model\".",
        "Each change is announced through a polite status region, and the review under the pill is a labelled section.",
        "Brand marks are decorative next to the model name and hidden from assistive technology.",
        "Focus returns to the pill when the menu closes. A pointer choice does not draw the keyboard focus ring; keyboard use keeps Arc's shared ring."
      ],
      "motion": [
        "The menu opens from the pill edge with a short offset, a slight scale and a quick fade, and reverses from where it is if closed mid-open.",
        "One highlight glides between rows on the snappy spring for the pointer and jumps for the keyboard, so moving through the list never flickers.",
        "On a change, the pill's mark and name crossfade and the pill width springs to the new name, so it never snaps.",
        "The review's values crossfade in place with no character morphing; every slot keeps its height.",
        "On phones, moving between menu pages slides the content a few pixels and the menu height springs to the new page.",
        "Reduced motion swaps everything in place and keeps every state and announcement."
      ],
      "responsive": [
        "The pill truncates long names inside narrow toolbars and keeps its chevron visible.",
        "Below 640px the Effort and More models submenus become pages inside the same menu with a Back row, so nothing opens off screen.",
        "Long lists scroll inside the menu, and the review's four values fold into a two by two grid under 400px."
      ],
      "performance": [
        "The menu mounts only while open. The highlight is one element per menu surface, moved with transforms.",
        "The pill width is measured with a ResizeObserver and animated once per change."
      ],
      "aiNotes": [
        "Use it wherever a person chooses which model answers: a composer toolbar, a settings row or an API playground. Keep the featured list short (three to five) and put the rest under More models.",
        "Pass blurb as a few words on what the model is for; it is the only line under the name in the menu. Put the longer sentence in description for the review.",
        "Only models with an effort field show the Effort row. Effort levels are per model, and a level the new model does not accept snaps to the nearest one it does.",
        "Show real prices only for models you have verified. The sample marks non-Anthropic prices as sample values."
      ],
      "related": [
        "ai-composer",
        "model-arena",
        "ai-chat",
        "context-window",
        "dropdown-menu"
      ],
      "usage": "import { ModelPicker } from \"@/components/arc/blocks/model-picker/model-picker\";\n\nexport function ComposerModel({ model, setModel, effort, setEffort }) {\n  return (\n    <ModelPicker\n      value={model}\n      onValueChange={id => setModel(id)}\n      effort={effort}\n      onEffortChange={setEffort}\n      featured={[\"claude-opus-5-5\", \"claude-sonnet-5-5\", \"claude-haiku-5-5\"]}\n      showSummary={false}\n    />\n  );\n}",
      "exports": [
        "ModelPicker"
      ]
    },
    {
      "name": "model-arena",
      "kind": "block",
      "title": "Model arena",
      "description": "A blind side by side model comparison in one card: send one prompt to two models, watch both answers stream at the same pace, vote for the better one, then reveal which model wrote each.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Button",
        "Popover",
        "Switch",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/model-arena",
      "registry": null,
      "keywords": [
        "model arena",
        "llm comparison",
        "blind test",
        "side by side",
        "model battle",
        "ab test",
        "human preference",
        "vote",
        "leaderboard",
        "win rate",
        "time to first token",
        "tokens per second",
        "latency",
        "cost",
        "streaming",
        "claude",
        "gpt",
        "gemini"
      ],
      "whenToUse": [
        "Blind human preference tests between two models on the same prompt, such as an internal bake-off before switching providers.",
        "Prompt or model evaluation tools where people should judge answers without knowing which model wrote them.",
        "Showing latency, speed and cost differences between two models live, side by side, on a real prompt."
      ],
      "whenNotToUse": [
        "Use model-picker to let someone choose one model for their own chat; this block compares answers, it does not set a default.",
        "Use eval-suite to score many prompts automatically against expected outputs rather than collecting one human vote at a time.",
        "Use model-router to explain or configure automatic routing between models.",
        "Use ai-chat for a single ongoing conversation with one model."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "Each pane is a labelled region (\"Model A\", or \"Model A, Claude Opus 5.5\" once revealed). Its answer is a focusable scroll region marked aria-busy while it streams.",
        "In blind mode no model name, mark or title reaches the page or the accessibility tree until the vote.",
        "Every status pairs an icon with a word: Thinking, Writing, Done, Finished first, Stopped, Failed. Color only reinforces it.",
        "A polite status region narrates the round: that a run started (and that the preview is simulated), which model finished first, that voting is open, the vote result with both real models, and a leaderboard reset.",
        "On narrow containers the panes use tab semantics: a tablist with aria-selected tabs, each controlling a tabpanel; the hidden panel leaves the tab order.",
        "The vote buttons form a labelled group. Leaderboard rows read their rank and win rate as text, the bars are hidden from assistive technology, and the Leaderboard button's name says when a vote has updated it.",
        "Run carries aria-keyshortcuts for its shortcut. Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, inset inside scroll regions."
      ],
      "motion": [
        "Answers stream in chunks of whole words every 30 to 40ms, each fading in over 160ms with no blur or travel, at the same pace in both panes; settled text never re-renders, and each answer follows its stream until the reader scrolls up.",
        "The reveal is one minimal crossfade: \"Model A\" fades out in 100ms while the brand mark and name fade in over 160ms from 2px below, and the label's measured width follows on a spring with no bounce. Status words and tab labels swap the same way; no character morph, blur or per letter motion anywhere.",
        "Working statuses shimmer with Text shimmer, and each status word mounts with its own shimmer so a change is a crossfade, never a morph inside the label.",
        "First token time counts live while the model thinks; speed and cost are sampled twice a second while it writes and roll on Animated counter digits, so each roll settles before the next.",
        "The vote row crossfades between waiting, voting and result while its height follows on the smooth spring. Run and Stop share one cell, so the button never changes width.",
        "Opening the leaderboard after a vote replays it: rows start at their earlier ranks and glide to the new ones on a critically damped spring while win rates roll and bars scale in each model's series color; ranks stay put while rows move. After the first vote, the bars grow from empty.",
        "Narrow tabs slide their highlight on a 220ms tween with no bounce, and the chosen pane fades in.",
        "Reduced motion keeps every state, label and announcement but shows chunks without fading, swaps labels instantly, reorders rows in place and stops the spinner and shimmer."
      ],
      "responsive": [
        "The card is up to 800px wide. From 560px the panes sit side by side; below that they become two tabs, each showing the model's name and status, so both statuses stay visible while you read one answer.",
        "Each answer scrolls inside a fixed height, so the card never grows while text streams. Code blocks scroll sideways inside the pane.",
        "Below 480px the Leaderboard button collapses to its icon with an accessible name, the shortcut hint hides, sample chips scroll sideways in one line, and the vote buttons form a two by two grid."
      ],
      "performance": [
        "Streamed chunks are memoized spans that never re-render once shown, so long answers stay cheap.",
        "The first token clock ticks on animation frames only while a model thinks, and speed and cost are sampled twice a second only while it writes.",
        "Every stream has its own AbortController: Stop, a new prompt, Next round and Retry abort the work they replace, and the arena aborts everything on unmount.",
        "The leaderboard rows mount only while the popover is open. The simulation runs on requestAnimationFrame and pauses with the tab."
      ],
      "aiNotes": [
        "Pass run to stream real models; it receives the ArenaModel, the prompt text and an AbortSignal. Timing (first token, speed) is measured from your stream on the client, so yield as soon as text arrives rather than buffering.",
        "Report the provider's usage by yielding { inputTokens, outputTokens } (with or without text) for exact cost; otherwise tokens are estimated from characters and cost uses the model's price.",
        "Throw from run to fail one pane; the other keeps streaming. ArenaRunError(message, { retryable: false }) hides the retry button.",
        "Persist votes in onVote. To show standings from earlier sessions, pass initialStandings keyed by model id; a tie counts as half a win and Both bad counts as a loss for each model.",
        "Give each model a color from --series-1 to --series-4 so providers read as families on the leaderboard; the default pool colors by provider.",
        "In blind mode cost and speed can hint at a model's identity; leave them for honest measurement, or pass the same price for every contestant if identity must stay fully hidden.",
        "Model names, marks and non-Anthropic prices in the default pool are sample content, not endorsements or published benchmarks."
      ],
      "related": [
        "model-picker",
        "eval-suite",
        "prompt-studio",
        "ai-chat"
      ],
      "usage": "import { ModelArena, ArenaRunError } from \"@/components/arc/blocks/model-arena/model-arena\";\n\n// Stream each model through your own endpoint. Yield text as it arrives.\nasync function* run(model, prompt, signal) {\n  const response = await fetch(\"/api/arena\", { method: \"POST\", body: JSON.stringify({ model: model.id, prompt }), signal });\n  if (!response.ok) throw new ArenaRunError(response.status === 429 ? \"Rate limited. Try again in a moment.\" : \"The model did not respond.\");\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder();\n  for (;;) {\n    const { done, value } = await reader.read();\n    if (done) return;\n    yield decoder.decode(value, { stream: true });\n  }\n}\n\nexport function Arena() {\n  return <ModelArena run={run} onVote={vote => saveVote(vote)} />;\n}",
      "exports": [
        "ModelArena"
      ]
    },
    {
      "name": "prompt-studio",
      "kind": "block",
      "title": "Prompt studio",
      "description": "One card for editing, testing and shipping a prompt template: variables as chips with sample values, a test run against the chosen model, and versions you can compare and promote to production.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Copy button",
        "Kbd",
        "Popover"
      ],
      "docs": "https://uiarc.dev/components/blocks/prompt-studio",
      "registry": null,
      "keywords": [
        "prompt studio",
        "prompt editor",
        "prompt template",
        "prompt management",
        "prompt versioning",
        "llm playground",
        "template variables",
        "test cases",
        "production prompt",
        "promote to production",
        "prompt diff",
        "system prompt",
        "model effort",
        "streaming reply"
      ],
      "whenToUse": [
        "A settings or admin page where a support lead or engineer tunes the prompt behind one AI feature and tests it on real cases.",
        "Prompt management that needs a compact version history with authors, a diff against an earlier version and a visible production pointer.",
        "Any LLM feature with template variables filled from records, such as a support reply, a summary or a classifier prompt."
      ],
      "whenNotToUse": [
        "Use ai-chat for an end user conversation; this card edits templates, not messages.",
        "Use model-arena to compare the replies of several models side by side on one prompt.",
        "Use eval-suite to score a prompt across many cases with graders and pass rates.",
        "Use model-picker when the only job is choosing a model and effort."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The editor is a role=\"textbox\" with aria-multiline and an accessible name per tab; while the {{ list is open it points at the list with aria-controls and at the highlighted option with aria-activedescendant.",
        "System and User are real tabs with tabpanels. The hidden panel is inert, so it leaves the tab order and the reading order.",
        "Every variable field has a visible label in its variable's color, paired with a colored dot. Clicking a chip focuses its field.",
        "Status is never color alone: the version chip says Production or Latest, the run status pairs an icon with Reply, Stopped or Run failed, and an empty variable's chips switch to a dashed outline.",
        "The diff uses ins and del elements with visually hidden Added and Removed prefixes, plus a visible + or minus sign before each change and a strikethrough on removals.",
        "Version rows and model rows are radios in a labelled group. The Production marker is text, and the menu's status line says which version production serves.",
        "Runs, results, errors, promotions, undos and compare mode are announced through one polite status region, including when a run is simulated.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, inset on menu rows and segmented options."
      ],
      "motion": [
        "Promoting a version glides the Production marker from its old row to the new one on the critically damped smooth spring, with no overshoot. Undo glides it back.",
        "The reply streams in chunks of three to six whole words every 40ms on a requestAnimationFrame clock, about 100 words a second, and a backlog widens the chunks. Each chunk only fades in over 150ms; settled text never moves. Thinking lasts 0.5 to 1.2 seconds by effort.",
        "The output panel follows its content's height on the smooth spring. A rerun holds the panel at its current height until the new reply is in, then settles, so nothing collapses and regrows.",
        "Labels that change (version state, model name, effort, run status, the footer) use one crossfade: the old label fades out in 100ms while the new one fades in from 2px over 160ms, and the chip width follows on a fast spring without bounce. No blur, no per-letter motion.",
        "Tab and test case highlights slide on a critically damped spring. Token counts roll their digits with tabular numerals in a fixed slot. Editing a value flashes its chips once.",
        "The working label uses a calm shimmer that stops on Reply, Stopped or Run failed.",
        "Reduced motion removes the glides, the chunk fade, the shimmer and every height animation while keeping each state, label and announcement."
      ],
      "responsive": [
        "The card is up to 760px wide and reads well from 320px. It uses container queries, so it adapts to its slot, not the viewport.",
        "Below 560px of container width the variable fields fall into two columns, and below 340px into one.",
        "Below 480px the test cases and Run each take a full row, and the model chip wraps under the title.",
        "System and User share one editor slot sized to the taller text, so switching tabs never changes the card's height."
      ],
      "performance": [
        "The editor rebuilds a few dozen DOM nodes per keystroke from the string, well under a millisecond for prompt sized text.",
        "The word diff trims the shared start and end before its longest common subsequence pass, so typical edits compare a few dozen tokens.",
        "Streaming appends one chunk per tick and never re-renders earlier chunks; the clock stops when the reply is complete, stopped or failed.",
        "Only one run is active at a time; starting another cancels the first and aborts its signal."
      ],
      "aiNotes": [
        "Choose it when a team edits a prompt template, tries it on saved cases and promotes a version to production, inside a product page or settings screen.",
        "Write placeholders as {{name}} in the system and user text. Pass `variables` to fix the color order; values in test cases are matched by name.",
        "Wire onRun to stream real replies. Return an async iterable of text deltas, or a promise of the full text, and respect the AbortSignal: Stop aborts it. The footer estimates tokens and cost from the model prices you pass.",
        "Keep onPromote as the source of truth: the card updates its own pointer optimistically and calls back, and Undo calls it again with the ids swapped.",
        "Model prices outside Anthropic in the sample list are illustrative. Pass your own models with real prices for cost estimates.",
        "Without props it renders the full demo: a Fernwood support reply prompt with four versions, v4 in production and three saved cases. Without onRun, the first run of the Past window case returns a simulated overload error so the retry path can be seen."
      ],
      "related": [
        "model-picker",
        "model-arena",
        "eval-suite",
        "ai-inline-edit",
        "ai-chat"
      ],
      "usage": "import { PromptStudio } from \"@/components/arc/blocks/prompt-studio/prompt-studio\";\n\nexport function SupportPrompt({ prompt, cases }) {\n  return (\n    <PromptStudio\n      name={prompt.name}\n      versions={prompt.versions}\n      productionId={prompt.productionId}\n      variables={[\"customer_name\", \"order_id\", \"policy\"]}\n      testCases={cases}\n      onRun={async function* (request, signal) {\n        const response = await fetch(\"/api/prompt-run\", { method: \"POST\", body: JSON.stringify(request), signal });\n        const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();\n        for (;;) {\n          const { value, done } = await reader.read();\n          if (done) return;\n          yield value;\n        }\n      }}\n      onPromote={versionId => setProduction(prompt.id, versionId)}\n    />\n  );\n}\n\n// A version is plain text with {{variables}}.\nconst version = {\n  id: \"v4\", label: \"v4\", createdAt: \"2026-10-16T15:20:00Z\", message: \"Escalate when the policy falls short\",\n  author: { name: \"Hannah Walsh\", avatar: \"/media/people/hannah-walsh.svg\" },\n  system: \"You are a support agent for Fernwood. Follow this policy exactly: {{policy}}\",\n  user: \"Reply to the latest message from {{customer_name}} about order {{order_id}}.\",\n};",
      "exports": [
        "PromptStudio",
        "PromptEditor"
      ]
    },
    {
      "name": "document-extract",
      "kind": "block",
      "title": "Document extraction",
      "description": "Turns a document into typed data with a model and shows where every value came from: fields fill in as the page is read, and hovering a value lights its source on the PDF.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Copy button",
        "Tabs",
        "Text shimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/document-extract",
      "registry": null,
      "keywords": [
        "document extraction",
        "structured output",
        "invoice parsing",
        "pdf viewer",
        "data extraction",
        "ocr",
        "llm extraction",
        "json schema",
        "typed output",
        "confidence",
        "human review",
        "source highlighting",
        "citations",
        "form filling",
        "claude haiku"
      ],
      "whenToUse": [
        "Accounts payable, logistics or back office tools that turn invoices, receipts, emails or forms into records a person accepts before saving.",
        "Any extraction flow where a reviewer needs to see exactly where each value came from and fix the few the model was unsure about.",
        "Demos and settings pages for a structured output feature, with the schema and live typed JSON one tab away."
      ],
      "whenNotToUse": [
        "Use ai-answer for a written answer with citations rather than typed fields.",
        "Use knowledge-base to search and cite across many documents instead of reading one.",
        "Use ai-inline-edit to rewrite text in place rather than pull values out of it.",
        "Use agent-approvals when the decision is approving an agent's actions, not reviewing extracted values."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tabs",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The run's state is announced through a polite status region: extracting, done with how many fields need a check, failed with the error, and stopped. Checking a value and accepting speak through a second polite region, including that the preview only simulates the hand-off.",
        "Confidence is a percent with an icon for every state that matters: a warning for low confidence, a check once checked or accepted, a pencil once edited. Spoken text reads the state and percent, and a flagged value's reason is tied to its input with aria-describedby.",
        "Every value is a real input with a label. Table cells are inputs inside a table role with column headers, and each cell's name includes its column, row and whether it needs a check.",
        "The viewer is a labeled region that announces its zoom; icon buttons have names and tooltips, and Fit reports its pressed state. The document switcher is a menu of radio items.",
        "The connector, the scan bar and the confidence bars are decorative and hidden from assistive technology; the linked field, its status text and the summary carry the same information.",
        "Values that do not fit their type, such as a date that is not YYYY-MM-DD or an IBAN whose check digits fail, show a message tied to the field, become null in the JSON, and block Accept all with a spoken reason.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring. Inputs show focus with their border."
      ],
      "motion": [
        "The connector is one cubic Bezier that draws in over 220ms with round caps in the field's series color; a soft band of light travels along it once, then the line rests. Round anchors sit at both ends and the far one settles in as the line reaches it. It is measured on requestAnimationFrame while shown, so it follows scrolling, zoom and resizing without lag, and fades out in 120ms.",
        "While extracting, one thin bar of light with a soft trailing fade passes down the page once. Each cited span takes a brief stronger tint as its value lands, then rests at a faint tint; the field's text fades in.",
        "Zoom eases on the smooth spring around the middle of the canvas, so the point you are reading stays put.",
        "Accept all turns each field's status into a check with a short stagger and a crisp snappy pop, and the footer bars turn green in the same order.",
        "Table rows grow in on the smooth spring; reasons for flagged values open and close their height on the same spring. Status lines crossfade (out in 100ms, in over 160ms) inside a container that animates its height. Average confidence and cost roll on a short tween with tabular numerals.",
        "Reduced motion removes the scan bar, the draw, the sheen, the cascade, flashes and tweens. Selection, tints, the connector, labels and announcements stay, and zoom changes in one step."
      ],
      "responsive": [
        "The block caps at 1080px. From 760px of its own width the viewer and the form sit side by side, 600px tall, with the connector between them; the form keeps up to 436px.",
        "Below 760px the viewer stacks above the form with its own 330px canvas and the form scrolls in 430px below it. The connector hides there, since it would cross the form; the matching tints, the auto scroll to each source and a press on a span that brings its field into view carry the link.",
        "Fit keeps the A4 sheet at the canvas width, so on a phone the whole page shows and zoom brings the text up. Below 520px the page count and model name drop, field labels narrow, the line items table scrolls sideways inside its own box and Accept all takes the full width.",
        "The page, the form and the code tabs scroll inside their own panes, so the block never scrolls the page sideways."
      ],
      "performance": [
        "Values arrive through one reducer; the form, links, output and JSON lines are derived with memoized maps keyed on the values object.",
        "Span hover is one delegated pointer listener on the page and one on each JSON block. Span flashes write a data attribute directly instead of re-rendering the document.",
        "Zoom writes the sheet's scale and size straight to the DOM on each animation frame, so the page never re-renders while it zooms.",
        "The connector runs one requestAnimationFrame loop only while a link shows, measures two rectangles per frame, and writes path attributes only when they change. The sheen moves three gradient stops; the scan bar is a single transform animation.",
        "Panes scroll themselves with scrollTo on their own element, never scrollIntoView, so the host page never jumps; the one exception is a press on a span in the stacked layout, which may bring its field into the window."
      ],
      "aiNotes": [
        "Mark every value the model may cite as a span in the document data, and ask the model to return the span id as source. Without a source a value still fills its field but cannot be linked.",
        "Ask for a confidence between 0 and 1 and a short note for anything inferred rather than read, such as a due date worked out from payment terms. Notes appear under flagged values.",
        "Stream values in schema order so the form fills top to bottom. Emit usage events as the stream goes so cost rolls instead of jumping at the end.",
        "Keep display values as the document writes them (1,400.00, CH56 0483 ...); the block parses them into typed output with parseValue rules.",
        "Use toJsonSchema(schema) as the model's structured output format so the Schema tab shows exactly what was sent.",
        "Give the schema up to four groups so each gets its own series color; more groups repeat the colors.",
        "The preview is fully simulated: scripted values, usage and timing for the two samples, an overloaded_error on the first run of the shipping email, and a simulated hand-off on Accept all."
      ],
      "related": [
        "knowledge-base",
        "ai-answer",
        "agent-approvals",
        "model-picker",
        "text-shimmer"
      ],
      "usage": "import { DocumentExtract, documentToText, invoiceSchema, toJsonSchema } from \"@/components/arc/blocks/document-extract/document-extract\";\n\nexport function InvoiceInbox({ document }) {\n  return (\n    <DocumentExtract\n      document={document}\n      schema={invoiceSchema}\n      extract={async function* (doc, schema, { signal }) {\n        // Your route sends documentToText(doc) and toJsonSchema(schema) to the model as structured output,\n        // and streams back one line of JSON per value: { path, value, source, confidence, note }.\n        const response = await fetch(\"/api/extract\", {\n          method: \"POST\",\n          body: JSON.stringify({ text: documentToText(doc), schema: toJsonSchema(schema) }),\n          signal,\n        });\n        const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();\n        let buffer = \"\";\n        for (;;) {\n          const { value, done } = await reader.read();\n          if (done) break;\n          buffer += value;\n          const lines = buffer.split(\"\\n\");\n          buffer = lines.pop() ?? \"\";\n          for (const line of lines) if (line.trim()) yield { type: \"value\", ...JSON.parse(line) };\n        }\n      }}\n      onAccept={data => saveInvoice(data)}\n    />\n  );\n}\n\n// Mark the spans a value can cite, so the form can point back at them.\nconst document = {\n  id: \"inv-0418\",\n  name: \"larkspur-invoice-0418.pdf\",\n  label: \"Invoice\",\n  kind: \"pdf\",\n  blocks: [\n    { type: \"masthead\", logo: { name: \"larkspur\", detail: \"studio\", color: \"#3d4fc4\", symbol: \"petals\" }, title: \"Invoice\" },\n    { type: \"meta\", items: [[\"Invoice no.\", [{ span: \"number\", text: \"INV-2026-0418\" }]], [\"Issue date\", [{ span: \"issue\", text: \"14 October 2026\" }]]] },\n  ],\n};",
      "exports": [
        "DocumentExtract"
      ]
    },
    {
      "name": "agent-memory",
      "kind": "block",
      "title": "Agent memory",
      "description": "One card for what an assistant remembers about you: filter and search memories, edit or forget one in place, and turn saved memories and chat history on or off.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Button",
        "Switch",
        "Text shimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-memory",
      "registry": null,
      "keywords": [
        "memory",
        "assistant memory",
        "agent memory",
        "saved memories",
        "chat history",
        "personalization",
        "memory updated",
        "remember",
        "forget",
        "undo",
        "filter",
        "settings",
        "preferences",
        "llm",
        "ai",
        "privacy",
        "user profile"
      ],
      "whenToUse": [
        "Settings for an assistant that remembers people across chats, where people need to see, filter, search, correct and delete what it knows.",
        "Showing the moment an assistant saves something from a chat, with a way to see where it was filed.",
        "Any small personal knowledge store grouped by kind, where each entry is a sentence with a source and an undoable delete."
      ],
      "whenNotToUse": [
        "Use knowledge-base for documents and files an agent retrieves from, rather than short facts about the user.",
        "Use context-window to show what fills a single conversation's token budget, rather than what persists across chats.",
        "Use ai-chat for the conversation itself; this card only receives what the chat noted.",
        "Use agent-routines for scheduled instructions the agent runs, not facts it recalls."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "The card has a level two heading; in the All view and in search results each group has a level three heading with its count, and every group is a labelled group of list items.",
        "Switches are labelled by their visible titles and described by the line beneath; the meter is a meter role with a text value such as \"62% full\". Filter pills are toggle buttons with aria-pressed.",
        "Status is never color alone: the pill reads Memory updated, then Saved to Preferences with a check; the meter's warning pairs color with an icon; the add field shows Added to People with a check or an error with an icon; a paused list carries a note with an icon.",
        "Icon buttons name the memory they act on, such as \"Forget memory: Prefers metric units and 24 hour times\", and show a tooltip. Row actions stay visible on touch screens.",
        "Focus follows each change: into the edit field, back to Edit after saving or cancelling, onto Undo after Forget, back to Forget after Undo, into the search field when it opens and back to the search button when it closes, and to the selected filter when an Undo button times out.",
        "A polite status region announces captures, adds (including that the preview sorts with a keyword guess), saves, forgets with their undo window, restores, switch changes and the number of search matches.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, and text fields show focus with their border."
      ],
      "motion": [
        "Capture and add: a copy of the new row lifts off the pill or the field and glides to the row's place in its group on the smooth spring (0.4s, no bounce), moving only by transform, while the row's slot opens beneath it. The row's own text takes over as it lands, under a soft tint that fades over about a second. If the memory's group is filtered out or a search is open, the filter switches or the search clears first, then the copy flies.",
        "Filtering opens and closes groups and rows by height on a critically damped spring (0.32s, no bounce) with a quick fade, so the list closes its gap instead of jumping. The active pill's background glides to the new pill on a 0.26s spring without bounce.",
        "The search button widens into a field over the pills with a 240ms ease-out width transition while the pills fade out.",
        "A row swaps between reading, editing and the undo bar with one crossfade (100ms out, 160ms in) while its height follows on the smooth spring. A saved edit crossfades the old sentence into the new one. When the undo countdown ends, the bar folds away.",
        "Labels that change (a switch's line, the pill, the add status) crossfade in place with at most 2px of rise; the pill keeps its width with an invisible sizer, so nothing shifts. No per-letter, blurred or morphing text.",
        "The meter's percentage counts in tabular numerals inside a reserved width and the bar scales on the smooth spring. Sorting a typed memory shows a short shimmer on its status. The undo ring drains linearly.",
        "Reduced motion skips the flight, places heights and rows immediately, stops the shimmer and the ring, and keeps every state, focus move and announcement."
      ],
      "responsive": [
        "The card is at most 640px wide and centered; the list scrolls inside a 380px area that fades at an edge only while there is more to see.",
        "Below 440px of container width the inset tightens, the subtitle wraps under the title next to the meter, and the filter pills scroll sideways with an edge fade, keeping the selected pill in view.",
        "Row actions and sources show on hover and focus with a mouse and stay visible on touch screens; their space is always reserved, so text never shifts.",
        "The Memory updated pill truncates its note with an ellipsis on narrow cards; the full text is in its accessible name."
      ],
      "performance": [
        "Heights animate through motion values and one ResizeObserver per row; the list's edge fade and the filter fades are toggled as attributes, without React renders per frame.",
        "The flight moves one absolutely positioned copy by transform; scrolling the list to the landing row runs on the same spring.",
        "Undo windows share one 200ms interval that runs only while something can be restored.",
        "Search is a plain substring filter; matches are marked by splitting the text, without regular expressions built from input."
      ],
      "aiNotes": [
        "Use it as the memory page of an assistant's settings or in a side panel next to a chat. Pass captured={null} when capture happens in your real chat, or pass what the model just noted to offer it as a pill.",
        "Write memories as one plain third person sentence without a closing period, such as \"Prefers metric units and 24 hour times\". Keep them under 200 characters; the editor enforces it.",
        "Set source.kind to \"chat\" (reads From a chat) or \"manual\" (Added by you) with an ISO date; add href to link the source to the chat.",
        "For memories about a person, set group \"people\" and person { name, src } to show their portrait. Typed memories that name someone in lib/media get that portrait automatically.",
        "onChange receives the whole list, so you can store it as is, or act on change.type. Reject from add or edit to surface server errors in place. Forget is final only after undoSeconds, which is when onChange reports it.",
        "The sample shows Claude as the assistant; set assistantName for your own. Do not invent capture claims for real products."
      ],
      "related": [
        "ai-chat",
        "ai-side-panel",
        "knowledge-base",
        "context-window",
        "agent-tasks",
        "agent-routines",
        "mcp-servers"
      ],
      "usage": "import { AgentMemory, type MemoryItem } from \"@/components/arc/blocks/agent-memory/agent-memory\";\n\nexport function MemorySettings({ memories, userId }: { memories: MemoryItem[]; userId: string }) {\n  return (\n    <AgentMemory\n      memories={memories}\n      captured={null}\n      classify={text => fetch(\"/api/memory/classify\", { method: \"POST\", body: text }).then(r => r.json()).then(r => r.group)}\n      onChange={async (next, change) => {\n        if (change.type === \"forget\") await deleteMemory(userId, change.memory.id);\n        else await saveMemory(userId, change.memory); // throw to show the error in place\n      }}\n      onToggle={(setting, on) => updatePreference(userId, setting, on)}\n    />\n  );\n}\n\n// Offer what the assistant noted in the current chat as a \"Memory updated\" pill.\n<AgentMemory captured={[{ note: \"Noted: you prefer pnpm over npm\", memory: { id: \"m-pnpm\", group: \"preferences\", text: \"Prefers pnpm over npm in install commands\" } }]} />",
      "exports": [
        "AgentMemory"
      ]
    },
    {
      "name": "agent-tasks",
      "kind": "block",
      "title": "Agent tasks",
      "description": "A home for background coding agents: start a task on a repository and branch, watch each card move from queued to running to ready for review, and open a pull request from the result.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Kbd",
        "Segmented control",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-tasks",
      "registry": null,
      "keywords": [
        "background agents",
        "coding agent",
        "agent tasks",
        "task queue",
        "cloud agent",
        "pull request",
        "diff stat",
        "ci checks",
        "step log",
        "changed files",
        "retry",
        "cancel",
        "archive",
        "repository picker",
        "branch picker",
        "model picker",
        "ai agent dashboard"
      ],
      "whenToUse": [
        "A product surface where people hand coding tasks to background agents and come back later to review, open a pull request, retry or archive.",
        "Dashboards for automated maintenance runs, such as dependency upgrades, flaky test fixes or codemods, where each run ends in a branch with a diff and checks.",
        "Any queue of long running agent jobs where the current step, the elapsed time and a clear finished state matter more than a transcript."
      ],
      "whenNotToUse": [
        "Use agent-run to follow one live agent run in detail, with its plan, tool calls and output.",
        "Use code-review when people need to read the diff line by line and leave comments; this block only summarizes the change.",
        "Use agent-approvals when the agent pauses for a person to approve a risky action before it continues.",
        "Use agent-routines for scheduled, recurring agent jobs rather than one-off tasks.",
        "Use ai-composer for a chat style prompt box without a task list."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "Each card's title row is a button with aria-expanded and aria-controls pointing at its details; closed details are inert so their content leaves the tab order.",
        "Status is never color alone: every state pairs an icon with a label, such as Ready for review, Failed or Checks passed, and the live step is read with its status.",
        "The diff stat has a spoken label (lines added, removed and files changed); each file and step carries a hidden change or state word before it.",
        "Elapsed time has a spoken label such as Running for 2 minutes 14 seconds, so the compact 2m 14s is never the only form.",
        "Starting, finishing, cancelling, retrying, archiving and opening a pull request are announced in a polite status region, including that the preview simulates the run and the PR.",
        "Focus follows the control that replaced the pressed one: Cancel to Retry, Retry to Cancel, a created PR back to its card, and an archived card to its neighbour.",
        "Chips are Radix dropdown menus with radio items, so the chosen repository, branch and model are announced as checked. Pointer focus draws no ring; keyboard focus uses Arc's shared ring."
      ],
      "motion": [
        "A started task grows in at the top on the critically damped smooth spring and its background flashes the accent tint once, so the eye lands on it.",
        "The live step shimmers with a calm, linear sweep while the agent works. A new step crossfades in: the old line fades out in about 100ms and the new one fades in over 160ms from 3px below. The sweep stops the moment the run stops.",
        "Status changes crossfade the state line once. Actions crossfade as a group and their slot follows the new width on the morph spring, which is how Create pull request becomes PR #482 open.",
        "Filter counts roll up or down with tabular numerals on the snappy spring, and the tab highlight glides on the segmented control's morph spring.",
        "Expanding a card animates height and opacity together on the smooth spring; new log lines grow in the same way. Archiving collapses the card so the list closes the gap.",
        "Reduced motion keeps every state, label, announcement and focus move but drops the sweep, the spinner rotation, the travel and the springs; content crossfades quickly or appears at once."
      ],
      "responsive": [
        "The block is a single panel up to 760px wide and works from 320px. Layout follows the panel's own width through a container query, not the viewport.",
        "The composer chips wrap under the task field on narrow panels, the shortcut hint hides below 520px and Start stays at the end of the row.",
        "Card titles wrap to two lines; the branch truncates first in the meta line; the state line wraps its facts and the actions drop to their own line when there is no room.",
        "Filter tabs scroll inside their track with edge fades when all four do not fit. The expanded details show steps and changed files side by side from 600px and stack below."
      ],
      "performance": [
        "One shared clock ticks once a second only while a task is queued, setting up or running, and stops when nothing runs.",
        "The demo runs each task on a single chained timer that is cleared on cancel, retry and unmount.",
        "Collapsed details stay mounted and inert, so reopening a card is instant; counts and filters are derived from the task list on each render without extra state."
      ],
      "aiNotes": [
        "Use it for asynchronous agent work that ends in a branch: background coding agents, dependency bots, code migration runs. Pass `tasks` from your store and update them from your backend events (queued, setting-up, running with `step`, then ready with `files` and `checks`).",
        "Statuses: queued, setting-up and running count as Running; ready waits for review (open or not); merged is final; failed shows `error` as the reason and offers Retry; cancelled offers Retry.",
        "Keep `step` short and concrete, such as \"Reading src/billing/invoice.ts\" or \"Running tests\". Push a matching `steps` entry with state \"active\" so the expanded log matches the card.",
        "The diff stat and file count are summed from `files`; do not pass totals. Paths render in a monospace font with the directory muted and truncated first.",
        "A pull request on the task (`pullRequest`) wins over the local result of `onCreatePullRequest`, so once your data has the PR you can return anything.",
        "Model ids must exist in `models`; the mark comes from the provider. Use current models only, such as Claude Opus 5.5 or Claude Sonnet 5.5.",
        "This block lists runs and their outcome. Send people to code-review for the full diff and to agent-run for a single live run with its tool calls."
      ],
      "related": [
        "agent-run",
        "agent-approvals",
        "agent-routines",
        "agent-orchestrator",
        "model-picker",
        "ai-composer"
      ],
      "usage": "import { AgentTasks } from \"@/components/arc/blocks/agent-tasks/agent-tasks\";\n\nexport function BackgroundAgents({ tasks, repositories }) {\n  return (\n    <AgentTasks\n      tasks={tasks}\n      repositories={repositories}\n      onStart={({ prompt, repo, branch, model }) => api.tasks.create({ prompt, repo, branch, model })}\n      onCreatePullRequest={async task => {\n        const pr = await api.tasks.openPullRequest(task.id);\n        return { number: pr.number, url: pr.html_url };\n      }}\n      onCancel={task => api.tasks.cancel(task.id)}\n      onRetry={task => api.tasks.retry(task.id)}\n      onArchive={task => api.tasks.archive(task.id)}\n    />\n  );\n}\n\n// A running task streams its current step; a finished one carries files and checks.\nconst task = {\n  id: \"task_8f2\",\n  title: \"Fix the flaky checkout test in CI\",\n  repo: \"arc-ui/web\",\n  branch: \"fix/flaky-checkout-test\",\n  baseBranch: \"main\",\n  model: \"claude-opus-5-5\",\n  status: \"running\",\n  startedAt: Date.parse(\"2026-10-20T09:14:00Z\"),\n  step: \"Running tests\",\n  steps: [{ text: \"Installed dependencies with pnpm\", state: \"done\", seconds: 38 }, { text: \"Running tests\", state: \"active\" }],\n};",
      "exports": [
        "AgentTasks"
      ]
    },
    {
      "name": "agent-approvals",
      "kind": "block",
      "title": "Agent approvals",
      "description": "One approval card for agents: the waiting tool call reads like a permission prompt with the tool's mark, the agent's reason and its arguments, and you approve, deny or allow it from now on.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Button",
        "Dropdown menu",
        "Kbd",
        "Popover",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-approvals",
      "registry": null,
      "keywords": [
        "agent approvals",
        "human in the loop",
        "tool call approval",
        "permission prompt",
        "approve deny",
        "always allow",
        "agent permissions",
        "rules",
        "audit log",
        "keyboard shortcuts",
        "ai agent",
        "mcp",
        "guardrails",
        "risk level",
        "editable arguments"
      ],
      "whenToUse": [
        "Human in the loop control for agents that call tools with side effects: deploys, emails, database writes, payments and ticket changes.",
        "An approvals card in an agent console or side panel where one person reviews, edits and decides tool calls quickly, from the keyboard if they like.",
        "Products that let people turn repeated approvals into standing rules for one agent or a whole workspace."
      ],
      "whenNotToUse": [
        "Use agent-run to follow one agent's steps and tool calls live, with an inline confirm inside the run.",
        "Use mcp-servers to connect tools and set their permissions up front rather than per call.",
        "Use agent-tasks or agent-orchestrator to plan and watch work across agents, not to gate individual tool calls.",
        "Use code-review when the thing to approve is a diff with comments."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-popover"
      ],
      "accessibility": [
        "The request in focus is a group named by its title, inside a region that takes focus after each decision, so the shortcuts keep working when the buttons that were pressed leave.",
        "Shortcuts answer anywhere in the card, and after a click in it left focus on the page, but stay quiet in fields, the menu and the popover, so typing an edited value never approves anything.",
        "Risk is a shield icon in its status color plus the word Low, Medium or High (with risk added for screen readers). Outcomes in the card and the log are an icon plus a word: Approved, Denied, Allowed by rule, Not sent.",
        "Every decision, edit, new rule, removed rule and failure is announced in a polite status region, and the demo says that nothing reached a real agent.",
        "Editable values are buttons named with their label, value and Edit; edited rows add a hidden \"edited\" to the label and a named Restore button.",
        "Queue rows are buttons with the agent, the summary and the risk in their name. The Resolved toggle carries aria-expanded and aria-controls.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Bringing a queued request into focus is one crossfade of the request (the old one fades out in 100ms, the new one fades in over 200ms from 3px) while the card's height follows on a critically damped spring and the row closes its gap. Nothing bounces.",
        "After a decision the next request slides up: it rises 14px into place on the same spring as its row collapses below.",
        "Always allow sends a small rule chip from the menu up into the Rules button on an ease-in-out path, then the button's count rolls and a faint accent wash fades out of it. Covered rows swap their risk for Allowed by rule with one crossfade and collapse a moment later.",
        "Sending shows a calm text shimmer that stops when the agent answers. The pending, rules and resolved counts roll with tabular numerals.",
        "Reduced motion drops travel, the chip flight and the shimmer: requests, rows and labels change in place with every state, focus move and announcement kept."
      ],
      "responsive": [
        "One card up to 640px wide that fills narrower containers; it holds without horizontal scroll from 320px up.",
        "Below a 540px container the shortcut hints and the agent names in the queue hide, since touch is the likely input there.",
        "Below 440px the model name and the word risk drop out of the meta line, argument labels sit above their values, the Enter key cap leaves the Approve button and queue rows keep the marks, the summary and a short status.",
        "Long titles wrap, while row summaries and log entries truncate with the full text still in their accessible names or titles."
      ],
      "performance": [
        "Only the request in focus renders its arguments and actions; queued requests are one row each.",
        "Timers for simulated delivery, settling and rule matches are tracked and cleared on unmount, and async callbacks read the latest state from a ref instead of re-subscribing.",
        "The relative times share one 15 second clock through useSyncExternalStore, so the card re-renders a few times a minute at most.",
        "Height changes are measured with one ResizeObserver per animated slot and animate only that slot."
      ],
      "aiNotes": [
        "Write title as the call itself in a few words and summary as a short row label. Backticks in title render code in the mono face.",
        "Use real agent and model names and set provider so the right mark shows (anthropic, openai or google); any other value shows a neutral bot mark.",
        "Give each permission a stable tool id (vercel.deploy, gmail.send). Rules match on it, so several requests for the same permission are covered by one rule.",
        "Keep the reason to one line in the agent's voice and put anything a person might change into args. Mark values that must not change with editable: false.",
        "onDecision receives the edited argument values. Apply those, not the originals, and keep edited in your audit trail.",
        "Always allow both approves the request in focus and creates the rule; requests the rule then covers arrive through onDecision with outcome \"allowed\" and the rule id.",
        "Return a rejected promise from onDecision to show the error state; the Error's message is shown to the person."
      ],
      "related": [
        "agent-run",
        "agent-tasks",
        "agent-orchestrator",
        "agent-routines",
        "mcp-servers"
      ],
      "usage": "import { AgentApprovals } from \"@/components/arc/blocks/agent-approvals/agent-approvals\";\n\nexport function ApprovalPrompt({ pending, rules }) {\n  return (\n    <AgentApprovals\n      requests={pending}\n      rules={rules}\n      reviewer=\"Emma Collins\"\n      workspace=\"Northwind\"\n      onDecision={decision => fetch(`/api/approvals/${decision.requestId}`, { method: \"POST\", body: JSON.stringify(decision) }).then(response => {\n        if (!response.ok) throw new Error(\"The agent did not get your decision. Nothing ran.\");\n      })}\n      onRuleChange={({ type, rule }) => type === \"added\" ? saveRule(rule) : deleteRule(rule.id)}\n    />\n  );\n}\n\n// One pending request\nconst request = {\n  id: \"req-deploy-web\",\n  agent: { id: \"deploy\", name: \"Deploy agent\", model: \"Claude Opus 5.5\", provider: \"anthropic\" },\n  tool: { id: \"vercel.deploy\", name: \"Vercel\", action: \"Deploy to production\", brand: \"vercel\" },\n  title: \"Run `vercel deploy --prod`\",\n  summary: \"Deploy northwind-web to production\",\n  reason: \"The preview passed all 214 checks and the release window opened at 14:00.\",\n  args: [\n    { key: \"project\", label: \"Project\", value: \"northwind-web\", mono: true },\n    { key: \"commit\", label: \"Commit\", value: \"8f3c2a1\", mono: true, editable: false },\n    { key: \"target\", label: \"Target\", value: \"production\", mono: true },\n  ],\n  risk: \"high\",\n  requestedAt: \"2026-10-20T14:30:00\",\n};",
      "exports": [
        "AgentApprovals",
        "ApprovalRequest"
      ]
    },
    {
      "name": "agent-routines",
      "kind": "block",
      "title": "Agent routines",
      "description": "Scheduled agents in one card: each routine reads as a sentence with its schedule, next run countdown, connectors and destination, and opens smoothly to edit or run it now.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Input",
        "Popover",
        "Segmented control",
        "Select",
        "Switch",
        "Text shimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-routines",
      "registry": null,
      "keywords": [
        "scheduled agents",
        "agent routines",
        "cron",
        "automations",
        "recurring tasks",
        "ai agent schedule",
        "run history",
        "slack digest",
        "triage bot",
        "agent runs",
        "natural language schedule",
        "workflow automation"
      ],
      "whenToUse": [
        "Settings or automation screens where people schedule agents to run on their own and post results to Slack.",
        "A card on a dashboard that shows at a glance which scheduled agents are healthy, which failed and when each runs next.",
        "Products that let people create automations from a sentence and confirm the parsed schedule, sources and destination."
      ],
      "whenNotToUse": [
        "Use agent-run to watch a single agent work through a task in real time with its tool calls.",
        "Use agent-tasks for a queue of one-off jobs that run once rather than on a schedule.",
        "Use agent-approvals when the agent's output needs a person to approve it before anything is posted.",
        "Use agent-orchestrator to show several agents handing work to each other within one run."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch",
        "@radix-ui/react-tooltip",
        "@radix-ui/react-popover",
        "@radix-ui/react-select"
      ],
      "accessibility": [
        "Each routine name is a real button with aria-expanded; the rest of the row also opens it on click, while Run now, the switch and the ticks stay separate controls.",
        "Status is never color alone: the last run pairs an icon with its time, each tick has a full text label and a tooltip with an icon and a label, and the strip is a group named with its counts (for example 12 succeeded, 1 failed, 1 skipped).",
        "Connector, destination and model marks are hidden from assistive technology and replaced by one sentence per row: what it reads, where it posts and which model runs it.",
        "The run ticks are one tab stop with a roving index, so a list of routines does not add 14 stops per row.",
        "Run starts, results, failures, stops, toggles, schedule changes and new routines are announced in a polite status region, including that the preview only simulates runs and Slack posts.",
        "While a run is in progress the open row lists its steps as an ordered list with the current step marked aria-current=\"step\"; a failure is announced as an alert with the step and the error.",
        "The sentence parser's state and the simulation note sit in a polite live region. Chips are buttons named with their field and value, and their popovers use pressed buttons for sources and the destination.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "A row opens from height 0 to auto and back on a critically damped spring (about 340ms, no bounce). Opacity starts a beat after the height and ends a beat before it on close, the content rises 6px into place, and the chevron and the row's quiet tint change on the same timing. Opening one row closes the other at the same time, and the rows below glide with the height.",
        "While a row is open its height keeps following its content on the same spring, so a streamed report or the step list never resizes the card in one frame.",
        "Run now fills a 2px line under the row at the pace of the steps, in the routine's series color, then the line fades. A new tick slides in from one slot to the right while the others tween one slot left and the oldest fades out, on a 240ms ease-out tween. Ticks sit in fixed slots and never measure layout, so they do not drift while rows open or close.",
        "Labels change with one minimal crossfade: the old one fades out in 100ms and the new one fades in over 160ms from 2px below. A schedule change slides the text after it as its width follows on a critically damped spring. The countdown uses tabular numerals in a reserved width.",
        "The Running label and the active step carry a calm shimmer that stops when the run ends. Run now and Stop share one button and one width, so the swap never resizes it.",
        "The latest report streams in chunks of 15 to 40 characters every 32ms on a requestAnimationFrame clock; each new chunk only fades in and settled text never re-renders.",
        "Reduced motion opens, closes, shifts and swaps at once and keeps every state, label and announcement; the shimmer stops and streamed chunks appear without the fade."
      ],
      "responsive": [
        "The card is up to 680px wide and reads the same at 360px: every row keeps its three lines, and the marks slot and ticks never wrap.",
        "Below 540px of card width the last run moves out of line two (it stays in the ticks' tooltips and the open row), and the insets tighten.",
        "Below 420px Run now and Stop become icon buttons with the same accessible names, and the schedule's time field takes its own line.",
        "Long step labels and destinations truncate with an ellipsis; the full text is in the open row, the tooltips and the status region."
      ],
      "performance": [
        "One shared one second clock drives every countdown; nothing else re-renders on a timer.",
        "Runs are cancelled with an AbortController when stopped or when the card unmounts, so no timers outlive it.",
        "Ticks animate only transform and opacity in fixed slots; nothing in the list uses layout animation.",
        "Closed rows unmount their details, so a long list only renders one report and one schedule editor.",
        "Brand marks are inline SVG components in the block's folder; the Gemini mark scopes its gradient ids per instance."
      ],
      "aiNotes": [
        "Choose it for agents that run on a schedule and report back: daily triage, weekly digests, hourly watchers, meeting briefs. One row per routine; keep names short and specific so the first line never truncates.",
        "Schedules are four plain shapes (hourly at a minute, daily, weekdays, weekly on a day) with 24 hour times. The card derives the plain words, the cron expression and the next run in local time.",
        "History is oldest first and only the last 14 runs show. Use status skipped for runs that found nothing to post, so the strip separates quiet runs from failures.",
        "The report is short plain text: the first line is the title, lines starting with \"- \" become list items and other lines become paragraphs. Issue keys like ENG-2141, pull requests like #1842 and channels like #eng are set apart.",
        "Rows show up to three source marks; with four or more they show two and a +N count whose tooltip names the rest. Give each routine step labels that match what your agent really does, so the progress is honest.",
        "New routine parsing is a local, simulated pattern match and says so. In production, send the sentence to a model and keep the chips as the confirmation step before onSave."
      ],
      "related": [
        "agent-run",
        "agent-tasks",
        "agent-approvals",
        "agent-orchestrator",
        "mcp-servers",
        "model-picker",
        "research-run",
        "text-shimmer"
      ],
      "usage": "import { AgentRoutines, type Routine } from \"@/components/arc/blocks/agent-routines/agent-routines\";\n\nexport function Routines({ routines }: { routines: Routine[] }) {\n  return (\n    <AgentRoutines\n      routines={routines}\n      onRun={async routine => {\n        const result = await api.routines.run(routine.id); // { id, status, summary, durationMs, report }\n        return {\n          run: { id: result.id, status: result.status, at: Date.now(), summary: result.summary, durationMs: result.durationMs },\n          report: result.report,\n        };\n      }}\n      onToggle={(id, enabled) => api.routines.update(id, { enabled })}\n      onSave={(routine, { created }) => created ? api.routines.create(routine) : api.routines.update(routine.id, routine)}\n    />\n  );\n}\n\n// A routine\nconst triage: Routine = {\n  id: \"morning-triage\",\n  name: \"Morning triage\",\n  instruction: \"Read new Linear issues, match them to Sentry errors and post a ranked list to #triage.\",\n  schedule: { frequency: \"weekdays\", time: \"09:00\" },\n  sources: [\"linear\", \"sentry\"],\n  destination: \"#triage\",\n  model: \"claude-sonnet-5-5\",\n  enabled: true,\n  steps: [\"Reading new Linear issues\", \"Matching Sentry errors\", \"Ranking by customer impact\", \"Posting to #triage\"],\n  history: [{ id: \"r-1\", status: \"success\", at: \"2026-10-19T09:00:00\", durationMs: 41000, summary: \"Triaged 9 issues, 2 urgent\" }],\n  report: \"Triage for Monday 19 October\\n- ENG-2141 Checkout button stays disabled. Urgent, assigned to Priya.\\n9 issues reviewed, 2 urgent.\",\n};",
      "exports": [
        "AgentRoutines"
      ]
    },
    {
      "name": "agent-orchestrator",
      "kind": "block",
      "title": "Agent orchestrator",
      "description": "A lead agent splits a goal into a plan, fans the subtasks out to parallel worker lanes and merges what comes back, with each lane's model, progress and result in view.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Copy button",
        "Segmented control",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-orchestrator",
      "registry": null,
      "keywords": [
        "multi agent",
        "orchestrator",
        "subagents",
        "parallel agents",
        "fan out",
        "fan in",
        "lead agent",
        "agent workflow",
        "concurrency",
        "tool calls",
        "token usage",
        "cost",
        "retry",
        "agent observability",
        "claude",
        "ai agents"
      ],
      "whenToUse": [
        "Showing a multi agent run where a lead model splits a task across subagents, such as research, audits, migrations or comparisons.",
        "Debug or observability panels that need per worker progress, tool calls, errors, retries, tokens and cost in one view.",
        "Explaining the cost and speed tradeoff of parallel agents, with a live concurrency control."
      ],
      "whenNotToUse": [
        "Use agent-run for a single agent working through steps in one thread.",
        "Use agent-plan when the job is to review and edit a plan before anything runs.",
        "Use research-run for a single research answer with sources and citations.",
        "Use agent-tasks to track many independent background tasks that do not merge into one result.",
        "Use model-router when one request is routed to one model rather than split across several."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The goal is the card heading, and the lead, the plan and the summary are labelled sections, so the flow reads top to bottom in the order the work happens.",
        "Each lane is a button with aria-expanded and aria-controls, named by its subtask, model and status. A closed transcript is inert, so it leaves the reading order.",
        "Status never rests on color: every lane state pairs a glyph with a word (waiting, planned, queued with its place in line, starting, the running tool, failed with a retry countdown, returned, gave up, stopped).",
        "A polite status region announces planning, dispatch, errors and retries, each returned lane, the start of the summary and the final totals, and says that the default run is a simulation.",
        "Connectors and pulses are decorative and hidden from assistive technology; the plan's colored dots repeat information that the lane titles already give.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring, inset on lanes so it follows their corners."
      ],
      "motion": [
        "Plan items grow in one by one on a critically damped spring and their text streams in chunks of a few words that only fade in.",
        "A dispatch sends a short colored packet down the connector to the lane; a returning result sends one from the lane to the summary. The simulation waits for the packet to land before the lane starts, so cause always comes before effect.",
        "A lane's branch and port take its series color when work reaches it; queued branches stay dashed with a hollow port.",
        "Tool, status and output swaps are one minimal crossfade: the old label fades out in 100ms while the new one fades in over 160ms from 2px below. Running labels carry a calm shimmer that stops when the lane settles.",
        "One line of output streams on a steady requestAnimationFrame clock and glides left under a soft edge when it outgrows the lane, so the newest words stay in view; settled lines rest at the start with an ellipsis.",
        "Tokens and cost count to each new value with tabular digits; elapsed times tick in tenths.",
        "Transcripts and the totals row open with height and opacity on a spring without bounce.",
        "Reduced motion removes pulses, spinners, shimmer, chunk fades and height travel, keeps every state, label and announcement, and never starts a run on its own."
      ],
      "responsive": [
        "Above 720px of card width, up to four lanes sit side by side; the trunk drops from the lead into a bus above them and their results merge on a second bus into the summary.",
        "Below that, lanes stack under a single trunk in the lead's column with a short branch into each lane, and the same pulses run down and back along it.",
        "Connectors are measured from the rendered layout with a ResizeObserver, so they follow wrapping text, an opened transcript and container resizes without breakpoints of their own.",
        "Below 440px the side padding tightens, the concurrency label and the lead's elapsed time are hidden, and the totals fall into two columns. It holds at 320px without horizontal scroll."
      ],
      "performance": [
        "The run hook's events go through one reducer; nothing renders per animation frame except the chunk clock of a line that is streaming.",
        "Token and cost counters write their text straight to the DOM through motion values, so a busy run does not re-render lanes for every tick.",
        "Pulses are single SVG paths animated with the Web Animations API on stroke-dashoffset and removed when they finish.",
        "Connector geometry is recomputed only when an observed size changes and is skipped when nothing moved."
      ],
      "aiNotes": [
        "Choose it when one model plans and several models work in parallel, and the reader needs to see who is doing what, what failed, and what parallelism saved.",
        "Write the run hook as an async generator. Yield plan events first, then dispatch before a lane's first tool event, then result (or fail) for every dispatched subtask, then summary deltas and done.",
        "Respect context.concurrency() before each dispatch. The block shows a lane as queued only while every slot is taken, so a hook that ignores the limit will simply show no queue.",
        "usage events are increments, not totals. Leave out subtaskId for the lead's own tokens; they count toward the totals row but not toward any lane.",
        "output deltas append to the lane's line; set newLine on the first delta of a new step. The block smooths bursty deltas into a steady stream.",
        "The sequential estimate is the wall time minus the parallel stretch plus every lane's own time from dispatch to return, so concurrency 1 reads as about the same as running in sequence.",
        "Company names, prices and pages in the sample are fictional. Prices for non Anthropic models are illustrative sample values."
      ],
      "related": [
        "agent-run",
        "agent-plan",
        "agent-tasks",
        "research-run",
        "agent-loop",
        "mixture-of-experts",
        "agent-status"
      ],
      "usage": "import { AgentOrchestrator, type OrchestratorRun } from \"@/components/arc/blocks/agent-orchestrator/agent-orchestrator\";\n\n// Yield events from your orchestrator. The block handles timing, pulses, queues and totals.\nconst run: OrchestratorRun = async function* ({ goal, workers, concurrency, signal }) {\n  const plan = await planWithLead(goal, workers);           // your lead model call\n  for (const subtask of plan) yield { type: \"plan\", subtask };\n\n  for await (const update of runWorkers(plan, { concurrency, signal })) {\n    // update.kind: \"dispatch\" | \"tool\" | \"toolDone\" | \"text\" | \"usage\" | \"error\" | \"retry\" | \"result\"\n    if (update.kind === \"dispatch\") yield { type: \"dispatch\", subtaskId: update.id };\n    if (update.kind === \"tool\") yield { type: \"tool\", subtaskId: update.id, tool: update.tool, target: update.target };\n    if (update.kind === \"text\") yield { type: \"output\", subtaskId: update.id, delta: update.text, newLine: update.first };\n    if (update.kind === \"usage\") yield { type: \"usage\", subtaskId: update.id, tokens: update.tokens, cost: update.cost };\n    if (update.kind === \"error\") yield { type: \"error\", subtaskId: update.id, message: update.message, retryInMs: 2000 };\n    if (update.kind === \"result\") yield { type: \"result\", subtaskId: update.id, text: update.text };\n  }\n\n  for await (const delta of streamSummary(signal)) yield { type: \"summary\", delta };\n  yield { type: \"done\" };\n};\n\nexport function PricingReview() {\n  return (\n    <AgentOrchestrator\n      goal=\"Compare our pricing page with four competitors and suggest changes\"\n      run={run}\n      defaultConcurrency={3}\n      onComplete={result => saveReport(result.summary, result.cost)}\n    />\n  );\n}",
      "exports": [
        "AgentOrchestrator",
        "simulateOrchestration"
      ]
    },
    {
      "name": "mcp-servers",
      "kind": "block",
      "title": "MCP servers",
      "description": "Manage the MCP servers that give an agent its tools: see what the enabled tools cost in context, connect or pause a server, and switch single tools on or off.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Avatar",
        "Button",
        "Input",
        "Search field",
        "Segmented control",
        "Switch",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/mcp-servers",
      "registry": null,
      "keywords": [
        "mcp",
        "model context protocol",
        "mcp servers",
        "tools",
        "integrations",
        "connectors",
        "agent tools",
        "tool definitions",
        "context budget",
        "token cost",
        "oauth",
        "tool discovery",
        "permissions",
        "always allow reads",
        "ai agent settings"
      ],
      "whenToUse": [
        "Settings for an agent or AI assistant that connects to MCP servers: which servers it has, which tools are on and what they cost in context.",
        "Onboarding a new integration with a directory, sign in and tool discovery in one place.",
        "Keeping tool definitions inside a context budget by switching off tools the agent does not need."
      ],
      "whenNotToUse": [
        "Use agent-approvals to approve or deny individual tool calls while the agent runs.",
        "Use context-window to show everything in the context window, not only tool definitions.",
        "Use agent-run to follow an agent's steps and tool calls live.",
        "Use knowledge-base to manage documents an agent retrieves from, rather than tools it calls."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "Each server row is a button with aria-expanded and aria-controls, named with the server, its status, transport, tools on and token cost.",
        "Status is an icon in its semantic color plus a word (Connected, Needs sign in, Error, Connecting), never color alone. Read and write tools carry an eye or pen icon and the word; below 400px the word stays for screen readers only.",
        "The context bar is a meter with a value text such as \"6,400 of 8,000 budgeted tokens\", and over budget adds how far over it is beside a warning icon.",
        "Every tool switch is named with the tool, its access and its token cost, and the tool name is its label, so clicking the name toggles it.",
        "The sheet is a modal dialog: the card behind it is inert, Tab is trapped inside, Escape closes it, and focus moves to the search field, the Authorize button or the step heading as the flow advances.",
        "Toggles, permission changes, connects, failures, removes and undo are announced in a polite status region, and the simulated authorize screen says plainly that no account is contacted.",
        "When a row's action button disappears (Retry, Sign in) or a row is removed, focus moves to that row or its neighbour so keyboard users never land on the page body.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Toggling a tool rolls the tool count and token numbers with tabular digits, and its server's segment resizes on a critically damped spring.",
        "Hovering or focusing a server highlights its segment in the first series color and dims the others.",
        "Rows open and close their tools on a smooth height spring with a short fade; the chevron turns.",
        "The sheet rises from the card's bottom edge over a scrim, without overshoot. Steps cross fade with an 18px shift in the direction of the flow.",
        "Discovered tools arrive one by one with a 4px fade in, under a calm shimmer on \"Discovering tools\" that settles into \"Found 5 tools\".",
        "A new server slides into the top of the list as its height opens, with a brief accent tint that fades out. Removed rows close their gap, and Undo opens them again in place.",
        "Status labels and actions cross fade (about 100ms out, 160ms in). No text morphs or per letter motion.",
        "Reduced motion drops travel, height animation and the shimmer: content appears in place with every state, focus move and announcement kept."
      ],
      "responsive": [
        "Fills its container up to 720px. Rows keep one line of name and one of status at every width.",
        "Below a 520px container the tool list drops its indent, \"5 of 6 tools\" shortens to \"5 of 6\" and the sheet rises higher.",
        "Below 400px the tool count leaves the row (the token cost stays), read and write show as icons, and the permission control wraps under its label.",
        "While the sheet is open the card grows to at least 600px so the directory has room, and scrolls its bottom edge into view if needed. Holds without horizontal scroll from 320px up."
      ],
      "performance": [
        "Only the open row renders its tools; the others are one button each.",
        "The meter and counters derive from the server list on render; there is no separate state to keep in sync.",
        "Simulated delays and the undo window use tracked timers that clear on unmount, and an abandoned discovery is ignored through a run ticket."
      ],
      "aiNotes": [
        "Use real server names and pick a brand so the real mark shows: github, linear, notion, slack, sentry, stripe, postgresql, figma, vercel, supabase, cloudflare, googledrive, atlassian, datadog, hubspot, asana, posthog, airtable, docker, zendesk, intercom, gmail, googlecalendar. Leave brand unset for the generic MCP mark.",
        "Measure tokens per tool from the tool's JSON definition with your provider's token counter. Typical definitions take 150 to 700 tokens.",
        "Mark every tool read or write honestly: Always allow reads lets read tools run without asking, so a tool that changes data must be write.",
        "Set status \"needs-auth\" when a token expired or was revoked, and \"error\" with a short error string when tools/list failed. Neither counts toward context until it connects again.",
        "Return the discovered tools from onConnect so the sheet shows what the server really reported. Throw an Error with a readable message to show the failure state.",
        "The endpoints, token counts and directory in the demo are sample values."
      ],
      "related": [
        "agent-approvals",
        "context-window",
        "agent-run",
        "ai-composer",
        "agent-routines",
        "agent-orchestrator",
        "knowledge-base"
      ],
      "usage": "import { McpServers } from \"@/components/arc/blocks/mcp-servers/mcp-servers\";\n\nexport function AgentTools({ servers, directory }) {\n  return (\n    <McpServers\n      servers={servers}\n      directory={directory}\n      budget={8000}\n      clientName=\"Northwind agent\"\n      account={{ name: \"Emma Collins\", email: \"emma@northwind.dev\", avatar: \"/media/people/emma-collins.svg\" }}\n      onToggleTool={(serverId, toolId, enabled) => saveTool(serverId, toolId, enabled)}\n      onPermissionChange={(serverId, permission) => savePermission(serverId, permission)}\n      onConnect={async ({ kind, serverId, endpoint }) => {\n        if (kind !== \"url\" && kind !== \"retry\") await openOAuthWindow(serverId);\n        const response = await fetch(`/api/mcp/${serverId}/tools`, { method: \"POST\", body: JSON.stringify({ endpoint }) });\n        if (!response.ok) throw new Error(\"The server closed the connection during tools/list.\");\n        return response.json(); // [{ id, name, access, tokens }]\n      }}\n      onRemove={serverId => fetch(`/api/mcp/${serverId}`, { method: \"DELETE\" })}\n    />\n  );\n}\n\n// One server\nconst github = {\n  id: \"github\",\n  name: \"GitHub\",\n  brand: \"github\",\n  transport: \"remote\",\n  endpoint: \"https://api.githubcopilot.com/mcp/\",\n  status: \"connected\",\n  permission: \"allow-reads\",\n  tools: [\n    { id: \"search_code\", name: \"search_code\", access: \"read\", tokens: 420, enabled: true },\n    { id: \"create_pull_request\", name: \"create_pull_request\", access: \"write\", tokens: 640, enabled: true },\n    { id: \"merge_pull_request\", name: \"merge_pull_request\", access: \"write\", tokens: 360, enabled: false },\n  ],\n};",
      "exports": [
        "McpServers",
        "McpTool"
      ]
    },
    {
      "name": "knowledge-base",
      "kind": "block",
      "title": "Knowledge base",
      "description": "A retrieval knowledge base in one card: add sources and watch them parse, chunk, embed and index, then test a question and see which chunks come back and why.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Copy button",
        "Dropdown menu",
        "Popover",
        "Segmented control",
        "Select",
        "Slider",
        "Switch",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/knowledge-base",
      "registry": null,
      "keywords": [
        "knowledge base",
        "rag",
        "retrieval",
        "vector store",
        "embeddings",
        "chunking",
        "ingestion",
        "reranking",
        "reranker",
        "hybrid search",
        "semantic search",
        "document indexing",
        "top k",
        "similarity score",
        "retrieval test",
        "ai search"
      ],
      "whenToUse": [
        "Admin screens for a RAG or support assistant where people add documents, watch them index, and check what a question retrieves.",
        "Explaining or tuning chunking, hybrid search and reranking with a working test bench instead of a diagram.",
        "Debugging why an assistant answered from the wrong source by looking at scores, matches and neighbouring chunks."
      ],
      "whenNotToUse": [
        "Use vector-search to illustrate how embeddings and nearest neighbours work; this block is the working product surface.",
        "Use document-extract to pull structured fields out of a single document.",
        "Use agent-memory to show what an agent remembers about a user or a project.",
        "Use ai-answer or research-run to show the cited answer a model writes from retrieved sources."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-popover",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-select",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "The card is a labelled section. Each pipeline is a progressbar with the overall percent and a value text that names the step, such as Embed, step 3 of 4, or Failed at embed.",
        "Status never relies on color: indexed rows show a check with a label, failed rows an alert icon with the reason in text, and working rows a named step and a percent.",
        "Each result is a button with aria-expanded and aria-controls; its similarity or reranker score and any rank change (for example, Up 2 after reranking, was 3) are in its accessible name.",
        "Matching words are wrapped in mark elements, so assistive technology can report highlighted text.",
        "Ingestion milestones, failures, search results, reranking and simulated actions are announced through one polite status region; simulated uploads say that files stay in the browser.",
        "Icon-only controls (settings, clear query) have specific names. The settings toggles are full labels, so the text toggles the switch too.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Turning on Rerank shows a short reranking state, then moves each result to its new rank with a critically damped layout animation (no bounce) while score bars and numbers roll to the reranker's values and a rank change fades in.",
        "Opening a result springs its height open: the previous chunk slides in above, the excerpt grows from two lines to the full chunk, and the next chunk slides in below, all on the smooth spring.",
        "Pipeline segments fill from a single animation-frame clock while counts climb; the meta line crossfades when a step changes. Active labels use a finite text shimmer that settles when the step ends.",
        "New sources open in at the top of the list on a height spring; removed ones close their gap. Views crossfade without moving.",
        "Reduced motion places results, heights and values immediately, stops the spinner and the skeleton pulse, and keeps every label and announcement."
      ],
      "responsive": [
        "The card is container aware: from about 720px down to 320px it keeps one column, with no horizontal scroll.",
        "Below 560px the pipeline moves under each source name at full width, step labels drop, the page or path beside a result shortens, and the chunk-in-context rail moves inline.",
        "Below 440px the gutters tighten, the header actions wrap under the title, meta lines wrap instead of truncating, and the indexing summary beside the view switch hides.",
        "Long source names truncate with the full name in a title; failure reasons always wrap so they stay readable."
      ],
      "performance": [
        "The simulation advances all sources from one requestAnimationFrame loop that only runs while something is queued or working.",
        "Scores and counts roll through motion values without React re-renders; excerpt heights are measured once with a ResizeObserver and only re-measured when their width changes.",
        "The local retriever scores a few dozen chunks with plain string matching; real deployments pass onQuery and skip it."
      ],
      "aiNotes": [
        "Pass sources from your ingestion job and update status and progress as events arrive; the card only renders them when sources is set. Leave sources unset for a self-running demo.",
        "Return more candidates than top k from onQuery (for example 20) with both similarity and relevance. That is what lets the reranker pull a chunk up from below the cut, which is the point of the comparison.",
        "Scores are expected between 0 and 1. Map cosine similarity or a reranker logit into that range before returning it.",
        "Give each result before and after neighbours (or index plus a chunks prop) so opening a result can show it in context.",
        "location is free text: a page such as \"p. 3\" for documents, a heading for Notion, or a path for a crawled site.",
        "The embedding model labels are generic placeholders. Replace them with the models your stack actually uses."
      ],
      "related": [
        "vector-search",
        "document-extract",
        "agent-memory",
        "ai-answer",
        "research-run",
        "mcp-servers"
      ],
      "usage": "import { KnowledgeBase } from \"@/components/arc/blocks/knowledge-base/knowledge-base\";\n\nexport function SupportKnowledge({ sources, upload, retry }) {\n  return (\n    <KnowledgeBase\n      title=\"Support knowledge base\"\n      sources={sources}\n      onAddSource={({ kind, file }) => upload(kind, file)}\n      onRetrySource={retry}\n      onQuery={async (query, settings) => {\n        const res = await fetch(\"/api/retrieve\", { method: \"POST\", body: JSON.stringify({ query, ...settings, candidates: 20 }) });\n        if (!res.ok) throw new Error(\"The retriever did not respond\");\n        return res.json(); // [{ id, sourceId, text, location, similarity, relevance, before, after }]\n      }}\n    />\n  );\n}\n\n// With no props the card runs a full simulation with sample documents.\n<KnowledgeBase />",
      "exports": [
        "KnowledgeBase"
      ]
    },
    {
      "name": "eval-suite",
      "kind": "block",
      "title": "Eval suite",
      "description": "Run an evaluation suite on a new version of an LLM feature against its baseline: results stream in case by case, and regressions and fixes are called out with the cases behind them.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/eval-suite",
      "registry": null,
      "keywords": [
        "eval",
        "evals",
        "evaluation suite",
        "llm evaluation",
        "regression testing",
        "llm judge",
        "rubric",
        "test cases",
        "pass rate",
        "baseline",
        "prompt regression",
        "model comparison",
        "diff",
        "ai quality",
        "ci for prompts"
      ],
      "whenToUse": [
        "Regression testing an LLM feature before shipping a new prompt, model or agent version.",
        "CI style eval dashboards where people need the overall score, the per category picture and the exact cases that broke in one place.",
        "Triage of failing cases with an LLM judge: read both outputs, check the rubric, then accept the change or file an issue."
      ],
      "whenNotToUse": [
        "Use prompt-studio to edit a prompt and try it on one test case at a time.",
        "Use model-arena for a blind side by side vote between models on a single prompt.",
        "Use agent-run to follow one agent run step by step rather than scoring a suite.",
        "Use token-inspector to look at token level probabilities of a single reply."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each category row is one image with a full text summary (passed, failed, errors, not yet scored, percent and the change against the baseline), so the 120 cells never become 120 announcements.",
        "Outcomes never rely on color: pass, fail and error each have their own glyph, a regression is the only solid cell, and every list row spells out both outcomes in hidden text.",
        "Case rows with a drilldown are buttons with aria-expanded and aria-controls; the drilldown is a labelled region. Rows without one are plain text.",
        "The case filter is a radio group with roving focus. The run history is a slider whose value text names the run, its score and date.",
        "Run start, finish (score, delta, regressions, improvements), stop, failure and each triage are announced in a polite status region, including when a run or an issue is simulated.",
        "The score and every category percent count with tabular numerals; hidden text carries the final value so screen readers never read a number mid count.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Cells fill as results arrive: the background tints over 240ms and the glyph scales in from 40 percent with an ease out. Cases in flight breathe once a second.",
        "The score, category percents and cost count to their values on a critically damped spring, so a burst of results reads as one smooth count; a new run resets them at once.",
        "Bars fill with a clip-path tween and tint green or red only when the run finishes; the deltas fade up 3px at the same moment.",
        "The current run lands on the sparkline as a short drawn segment and a dot that scales in after it.",
        "Rows that appear mid run, the drilldown and Show more open on the smooth spring with no overshoot. The filter highlight glides on a critically damped spring and the list height follows the new filter.",
        "Run, Stop and Run again crossfade in place while the pill width follows the measured label.",
        "Reduced motion fills cells, numbers, bars and panels immediately, stops the breathing, and keeps every state and announcement."
      ],
      "responsive": [
        "The card is 760px wide at most. From 680px a category is one line: label, 24 cells and its bar; below that the cells move under the label, and below 480px they wrap into two rows of 12.",
        "Below 480px the header's model chips become a small aligned table (v12, v11 and Judge on the left, the model on the right) and the subtitle takes the full width under the title and Run.",
        "The summary goes from three columns to two at 620px and to one at 420px, with cost and time side by side.",
        "The two outputs share one frame split down the middle and stack below 560px; the drilldown labels move above their text. Below 480px each rubric row puts its name, score and change on one line with the bar under them.",
        "Case categories hide below 480px, triage tags keep only their icon, and titles truncate with the full title in hidden text.",
        "The filter drops its counts below 480px and tightens to fit 320px without scrolling."
      ],
      "performance": [
        "Scores, counts and per category numbers are derived from one results map in memoized passes; cells only change attributes, and their fills are CSS transitions.",
        "Counting numbers write their text through a motion value, so the spring never re-renders React per frame.",
        "The word diff runs once per opened case and is memoized on the two outputs.",
        "The simulation uses timers on eight workers and clears them on stop and unmount."
      ],
      "aiNotes": [
        "Use it when a team compares a candidate version of a prompt, model or agent against a baseline on a fixed suite and needs to know which cases regressed, not only the average.",
        "Score is the pass rate over all cases in percent; the delta compares it with the baseline pass rate computed from each case's baseline outcome. A regression is a case that passed on the baseline and does not pass now; improved is the reverse.",
        "Stream results from onRun as an async iterable for the live grid. Results may arrive in any order; the regression and improved lists keep arrival order while the run streams.",
        "Rubric scores are 1 to 5, one per suite.rubric criterion, for both baselineScores and result scores. The rubric bars reuse the category bar: new score as the fill, baseline as the ghost tick.",
        "When the two replies share little wording, the drilldown shows both in full without highlights instead of striking through the whole baseline.",
        "Model marks: pass brand \"claude\" or \"openai\" for the built-in marks, or any inline SVG as mark. The sample uses Claude Sonnet 5.5 against GPT-6 Luna with Claude Opus 5.5 as judge; costs and timings are sample values."
      ],
      "related": [
        "prompt-studio",
        "model-arena",
        "agent-run"
      ],
      "usage": "import { EvalSuite } from \"@/components/arc/blocks/eval-suite/eval-suite\";\n\nexport function SupportAgentEvals({ suite, history }) {\n  return (\n    <EvalSuite\n      suite={suite}\n      runs={history}\n      onRun={async function* (suite, { signal, started }) {\n        const response = await fetch(`/api/evals/${suite.name}/runs`, { method: \"POST\", signal });\n        for await (const event of readEvents(response)) {\n          if (event.type === \"started\") started(event.caseId);\n          if (event.type === \"scored\") yield event.result; // { caseId, outcome, output, scores, rationale, costUsd }\n        }\n      }}\n      onTriage={async (caseId, action) => {\n        if (action === \"issue\") return (await createIssue(caseId)).key; // \"SUP-2481\"\n        await markExpected(caseId);\n      }}\n    />\n  );\n}\n\n// A case with a drilldown. Cases without input still score and list, without the side by side view.\nconst refundCase = {\n  id: \"refunds-02\",\n  category: \"Refunds\",\n  title: \"Refund after partial shipment\",\n  baseline: \"pass\",\n  input: \"Only 2 of my 3 items arrived (order #48213). Can I get my money back for that part?\",\n  expected: \"Refund only the missing item, not the whole order.\",\n  baselineOutput: \"I've refunded $34.00 for the candle set to your original payment method.\",\n  baselineScores: [5, 5, 4, 5], // one per rubric criterion\n};",
      "exports": [
        "EvalSuite"
      ]
    },
    {
      "name": "agent-builder",
      "kind": "block",
      "title": "Agent builder",
      "description": "A card for setting up an AI agent: an inline editable name, a model picker, instructions, tool toggles with real app marks, and knowledge files with upload progress. A Configure / Test switch opens a compact chat that answers with the tools you turned on, and Publish ships the setup with a saved or unsaved state.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Segmented control",
        "Switch",
        "Text shimmer",
        "Radix dropdown menu (radio items)"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-builder",
      "registry": null,
      "keywords": [
        "agent builder",
        "configure AI agent",
        "GPT builder",
        "assistant settings",
        "AI agent config",
        "tools toggle",
        "knowledge base upload",
        "system prompt editor",
        "test agent chat",
        "publish agent",
        "Intercom Fin",
        "Zapier agents"
      ],
      "whenToUse": [
        "Letting customers configure their own AI agent inside your product, like Intercom Fin, Zapier agents or an OpenAI GPT builder.",
        "Internal admin screens where ops teams tune instructions, tools and knowledge for a support or billing agent.",
        "A product tour or landing section that shows how an agent is set up and tested in one place."
      ],
      "whenNotToUse": [
        "Running or monitoring an agent in production: use agent-status or agent-routines.",
        "Approving actions an agent wants to take: use agent-approvals.",
        "A full chat product: use ai-chat. The Test view here is a compact preview, not a conversation app.",
        "Only choosing a model: use model-picker."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "The card is a labelled section; the name field, instructions and every tool switch have labels, and the tool row text is part of the switch label so the whole row is clickable.",
        "The model picker is a Radix menu with radio items, so the selected model is announced as checked.",
        "Upload progress uses role=\"progressbar\" with a value; failed uploads show an icon and the error message, never color alone.",
        "Status changes (published, upload done or failed, reply ready) go to a polite live region, and simulated actions say so: \"Simulated, nothing was deployed\".",
        "The inactive view is inert and aria-hidden, so screen readers and the Tab key only reach what is visible."
      ],
      "motion": [
        "Configure and Test crossfade in the same box (100ms out, 160ms in); Test overlays Configure, so the card never changes size.",
        "Text swaps (model name, version, footer status, reply meta) are a single crossfade with a 2px rise. No blur or scramble.",
        "Test replies: a short Thinking shimmer, tool chips that shimmer while running and reveal their result in place, then fast chunked streaming of three to six words every 34ms, each chunk only fading in.",
        "Added and removed files animate their measured height with a bounce free spring; switches and the segmented control use the shared bounce free presets.",
        "Reduced motion drops the rises, height animation and streaming (replies appear whole) but keeps every state and status change."
      ],
      "responsive": [
        "Fills its container up to 640px. Below 480px the padding drops to 16px and the model picker narrows; the agent name, tool descriptions and file names truncate with an ellipsis. The Configure / Test switch stays beside the name down to 300px of card width, then takes its own row.",
        "Holds up at 360px without horizontal scroll; the Test view scrolls its own thread and keeps the composer pinned.",
        "Tool rows and buttons are at least 28px tall targets inside 52px rows for touch."
      ],
      "performance": [
        "Plain DOM and CSS; motion only animates opacity, small transforms and the height of added or removed file rows.",
        "The sample upload starts only once the card is on screen. Text shimmers pause off screen.",
        "Streaming appends one chunk every 34ms; each chunk is its own span so earlier text never re-renders its animation."
      ],
      "aiNotes": [
        "Use it for the settings screen of a support, sales or ops agent: one card that holds the whole config with a built in test loop.",
        "Wire onPublish to persist the config and onSend to call your agent runtime with the unsaved config, so people can test before they publish.",
        "Pass publishedConfig from the server so the unsaved state compares against what is live; files still uploading or failed do not count as changes.",
        "Tool ids in TestReply.calls must match AgentTool ids (or \"knowledge\" for a file lookup) so the chips get the right mark.",
        "Keep marks 16px square SVGs; mono marks should use currentColor so they follow the theme."
      ],
      "related": [
        "model-picker",
        "agent-routines",
        "agent-approvals",
        "ai-chat",
        "tool-call",
        "agent-status"
      ],
      "usage": "import { AgentBuilder, sampleAgentConfig } from \"@/components/arc/blocks/agent-builder/agent-builder\";\n\nexport function SupportAgentSettings() {\n  return (\n    <AgentBuilder\n      defaultConfig={sampleAgentConfig}\n      onPublish={async (config, signal) => {\n        const response = await fetch(\"/api/agents/billing\", { method: \"PUT\", body: JSON.stringify(config), signal });\n        if (!response.ok) throw new Error(\"Server rejected the config\");\n      }}\n      onSend={async (message, config, signal) => {\n        const response = await fetch(\"/api/agents/billing/test\", { method: \"POST\", body: JSON.stringify({ message, config }), signal });\n        return response.json(); // { text, calls: [{ tool: \"stripe\", label: \"Searched customers\", result: \"2 charges\" }] }\n      }}\n    />\n  );\n}",
      "exports": [
        "AgentBuilder",
        "simulateReply"
      ]
    },
    {
      "name": "ai-insight-card",
      "kind": "block",
      "title": "AI insight card",
      "description": "A KPI card that explains itself. Explain streams a short reason for the change and ranks three drivers with contribution bars. A numbered annotation lights up on the sparkline as each driver is read, and follow-up chips ask the next question in place.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-insight-card",
      "registry": null,
      "keywords": [
        "ai insight",
        "kpi card",
        "explain metric",
        "revenue explanation",
        "drivers",
        "sparkline",
        "analytics ai",
        "metric annotation",
        "why did revenue drop",
        "dashboard card"
      ],
      "whenToUse": [
        "A dashboard KPI where readers ask why it moved, such as net revenue, MRR, signups or churn.",
        "Weekly business reviews and finance dashboards that want an explanation next to the number.",
        "Products adding AI analysis to existing analytics without a separate chat."
      ],
      "whenNotToUse": [
        "Open ended questions across many tables: use ask-your-data.",
        "A plain number with no explanation: use metric-card or stat-card.",
        "Dense multi series charts: use a full chart component."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section; Explain carries aria-expanded and aria-controls for the answer panel.",
        "A polite live region announces when the answer starts, the full answer when it is ready, Stop and errors. Without `explain` it says the answer is simulated from sample data.",
        "Deltas carry a sign as well as color, and unread driver rows are hidden from assistive tech until the answer reaches them.",
        "The chart is a focusable group with arrow key reading; values are announced through its own live region.",
        "Follow-up chips use aria-pressed for the answer on screen and are disabled while a stream runs."
      ],
      "motion": [
        "The panel opens and closes on a critically damped 260ms spring; answers of a different length spring to their height. No bounce.",
        "The answer streams 3 to 7 words every 34ms, each chunk only fading in over 140ms. Its full text is reserved underneath, so the card never grows while streaming.",
        "Annotations fade and scale in over 200ms as their driver is read; the driver being read takes the accent on its badge, dot, guide, rank and underline.",
        "Contribution bars grow to their share over 240ms ease-out; skeletons crossfade to labels in 160ms.",
        "Reduced motion: the answer appears whole after the short thinking state, transitions cut, and the demo loop stays off."
      ],
      "responsive": [
        "The card fills its container up to 640px. Under 460px the legend hides and the contribution bars shorten, so driver labels keep their room and truncate with an ellipsis.",
        "Follow-up chips wrap onto a second row on phones. The chart redraws at the container width with crisp strokes.",
        "Touch scrubs the chart only while the finger is down, so vertical page scrolling still works."
      ],
      "performance": [
        "The chart is one inline SVG with two paths; annotations are absolutely positioned spans with CSS transitions.",
        "Streaming updates one counter per tick; tokenizing runs once per answer.",
        "The demo loop waits while the card is off screen and stops for good on the first interaction."
      ],
      "aiNotes": [
        "Use for one KPI that needs a why: revenue, signups, churn, pipeline. Return drivers that add up to the change, ranked by absolute size, at most three.",
        "Point each driver at the series index where it shows, and write one text segment per driver with `driver` set, in rank order, so the chart lights up as the reader reaches it.",
        "Pass lowerIsBetter for costs and churn so a rise reads red. Pass `format` for non-currency metrics.",
        "With a real `explain`, autoPlay defaults to off so the card never spends model calls on its own."
      ],
      "related": [
        "ask-your-data",
        "metric-card",
        "sparkline",
        "account-health",
        "text-shimmer"
      ],
      "usage": "import { AiInsightCard } from \"@/components/arc/blocks/ai-insight-card/ai-insight-card\";\n\nexport function RevenueCard({ metric }) {\n  return (\n    <AiInsightCard\n      metric={metric}\n      followUps={[{ id: \"plan\", label: \"Break down by plan\" }]}\n      explain={async ({ metric, followUp, signal }) => {\n        const res = await fetch(\"/api/insights/explain\", {\n          method: \"POST\",\n          body: JSON.stringify({ metric: metric.label, followUp: followUp?.id }),\n          signal,\n        });\n        if (!res.ok) throw new Error(\"The analysis service did not respond.\");\n        return res.json(); // { segments: [{ text, driver? }], drivers: [{ id, label, delta, point }] }\n      }}\n    />\n  );\n}",
      "exports": [
        "AiInsightCard"
      ]
    },
    {
      "name": "catch-up",
      "kind": "block",
      "title": "Catch up",
      "description": "A catch me up digest for when you return, where one button reads Slack, Linear, GitHub and Gmail for the range you pick and streams back a short ranked summary: Needs you, Decisions made, Shipped and FYI. Each item is one sentence with its source mark, people and a jump link, and marking items as read clears them with a smooth collapse.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "SegmentedControl",
        "TextShimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/catch-up",
      "registry": null,
      "keywords": [
        "catch up",
        "catch me up",
        "digest",
        "ai summary",
        "chief of staff",
        "while you were away",
        "inbox summary",
        "slack summary",
        "notifications digest",
        "linear",
        "github",
        "gmail",
        "prioritized summary",
        "mark as read"
      ],
      "whenToUse": [
        "A home or inbox screen that summarizes what happened across connected tools while the user was away.",
        "An AI assistant feature that turns hundreds of notifications into a short ranked list with jump links.",
        "A weekly or daily digest preview inside a product's notification settings."
      ],
      "whenNotToUse": [
        "A live notification feed where every event matters; use a plain activity list instead.",
        "A long-form report or meeting summary with paragraphs; use a document view or ai-chat.",
        "Approving agent actions; use agent-approvals."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "A polite live region announces each phase: reading, digest ready with the item count, marked as read with the count left, and all caught up. The sample adds that it is simulated and nothing was read.",
        "Sections are labelled regions with a heading that includes the count, and each sentence starts with its source name for screen readers.",
        "Rows that have not streamed in yet are hidden from assistive tech and their buttons are out of the tab order.",
        "After marking an item as read, focus moves to the next item's mark as read button, or to the footer action when the list is clear.",
        "Status is never color alone: sections pair an icon with a label, and the reading state uses a spinner then a check per source."
      ],
      "motion": [
        "The digest streams four words every 34ms; each chunk only fades in. Rows reserve their full height up front behind quiet skeleton bars, so nothing shifts while text streams.",
        "Switching between sources, digest and outcome is one crossfade (100ms out, 160ms in) while the card height springs to the new size with no bounce.",
        "Mark as read collapses the row's height and opacity in 200ms; an emptied section collapses the same way. The card follows the collapse frame by frame instead of chasing it with a second spring.",
        "Labels, counts and the summary line swap with a single crossfade and a 2px rise. The button keeps a fixed minimum width across labels.",
        "Reduced motion shows the finished digest at once, keeps crossfades as plain fades, and stops the spinner."
      ],
      "responsive": [
        "The card fills its container up to 600px. Below 440px container width the padding tightens, the range selector wraps under the title, and meta lines truncate with an ellipsis.",
        "Item actions are always visible, so touch users do not depend on hover.",
        "Holds at 360px and 390px viewports without horizontal scroll."
      ],
      "performance": [
        "Streaming re-renders one small list on a 34ms timer only while text is streaming; it stops at ready.",
        "Word fades run on CSS opacity transitions, not per-word motion components.",
        "The sample starts once via IntersectionObserver; the spinner animates only during reading."
      ],
      "aiNotes": [
        "Use for a return or Monday morning moment: a manager or engineer comes back after time away and needs what changed, ranked by whether it needs them.",
        "Keep items to one sentence written as an outcome (\"API v2.14 is live with 38% faster search\"), not a raw message. Put the person, channel and time in people, context and when.",
        "Order is fixed by section: Needs you first. Only put items in Needs you when the reader has to act.",
        "Pass generate for real data; it receives an AbortSignal that fires on Stop, a range change or unmount.",
        "Sources are Slack, Linear, GitHub and Gmail. To add another, extend CatchUpSource and the marks file."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "agent-approvals",
        "ai-chat"
      ],
      "usage": "import { CatchUp } from \"@/components/arc/blocks/catch-up/catch-up\";\n\nexport function ReturnDigest() {\n  return (\n    <CatchUp\n      generate={async (range, signal) => {\n        const response = await fetch(`/api/digest?range=${range}`, { signal });\n        if (!response.ok) throw new Error(\"Gmail did not respond. Try again in a minute.\");\n        return response.json();\n      }}\n      counts={{ \"1d\": { slack: 31, linear: 9 }, \"3d\": { slack: 86, linear: 31 }, \"1w\": { slack: 214, linear: 77 } }}\n      onOpen={item => window.open(item.href, \"_blank\")}\n      onMarkRead={ids => fetch(\"/api/digest/read\", { method: \"POST\", body: JSON.stringify({ ids }) })}\n    />\n  );\n}",
      "exports": [
        "CatchUp",
        "CatchUpItem"
      ]
    },
    {
      "name": "agent-gallery",
      "kind": "block",
      "title": "Agent gallery",
      "description": "A marketplace card of AI agent templates where you filter by team, search by agent or app, preview a sample run and install an agent inline: choose a workspace, grant access app by app, and the card confirms with a check.",
      "tier": "pro",
      "builtFrom": [
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-gallery",
      "registry": null,
      "keywords": [
        "agent marketplace",
        "agent templates",
        "ai agents",
        "agent gallery",
        "integrations",
        "install flow",
        "oauth scopes",
        "grant access",
        "app directory",
        "filter chips",
        "search",
        "claude",
        "gpt",
        "gemini",
        "slack",
        "hubspot",
        "github"
      ],
      "whenToUse": [
        "An agents or integrations page where people browse and install ready made AI agents.",
        "Onboarding that suggests agent templates by team and installs one without leaving the page.",
        "A marketplace teaser on a dashboard or settings page."
      ],
      "whenNotToUse": [
        "Configuring a single agent's prompt, tools or schedule: use a settings form or agent routines.",
        "Showing a running agent's progress: use agent status or tool call.",
        "Catalogs with hundreds of items that need pagination, sorting and facets: use a full page table or list."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The root is a labelled section; chips are a radiogroup with a roving tab stop, and workspaces are a second radiogroup.",
        "Each card is an article named after the agent. App marks are hidden from assistive tech and replaced by a sentence (\"Connects to HubSpot, Slack and Gmail\"); rating and installs are read as full words.",
        "After every step change made by the person, focus moves to the new step's main control, so keyboard users never fall back to the page. A leaving step is inert.",
        "A polite live region announces result counts, connect progress, errors and the installed result; without onInstall it adds \"Simulated, nothing was connected\".",
        "Status is never color alone: connected apps show a check, errors show an icon with \"Could not connect\", installed cards show a check with \"Installed\"."
      ],
      "motion": [
        "Filtering: removed cards fade out in 90ms and pop out of flow; remaining cards glide to their new cells on a critically damped spring (0.3s, no bounce), and the grid's height springs to the new row count.",
        "The chip highlight slides between chips on a 0.24s spring with no bounce. Text never animates layout.",
        "Card steps crossfade in place inside a fixed 176px card (out 100ms, in 160ms with 2px rise), so nothing changes size between states.",
        "Granting access connects apps one by one (340ms apart): a spinner, then a check that scales in without overshoot. The install button keeps its width while loading.",
        "Hover reveal of Preview and Install is a 160ms opacity and 2px rise. Touch screens show them permanently.",
        "Reduced motion keeps every end state and feedback but drops travel, staggers and the layout glide."
      ],
      "responsive": [
        "Uses container queries: two columns from 600px of card width, one column below. The header stacks the search under the title below 560px.",
        "Team chips scroll sideways inside their row on narrow screens; the page never scrolls horizontally. Tested at 360px, 390px and desktop.",
        "On touch screens Preview and Install stay visible since there is no hover."
      ],
      "performance": [
        "Filtering is a memoized pass over the catalog; only visible cards render.",
        "Height animation reads one ResizeObserver; resizes jump instead of animating.",
        "No timers run while idle; the connect sequence uses abortable timeouts cleared on cancel or unmount."
      ],
      "aiNotes": [
        "Pass real marks as ReactNode (inline SVG at 16px). Mono marks should draw with currentColor so they follow the theme.",
        "Keep job lines to one sentence of about 60 characters; the card reserves two lines and clamps the rest.",
        "Each app's scope is the plain language permission shown on the grant step. Keep it short and honest (\"Draft email for your review\").",
        "The card is a fixed height so the grid never jitters: preview shows the first three steps, the grant step fits three apps, and more than two workspaces scroll inside the step.",
        "onInstall should throw an Error with a short message; it is announced to screen readers after \"could not be installed\"."
      ],
      "related": [
        "agent-status",
        "tool-call",
        "model-picker",
        "agent-routines",
        "agent-approvals"
      ],
      "usage": "import { AgentGallery } from \"@/components/arc/blocks/agent-gallery/agent-gallery\";\n\n// Sample catalog with simulated installs\n<AgentGallery />\n\n// Real installs\n<AgentGallery\n  agents={templates}\n  workspaces={[{ id: \"acme\", name: \"Acme\", detail: \"Business · 48 seats\" }]}\n  installed={installedIds}\n  onInstalledChange={setInstalledIds}\n  onInstall={async (agent, workspace, signal) => {\n    const res = await fetch(\"/api/agents/install\", {\n      method: \"POST\",\n      body: JSON.stringify({ agent: agent.id, workspace: workspace.id }),\n      signal,\n    });\n    if (!res.ok) throw new Error(\"The workspace admin must approve this app.\");\n  }}\n  onOpen={agent => router.push(`/agents/${agent.id}`)}\n/>",
      "exports": [
        "AgentGallery"
      ]
    },
    {
      "name": "ai-feature-launch",
      "kind": "block",
      "title": "AI feature launch",
      "description": "An in-product launch card for an AI feature. A looping mini demo plays above a three-step tour (inline rewrite, thread summary, ask your workspace), with benefit rows, an admin-gated \"Enable for my workspace\" switch, and Try it now and Learn more actions.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Switch",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-feature-launch",
      "registry": null,
      "keywords": [
        "ai feature launch",
        "introducing ai",
        "feature announcement",
        "launch modal",
        "whats new",
        "product tour",
        "onboarding tour",
        "enable ai",
        "workspace setting",
        "admin toggle",
        "rollout",
        "announcement card",
        "notion ai",
        "linear launch",
        "pagination dots"
      ],
      "whenToUse": [
        "Announcing a new AI feature inside the product, with a tour of what it does and a switch to turn it on for the workspace.",
        "Admin-gated rollouts where an admin enables AI for everyone and members can still try it.",
        "A what's new modal or popover that needs to look polished in a launch clip."
      ],
      "whenNotToUse": [
        "Multi-step configuration with real inputs (models, data sources): use ai-setup-wizard.",
        "Ongoing activation tasks: use onboarding-checklist.",
        "A long changelog or release notes page: render the entries as a page instead."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "The card is a labelled section whose name follows the current step title. Only the active step is exposed; hidden steps are aria-hidden and inert.",
        "The preview stage is decorative (aria-hidden): the title, description and benefits carry the same message as text.",
        "Dots are buttons named \"Step 2 of 3: Catch up on any thread\" with aria-current=\"step\" and a roving tab stop.",
        "The switch is labelled by its visible label and described by the admin note. Saving, cancelled, saved, error and simulated results are announced in a polite live region.",
        "Status is never color alone: the saved note pairs a check with \"On for all 48 members\", the error note pairs an alert icon with text.",
        "Autoplay stops on the first interaction and waits while the pointer is over the card, so nobody loses a step they are reading."
      ],
      "motion": [
        "One shared loop clock drives the mini demo, the dot fill and autoplay, so they never drift. It pauses off screen.",
        "Step changes are a single crossfade: the old step fades out in 100ms, the new one fades in over 160ms and rises 2px. All steps share one grid cell, so the card never changes height.",
        "Mini demos stream text in chunks of three words every 40ms, each chunk only fading in; thinking labels shimmer for under a second.",
        "The active dot widens on a bounce-free spring (0.24s) and fills linearly with the loop.",
        "Reduced motion: no autoplay or loops, each step shows a still frame at posterTime, and feedback (notes, spinner) stays."
      ],
      "responsive": [
        "Fills its container up to 520px. Below 440px (container query) the actions span the width and the model credit centers under them.",
        "The mini demo panel caps at 380px inside the stage and truncates long lines with an ellipsis, so it holds at 360px.",
        "Dots have 24px tall hit areas; the switch keeps the Arc switch's touch size."
      ],
      "performance": [
        "The active demo re-renders on a 40ms grid (about 25 renders a second) while visible; leaving demos freeze and unmount.",
        "Inactive step copy stays mounted for the height reservation but is hidden and inert.",
        "Three inline SVG marks and three 20px avatars; no images in the copy."
      ],
      "aiNotes": [
        "Use it once, the first time a member opens the product after an AI feature ships; persist dismissal yourself (put it in a Dialog or a popover and close it from onTry).",
        "Swap the default steps for your own: write each demo as a pure function of time using the loop time argument rather than timers, so pause, loop and reduced motion work automatically.",
        "Wire onEnabledChange to a real settings endpoint and respect the AbortSignal; set canManage from the viewer's role.",
        "Keep three steps with three benefits each; the card is designed for 360 to 520px."
      ],
      "related": [
        "ai-setup-wizard",
        "onboarding-checklist",
        "catch-up",
        "ghost-text",
        "ai-answer",
        "switch",
        "text-shimmer"
      ],
      "usage": "import { AiFeatureLaunch } from \"@/components/arc/blocks/ai-feature-launch/ai-feature-launch\";\n\nexport function AiLaunch({ workspace, isAdmin }: { workspace: { name: string; members: number; aiEnabled: boolean }; isAdmin: boolean }) {\n  return (\n    <AiFeatureLaunch\n      workspaceName={workspace.name}\n      memberCount={workspace.members}\n      canManage={isAdmin}\n      defaultEnabled={workspace.aiEnabled}\n      onEnabledChange={async (enabled, signal) => {\n        const res = await fetch(\"/api/workspace/ai\", { method: \"PATCH\", body: JSON.stringify({ enabled }), signal });\n        if (!res.ok) throw new Error(\"Could not save\");\n      }}\n      onTry={() => router.push(\"/editor?ai=1\")}\n      onLearnMore={() => window.open(\"/docs/ai\", \"_blank\")}\n    />\n  );\n}",
      "exports": [
        "AiFeatureLaunch",
        "LaunchStep"
      ]
    },
    {
      "name": "ai-search-bar",
      "kind": "block",
      "title": "AI search bar",
      "description": "A search card for an app or docs, where typing lists grouped results right away (Docs, Customers, Issues) and pressing Tab streams a short AI answer on top. The answer's numbered citations point to the results below it.",
      "tier": "pro",
      "builtFrom": [
        "Kbd",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-search-bar",
      "registry": null,
      "keywords": [
        "ai search",
        "search bar",
        "ask ai",
        "command palette",
        "docs search",
        "instant search",
        "grouped results",
        "keyboard navigation",
        "citations",
        "streaming answer",
        "algolia ask ai",
        "mintlify search",
        "linear search",
        "combobox",
        "rag"
      ],
      "whenToUse": [
        "A product or docs search where people mix finding a page or record with asking a question.",
        "Support and success tools that search docs, customers and tickets in one place.",
        "A command bar or search dialog that should offer an AI answer without leaving the results."
      ],
      "whenNotToUse": [
        "Use ai-answer for a full answer page with long formatted answers, follow ups and feedback.",
        "Use ai-chat for a back and forth conversation.",
        "Use ai-command for an action palette that runs commands rather than searches content."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The field is a combobox with aria-activedescendant, so focus stays in the field while the arrow keys move the active option. Results sit in a listbox with one labelled group per section; the Ask AI row is its own small listbox, and aria-controls names both.",
        "Each option has a full label: title, description, meta or issue status, and \"cited as source n\" once the answer cites it. The number badges are aria-hidden because the label carries them.",
        "The answer is a labelled region with aria-busy while it streams. A polite status region announces Asking AI, Answer ready with the source count, Answer closed, results opened and errors, and says Simulated when the demo stands in for a real model or link.",
        "Citations are buttons named \"Source n: title\"; focus lights the matching result just like hover. Highlights in titles are a visual aid only.",
        "Issue status is an icon plus a text label, never color alone. The error state shows an icon, the message and a Try again button.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Results update in the same frame as each keystroke with no animation, like a native search; the active row's background changes in 80ms.",
        "Switching between the empty view and results crossfades (out 100ms, in 160ms). The body has a fixed height, so the card never resizes.",
        "Asking AI crossfades the Ask AI row into the answer panel while the slot's height follows its content on a 240ms spring with no bounce.",
        "The status shimmers \"Thinking\" for about half a second, then \"Writing\", then settles to \"4 sources\". The answer streams on a requestAnimationFrame clock: every 34ms three to eight words commit together and fade in over 140ms, with no blur, travel or stagger.",
        "Number badges fade in on results as their citations arrive. Copy swaps its icon for a check in place.",
        "Reduced motion keeps the chunked arrival and every state but drops the fades, shimmer, pulse and the auto play loop."
      ],
      "responsive": [
        "The card is 100% wide up to 640px. Below about 520px the footer keeps only the Ask AI hint and the model, and below about 400px result descriptions hide so titles keep their room.",
        "Titles truncate after descriptions, which truncate first; meta stays right aligned in tabular numerals.",
        "The field uses 16px text so phones do not zoom on focus."
      ],
      "performance": [
        "Streamed text is split into stable units, so each tick only appends a new span and earlier text never re-renders.",
        "One requestAnimationFrame loop runs only while an answer streams, and an aborted ask or a new query stops it and the request at once.",
        "Auto play pauses while the card is off screen or the tab is hidden."
      ],
      "aiNotes": [
        "Choose it for in-app or docs search that should answer questions as well as find things: a support console, an admin app, a docs site, an internal knowledge base.",
        "Send `sources` before the text so badges can appear as citations stream. Cite only results the request carried; a cited id that is not on screen still shows its number but has nothing to jump to.",
        "Keep answers to two to four sentences; this is a search, not a chat. Link out to a full answer page (ai-answer) for longer replies.",
        "Results are grouped by `group` in the order of `groups`; return at most three to five per group so the answer and every section fit in the body.",
        "Brand marks and ARR figures are sample content. Pass your own `icon` per result; keep color brands in their colors and mono marks on currentColor."
      ],
      "related": [
        "ai-answer",
        "ai-command",
        "ai-chat",
        "ai-side-panel",
        "text-shimmer"
      ],
      "usage": "import { AiSearchBar, type AiSearchAnswerEvent } from \"@/components/arc/blocks/ai-search-bar/ai-search-bar\";\n\nasync function search(query: string, signal: AbortSignal) {\n  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });\n  return response.json(); // [{ id, group: \"docs\", title, description, meta }]\n}\n\nasync function* answer({ query, results, signal }) {\n  const response = await fetch(\"/api/ask\", { method: \"POST\", body: JSON.stringify({ query, ids: results.map(r => r.id) }), signal });\n  if (!response.ok) throw new Error(\"The assistant is busy. Try again in a moment.\");\n  for await (const event of readNdjson<AiSearchAnswerEvent>(response.body!)) yield event;\n}\n\nexport function AppSearch() {\n  return (\n    <AiSearchBar\n      search={search}\n      answer={answer}\n      recent={recentSearches}\n      suggestions={[\"How do I set up SAML SSO?\", \"How do I rotate an API key safely?\"]}\n      onSelect={result => router.push(result.href!)}\n      onAsk={query => track(\"search_ask_ai\", { query })}\n    />\n  );\n}",
      "exports": [
        "AiSearchBar"
      ]
    },
    {
      "name": "ai-setup-wizard",
      "kind": "block",
      "title": "AI setup wizard",
      "description": "A three-step setup card for turning on an AI feature. You connect Notion, Google Drive, Confluence or Zendesk and watch each one sync. Then you pick the model, tone and guardrails, and ask a test question whose answer streams with numbered citations before you go live.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "SegmentedControl",
        "Switch",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-setup-wizard",
      "registry": null,
      "keywords": [
        "AI setup wizard",
        "onboarding wizard",
        "stepper",
        "connect data sources",
        "knowledge base sync",
        "model picker",
        "guardrails",
        "test AI answer",
        "citations",
        "go live",
        "AI feature onboarding",
        "RAG setup"
      ],
      "whenToUse": [
        "Turning on an AI answers feature for a support, sales or internal knowledge team, like Intercom Fin, Zendesk AI or Notion Q&A.",
        "Onboarding admins to a copilot that needs data sources, a model choice and guardrails before it is enabled.",
        "A landing page or product tour section that shows the full setup in one short, looping clip."
      ],
      "whenNotToUse": [
        "Configuring an agent's instructions, tools and files in depth: use agent-builder.",
        "Only choosing a model: use model-picker.",
        "A full conversation after setup: use ai-chat. The test step answers one question at a time.",
        "Monitoring a running agent: use agent-status."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "The stepper is an ordered list of buttons; the current step has aria-current=\"step\" and completed steps say so. Steps not reached yet are disabled.",
        "Model cards are a radiogroup with roving focus; each radio is labelled with the full model name and note.",
        "A polite status region announces step changes, finished syncs, answers and going live, and says when the sample is simulated.",
        "Status is never color alone: synced rows show a check and \"Synced\", errors an alert icon and text, the header a Draft or Live label with its icon.",
        "Focus stays on the footer button across steps because the same button changes its label; Back keeps its space when hidden so nothing shifts."
      ],
      "motion": [
        "Steps slide 32px horizontally with a bounce free spring (visualDuration .24) and an opacity crossfade, inside a fixed height viewport, so the card never changes size.",
        "Stepper bars fill with scaleX and glyphs crossfade from number to check. Row meta, footer meta and status labels swap with one crossfade (100ms out, 160ms in, 2px rise).",
        "Answers stream three to six words every 34ms and each chunk only fades in; citations fade in after the stream with a 40ms stagger.",
        "The confirmation draws its circle and check with pathLength. Reduced motion drops travel, drawing and streaming (answers appear whole) and turns off the self playing demo."
      ],
      "responsive": [
        "Fills its container up to 600px. Below 480px the padding drops to 16px, stepper labels shorten to one word and model names may wrap to two lines.",
        "Below 400px the footer meta hides and only Back and the primary button remain; citation source names hide and titles truncate.",
        "Holds up at 360px with no horizontal scroll; the step viewport keeps one height at every width."
      ],
      "performance": [
        "Plain DOM and CSS; motion animates opacity, small transforms and scaleX only.",
        "The self playing loop only counts time while the card is on screen (IntersectionObserver) and stops for good on the first interaction.",
        "Streaming appends one chunk span every 34ms with a CSS fade, so earlier text never re-renders its animation."
      ],
      "aiNotes": [
        "Use it as the first run setup for an AI assistant, copilot or answer engine inside a SaaS product, where admins choose data, behavior and test before turning it on.",
        "Wire onConnect to your sync job and call report as it progresses; onAsk should call the real assistant with the config so the test reflects unsaved settings.",
        "Citation sourceIds must match WizardSource ids so the marks line up. Keep the answer under about 320 characters so it fits without scrolling.",
        "Marks are 20px square SVGs in rows and 14px in citations, and the header logo is 24px; mono marks should use currentColor so they follow the theme.",
        "Tabular figures are applied only to spans that hold nothing but a number (sync counts, percentages, citation numbers), so model names like GPT-6.1 keep normal spacing.",
        "Pass autoPlay={false} when embedding with real handlers if you also leave one of them out."
      ],
      "related": [
        "agent-builder",
        "model-picker",
        "ai-chat",
        "agent-status",
        "tool-call"
      ],
      "usage": "import { AiSetupWizard } from \"@/components/arc/blocks/ai-setup-wizard/ai-setup-wizard\";\n\nexport function TurnOnAnswers() {\n  return (\n    <AiSetupWizard\n      featureName=\"Ask Acme\"\n      onConnect={async (source, report, signal) => {\n        const job = await startSync(source.id, { signal });\n        for await (const progress of job.progress()) report(progress);\n      }}\n      onAsk={(question, config, signal) => askAssistant(question, config, { signal })}\n      onGoLive={config => saveAssistant({ ...config, enabled: true })}\n    />\n  );\n}",
      "exports": [
        "AiSetupWizard"
      ]
    },
    {
      "name": "smart-scheduler",
      "kind": "block",
      "title": "Smart scheduler",
      "description": "A scheduling card where one sentence like \"Find 30 min with Maya and Leo next week, mornings\" is read into chips. The three best slots are then ranked across everyone's Google Calendar and Outlook calendars, with an availability strip per person and conflicts called out. Picking a slot opens a draft invite with a Google Meet link, ready to send.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "CopyButton",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/smart-scheduler",
      "registry": null,
      "keywords": [
        "ai scheduling",
        "natural language scheduling",
        "find a time",
        "meeting scheduler",
        "availability",
        "free busy",
        "calendar assistant",
        "google calendar",
        "outlook",
        "google meet",
        "invite",
        "booking"
      ],
      "whenToUse": [
        "A scheduling assistant inside a CRM, recruiting tool, or team app that books internal meetings from one sentence.",
        "Showing why a time was suggested: who is free, what conflicts, and what is tentative.",
        "An AI feature demo where the result has to be checkable at a glance before anything is sent."
      ],
      "whenNotToUse": [
        "Public booking pages where guests pick from your open times; use availability-picker or booking-pill.",
        "Browsing or editing a full calendar; use week-calendar or month-calendar.",
        "Picking a single date or time without other people involved; use date-picker or time-picker."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The field is a labeled search form, the chips are a list named What was understood, and the slots are a list of buttons whose text carries the day, the time and the reason.",
        "Each strip is an image with a label listing that person's events in the window, with tentative ones marked, or free.",
        "Conflicts are never color alone: a warning icon plus a reason in words, and a dashed outline for tentative events.",
        "Reading, the parsed request, the number of times found, the draft and the send result are announced in a polite status region; the demo says nothing was sent.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Chips fade in from 2px, 40ms apart. Reading and checking calendars show a short shimmer on the label.",
        "The slot highlight and the strip column slide together on a critically damped spring (0.24s, no bounce) as the pointer or focus moves; busy blocks crossfade when the day changes.",
        "Suggestions, states and the draft share one fixed height and swap with a single crossfade (out 100ms, in 160ms), so the card never changes size. Send invite turns into Sent through the button's own label crossfade.",
        "The demo loop pauses off screen. Reduced motion drops the loop and the travel and opens on the ranked times."
      ],
      "responsive": [
        "Holds from 360px to 600px. Below about 460px the button keeps only its icon, chip details hide and the name column narrows; below 400px the people chip shows avatars only.",
        "Long names, reasons and links truncate with an ellipsis; nothing wraps, so rows keep their height."
      ],
      "performance": [
        "Pure React and CSS with motion for the highlight, column and crossfades. No canvas, no timers beyond the demo script, and the loop stops off screen.",
        "Strips render only the blocks inside the time window."
      ],
      "aiNotes": [
        "Use parse for the LLM step (structured output with duration, people ids, range, window) and keep findSlots deterministic: query free/busy (Google Calendar freeBusy, Microsoft Graph getSchedule) and score slots server side.",
        "Return busy blocks for every attendee on each slot's day so the strips can draw them; titles can be \"Busy\" when you only have free/busy access.",
        "The built in parseScheduleRequest and findScheduleSlots are exported and work on the sample calendars; they are a reference, not a production parser."
      ],
      "related": [
        "availability-picker",
        "week-calendar",
        "booking-pill",
        "meeting-summary",
        "smart-form-fill",
        "date-range-picker"
      ],
      "usage": "import { SmartScheduler } from \"@/components/arc/blocks/smart-scheduler/smart-scheduler\";\n\nexport function ScheduleCard({ me }) {\n  return (\n    <SmartScheduler\n      organizer={me}\n      defaultQuery=\"\"\n      timeZone=\"ET\"\n      parse={(query, signal) =>\n        fetch(\"/api/schedule/parse\", { method: \"POST\", body: JSON.stringify({ query }), signal }).then(async (res) => {\n          if (!res.ok) throw new Error((await res.json()).message ?? \"Couldn't read that request.\");\n          return res.json();\n        })}\n      findSlots={(request, signal) =>\n        fetch(\"/api/schedule/slots\", { method: \"POST\", body: JSON.stringify(request), signal }).then((res) => res.json())}\n      onSend={(invite, signal) =>\n        fetch(\"/api/schedule/invite\", { method: \"POST\", body: JSON.stringify(invite), signal }).then((res) => {\n          if (!res.ok) throw new Error(\"Not sent\");\n        })}\n    />\n  );\n}",
      "exports": [
        "SmartScheduler",
        "BusyBlock"
      ]
    },
    {
      "name": "task-breakdown",
      "kind": "block",
      "title": "Task breakdown",
      "description": "A goal input where AI breaks one sentence into a plan: tasks stream in with estimates, owners and dependencies, and a live timeline shows whether the deadline holds. Reorder, edit or regenerate any task, then send the plan to Linear or Jira with progress for each issue and links.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/task-breakdown",
      "registry": null,
      "keywords": [
        "task breakdown",
        "ai project plan",
        "goal to tasks",
        "linear export",
        "jira export",
        "estimates",
        "critical path",
        "timeline",
        "drag reorder",
        "ai planning",
        "work breakdown"
      ],
      "whenToUse": [
        "Turning a goal or brief into a scoped project plan inside a PM, CRM or internal tool.",
        "Exporting AI drafted work to Linear or Jira with a review step before anything is created.",
        "Showing whether a plan fits a deadline and updating that as people edit estimates."
      ],
      "whenNotToUse": [
        "Showing an agent working through steps live; use agent-plan.",
        "A full backlog or board with many projects; use a dedicated table or kanban.",
        "Single task creation from a prompt; a simple composer is lighter."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section; the goal input has a label and the task list is an ordered list named Tasks.",
        "A polite live region announces planning, drafted counts, saves, moves (\"Moved to position 3 of 7\"), rewrites and issue creation, and says when an action was simulated.",
        "Each grip is a button named with the task and its position, and explains the arrow keys; row actions are named with the task number.",
        "Status never relies on color: late plans and blocked dependencies pair a warning icon with text, created issues show their key.",
        "Rows that are streaming or regenerating set aria-busy; the timeline bar has a text label with the ship date and the deadline."
      ],
      "motion": [
        "Rows stream three words per 36ms tick, each chunk fading in over 140ms; meta fades in when the title completes. The thinking shimmer lasts under a second.",
        "Reordering uses bounce-free springs (visualDuration .24, bounce 0) on position only, so text is never scaled.",
        "Titles, estimates, owners, dependency labels and footer status swap with one crossfade (100ms out, 160ms in, 2px rise).",
        "Timeline segments glide their left and width on the same bounce-free spring; the list area reserves seven rows so the card never changes height between empty, drafting and ready.",
        "Reduced motion skips streaming and travel and keeps every end state and status."
      ],
      "responsive": [
        "The card fills its container up to 640px and holds at 360px without horizontal scroll.",
        "Below 520px container width the dependency column hides; below 440px the footer buttons shorten to the tracker name with their marks.",
        "Titles truncate with an ellipsis; row actions are always visible on touch screens."
      ],
      "performance": [
        "Streaming is one interval and one state update per tick; it stops as soon as the last row lands.",
        "The schedule is a memoized dependency walk, linear in the number of tasks.",
        "Auto start waits for an IntersectionObserver and runs once; nothing loops off screen."
      ],
      "aiNotes": [
        "Choose this when a user states a goal and needs a plan with estimates, owners and dependencies they can push to a tracker; it is a planning surface, not an execution log.",
        "Wire generate to a model call that returns TaskDraft[] with stable ids and dependsOn ids; the block computes the critical path and ship date itself.",
        "createIssue is called sequentially per task, so issue keys come back in list order; return the tracker's real key and URL.",
        "Owners are matched by ownerId; pass your team as owners with portrait URLs.",
        "For an agent that executes steps after approval use agent-plan instead."
      ],
      "related": [
        "agent-plan",
        "agent-tasks",
        "tool-call",
        "agent-status"
      ],
      "usage": "import { TaskBreakdown } from \"@/components/arc/blocks/task-breakdown/task-breakdown\";\n\nexport function PlanFromGoal() {\n  return (\n    <TaskBreakdown\n      deadline=\"2026-12-01\"\n      generate={async (goal, signal) => {\n        const res = await fetch(\"/api/plan\", { method: \"POST\", body: JSON.stringify({ goal }), signal });\n        return res.json();\n      }}\n      createIssue={async (target, task, signal) => {\n        const res = await fetch(`/api/${target}/issues`, { method: \"POST\", body: JSON.stringify(task), signal });\n        return res.json(); // { key: \"BIL-241\", url: \"https://linear.app/...\" }\n      }}\n    />\n  );\n}",
      "exports": [
        "TaskBreakdown"
      ]
    },
    {
      "name": "call-insights",
      "kind": "block",
      "title": "Call insights",
      "description": "A sales call insights card where talk time, flagged pricing, competitor and objection moments, an AI summary and owned next steps sit in one view. Selecting a moment plays its quote, and checking off next steps lowers the deal risk.",
      "tier": "pro",
      "builtFrom": [
        "Avatar",
        "Checkbox",
        "CopyButton",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/call-insights",
      "registry": null,
      "keywords": [
        "call insights",
        "sales call",
        "conversation intelligence",
        "gong",
        "talk time",
        "talk ratio",
        "call recording",
        "deal risk",
        "next steps",
        "call summary",
        "objection",
        "competitor mention"
      ],
      "whenToUse": [
        "A deal page that shows the latest sales call with talk ratio, flagged moments, summary and next steps.",
        "A call review queue for sales managers who coach on talk time and objection handling.",
        "A research or support tool that recaps a recorded customer call with jump-to-quote clips."
      ],
      "whenNotToUse": [
        "Full transcript reading or editing: use transcript-studio.",
        "An internal meeting recap without a prospect or deal: use meeting-summary.",
        "A pipeline overview across many deals: use a table such as crm-contacts."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-checkbox"
      ],
      "accessibility": [
        "The card is a section named after the company. Markers are a radio group named Key moments; each radio names the type, timestamp and speaker, so type never relies on the icon alone.",
        "The waveform is a slider with the clip position as value text (0:03 of 0:07).",
        "Deal risk is an icon plus the words High, Medium or Low risk. Talk time is an image whose name lists every speaker and share.",
        "Playback, pauses, load failures and next step changes (with any change in risk) are announced in a polite status region, which also says clips are simulated with no audio.",
        "The summary is aria-busy while it streams and only exposed once complete; the thinking label is hidden from assistive tech.",
        "Pointer focus draws no ring; keyboard focus shows Arc's shared focus ring."
      ],
      "motion": [
        "Selecting a moment fills the rail up to it on one 240ms ease-out transform and crossfades the quote (old out in 100ms, new in over 160ms from 2px). All quotes share one grid cell, so the well never changes height.",
        "The waveform reshapes per clip with a 240ms height ease, and the played part is clipped in from a CSS variable each frame, so playback never re-renders React per frame.",
        "The summary waits 0.7s on a calm shimmer, then streams 3 to 8 words every 35ms with each chunk fading in over 140ms. Talk time reveals left to right once. The risk chip crossfades its icon and label inside the width of its longest label.",
        "The tour pauses while the card is off screen and stops at the first interaction. Nothing bounces.",
        "Reduced motion skips the tour, streaming and reveals and drops travel; selection, playback and every state and announcement stay."
      ],
      "responsive": [
        "Fluid from 360px to 680px. Container queries move the talk legend to two columns and hide the attendee stack below 560px; below 460px sections pad 16px and the company name, platform name, type legend, speaker title and owner names drop, leaving marks and avatars.",
        "Markers keep a 40px hit area and clamp inside the rail at both ends. Checkboxes keep their 44px target."
      ],
      "performance": [
        "Playback writes one CSS variable per frame and re-renders only when the whole second changes. The tour and playback stop off screen through an IntersectionObserver.",
        "Waveforms are 52 bars per layer, computed once per moment."
      ],
      "aiNotes": [
        "Use for a recorded call, demo or interview where the reader needs the gist and the moments that matter: sales call reviews, CRM deal pages, customer research recaps.",
        "Feed moments from your transcription or conversation intelligence pipeline; keep quotes under about 20 words so the well stays two lines.",
        "Wire loadClip to seek your audio player to moment.at and resolve when playback can start; abort on the signal.",
        "Pass risk when your own model scores deals; leave it unset to tie risk to the open next steps."
      ],
      "related": [
        "meeting-summary",
        "lead-scoring",
        "account-health"
      ],
      "usage": "import { CallInsights } from \"@/components/arc/blocks/call-insights/call-insights\";\n\nexport function DealCall({ call, transcript }) {\n  return (\n    <CallInsights\n      call={{ title: \"Data platform evaluation\", company: \"Ramp\", date: \"Oct 22, 2026\", duration: 2052 }}\n      speakers={transcript.speakers}\n      moments={transcript.moments}\n      summary={transcript.summary}\n      nextSteps={transcript.nextSteps}\n      autoPlay={false}\n      loadClip={(moment, signal) => audio.load(call.recordingUrl, moment.at, { signal })}\n      onNextStepChange={(step, done) => fetch(`/api/steps/${step.id}`, { method: \"PATCH\", body: JSON.stringify({ done }) })}\n    />\n  );\n}",
      "exports": [
        "CallInsights",
        "deriveRisk"
      ]
    },
    {
      "name": "lead-scoring",
      "kind": "block",
      "title": "Lead scoring",
      "description": "A lead scoring card where inbound B2B leads are ranked by an AI fit score, each with a score ring and a one-line reason. Open a lead to see its signals and score breakdown, then push it to HubSpot or Salesforce.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "SegmentedControl",
        "TextShimmer",
        "Tooltip"
      ],
      "docs": "https://uiarc.dev/components/blocks/lead-scoring",
      "registry": null,
      "keywords": [
        "lead scoring",
        "ai lead scoring",
        "inbound leads",
        "sales qualification",
        "fit score",
        "crm",
        "hubspot",
        "salesforce",
        "sdr queue",
        "b2b sales"
      ],
      "whenToUse": [
        "A sales inbox or SDR queue that ranks new inbound leads by fit.",
        "Showing why a model scored a lead, with signals and a weighted breakdown.",
        "Handing a qualified lead to HubSpot or Salesforce from the same view."
      ],
      "whenNotToUse": [
        "Large tables of hundreds of leads with filters and bulk edits; use a data table.",
        "Enriching columns of company data; use enrich-column.",
        "Tracking the health of existing customers; use account-health."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-tooltip"
      ],
      "accessibility": [
        "Each row is a button with aria-expanded and aria-controls; its label reads name, title, company, score, fit band and the reason.",
        "The score ring is decorative; the number and the fit band are in the row label and the breakdown list label.",
        "Status is never color alone: done and error states pair an icon with a short label.",
        "The CRM picker is a radio group with roving focus and named options.",
        "A polite live region announces pushes, errors and rescoring, and says when a push was simulated."
      ],
      "motion": [
        "Re-sorting glides each row to its new place with a bounce-free spring (position only, text is never scaled).",
        "The ring arc and number share one motion value, so they count up together in about 640ms with tabular numerals.",
        "Opening a row springs the measured height with no bounce; breakdown bars grow with a 30ms stagger.",
        "Time, synced, reason and status swaps are single crossfades in a fixed slot, so nothing changes size.",
        "The scoring spinner pauses off screen. Reduced motion keeps every end state and drops travel, counting and the spinner."
      ],
      "responsive": [
        "Fits 360px to 640px. Below 460px the job title leaves the row, details start at the card edge and the push button fills the row.",
        "Names, roles and reasons truncate with an ellipsis; scores and times keep fixed slots."
      ],
      "performance": [
        "Designed for a short queue of 5 to 20 leads; every row is rendered.",
        "Ring counting runs on motion values without React renders."
      ],
      "aiNotes": [
        "Use for ranking inbound leads, trial signups or accounts by an AI fit score with a reason and a CRM handoff.",
        "Pass leads newest first; let the score come from a weighted breakdown so the bars and the ring always agree.",
        "Keep the reason to one short line of evidence (funding stage, team size, intent), not a paragraph.",
        "Wire onPush to your backend; never call a CRM from the client with secret keys."
      ],
      "related": [
        "account-health",
        "enrich-column",
        "call-insights",
        "agent-approvals",
        "connector-setup"
      ],
      "usage": "import { LeadScoring } from \"@/components/arc/blocks/lead-scoring/lead-scoring\";\n\nexport function InboundLeads({ leads }: { leads: Lead[] }) {\n  return (\n    <LeadScoring\n      leads={leads}\n      onPush={async (lead, crm, signal) => {\n        const res = await fetch(`/api/crm/${crm}/leads`, { method: \"POST\", body: JSON.stringify({ id: lead.id }), signal });\n        if (!res.ok) throw new Error(\"Push failed\");\n      }}\n    />\n  );\n}",
      "exports": [
        "LeadScoring",
        "Lead"
      ]
    },
    {
      "name": "domain-verify",
      "kind": "block",
      "title": "Domain verify",
      "description": "A custom domain setup card where customers add a domain and copy the DNS records. A live checker then polls resolvers, marks each record valid, not found or misconfigured with a one-line fix, and finishes with the domain verified and the SSL certificate issued.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "CopyButton",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/domain-verify",
      "registry": null,
      "keywords": [
        "custom domain",
        "domain verification",
        "dns records",
        "cname",
        "txt record",
        "caa",
        "ssl certificate",
        "propagation",
        "verify domain",
        "sending domain",
        "dkim",
        "vercel domains",
        "resend domains"
      ],
      "whenToUse": [
        "Letting customers serve an app, docs site, status page or storefront from their own domain.",
        "Verifying a sending domain for email (SPF, DKIM, DMARC) before turning on delivery.",
        "Any setup step where a person must copy values into a third party and wait for propagation."
      ],
      "whenNotToUse": [
        "Buying or searching domains: use a search field with results instead.",
        "Managing many domains at once: use a table with one row per domain and open this card per domain.",
        "One-off copy of a single value: a copy-button next to the value is enough."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section; the records are a list labelled DNS records.",
        "A polite live region announces each step (added, checking, needs a fix, verified, certificate issued) and adds \"Simulated, no DNS was queried\" when no verify handler is passed.",
        "Status is never color alone: every record and the domain show an icon with a short label.",
        "The domain field is labelled, marks itself aria-invalid on a bad domain and is described by the footer status.",
        "Placeholder rows behind the empty state are inert, so their copy buttons are skipped by keyboard and screen readers.",
        "Each copy button has its own label, such as \"Copy TXT value\", and announces the copy."
      ],
      "motion": [
        "Text and icon swaps are single crossfades: the old one fades out in 100ms, the new one fades in over 160ms with a 2px rise.",
        "Rows fade in over 160ms with a 40ms stagger when the records arrive. No row, column or slot changes size between states.",
        "When a fix hint wraps on a narrow card, the card eases to its measured height on a spring with no bounce.",
        "Checking rows show a steady spinner, and the footer label carries a calm shimmer while checks run.",
        "Reduced motion keeps every state and label, swaps instantly and stops the spinner. The demo loop pauses off screen."
      ],
      "responsive": [
        "Fills its container up to 680px and adapts with a container query, not the viewport.",
        "Below about 544px the header row hides and each record stacks: type, name and status on one line, the value with its copy button below, then the meta line.",
        "On narrow cards the domain meta shortens (\"DNS at Cloudflare\", \"SSL by Let's Encrypt\") so the status never collides.",
        "Long names and values truncate with an ellipsis and keep the full text in a title; copy always copies the full value."
      ],
      "performance": [
        "Plain DOM and CSS; motion is limited to opacity and a measured height spring.",
        "Every poll is abortable: removing the domain, starting a new check or unmounting aborts the in-flight request and timer.",
        "The demo pauses its timers when the card is off screen."
      ],
      "aiNotes": [
        "Use for custom domain, sending domain or verified domain setup in a SaaS settings page. Wire getRecords, verify and issueCertificate to your backend; keep verify a single check, the card handles polling and aborts.",
        "Return `missing` while a record is still propagating and `invalid` only when a conflicting value is found, with `found` and a one sentence `fix`.",
        "Pass `provider` from your DNS lookup (NS records) so the hint can name the provider; pass null when unknown.",
        "For email sending domains, return MX, SPF (TXT) and DKIM (CNAME or TXT) records from getRecords and set a title like \"Sending domain\"."
      ],
      "related": [
        "copy-button",
        "button",
        "text-shimmer",
        "agent-status",
        "tool-call"
      ],
      "usage": "import { DomainVerify } from \"@/components/arc/blocks/domain-verify/domain-verify\";\n\nexport function DomainSettings({ projectId }: { projectId: string }) {\n  return <DomainVerify\n    defaultDomain=\"\"\n    provider={null}\n    getRecords={(domain, signal) => fetch(`/api/projects/${projectId}/domains`, { method: \"POST\", body: JSON.stringify({ domain }), signal }).then(r => r.json())}\n    verify={(domain, _records, { signal }) => fetch(`/api/projects/${projectId}/domains/${domain}/verify`, { signal }).then(r => r.json())}\n    issueCertificate={(domain, signal) => fetch(`/api/projects/${projectId}/domains/${domain}/certificate`, { method: \"POST\", signal }).then(r => r.json())}\n    onVerified={domain => console.log(`${domain} is live`)}\n  />;\n}",
      "exports": [
        "DomainVerify"
      ]
    },
    {
      "name": "two-factor-setup",
      "kind": "block",
      "title": "Two-factor setup",
      "description": "A two-factor setup card where you scan a real, working authenticator QR code, type the 6-digit code into slots that move to the next box on their own, and save your backup codes before it turns on. Every step shares one fixed-size card, so it moves between steps with a short slide and never changes size.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Checkbox",
        "Copy button"
      ],
      "docs": "https://uiarc.dev/components/blocks/two-factor-setup",
      "registry": null,
      "keywords": [
        "two-factor authentication",
        "2fa setup",
        "totp",
        "authenticator app",
        "qr code",
        "google authenticator",
        "1password",
        "backup codes",
        "recovery codes",
        "mfa enrolment",
        "otp input",
        "account security"
      ],
      "whenToUse": [
        "Turning on authenticator app two-factor from account or workspace security settings.",
        "Enforced two-factor onboarding for admins after sign-in.",
        "Re-enrolling a new phone, with fresh backup codes."
      ],
      "whenNotToUse": [
        "SMS or email one-time codes only: use otp-input in a sign-in form.",
        "Passkeys or WebAuthn enrolment, which needs no QR or code step.",
        "A full security settings page with sessions and password: use security-settings."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-checkbox"
      ],
      "accessibility": [
        "The card is a labelled section; each step has its own heading, and inactive steps are inert and hidden from assistive tech.",
        "The QR code is an SVG with role img and a label naming the issuer and account; the setup key is selectable text with a copy button for people who cannot scan.",
        "Code slots are a labelled group with per-digit labels, autocomplete one-time-code on the first slot, and aria-invalid plus a role alert message on a mismatch.",
        "Errors use an icon and text, never color alone. A polite live region announces verification, downloads and whether anything was simulated.",
        "Moving to the verify step places focus in the first empty slot; the primary button keeps focus across steps."
      ],
      "motion": [
        "All steps share one grid cell, so the card never changes size. The outgoing step fades in 100ms; the incoming one fades and slides 12px on a no-bounce spring (0.24s) in the direction of travel.",
        "Digits fade in with a 2px rise; the error state changes slot borders and swaps the hint for the message in one crossfade, with no shake.",
        "The countdown ring drains in sync with the real 30 second TOTP window using a negative animation delay and only ticks while the verify step is visible on screen.",
        "Progress segments fill on a 240ms ease-out; the done check draws itself once.",
        "Reduced motion keeps the crossfades and end states, drops the slide, the caret blink and the ring animation."
      ],
      "responsive": [
        "Fluid up to 34rem. Below a 460px card width the QR shrinks to 112px beside the copy, apps and setup key go full width, and backup codes move to two columns.",
        "Code slots shrink evenly down to a 360px viewport; the 44px checkbox hit area is offset so its square aligns with the text above.",
        "Container queries, so it adapts inside dialogs and side panels as well as pages."
      ],
      "performance": [
        "The QR is encoded once per otpauth value and drawn as a single merged path plus three eyes.",
        "TOTP uses Web Crypto HMAC-SHA1; the clock ticks once per second only on the verify step and pauses off screen."
      ],
      "aiNotes": [
        "Use for enabling authenticator app (TOTP) two-factor in account security settings. Pass the server-generated secret and backup codes; never generate secrets in the browser for production.",
        "Always pass verifyCode in production so the demo's simulated authenticator entry and local check are removed.",
        "Place it in a settings page or a dialog around 480 to 560px wide; it stacks the QR beside a narrower column below 460px.",
        "For pairing a device by scanning instead of typing a code, use qr-pairing; for a whole security page with password and sessions, use security-settings."
      ],
      "related": [
        "security-settings",
        "otp-input",
        "copy-button",
        "checkbox"
      ],
      "usage": "import { TwoFactorSetup } from \"@/components/arc/blocks/two-factor-setup/two-factor-setup\";\n\nexport function SecurityTwoFactor({ setup }: { setup: { secret: string; codes: string[] } }) {\n  return (\n    <TwoFactorSetup\n      issuer=\"Acme\"\n      account=\"dana@acme.com\"\n      secret={setup.secret}\n      backupCodes={setup.codes}\n      verifyCode={async (code, signal) => {\n        const res = await fetch(\"/api/2fa/verify\", { method: \"POST\", body: JSON.stringify({ code }), signal });\n        return res.ok;\n      }}\n      onEnable={signal => fetch(\"/api/2fa/enable\", { method: \"POST\", signal }).then(() => undefined)}\n      onDone={() => router.push(\"/settings/security\")}\n    />\n  );\n}",
      "exports": [
        "TwoFactorSetup"
      ]
    },
    {
      "name": "onboarding-survey",
      "kind": "block",
      "title": "Onboarding survey",
      "description": "A post-signup onboarding survey where each question gets its own step of large option cards, picked with keys 1 to 4 and Enter. A short personalization checklist then ticks off and lands on an editable \"You're all set\" summary.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Kbd",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/onboarding-survey",
      "registry": null,
      "keywords": [
        "onboarding survey",
        "welcome survey",
        "post signup",
        "personalization",
        "onboarding questions",
        "role picker",
        "team size",
        "option cards",
        "multi step form",
        "typeform style",
        "workspace setup",
        "keyboard shortcuts"
      ],
      "whenToUse": [
        "Post-signup personalization: role, team size and first action before the first screen.",
        "Short in-product surveys where every answer changes what someone sees next.",
        "Re-onboarding when a workspace changes plan or adds a team."
      ],
      "whenNotToUse": [
        "Long forms with free text or many fields; use a regular form or import-wizard.",
        "Guided tours over the real interface; use onboarding-tour.",
        "A single choice inside a settings page; use radio-cards."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each question is a radiogroup labelled by its title; options are radios with aria-checked and aria-keyshortcuts, and one roving tab stop.",
        "The progress bar is a progressbar with a percentage value.",
        "A polite live region announces picks, the next question, setup progress and the result, including \"Simulated, nothing was saved\" in the demo.",
        "When a step changes, focus moves to the new step's selected or first option so keyboard flow never drops to the page.",
        "Checklist rows pair every state with an icon and text: a spinner, a check with a hidden Done label, or an alert with Failed.",
        "Hidden controls (Back on the first question, Skip after the last) keep their space but leave the tab order and accessibility tree."
      ],
      "motion": [
        "Steps crossfade: the old step fades out in 100ms while the new one fades in over 160ms with a 2px rise.",
        "The stage springs to the incoming step's measured height with no bounce, so the footer glides instead of jumping.",
        "The progress fill scales from the left with a bounce-free spring; option checks and checklist icons swap with a short scale and fade.",
        "Number keys and Enter press their key caps and the main button for 150ms, so pointer and keyboard both get instant feedback.",
        "Reduced motion keeps the crossfades and end states, drops the rise, the height spring and the sample loop."
      ],
      "responsive": [
        "Up to 560px wide. Above 440px the options sit in a 2 by 2 grid of cards; below, each option becomes one compact row with icon, label, description and key.",
        "Labels and descriptions truncate with an ellipsis instead of wrapping, so every option keeps one height.",
        "Holds at 360px with 16px padding and no horizontal scroll; touch targets stay at least 40px tall."
      ],
      "performance": [
        "One component with local state; only the current step is mounted, plus the outgoing one during its 100ms fade.",
        "The sample run uses a single timeout at a time and pauses when the card leaves the viewport.",
        "Brand marks are inline SVG; no images or network requests."
      ],
      "aiNotes": [
        "Use right after sign-up to collect two to four answers that change the first screen. Keep each question to four options so keys 1 to 4 and the 2 by 2 grid hold.",
        "Pass personalize for the real work (create views, seed templates, invite flow) and keep setupSteps labels specific to the answers.",
        "Store answers from onAnswer as they come so a skipped or abandoned survey still informs the workspace.",
        "Option icons are 16px; lucide icons at strokeWidth 1.75 and mono brand marks in currentColor match the sample."
      ],
      "related": [
        "ai-setup-wizard",
        "invite-people",
        "signup-form",
        "radio-cards",
        "stepper"
      ],
      "usage": "import { OnboardingSurvey } from \"@/components/arc/blocks/onboarding-survey/onboarding-survey\";\n\nexport function Welcome() {\n  return (\n    <OnboardingSurvey\n      questions={questions}\n      onAnswer={(id, value) => analytics.track(\"onboarding_answer\", { id, value })}\n      personalize={async (answers, signal) => {\n        await fetch(\"/api/workspace/personalize\", { method: \"POST\", body: JSON.stringify(answers), signal });\n      }}\n      onComplete={() => router.push(\"/inbox\")}\n    />\n  );\n}",
      "exports": [
        "OnboardingSurvey"
      ]
    },
    {
      "name": "webhook-tester",
      "kind": "block",
      "title": "Webhook tester",
      "description": "A developer webhook tester where you pick an event, edit its JSON payload and send a signed test event. You watch it go from signed to sent to 200 OK in 184 ms, then read the response body, headers and exact request, and retry failed deliveries from a recent list.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "CopyButton",
        "Kbd",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/webhook-tester",
      "registry": null,
      "keywords": [
        "webhook tester",
        "test webhook",
        "send test event",
        "webhook endpoint",
        "signing secret",
        "hmac signature",
        "stripe webhook",
        "json editor",
        "request timeline",
        "delivery log",
        "retry webhook",
        "developer settings",
        "api",
        "event payload"
      ],
      "whenToUse": [
        "A developer settings page where customers test a webhook endpoint before going live.",
        "Integration onboarding that asks people to prove their handler returns 2xx and verifies signatures.",
        "Internal tools for replaying failed deliveries with a retry."
      ],
      "whenNotToUse": [
        "Managing many endpoints, filters and event subscriptions: use the webhooks block and link here for testing one.",
        "A general HTTP client for arbitrary methods and headers: this always sends a signed POST with a JSON body.",
        "Live event logs with thousands of rows: the list is for a few recent attempts, not a paginated log."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Every field has a visible label; the payload textarea is labelled with the event type and reports aria-invalid when the JSON does not parse, with the line named in the footer.",
        "The signing secret is hidden from assistive technology until revealed; the reveal button is a toggle with aria-pressed and a label that says what it will do.",
        "Response tabs follow the tabs pattern with roving tabindex; the panel is focusable so long bodies can be scrolled from the keyboard.",
        "Delivery rows are buttons labelled with event, status and time, and mark the selected one with aria-current. Status chips pair an icon and a code or word, never color alone.",
        "A polite live region announces each send and its outcome, and adds \"Simulated, nothing was sent\" when no onSend is wired."
      ],
      "motion": [
        "The timeline runs signed, sent, response: icons crossfade, the connector fills left to right and a small dot travels along it while the request is in flight, looping on the last leg until the response arrives.",
        "Text swaps (node labels, status chips, hints, the reveal icon) are a single crossfade: 100ms out, 160ms in with a 2px rise, in fixed slots so nothing shifts.",
        "A new delivery fades in at the top while the rows below glide down with a bounce free spring; the response panel crossfades between tabs and deliveries.",
        "Reduced motion keeps every end state and status change, drops the traveling dot, the fills and the glides, and slows the spinner."
      ],
      "responsive": [
        "Two columns from 680px of container width, stacked below; the payload editor stretches to match the response column when side by side.",
        "At 360px the delivery time hides and long event names truncate with an ellipsis; nothing scrolls horizontally except a long payload line inside the editor.",
        "Every target is at least 28px tall, and rows are 40px."
      ],
      "performance": [
        "Highlighting is a single regex pass over the payload on each keystroke; fine for typical webhook bodies (under a few hundred lines).",
        "Signing uses WebCrypto (crypto.subtle) and falls back to a placeholder where it is unavailable, such as a non secure context.",
        "The traveling dot is a transform only animation and stops when no request is in flight; the autoplay observer disconnects after its single run."
      ],
      "aiNotes": [
        "Use for the Test endpoint screen of a product that sends webhooks (billing, auth, email, CI): pass your own events and payloads, the endpoint's secret and an onSend that calls your backend, which performs the real POST and returns status, timing, headers and body.",
        "Never send the request from the browser to a customer endpoint; the request object is meant for your server, which can verify it re-signs identically.",
        "For a non Stripe product pass source (your product name and mark) and signatureHeader, such as \"webhook-signature\".",
        "Card is 760px max and splits into request and response columns from 680px; below that it stacks."
      ],
      "related": [
        "webhooks",
        "api-keys",
        "tool-call",
        "copy-button",
        "text-shimmer"
      ],
      "usage": "import { WebhookTester } from \"@/components/arc/blocks/webhook-tester/webhook-tester\";\n\nexport function EndpointTest({ endpointId }: { endpointId: string }) {\n  return (\n    <WebhookTester\n      defaultEndpoint=\"https://api.acme.com/webhooks/billing\"\n      secret={signingSecret}\n      onSend={async (request, signal) => {\n        const res = await fetch(`/api/endpoints/${endpointId}/test`, { method: \"POST\", body: JSON.stringify(request), signal });\n        return res.json(); // { status, durationMs, headers, body }\n      }}\n    />\n  );\n}",
      "exports": [
        "WebhookTester"
      ]
    },
    {
      "name": "seat-manager",
      "kind": "block",
      "title": "Seat manager",
      "description": "A team seats and billing card where avatars and a seat meter show members, invites and open seats against what you pay for, a stepper rolls the prorated charge for today and the new monthly total live, and changes go through a confirm step before anyone is charged. Pending invites can be resent or revoked with undo, and when every seat is taken the card says so and offers to add one.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Avatar",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/seat-manager",
      "registry": null,
      "keywords": [
        "seat management",
        "team seats",
        "per seat billing",
        "seat stepper",
        "proration",
        "prorated charge",
        "pending invites",
        "resend invite",
        "revoke invite",
        "seat limit",
        "team billing",
        "subscription quantity",
        "stripe seats"
      ],
      "whenToUse": [
        "Letting a team admin add or remove paid seats with the price impact visible before paying.",
        "Showing seat usage against purchased seats, including invites that reserve a seat.",
        "Handling the seat limit at the moment an admin hits it, with a one tap way to add a seat."
      ],
      "whenNotToUse": [
        "Managing roles and permissions for many members: use a member table and link to this card for seats.",
        "Usage based or credit billing: use credit-topup or a usage meter instead.",
        "Plan comparison or upgrades between tiers: use a pricing table."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section. The seat count is a spinbutton with min, max, value and a spoken value such as \"15 seats\".",
        "The seat meter is a single image with a full text label (used, members, invited, open, being added); the legend repeats the counts in text, so color is never the only signal.",
        "Limits stay explained, not silent: pressing past the minimum or the plan cap keeps the button focusable (aria-disabled) and shows why in the notice line.",
        "A polite live region announces review, update, resend, revoke and undo, and adds \"Simulated, nothing was charged\" (or sent) when no handlers are passed.",
        "Rolling numbers render the real value for screen readers and hide the digit strips.",
        "Every invite action has its own label, such as \"Revoke invite to kai.tanaka@halcyon.so\"."
      ],
      "motion": [
        "Numbers roll: each digit is a 0 to 9 strip that slides 240ms on an ease-out, with tabular numerals in a right-aligned slot so nothing shifts.",
        "Seat cells grow in from zero width and shrink out on a 240ms spring with no bounce; neighbours glide to make room.",
        "Text, icon and button swaps are single crossfades: old out in 100ms, new in over 160ms with a 2px rise.",
        "A revoked invite crossfades to an undo row, then collapses its measured height on a bounce-free spring.",
        "Press and hold on plus or minus repeats after 380ms, then every 70ms.",
        "Reduced motion keeps every state and number, swaps instantly and drops the rolls and cell growth. The demo loop pauses off screen."
      ],
      "responsive": [
        "Fills its container up to 560px and adapts with a container query, not the viewport.",
        "Below about 460px the padding tightens, the quote rows drop their muted notes and the legend hides the new seats item, which the notice line already states.",
        "Emails, notes and footer text truncate with an ellipsis; amounts and counts never wrap.",
        "Past 40 seats the meter groups cells into one segment per kind so it stays readable."
      ],
      "performance": [
        "Plain DOM and CSS; digit rolls are CSS transforms and cell growth is a flex-grow spring.",
        "Every request is abortable and aborted on unmount; timers are cleared on unmount.",
        "The demo loop pauses when the card is off screen."
      ],
      "aiNotes": [
        "Use for the seats or members section of a team billing page in any per-seat SaaS. Pass the subscription quantity as purchased and the real billing period so proration matches your processor.",
        "Proration here is a simple daily estimate. If your processor computes it (Stripe upcoming invoice preview), call it in onQuoteChange and pass the result to your confirm copy, or replace the numbers server side before charging.",
        "Pending invites hold seats, so the minimum is members plus invites. Revoking frees a seat at once with undo; the revoke handler runs after the undo window.",
        "Reject updateSeats with a human sentence (\"Your card was declined. Nothing was charged.\"); it is shown verbatim with Try again."
      ],
      "related": [
        "billing-overview",
        "credit-topup",
        "button",
        "avatar",
        "text-shimmer",
        "checkout-summary"
      ],
      "usage": "import { SeatManager } from \"@/components/arc/blocks/seat-manager/seat-manager\";\n\nexport function TeamBilling({ team }: { team: Team }) {\n  return <SeatManager\n    workspace={team.name}\n    plan={{ name: \"Pro\", pricePerSeat: 16, interval: \"month\", maxSeats: 25 }}\n    purchased={team.seats}\n    members={team.members.map(m => ({ id: m.id, name: m.name, avatar: m.imageUrl }))}\n    invites={team.invites.map(i => ({ id: i.id, email: i.email, invitedBy: i.inviter, note: i.sentAgo }))}\n    period={{ start: team.periodStart, end: team.periodEnd }}\n    paymentMethod={{ brand: \"Visa\", last4: team.cardLast4 }}\n    updateSeats={(quote, signal) => fetch(\"/api/billing/seats\", { method: \"POST\", body: JSON.stringify({ seats: quote.target }), signal }).then(r => { if (!r.ok) throw new Error(\"Your card was declined. Nothing was charged.\"); })}\n    resendInvite={(invite, signal) => fetch(`/api/invites/${invite.id}/resend`, { method: \"POST\", signal }).then(() => undefined)}\n    revokeInvite={(invite, signal) => fetch(`/api/invites/${invite.id}`, { method: \"DELETE\", signal }).then(() => undefined)}\n  />;\n}",
      "exports": [
        "SeatManager",
        "buildQuote"
      ]
    },
    {
      "name": "account-health",
      "kind": "block",
      "title": "Account health",
      "description": "A customer success card where a health score gauge, a renewal countdown and five sourced signals with sparklines lead into an AI risk summary and one recommended next step, like scheduling a QBR before usage slips further.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "SegmentedControl",
        "TextShimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/account-health",
      "registry": null,
      "keywords": [
        "account health",
        "customer health score",
        "customer success",
        "churn risk",
        "renewal",
        "QBR",
        "NPS",
        "seat utilization",
        "health gauge",
        "AI summary",
        "CSM",
        "sparkline"
      ],
      "whenToUse": [
        "A customer success or account page that needs a score, the signals behind it and a next step.",
        "Renewal prep views where the countdown and risk drivers matter more than raw metrics.",
        "Showing an AI written risk summary next to the evidence it was written from."
      ],
      "whenNotToUse": [
        "Portfolio views across many accounts; use a table with a score column instead.",
        "Deep metric exploration with axes and ranges; use a full chart.",
        "Single KPI tiles with no recommendation; use a stat card."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled section; the gauge is one image with the score and band in its label.",
        "Every status pairs an icon with a word (Good, Watch, At risk, Needs attention), never color alone.",
        "Signal sources are named for screen readers; sparklines are decorative and hidden.",
        "A polite live region reports the action result and says when it was simulated (\"Simulated, no invite was sent\").",
        "The summary is aria-busy while it is written; the action row stays inert until it appears."
      ],
      "motion": [
        "On first view the gauge sweeps and the score counts up from one motion value (560ms ease-out), sparklines draw in with a 40ms stagger, and the term bar fills.",
        "The summary thinks for about 0.9s with a shimmer on the label, then streams 3 to 6 words every 34ms, each chunk fading in over 140ms.",
        "Switching the time range morphs each sparkline point to point and crossfades the trend line; no element changes size.",
        "Reduced motion jumps to end states, shows the summary at once and keeps all feedback."
      ],
      "responsive": [
        "Holds 600px at most and fills narrower containers.",
        "Below 520px card width the sparklines step aside and padding drops to 16px; below 440px the score and renewal stack.",
        "Labels and details truncate with an ellipsis; values and status keep fixed columns."
      ],
      "performance": [
        "Pure SVG and CSS; one motion value drives the gauge and number.",
        "The entrance starts on first intersection and runs once; no loops besides the label shimmer while thinking.",
        "Summary runs are aborted on period change, regenerate and unmount."
      ],
      "aiNotes": [
        "Pick this for a customer success, account management or renewal view about one customer.",
        "Pass real signals per period; keep 5 signals or fewer so the card stays one glance.",
        "Return plain text from summarize; the card handles thinking, streaming, abort and error states.",
        "The action button uses the accent; keep destructive actions out of this slot."
      ],
      "related": [
        "agent-status",
        "ai-insight-card",
        "call-insights",
        "goal-tracker",
        "generation-stats"
      ],
      "usage": "import { AccountHealth } from \"@/components/arc/blocks/account-health/account-health\";\n\nexport function CustomerPanel({ account, periods }) {\n  return (\n    <AccountHealth\n      account={account}\n      periods={periods}\n      summarize={async ({ account, period }, signal) => {\n        const res = await fetch(\"/api/health-summary\", { method: \"POST\", body: JSON.stringify({ id: account.name, period: period.id }), signal });\n        return (await res.json()).text;\n      }}\n      onAction={async (action, { account }, signal) => {\n        await fetch(\"/api/qbr\", { method: \"POST\", body: JSON.stringify({ account: account.name }), signal });\n      }}\n    />\n  );\n}",
      "exports": [
        "AccountHealth",
        "AccountHealthSignal",
        "AccountHealthAction"
      ]
    },
    {
      "name": "magic-link",
      "kind": "block",
      "title": "Magic link",
      "description": "A passwordless sign in card where an email with inline validation and typo fixes sends a magic link, and the card crossfades in place to a check your inbox step with a sealing envelope, Gmail and Outlook shortcuts and a resend countdown. Google and GitHub sign in sit above, with their real marks.",
      "tier": "pro",
      "builtFrom": [
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/magic-link",
      "registry": null,
      "keywords": [
        "magic link",
        "passwordless",
        "email sign in",
        "login",
        "check your inbox",
        "resend countdown",
        "sign in with google",
        "sign in with github",
        "auth",
        "open gmail"
      ],
      "whenToUse": [
        "Passwordless sign in or sign up by email link, with optional Google and GitHub sign in.",
        "B2B products that want a work email gate with a clear what happens next screen.",
        "Replacing a password form to cut sign in friction and reset requests."
      ],
      "whenNotToUse": [
        "Password plus email sign in or MFA codes; use sign-in or a login block instead.",
        "One time codes typed into the app; use an OTP input flow.",
        "Full page marketing sign in layouts with imagery; use login-split."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The card is a labelled region (\"Sign in to Halden\"); the hidden step is inert, so it leaves the tab order and the accessibility tree.",
        "Focus follows the step: the Check your inbox heading takes focus when the link is sent, and Use a different email returns focus to the field with the address selected.",
        "The field has a real label, autocomplete=\"email\" and an email keyboard on touch. Errors set aria-invalid, are linked with aria-describedby and announced as alerts, and carry an icon as well as color.",
        "A polite live region announces sends, resends and simulated actions (\"Simulated, nothing was sent\").",
        "Resend stays focusable during the countdown (aria-disabled), so focus never drops when a resend starts."
      ],
      "motion": [
        "Steps crossfade in one grid cell: the old step fades out in 100ms, the new one fades in over 200ms with a 4px rise. Nothing resizes.",
        "The envelope plays once per send in about 0.8s: the letter drops in, the flap folds shut, a success check settles. It replays on every resend. No bounce, no loop.",
        "Messages under the field and resend labels swap with a single opacity crossfade inside reserved space; the countdown digits change in place in tabular numerals.",
        "Reduced motion drops the rise and the envelope travel and renders its sealed end state, keeping the crossfades short."
      ],
      "responsive": [
        "Fills its container up to 400px. Under 480px the padding and corner radius step down.",
        "Provider and inbox buttons stay in two equal columns down to 320px; long addresses truncate with an ellipsis and keep the full value in a tooltip.",
        "Touch targets are 44px buttons and 32px footer links; the email field opens the email keyboard."
      ],
      "performance": [
        "Both steps stay mounted (the hidden one inert), so switching never measures or reflows the card.",
        "The countdown ticks every 250ms only while it runs and stops at zero. The envelope is one small inline SVG.",
        "Brand marks are inline SVG; no images or network requests."
      ],
      "aiNotes": [
        "Use for passwordless email sign in or sign up. Wire onSendLink to your auth provider (Clerk, Supabase, Auth.js, Better Auth, WorkOS) and onProviderSignIn to its social redirect.",
        "Throw from onSendLink to show the error state; resolve to advance to the inbox step. Pass the AbortSignal to fetch.",
        "Keep it centered on a plain page or in a dialog; it is a self-contained card about 400px wide.",
        "Hide social sign in with providers={[]} for B2B email-only flows, and use validateEmail to require a work domain."
      ],
      "related": [
        "sign-in",
        "login-centered",
        "login-split",
        "input",
        "button"
      ],
      "usage": "import { MagicLink } from \"@/components/arc/blocks/magic-link/magic-link\";\nimport { authClient } from \"@/lib/auth-client\";\n\nexport function SignInPage() {\n  return (\n    <MagicLink\n      appName=\"Halden\"\n      sender=\"login@halden.app\"\n      onSendLink={async (email, signal) => {\n        const res = await fetch(\"/api/auth/magic-link\", {\n          method: \"POST\",\n          body: JSON.stringify({ email, redirectTo: \"/dashboard\" }),\n          signal,\n        });\n        if (!res.ok) throw new Error(\"Send failed\");\n      }}\n      onProviderSignIn={provider => authClient.signIn.social({ provider, callbackURL: \"/dashboard\" })}\n      validateEmail={email => /@(gmail|yahoo)\\./i.test(email) ? \"Use your work email\" : null}\n      termsHref=\"/terms\"\n      privacyHref=\"/privacy\"\n    />\n  );\n}",
      "exports": [
        "MagicLink"
      ]
    },
    {
      "name": "feature-voting",
      "kind": "block",
      "title": "Feature voting",
      "description": "A public feature request board where votes roll digit by digit and the button fills with the accent. Top, New and Trending reorder the rows with one smooth move. A built-in composer suggests similar requests as you type, so people vote instead of posting duplicates.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "SegmentedControl",
        "Avatar"
      ],
      "docs": "https://uiarc.dev/components/blocks/feature-voting",
      "registry": null,
      "keywords": [
        "feature voting",
        "feature requests",
        "upvote",
        "roadmap",
        "feedback board",
        "canny",
        "product feedback",
        "duplicate detection",
        "customer ideas",
        "vote button"
      ],
      "whenToUse": [
        "A public feature request page where customers vote and suggest ideas.",
        "An in-app Give feedback panel that shows what is planned and shipped.",
        "A customer portal that needs to deflect duplicate requests before they are posted."
      ],
      "whenNotToUse": [
        "A full roadmap with columns per stage: use a kanban style board.",
        "One off feedback on a single page or screen: use a short feedback form.",
        "Release notes of what already shipped: use changelog-feed."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Vote buttons are toggle buttons (aria-pressed) labelled with the request and its count, for example Vote for SAML single sign-on with Okta, 351 votes.",
        "Statuses are always an icon plus a word, never color alone.",
        "Every vote, duplicate vote and new post is announced through a polite status region; simulated actions say Simulated, nothing was sent.",
        "The similar requests list is linked to the title field with aria-describedby, and each Vote button names the request it votes for.",
        "The voters card is decorative (aria-hidden); the same count is in the vote button's label."
      ],
      "motion": [
        "Counts roll only the digits that change, up for a vote and down for an unvote, in tabular numerals so widths never shift.",
        "Sorting moves rows with one 240ms bounce free spring on transform. Rows never reorder on a live vote, so nothing moves under the pointer.",
        "The composer, the similar requests list and a newly posted row open by animating their measured height; everything below glides instead of jumping.",
        "The vote chevron nudges up 3px once when you vote. A new or chosen row gets a brief accent wash.",
        "Footer messages crossfade. With reduced motion, rolls, moves and folds cut to their end state and live votes stop."
      ],
      "responsive": [
        "One column card from 320px up. Below 440px the button reads Suggest and padding tightens; below 360px the integration tag hides.",
        "Titles and the footer truncate with an ellipsis; row height never changes.",
        "The voters card appears on hover with a mouse and on keyboard focus; touch skips it."
      ],
      "performance": [
        "Renders every request; paginate or virtualize past about 50 rows.",
        "Duplicate detection runs locally on each keystroke and is linear in the number of requests.",
        "The live vote timer pauses when the card is off screen or the tab is hidden."
      ],
      "aiNotes": [
        "Use for a public or in-app feature request board, roadmap feedback widget or customer voice page.",
        "Wire onVote and onSubmit to your API; both are optimistic and roll back on rejection.",
        "Pass findSimilar backed by your search index or embeddings to catch duplicates across thousands of requests.",
        "Keep the card between 360 and 760px; for a full kanban roadmap with columns, use a dedicated roadmap layout instead.",
        "Set disabled for signed out visitors and render your own sign in prompt next to the card."
      ],
      "related": [
        "feedback-board",
        "changelog-feed",
        "ai-feature-launch",
        "segmented-control",
        "avatar"
      ],
      "usage": "import { FeatureVoting } from \"@/components/arc/blocks/feature-voting/feature-voting\";\n\nexport function RoadmapBoard({ requests, user }) {\n  return (\n    <FeatureVoting\n      requests={requests}\n      currentUser={{ name: user.name, src: user.avatarUrl }}\n      onVote={async (id, voted, signal) => {\n        const response = await fetch(`/api/requests/${id}/vote`, { method: voted ? \"POST\" : \"DELETE\", signal });\n        if (!response.ok) throw new Error(\"Vote not saved\");\n      }}\n      onSubmit={async (draft, signal) => {\n        const response = await fetch(\"/api/requests\", { method: \"POST\", body: JSON.stringify(draft), signal });\n        if (!response.ok) throw new Error(\"Request not posted\");\n        return response.json();\n      }}\n      onOpenRequest={id => router.push(`/roadmap/${id}`)}\n    />\n  );\n}",
      "exports": [
        "FeatureVoting",
        "findSimilarRequests",
        "sortRequests"
      ]
    },
    {
      "name": "credit-topup",
      "kind": "block",
      "title": "Credit top-up",
      "description": "A card for buying AI credits. It shows the balance with a 14 day usage chart and how many days are left, three preset packs with bonus badges, a custom amount slider with a live price, auto top-up with a threshold and the saved Visa or Mastercard. When you buy, the new balance rolls in like an odometer.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Slider",
        "Switch"
      ],
      "docs": "https://uiarc.dev/components/blocks/credit-topup",
      "registry": null,
      "keywords": [
        "credits",
        "top up",
        "buy credits",
        "AI credits",
        "prepaid",
        "token balance",
        "auto recharge",
        "auto top-up",
        "usage based billing",
        "wallet",
        "balance",
        "odometer",
        "rolling numbers",
        "billing"
      ],
      "whenToUse": [
        "Selling prepaid AI credits or tokens with volume bonuses.",
        "A billing settings section where people also manage auto top-up and their card.",
        "An out of credits moment where the balance, burn rate and a one click refill belong together."
      ],
      "whenNotToUse": [
        "Subscription plan changes with seats and billing cycles: use a plan picker or checkout-live.",
        "Collecting a new card number: use a card form or a hosted payment element, then pass the saved card here.",
        "Read-only usage reporting: use a usage meter or a chart."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Packs and thresholds are radio groups with roving focus and labels that read the amount, price and bonus, such as \"5,000 credits, $50, 10% bonus\".",
        "Rolling numerals are aria-hidden; a visually hidden copy of each value is read instead, so screen readers never hear partial digits.",
        "A polite live region announces the charge, the result (\"Added 5,500 credits. Nothing was charged.\") and errors. Without onPurchase it says the checkout is simulated.",
        "Balance health is an icon plus a label (Running low, Healthy), never color alone. The usage chart is one image with a spoken summary.",
        "The Buy button stays focusable while processing (aria-busy) so keyboard focus is not lost."
      ],
      "motion": [
        "After a purchase the balance rolls up odometer style over 0.9s: each digit rolls one way to its target and the lower places take one extra lap, so it reads as counting. New leading places open their width on a bounce-free spring.",
        "The total and price roll in 0.24s as the slider moves, in tabular numerals so nothing shifts.",
        "The pack selection ring and threshold pill glide between options on a 0.24s spring with no bounce. Status, health, days left and the saved card crossfade (100ms out, 160ms in, 2px rise).",
        "The card never changes size between idle, processing, done and error.",
        "Reduced motion: numbers and highlights jump to their end state; crossfades stay as plain fades."
      ],
      "responsive": [
        "Fluid up to 480px. Below about 450px container width the usage chart moves under the balance and stretches full width, and padding drops to 16px.",
        "Packs stay three across down to 320px; the bonus badge sits on the price line so amounts never truncate.",
        "Container queries, not viewport queries, so it adapts inside dialogs and sidebars."
      ],
      "performance": [
        "Each digit is one transformed strip driven by a motion value: no re-renders while numbers roll.",
        "No timers or loops while idle; the only timeout clears the done message.",
        "Brand marks are inline SVG, about 1 KB."
      ],
      "aiNotes": [
        "Use for usage-based AI products that sell prepaid credits: a billing page section, an out of credits dialog body, or a top-up drawer.",
        "Keep pricing on the server authoritative: price orders with the exported quoteCredits and the same pricing object, never trust the client total.",
        "Pass balance controlled when credits change elsewhere (live usage); the numerals roll on every change, so it doubles as a live balance.",
        "Wire onPurchase to a saved payment method charge (for example a Stripe PaymentIntent with off_session) and throw an Error with a short human message on decline."
      ],
      "related": [
        "checkout-live",
        "payment-flow",
        "usage-meter",
        "slider",
        "switch",
        "button"
      ],
      "usage": "import { CreditTopup } from \"@/components/arc/blocks/credit-topup/credit-topup\";\n\nexport function BillingCredits({ workspace }) {\n  return (\n    <CreditTopup\n      balance={workspace.credits}\n      usage={workspace.dailyUsage}\n      paymentMethods={workspace.cards}\n      onPurchase={async (order, signal) => {\n        const res = await fetch(\"/api/credits/purchase\", { method: \"POST\", body: JSON.stringify(order), signal });\n        if (!res.ok) throw new Error((await res.json()).message ?? \"Payment failed. Try again or change card.\");\n      }}\n      onAutoTopupChange={settings => fetch(\"/api/credits/auto-topup\", { method: \"PUT\", body: JSON.stringify(settings) })}\n    />\n  );\n}",
      "exports": [
        "CreditTopup",
        "quoteCredits"
      ]
    },
    {
      "name": "goal-tracker",
      "kind": "block",
      "title": "Goal tracker",
      "description": "A team goals card where objectives open into key results with owners, confidence and progress bars. A weekly check-in updates a value, confidence and note in place, fills the bar and recalculates the objective, and a quarter selector steps from last quarter's final scores to next quarter's planning.",
      "tier": "pro",
      "builtFrom": [
        "Button",
        "Avatar",
        "SegmentedControl"
      ],
      "docs": "https://uiarc.dev/components/blocks/goal-tracker",
      "registry": null,
      "keywords": [
        "okr",
        "goals",
        "objectives",
        "key results",
        "goal tracker",
        "check-in",
        "progress bar",
        "quarterly goals",
        "confidence",
        "lattice",
        "team goals"
      ],
      "whenToUse": [
        "Showing a team's quarterly objectives and key results with owners and confidence.",
        "Collecting a lightweight weekly check-in (value, confidence, note) without leaving the page.",
        "Comparing the active quarter against last quarter's final scores."
      ],
      "whenNotToUse": [
        "A single KPI with a trend line; use a metric or stat card.",
        "Task or project tracking with assignees and due dates; use a task list or kanban block.",
        "Org-wide goal trees with parent and child alignment; this card is one level deep."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Objectives are disclosure buttons with aria-expanded and aria-controls; collapsed bodies are inert.",
        "Each key result button has a label with its value, target, progress and confidence, so the visual bar is not the only source.",
        "Confidence is always an icon plus a text label, never color alone.",
        "The check-in is a labelled form; the hidden form and hidden key results are inert so focus never lands on invisible controls.",
        "A polite live region announces saves, errors and the recalculated objective, and says when a save was simulated."
      ],
      "motion": [
        "Bars fill once when the card first scrolls into view, then tween to new values (560ms ease-out) with percentages counting in tabular numerals.",
        "The objective bar follows its key result 120ms later so cause and effect read clearly.",
        "Key results and the check-in form share one grid cell and crossfade (100ms out, 160ms in), so the card never changes height when the form opens.",
        "Objective bodies open with a bounce-free 240ms spring on measured height; quarters crossfade with an 8px directional shift and the empty state borrows the previous height.",
        "Reduced motion keeps the end states, crossfades and feedback, and drops the fill travel, shifts and the scripted demo."
      ],
      "responsive": [
        "Fluid from 360px to 640px wide using container queries, so it adapts to its column rather than the viewport.",
        "Below 480px the bars shorten and the confidence picker moves under the value field; below 380px owner names collapse to avatars.",
        "Titles truncate with an ellipsis; numbers, labels and bars never wrap or shift."
      ],
      "performance": [
        "Progress animations run on motion values and clip-path, so numbers and bars update without React re-renders.",
        "One IntersectionObserver for the first fill and one ResizeObserver for the quarter height; both disconnect on unmount.",
        "Designed for up to about five objectives with five key results each per quarter."
      ],
      "aiNotes": [
        "Use for OKRs, team goals or quarterly targets inside a product: a settings or team page, a dashboard column, or a weekly review.",
        "Pass real data through quarters and persist check-ins in onCheckIn; the card updates optimistically after the promise resolves, and shows Retry if it throws.",
        "Set start, target and current in the metric's own units; the card computes progress, so decreasing targets need no special handling.",
        "Set autoPlay={false} in production. The sample data plays one demo check-in by default.",
        "Keep one card per team; for many teams, stack cards rather than adding more objectives to one card."
      ],
      "related": [
        "agent-tasks",
        "activity-feed",
        "account-health"
      ],
      "usage": "import { GoalTracker, type GoalCheckIn, type GoalQuarter } from \"@/components/arc/blocks/goal-tracker/goal-tracker\";\n\nexport function TeamGoals({ quarters }: { quarters: GoalQuarter[] }) {\n  return (\n    <GoalTracker\n      title=\"Platform goals\"\n      quarters={quarters}\n      onCheckIn={async (checkIn: GoalCheckIn, { signal }) => {\n        const response = await fetch(\"/api/check-ins\", { method: \"POST\", body: JSON.stringify(checkIn), signal });\n        if (!response.ok) throw new Error(\"Check-in failed\");\n      }}\n    />\n  );\n}",
      "exports": [
        "GoalTracker",
        "GoalKeyResult",
        "GoalObjective"
      ]
    },
    {
      "name": "spend-alert",
      "kind": "block",
      "title": "Spend alert",
      "description": "A budget and spend alert card where the threshold is set right on the live spend bar, with forecast, Email, Slack and PagerDuty toggles, a hard stop and a live preview of the Slack message the alert will post.",
      "tier": "pro",
      "builtFrom": [
        "Switch",
        "Button"
      ],
      "docs": "https://uiarc.dev/components/blocks/spend-alert",
      "registry": null,
      "keywords": [
        "spend alert",
        "budget alert",
        "billing threshold",
        "usage alert",
        "cost alert",
        "AI spend",
        "slack notification preview",
        "pagerduty",
        "hard stop",
        "budget settings"
      ],
      "whenToUse": [
        "Letting a team set a monthly budget and alert threshold for metered spend.",
        "Showing how close current and forecast spend are to an alert line.",
        "Choosing where cost alerts go and previewing the Slack message before saving."
      ],
      "whenNotToUse": [
        "Read-only usage reporting with no settings; use usage-forecast or usage-billing instead.",
        "Per-seat plans with no metered spend.",
        "Complex multi-budget policies across many projects; use a table of budgets that opens this card per row."
      ],
      "dependencies": [
        "motion",
        "lucide-react",
        "@radix-ui/react-switch"
      ],
      "accessibility": [
        "The threshold is a role=\"slider\" labelled \"Alert at\" with aria-valuetext giving the percent and the amount.",
        "The budget field is labelled and described by its helper line, which also carries the invalid and over-budget messages with aria-invalid.",
        "Each channel switch has its own label; the hard stop switch is labelled by its row title.",
        "Save and test results land in a polite live region, including \"Simulated, nothing was sent\" when no handler is wired.",
        "Status is never color alone: every tone pairs an icon with a short label."
      ],
      "motion": [
        "Spend and forecast fills grow in once when the card scrolls into view (720ms ease-out), then follow budget changes in 160ms.",
        "The thumb follows the pointer 1:1 while dragging and glides 160ms on clicks, marker jumps and keys.",
        "Status, helper and footer lines swap with a single crossfade in one grid cell, so nothing changes size.",
        "Switching Slack off crossfades the preview to a placeholder inside the same box. A test send washes the preview once.",
        "Reduced motion keeps every end state and drops the fill growth and glides."
      ],
      "responsive": [
        "Holds from 360px to 580px. Under 440px the gutter tightens, channel destinations move under their names and the footer actions go full width.",
        "Every line truncates with an ellipsis rather than wrapping, so state changes never change the card height.",
        "The slider rail is 28px tall for touch and keeps vertical page scroll (touch-action: pan-y)."
      ],
      "performance": [
        "Bar fills animate clip-path and the thumb animates left on one element; no layout animation on text.",
        "One IntersectionObserver runs until the first reveal, then disconnects."
      ],
      "aiNotes": [
        "Use for usage-based billing settings: AI model spend, cloud cost budgets, API overage alerts.",
        "Pass spend and period from your usage API; forecast is derived unless you pass your own.",
        "Wire onSave and onSendTest with the AbortSignal; the card handles loading, success, error and dirty states.",
        "Keep it as one card in a settings page; do not wrap it in another card."
      ],
      "related": [
        "usage-forecast",
        "usage-billing",
        "billing-overview",
        "switch",
        "slider"
      ],
      "usage": "import { SpendAlert } from \"@/components/arc/blocks/spend-alert/spend-alert\";\n\nexport function BillingAlerts({ usage }: { usage: { spend: number } }) {\n  return (\n    <SpendAlert\n      project=\"Production\"\n      spend={usage.spend}\n      defaultValue={{ budget: 5000, threshold: 80 }}\n      destinations={{ email: \"finance@acme.com\", slack: \"#eng-spend\" }}\n      onSave={async (settings, signal) => {\n        const response = await fetch(\"/api/budget-alerts\", { method: \"PUT\", body: JSON.stringify(settings), signal });\n        if (!response.ok) throw new Error(\"Save failed\");\n      }}\n    />\n  );\n}",
      "exports": [
        "SpendAlert"
      ]
    }
  ]
}