{
  "name": "arc",
  "homepage": "https://uiarc.dev",
  "generatedAt": "2026-09-29T17:47:46.135Z",
  "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."
      ],
      "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 \"@/registry/components/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-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",
        "hold-to-confirm",
        "toast"
      ],
      "usage": "import { ActionButton } from \"@/registry/components/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 \"@/registry/components/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": "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 \"@/registry/components/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 \"@/registry/components/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": [
        "Icons trade places through a blur on a slow, nearly critically damped spring, and the check draws its stroke in.",
        "Only the changed letters of the label move; the same motion plays in reverse on reset.",
        "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": [
        "Only changed letters animate; the icon swap and stroke draw are a single short spring per copy.",
        "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 \"@/registry/components/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 \"@/registry/components/drawer/drawer\";\nimport { Button } from \"@/registry/components/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 \"@/registry/components/theme-switch/theme-switch\";\nimport type { Theme } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 renders next/image with a sizes hint per size, so phones never download a larger photo than the circle shows."
      ],
      "performance": [
        "Remote src hosts must be allowed in next.config images, since the photo goes through next/image.",
        "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.",
        "Remote src hosts must be allowed in next.config images, since it renders next/image."
      ],
      "related": [
        "avatar-group",
        "user-menu",
        "card",
        "timeline"
      ],
      "usage": "import { Avatar } from \"@/registry/components/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.",
        "Hover fans the stack apart in CSS. Reduced motion removes the fan and 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 fan applies only on hover-capable fine pointers, so touch taps do not spread the stack."
      ],
      "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; the fan is plain CSS transforms."
      ],
      "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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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",
        "search-field"
      ],
      "usage": "import { Combobox } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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.",
        "No focus rings: 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 \"@/registry/components/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\".",
        "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."
      ],
      "related": [
        "input"
      ],
      "usage": "import { PasswordField } from \"@/registry/components/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}",
      "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": [
        "combobox",
        "filter-toolbar"
      ],
      "usage": "import { SearchField } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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"
      ],
      "usage": "import { Accordion } from \"@/registry/components/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 \"@/registry/components/dialog/dialog\";\nimport { Button } from \"@/registry/components/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 \"@/registry/components/popover/popover\";\nimport { Button } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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"
      ],
      "usage": "import { ExpandableCard } from \"@/registry/components/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.",
        "Keep labels short; the component does not truncate or collapse long paths."
      ],
      "related": [
        "tree-view",
        "pagination",
        "tabs"
      ],
      "usage": "import { Breadcrumb } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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": "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 \"@/registry/components/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 \"@/registry/components/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.",
        "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 \"@/registry/components/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 \"@/registry/components/card/card\";\nimport { Avatar } from \"@/registry/components/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 \"@/registry/components/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",
        "sortable-data-table"
      ],
      "usage": "import { Search } from \"lucide-react\";\nimport { Button } from \"@/registry/components/button/button\";\nimport { EmptyState } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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": "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": [
        "The full text sits in a visually hidden span; the animated words are aria-hidden, so screen readers read one clean sentence.",
        "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 \"@/registry/components/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 \"@/registry/components/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",
        "text-shimmer"
      ],
      "usage": "import { TextMorph } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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",
        "dropdown-menu"
      ],
      "usage": "import { SwipeActions, SwipeActionsRow } from \"@/registry/components/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": "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 \"@/registry/components/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": "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 \"@/registry/components/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": "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 \"@/registry/components/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": "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 \"@/registry/components/bottom-sheet/bottom-sheet\";\nimport { Button } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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": "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 \"@/registry/components/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",
        "image-compare"
      ],
      "usage": "import { Carousel } from \"@/registry/components/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": "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 \"@/registry/components/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 selection springs between items with a shared layoutId.",
        "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 44px 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",
        "orbit-menu"
      ],
      "usage": "import { Dock } from \"@/registry/components/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": [
        "expandable-card"
      ],
      "usage": "import { WalletStack } from \"@/registry/components/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 liquid selection that stretches between them 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's leading edge races to the new tab while the trailing edge catches up, so it stretches with distance and speed and thins as it stretches; it leans toward a hovered tab.",
        "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 stretch 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 \"@/registry/components/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.",
        "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 { NowPlaying } from \"@/registry/components/now-playing/now-playing\";\n\nexport function Player() {\n  return (\n    <NowPlaying\n      tracks={[\n        { id: \"1\", title: \"Night Drive\", artist: \"Arc Ensemble\", duration: 212, artwork: \"/covers/night-drive.jpg\" },\n        { id: \"2\", title: \"Low Tide\", artist: \"Harbor\", duration: 185, artwork: \"/covers/low-tide.jpg\" },\n      ]}\n      onPlayingChange={playing => (playing ? audio.play() : audio.pause())}\n      onIndexChange={index => loadTrack(index)}\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 \"@/registry/components/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 \"@/registry/components/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",
        "photo-grid"
      ],
      "usage": "import { CoverFlow } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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"
      ],
      "usage": "import { StretchRefresh } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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.",
        "No focus rings are drawn: 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 \"@/registry/components/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 flat tables, up to tens of thousands of rows, that people filter, sort, and total.",
        "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 tree-table when rows nest under parents.",
        "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\"."
      ],
      "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.",
        "Reduced motion jumps the selection, drops the row slide, the shake, the press scales, 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 not virtualized, so keep column counts moderate. 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 and tree-table for nested rows.",
        "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.",
        "All filtering and sorting run on the client over the rows you pass. For server data, control sort and filters and fetch on change."
      ],
      "related": [
        "sortable-data-table",
        "filter-toolbar",
        "animated-counter",
        "pagination"
      ],
      "usage": "import { useState } from \"react\";\nimport { Archive } from \"lucide-react\";\nimport { DataGrid, type DataGridColumn, type DataGridRow, type DataGridSort } from \"@/registry/components/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",
        "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 use next/image with responsive sizes; viewer slides load lazily except the current one.",
        "The grid is not virtualized, so paginate very large collections.",
        "Remote image hosts must be allowed in next.config for next/image."
      ],
      "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; it uses next/image, so remote hosts must be allowed in next.config."
      ],
      "related": [
        "photo-grid",
        "carousel",
        "dialog",
        "cover-flow"
      ],
      "usage": "import { LightboxGallery } from \"@/registry/components/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": [
        "progress",
        "tabs",
        "breadcrumb"
      ],
      "usage": "import { Stepper } from \"@/registry/components/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"
      ],
      "usage": "import { MorphLoader } from \"@/registry/components/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 \"@/registry/components/voice-orb/voice-orb\";\nimport type { VoiceOrbState } from \"@/registry/components/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 \"@/registry/components/signature-pad/signature-pad\";\nimport type { InkStroke } from \"@/registry/components/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": "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 \"@/registry/components/date-range-picker/date-range-picker\";\nimport type { DateRange } from \"@/registry/components/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 \"@/registry/components/color-picker/color-picker\";\nimport type { ColorSwatch } from \"@/registry/components/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": "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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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. No focus rings are drawn."
      ],
      "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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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-view"
      ],
      "usage": "import { Sunburst } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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": "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": [
        "alert",
        "toast",
        "badge"
      ],
      "usage": "import { AnnouncementBar } from \"@/registry/components/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": "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 \"@/registry/components/product-gallery/product-gallery\";\nimport Button from \"@/registry/components/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": "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 \"@/registry/components/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 \"@/registry/components/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": "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 Kbd or ShortcutKeys."
      ],
      "whenNotToUse": [
        "Use Kbd 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 Kbd 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 \"@/registry/components/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",
        "Kbd",
        "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, and Cancel takes focus first so Enter never confirms by accident.",
        "Focus follows the morph: each new face focuses its main button, and the root holds focus while pending with aria-busy.",
        "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": [
        "The surface width springs to each face (a bouncier spring when growing, a critically damped one when shrinking), so nothing around it jumps.",
        "Faces slide in from the right going forward and from the left going back, with a soft blur; the done check draws itself.",
        "A thin bar drains along the bottom edge for the timeout.",
        "Reduced motion removes the width spring, slide, blur, and draw; faces crossfade and the spinner slows."
      ],
      "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 hold-to-confirm when a deliberate press and hold fits better, and dialog when the consequence needs a full explanation."
      ],
      "related": [
        "hold-to-confirm",
        "action-button",
        "action-morph",
        "dialog",
        "toast"
      ],
      "usage": "import { Trash2 } from \"lucide-react\";\nimport { ConfirmMorph } from \"@/registry/components/confirm-morph/confirm-morph\";\n\nexport function DeleteSelection({ ids }: { ids: string[] }) {\n  return (\n    <ConfirmMorph\n      label=\"Delete\"\n      icon={<Trash2 size={16} strokeWidth={1.75} />}\n      prompt={`Delete ${ids.length} files?`}\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 \"@/registry/components/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",
        "file-dropzone",
        "avatar",
        "textarea"
      ],
      "usage": "import { ChatThread, type ChatMessage } from \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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 \"@/registry/components/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",
        "avatar",
        "inline-edit"
      ],
      "usage": "import { CommentThread, type ThreadComment } from \"@/registry/components/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 \"@/registry/components/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": "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",
        "text-morph",
        "billing-toggle"
      ],
      "usage": "import { SlotText } from \"@/registry/components/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": "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": [
        "expandable-card"
      ],
      "usage": "import { GlassCard } from \"@/registry/components/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.",
        "No focus rings are drawn, in line with the library."
      ],
      "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 \"@/registry/components/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": "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 \"@/registry/components/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.",
        "No focus ring is drawn; 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 \"@/registry/components/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": "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.",
        "No focus ring is drawn."
      ],
      "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 \"@/registry/components/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.",
        "No focus ring is drawn."
      ],
      "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 \"@/registry/components/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": "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.",
        "No focus rings: 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 \"@/registry/components/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": "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."
      ],
      "usage": "import { MultiRegionFailover } from \"@/registry/components/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.",
        "No focus ring is drawn."
      ],
      "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": [
        "data-flow"
      ],
      "usage": "import { VectorSearch } from \"@/registry/components/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": "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.",
        "No focus ring is drawn."
      ],
      "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 \"@/registry/components/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": "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.",
        "No focus ring is drawn; 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 \"@/registry/components/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": "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 release-console to show a running deploy with stages and a live log.",
        "Use onboarding-checklist for a user's own getting-started tasks.",
        "Use feature-flags when the controls toggle features rather than confirm checks."
      ],
      "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": [
        "progress"
      ],
      "usage": "import { ReleaseReadiness } from \"@/registry/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",
        "otp-input"
      ],
      "usage": "import { SignupForm, type SignupDetails } from \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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": [
        "workspace-sidebar",
        "avatar-group",
        "filter-toolbar"
      ],
      "usage": "import { ProjectBoard } from \"@/registry/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 \"@/registry/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"
      ],
      "usage": "import { PlanComparison } from \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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.",
        "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 springs for pointer moves and slides in 70ms for 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",
        "search-field"
      ],
      "usage": "import { CommandPalette, type CommandItem } from \"@/registry/components/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 \"@/registry/components/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": "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 \"@/registry/components/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 \"@/registry/components/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 or project-intake 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 or project-intake 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 \"@/registry/components/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 \"@/registry/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 \"@/registry/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.",
        "Use release-console when the page manages a release rather than announcing it."
      ],
      "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 \"@/registry/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 \"@/registry/blocks/usage-pricing/usage-pricing\";\n\nexport default function PricingPage() {\n  return (\n    <main>\n      <UsagePricing />\n    </main>\n  );\n}",
      "exports": [
        "UsagePricing"
      ]
    },
    {
      "name": "ai-composer",
      "kind": "block",
      "title": "AI composer",
      "description": "A focused assistant thread where messages lift out of the composer and replies stream in.",
      "tier": "pro",
      "builtFrom": [
        "Copy button",
        "Text stream",
        "Text shimmer"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-composer",
      "registry": null,
      "keywords": [
        "react ai chat",
        "chatgpt clone ui",
        "ai composer",
        "chat input with attachments",
        "llm chat interface",
        "assistant chat panel",
        "streaming chat ui"
      ],
      "whenToUse": [
        "An in-product assistant or chat panel.",
        "Chats with file attachments and suggested questions.",
        "Replies that stream in with a thinking shimmer, a stop control, and copy buttons."
      ],
      "whenNotToUse": [
        "Use support-conversation for chats with human agents.",
        "Use command-palette for quick actions and search.",
        "Use text-stream on its own when you already have a chat layout."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Suggested questions are a labelled group of buttons.",
        "Sending, streaming, and stopping are announced through a visually hidden role=\"status\" region."
      ],
      "motion": [
        "Sent messages lift out of the composer into the thread, and replies stream in with a shimmer while thinking.",
        "Reduced motion removes the lift and shows replies without the travel animation."
      ],
      "responsive": [
        "The panel is up to 760px wide and 660px tall; below 560px it drops to 620px, hides the keyboard hint, and widens bubbles to 86%.",
        "Copy buttons on sent messages show on hover for mouse and are always visible on touch."
      ],
      "performance": [
        "The thread is not virtualized; long histories grow the DOM, so trim or paginate old messages.",
        "A ResizeObserver measures the composer so the thread keeps room for it."
      ],
      "aiNotes": [
        "Use for an in-product assistant or chat panel.",
        "onSend only reports the message; the reply is simulated inside the block, so stream your model's tokens into the reply state instead.",
        "Composes Arc copy-button, text-stream, and text-shimmer."
      ],
      "related": [
        "text-shimmer",
        "support-conversation",
        "command-palette",
        "copy-button"
      ],
      "usage": "import { AiComposer } from \"@/registry/blocks/ai-composer/ai-composer\";\n\nexport function Assistant() {\n  return (\n    <AiComposer\n      onSend={({ text, files }) =>\n        fetch(\"/api/chat\", { method: \"POST\", body: JSON.stringify({ text, files }) })\n      }\n    />\n  );\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 \"@/registry/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"
      ],
      "usage": "import { WorkspaceSidebar } from \"@/registry/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 \"@/registry/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 \"@/registry/blocks/sidebar-rail/sidebar-rail\";\n\nexport default function AppShell() {\n  return (\n    <main>\n      <SidebarRail />\n    </main>\n  );\n}",
      "exports": [
        "SidebarRail"
      ]
    },
    {
      "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 \"@/registry/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 \"@/registry/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",
        "invoice-studio",
        "settings-page"
      ],
      "usage": "import { BillingOverview } from \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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",
        "tabs"
      ],
      "usage": "import { EmptyStates } from \"@/registry/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 \"@/registry/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 \"@/registry/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",
        "otp-input"
      ],
      "usage": "import { SecuritySettings } from \"@/registry/blocks/security-settings/security-settings\";\n\nexport default function SecurityTab() {\n  return (\n    <section>\n      <SecuritySettings />\n    </section>\n  );\n}",
      "exports": [
        "SecuritySettings"
      ]
    },
    {
      "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 \"@/registry/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 \"@/registry/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 \"@/registry/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": "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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/blocks/metric-explorer/metric-explorer\";\n\nexport default function AnalyticsPage() {\n  return (\n    <main>\n      <MetricExplorer />\n    </main>\n  );\n}",
      "exports": [
        "MetricExplorer"
      ]
    },
    {
      "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",
        "animated-counter",
        "revenue-explorer"
      ],
      "usage": "import { RevenueGlobe } from \"@/registry/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 \"@/registry/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 \"@/registry/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",
        "segmented-control"
      ],
      "usage": "import { LayoutMorph } from \"@/registry/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 live AI agent run: steps stream in, tool calls expand, a diff waits for approval, and it all folds into a result.",
      "tier": "pro",
      "builtFrom": [
        "Animated counter",
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/agent-run",
      "registry": null,
      "keywords": [
        "react ai agent ui",
        "agent run timeline",
        "coding agent interface",
        "human in the loop approval",
        "ai task progress",
        "tool call timeline"
      ],
      "whenToUse": [
        "Showing an AI agent working through a task with a human approval step, such as a coding agent.",
        "Runs where each step should stream into a timeline and a diff waits for approval before writing."
      ],
      "whenNotToUse": [
        "Use text-stream for plain streamed text.",
        "Use ai-composer for the prompt input.",
        "Use timeline for a static list of events."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "Each step header is a disclosure button with aria-expanded and aria-controls pointing at a labelled details region.",
        "A polite live region announces the current step, the approval wait, pauses and the final result.",
        "After Approve, Reject or Retry, focus moves back to the gated step's header; Restart focuses the pause button."
      ],
      "motion": [
        "The run starts the first time the block scrolls into view, and the clock holds while the tab is hidden.",
        "Steps stream in with heights that follow content on a smooth spring, and the active step title shimmers.",
        "Reduced motion makes height and panel changes instant."
      ],
      "responsive": [
        "Below a 480px viewport the frame padding and prompt size shrink and hit text hides.",
        "Step details expand in place with a ResizeObserver tracking their height."
      ],
      "performance": [
        "The run clock uses one setInterval that holds while the tab is hidden.",
        "Steps are not virtualized; very long runs should collapse finished steps."
      ],
      "aiNotes": [
        "Use to show an AI agent working on a task with a human approval step, such as a coding agent or workflow runner.",
        "Replace STEPS, TIMING, GATE, REQUEST and RESULT in agent-run-data.ts with events streamed from your agent runtime.",
        "Wire Approve, Reject and Open pull request to real actions; nothing is executed in the preview."
      ],
      "related": [
        "ai-composer",
        "code-block",
        "timeline",
        "animated-counter"
      ],
      "usage": "import { AgentRun } from \"@/registry/blocks/agent-run/agent-run\";\n\nexport default function AgentTaskPage() {\n  return (\n    <main>\n      <AgentRun />\n    </main>\n  );\n}",
      "exports": [
        "AgentRun"
      ]
    },
    {
      "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. No focus rings: 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 \"@/registry/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 \"@/registry/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 @/lib/media, and Arc AnimatedCounter for numbers."
      ],
      "related": [
        "expandable-card",
        "scroll-story"
      ],
      "usage": "import { SpotlightGrid } from \"@/registry/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 \"@/registry/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 @/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 \"@/registry/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 \"@/registry/blocks/pricing-calculator/pricing-calculator\";\n\nexport function PricingPage() {\n  return <PricingCalculator />;\n}",
      "exports": [
        "PricingCalculator"
      ]
    },
    {
      "name": "ai-chat",
      "kind": "block",
      "title": "AI chat",
      "description": "A complete AI chat with streamed markdown answers, folding reasoning, tool chips, citations, and a model switcher.",
      "tier": "pro",
      "builtFrom": [
        "Motion"
      ],
      "docs": "https://uiarc.dev/components/blocks/ai-chat",
      "registry": null,
      "keywords": [
        "react ai chat",
        "chatgpt clone ui",
        "streaming chat interface",
        "llm chat ui",
        "ai assistant ui",
        "chat with citations",
        "model switcher",
        "chat with reasoning panel"
      ],
      "whenToUse": [
        "A full page assistant with conversation history, model choice, and streamed answers.",
        "Research or support assistants that show tool calls and cite sources inline.",
        "Products that need regenerate, answer versions, and editable prompts out of the box."
      ],
      "whenNotToUse": [
        "Use ai-composer when you only need the prompt input.",
        "Use chat-thread for person to person messaging without model output.",
        "Use support-widget for a small floating help chat."
      ],
      "dependencies": [
        "motion",
        "lucide-react"
      ],
      "accessibility": [
        "The sidebar is an aside labelled \"Chat history\" with a nav of conversations; the current one has aria-current=\"page\".",
        "Streaming answers set aria-busy. A polite live region announces generation, completion time, stops, and errors.",
        "The model switcher is a button with aria-haspopup=\"listbox\" and a listbox using aria-activedescendant.",
        "Citation chips are labelled with source number, title, and domain; the source card is a role=\"dialog\" linked with aria-controls.",
        "Thinking, tool detail, and source list toggles use aria-expanded and aria-controls. Icon buttons all have labels."
      ],
      "motion": [
        "Streamed words fade and unblur in with a CSS keyframe; code lines fade in as they arrive.",
        "Answer height follows content on a spring through a ResizeObserver, so streaming grows instead of jumping.",
        "The model pill grows into its menu as one surface and shrinks back around the new name.",
        "Switching answer versions slides in the direction of travel. A new conversation title scrambles in.",
        "The wide sidebar springs its width open and shut; on narrow layouts it slides over the thread as a drawer.",
        "Reduced motion stops the word and line keyframes and spinners, removes offsets and blur, and uses short fades."
      ],
      "responsive": [
        "Below 720px root width the sidebar becomes a drawer over the thread with a scrim, closed by Escape or a tap outside.",
        "The main column is a size container. At 560px and below the thread, dock, and header padding tighten; at 380px the header notice hides.",
        "Message hover actions are always visible on devices without hover."
      ],
      "performance": [
        "Every stream event patches state, so each chunk re-renders the active answer. Batch tiny tokens into larger chunks in respond.",
        "A ResizeObserver keeps the thread pinned to the bottom while you are within 56px of it; scrolling up releases it and shows a jump button.",
        "Messages are not virtualized. Very long conversations should load older messages on demand."
      ],
      "aiNotes": [
        "Use for an assistant or copilot that streams answers, shows reasoning and tool use, and cites sources.",
        "Write respond as an async generator that yields ChatStreamEvent objects and passes the signal to fetch. Write citations in the text as [1], [2], matching the sources event.",
        "Conversations are uncontrolled after mount; persist them from onConversationsChange, ideally debounced since it fires per chunk.",
        "Markdown supports headings, lists, quotes, tables, and fenced code with basic regex highlighting from markdown.tsx; swap in your own highlighter if you need more languages.",
        "The reasoning panel renders with StreamText from markdown.tsx; reuse it for any plain streamed text next to the chat.",
        "Attachments are images only, up to four, via picker, paste, or drop. They arrive as blob: URLs, so upload them inside respond if the model needs them."
      ],
      "related": [
        "ai-composer",
        "chat-thread",
        "agent-run",
        "support-conversation",
        "text-shimmer"
      ],
      "usage": "\"use client\";\n\nimport { AiChat } from \"@/registry/blocks/ai-chat/ai-chat\";\nimport type { ChatResponder } from \"@/registry/blocks/ai-chat/ai-chat\";\n\nconst respond: ChatResponder = async function* (request, signal) {\n  const response = await fetch(\"/api/chat\", {\n    method: \"POST\",\n    body: JSON.stringify({ messages: request.messages, model: request.model }),\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\", text: value };\n  }\n};\n\nexport function Assistant() {\n  return (\n    <AiChat\n      respond={respond}\n      notice={null}\n      models={[{ id: \"default\", name: \"Default\", description: \"Balanced for everyday work\" }]}\n      defaultModel=\"default\"\n      defaultConversations={[]}\n      user={{ name: \"Ava Mitchell\", detail: \"Pro plan\" }}\n    />\n  );\n}",
      "exports": [
        "AiChat",
        "ChatResponder",
        "ChatStreamEvent",
        "sampleResponder",
        "StreamText"
      ]
    },
    {
      "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"
      ],
      "usage": "import { CheckoutSummary } from \"@/registry/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 \"@/registry/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 \"@/registry/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. No focus rings are drawn, by product decision."
      ],
      "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 \"@/registry/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 \"@/registry/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 \"@/registry/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": "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 \"@/registry/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": "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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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 \"@/registry/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": "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 \"@/registry/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": "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.",
        "No focus rings: 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 \"@/registry/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.",
        "No focus rings: 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 \"@/registry/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": "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.",
        "No focus rings: 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 \"@/registry/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.",
        "No focus rings: 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 \"@/registry/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": "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. No focus rings: 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 \"@/registry/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"
      ]
    }
  ]
}