@plankui/mcp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +92 -5
  2. package/package.json +13 -4
  3. package/registry.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,32 @@
1
- # @plankui/mcp
1
+ <p align="center">
2
+ <a href="https://plankui.com/docs/mcp"><img src="https://plankui.com/logo.svg" width="64" height="64" alt="PlankUI" /></a>
3
+ </p>
2
4
 
3
- An [MCP](https://modelcontextprotocol.io) server that gives AI coding agents the whole [PlankUI](https://plankui.com) library: every component's source, props and usage, the templates, and the setup files.
5
+ <h1 align="center">@plankui/mcp</h1>
4
6
 
5
- It runs on your machine over stdio. Nothing is hosted and there is nothing to sign up for.
7
+ <p align="center">
8
+ The <a href="https://plankui.com">PlankUI</a> MCP server. Your AI coding agent gets the real source, props and usage<br />
9
+ of every component, so it builds with PlankUI instead of guessing.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/@plankui/mcp"><img src="https://img.shields.io/npm/v/@plankui/mcp?style=flat-square&color=2f6bff&label=npm" alt="npm version" /></a>
14
+ <a href="https://www.npmjs.com/package/@plankui/mcp"><img src="https://img.shields.io/npm/dm/@plankui/mcp?style=flat-square&color=2f6bff" alt="Downloads a month" /></a>
15
+ <a href="https://github.com/VectorLabsHQ/plankui"><img src="https://img.shields.io/github/stars/VectorLabsHQ/plankui?style=flat-square&color=2f6bff&logo=github" alt="GitHub stars" /></a>
16
+ <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-stdio-2f6bff?style=flat-square" alt="MCP over stdio" /></a>
17
+ <a href="https://github.com/VectorLabsHQ/plankui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@plankui/mcp?style=flat-square&color=2f6bff" alt="MIT licence" /></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://plankui.com">Website</a> ·
22
+ <a href="https://plankui.com/docs/mcp">MCP docs</a> ·
23
+ <a href="https://plankui.com/docs/components">Components</a> ·
24
+ <a href="https://www.npmjs.com/package/plankui">CLI</a>
25
+ </p>
26
+
27
+ <p align="center">
28
+ <a href="https://plankui.com/docs/mcp"><img src="https://plankui.com/readme/components-dark.jpg" alt="PlankUI components in dark mode: charts, forms, a command menu, navigation and toggles" /></a>
29
+ </p>
6
30
 
7
31
  ## Add it to your agent
8
32
 
@@ -22,10 +46,63 @@ claude mcp add plankui -- npx -y @plankui/mcp
22
46
  }
