{"_id":"@zuilib/primitives","_rev":"2-cbfca59a8d257f6610ab6c2e231ab361","name":"@zuilib/primitives","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.0":{"name":"@zuilib/primitives","version":"0.3.0","keywords":["design-system","component-library","tailwindcss","base-ui","react-hook-form","sonner","react","typescript"],"_id":"@zuilib/primitives@0.3.0","maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"dist":{"shasum":"01493271e8b91b737a4d2ad97a117c8d3d10d587","tarball":"https://registry.npmjs.org/@zuilib/primitives/-/primitives-0.3.0.tgz","fileCount":145,"integrity":"sha512-TcdYVzB/iqIfS0wrAuj0c2v5Awy4OGJgJIqR03+8JlkY0eLpZoyobDemPSqgqb93zYd5TAxxhCpZrjRpv3d3Vw==","signatures":[{"sig":"MEYCIQCPGered0b7LVt/I8QULVoRUGK/12bJ9jTxcC5ZVdv3mwIhAM/ecjT6xVgLrKLK4u1NdiIH0mApCm+O43jm5xUBcEpM","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1169183},"type":"module","_from":"file:zuilib-primitives-0.3.0.tgz","exports":{"./kbd":{"types":"./dist/kbd.d.ts","import":"./dist/kbd.js"},"./card":{"types":"./dist/card.d.ts","import":"./dist/card.js"},"./code":{"types":"./dist/code.d.ts","import":"./dist/code.js"},"./form":{"types":"./dist/form.d.ts","import":"./dist/form.js"},"./menu":{"types":"./dist/menu.d.ts","import":"./dist/menu.js"},"./tabs":{"types":"./dist/tabs.d.ts","import":"./dist/tabs.js"},"./text":{"types":"./dist/text.d.ts","import":"./dist/text.js"},"./alert":{"types":"./dist/alert.d.ts","import":"./dist/alert.js"},"./badge":{"types":"./dist/badge.d.ts","import":"./dist/badge.js"},"./field":{"types":"./dist/field.d.ts","import":"./dist/field.js"},"./input":{"types":"./dist/input.d.ts","import":"./dist/input.js"},"./label":{"types":"./dist/label.d.ts","import":"./dist/label.js"},"./stack":{"types":"./dist/stack.d.ts","import":"./dist/stack.js"},"./table":{"types":"./dist/table.d.ts","import":"./dist/table.js"},"./toast":{"types":"./dist/toast.d.ts","import":"./dist/toast.js"},"./avatar":{"types":"./dist/avatar.d.ts","import":"./dist/avatar.js"},"./button":{"types":"./dist/button.d.ts","import":"./dist/button.js"},"./dialog":{"types":"./dist/dialog.d.ts","import":"./dist/dialog.js"},"./drawer":{"types":"./dist/drawer.d.ts","import":"./dist/drawer.js"},"./lib/cn":{"types":"./dist/lib/cn.d.ts","import":"./dist/lib/cn.js"},"./select":{"types":"./dist/select.d.ts","import":"./dist/select.js"},"./slider":{"types":"./dist/slider.d.ts","import":"./dist/slider.js"},"./switch":{"types":"./dist/switch.d.ts","import":"./dist/switch.js"},"./heading":{"types":"./dist/heading.d.ts","import":"./dist/heading.js"},"./popover":{"types":"./dist/popover.d.ts","import":"./dist/popover.js"},"./spinner":{"types":"./dist/spinner.d.ts","import":"./dist/spinner.js"},"./stepper":{"types":"./dist/stepper.d.ts","import":"./dist/stepper.js"},"./tooltip":{"types":"./dist/tooltip.d.ts","import":"./dist/tooltip.js"},"./zui.css":"./dist/zui.css","./checkbox":{"types":"./dist/checkbox.d.ts","import":"./dist/checkbox.js"},"./combobox":{"types":"./dist/combobox.d.ts","import":"./dist/combobox.js"},"./fieldset":{"types":"./dist/fieldset.d.ts","import":"./dist/fieldset.js"},"./presence":{"types":"./dist/presence.d.ts","import":"./dist/presence.js"},"./progress":{"types":"./dist/progress.d.ts","import":"./dist/progress.js"},"./skeleton":{"types":"./dist/skeleton.d.ts","import":"./dist/skeleton.js"},"./textarea":{"types":"./dist/textarea.d.ts","import":"./dist/textarea.js"},"./accordion":{"types":"./dist/accordion.d.ts","import":"./dist/accordion.js"},"./ai-button":{"types":"./dist/ai-button.d.ts","import":"./dist/ai-button.js"},"./container":{"types":"./dist/container.d.ts","import":"./dist/container.js"},"./lib/focus":{"types":"./dist/lib/focus.d.ts","import":"./dist/lib/focus.js"},"./lib/icons":{"types":"./dist/lib/icons.d.ts","import":"./dist/lib/icons.js"},"./lib/reset":{"types":"./dist/lib/reset.d.ts","import":"./dist/lib/reset.js"},"./lib/sizes":{"types":"./dist/lib/sizes.d.ts","import":"./dist/lib/sizes.js"},"./pin-input":{"types":"./dist/pin-input.d.ts","import":"./dist/pin-input.js"},"./separator":{"types":"./dist/separator.d.ts","import":"./dist/separator.js"},"./disclosure":{"types":"./dist/disclosure.d.ts","import":"./dist/disclosure.js"},"./form-field":{"types":"./dist/form-field.d.ts","import":"./dist/form-field.js"},"./lib/labels":{"types":"./dist/lib/labels.d.ts","import":"./dist/lib/labels.js"},"./styles.css":"./dist/styles/index.css","./empty-state":{"types":"./dist/empty-state.d.ts","import":"./dist/empty-state.js"},"./field-error":{"types":"./dist/field-error.d.ts","import":"./dist/field-error.js"},"./file-upload":{"types":"./dist/file-upload.d.ts","import":"./dist/file-upload.js"},"./lib/spinner":{"types":"./dist/lib/spinner.d.ts","import":"./dist/lib/spinner.js"},"./radio-group":{"types":"./dist/radio-group.d.ts","import":"./dist/radio-group.js"},"./number-input":{"types":"./dist/number-input.d.ts","import":"./dist/number-input.js"},"./package.json":"./package.json","./search-input":{"types":"./dist/search-input.d.ts","import":"./dist/search-input.js"},"./tailwind.css":"./dist/styles/tailwind.css","./lib/telemetry":{"types":"./dist/lib/telemetry.d.ts","import":"./dist/lib/telemetry.js"},"./native-select":{"types":"./dist/native-select.d.ts","import":"./dist/native-select.js"},"./textarea-field":{"types":"./dist/textarea-field.d.ts","import":"./dist/textarea-field.js"},"./command-palette":{"types":"./dist/command-palette.d.ts","import":"./dist/command-palette.js"},"./lib/transitions":{"types":"./dist/lib/transitions.d.ts","import":"./dist/lib/transitions.js"},"./date-range-input":{"types":"./dist/date-range-input.d.ts","import":"./dist/date-range-input.js"},"./lib/use-autosave":{"types":"./dist/lib/use-autosave.d.ts","import":"./dist/lib/use-autosave.js"},"./resizable-panels":{"types":"./dist/resizable-panels.d.ts","import":"./dist/resizable-panels.js"},"./field-description":{"types":"./dist/field-description.d.ts","import":"./dist/field-description.js"},"./lib/control-frame":{"types":"./dist/lib/control-frame.d.ts","import":"./dist/lib/control-frame.js"},"./lib/field-context":{"types":"./dist/lib/field-context.d.ts","import":"./dist/lib/field-context.js"},"./lib/form-variants":{"types":"./dist/lib/form-variants.d.ts","import":"./dist/lib/form-variants.js"},"./lib/popover-panel":{"types":"./dist/lib/popover-panel.d.ts","import":"./dist/lib/popover-panel.js"},"./zui-no-preflight.css":"./dist/zui-no-preflight.css"},"scripts":{"test":"node scripts/check-logical.mjs && vitest run","build":"tsup","typecheck":"tsc --noEmit -p tsconfig.json","check:logical":"node scripts/check-logical.mjs","registry:build":"node scripts/build-registry.mjs"},"_npmUser":{"name":"mahuzedada","email":"chatis@afrointelligence.com"},"_resolved":"/private/var/folders/mn/n3l7wn4s7bzbxsb8xcdwyvyc0000gn/T/89bcdb20218e5df98e2d2b7e3f1a3f8c/zuilib-primitives-0.3.0.tgz","_integrity":"sha512-TcdYVzB/iqIfS0wrAuj0c2v5Awy4OGJgJIqR03+8JlkY0eLpZoyobDemPSqgqb93zYd5TAxxhCpZrjRpv3d3Vw==","_npmVersion":"11.13.0","description":"ZUI primitives — accessible React components on Base UI and Tailwind v4, plus the react-hook-form binding and toasts","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.17.0","dependencies":{"clsx":"^2.1.1","sonner":"^1.7.1","@base-ui/react":"^1.8.0","@zuilib/tokens":"^0.2.1","tailwind-merge":"3.4.0","@floating-ui/react":"^0.26.0"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^4.1.11","axe-core":"^4.13.0","typescript":"^6.0.2","tailwindcss":"^4.1.17","@types/react":"^18.0.0","react-hook-form":"^7.0.0","@tailwindcss/cli":"^4.1.17","@types/react-dom":"^18.0.0","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","tailwindcss":"^4.0.0","react-hook-form":"^7.0.0"},"peerDependenciesMeta":{"tailwindcss":{"optional":true},"react-hook-form":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/primitives_0.3.0_1788759549298_0.8427900677447966","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"_id":"@zuilib/primitives@0.4.0","dist":{"shasum":"4ad0d3b44cfa70f0805437643a1f4af57642f7bc","tarball":"https://registry.npmjs.org/@zuilib/primitives/-/primitives-0.4.0.tgz","fileCount":155,"integrity":"sha512-Fj94ky/XYfZwCn6pYMm2GXLdzuvp7WoswIQXIReiIdB/CI3PPK1YV+D/f2K0CmTS/freXsLq6E9ual0vVpO4zw==","signatures":[{"sig":"MEYCIQCFE9d+8NRz1dTQZogH8iqZuq9XLHIIRXcwsc14ZiMlcgIhAL8NpklXVlhnVszvMfiJPsGoBXEIqMnoOM1zqGb1n14v","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHWbNs5KBD0YGagkf3eG/N7tRXNlhcxtW/+22GAyYrfHAiBdkd2Xz65rfY6enzBj3BHnqoZ+4spSor8QeSMVHp/Ysg=="}],"unpackedSize":1329174},"name":"@zuilib/primitives","type":"module","_from":"file:zuilib-primitives-0.4.0.tgz","exports":{"./kbd":{"types":"./dist/kbd.d.ts","import":"./dist/kbd.js"},"./card":{"types":"./dist/card.d.ts","import":"./dist/card.js"},"./code":{"types":"./dist/code.d.ts","import":"./dist/code.js"},"./form":{"types":"./dist/form.d.ts","import":"./dist/form.js"},"./menu":{"types":"./dist/menu.d.ts","import":"./dist/menu.js"},"./tabs":{"types":"./dist/tabs.d.ts","import":"./dist/tabs.js"},"./text":{"types":"./dist/text.d.ts","import":"./dist/text.js"},"./alert":{"types":"./dist/alert.d.ts","import":"./dist/alert.js"},"./badge":{"types":"./dist/badge.d.ts","import":"./dist/badge.js"},"./field":{"types":"./dist/field.d.ts","import":"./dist/field.js"},"./input":{"types":"./dist/input.d.ts","import":"./dist/input.js"},"./label":{"types":"./dist/label.d.ts","import":"./dist/label.js"},"./stack":{"types":"./dist/stack.d.ts","import":"./dist/stack.js"},"./table":{"types":"./dist/table.d.ts","import":"./dist/table.js"},"./toast":{"types":"./dist/toast.d.ts","import":"./dist/toast.js"},"./avatar":{"types":"./dist/avatar.d.ts","import":"./dist/avatar.js"},"./button":{"types":"./dist/button.d.ts","import":"./dist/button.js"},"./dialog":{"types":"./dist/dialog.d.ts","import":"./dist/dialog.js"},"./drawer":{"types":"./dist/drawer.d.ts","import":"./dist/drawer.js"},"./lib/cn":{"types":"./dist/lib/cn.d.ts","import":"./dist/lib/cn.js"},"./select":{"types":"./dist/select.d.ts","import":"./dist/select.js"},"./slider":{"types":"./dist/slider.d.ts","import":"./dist/slider.js"},"./switch":{"types":"./dist/switch.d.ts","import":"./dist/switch.js"},"./heading":{"types":"./dist/heading.d.ts","import":"./dist/heading.js"},"./popover":{"types":"./dist/popover.d.ts","import":"./dist/popover.js"},"./spinner":{"types":"./dist/spinner.d.ts","import":"./dist/spinner.js"},"./stepper":{"types":"./dist/stepper.d.ts","import":"./dist/stepper.js"},"./tooltip":{"types":"./dist/tooltip.d.ts","import":"./dist/tooltip.js"},"./zui.css":"./dist/zui.css","./activity":{"types":"./dist/activity.d.ts","import":"./dist/activity.js"},"./checkbox":{"types":"./dist/checkbox.d.ts","import":"./dist/checkbox.js"},"./combobox":{"types":"./dist/combobox.d.ts","import":"./dist/combobox.js"},"./fieldset":{"types":"./dist/fieldset.d.ts","import":"./dist/fieldset.js"},"./presence":{"types":"./dist/presence.d.ts","import":"./dist/presence.js"},"./progress":{"types":"./dist/progress.d.ts","import":"./dist/progress.js"},"./skeleton":{"types":"./dist/skeleton.d.ts","import":"./dist/skeleton.js"},"./textarea":{"types":"./dist/textarea.d.ts","import":"./dist/textarea.js"},"./accordion":{"types":"./dist/accordion.d.ts","import":"./dist/accordion.js"},"./ai-button":{"types":"./dist/ai-button.d.ts","import":"./dist/ai-button.js"},"./container":{"types":"./dist/container.d.ts","import":"./dist/container.js"},"./lib/focus":{"types":"./dist/lib/focus.d.ts","import":"./dist/lib/focus.js"},"./lib/icons":{"types":"./dist/lib/icons.d.ts","import":"./dist/lib/icons.js"},"./lib/reset":{"types":"./dist/lib/reset.d.ts","import":"./dist/lib/reset.js"},"./lib/sizes":{"types":"./dist/lib/sizes.d.ts","import":"./dist/lib/sizes.js"},"./pin-input":{"types":"./dist/pin-input.d.ts","import":"./dist/pin-input.js"},"./separator":{"types":"./dist/separator.d.ts","import":"./dist/separator.js"},"./disclosure":{"types":"./dist/disclosure.d.ts","import":"./dist/disclosure.js"},"./form-field":{"types":"./dist/form-field.d.ts","import":"./dist/form-field.js"},"./lib/labels":{"types":"./dist/lib/labels.d.ts","import":"./dist/lib/labels.js"},"./styles.css":"./dist/styles/index.css","./empty-state":{"types":"./dist/empty-state.d.ts","import":"./dist/empty-state.js"},"./field-error":{"types":"./dist/field-error.d.ts","import":"./dist/field-error.js"},"./file-upload":{"types":"./dist/file-upload.d.ts","import":"./dist/file-upload.js"},"./lib/spinner":{"types":"./dist/lib/spinner.d.ts","import":"./dist/lib/spinner.js"},"./radio-group":{"types":"./dist/radio-group.d.ts","import":"./dist/radio-group.js"},"./number-input":{"types":"./dist/number-input.d.ts","import":"./dist/number-input.js"},"./package.json":"./package.json","./search-input":{"types":"./dist/search-input.d.ts","import":"./dist/search-input.js"},"./tailwind.css":"./dist/styles/tailwind.css","./native-select":{"types":"./dist/native-select.d.ts","import":"./dist/native-select.js"},"./textarea-field":{"types":"./dist/textarea-field.d.ts","import":"./dist/textarea-field.js"},"./command-palette":{"types":"./dist/command-palette.d.ts","import":"./dist/command-palette.js"},"./lib/transitions":{"types":"./dist/lib/transitions.d.ts","import":"./dist/lib/transitions.js"},"./activity/testing":{"types":"./dist/activity/testing.d.ts","import":"./dist/activity/testing.js"},"./date-range-input":{"types":"./dist/date-range-input.d.ts","import":"./dist/date-range-input.js"},"./lib/use-autosave":{"types":"./dist/lib/use-autosave.d.ts","import":"./dist/lib/use-autosave.js"},"./resizable-panels":{"types":"./dist/resizable-panels.d.ts","import":"./dist/resizable-panels.js"},"./activity/adapters":{"types":"./dist/activity/adapters.d.ts","import":"./dist/activity/adapters.js"},"./field-description":{"types":"./dist/field-description.d.ts","import":"./dist/field-description.js"},"./lib/control-frame":{"types":"./dist/lib/control-frame.d.ts","import":"./dist/lib/control-frame.js"},"./lib/field-context":{"types":"./dist/lib/field-context.d.ts","import":"./dist/lib/field-context.js"},"./lib/form-variants":{"types":"./dist/lib/form-variants.d.ts","import":"./dist/lib/form-variants.js"},"./lib/popover-panel":{"types":"./dist/lib/popover-panel.d.ts","import":"./dist/lib/popover-panel.js"},"./zui-no-preflight.css":"./dist/zui-no-preflight.css"},"license":"MIT","scripts":{"test":"node scripts/check-logical.mjs && vitest run","build":"tsup","typecheck":"tsc --noEmit -p tsconfig.json","check:logical":"node scripts/check-logical.mjs","registry:build":"node scripts/build-registry.mjs"},"version":"0.4.0","_npmUser":{"name":"mahuzedada","email":"chatis@afrointelligence.com"},"keywords":["design-system","component-library","tailwindcss","base-ui","react-hook-form","sonner","react","typescript"],"_resolved":"/private/var/folders/mn/n3l7wn4s7bzbxsb8xcdwyvyc0000gn/T/c07266fb057651bf9f8b3819b43c0919/zuilib-primitives-0.4.0.tgz","_integrity":"sha512-Fj94ky/XYfZwCn6pYMm2GXLdzuvp7WoswIQXIReiIdB/CI3PPK1YV+D/f2K0CmTS/freXsLq6E9ual0vVpO4zw==","_npmVersion":"11.13.0","description":"ZUI primitives — accessible React components on Base UI and Tailwind v4, plus the react-hook-form binding and toasts","directories":{},"maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"sideEffects":["**/*.css"],"_nodeVersion":"24.17.0","dependencies":{"clsx":"^2.1.1","sonner":"^1.7.1","@base-ui/react":"^1.8.0","@zuilib/tokens":"^0.2.1","tailwind-merge":"3.4.0","@floating-ui/react":"^0.26.0"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^4.1.11","axe-core":"^4.13.0","typescript":"^6.0.2","tailwindcss":"^4.1.17","@types/react":"^18.0.0","react-hook-form":"^7.0.0","@tailwindcss/cli":"^4.1.17","@types/react-dom":"^18.0.0","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","tailwindcss":"^4.0.0","react-hook-form":"^7.0.0"},"peerDependenciesMeta":{"tailwindcss":{"optional":true},"react-hook-form":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/primitives_0.4.0_1789964938418_0.897856357307862"}}},"time":{"created":"2026-09-07T05:39:09.162Z","modified":"2026-09-21T04:28:58.735Z","0.3.0":"2026-09-07T05:39:09.435Z","0.4.0":"2026-09-21T04:28:58.534Z"},"keywords":["design-system","component-library","tailwindcss","base-ui","react-hook-form","sonner","react","typescript"],"description":"ZUI primitives — accessible React components on Base UI and Tailwind v4, plus the react-hook-form binding and toasts","maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"readme":"# @zuilib/primitives\n\n> Renamed from `@zuilib/components` (and before that `@zuilib/core`). The design tokens live in [`@zuilib/tokens`](../tokens); `@zuilib/primitives/styles.css` loads them for you. The former `@zuilib/form` and `@zuilib/toast` packages are folded in as the `form`, `form-field`, `lib/use-autosave` and `toast` subpaths.\n\nReact UI primitives for the ZUI design system. Built on **Base UI** and **Tailwind CSS v4**, with subpath exports for tree-shaking.\n\n## When to use\n\n- Building forms, settings panels, or any UI that needs accessible inputs and actions\n- Toast notifications (`toast`, on Sonner) and a react-hook-form binding (`form`, `form-field`) ship in the same package; each is its own subpath, so an app that imports neither pays for neither\n- The data grid, charts, AI components, Apps view/runtime layer and the text editor are separate packages on the same tokens\n\n## Prerequisites\n\n| Package | Version |\n|---------|---------|\n| `react` | `^18.0.0` |\n| `react-dom` | `^18.0.0` |\n| `react-hook-form` | `^7.0.0` — optional peer, only for `form` / `form-field` |\n\nBase UI, ZUI tokens, Floating UI, Sonner and the class-name utilities are\nregular dependencies and install with the package. Sonner is only loaded by\nthe `toast` subpath.\n\nThe components are styled with Tailwind v4 utility classes. Either your app runs Tailwind v4 and generates them from `@zuilib/primitives/styles.css`, or you load the prebuilt `@zuilib/primitives/zui.css` (no Tailwind needed). See Setup.\n\n## Installation\n\n**Monorepo (this repo):**\n\n```json\n{ \"dependencies\": { \"@zuilib/primitives\": \"workspace:*\" } }\n```\n\n**External app:**\n\n```bash\npnpm add @zuilib/primitives\n```\n\nReact and React DOM remain peers so the package shares the application's React\nruntime. To evaluate an unpublished workspace build from tarballs (`pnpm pack`\nin `packages/tokens` and `packages/primitives`), install both tarballs because\nthe unpublished primitives tarball refers to the unpublished token version:\n\n```json\n{\n  \"dependencies\": {\n    \"@zuilib/primitives\": \"file:./zuilib-primitives-0.2.0.tgz\",\n    \"@zuilib/tokens\": \"file:./zuilib-tokens-0.2.0.tgz\"\n  }\n}\n```\n\n## Setup (required once per app)\n\nLoad exactly one of the four stylesheets. Each gives you the tokens, the base styles and every utility the components use; they differ in who runs Tailwind and whether Tailwind's preflight reset is included.\n\n| Your app | Import | What it is |\n|---|---|---|\n| Tailwind v4, and you do **not** `@import \"tailwindcss\"` yourself | `@zuilib/primitives/styles.css` | All-in-one Tailwind source: `@import \"tailwindcss\"` (with preflight) + the component `@source` + tokens. Your pipeline compiles it |\n| Tailwind v4, and you **already** `@import \"tailwindcss\"` | `@zuilib/primitives/tailwind.css` | Tailwind source without Tailwind: the component `@source` + tokens (`@zuilib/tokens/tailwind.css` + `tokens.css`) + the slider's vendor CSS. Import it after your `tailwindcss` import |\n| No Tailwind | `@zuilib/primitives/zui.css` | Prebuilt, flat CSS with Tailwind's preflight reset. Nothing to process |\n| No Tailwind, own reset / base styles | `@zuilib/primitives/zui-no-preflight.css` | The same prebuilt CSS built from Tailwind's `theme` and `utilities` layers only: no preflight, so your reset stays in charge |\n\nImporting `styles.css` twice or next to a second `@import \"tailwindcss\"` duplicates Tailwind; use `tailwind.css` when Tailwind is already there.\n\n**Tailwind app, one line** — `styles.css` is a Tailwind *source* file. Import it from your app's CSS so your Tailwind pipeline (`@tailwindcss/vite`, `@tailwindcss/postcss`, the CLI) processes it; Tailwind's automatic source detection stays on, so the token utilities (`bg-primary`, `rounded-md`, …) work in your own markup with no extra `@source` line:\n\n```css\n/* app.css */\n@import \"@zuilib/primitives/styles.css\";\n```\n\n**Tailwind app that already imports Tailwind** (for its own `source()` / `@theme` / plugin setup):\n\n```css\n/* app.css */\n@import \"tailwindcss\";\n@import \"@zuilib/primitives/tailwind.css\";\n```\n\n`tailwind.css` carries `@source \"../**/*.{js,ts,tsx}\"`, resolved relative to the file, so the component JS under `node_modules/@zuilib/primitives/dist` is scanned even though Tailwind skips `node_modules` by default. `tailwindcss` is an optional peer dependency for both Tailwind paths (`pnpm add -D tailwindcss @tailwindcss/vite`).\n\n**App without Tailwind** — `zui.css` is the prebuilt, flat bundle; `zui-no-preflight.css` the same without the reset. Import one at the app entry:\n\n```tsx\n// main.tsx\nimport '@zuilib/primitives/zui.css' // or '@zuilib/primitives/zui-no-preflight.css'\n```\n\nImporting `styles.css` or `tailwind.css` from JavaScript or a non-Tailwind bundler does not work: their `@import \"tailwindcss\"` / `@source` are only meaningful to Tailwind.\n\n**Dark mode:** add or remove the `dark` class on `<html>` or a root wrapper.\n\n## Import rules\n\n| Rule | Detail |\n|------|--------|\n| **One subpath per component** | `import Button from '@zuilib/primitives/button'` (a named export `{ Button }` exists too) — no barrel `import { Button } from '@zuilib/primitives'` |\n| **Styles are separate** | Components do not auto-inject CSS; you must load one stylesheet: `styles.css` / `tailwind.css` (Tailwind app) or `zui.css` / `zui-no-preflight.css` (prebuilt). See Setup |\n| **Utilities** | `import { cn } from '@zuilib/primitives/lib/cn'` for class merging; `import SpinnerIcon from '@zuilib/primitives/lib/spinner'`; `lib/focus`, `lib/sizes`, `lib/field-context` for custom controls that should match |\n\n## Exports\n\n| Subpath | Default export | Purpose |\n|---------|----------------|---------|\n| `@zuilib/primitives/styles.css` | — | Tailwind v4 source: Tailwind + tokens + `@source` for the components. Needs a Tailwind pipeline |\n| `@zuilib/primitives/tailwind.css` | — | Tailwind v4 source without `@import \"tailwindcss\"`: tokens + `@source` for the components, for apps that import Tailwind themselves |\n| `@zuilib/primitives/zui.css` | — | Prebuilt CSS: tokens + the utilities the components use + preflight. No Tailwind needed |\n| `@zuilib/primitives/zui-no-preflight.css` | — | Prebuilt CSS without Tailwind's preflight reset, for apps with their own reset |\n| `@zuilib/primitives/button` | `Button` | Primary actions |\n| `@zuilib/primitives/ai-button` | `AIButton` | Button with sparkle / generating state |\n| `@zuilib/primitives/input` | `Input` | Text input |\n| `@zuilib/primitives/textarea` | `Textarea` | Multiline text |\n| `@zuilib/primitives/textarea-field` | `TextareaField` | Textarea with label, description, error, character count |\n| `@zuilib/primitives/checkbox` | `Checkbox` | Boolean / indeterminate toggle |\n| `@zuilib/primitives/switch` | `Switch` | Boolean toggle |\n| `@zuilib/primitives/select` | `Select` | Single- or multi-select dropdown (compound parts as statics) |\n| `@zuilib/primitives/native-select` | `NativeSelect` | Native `<select>` |\n| `@zuilib/primitives/combobox` | `Combobox` | Searchable single- or multi-select (compound parts as statics) |\n| `@zuilib/primitives/radio-group` | `RadioGroup` | Radio options, card or list (compound parts as statics) |\n| `@zuilib/primitives/fieldset` | `Fieldset` | Grouped fields with legend |\n| `@zuilib/primitives/field` | `Field` | Field wrapper (Base UI `Field`): wires label / description / control ids |\n| `@zuilib/primitives/label` | `Label` | Accessible label |\n| `@zuilib/primitives/field-description` | `FieldDescription` | Helper text |\n| `@zuilib/primitives/field-error` | `FieldError` | Validation / error text |\n| `@zuilib/primitives/disclosure` | `Disclosure` | Collapsible section |\n| `@zuilib/primitives/stepper` | `Stepper` | Multi-step progress |\n| `@zuilib/primitives/slider` | `Slider` | Native range input with label, value read-out and marks |\n| `@zuilib/primitives/number-input` | `NumberInput` | Input with stepper, clamping and number formatting |\n| `@zuilib/primitives/pin-input` | `PinInput` | One-time-code / PIN cells |\n| `@zuilib/primitives/file-upload` | `FileUpload` | Drop zone + file list |\n| `@zuilib/primitives/date-range-input` | `DateRangeInput` | Controlled or uncontrolled start/end date pair |\n| `@zuilib/primitives/search-input` | `SearchInput` | Search field with clear button and loading state |\n| `@zuilib/primitives/card` | `Card` | Surface with header / content / footer (compound parts as statics) |\n| `@zuilib/primitives/container` | `Container` | Centred max-width wrapper |\n| `@zuilib/primitives/stack` | `Stack` | Flex row / column with gap and separators |\n| `@zuilib/primitives/separator` | `Separator` | Horizontal / vertical rule, optional label |\n| `@zuilib/primitives/heading` | `Heading` | `h1`–`h6` on the type scale |\n| `@zuilib/primitives/text` | `Text` | Body text with size / weight / tone |\n| `@zuilib/primitives/code` | `Code` | Inline code or code block |\n| `@zuilib/primitives/kbd` | `Kbd` | Keyboard key / shortcut |\n| `@zuilib/primitives/alert` | `Alert` | Inline status message, dismissible (compound parts as statics) |\n| `@zuilib/primitives/badge` | `Badge` | Label / tag, removable |\n| `@zuilib/primitives/avatar` | `Avatar` | Image with initials fallback and status; `Avatar.Group` |\n| `@zuilib/primitives/presence` | `Presence` | Compact avatar stack for active collaborators |\n| `@zuilib/primitives/skeleton` | `Skeleton` | Loading placeholder |\n| `@zuilib/primitives/spinner` | `Spinner` | Loading indicator with live-region label |\n| `@zuilib/primitives/progress` | `Progress` | Determinate / indeterminate progress bar |\n| `@zuilib/primitives/empty-state` | `EmptyState` | Icon + title + description + actions |\n| `@zuilib/primitives/tooltip` | `Tooltip` | Hover / focus tooltip (floating-ui) |\n| `@zuilib/primitives/dialog` | `Dialog` | Modal dialog (compound parts as statics) |\n| `@zuilib/primitives/command-palette` | `CommandPalette` | ⌘K launcher: Dialog + Combobox, groups, recent items, keyboard shortcut |\n| `@zuilib/primitives/drawer` | `Drawer` | Side / top / bottom sheet (compound parts as statics) |\n| `@zuilib/primitives/popover` | `Popover` | Anchored floating panel (compound parts as statics) |\n| `@zuilib/primitives/menu` | `Menu` | Dropdown action menu (compound parts as statics) |\n| `@zuilib/primitives/tabs` | `Tabs` | Tab list + panels (compound parts as statics) |\n| `@zuilib/primitives/accordion` | `Accordion` | Single / multiple expandable items (compound parts as statics) |\n| `@zuilib/primitives/table` | `Table` | Styled table primitives (compound parts as statics), no TanStack; data grids live in the separate `@zuilib/data-grid` package |\n| `@zuilib/primitives/resizable-panels` | `ResizablePanels` | Keyboard- and pointer-resizable two-panel layout |\n| `@zuilib/primitives/toast` | `Toaster` | Sonner toaster on the tokens; `toast` (+ `Toast`, `ToastOptions` types) re-exported |\n| `@zuilib/primitives/form` | `Form` | react-hook-form `FormProvider` + `<form>` wired to `handleSubmit` (needs the `react-hook-form` peer) |\n| `@zuilib/primitives/form-field` | `FormField` | react-hook-form `Controller` inside a `Field`: error, ids and optional autosave as render props |\n| `@zuilib/primitives/lib/cn` | `cn` | `clsx` + `tailwind-merge` helper |\n| `@zuilib/primitives/lib/spinner` | `SpinnerIcon` | Indeterminate spinner icon (also a named export) |\n| `@zuilib/primitives/lib/icons` | — | `ChevronDownIcon`, `CheckIcon`, `CloseIcon`, `SearchIcon`, `InfoIcon` / `SuccessIcon` / `WarningIcon` / `DangerIcon` (+ `statusIcons`): the glyphs the components share |\n| `@zuilib/primitives/lib/transitions` | — | `fadeTransitionClasses`, `scaleFadeTransitionClasses`, `slideTransitionClasses()`: the `data-[closed]` enter / leave recipes for Base UI's `transition` prop |\n| `@zuilib/primitives/lib/focus` | — | `focusRingClasses`, `focusRingInsetClasses`, `dataFocusRingClasses`, `dataFocusRingInsetClasses`, `focusOutlineInsetClasses`, `fieldFocusRingClasses`: the shared focus-ring class strings |\n| `@zuilib/primitives/lib/sizes` | — | `ControlSize`, `controlSizeClasses`, `iconSizeClasses`, … the shared size maps; `touchTargetClasses` / `touchHitAreaClasses` for 44px touch targets |\n| `@zuilib/primitives/lib/reset` | — | `nativeControlResetClasses`: zeroes the user-agent styles of a native `<button>` (padding, border, background, font) so button-rendered parts look the same without Tailwind's preflight. Put it first in `cn()` |\n| `@zuilib/primitives/lib/field-context` | — | `useField()`, `FieldContext`, `fieldStateAttributes()` for custom controls inside a `Field` |\n| `@zuilib/primitives/lib/form-variants` | — | `formVariantClasses`, `formValidityClasses` (the field frame styles) |\n| `@zuilib/primitives/lib/control-frame` | — | `splitControlProps()` / `splitHoverHandlers()` for frame + native-element controls |\n| `@zuilib/primitives/lib/popover-panel` | — | `popoverSurfaceClasses`, `popoverPanelClassName()`, the anchor classes and the option-row classes Select / Combobox / Menu / Popover / Dialog share |\n| `@zuilib/primitives/lib/labels` | — | `LabelsProvider`, `useLabels()`, `defaultLabels`, `Labels`: every built-in string, translated once |\n| `@zuilib/primitives/activity` | — | `ActivityProvider`, `ActivityScope`, `useActivity`, `useActivityTag`, `TrackName`, `createActivityTracker`, `attachActivityCapture`: user-activity tracking |\n| `@zuilib/primitives/activity/adapters` | — | `gtagAdapter`, `dataLayerAdapter`, `heapAdapter`, `segmentAdapter`, `amplitudeAdapter`, `postHogAdapter`, `mixpanelAdapter`, `plausibleAdapter`, `endpointAdapter`, `consoleAdapter`, `memoryAdapter`, `registerActivityAdapterType` |\n| `@zuilib/primitives/activity/testing` | — | `createActivityHarness`, `memoryAdapter`: assert on the events a test produced |\n| `@zuilib/primitives/lib/use-autosave` | — | `useAutosave()`, `AutosaveState`, `AutosaveStatus`: the debounced save behind `FormField`'s `onAutosave` |\n\n## Quick start\n\n```tsx\nimport '@zuilib/primitives/zui.css' // or `@import \"@zuilib/primitives/styles.css\"` in a Tailwind app's CSS\nimport Button from '@zuilib/primitives/button'\nimport Input from '@zuilib/primitives/input'\n\nexport function Example() {\n  return (\n    <form className=\"flex flex-col gap-4 max-w-sm\">\n      <Input track=\"name\" placeholder=\"Name\" fullWidth />\n      <Button track=\"save\" type=\"submit\">Save</Button>\n    </form>\n  )\n}\n```\n\n## Component API\n\n### Shared conventions\n\nEvery component follows the same contract, so what you learn on one applies to the rest:\n\n| Convention | Detail |\n|---|---|\n| `variant` / `tone` | `variant` is visual style only (`solid \\| outline \\| ghost \\| link` on Button, `solid \\| subtle \\| outline` on Badge, …); `tone` is the semantic colour (`neutral \\| primary \\| success \\| warning \\| danger \\| info`, a subset per component). The word `destructive` does not exist in this library: the dangerous tone is `danger` |\n| Controlled state | Every piece of controlled state ships the full triad `x` / `defaultX` / `onXChange` (`open` / `defaultOpen` / `onOpenChange`, `value` / `defaultValue` / `onValueChange`, `checked` / `defaultChecked` / `onCheckedChange`). There is no `onClose` / `onDismiss`: close events are `onOpenChange(false)` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` on every sized control (Button adds `'icon'`, Spinner and Avatar add `'xl'`, Table and Badge stop at `'md'`). Heights and paddings come from `--control-height-*` / `--control-padding-x-*`; one density retheme moves every component |\n| `invalid` | Sets `aria-invalid` + `data-invalid` and the danger border / ring. Leave it unset inside a `Field invalid` to inherit it |\n| `disabled` | Leave it unset inside a `Field disabled` / `Fieldset disabled` to inherit it (Base UI only inherits when the prop is `undefined`) |\n| `fullWidth` | `w-full` on the root. Defaults to `false` on Input / Textarea / NativeSelect / Button and to `true` on Select / Combobox |\n| `name` / `form` | Native form participation. Select, Combobox, RadioGroup, Checkbox and Switch render hidden inputs for it |\n| `anchor` / `portal` | Select and Combobox: where the panel floats (`'bottom start'` default, `false` for an inline panel) and whether it is portalled to `<body>` (default `true` when anchored). `portal={false}` keeps the portal container inside the component root so subtree themes still apply |\n| `className` | Merged last onto the root. Controls that wrap a native element in a frame also take `inputClassName` / `textareaClassName` for the element itself |\n| Native reset | Every part that renders a native `<button>` (Button, Checkbox, Switch, Tab, Select / Combobox / Popover / Menu buttons, close and remove buttons, steppers) starts its `cn()` with `nativeControlResetClasses` from `lib/reset`, so the user-agent padding, border and font never leak through when Tailwind's preflight is absent |\n| Focus ring | Standalone controls (Button, Checkbox, Switch, Radio, Disclosure, Stepper): a 2px `--ring` ring offset by 2px of `--background`, keyboard focus only. Fields (Input, Textarea, NativeSelect, Select button, Combobox input): a 1px ring hugging the border, coloured by validity |\n| `data-slot` | Every root and every part (`[data-slot=\"select-option\"]`), listed per component below. Visual state is `data-state` where Base UI does not already provide a data attribute |\n| Icons | The built-in glyphs (chevron, check, cross, search, the four status icons) come from `lib/icons`: `currentColor`, `aria-hidden`, `1em` unless a `size-*` class is given. Sized beside text by the shared `iconSizeClasses` (`size-4` / `size-5` / `size-6` for `sm` / `md` / `lg`) |\n| Overlays | Select, Combobox, Menu and Popover panels share the `bg-popover` surface from `lib/popover-panel`; Dialog and Drawer use the same surface with `shadow-4`. Enter / leave motion comes from `lib/transitions` (fade, scale + fade, slide) through Base UI's `data-closed`, off under `prefers-reduced-motion` |\n| Compound parts | Attached as statics (`Select.Option`) and also named exports (`SelectOption`) |\n| Strings | Every string a component renders on its own (`Close`, `Loading`, `Toggle options`, `No results found.`, `Digit n of N`, …) is a prop with an English default read from `LabelsProvider` (`lib/labels`): translate once at the root, override per element with the prop |\n| Activity tracking | Required, not opt-in. Every interactive primitive takes `track: string \\| false` (Badge with `onRemove`, sortable `Table.Head` and dismissible `Alert` in that mode), written as `data-zui-tag` — or `data-zui-untracked` for `false`. Under an `ActivityProvider` clicks, field changes, submissions and route changes are read from the DOM; Dialog / Drawer open and close, Tabs changes, Combobox and CommandPalette selections are reported by the component, since they also happen by keyboard. Portaled surfaces carry their owner's feature and name |\n| Density | Heights and paddings come from `--control-height-*` / `--control-padding-x-*`, so a density mode is a token override on a subtree (`[data-zui-density=\"compact\"] { --control-height-md: 2rem }`), not a prop |\n| Print | Dialog, Drawer, Popover.Panel and Tooltip carry `print:hidden`; an elevated Card drops its shadow (`print:shadow-none`) |\n| Phones and touch | Mobile-first, desktop untouched. Below the `sm` breakpoint (640px) a Dialog is the viewport minus a 1rem gutter, a start / end Drawer is full-width, a top / bottom Drawer at most `85dvh`, the CommandPalette a full-width sheet pinned to the top, `md` / `lg` Card and Accordion padding step down one token, and `sm` fields type at 16px so iOS does not zoom on focus. Floating panels (Select, Combobox, Menu, Popover, Tooltip) never exceed `100vw - 2rem` and cap their height in `dvh`. On a coarse pointer (`pointer-coarse:`) tabs, menu items, list options and the Table sort button are at least 44px tall, and Button, the close / clear buttons, the Checkbox box, Radio indicator, Switch track and Slider thumb take a 44 × 44 hit area without changing size (`touchTargetClasses` / `touchHitAreaClasses` from `lib/sizes` for your own controls). Fixed UI (Drawer, the `full` Dialog, toasts) pads past `env(safe-area-inset-*)` |\n| Direction | Only logical utilities (`ps-` / `pe-` / `start` / `end` / `text-start`), enforced by `scripts/check-logical.mjs`; PinInput's arrow keys follow `dir`. Public direction values are logical too (`Drawer side=\"start\" | \"end\"`) |\n\n### Button — `@zuilib/primitives/button`\n\n```tsx\n<Button track=\"save\" size=\"md\" loading={saving} leadingIcon={<PlusIcon />}>Save</Button>\n<Button track=\"delete\" variant=\"solid\" tone=\"danger\">Delete</Button>\n<Button track=\"docs\" as=\"a\" href=\"/docs\" variant=\"link\" tone=\"primary\">Docs</Button>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `variant` | `'solid' \\| 'outline' \\| 'ghost' \\| 'link'` | `'solid'` — visual style only |\n| `tone` | `'primary' \\| 'neutral' \\| 'danger'` | `'primary'` for `solid`, `'neutral'` for `outline` / `ghost` / `link` |\n| `size` | `'sm' \\| 'md' \\| 'lg' \\| 'icon'` | `'md'` — `icon` is a square the height of an `md` control |\n| `fullWidth` | `boolean` | `false` |\n| `loading` | `boolean` | `false` — shows a spinner, sets `aria-busy`, swallows clicks, keeps focus |\n| `loadingLabel` | `string` | `'Loading'` (`Labels.loading`) — visually hidden, announced with the spinner |\n| `track` | `string \\| false` | **required** — the analytics name every click reports under (`data-zui-tag`); `false` opts out deliberately |\n| `leadingIcon` / `trailingIcon` | `ReactNode` | — `leadingIcon` is replaced by the spinner while `loading` |\n| `as` | `ElementType` | `'button'` — polymorphic: `<Button track=\"send\" as=\"a\" href>` types `href` from the anchor; a disabled non-button gets `aria-disabled` and leaves the tab order |\n\nExtends Base UI `Button` props (`onClick`, `disabled`, `type`, …). Slots: `button` (with `data-variant`, `data-tone`, `data-size`, `data-loading`), `button-spinner`, `button-leading-icon`, `button-trailing-icon`, `button-content` (icon-size button while loading). SVG children without a `size-*` class are sized `1em`.\n\n### AIButton — `@zuilib/primitives/ai-button`\n\nSame as `Button` (including `as`), minus `loading` / `loadingLabel` / `leadingIcon`, plus:\n\n| Prop | Type | Default |\n|------|------|---------|\n| `generating` | `boolean` | `false` — Button `loading` with the generating text announced |\n| `generatingLabel` | `string` | `'Generating'` (`Labels.generating`) — visually hidden, announced with the spinner |\n| `sparkle` | `boolean` | `true` — sparkle icon before the label |\n\nThe root keeps `data-slot=\"button\"` (every button rule applies) and adds `data-ai-button` and `data-generating`; the icon is `ai-button-sparkle`.\n\n### Input / Textarea — `@zuilib/primitives/input`, `@zuilib/primitives/textarea`\n\n```tsx\n<Input track=\"outline-input\" variant=\"outline\" invalid size=\"md\" fullWidth unsaved leadingContent={<SearchIcon />} />\n<Textarea resize=\"vertical\" rows={4} buttonContent={<Button track=\"send\" size=\"sm\">Send</Button>} />\n```\n\nBoth are Base UI controls rendered inside a styled frame: inside a `Field` they receive the field's id, `aria-labelledby`, `aria-describedby`, `aria-invalid` and disabled state automatically.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `variant` | `'outline' \\| 'ghost'` | `'outline'` |\n| `invalid` | `boolean` | inherits `Field invalid` — `aria-invalid` + `data-invalid` + danger styles |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` (Input only; Textarea uses the `md` padding) |\n| `fullWidth` | `boolean` | `false` |\n| `unsaved` | `boolean` | `false` — tints the field `--input-unsaved` and sets `data-unsaved` (pair with `Label unsaved`) |\n| `leadingContent` / `trailingContent` | `ReactNode` | Input only |\n| `resize` | `'none' \\| 'vertical' \\| 'horizontal' \\| 'both'` | `'vertical'` (Textarea only) |\n| `buttonContent` | `ReactNode` | Textarea only — rendered bottom-right inside the frame |\n| `className` | `string` | merged onto the frame |\n| `inputClassName` / `textareaClassName` | `string` | merged onto the native element |\n\nSlots: `input` (frame), `input-control`, `input-leading`, `input-trailing`; `textarea` (frame), `textarea-control`, `textarea-actions`. The frame and the native element both carry Base UI's `data-focus` / `data-hover` / `data-disabled` / `data-invalid`.\n\n### NativeSelect — `@zuilib/primitives/native-select`\n\nNative `<select>` rendered through Base UI `Field.Control`, with field styling and a token-positioned chevron. Standard `<option>` children, `value` / `onChange` (the native DOM handler); the custom widget is `Select`.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `variant` | `'outline' \\| 'ghost'` | `'outline'` |\n| `invalid` | `boolean` | inherits `Field invalid` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `fullWidth` | `boolean` | `false` |\n\nSlot: `native-select` (with `data-invalid`).\n\n### TextareaField — `@zuilib/primitives/textarea-field`\n\n`Field` + `Label` + `FieldDescription` + `Textarea` + `FieldError` in one, with an optional character counter. Spreads textarea HTML attributes (`{...field}` from react-hook-form works).\n\n| Prop | Type |\n|------|------|\n| `label`, `description`, `error` | `string` — `error` renders a `FieldError` (`role=\"alert\"`) and marks the control invalid |\n| `showCharacterCount`, `maxLength` | `boolean`, `number` — over the limit the counter gets `data-state=\"over\"` and the control is invalid |\n| `unsaved` | `boolean` |\n| `buttonContent` | `ReactNode` — Textarea's action slot |\n| `className` / `textareaClassName` | root / native `<textarea>` |\n\nSlots: `textarea-field` (root), `textarea-field-label`, `textarea-field-description`, `textarea-field-footer`, `textarea-field-error`, `textarea-field-count`, plus the Textarea slots.\n\n### Checkbox / Switch — `@zuilib/primitives/checkbox`, `@zuilib/primitives/switch`\n\n```tsx\n<Checkbox track=\"accept\" checked={value} onCheckedChange={setValue} label=\"Accept\" description=\"Required\" />\n<Switch track=\"notifications\" defaultChecked label=\"Notifications\" labelPosition=\"start\" size=\"lg\" />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `checked` / `defaultChecked` / `onCheckedChange` | `boolean`, `boolean`, `(checked: boolean) => void` | controlled or uncontrolled |\n| `label`, `description` | `ReactNode` | — with either, the control is wrapped in a Base UI `Field` so the label toggles it and the description is in `aria-describedby` |\n| `labelPosition` | `'end' \\| 'start'` | `'end'` |\n| `indeterminate` | `boolean` | `false` (Checkbox only) — `aria-checked=\"mixed\"`, `data-indeterminate` |\n| `invalid` | `boolean` | inherits `Field invalid` |\n| `disabled`, `name`, `value` | | native form participation |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `className` | `string` | merged onto the box / track (the element `ref` points at) |\n\nWithout `label` / `description` only the control renders, so it picks up an enclosing `Field`'s label and description. Slots: `checkbox`, `checkbox-indicator`, `checkbox-field`, `checkbox-text`, `checkbox-label`, `checkbox-description`; `switch`, `switch-thumb`, `switch-field`, `switch-text`, `switch-label`, `switch-description`.\n\n### Select — `@zuilib/primitives/select`\n\nThe custom select widget (Base UI `Select` underneath; the native element is `NativeSelect`). Generic over the value type. Convenience mode takes `options`; compound mode takes children.\n\n```tsx\n<Select track=\"role\" value={role} onValueChange={setRole} options={[{ value: 'admin', label: 'Admin', icon: <ShieldIcon />, description: 'Full access' }]} />\n\n<Select track=\"assignees\" value={ids} onValueChange={setIds} multiple compareBy=\"id\">\n  <Select.Label>Assignees</Select.Label>\n  <Select.Button>{ids.length} selected</Select.Button>\n  <Select.Options>\n    {users.map((u) => <Select.Option key={u.id} value={u}>{u.name}</Select.Option>)}\n  </Select.Options>\n</Select>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `value` / `defaultValue` / `onValueChange` | `T`, `T`, `(value: T) => void` | controlled or uncontrolled; arrays with `multiple` |\n| `options` | `Array<{ value, label, disabled?, icon?, description? }>` | — ignored when children are given |\n| `placeholder` | `string` | `'Select option'` |\n| `multiple` | `boolean` | `false` — the button joins the selected labels with `\", \"` |\n| `compareBy` | `keyof T \\| (a, b) => boolean` | reference equality — for object values |\n| `invalid` | `boolean` | inherits `Field invalid` |\n| `disabled`, `name`, `form` | | |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `fullWidth` | `boolean` | `true` |\n| `anchor` | Base UI anchor \\| `false` | `'bottom start'` |\n| `portal` | `boolean` | `true` when anchored |\n| `renderOption` | `(option, { focus, selected, disabled }) => ReactNode` | — custom option body; the check mark is still drawn |\n\nStatics / named exports: `Select.Label` (`passiveLabel?`), `Select.Button` (`indicator?: ReactNode \\| false`, `autoFocus?`), `Select.Options` (`anchor?`, `portal?`, `modal?`, `keepMounted?`), `Select.Option` (`value`, `disabled?`, `icon?`, `description?`, children or `(state) => ReactNode`). Parts inherit `size` / `invalid` / `disabled` from the root. Inside a `Field` the button takes the field's `Label` / `FieldDescription`. The option data type is `SelectOptionData<T>`.\n\nSlots: `select` (with `data-size`), `select-label`, `select-button`, `select-value`, `select-value-icon`, `select-chevron`, `select-options`, `select-option`, `select-option-icon`, `select-option-content`, `select-option-description`, `select-option-check`.\n\n### Combobox — `@zuilib/primitives/combobox`\n\nSearchable select, generic over the value type and `multiple`. Convenience mode takes `options`; compound mode takes children.\n\n```tsx\n<Combobox track=\"search-users\"\n  value={userId}\n  onValueChange={setUserId}\n  options={users.map((u) => ({ value: u.id, label: u.name }))}\n  placeholder=\"Search users...\"\n  onQueryChange={setQuery}\n  loading={isFetching}\n/>\n\n<Combobox track=\"tag\" value={tag} onValueChange={setTag} compareBy=\"id\">\n  <Combobox.Input displayValue={(t) => t?.name ?? ''} />\n  <Combobox.Button />\n  <Combobox.Options>\n    {tags.map((t) => <Combobox.Option key={t.id} value={t}>{t.name}</Combobox.Option>)}\n  </Combobox.Options>\n</Combobox>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `value` / `defaultValue` / `onValueChange` | `T \\| null` (or `T[]` with `multiple`) | controlled or uncontrolled |\n| `options` | `Array<{ value, label, disabled? }>` | — labels double as keys, keep them unique |\n| `placeholder` | `string` | `'Search...'` (`Labels.search`) |\n| `toggleLabel` | `string` | `'Toggle options'` (`Labels.toggleOptions`) — name of the chevron button |\n| `track` | `string` | — names the `select` telemetry event; falls back to `name`, then `aria-label` |\n| `multiple` | `boolean` | `false` |\n| `compareBy` | `keyof T \\| (a, b) => boolean` | reference equality |\n| `invalid` | `boolean` | inherits `Field invalid` |\n| `disabled`, `name`, `form`, `id`, `autoFocus`, `aria-label`, `aria-describedby` | | `id` only outside a `Field` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `fullWidth` | `boolean` | `true` |\n| `anchor` | placement \\| `{ to, gap, offset, padding }` \\| `false` | `'bottom start'` |\n| `portal` | `boolean` | `true` when anchored |\n| `openOnFocus` | `boolean` | `false` — open the panel as soon as the input receives focus |\n| `onQueryChange` | `(query: string) => void` | — every keystroke, `''` on close |\n| `filter` | `false \\| (option, query) => boolean` | case-insensitive label match; `false` for server-side filtering |\n| `loading` / `loadingMessage` | `boolean`, `ReactNode` | `false`, `'Loading…'` (`Labels.loadingMessage`) |\n| `emptyMessage` | `ReactNode` | `'No results found.'` (`Labels.noResults`) |\n| `renderOption` | `(option, { focus, selected, disabled }) => ReactNode` | |\n| `displayValue` | `(value) => string` | option label (single) / `''` (multiple) |\n\nStatics / named exports: `Combobox.Input` (`displayValue?`, `size?`), `Combobox.Button` (`toggleLabel?`; children replace the chevron), `Combobox.Options` (`anchor?`, `portal?`, `keepMounted?`, `keepHighlightOnPointerLeave?`, `modal?`, `transition?`), `Combobox.Option` (`value`, `disabled?`, children or `(state) => ReactNode`).\n\nSlots: `combobox` (with `data-size`), `combobox-input`, `combobox-button`, `combobox-chevron`, `combobox-options`, `combobox-option`, `combobox-loading`, `combobox-empty`, `combobox-status` (visually hidden live region).\n\n### RadioGroup — `@zuilib/primitives/radio-group`\n\nA `<fieldset role=\"radiogroup\">`; the label is its `<legend>`. Convenience mode takes `options`; compound mode takes `RadioGroup.Option` children.\n\n```tsx\n<RadioGroup track=\"plan\"\n  value={plan}\n  onValueChange={setPlan}\n  label=\"Plan\"\n  description=\"Billed monthly\"\n  options={[\n    { value: 'free', label: 'Free', description: 'Basic', icon: <StarIcon /> },\n    { value: 'pro', label: 'Pro' },\n  ]}\n/>\n\n<RadioGroup track=\"plan\" value={plan} onValueChange={setPlan} layout=\"list\" orientation=\"horizontal\">\n  <RadioGroup.Label>Plan</RadioGroup.Label>\n  <RadioGroup.Option value=\"free\" label=\"Free\" />\n  <RadioGroup.Option value=\"pro\">\n    <RadioGroup.Indicator /> custom content\n  </RadioGroup.Option>\n</RadioGroup>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `value` / `defaultValue` / `onValueChange` | `T`, `T`, `(value: T) => void` | controlled or uncontrolled |\n| `options` | `Array<{ value, label, description?, icon?, disabled? }>` | — the entry type is `RadioGroupOptionData<T>` |\n| `label`, `description` | `ReactNode` | wired to `aria-labelledby` / `aria-describedby` |\n| `compareBy` | `keyof T \\| (a, b) => boolean` | reference equality |\n| `layout` | `'card' \\| 'list'` | `'card'` — bordered clickable rows, or plain radios |\n| `orientation` | `'vertical' \\| 'horizontal'` | `'vertical'` — also `aria-orientation` / `data-orientation` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` |\n| `invalid` | `boolean` | inherits `Field invalid` — `aria-invalid` on the group, `data-invalid` on every radio |\n| `disabled`, `name`, `form`, `id` | | |\n\n`RadioGroup.Option` props: `value`, `label?`, `description?`, `icon?`, `disabled?`, `autoFocus?`, `children?` (replaces the default row), `className?`. Statics / named exports: `RadioGroup.Option`, `RadioGroup.Indicator`, `RadioGroup.Label`, `RadioGroup.Description`.\n\nInside a `Field` the group adopts the field's control id and appends the field's `Label` / `FieldDescription` / `FieldError` ids. Slots: `radio-group` (with `data-layout`, `data-orientation`, `data-size`), `radio-group-label`, `radio-group-description`, `radio-group-option`, `radio-group-option-field`, `radio-group-indicator`, `radio-group-indicator-dot`, `radio-group-option-icon`, `radio-group-option-content`, `radio-group-option-label`, `radio-group-option-description`.\n\n### Fieldset — `@zuilib/primitives/fieldset`\n\n```tsx\n<Fieldset label=\"Account\" description=\"Your login details\" disabled={locked}>\n  <Field>…</Field>\n</Fieldset>\n```\n\n| Prop | Type |\n|------|------|\n| `label` | `string` — rendered as the `<legend>`, wired to `aria-labelledby` |\n| `description` | `string` — wired to `aria-describedby` |\n| `disabled` | `boolean` — inherited by every `Field` / control inside |\n\nSlots: `fieldset`, `fieldset-legend`, `fieldset-description`, `fieldset-content`.\n\n### Form layout — `field`, `label`, `field-description`, `field-error`\n\n`Field` is a Base UI `Field`: it generates the control id and wires `for`, `aria-labelledby` and `aria-describedby` between its parts, so no ids are needed (an explicit `id` / `htmlFor` still wins). Used standalone or via `FormField` (`@zuilib/primitives/form-field`):\n\n```tsx\n<Field invalid={Boolean(fieldError)} disabled={saving}>\n  <Label required unsaved={dirty}>Email</Label>\n  <Input track=\"email\" type=\"email\" />\n  <FieldDescription>We never share your email.</FieldDescription>\n  <FieldError error={fieldError} />\n</Field>\n```\n\n| Component | Props | Notes |\n|---|---|---|\n| `Field` | `disabled?`, `invalid?` | `disabled` disables every Base UI control inside (leave unset to inherit `Fieldset disabled`); `invalid` sets `data-invalid` on the field and its parts and is the default `invalid` of every control inside. `useField()` / `FieldContext` (`lib/field-context`) expose `{ disabled, invalid }` to custom controls |\n| `Label` | `required?`, `unsaved?` | `required` adds a marker (`label-required`: asterisk + visually hidden \"(required)\"); `unsaved` an \"Unsaved\" badge (`label-unsaved`, `--warning` dot, `--warning-text`, text from `Labels.unsaved`) |\n| `FieldDescription` | — | in the control's `aria-describedby` |\n| `FieldError` | `error?: FieldErrorLike` | `FieldErrorLike = { message?: string } \\| string`: a string renders as is, an object renders its `message` (a react-hook-form field error fits without any import), otherwise `children`; renders nothing without text. `role=\"alert\"`; in the control's `aria-describedby` |\n\nOutside a `Field`, `Label`, `FieldDescription` and `FieldError` are plain `<label>` / `<p>` elements: associate them with `htmlFor` / `id` / `aria-describedby` yourself. Slots: `field`, `label`, `label-required`, `label-unsaved`, `field-description`, `field-error`; the parts mirror the field's `data-invalid` / `data-disabled`.\n\n### Disclosure — `@zuilib/primitives/disclosure`\n\n```tsx\n<Disclosure track=\"advanced-options\" title=\"Advanced options\" defaultOpen={false} panelClassName=\"space-y-4\">\n  <Input track=\"advanced-setting\" />\n</Disclosure>\n```\n\n| Prop | Type |\n|------|------|\n| `title` | `ReactNode` |\n| `open` / `defaultOpen` / `onOpenChange` | `boolean`, `boolean`, `(open: boolean) => void` — controlled or uncontrolled |\n| `className` / `buttonClassName` / `panelClassName` | root / toggle / panel |\n\nSlots: `disclosure` (with `data-state=\"open\" \\| \"closed\"`), `disclosure-button`, `disclosure-title`, `disclosure-icon`, `disclosure-panel`.\n\n### Stepper — `@zuilib/primitives/stepper`\n\n```tsx\n<Stepper track=\"step\"\n  steps={[\n    { id: '1', label: 'Details', description: 'Basic info' },\n    { id: '2', label: 'Review' },\n  ]}\n  value={step}\n  onValueChange={setStep}\n  orientation=\"horizontal\"\n/>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `steps` | `StepperStep[]` (`Array<{ id, label, description? }>`) | required |\n| `value` / `defaultValue` / `onValueChange` | `number`, `number`, `(value: number) => void` | controlled or uncontrolled; zero-based. `onValueChange` makes completed steps buttons |\n| `nonLinear` | `boolean` | `false` — with `onValueChange`, current and upcoming steps are clickable too |\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n\nA `<nav aria-label=\"Progress\">`; the current step has `aria-current=\"step\"`. Slots: `stepper` (with `data-orientation`), `stepper-list`, `stepper-step` and `stepper-body` (with `data-state=\"complete\" \\| \"current\" \\| \"upcoming\"`), `stepper-connector`, `stepper-circle`, `stepper-check` (the completed glyph), `stepper-text`, `stepper-label`, `stepper-description`.\n\n### SpinnerIcon — `@zuilib/primitives/lib/spinner`\n\nThe indeterminate spinner icon Button and Combobox use (`SpinnerIcon`), for your own loading states. Draws in `currentColor`, sizes at `1em`, stops under `prefers-reduced-motion`, `aria-hidden` (announce the busy state on the host). Takes SVG attributes; slot `spinner`.\n\n### Card — `@zuilib/primitives/card`\n\n```tsx\n<Card variant=\"elevated\" padding=\"md\">\n  <Card.Header action={<Button track=\"ghost-button\" size=\"icon\" variant=\"ghost\"><MoreIcon /></Button>}>\n    <Card.Title>Billing</Card.Title>\n    <Card.Description>Plan and invoices</Card.Description>\n  </Card.Header>\n  <Card.Content>…</Card.Content>\n  <Card.Footer><Button track=\"save\">Save</Button></Card.Footer>\n</Card>\n<Card as=\"a\" href=\"/docs\" interactive>…</Card>\n<Card interactive onClick={pick}>          {/* div with role=\"button\", Enter / Space */}\n  <Card.Header><Card.Title>Team</Card.Title></Card.Header>\n</Card>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `variant` | `'elevated' \\| 'outline' \\| 'ghost'` | `'elevated'` — `bg-card` + border + `shadow-card`; `outline` drops the shadow; `ghost` is transparent |\n| `padding` | `'none' \\| 'sm' \\| 'md' \\| 'lg'` | `'md'` — sets `--card-padding` from `--card-padding-sm/md/lg`; the sections read it. Below `sm`, `md` and `lg` step down to the next token (`--card-padding-sm` / `-md`) |\n| `interactive` | `boolean` | `false` — the card is one control: hover / active styles, focus-visible ring, `cursor-pointer`. `as=\"a\"` / Link for navigation, `as=\"button\"` for an action; any other tag (the default `div`) gets `role=\"button\"`, `tabIndex={0}` and Enter / Space activation of `onClick` |\n| `disabled` | `boolean` | `false` — dims the card (`data-disabled`) and blocks `onClick` from pointer and keyboard; a native `button` gets `disabled`, anything else `aria-disabled` + `tabIndex={-1}`. A pass-through `aria-disabled=\"true\"` behaves the same |\n| `as` | `ElementType` | `'div'` — polymorphic like Button; `as=\"button\"` gets `type=\"button\"` |\n\nAn interactive / `button` / `a` card is a single control, so do not put another control inside it (`Card.Header action`, a Button in the footer): use a plain card and put the control in a part instead (`Card.Header` warns in development). Inside `as=\"button\"` the parts render as `span`s (a native button only allows phrasing content), `Card.Title` included; inside `as=\"a\"` they keep their block markup.\n\nA mounted `Card.Title` names the card: it takes the generated id and the root gets `aria-labelledby` (after hydration, so it never points at nothing), which gives `as=\"section\"` / `as=\"article\"` landmarks and interactive cards a name from the title rather than from their whole text. Your own `aria-label` / `aria-labelledby` wins.\n\nStatics / named exports: `Card.Header` (`action?: ReactNode` pinned to the end of the row), `Card.Title` (`as?`, default `h3`; `span` inside a `button` card), `Card.Description` (`<p>`; `span` inside a `button` card), `Card.Content`, `Card.Footer`. Used outside a Card the sections pad themselves at `md`. Slots: `card` (with `data-variant`, `data-padding`, `data-interactive`, `data-disabled`), `card-header`, `card-header-text`, `card-header-action`, `card-title`, `card-description`, `card-content`, `card-footer`.\n\n### Container / Stack — `@zuilib/primitives/container`, `@zuilib/primitives/stack`\n\n```tsx\n<Container size=\"lg\" as=\"main\">…</Container>\n<Stack direction=\"row\" gap={4} align=\"center\" justify=\"between\" separator>…</Stack>\n```\n\n| Component | Prop | Type | Default |\n|---|------|------|---------|\n| `Container` | `size` | `'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| 'full'` | `'lg'` — `max-w-(--container-width-*)` (`full` is `max-w-none`); gutter `px-(--container-padding)`. Override a step on `:root` or a subtree, or pass a `max-w-*` in `className` for one element |\n| `Container` | `as` | `ElementType` | `'div'` |\n| `Stack` | `direction` | `'row' \\| 'column'` | `'column'` |\n| `Stack` | `gap` | `0 \\| 1 \\| 2 \\| 3 \\| 4 \\| 6 \\| 8` | `2` — multiples of `--spacing` |\n| `Stack` | `align` | `'start' \\| 'center' \\| 'end' \\| 'stretch' \\| 'baseline'` | `'stretch'` |\n| `Stack` | `justify` | `'start' \\| 'center' \\| 'end' \\| 'between' \\| 'around' \\| 'evenly'` | `'start'` |\n| `Stack` | `wrap` | `boolean` | `false` |\n| `Stack` | `separator` | `ReactNode` | — `true` draws a `--border` hairline between consecutive children; any other node is rendered inside the separator element in place of the hairline. `null` / `false` children get none; Fragments are flattened, so their members are separated like direct children |\n| `Stack` | `separatorAs` | `ElementType` | `'li'` inside `as=\"ul\" \\| \"ol\" \\| \"menu\"` (a list may only contain list items), `'div'` otherwise |\n| `Stack` | `separatorDecorative` | `boolean` | `true` — `role=\"none\"` + `aria-hidden`; `false` makes each separator a `role=\"separator\"` with `aria-orientation`, like `Separator` |\n| `Stack` | `separatorClassName` | `string` | — merged last onto every separator |\n| `Stack` | `as` | `ElementType` | `'div'` |\n\n`wrap` and `separator` are meant for columns: in a wrapping row the vertical rule is a flex item too, so it can land first or last on a line and only spans that line's height.\n\nSlots: `container` (with `data-size`); `stack` (with `data-direction`, `data-gap`), `stack-separator` (with `data-orientation`, `data-decorative`).\n\n### Separator — `@zuilib/primitives/separator`\n\n```tsx\n<Separator />\n<Separator orientation=\"vertical\" decorative className=\"h-6\" />\n<Separator>or</Separator>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `orientation` | `'horizontal' \\| 'vertical'` | `'horizontal'` |\n| `decorative` | `boolean` | `false` — `role=\"separator\"` + `aria-orientation`; `true` renders `role=\"none\"` |\n| `children` | `ReactNode` | — a label centred between two lines, named via `aria-labelledby` (`''`, `null`, booleans draw a plain rule) |\n| `lineClassName` | `string` | merged onto the drawn line(s) in both modes — the root of a plain rule, both `separator-line` spans of a labelled one (`bg-primary`, `h-0.5`) |\n| `labelClassName` | `string` | merged onto the label |\n\n`role` is not accepted: `decorative` is the only switch between `role=\"separator\"` and `role=\"none\"`.\n\nA vertical separator is `h-full self-stretch`, so it takes its height from a flex parent (`<Stack direction=\"row\">`, `flex items-center`). Outside a flex parent it collapses to `0px`; pass an explicit height (`className=\"h-6\"`) as in the example. The labelled vertical variant needs a definite height the same way, since its two `flex-1` lines share the root's height.\n\nSlots: `separator` (with `data-orientation`, `data-decorative`), `separator-line`, `separator-label`.\n\n### Heading / Text — `@zuilib/primitives/heading`, `@zuilib/primitives/text`\n\n```tsx\n<Heading as=\"h1\">Settings</Heading>\n<Heading as=\"h3\" size=\"md\" weight=\"medium\" truncate>…</Heading>\n<Text muted size=\"sm\">Helper copy</Text>\n<Text as=\"label\" htmlFor=\"name\" weight=\"medium\">Name</Text>\n```\n\n| Component | Prop | Type | Default |\n|---|------|------|---------|\n| `Heading` | `as` | `'h1' … 'h6'` | `'h2'` |\n| `Heading` | `size` | `'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| '2xl' \\| '3xl'` | per level: h1 `3xl`, h2 `2xl`, h3 `xl`, h4 `lg`, h5 `md`, h6 `sm` (`--text-*`) |\n| `Heading` | `weight` | `'normal' \\| 'medium' \\| 'semibold' \\| 'bold'` | `'semibold'` |\n| `Heading` | `tracking` | `'tighter' \\| 'tight' \\| 'normal' \\| 'wide' \\| 'wider' \\| 'widest'` | `'tight'` at `2xl` / `3xl`; below that no class is set, so `letter-spacing` inherits (`--tracking-*`) |\n| `Heading` | `truncate` | `boolean` | `false` |\n| `Text` | `as` | `'p' \\| 'span' \\| 'div' \\| 'label'` | `'p'` — `as=\"label\"` requires `htmlFor` |\n| `Text` | `size` | `'xs' \\| 'sm' \\| 'md' \\| 'lg'` | `'md'` (`text-base`) |\n| `Text` | `weight` | `'normal' \\| 'medium' \\| 'semibold' \\| 'bold'` | `'normal'` |\n| `Text` | `tone` | `'neutral' \\| 'success' \\| 'warning' \\| 'danger'` | `'neutral'` — status tones use `--success-text` / `--warning-text` / `--danger-text` |\n| `Text` | `muted` | `boolean` | `false` — de-emphasised (`--muted-foreground`); orthogonal to `tone` and wins the colour |\n| `Text` | `truncate` / `lineClamp` | `boolean` / `number` | — `lineClamp` (a positive integer) wins over `truncate` |\n\nBoth map to the type scale only: `size` sets `--text-*` (font size + line height), `weight` sets `--font-weight-*`; nothing hardcodes a pixel size. A heading's `as` is its outline level and `size` its look, so the two are independent. Every `Text` tone meets WCAG AA (4.5:1) on `--background` in both themes; the plain `--success` / `--warning` / `--danger` tokens are surfaces and are not used as text colour. There is nothing dimmer than `muted`: on the light theme `--muted-foreground` is already at the AA floor.\n\n`Text as=\"label\"` is a bare `<label>` with the typography props; it does not join a Base UI `Field`, which is why `htmlFor` is required. Use `Label` for form labels that wire themselves to the control. `truncate` on an inline tag (`span`, `label`) adds `inline-block max-w-full` so there is a box to overflow; `lineClamp` ignores non-integers (`-webkit-line-clamp` takes an integer).\n\nSlots: `heading` (with `data-size`, `data-weight`, `data-truncate`), `text` (with `data-size`, `data-weight`, `data-tone`, `data-muted`, `data-truncate`, `data-line-clamp`).\n\n### Code / Kbd — `@zuilib/primitives/code`, `@zuilib/primitives/kbd`\n\n```tsx\n<Code language=\"ts\">const x = 1</Code>\n<Code block language=\"tsx\" className=\"max-h-64\">{source}</Code>\n<Kbd>⌘</Kbd> <Kbd keys={['Ctrl', 'Shift', 'P']} />\n```\n\n| Component | Prop | Type | Default |\n|---|------|------|---------|\n| `Code` | `block` | `boolean` | `false` — inline `<code>`; `true` renders `<pre><code>` (padded, `overflow-x-auto`, `tabIndex={0}` with a keyboard focus ring so it scrolls from the keyboard) |\n| `Code` | `language` | `string` | — `data-language` on the root and `language-<name>` on the `<code>` (what Prism / highlight.js look for); no colouring itself |\n| `Code` | `codeClassName` | `string` | block only: merged onto the inner `<code>` |\n| `Kbd` | `keys` | `string[]` | — one styled `<kbd>` per key inside an outer `<kbd>`, joined by `separator`; a non-empty array takes precedence over `children` |\n| `Kbd` | `separator` | `ReactNode` | `'+'` — read by assistive tech so a chord announces as `Ctrl+Shift+P` |\n\nSlots: `code`, `code-block` (the `<pre>`); `kbd`, `kbd-group` (the root when `keys` is given), `kbd-separator`.\n\n### Alert — `@zuilib/primitives/alert`\n\n```tsx\n<Alert track=\"unsaved-changes\" tone=\"warning\" dismissible onOpenChange={() => setSeen(true)}>\n  <Alert.Title>Unsaved changes</Alert.Title>\n  <Alert.Description>Save before leaving.</Alert.Description>\n  <Alert.Actions><Button track=\"save\" size=\"sm\">Save</Button></Alert.Actions>\n</Alert>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `tone` | `'info' \\| 'success' \\| 'warning' \\| 'danger' \\| 'neutral'` | `'info'` — a 10% tint / 30% border of the tone hue under `text-foreground`; `info` tints from `--primary` (`--info` is a surface token, not a hue; retheme it via `[data-slot=\"alert\"][data-tone=\"info\"]`); `neutral` is `bg-muted` |\n| `icon` | `ReactNode \\| false` | per-tone icon (success / warning glyphs use the `--success-text` / `--warning-text` contrast twins); `false` or `null` removes the icon column; larger custom icons are not clipped |\n| `politeness` | `'assertive' \\| 'polite' \\| 'off'` | `'assertive'` (`role=\"alert\"`); `polite` is `role=\"status\"` for info / success / neutral; an explicit `role` prop wins |\n| `dismissible` | `boolean` | `false` — close button; a click calls `onOpenChange(false)` and (uncontrolled) removes the alert. If keyboard focus is inside the alert when it goes, focus moves to the element it came from, else the next tabbable element after the alert (then the previous one, then a focusable ancestor) instead of dropping to `<body>` |\n| `open` / `defaultOpen` / `onOpenChange` | `boolean`, `boolean`, `(open: boolean) => void` | controlled or uncontrolled (`defaultOpen` defaults to `true`) — while controlled, the close button only calls `onOpenChange(false)` and the alert renders while `open` is `true`, so the parent can veto / defer the dismissal and show it again without a remount |\n| `closeLabel` | `string` | `'Close'` (`Labels.close`) — composed with the `Alert.Title` text via `aria-labelledby` (“Close, Unsaved changes”), so several alerts' close buttons stay distinguishable; a custom `id` on the title opts out |\n\nStatics / named exports: `Alert.Title`, `Alert.Description`, `Alert.Actions`. Slots: `alert` (with `data-tone`, `data-politeness`, `data-dismissible`), `alert-icon`, `alert-content`, `alert-title`, `alert-description`, `alert-actions`, `alert-close`, `alert-close-icon`.\n\n### Badge — `@zuilib/primitives/badge`\n\n```tsx\n<Badge>New</Badge>\n<Badge tone=\"warning\" variant=\"subtle\" dot shape=\"pill\" size=\"sm\">Pending</Badge>\n<Badge track=\"deployed\" tone=\"success\" icon={<CheckIcon />} onRemove={() => remove(tag)}>Deployed</Badge>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `variant` | `'solid' \\| 'subtle' \\| 'outline'` | `'solid'` — visual style only: `solid` paints the tone as the surface, `subtle` is `bg-<hue>/10` + `text-<hue>-text` (`--primary-text`, `--danger-text`, `--success-text`, `--warning-text`: the hue made legible as small text; neutral tints `--muted` at `/60`), `outline` draws a border in the tone with no fill (neutral: `border-border` + `text-foreground`) |\n| `tone` | `'neutral' \\| 'primary' \\| 'success' \\| 'warning' \\| 'danger' \\| 'info'` | `'primary'` — `info` reads the `--primary` hue (as Alert does; `--info` is a surface token), distinct only through `data-tone` |\n| `size` | `'sm' \\| 'md'` | `'md'` — paddings are `--spacing` multiples, height comes from `--text-xs` / `--text-sm` line-height |\n| `shape` | `'rounded' \\| 'pill'` | `'rounded'` |\n| `dot` | `boolean` | `false` — status dot before the content, `bg-current` |\n| `icon` | `ReactNode` | — `aria-hidden`, sized `1em` |\n| `onRemove` | `(event) => void` | — renders a remove button (`type=\"button\"`, 24px hit box, `focusRingClasses`); the click does not bubble to the badge's `onClick` |\n| `removeLabel` | `string` | `Remove <children>` when the children are text (string, number, or an array of those), else `'Remove'` |\n\nA `<span>`; `onRemove` adds the only control. Set `max-w-*` in `className` to truncate the content with an ellipsis. Slots: `badge` (with `data-variant`, `data-tone`, `data-size`, `data-shape`, `data-removable`), `badge-dot`, `badge-icon`, `badge-content`, `badge-remove`, `badge-remove-icon`.\n\n### Avatar — `@zuilib/primitives/avatar`\n\n```tsx\n<Avatar src={user.avatarUrl} name=\"Ada Lovelace\" status=\"online\" />\n<Avatar.Group max={3} size=\"sm\">\n  {users.map((u) => <Avatar key={u.id} name={u.name} src={u.avatarUrl} />)}\n</Avatar.Group>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `src` | `string` | — the fallback shows until it loads; on error it stays. A failed URL is not retried until `src` changes (or the avatar is re-mounted with a new `key`) |\n| `name` | `string` | — source of the initials (`\"Ada Lovelace\"` → `AL`, first code point of the first and last word) and the default `alt` |\n| `alt` | `string` | `name` — the accessible name (`role=\"img\"`); `''` makes the avatar decorative (`aria-hidden`, status included). With neither, and no `aria-label` / `aria-labelledby`, it is decorative too |\n| `fallback` | `ReactNode` | initials, else a person icon |\n| `size` | `'xs' \\| 'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'md'` — `sm` / `md` / `lg` are `--control-height-*`; inherits `Avatar.Group` |\n| `shape` | `'circle' \\| 'square'` | `'circle'`; inherits `Avatar.Group`. Any radius in `className` reaches the image too (`rounded-[inherit]`) |\n| `status` | `'online' \\| 'offline' \\| 'busy' \\| 'away'` | — dot on the bottom-end corner, appended to the label and shown as the dot's `title`. Each status is a shape as well as a colour: `online` a solid `--success` disc, `away` a `--warning` crescent, `busy` a `--danger` disc with a `--danger-foreground` bar, `offline` a hollow ring (`--muted` fill, `--muted-foreground` outline) |\n| `statusLabel` | `string` | capitalised status |\n| `imgProps` | `Omit<ImgHTMLAttributes, 'src' \\| 'alt' \\| 'onLoad' \\| 'onError'>` | — spread on the `<img>` (`loading`, `srcSet`, `crossOrigin`, `referrerPolicy`, …) |\n\n`Avatar.Group` (`role=\"group\"`): `size?`, `shape?`, `max?` (the rest collapse into a `+N` avatar), `overflowLabel?: (hidden, names) => string` (default `\"N more: <hidden names>\"`; the `+N` avatar also carries the hidden names as its `title`). Children must be `Avatar` elements; Fragments around them are unwrapped so `max` counts avatars. The ring that separates overlapping avatars (and cuts out the status dot) is `--avatar-ring`, falling back to `--background`: set `--avatar-ring: var(--card)` on a card to blend it in. `getInitials(name)` is a named export. Slots: `avatar` (with `data-size`, `data-shape`, `data-status`, `data-state=\"loading\" \\| \"loaded\" \\| \"error\" \\| \"fallback\"`), `avatar-image`, `avatar-initials`, `avatar-fallback`, `avatar-icon`, `avatar-status` (with `data-status`), `avatar-group`; the overflow avatar is `avatar` + `data-overflow`.\n\n### Skeleton / Spinner — `@zuilib/primitives/skeleton`, `@zuilib/primitives/spinner`\n\n```tsx\n<Skeleton width={240} height={32} />\n<Skeleton shape=\"circle\" width={48} />\n<Skeleton shape=\"text\" lines={3} animation=\"wave\" label=\"Loading comments\" />\n<Spinner size=\"lg\" label=\"Loading results\" className=\"text-primary\" />\n```\n\n| Component | Prop | Type | Default |\n|---|------|------|---------|\n| `Skeleton` | `shape` | `'text' \\| 'rectangle' \\| 'circle'` | `'rectangle'` — `text` renders `lines` bars `1em` tall |\n| `Skeleton` | `width` / `height` | `number \\| string` | rectangle / text `w-full`, circle `size-(--control-height-md)`; numbers are px. For `text`, `width` sizes the stack and `height` each line; a `circle` given one of them uses it for both |\n| `Skeleton` | `lines` | `number` | `1` — the last of several bars is `w-3/5` |\n| `Skeleton` | `animation` | `'pulse' \\| 'wave' \\| 'none'` | `'pulse'` — `wave` is `--animate-skeleton-wave` sweeping a `foreground/8` sheen; both stop under `prefers-reduced-motion` |\n| `Skeleton` | `label` | `string` | — visually hidden; makes the root `role=\"status\"` (no `aria-busy`, which would defer the label; put that on the host), otherwise it is `aria-hidden` |\n| `Spinner` | `size` | `'sm' \\| 'md' \\| 'lg' \\| 'xl'` | `'md'` — sizes the root (`size-4` / `5` / `6` / `8`); a `size-*` in `className` replaces it and the ring follows |\n| `Spinner` | `label` | `string` | `'Loading'` — visually hidden; `''` when the host already names the busy state |\n\n`Spinner` is a `<span role=\"status\">` around the `lib/spinner` icon (draws in `currentColor`; a static ring under `prefers-reduced-motion`). For both a labelled `Skeleton` and `Spinner`, screen readers announce changes inside a live region that is already mounted, not one inserted together with its text: keep the region in the document and toggle its `label` (or its visibility), or let the host carry the state (`aria-busy`, or Button `loading`). Slots: `skeleton` (with `data-shape`, `data-animation`), `skeleton-line`, `skeleton-label`; `spinner` (with `data-size`), `spinner-icon`, `spinner-label`.\n\n### Progress — `@zuilib/primitives/progress`\n\n```tsx\n<Progress value={42} aria-label=\"Upload\" showValue />\n<Progress aria-labelledby=\"sync-heading\" />  {/* indeterminate */}\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `value` | `number \\| null` | — `null` / `undefined` is indeterminate; clamped to `[0, max]` |\n| `max` | `number` | `100` — must be finite and positive, otherwise `100` |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` — track `h-1` / `h-2` / `h-3` |\n| `tone` | `'primary' \\| 'success' \\| 'warning' \\| 'danger'` | `'primary'` |\n| `showValue` | `boolean` | `false` — the formatted value beside the bar (determinate only) |\n| `formatValue` | `(value, max) => string` | whole percentage; also `aria-valuetext` |\n| `aria-label` / `aria-labelledby` | `string` | one of the two is required (typed; a development warning when neither reaches the DOM) |\n\n`role=\"progressbar\"` with `aria-valuemin/max/now/valuetext`, `aria-busy` while indeterminate (`--animate-progress-indeterminate`, a pulse under `prefers-reduced-motion`). Direction-aware: under `dir=\"rtl\"` the track is mirrored, so the fill anchors to the right edge and the indeterminate sweep runs right-to-left. Slots: `progress` (with `data-state=\"determinate\" \\| \"indeterminate\"`, `data-tone`, `data-size`), `progress-track`, `progress-indicator`, `progress-label`.\n\n### EmptyState — `@zuilib/primitives/empty-state`\n\n```tsx\n<EmptyState icon={<InboxIcon />} title=\"No messages\" description=\"Messages you receive show up here.\" actions={<Button track=\"compose\">Compose</Button>} />\n\n<EmptyState size=\"lg\">\n  <EmptyState.Icon><InboxIcon /></EmptyState.Icon>\n  <EmptyState.Title as=\"h2\">Nothing here</EmptyState.Title>\n  <EmptyState.Description>…</EmptyState.Description>\n  <EmptyState.Actions>…</EmptyState.Actions>\n</EmptyState>\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `icon`, `title`, `description`, `actions` | `ReactNode` | — the prop parts; `children` render after them. The prop `title` is a `<div>`; compose `EmptyState.Title as=\"h2\"` for a heading in the outline |\n| `size` | `'sm' \\| 'md' \\| 'lg'` | `'md'` — scales gap, padding, icon disc and type through context |\n\nStatics / named exports: `EmptyState.Icon` (`aria-hidden` muted disc), `EmptyState.Title` (a `<div>` by default; `as=\"h2\"` for a heading), `EmptyState.Description`, `EmptyState.Actions` (centred flex-wrap row). Slots: `empty-state` (with `data-size`), `empty-state-icon`, `empty-state-title`, `empty-state-description`, `empty-state-actions`.\n\n### Tooltip — `@zuilib/primitives/tooltip`\n\n```tsx\n<Tooltip content=\"Delete\" placement=\"bottom\" arrow>\n  <Button track=\"delete\" size=\"icon\" variant=\"ghost\" aria-label=\"Delete\"><TrashIcon /></Button>\n</Tooltip>\n<Tooltip content=\"…\">{(props) => <button type=\"button\" {...props}>hover me</button>}</Tooltip>\n```\n\nPositioned with `@floating-ui/react` (flip, shift, `autoUpdate`). The child is cloned with the trigger props merged (its own `ref`, handlers and `aria-describedby` are composed, not overwritten), or a render function receives them.\n\nThe trigger must be keyboard focusable, or keyboard and screen-reader users can never open the tooltip: use a button or a link. A plain `<span>` / `<div>` / `<svg>` child gets `tabIndex={0}` automatically; a custom component that mounts a non-focusable node logs a warning in development.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `content` | `ReactNode` | required |\n| `placement` | floating-ui `Placement` | `'top'` |\n| `delay` | `number \\| { open?, close? }` | `{ open: 200, close: 0 }` (hover only; keyboard focus opens at once) |\n| `offset` | `number` | 1.5 spacing units (`--spacing` × 1.5 = 6px; the arrow height is added automatically) |\n| `arrow` | `boolean` | `false` |\n| `disabled` | `boolean` | `false` — never opens; closes an open tooltip |\n| `open` / `defaultOpen` / `onOpenChange` | | controlled or uncontrolled |\n| `portal` | `boolean` | `true` — only honoured together with `anchor={false}`; use `anchor={false}` to keep the panel inline so subtree token overrides reach it |\n| `className` / `arrowClassName` / `panelProps` | | panel (the `ref` target) / arrow / extra panel attributes |\n\nOpens on mouse hover and keyboard focus (touch taps do not open it); moving the pointer onto the tooltip keeps it open (WCAG 1.4.13). Closes on Escape (the key is not swallowed: a surrounding Dialog closes on the same press), on pressing the trigger, and on an outside press. `role=\"tooltip\"` and `aria-describedby` on the trigger while open. The surface colour is the `--tooltip` token (`--foreground` by default) and the arrow fills with it, so `className=\"[--tooltip:var(--popover)] text-popover-foreground\"` recolours both. Slots: `tooltip` (with `data-state=\"open\"`, `data-placement`, `data-side`), `tooltip-arrow`; the trigger gets `data-state=\"open\" \\| \"closed\"`.\n\n### Dialog — `@zuilib/primitives/dialog`\n\n```tsx\n<Dialog track=\"delete-project\" open={open} onOpenChange={setOpen} size=\"md\">\n  <Dialog.Panel>\n    <Dialog.Header>\n      <Dialog.Title>Delete project</Dialog.Title>\n      <Dialog.Description>This cannot be undone.</Dialog.Description>\n      <Dialog.Close />\n    </Dialog.Header>\n    <Dialog.Body>…</Dialog.Body>\n    <Dialog.Footer>\n      <Dialog.Close as={Fragment}><Button track=\"cancel\" variant=\"outline\">Cancel</Button></Dialog.Close>\n      <Button track=\"delete\" tone=\"danger\">Delete</Button>\n    </Dialog.Footer>\n  </Dialog.Panel>\n</Dialog>\n```\n\nBase UI `Dialog` (portalled, focus-trapped, scroll-locked) with a backdrop and a centred panel; both transition on `data-[closed]`.\n\n| Prop | Type | Default |\n|------|------|---------|\n| `open` / `defaultOpen` / `onOpenChange` | `boolean`, `boolean`, `(open: boolean) => void` | controlled or uncontrolled — same contract as `Drawer`: `onOpenChange(false)` runs on Escape / backdrop click (while `dismissible`) and from `Dialog.Close`; a controlled dialog flips `open` in response |\n| `size` | `'sm' \\| 'md' \\| 'lg' \\| 'xl' \\| 'full'` | `'md'` — panel `max-width` from `--dialog-width-sm` / `-md` / `-lg` / `-xl` (24 / 32 / 42 / 56rem); below `sm` every size is the viewport minus a 1rem gutter; `full` fills the viewport and pads past the safe areas |\n| `scrollBehavior` | `'inside' \\| 'outside'` | `'inside'` — the panel is capped at the viewport and `Dialog.Body` scrolls; `outside` scrolls the page-level container |\n| `dismissible` | `boolean` | `true` — `false` ignores Escape and backdrop clicks (`Dialog.Close` still works) |\n| `initialFocus` | `MutableRefObject<HTMLElement \\| null>` | — overrides `autoFocus` with an explicit element |\n| `autoFocus` | `boolean` | `true` — focuses the `data-autofocus` element (`<Dialog.Close autoFocus />`, any Base UI button with `autoFocus`), else the dialog root; `false` focuses the first focusable element instead |\n| `role` | `'dialog' \\| 'alertdialog'` | `'dialog'` |\n| `className` / `containerClassName` / `backdropClassName` | | root / centring layer / backdrop |\n| `track` | `string` | — names the `open` / `close` telemetry events (emitted whenever a provider is present) |\n\nStatics / named exports: `Dialog.Panel` (`bg-popover`, `shadow-4`; padding from `--dialog-padding`, default `--spaci","readmeFilename":"README.md","license":"MIT"}