23
47
  ```
24
48
 
25
- Setup for every client, with one-click installs for Cursor and VS Code: [plankui.com/docs/mcp](https://plankui.com/docs/mcp).
49
+ <details>
50
+ <summary><b>VS Code</b></summary>
51
+
52
+ In `.vscode/mcp.json`:
53
+
54
+ ```json
55
+ {
56
+ "servers": {
57
+ "plankui": { "type": "stdio", "command": "npx", "args": ["-y", "@plankui/mcp"] }
58
+ }
59
+ }
60
+ ```
61
+
62
+ </details>
63
+
64
+ <details>
65
+ <summary><b>Codex</b></summary>
66
+
67
+ In `~/.codex/config.toml`:
68
+
69
+ ```toml
70
+ [mcp_servers.plankui]
71
+ command = "npx"
72
+ args = ["-y", "@plankui/mcp"]
73
+ ```
74
+
75
+ </details>
76
+
77
+ <details>
78
+ <summary><b>Zed</b></summary>
79
+
80
+ In your Zed settings:
81
+
82
+ ```json
83
+ {
84
+ "context_servers": {
85
+ "plankui": { "source": "custom", "command": "npx", "args": ["-y", "@plankui/mcp"] }
86
+ }
87
+ }
88
+ ```
89
+
90
+ </details>
91
+
92
+ One-click installs for Cursor and VS Code, and setup for every client: [plankui.com/docs/mcp](https://plankui.com/docs/mcp).
26
93
 
27
94
  Tools load when a session starts, so restart the client or open a new chat after adding the server.
28
95
 
96
+ ## Then just ask
97
+
98
+ > Build a settings page with PlankUI: tabs for Profile and Billing, a form with a select and a switch, and a confirm dialog before deleting the account.
99
+
100
+ > Which PlankUI component should I use for picking one option from a short list?
101
+
102
+ > Set up PlankUI in this project with the Ember theme and the Keycap style.
103
+
104
+ The agent looks components up, reads their real source and props, and writes code that matches the library, including the right install commands.
105
+
29
106
  ## Tools
30
107
 
31
108
  | Tool | Returns |
@@ -39,4 +116,14 @@ Tools load when a session starts, so restart the client or open a new chat after
39
116
  | `get_setup` | The tokens, styles and `cn` helper every project needs |
40
117
  | `get_theming` | Themes, styles, accents and how to change them |
41
118
 
42
- Needs Node 18 or later. MIT licensed.
119
+ ## Private by design
120
+
121
+ The server runs on your machine over stdio and reads a registry bundled in the package. Nothing is hosted, nothing is sent anywhere and there is nothing to sign up for.
122
+
123
+ ## Requirements
124
+
125
+ Node 18 or later and any client that speaks MCP.
126
+
127
+ ## Licence
128
+
129
+ MIT. Made by [Vector Labs](https://github.com/VectorLabsHQ).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plankui/mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for PlankUI: gives AI coding agents every component's source, props, usage and the setup files.",
3
+ "version": "0.1.2",
4
+ "description": "MCP server for PlankUI: gives Cursor, Claude Code, VS Code, Codex and other AI agents the real source, props and usage of every component.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "plank-mcp": "server.mjs"
@@ -15,7 +15,7 @@
15
15
  "node": ">=18"
16
16
  },
17
17
  "license": "MIT",
18
- "homepage": "https://plankui.com",
18
+ "homepage": "https://plankui.com/docs/mcp",
19
19
  "repository": {
20
20
  "type": "git",
21
21
  "url": "git+https://github.com/VectorLabsHQ/plankui.git",
@@ -26,9 +26,18 @@
26
26
  },
27
27
  "keywords": [
28
28
  "mcp",
29
+ "mcp-server",
29
30
  "model-context-protocol",
31
+ "ai",
32
+ "cursor",
33
+ "claude-code",
34
+ "codex",
35
+ "vscode",
30
36
  "react",
31
37
  "tailwindcss",
38
+ "components",
39
+ "design-system",
32
40
  "plankui"
33
- ]
41
+ ],
42
+ "author": "Vector Labs (https://plankui.com)"
34
43
  }
package/registry.json CHANGED
@@ -1 +1 @@
1
- {"name":"PlankUI","version":"0.1.0","setup":{"deps":"npm i radix-ui class-variance-authority clsx tailwind-merge lucide-react","devDeps":"npm i -D tailwindcss @tailwindcss/vite","files":{"src/tokens.css":"/* PlankUI design tokens.\n * 1. Knobs: hue, chroma, lightness, radius, fonts.\n * 2. Palette: :root (white) and .dark (black).\n * 3. Themes: [data-theme] tints the palette and sets fonts and the accent.\n * 4. Accent formulas, recomputed wherever a theme or accent is set.\n * 5. Accent presets: [data-accent].\n * 6. Component styles: [data-style] = soft | flat | glass | keycap | brutal. */\n\n/* ───────── 1. Knobs ───────── */\n:root {\n --h: 262;\n --c: 0.22;\n --l: 55%;\n --radius-factor: 1;\n --ff-display: \"Inter\", ui-sans-serif, system-ui, sans-serif;\n --ff-body: \"Inter\", ui-sans-serif, system-ui, sans-serif;\n --ff-code: \"JetBrains Mono\", ui-monospace, monospace;\n --display-weight: 600;\n --display-tracking: -0.045em;\n --ease-out: cubic-bezier(0.16, 1, 0.3, 1);\n --dur-micro: 120ms;\n --dur-short: 220ms;\n}\n\n/* ───────── 2. Palette ───────── */\n:root {\n color-scheme: light;\n --paper: oklch(100% 0 0);\n --paper-2: oklch(97% 0 0);\n --paper-3: oklch(92.2% 0 0);\n --ink: oklch(14.5% 0 0);\n --ink-2: oklch(37% 0 0);\n --muted: oklch(52% 0 0);\n --rule: oklch(92.2% 0 0);\n --rule-2: oklch(87% 0 0);\n --field-edge: oklch(70.8% 0 0);\n\n --glass-edge: oklch(100% 0 0 / 0.65);\n --glass-hi: oklch(100% 0 0 / 0.8);\n --sh-a: 0.06;\n\n --wash-l: 96.5%;\n --wash-c: 0.16;\n --wash-ink-l: 45%;\n --hover-d: -5%;\n --focus-d: 0%;\n\n --graphite: oklch(17% 0 0);\n --graphite-2: oklch(21% 0 0);\n --graphite-ink: oklch(94% 0 0);\n --graphite-muted: oklch(66% 0 0);\n --graphite-rule: oklch(27% 0 0);\n --code-string: oklch(52% 0.13 150);\n --key-edge: oklch(84% 0 0);\n\n --danger: oklch(57.7% 0.215 27);\n --danger-edge: oklch(44% 0.17 27);\n --danger-wash: oklch(96.5% 0.02 17);\n --danger-wash-ink: oklch(50.5% 0.19 27);\n --success: oklch(62.7% 0.17 150);\n --success-wash: oklch(96.5% 0.045 150);\n --success-wash-ink: oklch(44.8% 0.12 151);\n --warning: oklch(76.9% 0.165 70);\n --warning-wash: oklch(97% 0.05 90);\n --warning-wash-ink: oklch(47% 0.12 46);\n --white: oklch(100% 0 0);\n --overlay: oklch(0% 0 0 / 0.4);\n\n /* Chart series: the accent first, then four companions that stay distinct in every theme */\n --chart-2: oklch(70% 0.17 350);\n --chart-3: oklch(66% 0.17 295);\n --chart-4: oklch(76% 0.15 62);\n --chart-5: oklch(70% 0.15 165);\n}\n\n.dark {\n color-scheme: dark;\n --paper: oklch(0% 0 0);\n --paper-2: oklch(15.5% 0 0);\n --paper-3: oklch(21.5% 0 0);\n --ink: oklch(98.5% 0 0);\n --ink-2: oklch(85% 0 0);\n --muted: oklch(66% 0 0);\n --rule: oklch(23% 0 0);\n --rule-2: oklch(31% 0 0);\n --field-edge: oklch(44% 0 0);\n\n --glass-edge: oklch(100% 0 0 / 0.13);\n --glass-hi: oklch(100% 0 0 / 0.1);\n --sh-a: 0.5;\n\n --wash-l: 25%;\n --wash-c: 0.4;\n --wash-ink-l: 84%;\n --hover-d: 6%;\n --focus-d: 12%;\n\n --key-edge: oklch(0% 0 0);\n --code-string: oklch(80% 0.12 150);\n\n --danger-wash: oklch(25% 0.07 25);\n --danger-wash-ink: oklch(80% 0.13 25);\n --success-wash: oklch(24% 0.055 150);\n --success-wash-ink: oklch(82% 0.15 150);\n --warning-wash: oklch(25% 0.055 75);\n --warning-wash-ink: oklch(86% 0.13 80);\n --overlay: oklch(0% 0 0 / 0.7);\n}\n\n/* ───────── 3. Themes ───────── */\n/* Base: pure white and black, Inter, blue. The defaults above. */\n[data-theme=\"base\"] { --h: 262; --c: 0.22; --l: 55%; }\n\n/* Ember: warm paper, Bricolage Grotesque, orange. */\n[data-theme=\"ember\"] {\n --h: 48; --c: 0.19; --l: 66%;\n --ff-display: \"Bricolage Grotesque\", \"Inter\", sans-serif;\n --display-weight: 700;\n --display-tracking: -0.04em;\n --accent-ink-override: oklch(20% 0.04 50);\n --paper: oklch(99.2% 0.005 80);\n --paper-2: oklch(96.4% 0.011 75);\n --paper-3: oklch(91.6% 0.016 72);\n --ink: oklch(19% 0.02 55);\n --ink-2: oklch(38% 0.025 55);\n --muted: oklch(52% 0.025 60);\n --rule: oklch(91% 0.014 72);\n --rule-2: oklch(85.5% 0.018 70);\n --field-edge: oklch(70% 0.025 65);\n}\n.dark[data-theme=\"ember\"], .dark [data-theme=\"ember\"] {\n --paper: oklch(12.5% 0.012 55);\n --paper-2: oklch(18% 0.016 55);\n --paper-3: oklch(23.5% 0.02 55);\n --ink: oklch(97% 0.01 80);\n --ink-2: oklch(85% 0.015 75);\n --muted: oklch(67% 0.02 70);\n --rule: oklch(24.5% 0.018 55);\n --rule-2: oklch(31.5% 0.022 55);\n --field-edge: oklch(45% 0.025 60);\n}\n\n/* Grove: olive paper, Fraunces serif heads, sage. */\n[data-theme=\"grove\"] {\n --h: 150; --c: 0.085; --l: 48%;\n --ff-display: \"Fraunces\", Georgia, serif;\n --display-weight: 500;\n --display-tracking: -0.025em;\n --paper: oklch(99% 0.006 110);\n --paper-2: oklch(96.2% 0.012 115);\n --paper-3: oklch(91.5% 0.018 115);\n --ink: oklch(21% 0.025 140);\n --ink-2: oklch(38% 0.025 140);\n --muted: oklch(51% 0.025 130);\n --rule: oklch(90.5% 0.016 115);\n --rule-2: oklch(85% 0.02 115);\n --field-edge: oklch(68% 0.025 120);\n}\n.dark[data-theme=\"grove\"], .dark [data-theme=\"grove\"] {\n --paper: oklch(13% 0.014 130);\n --paper-2: oklch(18.5% 0.018 130);\n --paper-3: oklch(24% 0.022 130);\n --ink: oklch(96.5% 0.012 110);\n --ink-2: oklch(84% 0.018 115);\n --muted: oklch(67% 0.025 120);\n --rule: oklch(25% 0.02 130);\n --rule-2: oklch(32% 0.024 130);\n --field-edge: oklch(46% 0.028 125);\n}\n\n/* Nebula: navy paper, Space Grotesk, violet. */\n[data-theme=\"nebula\"] {\n --h: 292; --c: 0.2; --l: 56%;\n --ff-display: \"Space Grotesk\", \"Inter\", sans-serif;\n --ff-body: \"Space Grotesk\", \"Inter\", sans-serif;\n --display-weight: 600;\n --display-tracking: -0.04em;\n --paper: oklch(99.3% 0.004 285);\n --paper-2: oklch(96.5% 0.01 285);\n --paper-3: oklch(92% 0.016 285);\n --ink: oklch(18% 0.035 280);\n --ink-2: oklch(36% 0.035 280);\n --muted: oklch(52% 0.03 280);\n --rule: oklch(91.5% 0.014 285);\n --rule-2: oklch(86% 0.018 285);\n --field-edge: oklch(69% 0.03 285);\n}\n.dark[data-theme=\"nebula\"], .dark [data-theme=\"nebula\"] {\n --paper: oklch(13% 0.035 275);\n --paper-2: oklch(18.5% 0.042 275);\n --paper-3: oklch(24% 0.048 278);\n --ink: oklch(97% 0.012 285);\n --ink-2: oklch(85% 0.02 285);\n --muted: oklch(68% 0.035 285);\n --rule: oklch(25% 0.045 278);\n --rule-2: oklch(32% 0.05 280);\n --field-edge: oklch(47% 0.05 282);\n}\n\n/* Terminal: pure neutrals, JetBrains Mono everywhere, phosphor green. */\n[data-theme=\"terminal\"] {\n --h: 148; --c: 0.19; --l: 60%;\n --ff-display: \"JetBrains Mono\", ui-monospace, monospace;\n --ff-body: \"JetBrains Mono\", ui-monospace, monospace;\n --display-weight: 700;\n --display-tracking: -0.06em;\n --accent-ink-override: oklch(14% 0.03 150);\n}\n\n/* Candy: blush paper, Nunito, rose. */\n[data-theme=\"candy\"] {\n --h: 355; --c: 0.19; --l: 62%;\n --ff-display: \"Nunito\", \"Inter\", sans-serif;\n --ff-body: \"Nunito\", \"Inter\", sans-serif;\n --display-weight: 800;\n --display-tracking: -0.035em;\n --paper: oklch(99.4% 0.004 350);\n --paper-2: oklch(97% 0.012 350);\n --paper-3: oklch(93% 0.02 350);\n --ink: oklch(22% 0.03 350);\n --ink-2: oklch(40% 0.035 350);\n --muted: oklch(54% 0.035 350);\n --rule: oklch(92.5% 0.016 350);\n --rule-2: oklch(87.5% 0.022 350);\n --field-edge: oklch(71% 0.035 350);\n}\n.dark[data-theme=\"candy\"], .dark [data-theme=\"candy\"] {\n --paper: oklch(13.5% 0.02 350);\n --paper-2: oklch(19% 0.026 350);\n --paper-3: oklch(24.5% 0.03 350);\n --ink: oklch(97.5% 0.01 350);\n --ink-2: oklch(86% 0.02 350);\n --muted: oklch(69% 0.03 350);\n --rule: oklch(25.5% 0.028 350);\n --rule-2: oklch(32.5% 0.032 350);\n --field-edge: oklch(47% 0.04 350);\n}\n\n/* Surfaces derived from the palette, recomputed wherever a theme re-tints it */\n:root, [data-theme] {\n --raised: var(--paper);\n --inset-c: var(--paper);\n --pop-c: var(--paper);\n --field-c: var(--paper);\n}\n.dark, .dark [data-theme] {\n --raised: oklch(from var(--paper-2) calc(l + 0.02) c h);\n --inset-c: oklch(from var(--paper-2) calc(l + 0.045) c h);\n --pop-c: oklch(from var(--paper-2) calc(l - 0.02) c h);\n --field-c: oklch(from var(--paper) calc(l + 0.06) c h);\n --graphite: oklch(from var(--paper-2) calc(l - 0.035) c h);\n --graphite-2: oklch(from var(--paper-2) calc(l + 0.01) c h);\n --graphite-rule: var(--rule);\n}\n\n/* ───────── 4. Accent formulas ─────────\n * Declared on every element that can change the knobs, so a scoped theme recomputes them. */\n:root, .dark, [data-theme], [data-accent] {\n --accent: oklch(var(--l) var(--c) var(--h));\n --accent-hover: oklch(calc(var(--l) + var(--hover-d)) var(--c) var(--h));\n --accent-edge: oklch(calc(var(--l) - 16%) calc(var(--c) * 0.85) var(--h));\n --accent-ink: var(--accent-ink-override, oklch(100% 0 0));\n --accent-wash: oklch(var(--wash-l) calc(var(--c) * var(--wash-c)) var(--h));\n --accent-wash-ink: oklch(var(--wash-ink-l) var(--c) var(--h));\n --accent-2: oklch(80% calc(var(--c) * 0.55) var(--h));\n --focus: oklch(calc(var(--l) + var(--focus-d)) var(--c) var(--h));\n --ring: oklch(calc(var(--l) + var(--focus-d)) var(--c) var(--h) / 0.28);\n}\n\n/* ───────── 5. Accent presets (override a theme's accent) ───────── */\n[data-accent=\"blue\"] { --h: 262; --c: 0.22; --l: 55%; --swatch-l: 58%; --accent-ink-override: initial; }\n[data-accent=\"violet\"] { --h: 292; --c: 0.22; --l: 54%; --swatch-l: 58%; --accent-ink-override: initial; }\n[data-accent=\"emerald\"] { --h: 160; --c: 0.15; --l: 50%; --swatch-l: 62%; --accent-ink-override: initial; }\n[data-accent=\"vermilion\"] { --h: 33; --c: 0.205; --l: 58%; --swatch-l: 60%; --accent-ink-override: initial; }\n[data-accent=\"rose\"] { --h: 12; --c: 0.21; --l: 55%; --swatch-l: 62%; --accent-ink-override: initial; }\n[data-accent=\"amber\"] { --h: 62; --c: 0.16; --l: 76%; --swatch-l: 76%; --accent-ink-override: oklch(21% 0.03 62); }\n[data-accent=\"mono\"] {\n --h: 0;\n --c: 0;\n --swatch-l: 22%;\n --accent: var(--ink);\n --accent-hover: var(--ink-2);\n --accent-edge: var(--ink);\n --accent-ink: var(--paper);\n --accent-wash: var(--paper-3);\n --accent-wash-ink: var(--ink);\n --accent-2: var(--graphite-ink);\n --focus: var(--ink);\n --ring: oklch(55% 0 0 / 0.3);\n}\n\n/* ───────── 6. Component styles ─────────\n * The :where() part re-applies a style inside a scoped theme so its tokens follow that palette. */\n:root,\n[data-style=\"soft\"],\n:where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --card: var(--paper-2);\n --card-border: transparent;\n --elev-card: none;\n --inset: var(--inset-c);\n --pop: var(--pop-c);\n --pop-border: var(--rule);\n --elev-pop: 0 1px 2px oklch(0% 0 0 / var(--sh-a)), 0 14px 36px -10px oklch(0% 0 0 / calc(var(--sh-a) * 2.6));\n --field: var(--field-c);\n --field-border: var(--field-edge);\n --elev-field: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --key-face: var(--raised);\n --key-border: var(--rule);\n --elev-key: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --btn-bg: var(--accent);\n --btn-image: linear-gradient(oklch(calc(var(--l) + 5%) var(--c) var(--h)), oklch(calc(var(--l) - 4%) var(--c) calc(var(--h) + 6)));\n --btn-border: transparent;\n --elev-key-accent: inset 0 1px 0 oklch(100% 0 0 / 0.2), 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 1.6));\n --elev-key-danger: inset 0 1px 0 oklch(100% 0 0 / 0.2), 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 1.6));\n --elev-key-press: inset 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 2.5));\n --lift: 0px;\n --press: 0.5px;\n --tray: var(--paper-2);\n --tray-border: var(--rule);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"flat\"],\n:where([data-style=\"flat\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--rule-2);\n --elev-card: none;\n --inset: var(--paper-2);\n --pop: var(--pop-c);\n --pop-border: var(--rule-2);\n --elev-pop: none;\n --field: var(--paper-2);\n --field-border: var(--field-edge);\n --elev-field: none;\n --key-face: var(--paper-2);\n --key-border: var(--paper-2);\n --elev-key: none;\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: transparent;\n --elev-key-accent: none;\n --elev-key-danger: none;\n --elev-key-press: none;\n --lift: 0px;\n --press: 0px;\n --tray: var(--paper-2);\n --tray-border: var(--paper-2);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"glass\"],\n:where([data-style=\"glass\"]) :where([data-theme], .dark) {\n --card: color-mix(in oklab, var(--paper) 42%, transparent);\n --card-border: var(--glass-edge);\n --elev-card: inset 0 1px 0 var(--glass-hi), 0 10px 40px -14px oklch(0% 0 0 / calc(var(--sh-a) * 4));\n --inset: color-mix(in oklab, var(--paper) 55%, transparent);\n --pop: color-mix(in oklab, var(--pop-c) 66%, transparent);\n --pop-border: var(--glass-edge);\n --elev-pop: inset 0 1px 0 var(--glass-hi), 0 18px 48px -14px oklch(0% 0 0 / calc(var(--sh-a) * 5));\n --field: color-mix(in oklab, var(--paper) 40%, transparent);\n --field-border: var(--glass-edge);\n --elev-field: inset 0 1px 3px oklch(0% 0 0 / var(--sh-a));\n --key-face: color-mix(in oklab, var(--paper) 38%, transparent);\n --key-border: var(--glass-edge);\n --elev-key: inset 0 1px 0 var(--glass-hi), 0 4px 14px -6px oklch(0% 0 0 / calc(var(--sh-a) * 3));\n --btn-bg: transparent;\n --btn-image: linear-gradient(oklch(calc(var(--l) + 8%) var(--c) var(--h) / 0.85), oklch(var(--l) var(--c) var(--h) / 0.9));\n --btn-border: oklch(100% 0 0 / 0.28);\n --elev-key-accent: inset 0 1px 0 oklch(100% 0 0 / 0.4), 0 6px 18px -6px oklch(var(--l) var(--c) var(--h) / 0.65);\n --elev-key-danger: inset 0 1px 0 oklch(100% 0 0 / 0.4), 0 6px 18px -6px oklch(57% 0.2 27 / 0.6);\n --elev-key-press: inset 0 2px 6px oklch(0% 0 0 / calc(var(--sh-a) * 2));\n --lift: 0px;\n --press: 0.5px;\n --tray: color-mix(in oklab, var(--paper) 30%, transparent);\n --tray-border: var(--glass-edge);\n --surface-filter: blur(22px) saturate(1.8);\n /* One soft wash of the accent on the page colour: enough behind the glass to show the frost, no rainbow. */\n --stage:\n radial-gradient(60% 80% at 25% 15%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 70%),\n linear-gradient(160deg, color-mix(in oklab, var(--paper) 94%, var(--accent)), var(--paper-2));\n}\n\n[data-style=\"keycap\"],\n:where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--rule);\n --elev-card: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --inset: var(--paper-2);\n --pop: var(--pop-c);\n --pop-border: var(--rule);\n --elev-pop: 0 1px 2px oklch(0% 0 0 / var(--sh-a)), 0 14px 36px -10px oklch(0% 0 0 / calc(var(--sh-a) * 2.6));\n --field: var(--field-c);\n --field-border: var(--field-edge);\n --elev-field: inset 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --key-face: var(--raised);\n --key-border: var(--rule-2);\n --elev-key: 0 2px 0 0 var(--key-edge), 0 5px 8px -5px oklch(0% 0 0 / calc(var(--sh-a) * 4)), inset 0 1px 0 var(--glass-hi);\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: var(--accent-edge);\n --elev-key-accent: 0 2px 0 0 var(--accent-edge), 0 5px 8px -5px oklch(30% 0.12 var(--h) / 0.45), inset 0 1px 0 oklch(100% 0 0 / 0.22);\n --elev-key-danger: 0 2px 0 0 var(--danger-edge), 0 5px 8px -5px oklch(30% 0.12 27 / 0.45), inset 0 1px 0 oklch(100% 0 0 / 0.22);\n --elev-key-press: inset 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 2.5));\n --lift: -1px;\n --press: 2px;\n --tray: var(--paper-3);\n --tray-border: var(--rule);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"brutal\"],\n:where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--ink);\n --elev-card: 4px 4px 0 0 var(--ink);\n --inset: var(--paper-2);\n --pop: var(--paper);\n --pop-border: var(--ink);\n --elev-pop: 5px 5px 0 0 var(--ink);\n --field: var(--paper);\n --field-border: var(--ink);\n --elev-field: 2px 2px 0 0 var(--ink);\n --key-face: var(--paper);\n --key-border: var(--ink);\n --elev-key: 3px 3px 0 0 var(--ink);\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: var(--ink);\n --elev-key-accent: 3px 3px 0 0 var(--ink);\n --elev-key-danger: 3px 3px 0 0 var(--ink);\n --elev-key-press: 0 0 0 0 var(--ink);\n --lift: -1px;\n --press: 2px;\n --tray: var(--paper);\n --tray-border: var(--ink);\n --surface-filter: none;\n --stage: none;\n}\n\n/* ───────── 7. Field and control tokens ─────────\n * Soft: filled fields with no border and an inset focus ring, white controls that\n * turn into a lit accent gradient when on. The other styles keep outlined fields and flat fills. */\n:root, [data-theme] {\n --field-soft: color-mix(in oklab, var(--ink) 7.5%, var(--paper));\n --field-soft-hover: color-mix(in oklab, var(--ink) 10.5%, var(--paper));\n --control-on-soft: linear-gradient(var(--accent), color-mix(in oklab, var(--accent) 80%, oklch(0% 0 0)));\n}\n:root:not([data-style]),\n[data-style=\"soft\"],\n:where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --field: var(--field-soft);\n --field-border: transparent;\n --elev-field: none;\n --field-hover: var(--field-soft-hover);\n --field-hover-border: transparent;\n --field-focus-border: transparent;\n --field-focus-ring: inset 0 0 0 2px var(--accent);\n --control: var(--raised);\n --control-border: var(--rule-2);\n --elev-control: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --control-on-image: var(--control-on-soft);\n --elev-control-on: inset 0 2px 0 oklch(100% 0 0 / 0.25), inset 0 0 0 1px var(--accent);\n --track-off: var(--field-soft-hover);\n --thumb-image: linear-gradient(var(--raised) 44%, var(--paper-2));\n --elev-thumb: 0 3px 3px oklch(0% 0 0 / 0.04), 0 0.75px 0 oklch(0% 0 0 / 0.08);\n --tray-border: transparent;\n /* Floating bars need an edge even where trays have none. */\n --float-border: var(--rule);\n --pill-radius: calc(6px * var(--radius-factor));\n}\n[data-style=\"flat\"], :where([data-style=\"flat\"]) :where([data-theme], .dark),\n[data-style=\"glass\"], :where([data-style=\"glass\"]) :where([data-theme], .dark),\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark),\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --field-hover: var(--field);\n --field-hover-border: var(--muted);\n --field-focus-border: var(--accent);\n --field-focus-ring: 0 0 0 3px var(--ring);\n --control: var(--field);\n --control-border: var(--field-border);\n --elev-control: var(--elev-field);\n --control-on-image: none;\n --elev-control-on: none;\n --track-off: var(--rule-2);\n --thumb-image: none;\n --elev-thumb: 0 1px 3px var(--overlay);\n --float-border: var(--tray-border);\n --pill-radius: var(--radius-input);\n}\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --control-on-image: var(--control-on-soft);\n --elev-control-on: inset 0 1px 0 oklch(100% 0 0 / 0.25), 0 1px 0 var(--accent-edge);\n}\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --field-focus-border: var(--ink);\n --field-focus-ring: 3px 3px 0 0 var(--accent);\n --elev-thumb: none;\n}\n\n/* Soft popovers: a hairline and a light lift. Danger buttons get the same lit gradient as primary. */\n:root:not([data-style]), [data-style=\"soft\"], :where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --elev-pop: 0 1px 1px oklch(0% 0 0 / 0.04), 0 4px 4px oklch(0% 0 0 / 0.02), 0 16px 32px -16px oklch(0% 0 0 / calc(var(--sh-a) * 2));\n --btn-danger-image: linear-gradient(color-mix(in oklab, var(--danger) 92%, oklch(100% 0 0)), color-mix(in oklab, var(--danger) 86%, oklch(0% 0 0)));\n}\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --btn-danger-image: linear-gradient(color-mix(in oklab, var(--danger) 92%, oklch(100% 0 0)), color-mix(in oklab, var(--danger) 86%, oklch(0% 0 0)));\n}\n[data-style=\"flat\"], :where([data-style=\"flat\"]) :where([data-theme], .dark),\n[data-style=\"glass\"], :where([data-style=\"glass\"]) :where([data-theme], .dark),\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --btn-danger-image: none;\n}\n\n/* ───────── 8. Brand ─────────\n * The PlankUI mark on the site takes the visitor's accent: planks in the accent's ink on an accent tile.\n * Files outside the site (favicon, badges, social card) keep the fixed blue below. */\n:root {\n --brand-blue: oklch(56% 0.22 263);\n --brand-tile: var(--accent, var(--brand-blue));\n --brand-plank: var(--accent-ink, oklch(100% 0 0));\n}\n\n/* Holographic foil for PlankUI Pro: soft pastels that shift like a tilted sticker. Fixed in every theme. */\n:root {\n --holo-1: oklch(88% 0.08 350);\n --holo-2: oklch(86% 0.08 285);\n --holo-3: oklch(90% 0.07 215);\n --holo-4: oklch(92% 0.08 160);\n --holo-5: oklch(95% 0.09 95);\n --holo-ink: oklch(24% 0.06 285);\n --holo-edge: oklch(100% 0 0);\n}\n","src/styles.css":"@import \"tailwindcss\";\n@import \"./tokens.css\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n@theme inline {\n --color-paper: var(--paper);\n --color-paper-2: var(--paper-2);\n --color-paper-3: var(--paper-3);\n --color-ink: var(--ink);\n --color-ink-2: var(--ink-2);\n --color-muted: var(--muted);\n --color-rule: var(--rule);\n --color-rule-2: var(--rule-2);\n --color-accent: var(--accent);\n --color-accent-hover: var(--accent-hover);\n --color-accent-edge: var(--accent-edge);\n --color-accent-ink: var(--accent-ink);\n --color-accent-wash: var(--accent-wash);\n --color-accent-wash-ink: var(--accent-wash-ink);\n --color-accent-2: var(--accent-2);\n --color-graphite: var(--graphite);\n --color-graphite-2: var(--graphite-2);\n --color-graphite-ink: var(--graphite-ink);\n --color-graphite-muted: var(--graphite-muted);\n --color-graphite-rule: var(--graphite-rule);\n --color-key-face: var(--key-face);\n --color-key-edge: var(--key-edge);\n --color-danger: var(--danger);\n --color-danger-wash: var(--danger-wash);\n --color-danger-wash-ink: var(--danger-wash-ink);\n --color-success: var(--success);\n --color-success-wash: var(--success-wash);\n --color-success-wash-ink: var(--success-wash-ink);\n --color-warning: var(--warning);\n --color-warning-wash: var(--warning-wash);\n --color-warning-wash-ink: var(--warning-wash-ink);\n --color-overlay: var(--overlay);\n --color-white: var(--white);\n --color-focus: var(--focus);\n --color-field-edge: var(--field-edge);\n --color-card: var(--card);\n --color-card-border: var(--card-border);\n --color-inset: var(--inset);\n --color-pop: var(--pop);\n --color-pop-border: var(--pop-border);\n --color-field: var(--field);\n --color-field-border: var(--field-border);\n --color-tray: var(--tray);\n --color-tray-border: var(--tray-border);\n --color-raised: var(--raised);\n\n --font-display: var(--ff-display);\n --font-sans: var(--ff-body);\n --font-mono: var(--ff-code);\n\n --shadow-card: var(--elev-card);\n --shadow-pop: var(--elev-pop);\n --shadow-key: var(--elev-key);\n --shadow-key-accent: var(--elev-key-accent);\n --shadow-key-danger: var(--elev-key-danger);\n --shadow-key-press: var(--elev-key-press);\n --shadow-field: var(--elev-field);\n}\n\n@theme {\n --radius-sm: calc(6px * var(--radius-factor));\n --radius-input: calc(10px * var(--radius-factor));\n --radius-card: calc(12px * var(--radius-factor));\n --radius-panel: calc(18px * var(--radius-factor));\n --radius-tile: calc(16px * var(--radius-factor));\n --radius-shell: calc(22px * var(--radius-factor));\n --radius-frame: calc(28px * var(--radius-factor));\n --radius-pill: calc(999px * var(--radius-factor));\n}\n\n/* ───────────── Base ───────────── */\n*, *::before, *::after { box-sizing: border-box; }\nhtml, body { overflow-x: clip; }\nhtml { scroll-padding-top: 96px; scrollbar-color: var(--rule-2) var(--paper); }\nbody {\n margin: 0;\n background: var(--paper);\n color: var(--ink);\n font-family: var(--ff-body);\n font-size: 1rem;\n line-height: 1.55;\n -webkit-font-smoothing: antialiased;\n caret-color: var(--accent);\n}\n::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); color: var(--ink); }\n:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\nh1, h2, h3, h4 {\n font-family: var(--ff-display);\n font-weight: var(--display-weight);\n letter-spacing: var(--display-tracking);\n line-height: 1.05;\n overflow-wrap: break-word;\n min-width: 0;\n text-wrap: balance;\n font-synthesis: none;\n}\nh3, h4 { font-weight: 500; line-height: 1.2; }\np { text-wrap: pretty; }\ncode, kbd, pre { font-family: var(--ff-code); }\n\n/* Hairline ruled grid used behind previews and the hero bench */\n.bg-cells {\n background-image:\n radial-gradient(circle at 1px 1px, var(--rule-2) 1px, transparent 0);\n background-size: 16px 16px;\n}\n\n/* Radix enter/exit motion: opacity and transform only */\n@keyframes vx-pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }\n@keyframes vx-pop-out { to { opacity: 0; transform: translateY(-4px) scale(0.98); } }\n@keyframes vx-fade-in { from { opacity: 0; } }\n@keyframes vx-fade-out { to { opacity: 0; } }\n@keyframes vx-dialog-in { from { opacity: 0; transform: translate(-50%, -48%) scale(0.97); } }\n@keyframes vx-dialog-out { to { opacity: 0; transform: translate(-50%, -48%) scale(0.97); } }\n@keyframes vx-toast-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } }\n@keyframes vx-spin { to { transform: rotate(360deg); } }\n@keyframes vx-shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }\n@keyframes vx-reveal { from { opacity: 0; transform: translateY(4px); } }\n\n.vx-pop[data-state=\"open\"], .vx-pop[data-state=\"delayed-open\"], .vx-pop[data-state=\"instant-open\"] { animation: vx-pop-in var(--dur-short) var(--ease-out); }\n.vx-pop[data-state=\"closed\"] { animation: vx-pop-out var(--dur-micro) ease-in; }\n.vx-fade[data-state=\"open\"] { animation: vx-fade-in var(--dur-short) var(--ease-out); }\n.vx-fade[data-state=\"closed\"] { animation: vx-fade-out var(--dur-micro) ease-in; }\n.vx-dialog[data-state=\"open\"] { animation: vx-dialog-in var(--dur-short) var(--ease-out); }\n.vx-dialog[data-state=\"closed\"] { animation: vx-dialog-out var(--dur-micro) ease-in; }\n.vx-toast { animation: vx-toast-in var(--dur-short) var(--ease-out); }\n.vx-spin { animation: vx-spin 0.8s linear infinite; }\n.vx-reveal[data-state=\"open\"] { animation: vx-reveal var(--dur-short) var(--ease-out); }\n.vx-shimmer { position: relative; overflow: hidden; }\n.vx-shimmer::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: linear-gradient(90deg, transparent, color-mix(in oklab, var(--paper) 60%, transparent), transparent);\n animation: vx-shimmer 1.4s ease-in-out infinite;\n}\n\n/* sugar-high code tokens: one accent, never a rainbow */\n.code-block {\n --sh-class: var(--accent-wash-ink);\n --sh-identifier: var(--ink);\n --sh-sign: var(--muted);\n --sh-property: var(--ink-2);\n --sh-entity: var(--accent-wash-ink);\n --sh-jsxliterals: var(--ink);\n --sh-string: var(--code-string);\n --sh-keyword: oklch(from var(--accent-wash-ink) l c calc(h + 40));\n --sh-comment: var(--muted);\n}\n\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* An accent preview chip: computes its colour on the element, so a nested data-accent shows that hue */\n.swatch { background: oklch(var(--swatch-l, 56%) var(--c) var(--h)); }\n\n/* Sheet slides: transform only */\n@keyframes vx-sheet-right { from { transform: translateX(100%); } }\n@keyframes vx-sheet-right-out { to { transform: translateX(100%); } }\n@keyframes vx-sheet-left { from { transform: translateX(-100%); } }\n@keyframes vx-sheet-left-out { to { transform: translateX(-100%); } }\n@keyframes vx-sheet-bottom { from { transform: translateY(100%); } }\n@keyframes vx-sheet-bottom-out { to { transform: translateY(100%); } }\n.vx-sheet-right[data-state=\"open\"] { animation: vx-sheet-right var(--dur-short) var(--ease-out); }\n.vx-sheet-right[data-state=\"closed\"] { animation: vx-sheet-right-out var(--dur-short) ease-in; }\n.vx-sheet-left[data-state=\"open\"] { animation: vx-sheet-left var(--dur-short) var(--ease-out); }\n.vx-sheet-left[data-state=\"closed\"] { animation: vx-sheet-left-out var(--dur-short) ease-in; }\n.vx-sheet-bottom[data-state=\"open\"] { animation: vx-sheet-bottom var(--dur-short) var(--ease-out); }\n.vx-sheet-bottom[data-state=\"closed\"] { animation: vx-sheet-bottom-out var(--dur-short) ease-in; }\n@keyframes vx-caret { 50% { opacity: 0; } }\n.vx-caret { animation: vx-caret 1s step-end infinite; }\n\n/* Component style hooks */\n.vx-glass { -webkit-backdrop-filter: var(--surface-filter); backdrop-filter: var(--surface-filter); }\n[data-style=\"glass\"] .vx-stage, .vx-stage[data-style=\"glass\"] { background-image: var(--stage); }\n\n/* Moving columns: transform only, paused on hover, still under reduced motion */\n@keyframes vx-marquee-up { to { transform: translateY(-50%); } }\n@keyframes vx-marquee-down { from { transform: translateY(-50%); } to { transform: translateY(0); } }\n.vx-marquee-track { animation: vx-marquee-up var(--marquee-dur, 40s) linear infinite; will-change: transform; }\n.vx-marquee-track[data-reverse] { animation-name: vx-marquee-down; }\n.vx-marquee:hover .vx-marquee-track { animation-play-state: paused; }\n@media (prefers-reduced-motion: reduce) { .vx-marquee-track { animation: none !important; } }\n\n/* Scoped themes need their own body face */\n[data-theme] { font-family: var(--ff-body); }\n\n/* Theme graphics: one decorative layer behind the hero, different per theme */\n.vx-graphic {\n background:\n radial-gradient(28% 60% at 22% 0%, oklch(78% 0.14 var(--h) / 0.35), transparent 70%),\n radial-gradient(26% 55% at 50% 0%, oklch(82% 0.12 calc(var(--h) + 120) / 0.3), transparent 70%),\n radial-gradient(28% 60% at 80% 0%, oklch(84% 0.11 calc(var(--h) - 80) / 0.3), transparent 70%);\n}\n[data-theme=\"ember\"] .vx-graphic, .vx-graphic[data-theme=\"ember\"] {\n background:\n radial-gradient(50% 70% at 50% 0%, oklch(75% 0.17 var(--h) / 0.35), transparent 70%),\n repeating-linear-gradient(90deg, transparent 0 22px, oklch(70% 0.15 var(--h) / 0.14) 22px 24px, transparent 24px 60px, oklch(80% 0.12 calc(var(--h) + 20) / 0.1) 60px 61px);\n -webkit-mask-image: linear-gradient(to bottom, black, transparent 85%);\n mask-image: linear-gradient(to bottom, black, transparent 85%);\n}\n[data-theme=\"grove\"] .vx-graphic, .vx-graphic[data-theme=\"grove\"] {\n background:\n radial-gradient(circle at 1px 1px, oklch(55% 0.06 var(--h) / 0.35) 1.2px, transparent 0) 0 0 / 22px 22px,\n radial-gradient(45% 60% at 50% 0%, oklch(85% 0.08 var(--h) / 0.45), transparent 70%);\n -webkit-mask-image: radial-gradient(ellipse at 50% 20%, black 30%, transparent 75%);\n mask-image: radial-gradient(ellipse at 50% 20%, black 30%, transparent 75%);\n}\n[data-theme=\"nebula\"] .vx-graphic, .vx-graphic[data-theme=\"nebula\"] {\n background:\n radial-gradient(1px 1px at 12% 18%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 32% 42%, var(--ink) 50%, transparent 51%),\n radial-gradient(1.5px 1.5px at 58% 12%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 74% 36%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 88% 16%, var(--ink) 50%, transparent 51%),\n radial-gradient(1.5px 1.5px at 44% 28%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 22% 58%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 66% 54%, var(--ink) 50%, transparent 51%),\n radial-gradient(35% 50% at 25% 10%, oklch(65% 0.2 var(--h) / 0.4), transparent 70%),\n radial-gradient(35% 50% at 78% 20%, oklch(70% 0.16 calc(var(--h) - 60) / 0.35), transparent 70%);\n opacity: 0.9;\n}\n[data-theme=\"terminal\"] .vx-graphic, .vx-graphic[data-theme=\"terminal\"] {\n background:\n repeating-linear-gradient(0deg, oklch(60% 0.15 var(--h) / 0.07) 0 1px, transparent 1px 4px),\n linear-gradient(90deg, var(--rule) 1px, transparent 1px) 0 0 / 48px 48px,\n linear-gradient(0deg, var(--rule) 1px, transparent 1px) 0 0 / 48px 48px;\n -webkit-mask-image: radial-gradient(ellipse at 50% 15%, black 25%, transparent 75%);\n mask-image: radial-gradient(ellipse at 50% 15%, black 25%, transparent 75%);\n}\n[data-theme=\"candy\"] .vx-graphic, .vx-graphic[data-theme=\"candy\"] {\n background:\n radial-gradient(22% 38% at 15% 10%, oklch(85% 0.12 var(--h) / 0.7), transparent 70%),\n radial-gradient(20% 34% at 40% 0%, oklch(88% 0.1 80 / 0.6), transparent 70%),\n radial-gradient(22% 38% at 66% 8%, oklch(86% 0.1 200 / 0.6), transparent 70%),\n radial-gradient(20% 34% at 90% 4%, oklch(85% 0.12 300 / 0.55), transparent 70%);\n}\n\n/* Radius scales recomputed inside scoped themes */\n[data-theme], [style*=\"--radius-factor\"] {\n --radius-sm: calc(6px * var(--radius-factor));\n --radius-input: calc(10px * var(--radius-factor));\n --radius-card: calc(12px * var(--radius-factor));\n --radius-panel: calc(18px * var(--radius-factor));\n --radius-tile: calc(16px * var(--radius-factor));\n --radius-shell: calc(22px * var(--radius-factor));\n --radius-frame: calc(28px * var(--radius-factor));\n --radius-pill: calc(999px * var(--radius-factor));\n}\n\n/* Charts and composer motion: transform, opacity, clip-path and stroke only */\n@keyframes vx-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n@keyframes vx-fade-up { from { opacity: 0; transform: translateY(6px); } }\n.vx-draw { stroke-dasharray: 1; animation: vx-draw 1.1s var(--ease-out) both; }\n.vx-fade-up { animation: vx-fade-up var(--dur-short) var(--ease-out) both; }\n.vx-orbit { animation: vx-spin 2.6s linear infinite; }\n.vx-orbit-slow { animation: vx-spin 6s linear infinite; }\n\n/* Colour picker hue strip */\n.vx-hue { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }\n.vx-hue::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 999px; background: var(--white); border: 2px solid var(--white); box-shadow: 0 0 0 1px oklch(0% 0 0 / 0.3), 0 2px 4px oklch(0% 0 0 / 0.3); }\n.vx-hue::-moz-range-thumb { width: 14px; height: 14px; border-radius: 999px; background: var(--white); border: 2px solid var(--white); box-shadow: 0 0 0 1px oklch(0% 0 0 / 0.3); }\n\n/* Mode and theme switches: a circle grows from the click for mode, a soft crossfade for themes */\n::view-transition-old(root), ::view-transition-new(root) { animation-duration: 360ms; animation-timing-function: var(--ease-out); }\n[data-vt=\"reveal\"]::view-transition-old(root), [data-vt=\"reveal\"]::view-transition-new(root) { animation: none; mix-blend-mode: normal; }\n[data-vt=\"reveal\"]::view-transition-old(root) { z-index: 1; }\n[data-vt=\"reveal\"]::view-transition-new(root) { z-index: 2; }\n\n/* Composer light: a dash that laps the rim at constant speed */\n@keyframes vx-rim { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } }\n.vx-rim { animation: vx-rim 4.5s linear infinite; }\n@media (prefers-reduced-motion: reduce) { .vx-rim { animation: none; } }\n\n/* Thinking: text with a light passing through it */\n@keyframes vx-shimmer-text { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n.vx-shimmer-text {\n color: transparent;\n background: linear-gradient(90deg, var(--muted) 0%, var(--muted) 40%, var(--ink) 50%, var(--muted) 60%, var(--muted) 100%) 0 0 / 200% 100%;\n -webkit-background-clip: text;\n background-clip: text;\n animation: vx-shimmer-text 1.8s linear infinite;\n}\n\n/* Docs side menu: one guide line, a short tick into each row, a rounded corner on the last */\n/* Sidebar tree: the guide sits in the gutter, outside each row, so a selected row's card never covers it.\n Rows touch (no gap) and each guide segment runs past the row's 1px border, so the line is continuous. */\n.vx-tree { --tree-line: color-mix(in oklab, var(--muted) 38%, transparent); display: grid; gap: 0; }\n.vx-tree > * { position: relative; margin-left: 22px; padding-left: 10px !important; }\n.vx-tree > *::before {\n content: \"\"; position: absolute; left: -9px; top: -1px; bottom: -1px; border-left: 1px solid var(--tree-line);\n}\n.vx-tree > *::after {\n content: \"\"; position: absolute; left: -9px; top: 50%; width: 6px; border-top: 1px solid var(--tree-line);\n}\n.vx-tree > *:last-child::before { bottom: calc(50% - 1px); width: 6px; border-bottom: 1px solid var(--tree-line); border-bottom-left-radius: calc(5px * var(--radius-factor)); }\n.vx-tree > *:last-child::after { display: none; }\n\n/* Bar list, radar and activity rings entrances */\n@keyframes vx-grow-x { from { transform: scaleX(0); } }\n@keyframes vx-radar { from { opacity: 0; transform: scale(0.6); } }\n@keyframes vx-ring { from { stroke-dasharray: 0 var(--len); } }\n.vx-grow-x { transform-origin: left; animation: vx-grow-x 0.7s var(--ease-out) both; }\n.vx-radar { animation: vx-radar 0.6s var(--ease-out) both; }\n.vx-ring { animation: vx-ring 1.1s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-grow-x, .vx-radar, .vx-ring { animation: none; } }\n/* Reveal lines left to right; safe with non-scaling strokes, unlike dash-offset drawing. */\n@keyframes vx-wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }\n.vx-wipe { animation: vx-wipe 1.1s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-wipe { animation: none; } }\n\n/* Field states follow the style: Soft fills and rings inside, the rest outline and glow. */\n@utility field-hover {\n border-color: var(--field-hover-border);\n background-color: var(--field-hover);\n}\n@utility field-focus {\n border-color: var(--field-focus-border);\n box-shadow: var(--field-focus-ring);\n}\n@keyframes vx-slide-next { from { opacity: 0; transform: translateX(16px); } }\n@keyframes vx-slide-prev { from { opacity: 0; transform: translateX(-16px); } }\n@keyframes vx-rise { from { transform: scaleY(0); } }\n.vx-rise { animation: vx-rise 0.7s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-rise { animation: none; } }\n\n/* Holographic sticker: a conic foil, a light sweep on hover, and an optional slow drift. */\n@property --holo-angle { syntax: \"<angle>\"; initial-value: 200deg; inherits: false; }\n.vx-holo {\n position: relative;\n overflow: hidden;\n isolation: isolate;\n background:\n radial-gradient(120% 90% at var(--holo-x, 30%) var(--holo-y, 20%), oklch(100% 0 0 / 0.55), transparent 55%),\n conic-gradient(from var(--holo-angle) at 50% 50%, var(--holo-1), var(--holo-2), var(--holo-3), var(--holo-4), var(--holo-5), var(--holo-1));\n color: var(--holo-ink);\n}\n.vx-holo::after {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: -1;\n background: linear-gradient(115deg, transparent 30%, oklch(100% 0 0 / 0.75) 45%, transparent 60%);\n transform: translateX(-130%);\n transition: transform 0.9s var(--ease-out);\n}\n.vx-holo:hover::after, .group:hover .vx-holo::after { transform: translateX(130%); }\n@keyframes vx-holo-drift { to { --holo-angle: 560deg; } }\n.vx-holo-live { animation: vx-holo-drift 9s linear infinite; }\n@media (prefers-reduced-motion: reduce) {\n .vx-holo::after { transition: none; }\n .vx-holo-live { animation: none; }\n}\n\n/* Menus and dialogs lock scrolling with `overflow: hidden` on body. With overflow-x clipped on html, that turns\n body into its own scroll box, and sticky elements (the docs sidebar, the top bar) jump to the top of the page.\n `clip` keeps body from becoming a scroll box; the lock still blocks wheel and touch scrolling. */\nhtml body[data-scroll-locked] { overflow: clip !important; margin-right: 0 !important; padding-right: 0 !important; }\n","src/lib/cn.ts":"import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"},"menuStyles":"/**\n * One look for every dropdown in the library: Select, Dropdown menu, Context menu, Combobox,\n * Command, Tag input suggestions, Date picker presets and the Model picker.\n * Change these strings to restyle all of them at once.\n */\n\n/** The floating panel. */\nexport const menuPanel =\n \"vx-pop vx-glass z-[100] min-w-[12rem] overflow-hidden rounded-[calc(16px*var(--radius-factor))] border border-pop-border bg-pop p-2 text-ink shadow-pop outline-none\";\n\n/** A row. Highlight comes from keyboard or pointer (data-highlighted) or an explicit data-active attribute. */\nexport const menuItem =\n \"relative flex min-h-8 cursor-default select-none items-center gap-2 rounded-input px-2 py-1.5 text-[14px] font-medium text-ink outline-none transition-[background-color] duration-[var(--dur-micro)] data-[highlighted]:bg-paper-3 data-[active=true]:bg-paper-3 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted\";\n\n/** A section heading inside a menu. */\nexport const menuLabel = \"px-2 pb-1 pt-1.5 text-[14px] font-medium text-muted\";\n\n/** A divider between groups. */\nexport const menuSeparator = \"-mx-2 my-1.5 h-px bg-rule\";\n\n/** The selected indicator on the right of a row. */\nexport const menuCheck = \"ml-auto size-4 shrink-0 !text-accent\";\n"},"themes":[{"id":"base","name":"Base","font":"Inter","note":"Pure white and black, electric blue","style":"soft","radius":1},{"id":"ember","name":"Ember","font":"Bricolage Grotesque","note":"Warm paper, orange, light streaks","style":"keycap","radius":1},{"id":"grove","name":"Grove","font":"Fraunces","note":"Olive paper, sage, a serif voice","style":"flat","radius":0.5},{"id":"nebula","name":"Nebula","font":"Space Grotesk","note":"Deep navy, violet, frosted glass","style":"glass","radius":1.5},{"id":"terminal","name":"Terminal","font":"JetBrains Mono","note":"Monospace, phosphor green, hard edges","style":"brutal","radius":0},{"id":"candy","name":"Candy","font":"Nunito","note":"Blush paper, rose, soft round keys","style":"keycap","radius":1.5}],"styles":[{"id":"soft","name":"Soft","note":"Grey shells, white panels, gentle shadows"},{"id":"flat","name":"Flat","note":"Fills and hairlines, no shadows"},{"id":"glass","name":"Liquid glass","note":"Frosted, translucent, lit from above"},{"id":"keycap","name":"Keycap","note":"Raised keys with a real bottom edge"},{"id":"brutal","name":"Brutal","note":"Ink borders and hard offset shadows"}],"accents":[{"id":"auto","name":"Theme"},{"id":"blue","name":"Blue"},{"id":"vermilion","name":"Vermilion"},{"id":"violet","name":"Violet"},{"id":"emerald","name":"Emerald"},{"id":"rose","name":"Rose"},{"id":"amber","name":"Amber"},{"id":"mono","name":"Mono"}],"components":[{"slug":"button","name":"Button","category":"Actions","description":"A keycap with a real bottom edge. It lifts on hover and sinks when pressed.","deps":["radix-ui","class-variance-authority"],"uses":["spinner"],"usage":"<Button>Save</Button>\n<Button variant=\"secondary\" size=\"sm\">Cancel</Button>","props":[["variant","\"primary\" | \"secondary\" | \"soft\" | \"outline\" | \"ghost\" | \"danger\" | \"link\"","\"primary\"","Visual style."],["size","\"xs\" | \"sm\" | \"md\" | \"lg\" | \"icon\" | \"icon-sm\"","\"md\"","Height and padding."],["loading","boolean","false","Shows a spinner and blocks clicks."],["asChild","boolean","false","Render the child element, such as a router link, with button styles."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { Spinner } from \"./spinner\";\n\nexport const buttonVariants = cva(\n [\n \"relative inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium\",\n \"transition-[transform,box-shadow,background-color,color] duration-[var(--dur-micro)] ease-[var(--ease-out)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"disabled:pointer-events-none disabled:opacity-50 aria-busy:pointer-events-none\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n primary:\n \"vx-glass border border-[color:var(--btn-border)] bg-[color:var(--btn-bg)] [background-image:var(--btn-image)] text-accent-ink shadow-key-accent hover:translate-y-[var(--lift)] hover:brightness-110 active:translate-y-[var(--press)] active:shadow-key-press\",\n secondary:\n \"vx-glass border border-[color:var(--key-border)] bg-key-face text-ink shadow-key hover:translate-y-[var(--lift)] hover:bg-paper-2 active:translate-y-[var(--press)] active:shadow-key-press\",\n soft: \"bg-accent-wash text-accent-wash-ink hover:bg-accent-wash/70\",\n outline: \"border border-[color:var(--key-border)] text-ink hover:bg-paper-2\",\n ghost: \"text-ink-2 hover:bg-paper-3 hover:text-ink\",\n danger:\n \"border border-[color:var(--btn-border)] bg-danger [background-image:var(--btn-danger-image)] text-white shadow-key-danger hover:translate-y-[var(--lift)] hover:brightness-110 active:translate-y-[var(--press)] active:shadow-key-press\",\n link: \"h-auto px-0 text-accent underline-offset-4 decoration-rule-2 hover:underline hover:decoration-accent\",\n },\n size: {\n xs: \"h-7 rounded-sm px-2.5 text-xs\",\n sm: \"h-8 rounded-input px-3 text-sm\",\n md: \"h-9 rounded-input px-4 text-sm\",\n lg: \"h-11 rounded-card px-5 text-base\",\n icon: \"size-9 rounded-input\",\n \"icon-sm\": \"size-8 rounded-input\",\n },\n },\n defaultVariants: { variant: \"primary\", size: \"md\" },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Render the child element instead of a button, keeping the styles. */\n asChild?: boolean;\n /** Show a spinner and block interaction. */\n loading?: boolean;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild, loading, children, disabled, ...props }, ref) => {\n const Comp = asChild ? Slot.Root : \"button\";\n return (\n <Comp\n ref={ref}\n data-slot=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {loading && <Spinner className=\"size-4\" />}\n {children}\n </>\n )}\n </Comp>\n );\n },\n);\nButton.displayName = \"Button\";\n"},{"slug":"segmented","name":"Segmented control","category":"Actions","description":"One choice from a short set. A raised pill slides to the chosen option.","deps":["radix-ui"],"uses":[],"usage":"<Segmented options={[{ value: \"monthly\", label: \"Monthly\" }, { value: \"yearly\", label: \"Yearly\" }]} />","props":[["options","{ value: string; label: ReactNode; disabled?: boolean }[]","required","The choices."],["value / defaultValue","string","first option","Controlled or initial value."],["onValueChange","(value: string) => void","","Called when the choice changes."],["size","\"sm\" | \"md\"","\"md\"","Key height."]],"pro":false,"source":"import * as React from \"react\";\nimport { ToggleGroup } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Places a pill under the active item of a list and slides it on change. The pill is positioned\n * instantly and animated with a transform from where it was, so width and left never animate.\n */\nexport function useSlidingPill(list: React.RefObject<HTMLElement | null>, selector: string, active: unknown) {\n const pill = React.useRef<HTMLSpanElement>(null);\n const last = React.useRef<{ x: number; w: number } | null>(null);\n React.useLayoutEffect(() => {\n const l = list.current;\n const p = pill.current;\n if (!l || !p) return;\n const place = (animate: boolean) => {\n const a = l.querySelector<HTMLElement>(selector);\n if (!a) {\n p.style.opacity = \"0\";\n return;\n }\n const { offsetLeft: x, offsetTop: y, offsetWidth: w, offsetHeight: h } = a;\n Object.assign(p.style, { opacity: \"1\", left: `${x}px`, top: `${y}px`, width: `${w}px`, height: `${h}px` });\n const prev = last.current;\n last.current = { x, w };\n if (!animate || !prev || (prev.x === x && prev.w === w) || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n p.style.transition = \"none\";\n p.style.transform = `translateX(${prev.x - x}px) scaleX(${prev.w / w})`;\n void p.offsetWidth;\n p.style.transition = \"\";\n p.style.transform = \"\";\n };\n place(true);\n const ro = new ResizeObserver(() => place(false));\n ro.observe(l);\n return () => ro.disconnect();\n }, [list, selector, active]);\n return pill;\n}\n\n/** The pill itself: a raised key that slides between options. */\nexport const pillClass =\n \"pointer-events-none absolute left-0 top-0 origin-left rounded-[var(--pill-radius)] bg-key-face opacity-0 shadow-key transition-transform duration-200 ease-[var(--ease-out)]\";\n\nexport interface SegmentedProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n options: { value: string; label: React.ReactNode; disabled?: boolean }[];\n size?: \"sm\" | \"md\";\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** A single-choice control: options sit on a tray and a raised pill slides to the chosen one. */\nexport function Segmented({ value, defaultValue, onValueChange, options, size = \"md\", className, ...rest }: SegmentedProps) {\n const [inner, setInner] = React.useState(defaultValue ?? options[0]?.value);\n const current = value ?? inner;\n const list = React.useRef<HTMLDivElement>(null);\n const pill = useSlidingPill(list, '[data-state=\"on\"]', current);\n return (\n <ToggleGroup.Root\n ref={list}\n type=\"single\"\n value={current}\n onValueChange={(v) => {\n if (!v) return;\n setInner(v);\n onValueChange?.(v);\n }}\n className={cn(\"vx-glass relative inline-flex w-fit items-center gap-0.5 rounded-input border border-tray-border bg-tray p-1\", className)}\n {...rest}\n >\n <span ref={pill} aria-hidden className={pillClass} />\n {options.map((o) => (\n <ToggleGroup.Item\n key={o.value}\n value={o.value}\n disabled={o.disabled}\n className={cn(\n \"relative z-10 inline-flex items-center justify-center gap-1.5 rounded-[var(--pill-radius)] font-medium text-muted\",\n \"transition-colors duration-[var(--dur-micro)] hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:opacity-50 [&_svg]:size-4\",\n \"data-[state=on]:text-ink\",\n size === \"md\" ? \"h-7 px-3 text-sm\" : \"h-6 px-2 text-xs\",\n )}\n >\n {o.label}\n </ToggleGroup.Item>\n ))}\n </ToggleGroup.Root>\n );\n}\n"},{"slug":"dropdown-menu","name":"Dropdown menu","category":"Actions","description":"A menu of actions with shortcuts, checkable items and nested submenus.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<DropdownMenu>\n <DropdownMenuTrigger asChild><Button>Open</Button></DropdownMenuTrigger>\n <DropdownMenuContent>\n <DropdownMenuItem shortcut=\"⌘E\">Rename</DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>","props":[["DropdownMenuItem.shortcut","string","","Key hint shown on the right."],["DropdownMenuItem.destructive","boolean","false","Danger colouring for irreversible actions."],["DropdownMenuContent.align","\"start\" | \"center\" | \"end\"","\"center\"","Alignment against the trigger."]],"pro":false,"source":"import * as React from \"react\";\nimport { DropdownMenu as Menu } from \"radix-ui\";\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\nexport const DropdownMenu = Menu.Root;\nexport const DropdownMenuTrigger = Menu.Trigger;\nexport const DropdownMenuGroup = Menu.Group;\nexport const DropdownMenuSub = Menu.Sub;\nexport const DropdownMenuRadioGroup = Menu.RadioGroup;\n\nconst panel = menuPanel;\nconst item = menuItem;\n\nexport function DropdownMenuContent({ className, sideOffset = 6, ...props }: React.ComponentProps<typeof Menu.Content>) {\n return (\n <Menu.Portal>\n <Menu.Content sideOffset={sideOffset} className={cn(panel, className)} {...props} />\n </Menu.Portal>\n );\n}\n\nexport function DropdownMenuItem({\n className,\n shortcut,\n destructive,\n children,\n ...props\n}: React.ComponentProps<typeof Menu.Item> & { shortcut?: string; destructive?: boolean }) {\n return (\n <Menu.Item\n className={cn(item, destructive && \"text-danger data-[highlighted]:bg-danger-wash [&_svg]:text-danger\", className)}\n {...props}\n >\n {children}\n {shortcut && <span className=\"ml-auto font-mono text-xs text-muted\">{shortcut}</span>}\n </Menu.Item>\n );\n}\n\nexport function DropdownMenuCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof Menu.CheckboxItem>) {\n return (\n <Menu.CheckboxItem className={cn(item, \"pl-9\", className)} {...props}>\n <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n <Menu.ItemIndicator>\n <Check className=\"!text-accent\" />\n </Menu.ItemIndicator>\n </span>\n {children}\n </Menu.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof Menu.RadioItem>) {\n return (\n <Menu.RadioItem className={cn(item, \"pl-9\", className)} {...props}>\n <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n <Menu.ItemIndicator>\n <span className=\"block size-1.5 rounded-full bg-accent\" />\n </Menu.ItemIndicator>\n </span>\n {children}\n </Menu.RadioItem>\n );\n}\n\nexport function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof Menu.Label>) {\n return <Menu.Label className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof Menu.Separator>) {\n return <Menu.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n\nexport function DropdownMenuSubTrigger({ className, children, ...props }: React.ComponentProps<typeof Menu.SubTrigger>) {\n return (\n <Menu.SubTrigger className={cn(item, \"data-[state=open]:bg-paper-3\", className)} {...props}>\n {children}\n <ChevronRight className=\"ml-auto\" />\n </Menu.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof Menu.SubContent>) {\n return (\n <Menu.Portal>\n <Menu.SubContent className={cn(panel, className)} {...props} />\n </Menu.Portal>\n );\n}\n"},{"slug":"input","name":"Input","category":"Forms","description":"A text field with optional leading and trailing slots. Pair it with Field for label, hint and error.","deps":[],"uses":[],"usage":"<Field label=\"Email\" htmlFor=\"email\" hint=\"We never share it.\">\n <Input id=\"email\" leading={<AtSign />} />\n</Field>","props":[["leading","ReactNode","","Icon or text before the value."],["trailing","ReactNode","","Icon, key hint or button after the value."],["inputSize","\"sm\" | \"md\" | \"lg\"","\"md\"","Field height."],["aria-invalid","boolean","","Switches to the error style."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const fieldBase = [\n \"vx-glass w-full rounded-input border border-field-border bg-field text-sm text-ink shadow-field\",\n \"placeholder:text-muted/80 outline-none\",\n \"transition-[border-color,box-shadow] duration-[var(--dur-micro)]\",\n \"hover:field-hover\",\n \"focus-visible:field-focus focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:bg-paper-2 disabled:opacity-60\",\n \"aria-invalid:border-danger aria-invalid:focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--danger)_22%,transparent)]\",\n].join(\" \");\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n /** Icon or text placed before the value. */\n leading?: React.ReactNode;\n /** Icon, text or a button placed after the value. */\n trailing?: React.ReactNode;\n inputSize?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst sizes = { sm: \"h-8 px-2.5\", md: \"h-9 px-3\", lg: \"h-11 px-3.5 text-base\" };\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, leading, trailing, inputSize = \"md\", ...props }, ref) => {\n if (!leading && !trailing) {\n return <input ref={ref} className={cn(fieldBase, sizes[inputSize], className)} {...props} />;\n }\n return (\n <div className={cn(\"relative flex items-center\", className)}>\n {leading && (\n <span className=\"pointer-events-none absolute left-3 flex items-center text-muted [&_svg]:size-4\">\n {leading}\n </span>\n )}\n <input\n ref={ref}\n className={cn(fieldBase, sizes[inputSize], leading && \"pl-9\", trailing && \"pr-10\")}\n {...props}\n />\n {trailing && (\n <span className=\"absolute right-2 flex items-center text-muted [&_svg]:size-4\">{trailing}</span>\n )}\n </div>\n );\n },\n);\nInput.displayName = \"Input\";\n"},{"slug":"textarea","name":"Textarea","category":"Forms","description":"A multi-line field that shares the input's border, focus ring and error states.","deps":[],"uses":["input"],"usage":"<Textarea placeholder=\"Write a message\" />","props":[["...props","TextareaHTMLAttributes","","Every native textarea attribute."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { fieldBase } from \"./input\";\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(\n ({ className, ...props }, ref) => (\n <textarea ref={ref} className={cn(fieldBase, \"min-h-24 resize-y px-3 py-2 leading-6\", className)} {...props} />\n ),\n);\nTextarea.displayName = \"Textarea\";\n"},{"slug":"select","name":"Select","category":"Forms","description":"A listbox for picking one value, with groups, labels and disabled options.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Select defaultValue=\"fra\">\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"fra\">Frankfurt</SelectItem>\n </SelectContent>\n</Select>","props":[["value / defaultValue","string","","Controlled or initial value."],["onValueChange","(value: string) => void","","Called on selection."],["SelectItem.disabled","boolean","false","Makes an option unselectable."]],"pro":false,"source":"import * as React from \"react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\nexport const Select = SelectPrimitive.Root;\nexport const SelectGroup = SelectPrimitive.Group;\nexport const SelectValue = SelectPrimitive.Value;\n\nexport function SelectTrigger({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger>) {\n return (\n <SelectPrimitive.Trigger\n className={cn(\n \"flex h-9 w-full items-center justify-between gap-2 rounded-input border border-field-border bg-field px-3 text-sm text-ink shadow-field vx-glass\",\n \"hover:field-hover focus-visible:field-focus focus-visible:outline-none\",\n \"data-[placeholder]:text-muted disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate\",\n className,\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronsUpDown className=\"size-4 shrink-0 text-muted\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n}\n\nexport function SelectContent({\n className,\n children,\n position = \"popper\",\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n position={position}\n sideOffset={6}\n className={cn(\n menuPanel, \"relative max-h-[min(var(--radix-select-content-available-height),340px)] min-w-[8rem]\",\n position === \"popper\" && \"w-[var(--radix-select-trigger-width)]\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n}\n\nexport function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {\n return <SelectPrimitive.Label className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n className={cn(\n menuItem, \"w-full pr-9\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n <span className=\"absolute right-2.5 flex size-4 items-center justify-center [&_svg]:!text-accent\">\n <SelectPrimitive.ItemIndicator>\n <Check className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n );\n}\n\nexport function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n return <SelectPrimitive.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n"},{"slug":"checkbox","name":"Checkbox","category":"Forms","description":"A binary or indeterminate choice, for lists where many items can be on.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Checkbox id=\"terms\" />\n<Label htmlFor=\"terms\">Accept terms</Label>","props":[["checked","boolean | \"indeterminate\"","","Controlled state."],["onCheckedChange","(checked) => void","","Called on toggle."],["disabled","boolean","false","Blocks interaction."]],"pro":false,"source":"import * as React from \"react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer grid size-4 shrink-0 place-items-center rounded-[calc(4px*var(--radius-factor))] border border-[color:var(--control-border)] bg-[color:var(--control)] shadow-[var(--elev-control)]\",\n \"transition-[background-color,border-color,box-shadow] duration-[var(--dur-micro)] hover:border-muted\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:border-transparent data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)] data-[state=checked]:text-accent-ink\",\n \"data-[state=indeterminate]:border-transparent data-[state=indeterminate]:bg-accent data-[state=indeterminate]:[background-image:var(--control-on-image)] data-[state=indeterminate]:shadow-[var(--elev-control-on)] data-[state=indeterminate]:text-accent-ink\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator className=\"grid place-items-center\">\n {props.checked === \"indeterminate\" ? (\n <Minus className=\"size-3\" strokeWidth={3} />\n ) : (\n <Check className=\"size-3\" strokeWidth={3.5} />\n )}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = \"Checkbox\";\n"},{"slug":"radio-group","name":"Radio group","category":"Forms","description":"One choice from a visible set. RadioCard wraps each option in a selectable panel.","deps":["radix-ui"],"uses":[],"usage":"<RadioGroup defaultValue=\"team\">\n <RadioCard value=\"team\" title=\"Team\" description=\"Shared projects.\" />\n</RadioGroup>","props":[["value / defaultValue","string","","Selected option."],["RadioCard.title","ReactNode","required","Option title."],["RadioCard.description","ReactNode","","Supporting line."]],"pro":false,"source":"import * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return <RadioGroupPrimitive.Root className={cn(\"grid gap-2.5\", className)} {...props} />;\n}\n\nexport function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n className={cn(\n \"peer grid size-4 shrink-0 place-items-center rounded-full border border-[color:var(--control-border)] bg-[color:var(--control)] shadow-[var(--elev-control)]\",\n \"transition-[background-color,border-color,box-shadow] duration-[var(--dur-micro)] hover:border-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:border-transparent data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)]\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"block size-1.5 rounded-full bg-accent-ink\" />\n </RadioGroupPrimitive.Item>\n );\n}\n\n/** A bordered, selectable card that wraps a radio. */\nexport function RadioCard({\n value,\n title,\n description,\n className,\n}: {\n value: string;\n title: React.ReactNode;\n description?: React.ReactNode;\n className?: string;\n}) {\n return (\n <label\n className={cn(\n \"vx-glass flex cursor-pointer items-start gap-3 rounded-card border border-card-border bg-card p-3.5 shadow-card\",\n \"transition-[border-color,background-color] duration-[var(--dur-micro)] hover:border-rule-2\",\n \"has-[[data-state=checked]]:border-accent has-[[data-state=checked]]:bg-accent-wash/50\",\n className,\n )}\n >\n <RadioGroupItem value={value} className=\"mt-0.5\" />\n <span className=\"grid gap-0.5\">\n <span className=\"text-sm font-medium text-ink\">{title}</span>\n {description && <span className=\"text-sm text-muted\">{description}</span>}\n </span>\n </label>\n );\n}\n"},{"slug":"switch","name":"Switch","category":"Forms","description":"An on and off setting that applies immediately.","deps":["radix-ui"],"uses":[],"usage":"<Switch id=\"wifi\" defaultChecked />","props":[["checked / defaultChecked","boolean","","State."],["onCheckedChange","(checked: boolean) => void","","Called on toggle."],["size","\"sm\" | \"md\"","\"md\"","Track size."]],"pro":false,"source":"import * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Switch = React.forwardRef<\n React.ElementRef<typeof SwitchPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root> & { size?: \"sm\" | \"md\" }\n>(({ className, size = \"md\", ...props }, ref) => (\n <SwitchPrimitive.Root\n ref={ref}\n className={cn(\n \"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent p-0.5\",\n \"bg-[color:var(--track-off)] shadow-[inset_0_1px_2px_oklch(0%_0_0/calc(var(--sh-a)*1.2))] transition-[background-color,box-shadow] duration-[var(--dur-short)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)] disabled:cursor-not-allowed disabled:opacity-50\",\n size === \"md\" ? \"h-6 w-[42px]\" : \"h-4 w-7\",\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n className={cn(\n \"pointer-events-none grid place-items-center rounded-full bg-key-face [background-image:var(--thumb-image)] shadow-[var(--elev-thumb)]\",\n \"transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)] data-[state=unchecked]:translate-x-0\",\n size === \"md\" ? \"size-[18px] data-[state=checked]:translate-x-[18px]\" : \"size-2.5 data-[state=checked]:translate-x-3\",\n )}\n />\n </SwitchPrimitive.Root>\n));\nSwitch.displayName = \"Switch\";\n"},{"slug":"slider","name":"Slider","category":"Forms","description":"Pick a value or a range by dragging. Pass two values for a range.","deps":["radix-ui"],"uses":[],"usage":"<Slider defaultValue={[40]} max={100} step={1} />","props":[["value / defaultValue","number[]","","One value, or two for a range."],["min / max / step","number","0 / 100 / 1","Bounds and increment."],["onValueChange","(value: number[]) => void","","Called while dragging."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Slider({ className, defaultValue, value, ...props }: React.ComponentProps<typeof SliderPrimitive.Root>) {\n const count = (value ?? defaultValue ?? [0]).length;\n return (\n <SliderPrimitive.Root\n defaultValue={defaultValue}\n value={value}\n className={cn(\"relative flex w-full touch-none select-none items-center py-2 data-[disabled]:opacity-50\", className)}\n {...props}\n >\n <SliderPrimitive.Track className=\"relative h-1.5 w-full grow overflow-hidden rounded-full bg-[color:var(--track-off)] shadow-[inset_0_1px_1px_oklch(0%_0_0/0.06)]\">\n <SliderPrimitive.Range className=\"absolute h-full rounded-full bg-accent [background-image:var(--control-on-image)] shadow-[inset_0_1px_0_oklch(100%_0_0/0.24)]\" />\n </SliderPrimitive.Track>\n {Array.from({ length: count }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={props[\"aria-label\"] ?? \"Value\"}\n className={cn(\n \"block size-5 cursor-grab rounded-full border border-[color:var(--key-border)] bg-key-face [background-image:var(--thumb-image)] shadow-[0_2px_4px_oklch(0%_0_0/0.1),inset_0_1px_0_oklch(100%_0_0/0.8)]\",\n \"transition-transform duration-[var(--dur-micro)] hover:scale-110 active:cursor-grabbing\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n )}\n />\n ))}\n </SliderPrimitive.Root>\n );\n}\n"},{"slug":"dialog","name":"Dialog","category":"Overlays","description":"A modal window that traps focus and returns it to the trigger when closed.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Dialog>\n <DialogTrigger asChild><Button>Open</Button></DialogTrigger>\n <DialogContent>\n <DialogTitle>Title</DialogTitle>\n </DialogContent>\n</Dialog>","props":[["open / onOpenChange","boolean / (open) => void","","Controlled state."],["DialogContent.hideClose","boolean","false","Hide the corner close button."]],"pro":false,"source":"import * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport function DialogContent({\n className,\n children,\n hideClose,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { hideClose?: boolean }) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <DialogPrimitive.Content\n className={cn(\n \"vx-dialog fixed left-1/2 top-1/2 z-[400] grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4\",\n \"vx-glass rounded-panel border border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\",\n className,\n )}\n {...props}\n >\n {children}\n {!hideClose && (\n <DialogPrimitive.Close\n aria-label=\"Close\"\n className=\"absolute right-4 top-4 grid size-7 place-items-center rounded-input text-muted hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n}\n\nexport function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5 pr-8\", className)} {...props} />;\n}\nexport function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return <DialogPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n"},{"slug":"popover","name":"Popover","category":"Overlays","description":"Rich, interactive content anchored to a trigger.","deps":["radix-ui"],"uses":[],"usage":"<Popover>\n <PopoverTrigger asChild><Button>Open</Button></PopoverTrigger>\n <PopoverContent>…</PopoverContent>\n</Popover>","props":[["PopoverContent.align","\"start\" | \"center\" | \"end\"","\"center\"","Alignment."],["PopoverContent.sideOffset","number","8","Gap from the trigger in px."]],"pro":false,"source":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Popover = PopoverPrimitive.Root;\nexport const PopoverTrigger = PopoverPrimitive.Trigger;\nexport const PopoverClose = PopoverPrimitive.Close;\n\nexport function PopoverContent({ className, align = \"center\", sideOffset = 8, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align={align}\n sideOffset={sideOffset}\n className={cn(\"vx-pop z-[100] w-72 vx-glass rounded-card border border-pop-border bg-pop p-4 text-ink shadow-pop outline-none\", className)}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n}\n"},{"slug":"tooltip","name":"Tooltip","category":"Overlays","description":"A short label on hover or focus, with an optional key hint. Wrap your app in TooltipProvider.","deps":["radix-ui"],"uses":[],"usage":"<Tooltip content=\"Bold\" shortcut=\"⌘B\">\n <Button size=\"icon\">B</Button>\n</Tooltip>","props":[["content","ReactNode","required","The label."],["shortcut","ReactNode","","Key hint after the label."],["side","\"top\" | \"right\" | \"bottom\" | \"left\"","\"top\"","Preferred side."]],"pro":false,"source":"import * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const TooltipProvider = TooltipPrimitive.Provider;\n\nexport interface TooltipProps {\n content: React.ReactNode;\n children: React.ReactNode;\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n shortcut?: React.ReactNode;\n className?: string;\n}\n\nexport function Tooltip({ content, children, side = \"top\", shortcut, className }: TooltipProps) {\n return (\n <TooltipPrimitive.Root>\n <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n side={side}\n sideOffset={6}\n className={cn(\n \"vx-pop z-[600] flex items-center gap-2 rounded-input bg-graphite px-2 py-1 text-xs font-medium text-graphite-ink shadow-pop\",\n className,\n )}\n >\n {content}\n {shortcut && <span className=\"font-mono text-graphite-muted\">{shortcut}</span>}\n <TooltipPrimitive.Arrow className=\"fill-graphite\" />\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n </TooltipPrimitive.Root>\n );\n}\n"},{"slug":"toast","name":"Toast","category":"Feedback","description":"Brief, non-blocking messages. Mount Toaster once and call toast() from anywhere.","deps":["lucide-react"],"uses":[],"usage":"toast(\"Saved\")\ntoast.success(\"Deployed\", \"acme-web is live.\")\ntoast({ title: \"Archived\", action: { label: \"Undo\", onClick } })","props":[["title","string","required","Main line."],["description","string","","Second line."],["tone","\"default\" | \"success\" | \"danger\"","\"default\"","Icon and colour."],["action","{ label: string; onClick: () => void }","","One inline action."],["duration","number","4000","Milliseconds before it leaves."]],"pro":false,"source":"import * as React from \"react\";\nimport { CircleAlert, CircleCheck, Info, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Tone = \"default\" | \"success\" | \"danger\";\nexport interface ToastItem {\n id: number;\n title: string;\n description?: string;\n tone?: Tone;\n action?: { label: string; onClick: () => void };\n duration?: number;\n}\n\nlet items: ToastItem[] = [];\nlet nextId = 1;\nconst listeners = new Set<() => void>();\nconst emit = () => listeners.forEach((l) => l());\n\nexport function dismissToast(id: number) {\n items = items.filter((t) => t.id !== id);\n emit();\n}\n\n/** Show a toast from anywhere: toast(\"Saved\") or toast({ title, description, tone, action }). */\nexport function toast(input: string | Omit<ToastItem, \"id\">) {\n const t: ToastItem = typeof input === \"string\" ? { id: nextId++, title: input } : { id: nextId++, ...input };\n items = [...items, t].slice(-4);\n emit();\n window.setTimeout(() => dismissToast(t.id), t.duration ?? 4000);\n return t.id;\n}\ntoast.success = (title: string, description?: string) => toast({ title, description, tone: \"success\" });\ntoast.error = (title: string, description?: string) => toast({ title, description, tone: \"danger\" });\n\nconst icons = { default: Info, success: CircleCheck, danger: CircleAlert };\nconst iconTone = { default: \"text-accent\", success: \"text-success\", danger: \"text-danger\" };\n\n/** Mount once near the root of your app. */\nexport function Toaster({ className }: { className?: string }) {\n const list = React.useSyncExternalStore(\n (cb) => {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n () => items,\n () => items,\n );\n return (\n <ol\n aria-live=\"polite\"\n className={cn(\"pointer-events-none fixed bottom-4 right-4 z-[500] flex w-[min(360px,calc(100vw-2rem))] flex-col gap-2\", className)}\n >\n {list.map((t) => {\n const Icon = icons[t.tone ?? \"default\"];\n return (\n <li\n key={t.id}\n className=\"vx-toast pointer-events-auto flex items-start gap-3 rounded-card border border-pop-border bg-pop p-3.5 text-sm text-ink shadow-pop vx-glass\"\n >\n <Icon className={cn(\"mt-0.5 size-4 shrink-0\", iconTone[t.tone ?? \"default\"])} />\n <div className=\"grid flex-1 gap-0.5\">\n <p className=\"font-medium\">{t.title}</p>\n {t.description && <p className=\"text-muted\">{t.description}</p>}\n </div>\n {t.action && (\n <button\n onClick={() => {\n t.action!.onClick();\n dismissToast(t.id);\n }}\n className=\"rounded-sm px-1.5 py-0.5 text-sm font-medium text-accent hover:bg-accent-wash focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {t.action.label}\n </button>\n )}\n <button\n onClick={() => dismissToast(t.id)}\n aria-label=\"Dismiss\"\n className=\"-m-1 grid size-6 place-items-center rounded-sm text-muted hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </button>\n </li>\n );\n })}\n </ol>\n );\n}\n"},{"slug":"alert","name":"Alert","category":"Feedback","description":"An inline message about the state of a page or section.","deps":["class-variance-authority","lucide-react"],"uses":[],"usage":"<Alert variant=\"warning\" title=\"Your card expires soon\" />","props":[["variant","\"info\" | \"success\" | \"warning\" | \"danger\"","\"info\"","Tone and icon."],["title","ReactNode","","Bold first line."],["action","ReactNode","","Buttons under the message."],["onDismiss","() => void","","Shows a close button when set."]],"pro":false,"source":"import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CircleAlert, CircleCheck, Info, TriangleAlert, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nconst alertVariants = cva(\"relative flex gap-3 rounded-card border p-3.5 text-sm\", {\n variants: {\n variant: {\n info: \"border-rule bg-paper-2 text-ink [&>svg]:text-accent\",\n success: \"border-transparent bg-success-wash text-success-wash-ink\",\n warning: \"border-transparent bg-warning-wash text-warning-wash-ink\",\n danger: \"border-transparent bg-danger-wash text-danger-wash-ink\",\n },\n },\n defaultVariants: { variant: \"info\" },\n});\n\nconst icons = { info: Info, success: CircleCheck, warning: TriangleAlert, danger: CircleAlert };\n\nexport interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">, VariantProps<typeof alertVariants> {\n title?: React.ReactNode;\n action?: React.ReactNode;\n onDismiss?: () => void;\n}\n\nexport function Alert({ className, variant = \"info\", title, action, onDismiss, children, ...props }: AlertProps) {\n const Icon = icons[variant ?? \"info\"];\n return (\n <div role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props}>\n <Icon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"grid flex-1 gap-1\">\n {title && <p className=\"font-medium\">{title}</p>}\n {children && <div className=\"opacity-90\">{children}</div>}\n {action && <div className=\"mt-1.5 flex gap-2\">{action}</div>}\n </div>\n {onDismiss && (\n <button\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className=\"-m-1 grid size-6 place-items-center rounded-sm opacity-70 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n}\n"},{"slug":"progress","name":"Progress","category":"Feedback","description":"Linear progress for a task, and Meter, a cell meter for quotas.","deps":["radix-ui"],"uses":[],"usage":"<Progress value={64} />\n<Meter value={72} cells={24} />","props":[["value","number","0","Percent complete."],["tone","\"accent\" | \"success\" | \"danger\"","\"accent\"","Fill colour."],["Meter.cells","number","24","Number of cells."]],"pro":false,"source":"import * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Progress({\n value = 0,\n className,\n tone = \"accent\",\n ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root> & { tone?: \"accent\" | \"success\" | \"danger\" }) {\n const fill = { accent: \"bg-accent [background-image:var(--control-on-image)]\", success: \"bg-success\", danger: \"bg-danger\" }[tone];\n const v = Math.min(100, Math.max(0, value ?? 0));\n return (\n <ProgressPrimitive.Root\n value={v}\n className={cn(\"relative h-2 w-full overflow-hidden rounded-full bg-[color:var(--track-off)] shadow-[inset_0_1px_1px_oklch(0%_0_0/0.06)]\", className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n className={cn(\"h-full w-full rounded-full transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)]\", fill)}\n style={{ transform: `translateX(-${100 - v}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\n/** A meter of discrete cells, useful for quotas and usage. */\nexport function Meter({ value, cells = 24, className, label }: { value: number; cells?: number; className?: string; label?: string }) {\n const on = Math.round((value / 100) * cells);\n return (\n <div role=\"meter\" aria-valuenow={value} aria-valuemin={0} aria-valuemax={100} aria-label={label} className={cn(\"flex gap-[3px]\", className)}>\n {Array.from({ length: cells }).map((_, i) => (\n <span key={i} className={cn(\"h-4 flex-1 rounded-[2px]\", i < on ? \"bg-accent\" : \"bg-paper-3\")} />\n ))}\n </div>\n );\n}\n"},{"slug":"skeleton","name":"Skeleton","category":"Feedback","description":"A shimmering placeholder that holds the shape of content while it loads.","deps":[],"uses":[],"usage":"<Skeleton className=\"h-4 w-48\" />","props":[["className","string","","Set width, height and radius."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div aria-hidden className={cn(\"vx-shimmer rounded-input bg-paper-3\", className)} {...props} />;\n}\n"},{"slug":"spinner","name":"Spinner","category":"Feedback","description":"An indeterminate loading indicator that inherits the text colour.","deps":[],"uses":[],"usage":"<Spinner className=\"size-4\" />","props":[["label","string","\"Loading\"","Accessible name."],["className","string","","Size and colour."]],"pro":false,"source":"import { cn } from \"@/lib/cn\";\n\nexport function Spinner({ className, label = \"Loading\" }: { className?: string; label?: string }) {\n return (\n <svg\n role=\"status\"\n aria-label={label}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n className={cn(\"vx-spin size-5 shrink-0\", className)}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"3\" />\n <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n"},{"slug":"card","name":"Card","category":"Display","description":"A hairline-bordered surface with header, content and a footer strip.","deps":[],"uses":[],"usage":"<Card>\n <CardHeader><CardTitle>Title</CardTitle></CardHeader>\n <CardContent>…</CardContent>\n</Card>","props":[["className","string","","Every part accepts className and native div props."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"vx-glass rounded-panel border border-card-border bg-card text-ink shadow-card\", className)}\n {...props}\n />\n );\n}\nexport function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1 p-5 pb-0\", className)} {...props} />;\n}\nexport function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {\n return <h3 className={cn(\"text-base font-medium tracking-tight\", className)} {...props} />;\n}\nexport function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {\n return <p className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\nexport function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"p-5\", className)} {...props} />;\n}\nexport function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"flex items-center gap-2 border-t border-rule px-5 py-3\", className)}\n {...props}\n />\n );\n}\n\n/** A raised inner panel, for the shell-and-panel look of the soft style. */\nexport function CardInset({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"rounded-card bg-inset p-4\", className)} {...props} />;\n}\n"},{"slug":"avatar","name":"Avatar","category":"Display","description":"An image with initials as fallback, a presence dot, and an overlapping group.","deps":["radix-ui"],"uses":[],"usage":"<Avatar src=\"/ana.jpg\" fallback=\"AK\" status=\"online\" />","props":[["fallback","string","required","Initials when there is no image."],["size","\"xs\" | \"sm\" | \"md\" | \"lg\"","\"md\"","Diameter."],["status","\"online\" | \"away\" | \"offline\"","","Presence dot."],["AvatarGroup.max","number","","Collapse the rest into a count."]],"pro":false,"source":"import * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nconst sizes = { xs: \"size-6 text-[10px]\", sm: \"size-8 text-xs\", md: \"size-10 text-sm\", lg: \"size-12 text-base\" };\n\nexport interface AvatarProps extends React.ComponentProps<typeof AvatarPrimitive.Root> {\n src?: string;\n alt?: string;\n /** Initials shown while the image loads or when it fails. */\n fallback: string;\n size?: keyof typeof sizes;\n status?: \"online\" | \"away\" | \"offline\";\n}\n\nconst statusTone = { online: \"bg-success\", away: \"bg-warning\", offline: \"bg-rule-2\" };\n\nexport function Avatar({ src, alt, fallback, size = \"md\", status, className, ...props }: AvatarProps) {\n return (\n <span className=\"relative inline-flex shrink-0\">\n <AvatarPrimitive.Root\n className={cn(\n \"relative inline-flex shrink-0 overflow-hidden rounded-full border border-rule bg-paper-3 ring-2 ring-paper\",\n sizes[size],\n className,\n )}\n {...props}\n >\n {src && <AvatarPrimitive.Image src={src} alt={alt ?? fallback} className=\"size-full object-cover\" />}\n <AvatarPrimitive.Fallback className=\"flex size-full items-center justify-center font-medium text-ink-2\">\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n {status && (\n <span\n role=\"img\"\n aria-label={status}\n className={cn(\"absolute bottom-0 right-0 size-2.5 rounded-full ring-2 ring-paper\", statusTone[status])}\n />\n )}\n </span>\n );\n}\n\n/** Overlapping stack of avatars with an optional overflow count. */\nexport function AvatarGroup({ children, max, className }: { children: React.ReactNode; max?: number; className?: string }) {\n const items = React.Children.toArray(children);\n const shown = max ? items.slice(0, max) : items;\n const extra = items.length - shown.length;\n return (\n <div className={cn(\"flex items-center -space-x-2\", className)}>\n {shown}\n {extra > 0 && (\n <span className=\"relative inline-flex size-8 items-center justify-center rounded-full border border-rule bg-paper-2 text-xs font-medium text-ink-2 ring-2 ring-paper\">\n +{extra}\n </span>\n )}\n </div>\n );\n}\n"},{"slug":"badge","name":"Badge","category":"Display","description":"A compact label for status, version or category.","deps":["class-variance-authority"],"uses":[],"usage":"<Badge variant=\"success\">Deployed</Badge>","props":[["variant","\"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"solid\" | \"outline\"","\"neutral\"","Tone."]],"pro":false,"source":"import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const badgeVariants = cva(\n \"inline-flex h-[18px] items-center gap-1 whitespace-nowrap rounded-[calc(4px*var(--radius-factor))] border px-1 text-xs font-semibold leading-4 [&_svg]:size-3\",\n {\n variants: {\n variant: {\n neutral: \"border-transparent bg-paper-3 text-muted\",\n accent: \"border-transparent bg-accent-wash text-accent-wash-ink\",\n success: \"border-transparent bg-success-wash text-success-wash-ink\",\n warning: \"border-transparent bg-warning-wash text-warning-wash-ink\",\n danger: \"border-transparent bg-danger-wash text-danger-wash-ink\",\n solid: \"border-transparent bg-accent text-accent-ink\",\n outline: \"border-rule-2 text-ink-2\",\n },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {}\n\nexport function Badge({ className, variant, ...props }: BadgeProps) {\n return <span className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n"},{"slug":"kbd","name":"Kbd","category":"Display","description":"A keyboard key, for shortcuts in menus, tooltips and docs.","deps":[],"uses":[],"usage":"<Kbd>⌘</Kbd><Kbd>K</Kbd>","props":[["children","ReactNode","","The key label."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Kbd({ className, ...props }: React.HTMLAttributes<HTMLElement>) {\n return (\n <kbd\n className={cn(\n \"inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-rule bg-key-face px-1 font-mono text-[11px] font-medium text-ink-2 shadow-[0_1px_0_0_var(--key-edge)]\",\n className,\n )}\n {...props}\n />\n );\n}\n"},{"slug":"separator","name":"Separator","category":"Display","description":"A hairline between groups of content, horizontal or vertical.","deps":["radix-ui"],"uses":[],"usage":"<Separator />\n<Separator orientation=\"vertical\" />","props":[["orientation","\"horizontal\" | \"vertical\"","\"horizontal\"","Direction."],["decorative","boolean","true","Hide from assistive tech."]],"pro":false,"source":"import * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n return (\n <SeparatorPrimitive.Root\n decorative={decorative}\n orientation={orientation}\n className={cn(\"shrink-0 bg-rule\", orientation === \"horizontal\" ? \"h-px w-full\" : \"h-full w-px\", className)}\n {...props}\n />\n );\n}\n"},{"slug":"tabs","name":"Tabs","category":"Navigation","description":"Switch between views in place. Two variants: a tray with a sliding pill, and a line.","deps":["radix-ui"],"uses":["segmented"],"usage":"<Tabs defaultValue=\"a\">\n <TabsList variant=\"line\">\n <TabsTrigger value=\"a\">Overview</TabsTrigger>\n </TabsList>\n <TabsContent value=\"a\">…</TabsContent>\n</Tabs>","props":[["value / defaultValue","string","","Active tab."],["TabsList.variant","\"tray\" | \"line\"","\"tray\"","Visual style."]],"pro":false,"source":"import * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { pillClass, useSlidingPill } from \"./segmented\";\n\ntype Variant = \"tray\" | \"line\";\nconst VariantContext = React.createContext<Variant>(\"tray\");\n\nexport function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {\n // minmax(0, 1fr) keeps wide panels from stretching the page on small screens.\n return <TabsPrimitive.Root className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-4\", className)} {...props} />;\n}\n\nexport function TabsList({\n className,\n variant = \"tray\",\n children,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> & { variant?: Variant }) {\n const list = React.useRef<HTMLDivElement>(null);\n // Re-measure when the active tab changes; Radix flips data-state on the triggers.\n const [active, setActive] = React.useState<string | null>(null);\n React.useEffect(() => {\n const l = list.current;\n if (!l) return;\n const read = () => setActive(l.querySelector('[data-state=\"active\"]')?.id ?? null);\n read();\n const mo = new MutationObserver(read);\n mo.observe(l, { subtree: true, attributes: true, attributeFilter: [\"data-state\"] });\n return () => mo.disconnect();\n }, []);\n const pill = useSlidingPill(list, '[data-state=\"active\"]', active);\n return (\n <VariantContext.Provider value={variant}>\n <TabsPrimitive.List\n ref={list}\n className={cn(\n \"relative inline-flex w-fit max-w-full items-center overflow-x-auto [scrollbar-width:none]\",\n variant === \"tray\"\n ? \"vx-glass gap-0.5 rounded-input border border-tray-border bg-tray p-1\"\n : \"gap-5 border-b border-rule\",\n className,\n )}\n {...props}\n >\n {variant === \"tray\" && <span ref={pill} aria-hidden className={pillClass} />}\n {children}\n </TabsPrimitive.List>\n </VariantContext.Provider>\n );\n}\n\nexport function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n const variant = React.useContext(VariantContext);\n return (\n <TabsPrimitive.Trigger\n className={cn(\n \"inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium text-muted\",\n \"transition-[color,background-color,box-shadow] duration-[var(--dur-micro)] hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:opacity-50 [&_svg]:size-4\",\n variant === \"tray\"\n ? \"relative z-10 h-7 rounded-[var(--pill-radius)] px-3 data-[state=active]:text-ink\"\n : \"-mb-px h-9 border-b-2 border-transparent px-0.5 data-[state=active]:border-accent data-[state=active]:text-ink\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return <TabsPrimitive.Content className={cn(\"outline-none\", className)} {...props} />;\n}\n"},{"slug":"accordion","name":"Accordion","category":"Navigation","description":"Stacked sections that expand one or many at a time.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Accordion type=\"single\" collapsible>\n <AccordionItem value=\"a\">\n <AccordionTrigger>Question</AccordionTrigger>\n <AccordionContent>Answer</AccordionContent>\n </AccordionItem>\n</Accordion>","props":[["type","\"single\" | \"multiple\"","required","How many can be open."],["collapsible","boolean","false","Allow closing the open item in single mode."]],"pro":false,"source":"import * as React from \"react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\nimport { Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Accordion = AccordionPrimitive.Root;\n\nexport function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n return <AccordionPrimitive.Item className={cn(\"border-b border-rule last:border-b-0\", className)} {...props} />;\n}\n\nexport function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n className={cn(\n \"group flex flex-1 items-center justify-between gap-4 py-4 text-left text-sm font-medium text-ink\",\n \"hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n className,\n )}\n {...props}\n >\n {children}\n <Plus className=\"size-4 shrink-0 text-muted transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)] group-data-[state=open]:rotate-45\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n}\n\nexport function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n return (\n <AccordionPrimitive.Content className=\"vx-reveal overflow-hidden text-sm text-muted\" {...props}>\n <div className={cn(\"pb-4 pr-8\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n );\n}\n"},{"slug":"combobox","name":"Combobox","category":"Forms","description":"A select with a search field, for option lists too long to scan.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Combobox options={[{ value: \"vite\", label: \"Vite\" }]} value={v} onValueChange={setV} />","props":[["options","{ value: string; label: string; description?: string }[]","required","Choices."],["value / onValueChange","string / (v) => void","","Controlled value."],["placeholder","string","\"Select…\"","Shown with nothing chosen."],["searchPlaceholder","string","\"Search…\"","Search field hint."]],"pro":false,"source":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\nimport { Check, ChevronsUpDown, Search } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuCheck, menuItem, menuLabel, menuPanel } from \"./menu-styles\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n description?: string;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value?: string;\n onValueChange?: (value: string) => void;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** A select with a search field, for long option lists. */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Select…\",\n searchPlaceholder = \"Search…\",\n emptyText = \"Nothing found.\",\n className,\n ...rest\n}: ComboboxProps) {\n const [open, setOpen] = React.useState(false);\n const [inner, setInner] = React.useState(value);\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const current = value ?? inner;\n const selected = options.find((o) => o.value === current);\n const list = options.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()));\n\n const pick = (o: ComboboxOption) => {\n setInner(o.value);\n onValueChange?.(o.value);\n setOpen(false);\n setQ(\"\");\n };\n\n return (\n <PopoverPrimitive.Root open={open} onOpenChange={(o) => { setOpen(o); if (o) setActive(Math.max(0, list.indexOf(selected!))); }}>\n <PopoverPrimitive.Trigger\n role=\"combobox\"\n aria-expanded={open}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"flex h-9 w-full items-center justify-between gap-2 rounded-input border border-field-border bg-field px-3 text-sm shadow-field vx-glass\",\n \"hover:field-hover focus-visible:field-focus focus-visible:outline-none\",\n className,\n )}\n >\n <span className={cn(\"truncate\", !selected && \"text-muted\")}>{selected?.label ?? placeholder}</span>\n <ChevronsUpDown className=\"size-4 shrink-0 text-muted\" />\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n sideOffset={6}\n className={cn(menuPanel, \"w-[var(--radix-popover-trigger-width)] min-w-56 p-0\")}\n >\n <div className=\"flex items-center gap-2 border-b border-rule px-3\">\n <Search className=\"size-4 text-muted\" />\n <input\n autoFocus\n value={q}\n onChange={(e) => { setQ(e.target.value); setActive(0); }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") { e.preventDefault(); setActive((a) => Math.min(a + 1, list.length - 1)); }\n if (e.key === \"ArrowUp\") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }\n if (e.key === \"Enter\" && list[active]) { e.preventDefault(); pick(list[active]); }\n }}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n className=\"h-10 flex-1 bg-transparent text-sm outline-none placeholder:text-muted\"\n />\n </div>\n <ul role=\"listbox\" className=\"max-h-64 overflow-y-auto p-1.5\">\n {list.length === 0 && <li className=\"px-3 py-5 text-center text-sm text-muted\">{emptyText}</li>}\n {list.map((o, i) => (\n <li\n key={o.value}\n role=\"option\"\n aria-selected={o.value === current}\n onMouseMove={() => setActive(i)}\n onClick={() => pick(o)}\n data-active={i === active}\n className={menuItem}\n >\n <span className=\"grid flex-1\">\n <span>{o.label}</span>\n {o.description && <span className=\"text-xs text-muted\">{o.description}</span>}\n </span>\n {o.value === current && <Check className={menuCheck} />}\n </li>\n ))}\n </ul>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n );\n}\n"},{"slug":"input-otp","name":"Input OTP","category":"Forms","description":"One-time code entry with auto-advance, paste support and grouped cells.","deps":[],"uses":[],"usage":"<InputOTP length={6} groupSize={3} onComplete={verify} />","props":[["length","number","6","Number of cells."],["groupSize","number","","Insert a gap after this many cells."],["value / onChange","string / (v) => void","","Controlled value."],["onComplete","(code: string) => void","","Called when every cell is filled."],["invalid","boolean","false","Error style."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface InputOTPProps {\n length?: number;\n value?: string;\n onChange?: (value: string) => void;\n /** Called once every cell is filled. */\n onComplete?: (value: string) => void;\n /** Insert a gap after this many cells, e.g. 3 for 123-456. */\n groupSize?: number;\n disabled?: boolean;\n invalid?: boolean;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** One-time code entry: digits only, auto-advance, paste the full code. */\nexport function InputOTP({ length = 6, value, onChange, onComplete, groupSize, disabled, invalid, className, ...rest }: InputOTPProps) {\n const [inner, setInner] = React.useState(\"\");\n const [focused, setFocused] = React.useState(false);\n const code = (value ?? inner).slice(0, length);\n const ref = React.useRef<HTMLInputElement>(null);\n\n const set = (next: string) => {\n const clean = next.replace(/\\D/g, \"\").slice(0, length);\n setInner(clean);\n onChange?.(clean);\n if (clean.length === length) onComplete?.(clean);\n };\n\n return (\n <div className={cn(\"relative inline-flex items-center gap-2\", className)} onClick={() => ref.current?.focus()}>\n <input\n ref={ref}\n value={code}\n onChange={(e) => set(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n maxLength={length}\n disabled={disabled}\n aria-label={rest[\"aria-label\"] ?? \"One-time code\"}\n aria-invalid={invalid || undefined}\n className=\"absolute inset-0 cursor-text opacity-0 disabled:cursor-not-allowed\"\n />\n {Array.from({ length }).map((_, i) => {\n const active = focused && (i === code.length || (i === length - 1 && code.length === length));\n return (\n <React.Fragment key={i}>\n {groupSize && i > 0 && i % groupSize === 0 && <span aria-hidden className=\"h-px w-3 bg-rule-2\" />}\n <span\n aria-hidden\n className={cn(\n \"vx-glass grid size-11 place-items-center rounded-input border bg-field font-mono text-lg text-ink shadow-field\",\n \"transition-[border-color,box-shadow] duration-[var(--dur-micro)]\",\n invalid ? \"border-danger\" : active ? \"field-focus\" : \"border-field-border\",\n disabled && \"bg-paper-2 opacity-60\",\n )}\n >\n {code[i] ?? (active ? <span className=\"vx-caret h-5 w-px bg-ink\" /> : \"\")}\n </span>\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n"},{"slug":"sheet","name":"Sheet","category":"Overlays","description":"A panel that slides in from an edge, for forms and details that keep page context.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Sheet>\n <SheetTrigger asChild><Button>Open</Button></SheetTrigger>\n <SheetContent side=\"right\">…</SheetContent>\n</Sheet>","props":[["SheetContent.side","\"right\" | \"left\" | \"bottom\"","\"right\"","Edge it slides from."],["open / onOpenChange","boolean / (open) => void","","Controlled state."]],"pro":false,"source":"import * as React from \"react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Sheet = SheetPrimitive.Root;\nexport const SheetTrigger = SheetPrimitive.Trigger;\nexport const SheetClose = SheetPrimitive.Close;\n\nconst sides = {\n right: \"vx-sheet-right inset-y-0 right-0 h-full w-[min(420px,100%)] border-l\",\n left: \"vx-sheet-left inset-y-0 left-0 h-full w-[min(420px,100%)] border-r\",\n bottom: \"vx-sheet-bottom inset-x-0 bottom-0 max-h-[85svh] rounded-t-panel border-t\",\n};\n\nexport function SheetContent({\n side = \"right\",\n className,\n children,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: keyof typeof sides }) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay\" />\n <SheetPrimitive.Content\n className={cn(\"vx-glass fixed z-[400] flex flex-col gap-4 overflow-y-auto border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\", sides[side], className)}\n {...props}\n >\n {children}\n <SheetPrimitive.Close\n aria-label=\"Close\"\n className=\"absolute right-4 top-4 grid size-7 place-items-center rounded-input text-muted hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPrimitive.Portal>\n );\n}\n\nexport function SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5 pr-8\", className)} {...props} />;\n}\nexport function SheetFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"mt-auto flex flex-col-reverse gap-2 border-t border-rule pt-4 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return <SheetPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return <SheetPrimitive.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n"},{"slug":"alert-dialog","name":"Alert dialog","category":"Overlays","description":"A modal that interrupts to confirm an important or irreversible action.","deps":["radix-ui"],"uses":["button"],"usage":"<AlertDialog>\n <AlertDialogTrigger asChild><Button>Delete</Button></AlertDialogTrigger>\n <AlertDialogContent>…<AlertDialogAction destructive>Delete</AlertDialogAction></AlertDialogContent>\n</AlertDialog>","props":[["AlertDialogAction.destructive","boolean","false","Danger style for the confirm button."],["open / onOpenChange","boolean / (open) => void","","Controlled state."]],"pro":false,"source":"import * as React from \"react\";\nimport { AlertDialog as AD } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { buttonVariants } from \"./button\";\n\nexport const AlertDialog = AD.Root;\nexport const AlertDialogTrigger = AD.Trigger;\n\nexport function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof AD.Content>) {\n return (\n <AD.Portal>\n <AD.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <AD.Content\n className={cn(\n \"vx-dialog fixed left-1/2 top-1/2 z-[400] grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4\",\n \"vx-glass rounded-panel border border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\",\n className,\n )}\n {...props}\n />\n </AD.Portal>\n );\n}\n\nexport function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5\", className)} {...props} />;\n}\nexport function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AD.Title>) {\n return <AD.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AD.Description>) {\n return <AD.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n/** The confirming button. Pass destructive for irreversible actions. */\nexport function AlertDialogAction({ className, destructive, ...props }: React.ComponentProps<typeof AD.Action> & { destructive?: boolean }) {\n return <AD.Action className={cn(buttonVariants({ variant: destructive ? \"danger\" : \"primary\" }), className)} {...props} />;\n}\nexport function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof AD.Cancel>) {\n return <AD.Cancel className={cn(buttonVariants({ variant: \"secondary\" }), className)} {...props} />;\n}\n"},{"slug":"table","name":"Table","category":"Display","description":"Ruled data table parts with selectable rows and a sortable header button.","deps":["lucide-react"],"uses":[],"usage":"<Table>\n <TableHeader><TableRow><TableHead>Name</TableHead></TableRow></TableHeader>\n <TableBody><TableRow><TableCell>Ana</TableCell></TableRow></TableBody>\n</Table>","props":[["TableRow data-state","\"selected\"","","Highlights a selected row."],["TableSortButton.direction","\"asc\" | \"desc\" | false","","Current sort."],["...props","native table element props","","Every part forwards them."]],"pro":false,"source":"import * as React from \"react\";\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Table({ className, ...props }: React.ComponentProps<\"table\">) {\n return (\n <div className=\"vx-glass w-full overflow-x-auto rounded-card border border-card-border bg-card\">\n <table className={cn(\"w-full caption-bottom text-left text-sm\", className)} {...props} />\n </div>\n );\n}\nexport function TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return <thead className={cn(\"border-b border-rule bg-paper-2\", className)} {...props} />;\n}\nexport function TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return <tbody className={cn(\"divide-y divide-rule\", className)} {...props} />;\n}\nexport function TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return <tr className={cn(\"transition-[background-color] duration-[var(--dur-micro)] hover:bg-paper-2/60 data-[state=selected]:bg-accent-wash/50\", className)} {...props} />;\n}\nexport function TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return <th className={cn(\"h-11 whitespace-nowrap px-3 font-medium text-muted\", className)} {...props} />;\n}\nexport function TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n return <td className={cn(\"px-3 py-2.5 align-middle font-medium text-ink\", className)} {...props} />;\n}\nexport function TableCaption({ className, ...props }: React.ComponentProps<\"caption\">) {\n return <caption className={cn(\"border-t border-rule px-4 py-3 text-left text-sm text-muted\", className)} {...props} />;\n}\n\n/** A header button that cycles sort direction. */\nexport function TableSortButton({\n direction,\n onClick,\n children,\n}: {\n direction: \"asc\" | \"desc\" | false;\n onClick: () => void;\n children: React.ReactNode;\n}) {\n const Icon = direction === \"asc\" ? ArrowUp : direction === \"desc\" ? ArrowDown : ChevronsUpDown;\n return (\n <button\n onClick={onClick}\n aria-sort={direction ? (direction === \"asc\" ? \"ascending\" : \"descending\") : undefined}\n className=\"-mx-1.5 inline-flex items-center gap-1 rounded-sm px-1.5 py-1 hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {children}\n <Icon className={cn(\"size-3.5\", direction ? \"text-ink\" : \"text-muted/70\")} />\n </button>\n );\n}\n"},{"slug":"breadcrumb","name":"Breadcrumb","category":"Navigation","description":"Where the current page sits in the hierarchy, with links back up.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem><BreadcrumbLink href=\"/\">Home</BreadcrumbLink></BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem><BreadcrumbPage>Billing</BreadcrumbPage></BreadcrumbItem>\n </BreadcrumbList>\n</Breadcrumb>","props":[["BreadcrumbLink.asChild","boolean","false","Render your router link."],["BreadcrumbSeparator.children","ReactNode","chevron","Custom separator."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Breadcrumb(props: React.ComponentProps<\"nav\">) {\n return <nav aria-label=\"Breadcrumb\" {...props} />;\n}\nexport function BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return <ol className={cn(\"flex flex-wrap items-center gap-1.5 text-sm text-muted\", className)} {...props} />;\n}\nexport function BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return <li className={cn(\"inline-flex items-center gap-1.5\", className)} {...props} />;\n}\nexport function BreadcrumbLink({ asChild, className, ...props }: React.ComponentProps<\"a\"> & { asChild?: boolean }) {\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <Comp\n className={cn(\"rounded-sm transition-[color] duration-[var(--dur-micro)] hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\", className)}\n {...props}\n />\n );\n}\nexport function BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span aria-current=\"page\" className={cn(\"font-medium text-ink\", className)} {...props} />;\n}\nexport function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li role=\"presentation\" aria-hidden className={cn(\"text-rule-2 [&>svg]:size-3.5\", className)} {...props}>\n {children ?? <ChevronRight />}\n </li>\n );\n}\nexport function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span role=\"presentation\" className={cn(\"flex size-6 items-center justify-center\", className)} {...props}>\n <MoreHorizontal className=\"size-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n );\n}\n"},{"slug":"pagination","name":"Pagination","category":"Navigation","description":"Move through pages of results, with gaps for long ranges.","deps":["lucide-react"],"uses":[],"usage":"<Pagination page={page} pageCount={20} onPageChange={setPage} />","props":[["page","number","required","Current page, from 1."],["pageCount","number","required","Total pages."],["onPageChange","(page: number) => void","required","Called on navigation."],["siblings","number","1","Pages shown either side of the current one."]],"pro":false,"source":"import { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nfunction range(page: number, count: number, siblings: number): (number | \"gap\")[] {\n const total = siblings * 2 + 5;\n if (count <= total) return Array.from({ length: count }, (_, i) => i + 1);\n const left = Math.max(page - siblings, 2);\n const right = Math.min(page + siblings, count - 1);\n const out: (number | \"gap\")[] = [1];\n if (left > 2) out.push(\"gap\");\n for (let i = left; i <= right; i++) out.push(i);\n if (right < count - 1) out.push(\"gap\");\n out.push(count);\n return out;\n}\n\nexport interface PaginationProps {\n page: number;\n pageCount: number;\n onPageChange: (page: number) => void;\n /** Pages shown either side of the current one. */\n siblings?: number;\n /** Show the \"Previous\" and \"Next\" words. Turn off in narrow places; the arrows stay, with labels for screen readers. */\n labels?: boolean;\n className?: string;\n}\n\nconst cell =\n \"inline-flex h-8 min-w-8 items-center justify-center gap-1 rounded-input px-2 text-sm font-medium transition-[background-color,color] duration-[var(--dur-micro)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:pointer-events-none disabled:opacity-40\";\n\nexport function Pagination({ page, pageCount, onPageChange, siblings = 1, labels = true, className }: PaginationProps) {\n return (\n <nav aria-label=\"Pagination\" className={cn(\"flex max-w-full items-center gap-1\", className)}>\n <button className={cn(cell, \"text-ink-2 hover:bg-paper-3\")} disabled={page <= 1} onClick={() => onPageChange(page - 1)} aria-label={labels ? undefined : \"Previous page\"}>\n <ChevronLeft className=\"size-4\" /> {labels && <span className=\"hidden sm:inline\">Previous</span>}\n </button>\n {range(page, pageCount, siblings).map((p, i) =>\n p === \"gap\" ? (\n <span key={`g${i}`} className=\"px-1 text-muted\">…</span>\n ) : (\n <button\n key={p}\n aria-current={p === page ? \"page\" : undefined}\n onClick={() => onPageChange(p)}\n className={cn(\n cell,\n \"tabular-nums\",\n p === page ? \"vx-glass border border-[color:var(--key-border)] bg-key-face text-ink shadow-key\" : \"text-ink-2 hover:bg-paper-3\",\n )}\n >\n {p}\n </button>\n ),\n )}\n <button className={cn(cell, \"text-ink-2 hover:bg-paper-3\")} disabled={page >= pageCount} onClick={() => onPageChange(page + 1)} aria-label={labels ? undefined : \"Next page\"}>\n {labels && <span className=\"hidden sm:inline\">Next</span>} <ChevronRight className=\"size-4\" />\n </button>\n </nav>\n );\n}\n"},{"slug":"button-group","name":"Button group","category":"Actions","description":"Buttons fused into one row, for ranges and paired actions.","deps":[],"uses":["button"],"usage":"<ButtonGroup aria-label=\"Range\">\n <Button variant=\"secondary\">Day</Button>\n <Button>Week</Button>\n</ButtonGroup>","props":[["children","Button elements","","The fused buttons. Inner corners square off automatically."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Buttons fused into one row: inner corners square, borders shared.\n * Put Button elements inside; pass aria-label to describe the group.\n */\nexport function ButtonGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n role=\"group\"\n className={cn(\n \"inline-flex items-stretch [&>*]:rounded-none [&>*:first-child]:rounded-l-input [&>*:last-child]:rounded-r-input [&>*:not(:first-child)]:-ml-px [&>*]:shadow-none [&>*:focus-visible]:z-10 [&>*:hover]:z-10\",\n className,\n )}\n {...props}\n />\n );\n}\n"},{"slug":"password-input","name":"Password input","category":"Forms","description":"A password field with a show and hide key and an optional strength meter.","deps":["lucide-react"],"uses":["input"],"usage":"<PasswordInput showStrength placeholder=\"Choose a password\" />","props":[["showStrength","boolean","false","Four-step meter and the rules under the field."],["...props","InputProps","","Everything Input accepts, except type and trailing."]],"pro":false,"source":"import * as React from \"react\";\nimport { Check, Eye, EyeOff, Lock, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Input, type InputProps } from \"./input\";\n\nconst rules = [\n { label: \"At least 8 characters\", test: (v: string) => v.length >= 8 },\n { label: \"An uppercase letter\", test: (v: string) => /[A-Z]/.test(v) },\n { label: \"A number\", test: (v: string) => /\\d/.test(v) },\n { label: \"A symbol\", test: (v: string) => /[^A-Za-z0-9]/.test(v) },\n];\nconst levels = [\"Too weak\", \"Weak\", \"Fair\", \"Good\", \"Strong\"];\nconst tone = [\"bg-paper-3\", \"bg-danger\", \"bg-warning\", \"bg-accent\", \"bg-success\"];\n\nexport interface PasswordInputProps extends Omit<InputProps, \"type\" | \"trailing\"> {\n /** Show a four-step strength meter and the rules under the field. */\n showStrength?: boolean;\n}\n\n/** A password field with a show and hide key, and an optional strength meter. */\nexport const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(\n ({ showStrength, value, defaultValue, onChange, className, ...props }, ref) => {\n const [shown, setShown] = React.useState(false);\n const [inner, setInner] = React.useState(String(defaultValue ?? \"\"));\n const v = value === undefined ? inner : String(value);\n const score = rules.filter((r) => r.test(v)).length;\n return (\n <div className={cn(\"grid gap-2\", className)}>\n <Input\n ref={ref}\n type={shown ? \"text\" : \"password\"}\n value={v}\n onChange={(e) => {\n setInner(e.target.value);\n onChange?.(e);\n }}\n leading={<Lock />}\n trailing={\n <button\n type=\"button\"\n onClick={() => setShown(!shown)}\n aria-label={shown ? \"Hide password\" : \"Show password\"}\n aria-pressed={shown}\n className=\"grid size-7 place-items-center rounded-sm text-muted hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {shown ? <EyeOff className=\"size-4\" /> : <Eye className=\"size-4\" />}\n </button>\n }\n {...props}\n />\n {showStrength && (\n <div className=\"grid gap-2\" aria-live=\"polite\">\n <div className=\"flex items-center gap-2\">\n <div className=\"grid flex-1 grid-cols-4 gap-1\">\n {[1, 2, 3, 4].map((i) => (\n <span key={i} className={cn(\"h-1.5 rounded-full transition-[background-color] duration-[var(--dur-short)]\", score >= i ? tone[score] : \"bg-paper-3\")} />\n ))}\n </div>\n <span className=\"w-16 text-right text-xs text-muted\">{v ? levels[score] : \"\"}</span>\n </div>\n <ul className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs\">\n {rules.map((r) => {\n const ok = r.test(v);\n return (\n <li key={r.label} className={cn(\"flex items-center gap-1.5\", ok ? \"text-success-wash-ink\" : \"text-muted\")}>\n {ok ? <Check className=\"size-3.5\" /> : <X className=\"size-3.5\" />} {r.label}\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n },\n);\nPasswordInput.displayName = \"PasswordInput\";\n"},{"slug":"copy-button","name":"Copy button","category":"Actions","description":"Copies a value and confirms with a check for a moment.","deps":["lucide-react"],"uses":["button"],"usage":"<CopyButton value={apiKey} />\n<CopyButton value={link} label=\"Copy link\" />","props":[["value","string","required","Text to copy."],["label","string","","Text beside the icon; icon-only when omitted."],["onCopied","() => void","","Called after copying."]],"pro":false,"source":"import * as React from \"react\";\nimport { Check, Copy } from \"lucide-react\";\nimport { Button, type ButtonProps } from \"./button\";\n\nexport interface CopyButtonProps extends Omit<ButtonProps, \"onClick\"> {\n /** The text written to the clipboard. */\n value: string;\n /** Text shown beside the icon. Omit for an icon-only button. */\n label?: string;\n onCopied?: () => void;\n}\n\n/** Copies a value and confirms with a check for a moment. */\nexport function CopyButton({ value, label, onCopied, variant = \"secondary\", size, ...props }: CopyButtonProps) {\n const [done, setDone] = React.useState(false);\n return (\n <Button\n variant={variant}\n size={size ?? (label ? \"sm\" : \"icon-sm\")}\n aria-label={label ? undefined : done ? \"Copied\" : \"Copy\"}\n onClick={() =>\n navigator.clipboard?.writeText(value).then(() => {\n setDone(true);\n onCopied?.();\n window.setTimeout(() => setDone(false), 1500);\n })\n }\n {...props}\n >\n {done ? <Check className=\"text-success\" /> : <Copy />}\n {label && (done ? \"Copied\" : label)}\n </Button>\n );\n}\n"}],"templates":[{"slug":"sign-in","name":"Sign in","description":"A split sign-in with a two-step code screen.","pro":false,"uses":["input","checkbox","input-otp","button","card","toast"],"source":"import { useState } from \"react\";\nimport { Check, KeyRound, Lock, Mail } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Input } from \"@/components/ui/input\";\nimport { InputOTP } from \"@/components/ui/input-otp\";\nimport { toast } from \"@/components/ui/toast\";\n\nexport default function SignInTemplate() {\n const [step, setStep] = useState<\"password\" | \"code\">(\"password\");\n const [loading, setLoading] = useState(false);\n return (\n <div className=\"@container min-h-full bg-paper text-ink\">\n <div className=\"grid min-h-full @3xl:grid-cols-2\">\n <div className=\"relative isolate hidden overflow-hidden bg-paper-2 p-10 @3xl:grid @3xl:content-between\">\n <div aria-hidden className=\"vx-graphic absolute inset-0 -z-10\" />\n <p className=\"flex items-center gap-2 font-semibold\">\n <span className=\"grid size-7 place-items-center rounded-input bg-accent text-accent-ink\"><KeyRound className=\"size-4\" /></span>\n Acme\n </p>\n <div className=\"grid gap-4\">\n <h2 className=\"max-w-sm text-4xl leading-[1.05]\">Everything your team ships, in one place.</h2>\n <ul className=\"grid gap-2 text-sm text-ink-2\">\n {[\"Single sign-on for every member\", \"Two-step codes on every new device\", \"Audit log for each sign in\"].map((t) => (\n <li key={t} className=\"flex items-center gap-2\"><Check className=\"size-4 text-accent\" /> {t}</li>\n ))}\n </ul>\n </div>\n </div>\n <div className=\"grid place-items-center p-6 @xl:p-10\">\n <Card className=\"grid w-full max-w-sm gap-5 p-6\">\n {step === \"password\" ? (\n <>\n <div className=\"grid gap-1\">\n <h2 className=\"text-2xl\">Welcome back</h2>\n <p className=\"text-sm text-muted\">Sign in to your workspace.</p>\n </div>\n <form\n className=\"grid gap-3.5\"\n onSubmit={(e) => {\n e.preventDefault();\n setLoading(true);\n window.setTimeout(() => { setLoading(false); setStep(\"code\"); }, 700);\n }}\n >\n <div className=\"grid gap-1.5\"><label htmlFor=\"si-email\" className=\"text-sm font-medium text-ink\">Email</label><Input id=\"si-email\" type=\"email\" required defaultValue=\"ana@example.com\" leading={<Mail />} /></div>\n <div className=\"grid gap-1.5\"><label htmlFor=\"si-pass\" className=\"text-sm font-medium text-ink\">Password</label><Input id=\"si-pass\" type=\"password\" required defaultValue=\"plankui-demo\" leading={<Lock />} /></div>\n <div className=\"flex items-center justify-between text-sm\">\n <label className=\"flex items-center gap-2 text-ink-2\"><Checkbox defaultChecked /> Remember me</label>\n <a href=\"#\" className=\"font-medium text-accent hover:underline\">Forgot password?</a>\n </div>\n <Button type=\"submit\" loading={loading}>Sign in</Button>\n </form>\n <div className=\"flex items-center gap-3 text-xs text-muted\"><span className=\"h-px flex-1 bg-rule\" /> or <span className=\"h-px flex-1 bg-rule\" /></div>\n <Button variant=\"secondary\"><KeyRound /> Continue with SSO</Button>\n </>\n ) : (\n <>\n <div className=\"grid gap-1\">\n <h2 className=\"text-2xl\">Check your phone</h2>\n <p className=\"text-sm text-muted\">Enter the 6-digit code we sent to the number ending 42.</p>\n </div>\n <InputOTP length={6} groupSize={3} aria-label=\"Code\" className=\"justify-self-center [&>span]:size-10\" onComplete={() => toast.success(\"Signed in\", \"Welcome back, Ana.\")} />\n <Button variant=\"ghost\" onClick={() => setStep(\"password\")}>Use a different account</Button>\n </>\n )}\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"}]}
1
+ {"name":"PlankUI","version":"0.1.0","setup":{"deps":"npm i clsx tailwind-merge","devDeps":"npm i -D tailwindcss @tailwindcss/vite","files":{"src/tokens.css":"/* PlankUI design tokens.\n * 1. Knobs: hue, chroma, lightness, radius, fonts.\n * 2. Palette: :root (white) and .dark (black).\n * 3. Themes: [data-theme] tints the palette and sets fonts and the accent.\n * 4. Accent formulas, recomputed wherever a theme or accent is set.\n * 5. Accent presets: [data-accent].\n * 6. Component styles: [data-style] = soft | flat | glass | keycap | brutal. */\n\n/* ───────── 1. Knobs ───────── */\n:root {\n --h: 262;\n --c: 0.22;\n --l: 55%;\n --radius-factor: 1;\n --ff-display: \"Inter\", ui-sans-serif, system-ui, sans-serif;\n --ff-body: \"Inter\", ui-sans-serif, system-ui, sans-serif;\n --ff-code: \"JetBrains Mono\", ui-monospace, monospace;\n --display-weight: 600;\n --display-tracking: -0.045em;\n --ease-out: cubic-bezier(0.16, 1, 0.3, 1);\n --dur-micro: 120ms;\n --dur-short: 220ms;\n}\n\n/* ───────── 2. Palette ───────── */\n:root {\n color-scheme: light;\n --paper: oklch(100% 0 0);\n --paper-2: oklch(97% 0 0);\n --paper-3: oklch(92.2% 0 0);\n --ink: oklch(14.5% 0 0);\n --ink-2: oklch(37% 0 0);\n --muted: oklch(52% 0 0);\n --rule: oklch(92.2% 0 0);\n --rule-2: oklch(87% 0 0);\n --field-edge: oklch(70.8% 0 0);\n\n --glass-edge: oklch(100% 0 0 / 0.65);\n --glass-hi: oklch(100% 0 0 / 0.8);\n --sh-a: 0.06;\n\n --wash-l: 96.5%;\n --wash-c: 0.16;\n --wash-ink-l: 45%;\n --hover-d: -5%;\n --focus-d: 0%;\n\n --graphite: oklch(17% 0 0);\n --graphite-2: oklch(21% 0 0);\n --graphite-ink: oklch(94% 0 0);\n --graphite-muted: oklch(66% 0 0);\n --graphite-rule: oklch(27% 0 0);\n --code-string: oklch(52% 0.13 150);\n --key-edge: oklch(84% 0 0);\n\n --danger: oklch(57.7% 0.215 27);\n --danger-edge: oklch(44% 0.17 27);\n --danger-wash: oklch(96.5% 0.02 17);\n --danger-wash-ink: oklch(50.5% 0.19 27);\n --success: oklch(62.7% 0.17 150);\n --success-wash: oklch(96.5% 0.045 150);\n --success-wash-ink: oklch(44.8% 0.12 151);\n --warning: oklch(76.9% 0.165 70);\n --warning-wash: oklch(97% 0.05 90);\n --warning-wash-ink: oklch(47% 0.12 46);\n --white: oklch(100% 0 0);\n --overlay: oklch(0% 0 0 / 0.4);\n\n /* Chart series: the accent first, then four companions that stay distinct in every theme */\n --chart-2: oklch(70% 0.17 350);\n --chart-3: oklch(66% 0.17 295);\n --chart-4: oklch(76% 0.15 62);\n --chart-5: oklch(70% 0.15 165);\n}\n\n.dark {\n color-scheme: dark;\n --paper: oklch(0% 0 0);\n --paper-2: oklch(15.5% 0 0);\n --paper-3: oklch(21.5% 0 0);\n --ink: oklch(98.5% 0 0);\n --ink-2: oklch(85% 0 0);\n --muted: oklch(66% 0 0);\n --rule: oklch(23% 0 0);\n --rule-2: oklch(31% 0 0);\n --field-edge: oklch(44% 0 0);\n\n --glass-edge: oklch(100% 0 0 / 0.13);\n --glass-hi: oklch(100% 0 0 / 0.1);\n --sh-a: 0.5;\n\n --wash-l: 25%;\n --wash-c: 0.4;\n --wash-ink-l: 84%;\n --hover-d: 6%;\n --focus-d: 12%;\n\n --key-edge: oklch(0% 0 0);\n --code-string: oklch(80% 0.12 150);\n\n --danger-wash: oklch(25% 0.07 25);\n --danger-wash-ink: oklch(80% 0.13 25);\n --success-wash: oklch(24% 0.055 150);\n --success-wash-ink: oklch(82% 0.15 150);\n --warning-wash: oklch(25% 0.055 75);\n --warning-wash-ink: oklch(86% 0.13 80);\n --overlay: oklch(0% 0 0 / 0.7);\n}\n\n/* ───────── 3. Themes ───────── */\n/* Base: pure white and black, Inter, blue. The defaults above. */\n[data-theme=\"base\"] { --h: 262; --c: 0.22; --l: 55%; }\n\n/* Ember: warm paper, Bricolage Grotesque, orange. */\n[data-theme=\"ember\"] {\n --h: 48; --c: 0.19; --l: 66%;\n --ff-display: \"Bricolage Grotesque\", \"Inter\", sans-serif;\n --display-weight: 700;\n --display-tracking: -0.04em;\n --accent-ink-override: oklch(20% 0.04 50);\n --paper: oklch(99.2% 0.005 80);\n --paper-2: oklch(96.4% 0.011 75);\n --paper-3: oklch(91.6% 0.016 72);\n --ink: oklch(19% 0.02 55);\n --ink-2: oklch(38% 0.025 55);\n --muted: oklch(52% 0.025 60);\n --rule: oklch(91% 0.014 72);\n --rule-2: oklch(85.5% 0.018 70);\n --field-edge: oklch(70% 0.025 65);\n}\n.dark[data-theme=\"ember\"], .dark [data-theme=\"ember\"] {\n --paper: oklch(12.5% 0.012 55);\n --paper-2: oklch(18% 0.016 55);\n --paper-3: oklch(23.5% 0.02 55);\n --ink: oklch(97% 0.01 80);\n --ink-2: oklch(85% 0.015 75);\n --muted: oklch(67% 0.02 70);\n --rule: oklch(24.5% 0.018 55);\n --rule-2: oklch(31.5% 0.022 55);\n --field-edge: oklch(45% 0.025 60);\n}\n\n/* Grove: olive paper, Fraunces serif heads, sage. */\n[data-theme=\"grove\"] {\n --h: 150; --c: 0.085; --l: 48%;\n --ff-display: \"Fraunces\", Georgia, serif;\n --display-weight: 500;\n --display-tracking: -0.025em;\n --paper: oklch(99% 0.006 110);\n --paper-2: oklch(96.2% 0.012 115);\n --paper-3: oklch(91.5% 0.018 115);\n --ink: oklch(21% 0.025 140);\n --ink-2: oklch(38% 0.025 140);\n --muted: oklch(51% 0.025 130);\n --rule: oklch(90.5% 0.016 115);\n --rule-2: oklch(85% 0.02 115);\n --field-edge: oklch(68% 0.025 120);\n}\n.dark[data-theme=\"grove\"], .dark [data-theme=\"grove\"] {\n --paper: oklch(13% 0.014 130);\n --paper-2: oklch(18.5% 0.018 130);\n --paper-3: oklch(24% 0.022 130);\n --ink: oklch(96.5% 0.012 110);\n --ink-2: oklch(84% 0.018 115);\n --muted: oklch(67% 0.025 120);\n --rule: oklch(25% 0.02 130);\n --rule-2: oklch(32% 0.024 130);\n --field-edge: oklch(46% 0.028 125);\n}\n\n/* Nebula: navy paper, Space Grotesk, violet. */\n[data-theme=\"nebula\"] {\n --h: 292; --c: 0.2; --l: 56%;\n --ff-display: \"Space Grotesk\", \"Inter\", sans-serif;\n --ff-body: \"Space Grotesk\", \"Inter\", sans-serif;\n --display-weight: 600;\n --display-tracking: -0.04em;\n --paper: oklch(99.3% 0.004 285);\n --paper-2: oklch(96.5% 0.01 285);\n --paper-3: oklch(92% 0.016 285);\n --ink: oklch(18% 0.035 280);\n --ink-2: oklch(36% 0.035 280);\n --muted: oklch(52% 0.03 280);\n --rule: oklch(91.5% 0.014 285);\n --rule-2: oklch(86% 0.018 285);\n --field-edge: oklch(69% 0.03 285);\n}\n.dark[data-theme=\"nebula\"], .dark [data-theme=\"nebula\"] {\n --paper: oklch(13% 0.035 275);\n --paper-2: oklch(18.5% 0.042 275);\n --paper-3: oklch(24% 0.048 278);\n --ink: oklch(97% 0.012 285);\n --ink-2: oklch(85% 0.02 285);\n --muted: oklch(68% 0.035 285);\n --rule: oklch(25% 0.045 278);\n --rule-2: oklch(32% 0.05 280);\n --field-edge: oklch(47% 0.05 282);\n}\n\n/* Terminal: pure neutrals, JetBrains Mono everywhere, phosphor green. */\n[data-theme=\"terminal\"] {\n --h: 148; --c: 0.19; --l: 60%;\n --ff-display: \"JetBrains Mono\", ui-monospace, monospace;\n --ff-body: \"JetBrains Mono\", ui-monospace, monospace;\n --display-weight: 700;\n --display-tracking: -0.06em;\n --accent-ink-override: oklch(14% 0.03 150);\n}\n\n/* Candy: blush paper, Nunito, rose. */\n[data-theme=\"candy\"] {\n --h: 355; --c: 0.19; --l: 62%;\n --ff-display: \"Nunito\", \"Inter\", sans-serif;\n --ff-body: \"Nunito\", \"Inter\", sans-serif;\n --display-weight: 800;\n --display-tracking: -0.035em;\n --paper: oklch(99.4% 0.004 350);\n --paper-2: oklch(97% 0.012 350);\n --paper-3: oklch(93% 0.02 350);\n --ink: oklch(22% 0.03 350);\n --ink-2: oklch(40% 0.035 350);\n --muted: oklch(54% 0.035 350);\n --rule: oklch(92.5% 0.016 350);\n --rule-2: oklch(87.5% 0.022 350);\n --field-edge: oklch(71% 0.035 350);\n}\n.dark[data-theme=\"candy\"], .dark [data-theme=\"candy\"] {\n --paper: oklch(13.5% 0.02 350);\n --paper-2: oklch(19% 0.026 350);\n --paper-3: oklch(24.5% 0.03 350);\n --ink: oklch(97.5% 0.01 350);\n --ink-2: oklch(86% 0.02 350);\n --muted: oklch(69% 0.03 350);\n --rule: oklch(25.5% 0.028 350);\n --rule-2: oklch(32.5% 0.032 350);\n --field-edge: oklch(47% 0.04 350);\n}\n\n/* Surfaces derived from the palette, recomputed wherever a theme re-tints it */\n:root, [data-theme] {\n --raised: var(--paper);\n --inset-c: var(--paper);\n --pop-c: var(--paper);\n --field-c: var(--paper);\n}\n.dark, .dark [data-theme] {\n --raised: oklch(from var(--paper-2) calc(l + 0.035) c h);\n --inset-c: oklch(from var(--paper-2) calc(l + 0.045) c h);\n --pop-c: oklch(from var(--paper-2) calc(l - 0.02) c h);\n --field-c: oklch(from var(--paper) calc(l + 0.06) c h);\n --graphite: oklch(from var(--paper-2) calc(l - 0.035) c h);\n --graphite-2: oklch(from var(--paper-2) calc(l + 0.01) c h);\n --graphite-rule: var(--rule);\n}\n\n/* ───────── 4. Accent formulas ─────────\n * Declared on every element that can change the knobs, so a scoped theme recomputes them. */\n:root, .dark, [data-theme], [data-accent] {\n --accent: oklch(var(--l) var(--c) var(--h));\n --accent-hover: oklch(calc(var(--l) + var(--hover-d)) var(--c) var(--h));\n --accent-edge: oklch(calc(var(--l) - 16%) calc(var(--c) * 0.85) var(--h));\n --accent-ink: var(--accent-ink-override, oklch(100% 0 0));\n --accent-wash: oklch(var(--wash-l) calc(var(--c) * var(--wash-c)) var(--h));\n --accent-wash-ink: oklch(var(--wash-ink-l) var(--c) var(--h));\n --accent-2: oklch(80% calc(var(--c) * 0.55) var(--h));\n --focus: oklch(calc(var(--l) + var(--focus-d)) var(--c) var(--h));\n --ring: oklch(calc(var(--l) + var(--focus-d)) var(--c) var(--h) / 0.28);\n}\n\n/* ───────── 5. Accent presets (override a theme's accent) ───────── */\n[data-accent=\"blue\"] { --h: 262; --c: 0.22; --l: 55%; --swatch-l: 58%; --accent-ink-override: initial; }\n[data-accent=\"violet\"] { --h: 292; --c: 0.22; --l: 54%; --swatch-l: 58%; --accent-ink-override: initial; }\n[data-accent=\"emerald\"] { --h: 160; --c: 0.15; --l: 50%; --swatch-l: 62%; --accent-ink-override: initial; }\n[data-accent=\"vermilion\"] { --h: 33; --c: 0.205; --l: 58%; --swatch-l: 60%; --accent-ink-override: initial; }\n[data-accent=\"rose\"] { --h: 12; --c: 0.21; --l: 55%; --swatch-l: 62%; --accent-ink-override: initial; }\n[data-accent=\"amber\"] { --h: 62; --c: 0.16; --l: 76%; --swatch-l: 76%; --accent-ink-override: oklch(21% 0.03 62); }\n[data-accent=\"mono\"] {\n --h: 0;\n --c: 0;\n --swatch-l: 22%;\n --accent: var(--ink);\n --accent-hover: var(--ink-2);\n --accent-edge: var(--ink);\n --accent-ink: var(--paper);\n --accent-wash: var(--paper-3);\n --accent-wash-ink: var(--ink);\n --accent-2: var(--graphite-ink);\n --focus: var(--ink);\n --ring: oklch(55% 0 0 / 0.3);\n}\n\n/* ───────── 6. Component styles ─────────\n * The :where() part re-applies a style inside a scoped theme so its tokens follow that palette. */\n:root,\n[data-style=\"soft\"],\n:where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --card: var(--paper-2);\n --card-border: var(--soft-edge);\n --elev-card: var(--soft-lift);\n --inset: var(--inset-c);\n --pop: var(--pop-c);\n --pop-border: var(--rule);\n --elev-pop: 0 1px 2px oklch(0% 0 0 / var(--sh-a)), 0 14px 36px -10px oklch(0% 0 0 / calc(var(--sh-a) * 2.6));\n --field: var(--field-c);\n --field-border: var(--field-edge);\n --elev-field: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --key-face: var(--raised);\n --key-border: var(--soft-key-edge, var(--rule));\n --elev-key: inset 0 1px 0 oklch(100% 0 0 / var(--soft-hi-a, 0)), 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --btn-bg: var(--accent);\n --btn-image: linear-gradient(oklch(calc(var(--l) + 5%) var(--c) var(--h)), oklch(calc(var(--l) - 4%) var(--c) calc(var(--h) + 6)));\n --btn-border: transparent;\n --elev-key-accent: inset 0 1px 0 oklch(100% 0 0 / 0.2), 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 1.6));\n --elev-key-danger: inset 0 1px 0 oklch(100% 0 0 / 0.2), 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 1.6));\n --elev-key-press: inset 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 2.5));\n --lift: 0px;\n --press: 0.5px;\n --tray: var(--paper-2);\n --tray-border: var(--rule);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"flat\"],\n:where([data-style=\"flat\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--rule-2);\n --elev-card: none;\n --inset: var(--paper-2);\n --pop: var(--pop-c);\n --pop-border: var(--rule-2);\n --elev-pop: none;\n --field: var(--paper-2);\n --field-border: var(--field-edge);\n --elev-field: none;\n --key-face: var(--paper-2);\n --key-border: var(--paper-2);\n --elev-key: none;\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: transparent;\n --elev-key-accent: none;\n --elev-key-danger: none;\n --elev-key-press: none;\n --lift: 0px;\n --press: 0px;\n --tray: var(--paper-2);\n --tray-border: var(--paper-2);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"glass\"],\n:where([data-style=\"glass\"]) :where([data-theme], .dark) {\n --card: color-mix(in oklab, var(--paper) 42%, transparent);\n --card-border: var(--glass-edge);\n --elev-card: inset 0 1px 0 var(--glass-hi), 0 10px 40px -14px oklch(0% 0 0 / calc(var(--sh-a) * 4));\n --inset: color-mix(in oklab, var(--paper) 55%, transparent);\n --pop: color-mix(in oklab, var(--pop-c) 66%, transparent);\n --pop-border: var(--glass-edge);\n --elev-pop: inset 0 1px 0 var(--glass-hi), 0 18px 48px -14px oklch(0% 0 0 / calc(var(--sh-a) * 5));\n --field: color-mix(in oklab, var(--paper) 40%, transparent);\n --field-border: var(--glass-edge);\n --elev-field: inset 0 1px 3px oklch(0% 0 0 / var(--sh-a));\n --key-face: color-mix(in oklab, var(--paper) 38%, transparent);\n --key-border: var(--glass-edge);\n --elev-key: inset 0 1px 0 var(--glass-hi), 0 4px 14px -6px oklch(0% 0 0 / calc(var(--sh-a) * 3));\n --btn-bg: transparent;\n --btn-image: linear-gradient(oklch(calc(var(--l) + 8%) var(--c) var(--h) / 0.85), oklch(var(--l) var(--c) var(--h) / 0.9));\n --btn-border: oklch(100% 0 0 / 0.28);\n --elev-key-accent: inset 0 1px 0 oklch(100% 0 0 / 0.4), 0 6px 18px -6px oklch(var(--l) var(--c) var(--h) / 0.65);\n --elev-key-danger: inset 0 1px 0 oklch(100% 0 0 / 0.4), 0 6px 18px -6px oklch(57% 0.2 27 / 0.6);\n --elev-key-press: inset 0 2px 6px oklch(0% 0 0 / calc(var(--sh-a) * 2));\n --lift: 0px;\n --press: 0.5px;\n --tray: color-mix(in oklab, var(--paper) 30%, transparent);\n --tray-border: var(--glass-edge);\n --surface-filter: blur(22px) saturate(1.8);\n /* One soft wash of the accent on the page colour: enough behind the glass to show the frost, no rainbow. */\n --stage:\n radial-gradient(60% 80% at 25% 15%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 70%),\n linear-gradient(160deg, color-mix(in oklab, var(--paper) 94%, var(--accent)), var(--paper-2));\n}\n\n[data-style=\"keycap\"],\n:where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--rule);\n --elev-card: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --inset: var(--paper-2);\n --pop: var(--pop-c);\n --pop-border: var(--rule);\n --elev-pop: 0 1px 2px oklch(0% 0 0 / var(--sh-a)), 0 14px 36px -10px oklch(0% 0 0 / calc(var(--sh-a) * 2.6));\n --field: var(--field-c);\n --field-border: var(--field-edge);\n --elev-field: inset 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --key-face: var(--raised);\n --key-border: var(--rule-2);\n --elev-key: 0 2px 0 0 var(--key-edge), 0 5px 8px -5px oklch(0% 0 0 / calc(var(--sh-a) * 4)), inset 0 1px 0 var(--glass-hi);\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: var(--accent-edge);\n --elev-key-accent: 0 2px 0 0 var(--accent-edge), 0 5px 8px -5px oklch(30% 0.12 var(--h) / 0.45), inset 0 1px 0 oklch(100% 0 0 / 0.22);\n --elev-key-danger: 0 2px 0 0 var(--danger-edge), 0 5px 8px -5px oklch(30% 0.12 27 / 0.45), inset 0 1px 0 oklch(100% 0 0 / 0.22);\n --elev-key-press: inset 0 1px 2px oklch(0% 0 0 / calc(var(--sh-a) * 2.5));\n --lift: -1px;\n --press: 2px;\n --tray: var(--paper-3);\n --tray-border: var(--rule);\n --surface-filter: none;\n --stage: none;\n}\n\n[data-style=\"brutal\"],\n:where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --card: var(--paper);\n --card-border: var(--ink);\n --elev-card: 4px 4px 0 0 var(--ink);\n --inset: var(--paper-2);\n --pop: var(--paper);\n --pop-border: var(--ink);\n --elev-pop: 5px 5px 0 0 var(--ink);\n --field: var(--paper);\n --field-border: var(--ink);\n --elev-field: 2px 2px 0 0 var(--ink);\n --key-face: var(--paper);\n --key-border: var(--ink);\n --elev-key: 3px 3px 0 0 var(--ink);\n --btn-bg: var(--accent);\n --btn-image: none;\n --btn-border: var(--ink);\n --elev-key-accent: 3px 3px 0 0 var(--ink);\n --elev-key-danger: 3px 3px 0 0 var(--ink);\n --elev-key-press: 0 0 0 0 var(--ink);\n --lift: -1px;\n --press: 2px;\n --tray: var(--paper);\n --tray-border: var(--ink);\n --surface-filter: none;\n --stage: none;\n}\n\n/* ───────── 7. Field and control tokens ─────────\n * Soft: filled fields with no border and an inset focus ring, white controls that\n * turn into a lit accent gradient when on. The other styles keep outlined fields and flat fills. */\n:root, [data-theme] {\n --field-soft: color-mix(in oklab, var(--ink) 7.5%, var(--paper));\n --field-soft-hover: color-mix(in oklab, var(--ink) 10.5%, var(--paper));\n --control-on-soft: linear-gradient(var(--accent), color-mix(in oklab, var(--accent) 80%, oklch(0% 0 0)));\n}\n/* Dark Soft: shadows vanish on black, so cards get a faint light edge, and fields sit a step above the card\n * instead of sinking below it. Light Soft keeps no edge. */\n:root { --soft-edge: transparent; --soft-lift: none; }\n.dark {\n --soft-edge: oklch(100% 0 0 / 0.08);\n --soft-lift: inset 0 1px 0 oklch(100% 0 0 / 0.05);\n}\n.dark, .dark [data-theme] {\n --soft-key-edge: var(--rule-2);\n --soft-hi-a: 0.06;\n --field-soft: color-mix(in oklab, var(--ink) 8%, var(--paper-2));\n --field-soft-hover: color-mix(in oklab, var(--ink) 12%, var(--paper-2));\n}\n:root:not([data-style]),\n[data-style=\"soft\"],\n:where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --field: var(--field-soft);\n --field-border: transparent;\n --elev-field: none;\n --field-hover: var(--field-soft-hover);\n --field-hover-border: transparent;\n --field-focus-border: transparent;\n --field-focus-ring: inset 0 0 0 2px var(--accent);\n --control: var(--raised);\n --control-border: var(--rule-2);\n --elev-control: 0 1px 2px oklch(0% 0 0 / var(--sh-a));\n --control-on-image: var(--control-on-soft);\n --elev-control-on: inset 0 2px 0 oklch(100% 0 0 / 0.25), inset 0 0 0 1px var(--accent);\n --track-off: var(--field-soft-hover);\n --thumb-image: linear-gradient(var(--raised) 44%, var(--paper-2));\n --elev-thumb: 0 3px 3px oklch(0% 0 0 / 0.04), 0 0.75px 0 oklch(0% 0 0 / 0.08);\n --tray-border: transparent;\n /* Floating bars need an edge even where trays have none. */\n --float-border: var(--rule);\n --pill-radius: calc(6px * var(--radius-factor));\n}\n[data-style=\"flat\"], :where([data-style=\"flat\"]) :where([data-theme], .dark),\n[data-style=\"glass\"], :where([data-style=\"glass\"]) :where([data-theme], .dark),\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark),\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --field-hover: var(--field);\n --field-hover-border: var(--muted);\n --field-focus-border: var(--accent);\n --field-focus-ring: 0 0 0 3px var(--ring);\n --control: var(--field);\n --control-border: var(--field-border);\n --elev-control: var(--elev-field);\n --control-on-image: none;\n --elev-control-on: none;\n --track-off: var(--rule-2);\n --thumb-image: none;\n --elev-thumb: 0 1px 3px var(--overlay);\n --float-border: var(--tray-border);\n --pill-radius: var(--radius-input);\n}\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --control-on-image: var(--control-on-soft);\n --elev-control-on: inset 0 1px 0 oklch(100% 0 0 / 0.25), 0 1px 0 var(--accent-edge);\n}\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --field-focus-border: var(--ink);\n --field-focus-ring: 3px 3px 0 0 var(--accent);\n --elev-thumb: none;\n}\n\n/* Soft popovers: a hairline and a light lift. Danger buttons get the same lit gradient as primary. */\n:root:not([data-style]), [data-style=\"soft\"], :where([data-style=\"soft\"]) :where([data-theme], .dark) {\n --elev-pop: 0 1px 1px oklch(0% 0 0 / 0.04), 0 4px 4px oklch(0% 0 0 / 0.02), 0 16px 32px -16px oklch(0% 0 0 / calc(var(--sh-a) * 2));\n --btn-danger-image: linear-gradient(color-mix(in oklab, var(--danger) 92%, oklch(100% 0 0)), color-mix(in oklab, var(--danger) 86%, oklch(0% 0 0)));\n}\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --btn-danger-image: linear-gradient(color-mix(in oklab, var(--danger) 92%, oklch(100% 0 0)), color-mix(in oklab, var(--danger) 86%, oklch(0% 0 0)));\n}\n[data-style=\"flat\"], :where([data-style=\"flat\"]) :where([data-theme], .dark),\n[data-style=\"glass\"], :where([data-style=\"glass\"]) :where([data-theme], .dark),\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --btn-danger-image: none;\n}\n\n/* Chart colour, per style. Each series blends from a neighbouring hue (shifted by --chart-shift degrees and\n * lifted by --chart-lift) into its own colour, and areas fade down from --chart-area-a.\n * Soft and Keycap: a gentle blend. Glass: a wider, brighter blend. Flat: solid colour, light wash. Brutal: solid, no wash. */\n:root,\n[data-style=\"soft\"], :where([data-style=\"soft\"]) :where([data-theme], .dark),\n[data-style=\"keycap\"], :where([data-style=\"keycap\"]) :where([data-theme], .dark) {\n --chart-shift: -50;\n --chart-lift: 0.08;\n --chart-area-a: 0.34;\n}\n[data-style=\"glass\"], :where([data-style=\"glass\"]) :where([data-theme], .dark) {\n --chart-shift: -70;\n --chart-lift: 0.12;\n --chart-area-a: 0.42;\n}\n[data-style=\"flat\"], :where([data-style=\"flat\"]) :where([data-theme], .dark) {\n --chart-shift: 0;\n --chart-lift: 0;\n --chart-area-a: 0.14;\n}\n[data-style=\"brutal\"], :where([data-style=\"brutal\"]) :where([data-theme], .dark) {\n --chart-shift: 0;\n --chart-lift: 0;\n --chart-area-a: 0;\n}\n/* Override the style's chart look with data-chart on any element: blend, vivid or solid. */\n[data-chart=\"blend\"], [data-chart=\"blend\"] :where([data-theme], .dark) { --chart-shift: -50; --chart-lift: 0.08; --chart-area-a: 0.34; }\n[data-chart=\"vivid\"], [data-chart=\"vivid\"] :where([data-theme], .dark) { --chart-shift: -100; --chart-lift: 0.12; --chart-area-a: 0.45; }\n[data-chart=\"solid\"], [data-chart=\"solid\"] :where([data-theme], .dark) { --chart-shift: 0; --chart-lift: 0; --chart-area-a: 0.14; }\n\n/* ───────── 8. Brand ─────────\n * The PlankUI mark on the site takes the visitor's accent: planks in the accent's ink on an accent tile.\n * Files outside the site (favicon, badges, social card) keep the fixed blue below. */\n:root {\n --brand-blue: oklch(56% 0.22 263);\n --brand-tile: var(--accent, var(--brand-blue));\n --brand-plank: var(--accent-ink, oklch(100% 0 0));\n}\n\n/* Holographic foil for PlankUI Pro: soft pastels that shift like a tilted sticker. Fixed in every theme. */\n:root {\n --holo-1: oklch(88% 0.08 350);\n --holo-2: oklch(86% 0.08 285);\n --holo-3: oklch(90% 0.07 215);\n --holo-4: oklch(92% 0.08 160);\n --holo-5: oklch(95% 0.09 95);\n --holo-ink: oklch(24% 0.06 285);\n --holo-edge: oklch(100% 0 0);\n}\n","src/styles.css":"@import \"tailwindcss\";\n@import \"./tokens.css\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n@theme inline {\n --color-paper: var(--paper);\n --color-paper-2: var(--paper-2);\n --color-paper-3: var(--paper-3);\n --color-ink: var(--ink);\n --color-ink-2: var(--ink-2);\n --color-muted: var(--muted);\n --color-rule: var(--rule);\n --color-rule-2: var(--rule-2);\n --color-accent: var(--accent);\n --color-accent-hover: var(--accent-hover);\n --color-accent-edge: var(--accent-edge);\n --color-accent-ink: var(--accent-ink);\n --color-accent-wash: var(--accent-wash);\n --color-accent-wash-ink: var(--accent-wash-ink);\n --color-accent-2: var(--accent-2);\n --color-graphite: var(--graphite);\n --color-graphite-2: var(--graphite-2);\n --color-graphite-ink: var(--graphite-ink);\n --color-graphite-muted: var(--graphite-muted);\n --color-graphite-rule: var(--graphite-rule);\n --color-key-face: var(--key-face);\n --color-key-edge: var(--key-edge);\n --color-danger: var(--danger);\n --color-danger-wash: var(--danger-wash);\n --color-danger-wash-ink: var(--danger-wash-ink);\n --color-success: var(--success);\n --color-success-wash: var(--success-wash);\n --color-success-wash-ink: var(--success-wash-ink);\n --color-warning: var(--warning);\n --color-warning-wash: var(--warning-wash);\n --color-warning-wash-ink: var(--warning-wash-ink);\n --color-overlay: var(--overlay);\n --color-white: var(--white);\n --color-focus: var(--focus);\n --color-field-edge: var(--field-edge);\n --color-card: var(--card);\n --color-card-border: var(--card-border);\n --color-inset: var(--inset);\n --color-pop: var(--pop);\n --color-pop-border: var(--pop-border);\n --color-field: var(--field);\n --color-field-border: var(--field-border);\n --color-tray: var(--tray);\n --color-tray-border: var(--tray-border);\n --color-raised: var(--raised);\n\n --font-display: var(--ff-display);\n --font-sans: var(--ff-body);\n --font-mono: var(--ff-code);\n\n --shadow-card: var(--elev-card);\n --shadow-pop: var(--elev-pop);\n --shadow-key: var(--elev-key);\n --shadow-key-accent: var(--elev-key-accent);\n --shadow-key-danger: var(--elev-key-danger);\n --shadow-key-press: var(--elev-key-press);\n --shadow-field: var(--elev-field);\n}\n\n@theme {\n --radius-sm: calc(6px * var(--radius-factor));\n --radius-input: calc(10px * var(--radius-factor));\n --radius-card: calc(12px * var(--radius-factor));\n --radius-panel: calc(18px * var(--radius-factor));\n --radius-tile: calc(16px * var(--radius-factor));\n --radius-shell: calc(22px * var(--radius-factor));\n --radius-frame: calc(28px * var(--radius-factor));\n --radius-pill: calc(999px * var(--radius-factor));\n}\n\n/* ───────────── Base ───────────── */\n*, *::before, *::after { box-sizing: border-box; }\nhtml, body { overflow-x: clip; }\nhtml { scroll-padding-top: 96px; scrollbar-color: var(--rule-2) var(--paper); }\nbody {\n margin: 0;\n background: var(--paper);\n color: var(--ink);\n font-family: var(--ff-body);\n font-size: 1rem;\n line-height: 1.55;\n -webkit-font-smoothing: antialiased;\n caret-color: var(--accent);\n}\n::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); color: var(--ink); }\n:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\nh1, h2, h3, h4 {\n font-family: var(--ff-display);\n font-weight: var(--display-weight);\n letter-spacing: var(--display-tracking);\n line-height: 1.05;\n overflow-wrap: break-word;\n min-width: 0;\n text-wrap: balance;\n font-synthesis: none;\n}\nh3, h4 { font-weight: 500; line-height: 1.2; }\np { text-wrap: pretty; }\ncode, kbd, pre { font-family: var(--ff-code); }\n\n/* Hairline ruled grid used behind previews and the hero bench */\n.bg-cells {\n background-image:\n radial-gradient(circle at 1px 1px, var(--rule-2) 1px, transparent 0);\n background-size: 16px 16px;\n}\n\n/* Radix enter/exit motion: opacity and transform only */\n@keyframes vx-pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }\n@keyframes vx-pop-out { to { opacity: 0; transform: translateY(-4px) scale(0.98); } }\n@keyframes vx-fade-in { from { opacity: 0; } }\n@keyframes vx-fade-out { to { opacity: 0; } }\n@keyframes vx-dialog-in { from { opacity: 0; transform: translate(-50%, -48%) scale(0.97); } }\n@keyframes vx-dialog-out { to { opacity: 0; transform: translate(-50%, -48%) scale(0.97); } }\n@keyframes vx-toast-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } }\n@keyframes vx-spin { to { transform: rotate(360deg); } }\n@keyframes vx-shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }\n@keyframes vx-reveal { from { opacity: 0; transform: translateY(4px); } }\n\n.vx-pop[data-state=\"open\"], .vx-pop[data-state=\"delayed-open\"], .vx-pop[data-state=\"instant-open\"] { animation: vx-pop-in var(--dur-short) var(--ease-out); }\n.vx-pop[data-state=\"closed\"] { animation: vx-pop-out var(--dur-micro) ease-in; }\n.vx-fade[data-state=\"open\"] { animation: vx-fade-in var(--dur-short) var(--ease-out); }\n.vx-fade[data-state=\"closed\"] { animation: vx-fade-out var(--dur-micro) ease-in; }\n.vx-dialog[data-state=\"open\"] { animation: vx-dialog-in var(--dur-short) var(--ease-out); }\n.vx-dialog[data-state=\"closed\"] { animation: vx-dialog-out var(--dur-micro) ease-in; }\n.vx-toast { animation: vx-toast-in var(--dur-short) var(--ease-out); }\n.vx-spin { animation: vx-spin 0.8s linear infinite; }\n.vx-reveal[data-state=\"open\"] { animation: vx-reveal var(--dur-short) var(--ease-out); }\n.vx-shimmer { position: relative; overflow: hidden; }\n.vx-shimmer::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: linear-gradient(90deg, transparent, color-mix(in oklab, var(--paper) 60%, transparent), transparent);\n animation: vx-shimmer 1.4s ease-in-out infinite;\n}\n\n/* sugar-high code tokens: one accent, never a rainbow */\n.code-block {\n --sh-class: var(--accent-wash-ink);\n --sh-identifier: var(--ink);\n --sh-sign: var(--muted);\n --sh-property: var(--ink-2);\n --sh-entity: var(--accent-wash-ink);\n --sh-jsxliterals: var(--ink);\n --sh-string: var(--code-string);\n --sh-keyword: oklch(from var(--accent-wash-ink) l c calc(h + 40));\n --sh-comment: var(--muted);\n}\n\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* An accent preview chip: computes its colour on the element, so a nested data-accent shows that hue */\n.swatch { background: oklch(var(--swatch-l, 56%) var(--c) var(--h)); }\n\n/* Sheet slides: transform only */\n@keyframes vx-sheet-right { from { transform: translateX(100%); } }\n@keyframes vx-sheet-right-out { to { transform: translateX(100%); } }\n@keyframes vx-sheet-left { from { transform: translateX(-100%); } }\n@keyframes vx-sheet-left-out { to { transform: translateX(-100%); } }\n@keyframes vx-sheet-bottom { from { transform: translateY(100%); } }\n@keyframes vx-sheet-bottom-out { to { transform: translateY(100%); } }\n.vx-sheet-right[data-state=\"open\"] { animation: vx-sheet-right var(--dur-short) var(--ease-out); }\n.vx-sheet-right[data-state=\"closed\"] { animation: vx-sheet-right-out var(--dur-short) ease-in; }\n.vx-sheet-left[data-state=\"open\"] { animation: vx-sheet-left var(--dur-short) var(--ease-out); }\n.vx-sheet-left[data-state=\"closed\"] { animation: vx-sheet-left-out var(--dur-short) ease-in; }\n.vx-sheet-bottom[data-state=\"open\"] { animation: vx-sheet-bottom var(--dur-short) var(--ease-out); }\n.vx-sheet-bottom[data-state=\"closed\"] { animation: vx-sheet-bottom-out var(--dur-short) ease-in; }\n@keyframes vx-caret { 50% { opacity: 0; } }\n.vx-caret { animation: vx-caret 1s step-end infinite; }\n\n/* Component style hooks */\n.vx-glass { -webkit-backdrop-filter: var(--surface-filter); backdrop-filter: var(--surface-filter); }\n[data-style=\"glass\"] .vx-stage, .vx-stage[data-style=\"glass\"] { background-image: var(--stage); }\n\n/* Moving columns: transform only, paused on hover, still under reduced motion */\n@keyframes vx-marquee-up { to { transform: translateY(-50%); } }\n@keyframes vx-marquee-down { from { transform: translateY(-50%); } to { transform: translateY(0); } }\n.vx-marquee-track { animation: vx-marquee-up var(--marquee-dur, 40s) linear infinite; will-change: transform; }\n.vx-marquee-track[data-reverse] { animation-name: vx-marquee-down; }\n.vx-marquee:hover .vx-marquee-track { animation-play-state: paused; }\n@media (prefers-reduced-motion: reduce) { .vx-marquee-track { animation: none !important; } }\n\n/* Scoped themes need their own body face */\n[data-theme] { font-family: var(--ff-body); }\n\n/* Theme graphics: one decorative layer behind the hero, different per theme */\n.vx-graphic {\n background:\n radial-gradient(28% 60% at 22% 0%, oklch(78% 0.14 var(--h) / 0.35), transparent 70%),\n radial-gradient(26% 55% at 50% 0%, oklch(82% 0.12 calc(var(--h) + 120) / 0.3), transparent 70%),\n radial-gradient(28% 60% at 80% 0%, oklch(84% 0.11 calc(var(--h) - 80) / 0.3), transparent 70%);\n}\n[data-theme=\"ember\"] .vx-graphic, .vx-graphic[data-theme=\"ember\"] {\n background:\n radial-gradient(50% 70% at 50% 0%, oklch(75% 0.17 var(--h) / 0.35), transparent 70%),\n repeating-linear-gradient(90deg, transparent 0 22px, oklch(70% 0.15 var(--h) / 0.14) 22px 24px, transparent 24px 60px, oklch(80% 0.12 calc(var(--h) + 20) / 0.1) 60px 61px);\n -webkit-mask-image: linear-gradient(to bottom, black, transparent 85%);\n mask-image: linear-gradient(to bottom, black, transparent 85%);\n}\n[data-theme=\"grove\"] .vx-graphic, .vx-graphic[data-theme=\"grove\"] {\n background:\n radial-gradient(circle at 1px 1px, oklch(55% 0.06 var(--h) / 0.35) 1.2px, transparent 0) 0 0 / 22px 22px,\n radial-gradient(45% 60% at 50% 0%, oklch(85% 0.08 var(--h) / 0.45), transparent 70%);\n -webkit-mask-image: radial-gradient(ellipse at 50% 20%, black 30%, transparent 75%);\n mask-image: radial-gradient(ellipse at 50% 20%, black 30%, transparent 75%);\n}\n[data-theme=\"nebula\"] .vx-graphic, .vx-graphic[data-theme=\"nebula\"] {\n background:\n radial-gradient(1px 1px at 12% 18%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 32% 42%, var(--ink) 50%, transparent 51%),\n radial-gradient(1.5px 1.5px at 58% 12%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 74% 36%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 88% 16%, var(--ink) 50%, transparent 51%),\n radial-gradient(1.5px 1.5px at 44% 28%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 22% 58%, var(--ink) 50%, transparent 51%),\n radial-gradient(1px 1px at 66% 54%, var(--ink) 50%, transparent 51%),\n radial-gradient(35% 50% at 25% 10%, oklch(65% 0.2 var(--h) / 0.4), transparent 70%),\n radial-gradient(35% 50% at 78% 20%, oklch(70% 0.16 calc(var(--h) - 60) / 0.35), transparent 70%);\n opacity: 0.9;\n}\n[data-theme=\"terminal\"] .vx-graphic, .vx-graphic[data-theme=\"terminal\"] {\n background:\n repeating-linear-gradient(0deg, oklch(60% 0.15 var(--h) / 0.07) 0 1px, transparent 1px 4px),\n linear-gradient(90deg, var(--rule) 1px, transparent 1px) 0 0 / 48px 48px,\n linear-gradient(0deg, var(--rule) 1px, transparent 1px) 0 0 / 48px 48px;\n -webkit-mask-image: radial-gradient(ellipse at 50% 15%, black 25%, transparent 75%);\n mask-image: radial-gradient(ellipse at 50% 15%, black 25%, transparent 75%);\n}\n[data-theme=\"candy\"] .vx-graphic, .vx-graphic[data-theme=\"candy\"] {\n background:\n radial-gradient(22% 38% at 15% 10%, oklch(85% 0.12 var(--h) / 0.7), transparent 70%),\n radial-gradient(20% 34% at 40% 0%, oklch(88% 0.1 80 / 0.6), transparent 70%),\n radial-gradient(22% 38% at 66% 8%, oklch(86% 0.1 200 / 0.6), transparent 70%),\n radial-gradient(20% 34% at 90% 4%, oklch(85% 0.12 300 / 0.55), transparent 70%);\n}\n\n/* Radius scales recomputed inside scoped themes */\n[data-theme], [style*=\"--radius-factor\"] {\n --radius-sm: calc(6px * var(--radius-factor));\n --radius-input: calc(10px * var(--radius-factor));\n --radius-card: calc(12px * var(--radius-factor));\n --radius-panel: calc(18px * var(--radius-factor));\n --radius-tile: calc(16px * var(--radius-factor));\n --radius-shell: calc(22px * var(--radius-factor));\n --radius-frame: calc(28px * var(--radius-factor));\n --radius-pill: calc(999px * var(--radius-factor));\n}\n\n/* Charts and composer motion: transform, opacity, clip-path and stroke only */\n@keyframes vx-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n@keyframes vx-fade-up { from { opacity: 0; transform: translateY(6px); } }\n.vx-draw { stroke-dasharray: 1; animation: vx-draw 1.1s var(--ease-out) both; }\n.vx-fade-up { animation: vx-fade-up var(--dur-short) var(--ease-out) both; }\n.vx-orbit { animation: vx-spin 2.6s linear infinite; }\n.vx-orbit-slow { animation: vx-spin 6s linear infinite; }\n\n/* Colour picker hue strip */\n.vx-hue { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }\n.vx-hue::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 999px; background: var(--white); border: 2px solid var(--white); box-shadow: 0 0 0 1px oklch(0% 0 0 / 0.3), 0 2px 4px oklch(0% 0 0 / 0.3); }\n.vx-hue::-moz-range-thumb { width: 14px; height: 14px; border-radius: 999px; background: var(--white); border: 2px solid var(--white); box-shadow: 0 0 0 1px oklch(0% 0 0 / 0.3); }\n\n/* Mode and theme switches: a circle grows from the click for mode, a soft crossfade for themes */\n::view-transition-old(root), ::view-transition-new(root) { animation-duration: 360ms; animation-timing-function: var(--ease-out); }\n[data-vt=\"reveal\"]::view-transition-old(root), [data-vt=\"reveal\"]::view-transition-new(root) { animation: none; mix-blend-mode: normal; }\n[data-vt=\"reveal\"]::view-transition-old(root) { z-index: 1; }\n[data-vt=\"reveal\"]::view-transition-new(root) { z-index: 2; }\n\n/* Composer light: a dash that laps the rim at constant speed */\n@keyframes vx-rim { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } }\n.vx-rim { animation: vx-rim 4.5s linear infinite; }\n@media (prefers-reduced-motion: reduce) { .vx-rim { animation: none; } }\n\n/* Thinking: text with a light passing through it */\n@keyframes vx-shimmer-text { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n.vx-shimmer-text {\n color: transparent;\n background: linear-gradient(90deg, var(--muted) 0%, var(--muted) 40%, var(--ink) 50%, var(--muted) 60%, var(--muted) 100%) 0 0 / 200% 100%;\n -webkit-background-clip: text;\n background-clip: text;\n animation: vx-shimmer-text 1.8s linear infinite;\n}\n\n/* Docs side menu: one guide line, a short tick into each row, a rounded corner on the last */\n/* Sidebar tree: the guide sits in the gutter, outside each row, so a selected row's card never covers it.\n Rows touch (no gap) and each guide segment runs past the row's 1px border, so the line is continuous. */\n.vx-tree { --tree-line: color-mix(in oklab, var(--muted) 38%, transparent); display: grid; gap: 0; }\n.vx-tree > * { position: relative; margin-left: 22px; padding-left: 10px !important; }\n.vx-tree > *::before {\n content: \"\"; position: absolute; left: -9px; top: -1px; bottom: -1px; border-left: 1px solid var(--tree-line);\n}\n.vx-tree > *::after {\n content: \"\"; position: absolute; left: -9px; top: 50%; width: 6px; border-top: 1px solid var(--tree-line);\n}\n.vx-tree > *:last-child::before { bottom: calc(50% - 1px); width: 6px; border-bottom: 1px solid var(--tree-line); border-bottom-left-radius: calc(5px * var(--radius-factor)); }\n.vx-tree > *:last-child::after { display: none; }\n\n/* Bar list, radar and activity rings entrances */\n@keyframes vx-grow-x { from { transform: scaleX(0); } }\n@keyframes vx-radar { from { opacity: 0; transform: scale(0.6); } }\n@keyframes vx-ring { from { stroke-dasharray: 0 var(--len); } }\n.vx-grow-x { transform-origin: left; animation: vx-grow-x 0.7s var(--ease-out) both; }\n.vx-radar { animation: vx-radar 0.6s var(--ease-out) both; }\n.vx-ring { animation: vx-ring 1.1s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-grow-x, .vx-radar, .vx-ring { animation: none; } }\n/* Reveal lines left to right; safe with non-scaling strokes, unlike dash-offset drawing. */\n@keyframes vx-wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }\n.vx-wipe { animation: vx-wipe 1.1s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-wipe { animation: none; } }\n\n/* Field states follow the style: Soft fills and rings inside, the rest outline and glow. */\n@utility field-hover {\n border-color: var(--field-hover-border);\n background-color: var(--field-hover);\n}\n@utility field-focus {\n border-color: var(--field-focus-border);\n box-shadow: var(--field-focus-ring);\n}\n@keyframes vx-slide-next { from { opacity: 0; transform: translateX(16px); } }\n@keyframes vx-slide-prev { from { opacity: 0; transform: translateX(-16px); } }\n@keyframes vx-rise { from { transform: scaleY(0); } }\n.vx-rise { animation: vx-rise 0.7s var(--ease-out) both; }\n@media (prefers-reduced-motion: reduce) { .vx-rise { animation: none; } }\n\n/* Holographic sticker: a conic foil, a light sweep on hover, and an optional slow drift. */\n@property --holo-angle { syntax: \"<angle>\"; initial-value: 200deg; inherits: false; }\n.vx-holo {\n position: relative;\n overflow: hidden;\n isolation: isolate;\n background:\n radial-gradient(120% 90% at var(--holo-x, 30%) var(--holo-y, 20%), oklch(100% 0 0 / 0.55), transparent 55%),\n conic-gradient(from var(--holo-angle) at 50% 50%, var(--holo-1), var(--holo-2), var(--holo-3), var(--holo-4), var(--holo-5), var(--holo-1));\n color: var(--holo-ink);\n}\n.vx-holo::after {\n content: \"\";\n position: absolute;\n inset: 0;\n z-index: -1;\n background: linear-gradient(115deg, transparent 30%, oklch(100% 0 0 / 0.75) 45%, transparent 60%);\n transform: translateX(-130%);\n transition: transform 0.9s var(--ease-out);\n}\n.vx-holo:hover::after, .group:hover .vx-holo::after { transform: translateX(130%); }\n@keyframes vx-holo-drift { to { --holo-angle: 560deg; } }\n.vx-holo-live { animation: vx-holo-drift 9s linear infinite; }\n@media (prefers-reduced-motion: reduce) {\n .vx-holo::after { transition: none; }\n .vx-holo-live { animation: none; }\n}\n\n/* Menus and dialogs lock scrolling with `overflow: hidden` on body. With overflow-x clipped on html, that turns\n body into its own scroll box, and sticky elements (the docs sidebar, the top bar) jump to the top of the page.\n `clip` keeps body from becoming a scroll box; the lock still blocks wheel and touch scrolling. */\nhtml body[data-scroll-locked] { overflow: clip !important; margin-right: 0 !important; padding-right: 0 !important; }\n","src/lib/cn.ts":"import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"},"menuStyles":"/**\n * One look for every dropdown in the library: Select, Dropdown menu, Context menu, Combobox,\n * Command, Tag input suggestions, Date picker presets and the Model picker.\n * Change these strings to restyle all of them at once.\n */\n\n/** The floating panel. */\nexport const menuPanel =\n \"vx-pop vx-glass z-[100] min-w-[12rem] overflow-hidden rounded-[calc(16px*var(--radius-factor))] border border-pop-border bg-pop p-2 text-ink shadow-pop outline-none\";\n\n/** A row. Highlight comes from keyboard or pointer (data-highlighted) or an explicit data-active attribute. */\nexport const menuItem =\n \"relative flex min-h-8 cursor-default select-none items-center gap-2 rounded-input px-2 py-1.5 text-[14px] font-medium text-ink outline-none transition-[background-color] duration-[var(--dur-micro)] data-[highlighted]:bg-paper-3 data-[active=true]:bg-paper-3 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted\";\n\n/** A section heading inside a menu. */\nexport const menuLabel = \"px-2 pb-1 pt-1.5 text-[14px] font-medium text-muted\";\n\n/** A divider between groups. */\nexport const menuSeparator = \"-mx-2 my-1.5 h-px bg-rule\";\n\n/** The selected indicator on the right of a row. */\nexport const menuCheck = \"ml-auto size-4 shrink-0 !text-accent\";\n"},"themes":[{"id":"base","name":"Base","font":"Inter","note":"Pure white and black, electric blue","style":"soft","radius":1},{"id":"ember","name":"Ember","font":"Bricolage Grotesque","note":"Warm paper, orange, light streaks","style":"keycap","radius":1},{"id":"grove","name":"Grove","font":"Fraunces","note":"Olive paper, sage, a serif voice","style":"flat","radius":0.5},{"id":"nebula","name":"Nebula","font":"Space Grotesk","note":"Deep navy, violet, frosted glass","style":"glass","radius":1.5},{"id":"terminal","name":"Terminal","font":"JetBrains Mono","note":"Monospace, phosphor green, hard edges","style":"brutal","radius":0},{"id":"candy","name":"Candy","font":"Nunito","note":"Blush paper, rose, soft round keys","style":"keycap","radius":1.5}],"styles":[{"id":"soft","name":"Soft","note":"Grey shells, white panels, gentle shadows"},{"id":"flat","name":"Flat","note":"Fills and hairlines, no shadows"},{"id":"glass","name":"Liquid glass","note":"Frosted, translucent, lit from above"},{"id":"keycap","name":"Keycap","note":"Raised keys with a real bottom edge"},{"id":"brutal","name":"Brutal","note":"Ink borders and hard offset shadows"}],"accents":[{"id":"auto","name":"Theme"},{"id":"blue","name":"Blue"},{"id":"vermilion","name":"Vermilion"},{"id":"violet","name":"Violet"},{"id":"emerald","name":"Emerald"},{"id":"rose","name":"Rose"},{"id":"amber","name":"Amber"},{"id":"mono","name":"Mono"}],"components":[{"slug":"button","name":"Button","category":"Actions","description":"A keycap with a real bottom edge. It lifts on hover and sinks when pressed.","deps":["radix-ui","class-variance-authority"],"uses":["spinner"],"usage":"<Button>Save</Button>\n<Button variant=\"secondary\" size=\"sm\">Cancel</Button>","props":[["variant","\"primary\" | \"secondary\" | \"soft\" | \"outline\" | \"ghost\" | \"danger\" | \"link\"","\"primary\"","Visual style."],["size","\"xs\" | \"sm\" | \"md\" | \"lg\" | \"icon\" | \"icon-sm\"","\"md\"","Height and padding."],["loading","boolean","false","Shows a spinner and blocks clicks."],["asChild","boolean","false","Render the child element, such as a router link, with button styles."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { Spinner } from \"./spinner\";\n\nexport const buttonVariants = cva(\n [\n \"relative inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium\",\n \"transition-[transform,box-shadow,background-color,color] duration-[var(--dur-micro)] ease-[var(--ease-out)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"disabled:pointer-events-none disabled:opacity-50 aria-busy:pointer-events-none\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n primary:\n \"vx-glass border border-[color:var(--btn-border)] bg-[color:var(--btn-bg)] [background-image:var(--btn-image)] text-accent-ink shadow-key-accent hover:translate-y-[var(--lift)] hover:brightness-110 active:translate-y-[var(--press)] active:shadow-key-press\",\n secondary:\n \"vx-glass border border-[color:var(--key-border)] bg-key-face text-ink shadow-key hover:translate-y-[var(--lift)] hover:bg-paper-2 active:translate-y-[var(--press)] active:shadow-key-press\",\n soft: \"bg-accent-wash text-accent-wash-ink hover:bg-accent-wash/70\",\n outline: \"border border-[color:var(--key-border)] text-ink hover:bg-paper-2\",\n ghost: \"text-ink-2 hover:bg-paper-3 hover:text-ink\",\n danger:\n \"border border-[color:var(--btn-border)] bg-danger [background-image:var(--btn-danger-image)] text-white shadow-key-danger hover:translate-y-[var(--lift)] hover:brightness-110 active:translate-y-[var(--press)] active:shadow-key-press\",\n link: \"h-auto px-0 text-accent underline-offset-4 decoration-rule-2 hover:underline hover:decoration-accent\",\n },\n size: {\n xs: \"h-7 rounded-sm px-2.5 text-xs\",\n sm: \"h-8 rounded-input px-3 text-sm\",\n md: \"h-9 rounded-input px-4 text-sm\",\n lg: \"h-11 rounded-card px-5 text-base\",\n icon: \"size-9 rounded-input\",\n \"icon-sm\": \"size-8 rounded-input\",\n },\n },\n defaultVariants: { variant: \"primary\", size: \"md\" },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Render the child element instead of a button, keeping the styles. */\n asChild?: boolean;\n /** Show a spinner and block interaction. */\n loading?: boolean;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild, loading, children, disabled, ...props }, ref) => {\n const Comp = asChild ? Slot.Root : \"button\";\n return (\n <Comp\n ref={ref}\n data-slot=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {loading && <Spinner className=\"size-4\" />}\n {children}\n </>\n )}\n </Comp>\n );\n },\n);\nButton.displayName = \"Button\";\n"},{"slug":"segmented","name":"Segmented control","category":"Actions","description":"One choice from a short set. A raised pill slides to the chosen option.","deps":["radix-ui"],"uses":[],"usage":"<Segmented options={[{ value: \"monthly\", label: \"Monthly\" }, { value: \"yearly\", label: \"Yearly\" }]} />","props":[["options","{ value: string; label: ReactNode; disabled?: boolean }[]","required","The choices."],["value / defaultValue","string","first option","Controlled or initial value."],["onValueChange","(value: string) => void","","Called when the choice changes."],["size","\"sm\" | \"md\"","\"md\"","Key height."]],"pro":false,"source":"import * as React from \"react\";\nimport { ToggleGroup } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Places a pill under the active item of a list and slides it on change. The pill is positioned\n * instantly and animated with a transform from where it was, so width and left never animate.\n */\nexport function useSlidingPill(list: React.RefObject<HTMLElement | null>, selector: string, active: unknown) {\n const pill = React.useRef<HTMLSpanElement>(null);\n const last = React.useRef<{ x: number; w: number } | null>(null);\n React.useLayoutEffect(() => {\n const l = list.current;\n const p = pill.current;\n if (!l || !p) return;\n const place = (animate: boolean) => {\n const a = l.querySelector<HTMLElement>(selector);\n if (!a) {\n p.style.opacity = \"0\";\n return;\n }\n const { offsetLeft: x, offsetTop: y, offsetWidth: w, offsetHeight: h } = a;\n Object.assign(p.style, { opacity: \"1\", left: `${x}px`, top: `${y}px`, width: `${w}px`, height: `${h}px` });\n const prev = last.current;\n last.current = { x, w };\n if (!animate || !prev || (prev.x === x && prev.w === w) || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n p.style.transition = \"none\";\n p.style.transform = `translateX(${prev.x - x}px) scaleX(${prev.w / w})`;\n void p.offsetWidth;\n p.style.transition = \"\";\n p.style.transform = \"\";\n };\n place(true);\n const ro = new ResizeObserver(() => place(false));\n ro.observe(l);\n return () => ro.disconnect();\n }, [list, selector, active]);\n return pill;\n}\n\n/** The pill itself: a raised key that slides between options. */\nexport const pillClass =\n \"pointer-events-none absolute left-0 top-0 origin-left rounded-[var(--pill-radius)] bg-key-face opacity-0 shadow-key transition-transform duration-200 ease-[var(--ease-out)]\";\n\nexport interface SegmentedProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n options: { value: string; label: React.ReactNode; disabled?: boolean }[];\n size?: \"sm\" | \"md\";\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** A single-choice control: options sit on a tray and a raised pill slides to the chosen one. */\nexport function Segmented({ value, defaultValue, onValueChange, options, size = \"md\", className, ...rest }: SegmentedProps) {\n const [inner, setInner] = React.useState(defaultValue ?? options[0]?.value);\n const current = value ?? inner;\n const list = React.useRef<HTMLDivElement>(null);\n const pill = useSlidingPill(list, '[data-state=\"on\"]', current);\n return (\n <ToggleGroup.Root\n ref={list}\n type=\"single\"\n value={current}\n onValueChange={(v) => {\n if (!v) return;\n setInner(v);\n onValueChange?.(v);\n }}\n className={cn(\"vx-glass relative inline-flex w-fit items-center gap-0.5 rounded-input border border-tray-border bg-tray p-1\", className)}\n {...rest}\n >\n <span ref={pill} aria-hidden className={pillClass} />\n {options.map((o) => (\n <ToggleGroup.Item\n key={o.value}\n value={o.value}\n disabled={o.disabled}\n className={cn(\n \"relative z-10 inline-flex items-center justify-center gap-1.5 rounded-[var(--pill-radius)] font-medium text-muted\",\n \"transition-colors duration-[var(--dur-micro)] hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:opacity-50 [&_svg]:size-4\",\n \"data-[state=on]:text-ink\",\n size === \"md\" ? \"h-7 px-3 text-sm\" : \"h-6 px-2 text-xs\",\n )}\n >\n {o.label}\n </ToggleGroup.Item>\n ))}\n </ToggleGroup.Root>\n );\n}\n"},{"slug":"dropdown-menu","name":"Dropdown menu","category":"Actions","description":"A menu of actions with shortcuts, checkable items and nested submenus.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<DropdownMenu>\n <DropdownMenuTrigger asChild><Button>Open</Button></DropdownMenuTrigger>\n <DropdownMenuContent>\n <DropdownMenuItem shortcut=\"⌘E\">Rename</DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>","props":[["DropdownMenuItem.shortcut","string","","Key hint shown on the right."],["DropdownMenuItem.destructive","boolean","false","Danger colouring for irreversible actions."],["DropdownMenuContent.align","\"start\" | \"center\" | \"end\"","\"center\"","Alignment against the trigger."]],"pro":false,"source":"import * as React from \"react\";\nimport { DropdownMenu as Menu } from \"radix-ui\";\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\nexport const DropdownMenu = Menu.Root;\nexport const DropdownMenuTrigger = Menu.Trigger;\nexport const DropdownMenuGroup = Menu.Group;\nexport const DropdownMenuSub = Menu.Sub;\nexport const DropdownMenuRadioGroup = Menu.RadioGroup;\n\nconst panel = menuPanel;\nconst item = menuItem;\n\nexport function DropdownMenuContent({ className, sideOffset = 6, ...props }: React.ComponentProps<typeof Menu.Content>) {\n return (\n <Menu.Portal>\n <Menu.Content sideOffset={sideOffset} className={cn(panel, className)} {...props} />\n </Menu.Portal>\n );\n}\n\nexport function DropdownMenuItem({\n className,\n shortcut,\n destructive,\n children,\n ...props\n}: React.ComponentProps<typeof Menu.Item> & { shortcut?: string; destructive?: boolean }) {\n return (\n <Menu.Item\n className={cn(item, destructive && \"text-danger data-[highlighted]:bg-danger-wash [&_svg]:text-danger\", className)}\n {...props}\n >\n {children}\n {shortcut && <span className=\"ml-auto font-mono text-xs text-muted\">{shortcut}</span>}\n </Menu.Item>\n );\n}\n\nexport function DropdownMenuCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof Menu.CheckboxItem>) {\n return (\n <Menu.CheckboxItem className={cn(item, \"pl-9\", className)} {...props}>\n <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n <Menu.ItemIndicator>\n <Check className=\"!text-accent\" />\n </Menu.ItemIndicator>\n </span>\n {children}\n </Menu.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof Menu.RadioItem>) {\n return (\n <Menu.RadioItem className={cn(item, \"pl-9\", className)} {...props}>\n <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n <Menu.ItemIndicator>\n <span className=\"block size-1.5 rounded-full bg-accent\" />\n </Menu.ItemIndicator>\n </span>\n {children}\n </Menu.RadioItem>\n );\n}\n\nexport function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof Menu.Label>) {\n return <Menu.Label className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof Menu.Separator>) {\n return <Menu.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n\nexport function DropdownMenuSubTrigger({ className, children, ...props }: React.ComponentProps<typeof Menu.SubTrigger>) {\n return (\n <Menu.SubTrigger className={cn(item, \"data-[state=open]:bg-paper-3\", className)} {...props}>\n {children}\n <ChevronRight className=\"ml-auto\" />\n </Menu.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof Menu.SubContent>) {\n return (\n <Menu.Portal>\n <Menu.SubContent className={cn(panel, className)} {...props} />\n </Menu.Portal>\n );\n}\n"},{"slug":"input","name":"Input","category":"Forms","description":"A text field with optional leading and trailing slots. Pair it with Field for label, hint and error.","deps":[],"uses":[],"usage":"<Field label=\"Email\" htmlFor=\"email\" hint=\"We never share it.\">\n <Input id=\"email\" leading={<AtSign />} />\n</Field>","props":[["leading","ReactNode","","Icon or text before the value."],["trailing","ReactNode","","Icon, key hint or button after the value."],["inputSize","\"sm\" | \"md\" | \"lg\"","\"md\"","Field height."],["aria-invalid","boolean","","Switches to the error style."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const fieldBase = [\n \"vx-glass w-full rounded-input border border-field-border bg-field text-sm text-ink shadow-field\",\n \"placeholder:text-muted/80 outline-none\",\n \"transition-[border-color,box-shadow] duration-[var(--dur-micro)]\",\n \"hover:field-hover\",\n \"focus-visible:field-focus focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:bg-paper-2 disabled:opacity-60\",\n \"aria-invalid:border-danger aria-invalid:focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--danger)_22%,transparent)]\",\n].join(\" \");\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n /** Icon or text placed before the value. */\n leading?: React.ReactNode;\n /** Icon, text or a button placed after the value. */\n trailing?: React.ReactNode;\n inputSize?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst sizes = { sm: \"h-8 px-2.5\", md: \"h-9 px-3\", lg: \"h-11 px-3.5 text-base\" };\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, leading, trailing, inputSize = \"md\", ...props }, ref) => {\n if (!leading && !trailing) {\n return <input ref={ref} className={cn(fieldBase, sizes[inputSize], className)} {...props} />;\n }\n return (\n <div className={cn(\"relative flex items-center\", className)}>\n {leading && (\n <span className=\"pointer-events-none absolute left-3 flex items-center text-muted [&_svg]:size-4\">\n {leading}\n </span>\n )}\n <input\n ref={ref}\n className={cn(fieldBase, sizes[inputSize], leading && \"pl-9\", trailing && \"pr-10\")}\n {...props}\n />\n {trailing && (\n <span className=\"absolute right-2 flex items-center text-muted [&_svg]:size-4\">{trailing}</span>\n )}\n </div>\n );\n },\n);\nInput.displayName = \"Input\";\n"},{"slug":"textarea","name":"Textarea","category":"Forms","description":"A multi-line field that shares the input's border, focus ring and error states.","deps":[],"uses":["input"],"usage":"<Textarea placeholder=\"Write a message\" />","props":[["...props","TextareaHTMLAttributes","","Every native textarea attribute."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { fieldBase } from \"./input\";\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(\n ({ className, ...props }, ref) => (\n <textarea ref={ref} className={cn(fieldBase, \"min-h-24 resize-y px-3 py-2 leading-6\", className)} {...props} />\n ),\n);\nTextarea.displayName = \"Textarea\";\n"},{"slug":"select","name":"Select","category":"Forms","description":"A listbox for picking one value, with groups, labels and disabled options.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Select defaultValue=\"fra\">\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"fra\">Frankfurt</SelectItem>\n </SelectContent>\n</Select>","props":[["value / defaultValue","string","","Controlled or initial value."],["onValueChange","(value: string) => void","","Called on selection."],["SelectItem.disabled","boolean","false","Makes an option unselectable."]],"pro":false,"source":"import * as React from \"react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\nexport const Select = SelectPrimitive.Root;\nexport const SelectGroup = SelectPrimitive.Group;\nexport const SelectValue = SelectPrimitive.Value;\n\nexport function SelectTrigger({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger>) {\n return (\n <SelectPrimitive.Trigger\n className={cn(\n \"flex h-9 w-full items-center justify-between gap-2 rounded-input border border-field-border bg-field px-3 text-sm text-ink shadow-field vx-glass\",\n \"hover:field-hover focus-visible:field-focus focus-visible:outline-none\",\n \"data-[placeholder]:text-muted disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate\",\n className,\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronsUpDown className=\"size-4 shrink-0 text-muted\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n}\n\nexport function SelectContent({\n className,\n children,\n position = \"popper\",\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n position={position}\n sideOffset={6}\n className={cn(\n menuPanel, \"relative max-h-[min(var(--radix-select-content-available-height),340px)] min-w-[8rem]\",\n position === \"popper\" && \"w-[var(--radix-select-trigger-width)]\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n}\n\nexport function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {\n return <SelectPrimitive.Label className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n className={cn(\n menuItem, \"w-full pr-9\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n <span className=\"absolute right-2.5 flex size-4 items-center justify-center [&_svg]:!text-accent\">\n <SelectPrimitive.ItemIndicator>\n <Check className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n );\n}\n\nexport function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n return <SelectPrimitive.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n"},{"slug":"checkbox","name":"Checkbox","category":"Forms","description":"A binary or indeterminate choice, for lists where many items can be on.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Checkbox id=\"terms\" />\n<Label htmlFor=\"terms\">Accept terms</Label>","props":[["checked","boolean | \"indeterminate\"","","Controlled state."],["onCheckedChange","(checked) => void","","Called on toggle."],["disabled","boolean","false","Blocks interaction."]],"pro":false,"source":"import * as React from \"react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(\n \"peer grid size-4 shrink-0 place-items-center rounded-[calc(4px*var(--radius-factor))] border border-[color:var(--control-border)] bg-[color:var(--control)] shadow-[var(--elev-control)]\",\n \"transition-[background-color,border-color,box-shadow] duration-[var(--dur-micro)] hover:border-muted\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:border-transparent data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)] data-[state=checked]:text-accent-ink\",\n \"data-[state=indeterminate]:border-transparent data-[state=indeterminate]:bg-accent data-[state=indeterminate]:[background-image:var(--control-on-image)] data-[state=indeterminate]:shadow-[var(--elev-control-on)] data-[state=indeterminate]:text-accent-ink\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator className=\"grid place-items-center\">\n {props.checked === \"indeterminate\" ? (\n <Minus className=\"size-3\" strokeWidth={3} />\n ) : (\n <Check className=\"size-3\" strokeWidth={3.5} />\n )}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = \"Checkbox\";\n"},{"slug":"radio-group","name":"Radio group","category":"Forms","description":"One choice from a visible set. RadioCard wraps each option in a selectable panel.","deps":["radix-ui"],"uses":[],"usage":"<RadioGroup defaultValue=\"team\">\n <RadioCard value=\"team\" title=\"Team\" description=\"Shared projects.\" />\n</RadioGroup>","props":[["value / defaultValue","string","","Selected option."],["RadioCard.title","ReactNode","required","Option title."],["RadioCard.description","ReactNode","","Supporting line."]],"pro":false,"source":"import * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return <RadioGroupPrimitive.Root className={cn(\"grid gap-2.5\", className)} {...props} />;\n}\n\nexport function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n className={cn(\n \"peer grid size-4 shrink-0 place-items-center rounded-full border border-[color:var(--control-border)] bg-[color:var(--control)] shadow-[var(--elev-control)]\",\n \"transition-[background-color,border-color,box-shadow] duration-[var(--dur-micro)] hover:border-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:border-transparent data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)]\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator className=\"block size-1.5 rounded-full bg-accent-ink\" />\n </RadioGroupPrimitive.Item>\n );\n}\n\n/** A bordered, selectable card that wraps a radio. */\nexport function RadioCard({\n value,\n title,\n description,\n className,\n}: {\n value: string;\n title: React.ReactNode;\n description?: React.ReactNode;\n className?: string;\n}) {\n return (\n <label\n className={cn(\n \"vx-glass flex cursor-pointer items-start gap-3 rounded-card border border-card-border bg-card p-3.5 shadow-card\",\n \"transition-[border-color,background-color] duration-[var(--dur-micro)] hover:border-rule-2\",\n \"has-[[data-state=checked]]:border-accent has-[[data-state=checked]]:bg-accent-wash/50\",\n className,\n )}\n >\n <RadioGroupItem value={value} className=\"mt-0.5\" />\n <span className=\"grid gap-0.5\">\n <span className=\"text-sm font-medium text-ink\">{title}</span>\n {description && <span className=\"text-sm text-muted\">{description}</span>}\n </span>\n </label>\n );\n}\n"},{"slug":"switch","name":"Switch","category":"Forms","description":"An on and off setting that applies immediately.","deps":["radix-ui"],"uses":[],"usage":"<Switch id=\"wifi\" defaultChecked />","props":[["checked / defaultChecked","boolean","","State."],["onCheckedChange","(checked: boolean) => void","","Called on toggle."],["size","\"sm\" | \"md\"","\"md\"","Track size."]],"pro":false,"source":"import * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Switch = React.forwardRef<\n React.ElementRef<typeof SwitchPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root> & { size?: \"sm\" | \"md\" }\n>(({ className, size = \"md\", ...props }, ref) => (\n <SwitchPrimitive.Root\n ref={ref}\n className={cn(\n \"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent p-0.5\",\n \"bg-[color:var(--track-off)] shadow-[inset_0_1px_2px_oklch(0%_0_0/calc(var(--sh-a)*1.2))] transition-[background-color,box-shadow] duration-[var(--dur-short)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n \"data-[state=checked]:bg-accent data-[state=checked]:[background-image:var(--control-on-image)] data-[state=checked]:shadow-[var(--elev-control-on)] disabled:cursor-not-allowed disabled:opacity-50\",\n size === \"md\" ? \"h-6 w-[42px]\" : \"h-4 w-7\",\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n className={cn(\n \"pointer-events-none grid place-items-center rounded-full bg-key-face [background-image:var(--thumb-image)] shadow-[var(--elev-thumb)]\",\n \"transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)] data-[state=unchecked]:translate-x-0\",\n size === \"md\" ? \"size-[18px] data-[state=checked]:translate-x-[18px]\" : \"size-2.5 data-[state=checked]:translate-x-3\",\n )}\n />\n </SwitchPrimitive.Root>\n));\nSwitch.displayName = \"Switch\";\n"},{"slug":"slider","name":"Slider","category":"Forms","description":"Pick a value or a range by dragging. Pass two values for a range.","deps":["radix-ui"],"uses":[],"usage":"<Slider defaultValue={[40]} max={100} step={1} />","props":[["value / defaultValue","number[]","","One value, or two for a range."],["min / max / step","number","0 / 100 / 1","Bounds and increment."],["onValueChange","(value: number[]) => void","","Called while dragging."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Slider({ className, defaultValue, value, ...props }: React.ComponentProps<typeof SliderPrimitive.Root>) {\n const count = (value ?? defaultValue ?? [0]).length;\n return (\n <SliderPrimitive.Root\n defaultValue={defaultValue}\n value={value}\n className={cn(\"relative flex w-full touch-none select-none items-center py-2 data-[disabled]:opacity-50\", className)}\n {...props}\n >\n <SliderPrimitive.Track className=\"relative h-1.5 w-full grow overflow-hidden rounded-full bg-[color:var(--track-off)] shadow-[inset_0_1px_1px_oklch(0%_0_0/0.06)]\">\n <SliderPrimitive.Range className=\"absolute h-full rounded-full bg-accent [background-image:var(--control-on-image)] shadow-[inset_0_1px_0_oklch(100%_0_0/0.24)]\" />\n </SliderPrimitive.Track>\n {Array.from({ length: count }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={props[\"aria-label\"] ?? \"Value\"}\n className={cn(\n \"block size-5 cursor-grab rounded-full border border-[color:var(--key-border)] bg-key-face [background-image:var(--thumb-image)] shadow-[0_2px_4px_oklch(0%_0_0/0.1),inset_0_1px_0_oklch(100%_0_0/0.8)]\",\n \"transition-transform duration-[var(--dur-micro)] hover:scale-110 active:cursor-grabbing\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n )}\n />\n ))}\n </SliderPrimitive.Root>\n );\n}\n"},{"slug":"dialog","name":"Dialog","category":"Overlays","description":"A modal window that traps focus and returns it to the trigger when closed.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Dialog>\n <DialogTrigger asChild><Button>Open</Button></DialogTrigger>\n <DialogContent>\n <DialogTitle>Title</DialogTitle>\n </DialogContent>\n</Dialog>","props":[["open / onOpenChange","boolean / (open) => void","","Controlled state."],["DialogContent.hideClose","boolean","false","Hide the corner close button."]],"pro":false,"source":"import * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport function DialogContent({\n className,\n children,\n hideClose,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & { hideClose?: boolean }) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <DialogPrimitive.Content\n className={cn(\n \"vx-dialog fixed left-1/2 top-1/2 z-[400] grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4\",\n \"vx-glass rounded-panel border border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\",\n className,\n )}\n {...props}\n >\n {children}\n {!hideClose && (\n <DialogPrimitive.Close\n aria-label=\"Close\"\n className=\"absolute right-4 top-4 grid size-7 place-items-center rounded-input text-muted hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n}\n\nexport function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5 pr-8\", className)} {...props} />;\n}\nexport function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return <DialogPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return <DialogPrimitive.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n"},{"slug":"popover","name":"Popover","category":"Overlays","description":"Rich, interactive content anchored to a trigger.","deps":["radix-ui"],"uses":[],"usage":"<Popover>\n <PopoverTrigger asChild><Button>Open</Button></PopoverTrigger>\n <PopoverContent>…</PopoverContent>\n</Popover>","props":[["PopoverContent.align","\"start\" | \"center\" | \"end\"","\"center\"","Alignment."],["PopoverContent.sideOffset","number","8","Gap from the trigger in px."]],"pro":false,"source":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Popover = PopoverPrimitive.Root;\nexport const PopoverTrigger = PopoverPrimitive.Trigger;\nexport const PopoverClose = PopoverPrimitive.Close;\n\nexport function PopoverContent({ className, align = \"center\", sideOffset = 8, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align={align}\n sideOffset={sideOffset}\n className={cn(\"vx-pop z-[100] w-72 vx-glass rounded-card border border-pop-border bg-pop p-4 text-ink shadow-pop outline-none\", className)}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n}\n"},{"slug":"tooltip","name":"Tooltip","category":"Overlays","description":"A short label on hover or focus, with an optional key hint. Wrap your app in TooltipProvider.","deps":["radix-ui"],"uses":[],"usage":"<Tooltip content=\"Bold\" shortcut=\"⌘B\">\n <Button size=\"icon\">B</Button>\n</Tooltip>","props":[["content","ReactNode","required","The label."],["shortcut","ReactNode","","Key hint after the label."],["side","\"top\" | \"right\" | \"bottom\" | \"left\"","\"top\"","Preferred side."]],"pro":false,"source":"import * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const TooltipProvider = TooltipPrimitive.Provider;\n\nexport interface TooltipProps {\n content: React.ReactNode;\n children: React.ReactNode;\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n shortcut?: React.ReactNode;\n className?: string;\n}\n\nexport function Tooltip({ content, children, side = \"top\", shortcut, className }: TooltipProps) {\n return (\n <TooltipPrimitive.Root>\n <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n side={side}\n sideOffset={6}\n className={cn(\n \"vx-pop z-[600] flex items-center gap-2 rounded-input bg-graphite px-2 py-1 text-xs font-medium text-graphite-ink shadow-pop\",\n className,\n )}\n >\n {content}\n {shortcut && <span className=\"font-mono text-graphite-muted\">{shortcut}</span>}\n <TooltipPrimitive.Arrow className=\"fill-graphite\" />\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n </TooltipPrimitive.Root>\n );\n}\n"},{"slug":"toast","name":"Toast","category":"Feedback","description":"Brief, non-blocking messages. Mount Toaster once and call toast() from anywhere.","deps":["lucide-react"],"uses":[],"usage":"toast(\"Saved\")\ntoast.success(\"Deployed\", \"acme-web is live.\")\ntoast({ title: \"Archived\", action: { label: \"Undo\", onClick } })","props":[["title","string","required","Main line."],["description","string","","Second line."],["tone","\"default\" | \"success\" | \"danger\"","\"default\"","Icon and colour."],["action","{ label: string; onClick: () => void }","","One inline action."],["duration","number","4000","Milliseconds before it leaves."]],"pro":false,"source":"import * as React from \"react\";\nimport { CircleAlert, CircleCheck, Info, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Tone = \"default\" | \"success\" | \"danger\";\nexport interface ToastItem {\n id: number;\n title: string;\n description?: string;\n tone?: Tone;\n action?: { label: string; onClick: () => void };\n duration?: number;\n}\n\nlet items: ToastItem[] = [];\nlet nextId = 1;\nconst listeners = new Set<() => void>();\nconst emit = () => listeners.forEach((l) => l());\n\nexport function dismissToast(id: number) {\n items = items.filter((t) => t.id !== id);\n emit();\n}\n\n/** Show a toast from anywhere: toast(\"Saved\") or toast({ title, description, tone, action }). */\nexport function toast(input: string | Omit<ToastItem, \"id\">) {\n const t: ToastItem = typeof input === \"string\" ? { id: nextId++, title: input } : { id: nextId++, ...input };\n items = [...items, t].slice(-4);\n emit();\n window.setTimeout(() => dismissToast(t.id), t.duration ?? 4000);\n return t.id;\n}\ntoast.success = (title: string, description?: string) => toast({ title, description, tone: \"success\" });\ntoast.error = (title: string, description?: string) => toast({ title, description, tone: \"danger\" });\n\nconst icons = { default: Info, success: CircleCheck, danger: CircleAlert };\nconst iconTone = { default: \"text-accent\", success: \"text-success\", danger: \"text-danger\" };\n\n/** Mount once near the root of your app. */\nexport function Toaster({ className }: { className?: string }) {\n const list = React.useSyncExternalStore(\n (cb) => {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n () => items,\n () => items,\n );\n return (\n <ol\n aria-live=\"polite\"\n className={cn(\"pointer-events-none fixed bottom-4 right-4 z-[500] flex w-[min(360px,calc(100vw-2rem))] flex-col gap-2\", className)}\n >\n {list.map((t) => {\n const Icon = icons[t.tone ?? \"default\"];\n return (\n <li\n key={t.id}\n className=\"vx-toast pointer-events-auto flex items-start gap-3 rounded-card border border-pop-border bg-pop p-3.5 text-sm text-ink shadow-pop vx-glass\"\n >\n <Icon className={cn(\"mt-0.5 size-4 shrink-0\", iconTone[t.tone ?? \"default\"])} />\n <div className=\"grid flex-1 gap-0.5\">\n <p className=\"font-medium\">{t.title}</p>\n {t.description && <p className=\"text-muted\">{t.description}</p>}\n </div>\n {t.action && (\n <button\n onClick={() => {\n t.action!.onClick();\n dismissToast(t.id);\n }}\n className=\"rounded-sm px-1.5 py-0.5 text-sm font-medium text-accent hover:bg-accent-wash focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {t.action.label}\n </button>\n )}\n <button\n onClick={() => dismissToast(t.id)}\n aria-label=\"Dismiss\"\n className=\"-m-1 grid size-6 place-items-center rounded-sm text-muted hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </button>\n </li>\n );\n })}\n </ol>\n );\n}\n"},{"slug":"alert","name":"Alert","category":"Feedback","description":"An inline message about the state of a page or section.","deps":["class-variance-authority","lucide-react"],"uses":[],"usage":"<Alert variant=\"warning\" title=\"Your card expires soon\" />","props":[["variant","\"info\" | \"success\" | \"warning\" | \"danger\"","\"info\"","Tone and icon."],["title","ReactNode","","Bold first line."],["action","ReactNode","","Buttons under the message."],["onDismiss","() => void","","Shows a close button when set."]],"pro":false,"source":"import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CircleAlert, CircleCheck, Info, TriangleAlert, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nconst alertVariants = cva(\"relative flex gap-3 rounded-card border p-3.5 text-sm\", {\n variants: {\n variant: {\n info: \"border-rule bg-paper-2 text-ink [&>svg]:text-accent\",\n success: \"border-transparent bg-success-wash text-success-wash-ink\",\n warning: \"border-transparent bg-warning-wash text-warning-wash-ink\",\n danger: \"border-transparent bg-danger-wash text-danger-wash-ink\",\n },\n },\n defaultVariants: { variant: \"info\" },\n});\n\nconst icons = { info: Info, success: CircleCheck, warning: TriangleAlert, danger: CircleAlert };\n\nexport interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">, VariantProps<typeof alertVariants> {\n title?: React.ReactNode;\n action?: React.ReactNode;\n onDismiss?: () => void;\n}\n\nexport function Alert({ className, variant = \"info\", title, action, onDismiss, children, ...props }: AlertProps) {\n const Icon = icons[variant ?? \"info\"];\n return (\n <div role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props}>\n <Icon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"grid flex-1 gap-1\">\n {title && <p className=\"font-medium\">{title}</p>}\n {children && <div className=\"opacity-90\">{children}</div>}\n {action && <div className=\"mt-1.5 flex gap-2\">{action}</div>}\n </div>\n {onDismiss && (\n <button\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className=\"-m-1 grid size-6 place-items-center rounded-sm opacity-70 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n </div>\n );\n}\n"},{"slug":"progress","name":"Progress","category":"Feedback","description":"Linear progress for a task, and Meter, a cell meter for quotas.","deps":["radix-ui"],"uses":[],"usage":"<Progress value={64} />\n<Meter value={72} cells={24} />","props":[["value","number","0","Percent complete."],["tone","\"accent\" | \"success\" | \"danger\"","\"accent\"","Fill colour."],["Meter.cells","number","24","Number of cells."]],"pro":false,"source":"import * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Progress({\n value = 0,\n className,\n tone = \"accent\",\n ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root> & { tone?: \"accent\" | \"success\" | \"danger\" }) {\n const fill = { accent: \"bg-accent [background-image:var(--control-on-image)]\", success: \"bg-success\", danger: \"bg-danger\" }[tone];\n const v = Math.min(100, Math.max(0, value ?? 0));\n return (\n <ProgressPrimitive.Root\n value={v}\n className={cn(\"relative h-2 w-full overflow-hidden rounded-full bg-[color:var(--track-off)] shadow-[inset_0_1px_1px_oklch(0%_0_0/0.06)]\", className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n className={cn(\"h-full w-full rounded-full transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)]\", fill)}\n style={{ transform: `translateX(-${100 - v}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\n/** A meter of discrete cells, useful for quotas and usage. */\nexport function Meter({ value, cells = 24, className, label }: { value: number; cells?: number; className?: string; label?: string }) {\n const on = Math.round((value / 100) * cells);\n return (\n <div role=\"meter\" aria-valuenow={value} aria-valuemin={0} aria-valuemax={100} aria-label={label} className={cn(\"flex gap-[3px]\", className)}>\n {Array.from({ length: cells }).map((_, i) => (\n <span key={i} className={cn(\"h-4 flex-1 rounded-[2px]\", i < on ? \"bg-accent\" : \"bg-paper-3\")} />\n ))}\n </div>\n );\n}\n"},{"slug":"skeleton","name":"Skeleton","category":"Feedback","description":"A shimmering placeholder that holds the shape of content while it loads.","deps":[],"uses":[],"usage":"<Skeleton className=\"h-4 w-48\" />","props":[["className","string","","Set width, height and radius."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div aria-hidden className={cn(\"vx-shimmer rounded-input bg-paper-3\", className)} {...props} />;\n}\n"},{"slug":"spinner","name":"Spinner","category":"Feedback","description":"An indeterminate loading indicator that inherits the text colour.","deps":[],"uses":[],"usage":"<Spinner className=\"size-4\" />","props":[["label","string","\"Loading\"","Accessible name."],["className","string","","Size and colour."]],"pro":false,"source":"import { cn } from \"@/lib/cn\";\n\nexport function Spinner({ className, label = \"Loading\" }: { className?: string; label?: string }) {\n return (\n <svg\n role=\"status\"\n aria-label={label}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n className={cn(\"vx-spin size-5 shrink-0\", className)}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"3\" />\n <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n"},{"slug":"card","name":"Card","category":"Display","description":"A hairline-bordered surface with header, content and a footer strip.","deps":[],"uses":[],"usage":"<Card>\n <CardHeader><CardTitle>Title</CardTitle></CardHeader>\n <CardContent>…</CardContent>\n</Card>","props":[["className","string","","Every part accepts className and native div props."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"vx-glass rounded-panel border border-card-border bg-card text-ink shadow-card\", className)}\n {...props}\n />\n );\n}\nexport function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1 p-5 pb-0\", className)} {...props} />;\n}\nexport function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {\n return <h3 className={cn(\"text-base font-medium tracking-tight\", className)} {...props} />;\n}\nexport function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {\n return <p className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\nexport function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"p-5\", className)} {...props} />;\n}\nexport function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn(\"flex items-center gap-2 border-t border-rule px-5 py-3\", className)}\n {...props}\n />\n );\n}\n\n/** A raised inner panel, for the shell-and-panel look of the soft style. */\nexport function CardInset({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"rounded-card bg-inset p-4\", className)} {...props} />;\n}\n"},{"slug":"avatar","name":"Avatar","category":"Display","description":"An image with initials as fallback, a presence dot, and an overlapping group.","deps":["radix-ui"],"uses":[],"usage":"<Avatar src=\"/ana.jpg\" fallback=\"AK\" status=\"online\" />","props":[["fallback","string","required","Initials when there is no image."],["size","\"xs\" | \"sm\" | \"md\" | \"lg\"","\"md\"","Diameter."],["status","\"online\" | \"away\" | \"offline\"","","Presence dot."],["AvatarGroup.max","number","","Collapse the rest into a count."]],"pro":false,"source":"import * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nconst sizes = { xs: \"size-6 text-[10px]\", sm: \"size-8 text-xs\", md: \"size-10 text-sm\", lg: \"size-12 text-base\" };\n\nexport interface AvatarProps extends React.ComponentProps<typeof AvatarPrimitive.Root> {\n src?: string;\n alt?: string;\n /** Initials shown while the image loads or when it fails. */\n fallback: string;\n size?: keyof typeof sizes;\n status?: \"online\" | \"away\" | \"offline\";\n}\n\nconst statusTone = { online: \"bg-success\", away: \"bg-warning\", offline: \"bg-rule-2\" };\n\nexport function Avatar({ src, alt, fallback, size = \"md\", status, className, ...props }: AvatarProps) {\n return (\n <span className=\"relative inline-flex shrink-0\">\n <AvatarPrimitive.Root\n className={cn(\n \"relative inline-flex shrink-0 overflow-hidden rounded-full border border-rule bg-paper-3 ring-2 ring-paper\",\n sizes[size],\n className,\n )}\n {...props}\n >\n {src && <AvatarPrimitive.Image src={src} alt={alt ?? fallback} className=\"size-full object-cover\" />}\n <AvatarPrimitive.Fallback className=\"flex size-full items-center justify-center font-medium text-ink-2\">\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n {status && (\n <span\n role=\"img\"\n aria-label={status}\n className={cn(\"absolute bottom-0 right-0 size-2.5 rounded-full ring-2 ring-paper\", statusTone[status])}\n />\n )}\n </span>\n );\n}\n\n/** Overlapping stack of avatars with an optional overflow count. */\nexport function AvatarGroup({ children, max, className }: { children: React.ReactNode; max?: number; className?: string }) {\n const items = React.Children.toArray(children);\n const shown = max ? items.slice(0, max) : items;\n const extra = items.length - shown.length;\n return (\n <div className={cn(\"flex items-center -space-x-2\", className)}>\n {shown}\n {extra > 0 && (\n <span className=\"relative inline-flex size-8 items-center justify-center rounded-full border border-rule bg-paper-2 text-xs font-medium text-ink-2 ring-2 ring-paper\">\n +{extra}\n </span>\n )}\n </div>\n );\n}\n"},{"slug":"badge","name":"Badge","category":"Display","description":"A compact label for status, version or category.","deps":["class-variance-authority"],"uses":[],"usage":"<Badge variant=\"success\">Deployed</Badge>","props":[["variant","\"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"solid\" | \"outline\"","\"neutral\"","Tone."]],"pro":false,"source":"import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const badgeVariants = cva(\n \"inline-flex h-[18px] items-center gap-1 whitespace-nowrap rounded-[calc(4px*var(--radius-factor))] border px-1 text-xs font-semibold leading-4 [&_svg]:size-3\",\n {\n variants: {\n variant: {\n neutral: \"border-transparent bg-paper-3 text-muted\",\n accent: \"border-transparent bg-accent-wash text-accent-wash-ink\",\n success: \"border-transparent bg-success-wash text-success-wash-ink\",\n warning: \"border-transparent bg-warning-wash text-warning-wash-ink\",\n danger: \"border-transparent bg-danger-wash text-danger-wash-ink\",\n solid: \"border-transparent bg-accent text-accent-ink\",\n outline: \"border-rule-2 text-ink-2\",\n },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {}\n\nexport function Badge({ className, variant, ...props }: BadgeProps) {\n return <span className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n"},{"slug":"kbd","name":"Kbd","category":"Display","description":"A keyboard key, for shortcuts in menus, tooltips and docs.","deps":[],"uses":[],"usage":"<Kbd>⌘</Kbd><Kbd>K</Kbd>","props":[["children","ReactNode","","The key label."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Kbd({ className, ...props }: React.HTMLAttributes<HTMLElement>) {\n return (\n <kbd\n className={cn(\n \"inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-rule bg-key-face px-1 font-mono text-[11px] font-medium text-ink-2 shadow-[0_1px_0_0_var(--key-edge)]\",\n className,\n )}\n {...props}\n />\n );\n}\n"},{"slug":"separator","name":"Separator","category":"Display","description":"A hairline between groups of content, horizontal or vertical.","deps":["radix-ui"],"uses":[],"usage":"<Separator />\n<Separator orientation=\"vertical\" />","props":[["orientation","\"horizontal\" | \"vertical\"","\"horizontal\"","Direction."],["decorative","boolean","true","Hide from assistive tech."]],"pro":false,"source":"import * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n return (\n <SeparatorPrimitive.Root\n decorative={decorative}\n orientation={orientation}\n className={cn(\"shrink-0 bg-rule\", orientation === \"horizontal\" ? \"h-px w-full\" : \"h-full w-px\", className)}\n {...props}\n />\n );\n}\n"},{"slug":"tabs","name":"Tabs","category":"Navigation","description":"Switch between views in place. Two variants: a tray with a sliding pill, and a line.","deps":["radix-ui"],"uses":["segmented"],"usage":"<Tabs defaultValue=\"a\">\n <TabsList variant=\"line\">\n <TabsTrigger value=\"a\">Overview</TabsTrigger>\n </TabsList>\n <TabsContent value=\"a\">…</TabsContent>\n</Tabs>","props":[["value / defaultValue","string","","Active tab."],["TabsList.variant","\"tray\" | \"line\"","\"tray\"","Visual style."]],"pro":false,"source":"import * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { pillClass, useSlidingPill } from \"./segmented\";\n\ntype Variant = \"tray\" | \"line\";\nconst VariantContext = React.createContext<Variant>(\"tray\");\n\nexport function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {\n // minmax(0, 1fr) keeps wide panels from stretching the page on small screens.\n return <TabsPrimitive.Root className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-4\", className)} {...props} />;\n}\n\nexport function TabsList({\n className,\n variant = \"tray\",\n children,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> & { variant?: Variant }) {\n const list = React.useRef<HTMLDivElement>(null);\n // Re-measure when the active tab changes; Radix flips data-state on the triggers.\n const [active, setActive] = React.useState<string | null>(null);\n React.useEffect(() => {\n const l = list.current;\n if (!l) return;\n const read = () => setActive(l.querySelector('[data-state=\"active\"]')?.id ?? null);\n read();\n const mo = new MutationObserver(read);\n mo.observe(l, { subtree: true, attributes: true, attributeFilter: [\"data-state\"] });\n return () => mo.disconnect();\n }, []);\n const pill = useSlidingPill(list, '[data-state=\"active\"]', active);\n return (\n <VariantContext.Provider value={variant}>\n <TabsPrimitive.List\n ref={list}\n className={cn(\n \"relative inline-flex w-fit max-w-full items-center overflow-x-auto [scrollbar-width:none]\",\n variant === \"tray\"\n ? \"vx-glass gap-0.5 rounded-input border border-tray-border bg-tray p-1\"\n : \"gap-5 border-b border-rule\",\n className,\n )}\n {...props}\n >\n {variant === \"tray\" && <span ref={pill} aria-hidden className={pillClass} />}\n {children}\n </TabsPrimitive.List>\n </VariantContext.Provider>\n );\n}\n\nexport function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n const variant = React.useContext(VariantContext);\n return (\n <TabsPrimitive.Trigger\n className={cn(\n \"inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium text-muted\",\n \"transition-[color,background-color,box-shadow] duration-[var(--dur-micro)] hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:opacity-50 [&_svg]:size-4\",\n variant === \"tray\"\n ? \"relative z-10 h-7 rounded-[var(--pill-radius)] px-3 data-[state=active]:text-ink\"\n : \"-mb-px h-9 border-b-2 border-transparent px-0.5 data-[state=active]:border-accent data-[state=active]:text-ink\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return <TabsPrimitive.Content className={cn(\"outline-none\", className)} {...props} />;\n}\n"},{"slug":"accordion","name":"Accordion","category":"Navigation","description":"Stacked sections that expand one or many at a time.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Accordion type=\"single\" collapsible>\n <AccordionItem value=\"a\">\n <AccordionTrigger>Question</AccordionTrigger>\n <AccordionContent>Answer</AccordionContent>\n </AccordionItem>\n</Accordion>","props":[["type","\"single\" | \"multiple\"","required","How many can be open."],["collapsible","boolean","false","Allow closing the open item in single mode."]],"pro":false,"source":"import * as React from \"react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\nimport { Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Accordion = AccordionPrimitive.Root;\n\nexport function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n return <AccordionPrimitive.Item className={cn(\"border-b border-rule last:border-b-0\", className)} {...props} />;\n}\n\nexport function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n className={cn(\n \"group flex flex-1 items-center justify-between gap-4 py-4 text-left text-sm font-medium text-ink\",\n \"hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n className,\n )}\n {...props}\n >\n {children}\n <Plus className=\"size-4 shrink-0 text-muted transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)] group-data-[state=open]:rotate-45\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n}\n\nexport function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n return (\n <AccordionPrimitive.Content className=\"vx-reveal overflow-hidden text-sm text-muted\" {...props}>\n <div className={cn(\"pb-4 pr-8\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n );\n}\n"},{"slug":"combobox","name":"Combobox","category":"Forms","description":"A select with a search field, for option lists too long to scan.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Combobox options={[{ value: \"vite\", label: \"Vite\" }]} value={v} onValueChange={setV} />","props":[["options","{ value: string; label: string; description?: string }[]","required","Choices."],["value / onValueChange","string / (v) => void","","Controlled value."],["placeholder","string","\"Select…\"","Shown with nothing chosen."],["searchPlaceholder","string","\"Search…\"","Search field hint."]],"pro":false,"source":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\nimport { Check, ChevronsUpDown, Search } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuCheck, menuItem, menuLabel, menuPanel } from \"./menu-styles\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n description?: string;\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value?: string;\n onValueChange?: (value: string) => void;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** A select with a search field, for long option lists. */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Select…\",\n searchPlaceholder = \"Search…\",\n emptyText = \"Nothing found.\",\n className,\n ...rest\n}: ComboboxProps) {\n const [open, setOpen] = React.useState(false);\n const [inner, setInner] = React.useState(value);\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const current = value ?? inner;\n const selected = options.find((o) => o.value === current);\n const list = options.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()));\n\n const pick = (o: ComboboxOption) => {\n setInner(o.value);\n onValueChange?.(o.value);\n setOpen(false);\n setQ(\"\");\n };\n\n return (\n <PopoverPrimitive.Root open={open} onOpenChange={(o) => { setOpen(o); if (o) setActive(Math.max(0, list.indexOf(selected!))); }}>\n <PopoverPrimitive.Trigger\n role=\"combobox\"\n aria-expanded={open}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"flex h-9 w-full items-center justify-between gap-2 rounded-input border border-field-border bg-field px-3 text-sm shadow-field vx-glass\",\n \"hover:field-hover focus-visible:field-focus focus-visible:outline-none\",\n className,\n )}\n >\n <span className={cn(\"truncate\", !selected && \"text-muted\")}>{selected?.label ?? placeholder}</span>\n <ChevronsUpDown className=\"size-4 shrink-0 text-muted\" />\n </PopoverPrimitive.Trigger>\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n align=\"start\"\n sideOffset={6}\n className={cn(menuPanel, \"w-[var(--radix-popover-trigger-width)] min-w-56 p-0\")}\n >\n <div className=\"flex items-center gap-2 border-b border-rule px-3\">\n <Search className=\"size-4 text-muted\" />\n <input\n autoFocus\n value={q}\n onChange={(e) => { setQ(e.target.value); setActive(0); }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") { e.preventDefault(); setActive((a) => Math.min(a + 1, list.length - 1)); }\n if (e.key === \"ArrowUp\") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }\n if (e.key === \"Enter\" && list[active]) { e.preventDefault(); pick(list[active]); }\n }}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n className=\"h-10 flex-1 bg-transparent text-sm outline-none placeholder:text-muted\"\n />\n </div>\n <ul role=\"listbox\" className=\"max-h-64 overflow-y-auto p-1.5\">\n {list.length === 0 && <li className=\"px-3 py-5 text-center text-sm text-muted\">{emptyText}</li>}\n {list.map((o, i) => (\n <li\n key={o.value}\n role=\"option\"\n aria-selected={o.value === current}\n onMouseMove={() => setActive(i)}\n onClick={() => pick(o)}\n data-active={i === active}\n className={menuItem}\n >\n <span className=\"grid flex-1\">\n <span>{o.label}</span>\n {o.description && <span className=\"text-xs text-muted\">{o.description}</span>}\n </span>\n {o.value === current && <Check className={menuCheck} />}\n </li>\n ))}\n </ul>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n );\n}\n"},{"slug":"input-otp","name":"Input OTP","category":"Forms","description":"One-time code entry with auto-advance, paste support and grouped cells.","deps":[],"uses":[],"usage":"<InputOTP length={6} groupSize={3} onComplete={verify} />","props":[["length","number","6","Number of cells."],["groupSize","number","","Insert a gap after this many cells."],["value / onChange","string / (v) => void","","Controlled value."],["onComplete","(code: string) => void","","Called when every cell is filled."],["invalid","boolean","false","Error style."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface InputOTPProps {\n length?: number;\n value?: string;\n onChange?: (value: string) => void;\n /** Called once every cell is filled. */\n onComplete?: (value: string) => void;\n /** Insert a gap after this many cells, e.g. 3 for 123-456. */\n groupSize?: number;\n disabled?: boolean;\n invalid?: boolean;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** One-time code entry: digits only, auto-advance, paste the full code. */\nexport function InputOTP({ length = 6, value, onChange, onComplete, groupSize, disabled, invalid, className, ...rest }: InputOTPProps) {\n const [inner, setInner] = React.useState(\"\");\n const [focused, setFocused] = React.useState(false);\n const code = (value ?? inner).slice(0, length);\n const ref = React.useRef<HTMLInputElement>(null);\n\n const set = (next: string) => {\n const clean = next.replace(/\\D/g, \"\").slice(0, length);\n setInner(clean);\n onChange?.(clean);\n if (clean.length === length) onComplete?.(clean);\n };\n\n return (\n <div className={cn(\"relative inline-flex items-center gap-2\", className)} onClick={() => ref.current?.focus()}>\n <input\n ref={ref}\n value={code}\n onChange={(e) => set(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n maxLength={length}\n disabled={disabled}\n aria-label={rest[\"aria-label\"] ?? \"One-time code\"}\n aria-invalid={invalid || undefined}\n className=\"absolute inset-0 cursor-text opacity-0 disabled:cursor-not-allowed\"\n />\n {Array.from({ length }).map((_, i) => {\n const active = focused && (i === code.length || (i === length - 1 && code.length === length));\n return (\n <React.Fragment key={i}>\n {groupSize && i > 0 && i % groupSize === 0 && <span aria-hidden className=\"h-px w-3 bg-rule-2\" />}\n <span\n aria-hidden\n className={cn(\n \"vx-glass grid size-11 place-items-center rounded-input border bg-field font-mono text-lg text-ink shadow-field\",\n \"transition-[border-color,box-shadow] duration-[var(--dur-micro)]\",\n invalid ? \"border-danger\" : active ? \"field-focus\" : \"border-field-border\",\n disabled && \"bg-paper-2 opacity-60\",\n )}\n >\n {code[i] ?? (active ? <span className=\"vx-caret h-5 w-px bg-ink\" /> : \"\")}\n </span>\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n"},{"slug":"sheet","name":"Sheet","category":"Overlays","description":"A panel that slides in from an edge, for forms and details that keep page context.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Sheet>\n <SheetTrigger asChild><Button>Open</Button></SheetTrigger>\n <SheetContent side=\"right\">…</SheetContent>\n</Sheet>","props":[["SheetContent.side","\"right\" | \"left\" | \"bottom\"","\"right\"","Edge it slides from."],["open / onOpenChange","boolean / (open) => void","","Controlled state."]],"pro":false,"source":"import * as React from \"react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Sheet = SheetPrimitive.Root;\nexport const SheetTrigger = SheetPrimitive.Trigger;\nexport const SheetClose = SheetPrimitive.Close;\n\nconst sides = {\n right: \"vx-sheet-right inset-y-0 right-0 h-full w-[min(420px,100%)] border-l\",\n left: \"vx-sheet-left inset-y-0 left-0 h-full w-[min(420px,100%)] border-r\",\n bottom: \"vx-sheet-bottom inset-x-0 bottom-0 max-h-[85svh] rounded-t-panel border-t\",\n};\n\nexport function SheetContent({\n side = \"right\",\n className,\n children,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: keyof typeof sides }) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay\" />\n <SheetPrimitive.Content\n className={cn(\"vx-glass fixed z-[400] flex flex-col gap-4 overflow-y-auto border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\", sides[side], className)}\n {...props}\n >\n {children}\n <SheetPrimitive.Close\n aria-label=\"Close\"\n className=\"absolute right-4 top-4 grid size-7 place-items-center rounded-input text-muted hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-4\" />\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPrimitive.Portal>\n );\n}\n\nexport function SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5 pr-8\", className)} {...props} />;\n}\nexport function SheetFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"mt-auto flex flex-col-reverse gap-2 border-t border-rule pt-4 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return <SheetPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return <SheetPrimitive.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n"},{"slug":"alert-dialog","name":"Alert dialog","category":"Overlays","description":"A modal that interrupts to confirm an important or irreversible action.","deps":["radix-ui"],"uses":["button"],"usage":"<AlertDialog>\n <AlertDialogTrigger asChild><Button>Delete</Button></AlertDialogTrigger>\n <AlertDialogContent>…<AlertDialogAction destructive>Delete</AlertDialogAction></AlertDialogContent>\n</AlertDialog>","props":[["AlertDialogAction.destructive","boolean","false","Danger style for the confirm button."],["open / onOpenChange","boolean / (open) => void","","Controlled state."]],"pro":false,"source":"import * as React from \"react\";\nimport { AlertDialog as AD } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\nimport { buttonVariants } from \"./button\";\n\nexport const AlertDialog = AD.Root;\nexport const AlertDialogTrigger = AD.Trigger;\n\nexport function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof AD.Content>) {\n return (\n <AD.Portal>\n <AD.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <AD.Content\n className={cn(\n \"vx-dialog fixed left-1/2 top-1/2 z-[400] grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4\",\n \"vx-glass rounded-panel border border-pop-border bg-pop p-6 text-ink shadow-pop outline-none\",\n className,\n )}\n {...props}\n />\n </AD.Portal>\n );\n}\n\nexport function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"grid gap-1.5\", className)} {...props} />;\n}\nexport function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div className={cn(\"flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\nexport function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AD.Title>) {\n return <AD.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AD.Description>) {\n return <AD.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n/** The confirming button. Pass destructive for irreversible actions. */\nexport function AlertDialogAction({ className, destructive, ...props }: React.ComponentProps<typeof AD.Action> & { destructive?: boolean }) {\n return <AD.Action className={cn(buttonVariants({ variant: destructive ? \"danger\" : \"primary\" }), className)} {...props} />;\n}\nexport function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof AD.Cancel>) {\n return <AD.Cancel className={cn(buttonVariants({ variant: \"secondary\" }), className)} {...props} />;\n}\n"},{"slug":"table","name":"Table","category":"Display","description":"Ruled data table parts with selectable rows and a sortable header button.","deps":["lucide-react"],"uses":[],"usage":"<Table>\n <TableHeader><TableRow><TableHead>Name</TableHead></TableRow></TableHeader>\n <TableBody><TableRow><TableCell>Ana</TableCell></TableRow></TableBody>\n</Table>","props":[["TableRow data-state","\"selected\"","","Highlights a selected row."],["TableSortButton.direction","\"asc\" | \"desc\" | false","","Current sort."],["...props","native table element props","","Every part forwards them."]],"pro":false,"source":"import * as React from \"react\";\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Table({ className, ...props }: React.ComponentProps<\"table\">) {\n return (\n <div className=\"vx-glass w-full overflow-x-auto rounded-card border border-card-border bg-card\">\n <table className={cn(\"w-full caption-bottom text-left text-sm\", className)} {...props} />\n </div>\n );\n}\nexport function TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return <thead className={cn(\"border-b border-rule bg-paper-2\", className)} {...props} />;\n}\nexport function TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return <tbody className={cn(\"divide-y divide-rule\", className)} {...props} />;\n}\nexport function TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return <tr className={cn(\"transition-[background-color] duration-[var(--dur-micro)] hover:bg-paper-2/60 data-[state=selected]:bg-accent-wash/50\", className)} {...props} />;\n}\nexport function TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return <th className={cn(\"h-11 whitespace-nowrap px-3 font-medium text-muted\", className)} {...props} />;\n}\nexport function TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n return <td className={cn(\"px-3 py-2.5 align-middle font-medium text-ink\", className)} {...props} />;\n}\nexport function TableCaption({ className, ...props }: React.ComponentProps<\"caption\">) {\n return <caption className={cn(\"border-t border-rule px-4 py-3 text-left text-sm text-muted\", className)} {...props} />;\n}\n\n/** A header button that cycles sort direction. */\nexport function TableSortButton({\n direction,\n onClick,\n children,\n}: {\n direction: \"asc\" | \"desc\" | false;\n onClick: () => void;\n children: React.ReactNode;\n}) {\n const Icon = direction === \"asc\" ? ArrowUp : direction === \"desc\" ? ArrowDown : ChevronsUpDown;\n return (\n <button\n onClick={onClick}\n aria-sort={direction ? (direction === \"asc\" ? \"ascending\" : \"descending\") : undefined}\n className=\"-mx-1.5 inline-flex items-center gap-1 rounded-sm px-1.5 py-1 hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {children}\n <Icon className={cn(\"size-3.5\", direction ? \"text-ink\" : \"text-muted/70\")} />\n </button>\n );\n}\n"},{"slug":"breadcrumb","name":"Breadcrumb","category":"Navigation","description":"Where the current page sits in the hierarchy, with links back up.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem><BreadcrumbLink href=\"/\">Home</BreadcrumbLink></BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem><BreadcrumbPage>Billing</BreadcrumbPage></BreadcrumbItem>\n </BreadcrumbList>\n</Breadcrumb>","props":[["BreadcrumbLink.asChild","boolean","false","Render your router link."],["BreadcrumbSeparator.children","ReactNode","chevron","Custom separator."]],"pro":false,"source":"import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Breadcrumb(props: React.ComponentProps<\"nav\">) {\n return <nav aria-label=\"Breadcrumb\" {...props} />;\n}\nexport function BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return <ol className={cn(\"flex flex-wrap items-center gap-1.5 text-sm text-muted\", className)} {...props} />;\n}\nexport function BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return <li className={cn(\"inline-flex items-center gap-1.5\", className)} {...props} />;\n}\nexport function BreadcrumbLink({ asChild, className, ...props }: React.ComponentProps<\"a\"> & { asChild?: boolean }) {\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <Comp\n className={cn(\"rounded-sm transition-[color] duration-[var(--dur-micro)] hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\", className)}\n {...props}\n />\n );\n}\nexport function BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span aria-current=\"page\" className={cn(\"font-medium text-ink\", className)} {...props} />;\n}\nexport function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li role=\"presentation\" aria-hidden className={cn(\"text-rule-2 [&>svg]:size-3.5\", className)} {...props}>\n {children ?? <ChevronRight />}\n </li>\n );\n}\nexport function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span role=\"presentation\" className={cn(\"flex size-6 items-center justify-center\", className)} {...props}>\n <MoreHorizontal className=\"size-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n );\n}\n"},{"slug":"pagination","name":"Pagination","category":"Navigation","description":"Move through pages of results, with gaps for long ranges.","deps":["lucide-react"],"uses":[],"usage":"<Pagination page={page} pageCount={20} onPageChange={setPage} />","props":[["page","number","required","Current page, from 1."],["pageCount","number","required","Total pages."],["onPageChange","(page: number) => void","required","Called on navigation."],["siblings","number","1","Pages shown either side of the current one."]],"pro":false,"source":"import { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nfunction range(page: number, count: number, siblings: number): (number | \"gap\")[] {\n const total = siblings * 2 + 5;\n if (count <= total) return Array.from({ length: count }, (_, i) => i + 1);\n const left = Math.max(page - siblings, 2);\n const right = Math.min(page + siblings, count - 1);\n const out: (number | \"gap\")[] = [1];\n if (left > 2) out.push(\"gap\");\n for (let i = left; i <= right; i++) out.push(i);\n if (right < count - 1) out.push(\"gap\");\n out.push(count);\n return out;\n}\n\nexport interface PaginationProps {\n page: number;\n pageCount: number;\n onPageChange: (page: number) => void;\n /** Pages shown either side of the current one. */\n siblings?: number;\n /** Show the \"Previous\" and \"Next\" words. Turn off in narrow places; the arrows stay, with labels for screen readers. */\n labels?: boolean;\n className?: string;\n}\n\nconst cell =\n \"inline-flex h-8 min-w-8 items-center justify-center gap-1 rounded-input px-2 text-sm font-medium transition-[background-color,color] duration-[var(--dur-micro)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:pointer-events-none disabled:opacity-40\";\n\nexport function Pagination({ page, pageCount, onPageChange, siblings = 1, labels = true, className }: PaginationProps) {\n return (\n <nav aria-label=\"Pagination\" className={cn(\"flex max-w-full items-center gap-1\", className)}>\n <button className={cn(cell, \"text-ink-2 hover:bg-paper-3\")} disabled={page <= 1} onClick={() => onPageChange(page - 1)} aria-label={labels ? undefined : \"Previous page\"}>\n <ChevronLeft className=\"size-4\" /> {labels && <span className=\"hidden sm:inline\">Previous</span>}\n </button>\n {range(page, pageCount, siblings).map((p, i) =>\n p === \"gap\" ? (\n <span key={`g${i}`} className=\"px-1 text-muted\">…</span>\n ) : (\n <button\n key={p}\n aria-current={p === page ? \"page\" : undefined}\n onClick={() => onPageChange(p)}\n className={cn(\n cell,\n \"tabular-nums\",\n p === page ? \"vx-glass border border-[color:var(--key-border)] bg-key-face text-ink shadow-key\" : \"text-ink-2 hover:bg-paper-3\",\n )}\n >\n {p}\n </button>\n ),\n )}\n <button className={cn(cell, \"text-ink-2 hover:bg-paper-3\")} disabled={page >= pageCount} onClick={() => onPageChange(page + 1)} aria-label={labels ? undefined : \"Next page\"}>\n {labels && <span className=\"hidden sm:inline\">Next</span>} <ChevronRight className=\"size-4\" />\n </button>\n </nav>\n );\n}\n"},{"slug":"button-group","name":"Button group","category":"Actions","description":"Buttons fused into one row, for ranges and paired actions.","deps":[],"uses":["button"],"usage":"<ButtonGroup aria-label=\"Range\">\n <Button variant=\"secondary\">Day</Button>\n <Button>Week</Button>\n</ButtonGroup>","props":[["children","Button elements","","The fused buttons. Inner corners square off automatically."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Buttons fused into one row: inner corners square, borders shared.\n * Put Button elements inside; pass aria-label to describe the group.\n */\nexport function ButtonGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n role=\"group\"\n className={cn(\n \"inline-flex items-stretch [&>*]:rounded-none [&>*:first-child]:rounded-l-input [&>*:last-child]:rounded-r-input [&>*:not(:first-child)]:-ml-px [&>*]:shadow-none [&>*:focus-visible]:z-10 [&>*:hover]:z-10\",\n className,\n )}\n {...props}\n />\n );\n}\n"},{"slug":"password-input","name":"Password input","category":"Forms","description":"A password field with a show and hide key and an optional strength meter.","deps":["lucide-react"],"uses":["input"],"usage":"<PasswordInput showStrength placeholder=\"Choose a password\" />","props":[["showStrength","boolean","false","Four-step meter and the rules under the field."],["...props","InputProps","","Everything Input accepts, except type and trailing."]],"pro":false,"source":"import * as React from \"react\";\nimport { Check, Eye, EyeOff, Lock, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Input, type InputProps } from \"./input\";\n\nconst rules = [\n { label: \"At least 8 characters\", test: (v: string) => v.length >= 8 },\n { label: \"An uppercase letter\", test: (v: string) => /[A-Z]/.test(v) },\n { label: \"A number\", test: (v: string) => /\\d/.test(v) },\n { label: \"A symbol\", test: (v: string) => /[^A-Za-z0-9]/.test(v) },\n];\nconst levels = [\"Too weak\", \"Weak\", \"Fair\", \"Good\", \"Strong\"];\nconst tone = [\"bg-paper-3\", \"bg-danger\", \"bg-warning\", \"bg-accent\", \"bg-success\"];\n\nexport interface PasswordInputProps extends Omit<InputProps, \"type\" | \"trailing\"> {\n /** Show a four-step strength meter and the rules under the field. */\n showStrength?: boolean;\n}\n\n/** A password field with a show and hide key, and an optional strength meter. */\nexport const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(\n ({ showStrength, value, defaultValue, onChange, className, ...props }, ref) => {\n const [shown, setShown] = React.useState(false);\n const [inner, setInner] = React.useState(String(defaultValue ?? \"\"));\n const v = value === undefined ? inner : String(value);\n const score = rules.filter((r) => r.test(v)).length;\n return (\n <div className={cn(\"grid gap-2\", className)}>\n <Input\n ref={ref}\n type={shown ? \"text\" : \"password\"}\n value={v}\n onChange={(e) => {\n setInner(e.target.value);\n onChange?.(e);\n }}\n leading={<Lock />}\n trailing={\n <button\n type=\"button\"\n onClick={() => setShown(!shown)}\n aria-label={shown ? \"Hide password\" : \"Show password\"}\n aria-pressed={shown}\n className=\"grid size-7 place-items-center rounded-sm text-muted hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {shown ? <EyeOff className=\"size-4\" /> : <Eye className=\"size-4\" />}\n </button>\n }\n {...props}\n />\n {showStrength && (\n <div className=\"grid gap-2\" aria-live=\"polite\">\n <div className=\"flex items-center gap-2\">\n <div className=\"grid flex-1 grid-cols-4 gap-1\">\n {[1, 2, 3, 4].map((i) => (\n <span key={i} className={cn(\"h-1.5 rounded-full transition-[background-color] duration-[var(--dur-short)]\", score >= i ? tone[score] : \"bg-paper-3\")} />\n ))}\n </div>\n <span className=\"w-16 text-right text-xs text-muted\">{v ? levels[score] : \"\"}</span>\n </div>\n <ul className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs\">\n {rules.map((r) => {\n const ok = r.test(v);\n return (\n <li key={r.label} className={cn(\"flex items-center gap-1.5\", ok ? \"text-success-wash-ink\" : \"text-muted\")}>\n {ok ? <Check className=\"size-3.5\" /> : <X className=\"size-3.5\" />} {r.label}\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n },\n);\nPasswordInput.displayName = \"PasswordInput\";\n"},{"slug":"copy-button","name":"Copy button","category":"Actions","description":"Copies a value and confirms with a check for a moment.","deps":["lucide-react"],"uses":["button"],"usage":"<CopyButton value={apiKey} />\n<CopyButton value={link} label=\"Copy link\" />","props":[["value","string","required","Text to copy."],["label","string","","Text beside the icon; icon-only when omitted."],["onCopied","() => void","","Called after copying."]],"pro":false,"source":"import * as React from \"react\";\nimport { Check, Copy } from \"lucide-react\";\nimport { Button, type ButtonProps } from \"./button\";\n\nexport interface CopyButtonProps extends Omit<ButtonProps, \"onClick\"> {\n /** The text written to the clipboard. */\n value: string;\n /** Text shown beside the icon. Omit for an icon-only button. */\n label?: string;\n onCopied?: () => void;\n}\n\n/** Copies a value and confirms with a check for a moment. */\nexport function CopyButton({ value, label, onCopied, variant = \"secondary\", size, ...props }: CopyButtonProps) {\n const [done, setDone] = React.useState(false);\n return (\n <Button\n variant={variant}\n size={size ?? (label ? \"sm\" : \"icon-sm\")}\n aria-label={label ? undefined : done ? \"Copied\" : \"Copy\"}\n onClick={() =>\n navigator.clipboard?.writeText(value).then(() => {\n setDone(true);\n onCopied?.();\n window.setTimeout(() => setDone(false), 1500);\n })\n }\n {...props}\n >\n {done ? <Check className=\"text-success\" /> : <Copy />}\n {label && (done ? \"Copied\" : label)}\n </Button>\n );\n}\n"}],"templates":[]}