@plankui/mcp 0.1.2 → 0.1.3
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.
- package/README.md +4 -2
- package/package.json +4 -2
- package/registry.json +1 -1
- package/server.mjs +18 -11
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ Tools load when a session starts, so restart the client or open a new chat after
|
|
|
101
101
|
|
|
102
102
|
> Set up PlankUI in this project with the Ember theme and the Keycap style.
|
|
103
103
|
|
|
104
|
+
> Add a PlankUI select and dialog using Base UI.
|
|
105
|
+
|
|
104
106
|
The agent looks components up, reads their real source and props, and writes code that matches the library, including the right install commands.
|
|
105
107
|
|
|
106
108
|
## Tools
|
|
@@ -109,8 +111,8 @@ The agent looks components up, reads their real source and props, and writes cod
|
|
|
109
111
|
| --- | --- |
|
|
110
112
|
| `list_components` | Every component with its slug, category and one line on what it does |
|
|
111
113
|
| `search_components` | Components that match an intent, such as "confirm dialog" |
|
|
112
|
-
| `get_component` | Install command, full source of every file it needs, usage and props |
|
|
113
|
-
| `get_component_usage` | Usage and props only |
|
|
114
|
+
| `get_component` | Install command, full source of every file it needs, usage and props. Pass `primitives: "base"` for the Base UI version |
|
|
115
|
+
| `get_component_usage` | Usage and props only (same `primitives` option) |
|
|
114
116
|
| `list_templates` | Every template |
|
|
115
117
|
| `get_template` | A template's full source |
|
|
116
118
|
| `get_setup` | The tokens, styles and `cn` helper every project needs |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plankui/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
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": {
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"tailwindcss",
|
|
38
38
|
"components",
|
|
39
39
|
"design-system",
|
|
40
|
-
"plankui"
|
|
40
|
+
"plankui",
|
|
41
|
+
"base-ui",
|
|
42
|
+
"radix-ui"
|
|
41
43
|
],
|
|
42
44
|
"author": "Vector Labs (https://plankui.com)"
|
|
43
45
|
}
|
package/registry.json
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
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/* The same motion for the Base UI versions, which mark state with data-open and data-closed. */\n.vx-pop[data-open] { animation: vx-pop-in var(--dur-short) var(--ease-out); }\n.vx-pop[data-closed] { animation: vx-pop-out var(--dur-micro) ease-in; }\n.vx-fade[data-open] { animation: vx-fade-in var(--dur-short) var(--ease-out); }\n.vx-fade[data-closed] { animation: vx-fade-out var(--dur-micro) ease-in; }\n.vx-dialog[data-open] { animation: vx-dialog-in var(--dur-short) var(--ease-out); }\n.vx-dialog[data-closed] { animation: vx-dialog-out var(--dur-micro) ease-in; }\n.vx-reveal[data-open] { animation: vx-reveal var(--dur-short) var(--ease-out); }\n.vx-sheet-right[data-open] { animation: vx-sheet-right var(--dur-short) var(--ease-out); }\n.vx-sheet-right[data-closed] { animation: vx-sheet-right-out var(--dur-short) ease-in; }\n.vx-sheet-left[data-open] { animation: vx-sheet-left var(--dur-short) var(--ease-out); }\n.vx-sheet-left[data-closed] { animation: vx-sheet-left-out var(--dur-short) ease-in; }\n.vx-sheet-bottom[data-open] { animation: vx-sheet-bottom var(--dur-short) var(--ease-out); }\n.vx-sheet-bottom[data-closed] { animation: vx-sheet-bottom-out var(--dur-short) ease-in; }\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","baseSource":"import * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\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 // asChild renders the child element with the button's styles, like Radix Slot, through Base UI's useRender.\n const child = asChild && React.isValidElement(children) ? (children as React.ReactElement<Record<string, unknown>>) : undefined;\n return useRender({\n defaultTagName: \"button\",\n render: child,\n ref,\n props: mergeProps<\"button\">(\n {\n className: cn(buttonVariants({ variant, size }), className),\n disabled: disabled || loading,\n \"aria-busy\": loading || undefined,\n children: child ? undefined : (\n <>\n {loading && <Spinner className=\"size-4\" />}\n {children}\n </>\n ),\n },\n { \"data-slot\": \"button\", ...props } as React.ComponentProps<\"button\">,\n ),\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","baseSource":"import * as React from \"react\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { Toggle } from \"@base-ui/react/toggle\";\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-pressed]\", current);\n return (\n <ToggleGroup\n ref={list}\n value={current ? [current] : []}\n onValueChange={(v) => {\n const next = v[0];\n if (!next) return;\n setInner(next);\n onValueChange?.(next);\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 <Toggle\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 data-[disabled]:opacity-50 [&_svg]:size-4\",\n \"data-[pressed]:text-ink\",\n size === \"md\" ? \"h-7 px-3 text-sm\" : \"h-6 px-2 text-xs\",\n )}\n >\n {o.label}\n </Toggle>\n ))}\n </ToggleGroup>\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","baseSource":"import * as React from \"react\";\nimport { Menu } from \"@base-ui/react/menu\";\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\n/** asChild, as in the Radix version: the child element becomes the trigger. */\nfunction slot(asChild: boolean | undefined, children: React.ReactNode) {\n if (!asChild || !React.isValidElement(children)) return { children };\n const nativeButton = typeof children.type !== \"string\" || children.type === \"button\";\n return { render: children as React.ReactElement<Record<string, unknown>>, nativeButton };\n}\n\nexport const DropdownMenu = Menu.Root;\nexport const DropdownMenuGroup = Menu.Group;\nexport const DropdownMenuSub = Menu.SubmenuRoot;\n\nexport function DropdownMenuTrigger({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <Menu.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nexport function DropdownMenuRadioGroup({\n onValueChange,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.RadioGroup>, \"value\" | \"defaultValue\" | \"onValueChange\"> & {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n}) {\n return <Menu.RadioGroup onValueChange={(v) => onValueChange?.(String(v))} {...props} />;\n}\n\nconst panel = menuPanel;\nconst item = menuItem;\n\ntype Positioner = React.ComponentProps<typeof Menu.Positioner>;\n\nexport interface DropdownMenuContentProps extends Omit<React.ComponentProps<typeof Menu.Popup>, \"className\"> {\n className?: string;\n side?: Positioner[\"side\"];\n align?: Positioner[\"align\"];\n sideOffset?: number;\n alignOffset?: number;\n}\n\nexport function DropdownMenuContent({ className, side = \"bottom\", align = \"center\", sideOffset = 6, alignOffset, ...props }: DropdownMenuContentProps) {\n return (\n <Menu.Portal>\n <Menu.Positioner side={side} align={align} sideOffset={sideOffset} alignOffset={alignOffset} className=\"z-[100]\">\n <Menu.Popup className={cn(panel, className)} {...props} />\n </Menu.Positioner>\n </Menu.Portal>\n );\n}\n\nexport function DropdownMenuItem({\n className,\n shortcut,\n destructive,\n children,\n onSelect,\n onClick,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.Item>, \"className\" | \"onSelect\"> & {\n className?: string;\n shortcut?: string;\n destructive?: boolean;\n /** Runs when the item is chosen by pointer or keyboard, as in the Radix version. */\n onSelect?: (event: Event) => void;\n}) {\n return (\n <Menu.Item\n className={cn(item, destructive && \"text-danger data-[highlighted]:bg-danger-wash [&_svg]:text-danger\", className)}\n onClick={(e) => {\n onClick?.(e);\n onSelect?.(e.nativeEvent);\n }}\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({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.CheckboxItem>, \"className\"> & { className?: string }) {\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.CheckboxItemIndicator>\n <Check className=\"!text-accent\" />\n </Menu.CheckboxItemIndicator>\n </span>\n {children}\n </Menu.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.RadioItem>, \"className\" | \"value\"> & { className?: string; value: string }) {\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.RadioItemIndicator>\n <span className=\"block size-1.5 rounded-full bg-accent\" />\n </Menu.RadioItemIndicator>\n </span>\n {children}\n </Menu.RadioItem>\n );\n}\n\n/** A heading inside the menu. Base UI ties its own labels to a group, so this is a plain element that works anywhere. */\nexport function DropdownMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div role=\"presentation\" className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function DropdownMenuSeparator({ className, ...props }: Omit<React.ComponentProps<typeof Menu.Separator>, \"className\"> & { className?: string }) {\n return <Menu.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n\nexport function DropdownMenuSubTrigger({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof Menu.SubmenuTrigger>, \"className\"> & { className?: string }) {\n return (\n <Menu.SubmenuTrigger className={cn(item, \"data-[popup-open]:bg-paper-3\", className)} {...props}>\n {children}\n <ChevronRight className=\"ml-auto\" />\n </Menu.SubmenuTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({ className, ...props }: Omit<React.ComponentProps<typeof Menu.Popup>, \"className\"> & { className?: string }) {\n return (\n <Menu.Portal>\n <Menu.Positioner side=\"right\" align=\"start\" sideOffset={4} className=\"z-[100]\">\n <Menu.Popup className={cn(panel, className)} {...props} />\n </Menu.Positioner>\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","baseSource":null},{"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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuItem, menuLabel, menuPanel, menuSeparator } from \"./menu-styles\";\n\n/** Finds every SelectItem below the root, so the trigger can show an item's label rather than its value. */\nfunction collectItems(node: React.ReactNode, out: Record<string, React.ReactNode> = {}) {\n React.Children.forEach(node, (child) => {\n if (!React.isValidElement<{ value?: string; children?: React.ReactNode }>(child)) return;\n if (child.type === SelectItem && child.props.value !== undefined) out[child.props.value] = child.props.children;\n else collectItems(child.props.children, out);\n });\n return out;\n}\n\nexport interface SelectProps\n extends Omit<React.ComponentProps<typeof SelectPrimitive.Root>, \"value\" | \"defaultValue\" | \"onValueChange\" | \"items\"> {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n}\n\n/** Same props as the Radix version, with string values. */\nexport function Select({ children, onValueChange, ...props }: SelectProps) {\n const items = collectItems(children);\n return (\n <SelectPrimitive.Root items={items} onValueChange={(v) => onValueChange?.(v == null ? \"\" : String(v))} {...props}>\n {children}\n </SelectPrimitive.Root>\n );\n}\n\nexport const SelectGroup = SelectPrimitive.Group;\n\nexport function SelectValue({ className, ...props }: Omit<React.ComponentProps<typeof SelectPrimitive.Value>, \"className\"> & { className?: string }) {\n return <SelectPrimitive.Value className={cn(\"truncate data-[placeholder]:text-muted\", className)} {...props} />;\n}\n\nexport function SelectTrigger({ className, children, ...props }: Omit<React.ComponentProps<typeof SelectPrimitive.Trigger>, \"className\"> & { className?: string }) {\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-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 [&>span]:truncate\",\n className,\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon render={<ChevronsUpDown className=\"size-4 shrink-0 text-muted\" />} />\n </SelectPrimitive.Trigger>\n );\n}\n\nexport function SelectContent({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof SelectPrimitive.Popup>, \"className\"> & { className?: string; position?: \"popper\" | \"item-aligned\" }) {\n const { position = \"popper\", ...rest } = props as typeof props & { position?: string };\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Positioner alignItemWithTrigger={position === \"item-aligned\"} sideOffset={6} className=\"z-[100]\">\n <SelectPrimitive.Popup\n className={cn(menuPanel, \"relative min-w-[max(8rem,var(--anchor-width))]\", className)}\n {...rest}\n >\n <SelectPrimitive.List className=\"max-h-[min(var(--available-height),340px)] overflow-y-auto\">{children}</SelectPrimitive.List>\n </SelectPrimitive.Popup>\n </SelectPrimitive.Positioner>\n </SelectPrimitive.Portal>\n );\n}\n\n/** A heading inside the list. A plain element, so it works inside or outside a group. */\nexport function SelectLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div role=\"presentation\" className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function SelectItem({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof SelectPrimitive.Item>, \"className\" | \"value\"> & { className?: string; value: string }) {\n return (\n <SelectPrimitive.Item className={cn(menuItem, \"w-full pr-9\", className)} {...props}>\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 }: Omit<React.ComponentProps<typeof SelectPrimitive.Separator>, \"className\"> & { className?: string }) {\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","baseSource":"import * as React from \"react\";\nimport { Checkbox as CheckboxPrimitive } from \"@base-ui/react/checkbox\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\ntype CheckedState = boolean | \"indeterminate\";\n\nexport interface CheckboxProps\n extends Omit<React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, \"className\" | \"checked\" | \"defaultChecked\" | \"onCheckedChange\"> {\n className?: string;\n /** true, false or \"indeterminate\", the same as the Radix version. */\n checked?: CheckedState;\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean) => void;\n}\n\nexport const Checkbox = React.forwardRef<HTMLElement, CheckboxProps>(({ className, checked, onCheckedChange, ...props }, ref) => {\n const mixed = checked === \"indeterminate\";\n return (\n <CheckboxPrimitive.Root\n ref={ref}\n checked={mixed ? false : checked}\n indeterminate={mixed}\n onCheckedChange={(next) => onCheckedChange?.(next)}\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-[checked]:border-transparent data-[checked]:bg-accent data-[checked]:[background-image:var(--control-on-image)] data-[checked]:shadow-[var(--elev-control-on)] data-[checked]:text-accent-ink\",\n \"data-[indeterminate]:border-transparent data-[indeterminate]:bg-accent data-[indeterminate]:[background-image:var(--control-on-image)] data-[indeterminate]:shadow-[var(--elev-control-on)] data-[indeterminate]:text-accent-ink\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator className=\"grid place-items-center\">\n {mixed ? <Minus className=\"size-3\" strokeWidth={3} /> : <Check className=\"size-3\" strokeWidth={3.5} />}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\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","baseSource":"import * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface RadioGroupProps\n extends Omit<React.ComponentProps<typeof RadioGroupPrimitive>, \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\"> {\n className?: string;\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n}\n\nexport function RadioGroup({ className, onValueChange, ...props }: RadioGroupProps) {\n return <RadioGroupPrimitive className={cn(\"grid gap-2.5\", className)} onValueChange={(v) => onValueChange?.(String(v))} {...props} />;\n}\n\nexport function RadioGroupItem({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof Radio.Root>, \"className\" | \"value\"> & { className?: string; value: string }) {\n return (\n <Radio.Root\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-[checked]:border-transparent data-[checked]:bg-accent data-[checked]:[background-image:var(--control-on-image)] data-[checked]:shadow-[var(--elev-control-on)]\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50\",\n className,\n )}\n {...props}\n >\n <Radio.Indicator className=\"block size-1.5 rounded-full bg-accent-ink\" />\n </Radio.Root>\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-checked]]:border-accent has-[[data-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","baseSource":"import * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Switch = React.forwardRef<\n HTMLElement,\n Omit<React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, \"className\"> & { className?: string; 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-[checked]:bg-accent data-[checked]:[background-image:var(--control-on-image)] data-[checked]:shadow-[var(--elev-control-on)] data-[disabled]:cursor-not-allowed data-[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-[unchecked]:translate-x-0\",\n size === \"md\" ? \"size-[18px] data-[checked]:translate-x-[18px]\" : \"size-2.5 data-[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","baseSource":"import * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface SliderProps\n extends Omit<React.ComponentProps<typeof SliderPrimitive.Root>, \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\" | \"onValueCommitted\"> {\n className?: string;\n value?: number[];\n defaultValue?: number[];\n onValueChange?: (value: number[]) => void;\n onValueCommit?: (value: number[]) => void;\n \"aria-label\"?: string;\n}\n\nconst list = (v: number | readonly number[]) => (Array.isArray(v) ? [...v] : [v as number]);\n\nexport function Slider({ className, defaultValue, value, onValueChange, onValueCommit, ...props }: SliderProps) {\n const count = (value ?? defaultValue ?? [0]).length;\n return (\n <SliderPrimitive.Root\n defaultValue={defaultValue}\n value={value}\n onValueChange={(v) => onValueChange?.(list(v))}\n onValueCommitted={(v) => onValueCommit?.(list(v))}\n className={cn(\"relative w-full touch-none select-none data-[disabled]:opacity-50\", className)}\n {...props}\n >\n <SliderPrimitive.Control className=\"flex w-full items-center py-2\">\n <SliderPrimitive.Track className=\"relative h-1.5 w-full grow rounded-full bg-[color:var(--track-off)] shadow-[inset_0_1px_1px_oklch(0%_0_0/0.06)]\">\n <SliderPrimitive.Indicator className=\"rounded-full bg-accent [background-image:var(--control-on-image)] shadow-[inset_0_1px_0_oklch(100%_0_0/0.24)]\" />\n {Array.from({ length: count }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n index={i}\n getAriaLabel={() => 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-[scale] duration-[var(--dur-micro)] hover:scale-110 active:cursor-grabbing data-[dragging]:cursor-grabbing\",\n \"has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-focus\",\n )}\n />\n ))}\n </SliderPrimitive.Track>\n </SliderPrimitive.Control>\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","baseSource":"import * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\n/** asChild, as in the Radix version: the child element becomes the trigger. */\nfunction slot(asChild: boolean | undefined, children: React.ReactNode) {\n if (!asChild || !React.isValidElement(children)) return { children };\n const nativeButton = typeof children.type !== \"string\" || children.type === \"button\";\n return { render: children as React.ReactElement<Record<string, unknown>>, nativeButton };\n}\n\nexport const Dialog = DialogPrimitive.Root;\n\nexport function DialogTrigger({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof DialogPrimitive.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <DialogPrimitive.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nexport function DialogClose({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof DialogPrimitive.Close>, \"render\"> & { asChild?: boolean }) {\n return <DialogPrimitive.Close {...props} {...slot(asChild, children)} />;\n}\n\nexport function DialogContent({\n className,\n children,\n hideClose,\n ...props\n}: Omit<React.ComponentProps<typeof DialogPrimitive.Popup>, \"className\"> & { className?: string; hideClose?: boolean }) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Backdrop className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <DialogPrimitive.Popup\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.Popup>\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 }: Omit<React.ComponentProps<typeof DialogPrimitive.Title>, \"className\"> & { className?: string }) {\n return <DialogPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function DialogDescription({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof DialogPrimitive.Description>, \"className\"> & { className?: string }) {\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","baseSource":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\nimport { cn } from \"@/lib/cn\";\n\n/** asChild, as in the Radix version: the child element becomes the trigger. */\nfunction slot(asChild: boolean | undefined, children: React.ReactNode) {\n if (!asChild || !React.isValidElement(children)) return { children };\n const nativeButton = typeof children.type !== \"string\" || children.type === \"button\";\n return { render: children as React.ReactElement<Record<string, unknown>>, nativeButton };\n}\n\nexport const Popover = PopoverPrimitive.Root;\n\nexport function PopoverTrigger({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof PopoverPrimitive.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <PopoverPrimitive.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nexport function PopoverClose({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof PopoverPrimitive.Close>, \"render\"> & { asChild?: boolean }) {\n return <PopoverPrimitive.Close {...props} {...slot(asChild, children)} />;\n}\n\ntype Positioner = React.ComponentProps<typeof PopoverPrimitive.Positioner>;\n\nexport interface PopoverContentProps extends Omit<React.ComponentProps<typeof PopoverPrimitive.Popup>, \"className\"> {\n className?: string;\n side?: Positioner[\"side\"];\n align?: Positioner[\"align\"];\n sideOffset?: number;\n alignOffset?: number;\n}\n\nexport function PopoverContent({ className, side = \"bottom\", align = \"center\", sideOffset = 8, alignOffset, ...props }: PopoverContentProps) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Positioner side={side} align={align} sideOffset={sideOffset} alignOffset={alignOffset} className=\"z-[100]\">\n <PopoverPrimitive.Popup\n className={cn(\"vx-pop 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.Positioner>\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","baseSource":"import * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { cn } from \"@/lib/cn\";\n\n/** Same props as the Radix version: delayDuration maps to Base UI's delay. */\nexport function TooltipProvider({\n delayDuration,\n skipDelayDuration,\n children,\n}: {\n delayDuration?: number;\n skipDelayDuration?: number;\n children: React.ReactNode;\n}) {\n return (\n <TooltipPrimitive.Provider delay={delayDuration} timeout={skipDelayDuration}>\n {children}\n </TooltipPrimitive.Provider>\n );\n}\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 const trigger = React.isValidElement(children) ? (children as React.ReactElement<Record<string, unknown>>) : <span>{children}</span>;\n return (\n <TooltipPrimitive.Root>\n <TooltipPrimitive.Trigger render={trigger} />\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Positioner side={side} sideOffset={6} className=\"z-[600]\">\n <TooltipPrimitive.Popup\n className={cn(\n \"vx-pop 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=\"size-2 rotate-45 rounded-[1px] bg-graphite data-[side=bottom]:-top-1 data-[side=left]:-right-1 data-[side=right]:-left-1 data-[side=top]:-bottom-1\" />\n </TooltipPrimitive.Popup>\n </TooltipPrimitive.Positioner>\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","baseSource":null},{"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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"@base-ui/react/progress\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Progress({\n value = 0,\n className,\n tone = \"accent\",\n ...props\n}: Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, \"className\" | \"value\"> & {\n value?: number | null;\n className?: string;\n tone?: \"accent\" | \"success\" | \"danger\";\n}) {\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 value={v} {...props}>\n <ProgressPrimitive.Track\n className={cn(\"relative block 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 >\n <ProgressPrimitive.Indicator\n className={cn(\"block h-full rounded-full transition-[width] duration-[var(--dur-short)] ease-[var(--ease-out)]\", fill)}\n />\n </ProgressPrimitive.Track>\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","baseSource":null},{"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","baseSource":null},{"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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\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 Omit<React.ComponentProps<typeof AvatarPrimitive.Root>, \"className\"> {\n className?: string;\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","baseSource":null},{"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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"@base-ui/react/separator\";\nimport { cn } from \"@/lib/cn\";\n\nexport function Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: Omit<React.ComponentProps<typeof SeparatorPrimitive>, \"className\"> & { className?: string; decorative?: boolean }) {\n return (\n <SeparatorPrimitive\n orientation={orientation}\n {...(decorative ? { role: \"none\", \"aria-orientation\": undefined } : {})}\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","baseSource":"import * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cn } from \"@/lib/cn\";\nimport { pillClass, useSlidingPill } from \"./segmented\";\n\ntype Variant = \"tray\" | \"line\";\nconst VariantContext = React.createContext<Variant>(\"tray\");\n\nexport interface TabsProps\n extends Omit<React.ComponentProps<typeof TabsPrimitive.Root>, \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\"> {\n className?: string;\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n}\n\nexport function Tabs({ className, onValueChange, ...props }: TabsProps) {\n // minmax(0, 1fr) keeps wide panels from stretching the page on small screens.\n return (\n <TabsPrimitive.Root\n className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-4\", className)}\n onValueChange={(v) => onValueChange?.(String(v))}\n {...props}\n />\n );\n}\n\nexport function TabsList({\n className,\n variant = \"tray\",\n children,\n ...props\n}: Omit<React.ComponentProps<typeof TabsPrimitive.List>, \"className\"> & { className?: string; variant?: Variant }) {\n const list = React.useRef<HTMLDivElement>(null);\n // Re-measure when the active tab changes; Base UI marks it with data-active.\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-active]\")?.id ?? null);\n read();\n const mo = new MutationObserver(read);\n mo.observe(l, { subtree: true, attributes: true, attributeFilter: [\"data-active\"] });\n return () => mo.disconnect();\n }, []);\n const pill = useSlidingPill(list, \"[data-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({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof TabsPrimitive.Tab>, \"className\" | \"value\"> & { className?: string; value: string }) {\n const variant = React.useContext(VariantContext);\n return (\n <TabsPrimitive.Tab\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 data-[disabled]:opacity-50 [&_svg]:size-4\",\n variant === \"tray\"\n ? \"relative z-10 h-7 rounded-[var(--pill-radius)] px-3 data-[active]:text-ink\"\n : \"-mb-px h-9 border-b-2 border-transparent px-0.5 data-[active]:border-accent data-[active]:text-ink\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function TabsContent({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof TabsPrimitive.Panel>, \"className\" | \"value\"> & { className?: string; value: string }) {\n return <TabsPrimitive.Panel 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","baseSource":"import * as React from \"react\";\nimport { Accordion as AccordionPrimitive } from \"@base-ui/react/accordion\";\nimport { Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Single = { type?: \"single\"; value?: string; defaultValue?: string; onValueChange?: (value: string) => void };\ntype Multiple = { type: \"multiple\"; value?: string[]; defaultValue?: string[]; onValueChange?: (value: string[]) => void };\n\nexport type AccordionProps = Omit<\n React.ComponentProps<typeof AccordionPrimitive.Root>,\n \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\" | \"multiple\"\n> & {\n className?: string;\n /** Kept for parity with the Radix version: a single accordion can always close its open item. */\n collapsible?: boolean;\n} & (Single | Multiple);\n\nconst asList = (v: string | string[] | undefined) => (v === undefined ? undefined : Array.isArray(v) ? v : v ? [v] : []);\n\n/** Same props as the Radix version: type \"single\" (default) or \"multiple\", with string values. */\nexport function Accordion({ type = \"single\", collapsible: _collapsible, value, defaultValue, onValueChange, ...props }: AccordionProps) {\n return (\n <AccordionPrimitive.Root\n multiple={type === \"multiple\"}\n value={asList(value)}\n defaultValue={asList(defaultValue)}\n onValueChange={(v) => {\n const list = (v as unknown[]).map(String);\n if (type === \"multiple\") (onValueChange as Multiple[\"onValueChange\"])?.(list);\n else (onValueChange as Single[\"onValueChange\"])?.(list[0] ?? \"\");\n }}\n {...props}\n />\n );\n}\n\nexport function AccordionItem({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof AccordionPrimitive.Item>, \"className\"> & { className?: string; value: string }) {\n return <AccordionPrimitive.Item className={cn(\"border-b border-rule last:border-b-0\", className)} {...props} />;\n}\n\nexport function AccordionTrigger({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof AccordionPrimitive.Trigger>, \"className\"> & { className?: string }) {\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-[panel-open]:rotate-45\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n );\n}\n\nexport function AccordionContent({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof AccordionPrimitive.Panel>, \"className\"> & { className?: string }) {\n return (\n <AccordionPrimitive.Panel className=\"vx-reveal overflow-hidden text-sm text-muted\" {...props}>\n <div className={cn(\"pb-4 pr-8\", className)}>{children}</div>\n </AccordionPrimitive.Panel>\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","baseSource":"import * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\";\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.Positioner align=\"start\" sideOffset={6} className=\"z-[100]\">\n <PopoverPrimitive.Popup initialFocus={false} className={cn(menuPanel, \"w-[var(--anchor-width)] min-w-56 p-0\")}>\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.Popup>\n </PopoverPrimitive.Positioner>\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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { Dialog as SheetPrimitive } from \"@base-ui/react/dialog\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\n/** asChild, as in the Radix version: the child element becomes the trigger. */\nfunction slot(asChild: boolean | undefined, children: React.ReactNode) {\n if (!asChild || !React.isValidElement(children)) return { children };\n const nativeButton = typeof children.type !== \"string\" || children.type === \"button\";\n return { render: children as React.ReactElement<Record<string, unknown>>, nativeButton };\n}\n\nexport const Sheet = SheetPrimitive.Root;\n\nexport function SheetTrigger({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof SheetPrimitive.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <SheetPrimitive.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nexport function SheetClose({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof SheetPrimitive.Close>, \"render\"> & { asChild?: boolean }) {\n return <SheetPrimitive.Close {...props} {...slot(asChild, children)} />;\n}\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}: Omit<React.ComponentProps<typeof SheetPrimitive.Popup>, \"className\"> & { className?: string; side?: keyof typeof sides }) {\n return (\n <SheetPrimitive.Portal>\n <SheetPrimitive.Backdrop className=\"vx-fade fixed inset-0 z-[400] bg-overlay\" />\n <SheetPrimitive.Popup\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.Popup>\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 }: Omit<React.ComponentProps<typeof SheetPrimitive.Title>, \"className\"> & { className?: string }) {\n return <SheetPrimitive.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function SheetDescription({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof SheetPrimitive.Description>, \"className\"> & { className?: string }) {\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","baseSource":"import * as React from \"react\";\nimport { AlertDialog as AD } from \"@base-ui/react/alert-dialog\";\nimport { cn } from \"@/lib/cn\";\nimport { buttonVariants } from \"./button\";\n\n/** asChild, as in the Radix version: the child element becomes the trigger. */\nfunction slot(asChild: boolean | undefined, children: React.ReactNode) {\n if (!asChild || !React.isValidElement(children)) return { children };\n const nativeButton = typeof children.type !== \"string\" || children.type === \"button\";\n return { render: children as React.ReactElement<Record<string, unknown>>, nativeButton };\n}\n\nexport const AlertDialog = AD.Root;\n\nexport function AlertDialogTrigger({\n asChild,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof AD.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <AD.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nexport function AlertDialogContent({ className, ...props }: Omit<React.ComponentProps<typeof AD.Popup>, \"className\"> & { className?: string }) {\n return (\n <AD.Portal>\n <AD.Backdrop className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <AD.Popup\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 }: Omit<React.ComponentProps<typeof AD.Title>, \"className\"> & { className?: string }) {\n return <AD.Title className={cn(\"font-display text-lg font-semibold tracking-tight\", className)} {...props} />;\n}\nexport function AlertDialogDescription({ className, ...props }: Omit<React.ComponentProps<typeof AD.Description>, \"className\"> & { className?: string }) {\n return <AD.Description className={cn(\"text-sm text-muted\", className)} {...props} />;\n}\n\ntype CloseProps = Omit<React.ComponentProps<typeof AD.Close>, \"className\" | \"render\"> & { className?: string; asChild?: boolean };\n\n/** The confirming button. Pass destructive for irreversible actions. It runs onClick, then closes. */\nexport function AlertDialogAction({ className, destructive, asChild, children, ...props }: CloseProps & { destructive?: boolean }) {\n return <AD.Close className={cn(buttonVariants({ variant: destructive ? \"danger\" : \"primary\" }), className)} {...props} {...slot(asChild, children)} />;\n}\nexport function AlertDialogCancel({ className, asChild, children, ...props }: CloseProps) {\n return <AD.Close className={cn(buttonVariants({ variant: \"secondary\" }), className)} {...props} {...slot(asChild, children)} />;\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","baseSource":null},{"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","baseSource":"import * as React from \"react\";\nimport { useRender } from \"@base-ui/react/use-render\";\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, children, ...props }: React.ComponentProps<\"a\"> & { asChild?: boolean }) {\n const child = asChild && React.isValidElement(children) ? (children as React.ReactElement<Record<string, unknown>>) : undefined;\n return useRender({\n defaultTagName: \"a\",\n render: child,\n props: {\n className: cn(\"rounded-sm transition-[color] duration-[var(--dur-micro)] hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\", className),\n children: child ? undefined : children,\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","baseSource":null},{"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","baseSource":null},{"slug":"bar-chart","name":"Bar chart","category":"Charts","description":"Rounded bars on a track. Bars grow in; hover or focus a bar for its value.","deps":[],"uses":[],"usage":"<BarChart data={[{ label: \"Mon\", value: 420 }, { label: \"Tue\", value: 380 }]} highlight={1} />","props":[["data","{ label: string; value: number }[]","required","Bars."],["highlight","number","","Index drawn in the full accent."],["format","(n: number) => string","","Axis and tooltip format."],["height / ticks","number","200 / 4","Plot height and guide lines."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface BarDatum {\n label: string;\n value: number;\n}\n\nexport interface BarChartProps {\n data: BarDatum[];\n height?: number;\n /** Format values in the tooltip and axis. */\n format?: (n: number) => string;\n /** Number of horizontal guide lines. */\n ticks?: number;\n /** Index drawn in the full accent; the rest use a lighter tint. */\n highlight?: number;\n /** Show the value axis on the left. Turn off in tight cards. */\n axis?: boolean;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** Each bar blends up from its partner hue into the accent; solid in Flat and Brutal. */\nconst BAR_FILL = `linear-gradient(to top, ${blendFrom(\"var(--accent)\")}, var(--accent))`;\n\n/** Rounded bars on a track. Bars grow with clip-path, hover or focus a bar for its value. */\nexport function BarChart({ data, height = 200, format = (n) => n.toLocaleString(), ticks = 4, highlight, axis = true, className, ...rest }: BarChartProps) {\n const max = Math.max(...data.map((d) => d.value), 1);\n const top = niceMax(max);\n const [hover, setHover] = React.useState<number | null>(null);\n const [ready, setReady] = React.useState(false);\n React.useEffect(() => {\n const r = requestAnimationFrame(() => setReady(true));\n return () => cancelAnimationFrame(r);\n }, []);\n return (\n <div className={cn(\"grid gap-x-3 gap-y-2\", axis ? \"grid-cols-[auto_minmax(0,1fr)]\" : \"grid-cols-[0_minmax(0,1fr)] gap-x-0\", className)} role=\"img\" aria-label={rest[\"aria-label\"] ?? \"Bar chart\"}>\n <div className={cn(\"relative text-right text-[11px] tabular-nums text-muted\", !axis && \"invisible\")} style={{ height }}>\n {Array.from({ length: ticks + 1 }, (_, i) => (\n <span key={i} className=\"absolute right-0 -translate-y-1/2\" style={{ top: `${(i / ticks) * 100}%` }}>\n {format(Math.round(top - (top / ticks) * i))}\n </span>\n ))}\n </div>\n <div className=\"relative\" style={{ height }}>\n {Array.from({ length: ticks + 1 }, (_, i) => (\n <span key={i} aria-hidden className=\"absolute inset-x-0 border-t border-dashed border-rule\" style={{ top: `${(i / ticks) * 100}%` }} />\n ))}\n <div className=\"absolute inset-0 flex items-stretch gap-[6%] px-[2%]\" onMouseLeave={() => setHover(null)}>\n {data.map((d, i) => {\n const pct = (d.value / top) * 100;\n const on = hover === i || (hover === null && highlight === i);\n return (\n <button\n key={d.label}\n type=\"button\"\n aria-label={`${d.label}: ${format(d.value)}`}\n onMouseEnter={() => setHover(i)}\n onFocus={() => setHover(i)}\n onBlur={() => setHover(null)}\n className=\"group relative flex-1 rounded-[6px] bg-paper-2 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\"\n >\n <span\n className={cn(\n \"absolute inset-0 rounded-[6px] transition-[clip-path,opacity] duration-700 ease-[var(--ease-out)]\",\n on ? \"opacity-100\" : hover !== null ? \"opacity-35\" : \"opacity-60\",\n )}\n style={{ backgroundImage: BAR_FILL, backgroundSize: `100% ${Math.max(pct, 1)}%`, backgroundPosition: \"bottom\", backgroundRepeat: \"no-repeat\", clipPath: `inset(${ready ? 100 - pct : 100}% 0 0 0 round 6px)`, transitionDelay: ready ? \"0ms\" : `${i * 40}ms` }}\n />\n {hover === i && (\n <span className=\"vx-fade-up pointer-events-none absolute left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded-input bg-graphite px-2 py-1 text-xs font-medium text-graphite-ink shadow-pop\" style={{ bottom: `calc(${pct}% + 8px)` }}>\n {d.label} · {format(d.value)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n </div>\n <span />\n <div className=\"flex gap-[6%] px-[2%] text-center text-[11px] text-muted\">\n {data.map((d) => <span key={d.label} className=\"flex-1 truncate\">{d.label}</span>)}\n </div>\n </div>\n );\n}\n\n/** Round a maximum up to a tidy axis value. */\nexport function niceMax(n: number) {\n const p = Math.pow(10, Math.floor(Math.log10(n)));\n const m = n / p;\n return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10) * p;\n}\n\n/* ───────── Shared chart helpers, used by the other charts ───────── */\n\n/** A point on a chart, in SVG units. */\nexport type Point = readonly [number, number];\n\nfunction sign(n: number) {\n return n < 0 ? -1 : 1;\n}\n\n/**\n * A smooth SVG path through the points, using monotone cubic interpolation (the \"monotone\" curve most chart\n * libraries default to). It bends gently through every point without overshooting, so a line never dips\n * below a real minimum or rises above a real maximum between two values.\n */\nexport function smoothPath(pts: readonly Point[]): string {\n const n = pts.length;\n if (!n) return \"\";\n if (n < 3) return pts.map(([x, y], i) => `${i ? \"L\" : \"M\"} ${x} ${y}`).join(\" \");\n\n // Slope of each segment, then a tangent at each point that keeps the curve monotone between points.\n const h = pts.slice(1).map(([x], i) => x - pts[i][0]);\n const s = pts.slice(1).map(([, y], i) => (y - pts[i][1]) / (h[i] || 1));\n const t = pts.map((_, i) => {\n if (i === 0 || i === n - 1) return 0;\n const [s0, s1, h0, h1] = [s[i - 1], s[i], h[i - 1], h[i]];\n const p = (s0 * h1 + s1 * h0) / (h0 + h1);\n return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;\n });\n // The ends lean into their neighbour instead of starting flat.\n t[0] = (3 * s[0] - t[1]) / 2;\n t[n - 1] = (3 * s[n - 2] - t[n - 2]) / 2;\n\n let d = `M ${pts[0][0]} ${pts[0][1]}`;\n for (let i = 0; i < n - 1; i++) {\n const [x0, y0] = pts[i];\n const [x1, y1] = pts[i + 1];\n const k = h[i] / 3;\n d += ` C ${x0 + k} ${y0 + t[i] * k}, ${x1 - k} ${y1 - t[i + 1] * k}, ${x1} ${y1}`;\n }\n return d;\n}\n\n/**\n * The partner colour a series blends from: the same colour turned around the hue wheel and lifted, by amounts\n * each style sets (--chart-shift, --chart-lift). In Flat and Brutal both are 0, so charts stay solid.\n */\nexport function blendFrom(color: string) {\n return `oklch(from ${color} calc(l + var(--chart-lift, 0)) c calc(h + var(--chart-shift, 0)))`;\n}\n","baseSource":null},{"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","baseSource":null},{"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","baseSource":null},{"slug":"sparkline","name":"Sparkline","category":"Charts","description":"A small trend line for tables, stats and cards.","deps":[],"uses":["bar-chart"],"usage":"<Sparkline values={[12, 14, 13, 18, 21]} area showLast />","props":[["values","number[]","required","The series."],["area / showLast","boolean","false","Fill and last-point marker."],["width / height / color","number / number / string","120 / 36 / accent","Size and colour."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { blendFrom, smoothPath } from \"./bar-chart\";\n\nexport interface SparklineProps {\n values: number[];\n width?: number;\n height?: number;\n /** Fill under the line. */\n area?: boolean;\n /** Any CSS colour; defaults to the accent. */\n color?: string;\n /** Mark the last point. */\n showLast?: boolean;\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** A small trend line for tables, stats and cards. */\nexport function Sparkline({ values, width = 120, height = 36, area, color = \"var(--accent)\", showLast, className, ...rest }: SparklineProps) {\n const id = React.useId().replace(/:/g, \"\");\n const min = Math.min(...values);\n const max = Math.max(...values);\n const pts = values.map((v, i) => [(i / (values.length - 1)) * width, height - 3 - ((v - min) / (max - min || 1)) * (height - 6)] as const);\n const line = smoothPath(pts);\n const last = pts[pts.length - 1];\n return (\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width={width}\n height={height}\n className={cn(\"overflow-visible\", className)}\n role=\"img\"\n aria-label={rest[\"aria-label\"] ?? `Trend from ${values[0]} to ${values[values.length - 1]}`}\n >\n <defs>\n <linearGradient id={`${id}-s`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n <stop offset=\"0%\" style={{ stopColor: blendFrom(color) }} />\n <stop offset=\"100%\" style={{ stopColor: color }} />\n </linearGradient>\n {area && (\n <linearGradient id={id} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" style={{ stopColor: color, stopOpacity: \"var(--chart-area-a)\" }} />\n <stop offset=\"100%\" style={{ stopColor: color, stopOpacity: 0 }} />\n </linearGradient>\n )}\n </defs>\n {area && <path d={`${line} L ${width} ${height} L 0 ${height} Z`} fill={`url(#${id})`} />}\n <path d={line} fill=\"none\" stroke={`url(#${id}-s)`} strokeWidth=\"1.75\" strokeLinejoin=\"round\" strokeLinecap=\"round\" pathLength={1} className=\"vx-draw\" />\n {showLast && <circle cx={last[0]} cy={last[1]} r=\"2.75\" fill={color} stroke=\"var(--paper)\" strokeWidth=\"1.5\" />}\n </svg>\n );\n}\n","baseSource":null},{"slug":"line-chart","name":"Line chart","category":"Charts","description":"Lines with a y axis and legend. Dash a series for targets or last period. Hover or arrow through points.","deps":[],"uses":["bar-chart"],"usage":"<LineChart labels={months} series={[{ name: \"This year\", values }, { name: \"Last year\", values: prev, dashed: true }]} />","props":[["labels","string[]","required","X axis labels."],["series","{ name; values: number[]; color?; dashed? }[]","required","One or more lines."],["curved","boolean","true","Smooth curves or straight segments."],["legend","boolean","true","Show the legend."],["format","(n: number) => string","","Axis and tooltip format."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { blendFrom, niceMax, smoothPath, type Point } from \"./bar-chart\";\n\nexport interface LineSeries {\n name: string;\n values: number[];\n color?: string;\n /** Draw as a dashed line, for targets or last period. */\n dashed?: boolean;\n}\n\nexport interface LineChartProps {\n labels: string[];\n series: LineSeries[];\n height?: number;\n /** Smooth curves instead of straight segments. */\n curved?: boolean;\n /** Show the legend above the plot. */\n legend?: boolean;\n format?: (n: number) => string;\n className?: string;\n \"aria-label\"?: string;\n}\n\nconst palette = [\"var(--accent)\", \"var(--chart-2)\", \"var(--chart-3)\", \"var(--chart-4)\", \"var(--chart-5)\"];\n\nfunction toPath(pts: Point[], curved: boolean) {\n return curved ? smoothPath(pts) : pts.map(([x, y], i) => `${i ? \"L\" : \"M\"} ${x} ${y}`).join(\" \");\n}\n\n/** Lines with a y axis, a legend and dashed comparison series. Hover or arrow through points. */\nexport function LineChart({ labels, series, height = 240, curved = true, legend = true, format = (n) => n.toLocaleString(), className, ...rest }: LineChartProps) {\n const W = 600;\n const H = height;\n const top = niceMax(Math.max(...series.flatMap((s) => s.values), 1));\n const [hover, setHover] = React.useState<number | null>(null);\n const ref = React.useRef<SVGSVGElement>(null);\n const id = React.useId().replace(/:/g, \"\");\n const step = W / Math.max(1, labels.length - 1);\n const color = (s: LineSeries, i: number) => s.color ?? palette[i % palette.length];\n const y = (v: number) => H - (v / top) * H;\n const pick = (clientX: number) => {\n const r = ref.current!.getBoundingClientRect();\n setHover(Math.max(0, Math.min(labels.length - 1, Math.round((((clientX - r.left) / r.width) * W) / step))));\n };\n const ticks = [1, 0.75, 0.5, 0.25, 0];\n return (\n <div className={cn(\"grid gap-3\", className)}>\n {legend && (\n <ul className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-ink-2\">\n {series.map((s, i) => (\n <li key={s.name} className=\"flex items-center gap-1.5\">\n <span aria-hidden className={cn(\"h-0.5 w-3 rounded-full\", s.dashed && \"bg-transparent border-t-2 border-dashed\")} style={s.dashed ? { borderColor: color(s, i) } : { background: color(s, i) }} />\n {s.name}\n </li>\n ))}\n </ul>\n )}\n <div className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-3\">\n <div className=\"flex flex-col justify-between text-right text-[11px] tabular-nums text-muted\" style={{ height: H }} aria-hidden>\n {ticks.map((t) => <span key={t} className=\"-translate-y-1/2 first:translate-y-0 last:translate-y-0\">{format(Math.round(top * t))}</span>)}\n </div>\n <div className=\"relative\">\n <svg\n ref={ref}\n viewBox={`0 0 ${W} ${H}`}\n preserveAspectRatio=\"none\"\n className=\"block w-full overflow-visible outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-focus\"\n style={{ height: H }}\n tabIndex={0}\n role=\"img\"\n aria-label={rest[\"aria-label\"] ?? \"Line chart\"}\n onMouseMove={(e) => pick(e.clientX)}\n onMouseLeave={() => setHover(null)}\n onBlur={() => setHover(null)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowRight\") setHover((h) => Math.min(labels.length - 1, (h ?? -1) + 1));\n if (e.key === \"ArrowLeft\") setHover((h) => Math.max(0, (h ?? labels.length) - 1));\n }}\n >\n {ticks.map((t) => (\n <line key={t} x1=\"0\" x2={W} y1={(1 - t) * H} y2={(1 - t) * H} stroke=\"var(--rule)\" strokeDasharray={t === 0 ? undefined : \"4 4\"} vectorEffect=\"non-scaling-stroke\" />\n ))}\n {hover !== null && <line x1={hover * step} x2={hover * step} y1=\"0\" y2={H} stroke=\"var(--rule-2)\" vectorEffect=\"non-scaling-stroke\" />}\n <defs>\n {series.map((s, i) => (\n <React.Fragment key={s.name}>\n <linearGradient id={`${id}-s${i}`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n <stop offset=\"0%\" style={{ stopColor: blendFrom(color(s, i)) }} />\n <stop offset=\"100%\" style={{ stopColor: color(s, i) }} />\n </linearGradient>\n <linearGradient id={`${id}-f${i}`} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" style={{ stopColor: color(s, i), stopOpacity: \"var(--chart-area-a)\" }} />\n <stop offset=\"100%\" style={{ stopColor: color(s, i), stopOpacity: 0 }} />\n </linearGradient>\n </React.Fragment>\n ))}\n </defs>\n {series.map((s, i) =>\n s.dashed || i > 0 ? null : (\n <path key={s.name} d={`${toPath(s.values.map((v, j) => [j * step, y(v)] as const), curved)} L ${(s.values.length - 1) * step} ${H} L 0 ${H} Z`} fill={`url(#${id}-f${i})`} className=\"vx-fade-up\" />\n ),\n )}\n <g className=\"vx-wipe\">\n {series.map((s, i) => (\n <path\n key={s.name}\n d={toPath(s.values.map((v, j) => [j * step, y(v)] as const), curved)}\n fill=\"none\"\n stroke={s.dashed ? color(s, i) : `url(#${id}-s${i})`}\n strokeWidth=\"2.25\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeDasharray={s.dashed ? \"6 6\" : undefined}\n vectorEffect=\"non-scaling-stroke\"\n />\n ))}\n </g>\n </svg>\n {hover !== null && (\n <>\n {series.map((s, i) => (\n <span\n key={s.name}\n aria-hidden\n className=\"pointer-events-none absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-paper\"\n style={{ left: `${(hover / Math.max(1, labels.length - 1)) * 100}%`, top: y(s.values[hover]), background: color(s, i) }}\n />\n ))}\n <div\n className=\"vx-fade-up pointer-events-none absolute top-0 z-10 grid min-w-32 gap-1 rounded-card border border-pop-border bg-pop p-2.5 text-xs shadow-pop\"\n style={{ left: `${(hover / Math.max(1, labels.length - 1)) * 100}%`, transform: `translateX(${hover > labels.length / 2 ? \"calc(-100% - 12px)\" : \"12px\"})` }}\n >\n <span className=\"font-medium text-ink\">{labels[hover]}</span>\n {series.map((s, i) => (\n <span key={s.name} className=\"flex items-center gap-2 text-ink-2\">\n <span className=\"size-2 rounded-full\" style={{ background: color(s, i) }} />\n {s.name}\n <span className=\"ml-auto font-medium tabular-nums text-ink\">{format(s.values[hover])}</span>\n </span>\n ))}\n </div>\n </>\n )}\n <div className=\"mt-2 flex justify-between text-[11px] text-muted\" aria-hidden>\n {labels.map((l, i) => <span key={i}>{l}</span>)}\n </div>\n </div>\n </div>\n </div>\n );\n}\n","baseSource":null},{"slug":"bar-list","name":"Bar list","category":"Charts","description":"Ranked rows with a bar behind each name, for top pages, sources or countries.","deps":[],"uses":[],"usage":"<BarList columns={[\"Page\", \"Visits\"]} items={[{ name: \"/pricing\", value: 3210 }]} limit={5} />","props":[["items","{ name; value; icon?; href? }[]","required","The rows."],["sorted","boolean","true","Largest first."],["limit","number","","Show this many, with a toggle for the rest."],["columns","[string, string]","","Header labels."],["format","(n: number) => string","","Value format."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface BarListItem {\n name: string;\n value: number;\n /** An icon or favicon shown before the name. */\n icon?: React.ReactNode;\n href?: string;\n}\n\nexport interface BarListProps {\n items: BarListItem[];\n /** Sort from largest to smallest. */\n sorted?: boolean;\n /** Show only the first n, with a toggle for the rest. */\n limit?: number;\n format?: (n: number) => string;\n /** Header labels for the name and value columns. */\n columns?: [string, string];\n className?: string;\n}\n\n/** Ranked rows with a bar behind each name, for top pages, sources or countries. */\nexport function BarList({ items, sorted = true, limit, format = (n) => n.toLocaleString(), columns, className }: BarListProps) {\n const [all, setAll] = React.useState(false);\n const rows = sorted ? [...items].sort((a, b) => b.value - a.value) : items;\n const shown = limit && !all ? rows.slice(0, limit) : rows;\n const max = Math.max(...items.map((i) => i.value), 1);\n return (\n <div className={cn(\"grid gap-1.5\", className)}>\n {columns && (\n <div className=\"flex justify-between px-2 pb-1 text-xs text-muted\">\n <span>{columns[0]}</span>\n <span>{columns[1]}</span>\n </div>\n )}\n <ul className=\"grid gap-1.5\">\n {shown.map((item, i) => {\n const body = (\n <>\n <span\n aria-hidden\n className=\"vx-grow-x absolute inset-y-0 left-0 rounded-sm bg-accent/14 group-hover:bg-accent/22\"\n style={{ width: `${(item.value / max) * 100}%`, animationDelay: `${i * 40}ms` }}\n />\n <span className=\"relative flex min-w-0 items-center gap-2 [&_svg]:size-4 [&_svg]:text-muted\">\n {item.icon}\n <span className=\"truncate\">{item.name}</span>\n </span>\n <span className=\"relative tabular-nums text-ink\">{format(item.value)}</span>\n </>\n );\n const row = \"group relative flex h-8 items-center justify-between gap-3 px-2 text-sm text-ink-2\";\n return (\n <li key={item.name}>\n {item.href ? (\n <a href={item.href} className={cn(row, \"rounded-sm hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\")}>{body}</a>\n ) : (\n <div className={row}>{body}</div>\n )}\n </li>\n );\n })}\n </ul>\n {limit && rows.length > limit && (\n <button\n type=\"button\"\n onClick={() => setAll((v) => !v)}\n className=\"justify-self-start rounded-sm px-2 py-1 text-xs font-medium text-accent hover:underline focus-visible:outline-2 focus-visible:outline-focus\"\n >\n {all ? \"Show less\" : `Show all ${rows.length}`}\n </button>\n )}\n </div>\n );\n}\n","baseSource":null},{"slug":"stage-bars","name":"Stage bars","category":"Charts","description":"Horizontal bars split into stages, such as a pipeline per rep or sleep phases per night.","deps":[],"uses":[],"usage":"<StageBars rows={[{ label: \"Ana\", stages: [{ label: \"Lead\", value: 40 }, { label: \"Won\", value: 18 }] }]} />","props":[["rows","{ label; stages: { label; value; color? }[] }[]","required","Same stages in every row."],["legend","boolean","true","Show the stage legend."]],"pro":false,"source":"import { cn } from \"@/lib/cn\";\n\nexport interface Stage {\n label: string;\n value: number;\n color?: string;\n}\n\nexport interface StageBarsProps {\n /** One row per item, each split into the same stages. */\n rows: { label: string; stages: Stage[] }[];\n format?: (n: number) => string;\n /** Show the stage legend. */\n legend?: boolean;\n className?: string;\n}\n\nconst palette = [\"var(--accent)\", \"var(--chart-3)\", \"var(--chart-2)\", \"var(--chart-5)\", \"var(--chart-4)\"];\n\n/** Horizontal bars split into stages, such as sleep phases or a deal pipeline per rep. */\nexport function StageBars({ rows, format = (n) => n.toLocaleString(), legend = true, className }: StageBarsProps) {\n const max = Math.max(...rows.map((r) => r.stages.reduce((a, s) => a + s.value, 0)), 1);\n const stageNames = rows[0]?.stages ?? [];\n return (\n <div className={cn(\"grid gap-3\", className)}>\n {legend && (\n <ul className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-ink-2\">\n {stageNames.map((s, i) => <li key={s.label} className=\"flex items-center gap-1.5\"><span aria-hidden className=\"size-2.5 rounded-[3px]\" style={{ background: s.color ?? palette[i % 5] }} />{s.label}</li>)}\n </ul>\n )}\n <ul className=\"grid gap-2.5\">\n {rows.map((r, ri) => {\n const total = r.stages.reduce((a, s) => a + s.value, 0);\n return (\n <li key={r.label} className=\"grid grid-cols-[88px_minmax(0,1fr)_auto] items-center gap-3 text-sm\">\n <span className=\"truncate text-ink-2\">{r.label}</span>\n <span\n className=\"vx-grow-x flex h-3 gap-0.5 overflow-hidden rounded-full\"\n style={{ width: `${(total / max) * 100}%`, animationDelay: `${ri * 60}ms` }}\n role=\"img\"\n aria-label={`${r.label}: ${r.stages.map((s) => `${s.label} ${format(s.value)}`).join(\", \")}`}\n >\n {r.stages.map((s, i) => <span key={s.label} title={`${s.label}: ${format(s.value)}`} className=\"h-full first:rounded-l-full last:rounded-r-full\" style={{ flexGrow: s.value, background: s.color ?? palette[i % 5] }} />)}\n </span>\n <span className=\"text-right tabular-nums text-ink\">{format(total)}</span>\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n","baseSource":null},{"slug":"chart-card","name":"Chart card","category":"Charts","description":"The shell every chart card shares: title, number and change, the chart on a white panel, a footer.","deps":["lucide-react"],"uses":[],"usage":"<ChartCard title=\"Active users\" value=\"12,480\" delta={9.4} action={<Segmented … />}>\n <LineChart … />\n</ChartCard>","props":[["title / value","string / ReactNode","required","Header text and number."],["delta / invert","number / boolean","","Change pill."],["action","ReactNode","","Header controls."],["footer","ReactNode","","A takeaway under the chart."]],"pro":false,"source":"import * as React from \"react\";\nimport { TrendingDown, TrendingUp } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ChartCardProps {\n title: string;\n /** The headline number. */\n value?: React.ReactNode;\n /** Percent change, shown as a pill beside the value. */\n delta?: number;\n invert?: boolean;\n /** Text after the value, such as \"this week\". */\n caption?: React.ReactNode;\n /** Controls in the header, such as a period switcher. */\n action?: React.ReactNode;\n /** A white strip under the chart for a takeaway or a link. */\n footer?: React.ReactNode;\n children: React.ReactNode;\n /** Drop the card and the inner panel, for a chart that already sits on a surface of its own. */\n flat?: boolean;\n className?: string;\n}\n\n/** The shell every chart card shares: a grey card, the title and number, the chart on a white panel, and an optional footer. */\nexport function ChartCard({ title, value, delta, invert, caption, action, footer, children, flat, className }: ChartCardProps) {\n const good = delta === undefined ? undefined : invert ? delta < 0 : delta >= 0;\n const Trend = delta !== undefined && delta < 0 ? TrendingDown : TrendingUp;\n return (\n <section aria-label={title} className={cn(\"flex min-w-0 flex-col text-ink\", flat ? \"gap-4\" : \"vx-glass gap-2 rounded-panel border border-card-border bg-card p-2 shadow-card\", className)}>\n <header className={cn(\"flex items-start justify-between gap-3\", !flat && \"px-2.5 pt-2\")}>\n <div className=\"grid min-w-0 gap-1\">\n <h3 className=\"text-sm font-medium text-muted\">{title}</h3>\n {value !== undefined && (\n <p className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n <span className=\"text-2xl font-semibold tracking-[-0.02em] tabular-nums\">{value}</span>\n {delta !== undefined && (\n <span className={cn(\"flex items-center gap-0.5 self-center rounded-full py-0.5 pl-1 pr-1.5 text-xs font-semibold\", good ? \"bg-success-wash text-success-wash-ink\" : \"bg-danger-wash text-danger-wash-ink\")}>\n <Trend className=\"size-3\" /> {Math.abs(delta)}%\n </span>\n )}\n {caption && <span className=\"text-sm text-muted\">{caption}</span>}\n </p>\n )}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </header>\n <div className={cn(!flat && \"rounded-input bg-paper p-3 shadow-[0_1px_1px_oklch(0%_0_0/0.05)]\")}>{children}</div>\n {footer && <div className={cn(\"text-sm text-muted\", flat ? \"border-t border-rule pt-3\" : \"px-2.5 pb-1\")}>{footer}</div>}\n </section>\n );\n}\n","baseSource":null},{"slug":"steps-card","name":"Steps card","category":"Charts","description":"Daily steps against a dashed goal line. Days over the goal are lit.","deps":[],"uses":["chart-card"],"usage":"<StepsCard goal={10000} days={[{ label: \"Mon\", value: 8420 }]} />","props":[["days","{ label; value }[]","a sample week","One bar per day."],["goal","number","10000","Where the dashed line sits."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ChartCard } from \"./chart-card\";\n\nexport interface StepsCardProps {\n days?: { label: string; value: number }[];\n goal?: number;\n className?: string;\n}\n\nconst week = [\n { label: \"Mon\", value: 8420 }, { label: \"Tue\", value: 11240 }, { label: \"Wed\", value: 6310 }, { label: \"Thu\", value: 10380 },\n { label: \"Fri\", value: 12950 }, { label: \"Sat\", value: 4120 }, { label: \"Sun\", value: 9060 },\n];\n\n/** Daily steps against a dashed goal line. Days over the goal are filled; hover a day for its count. */\nexport function StepsCard({ days = week, goal = 10000, className }: StepsCardProps) {\n const [hover, setHover] = React.useState<number | null>(null);\n const max = Math.max(goal * 1.25, ...days.map((d) => d.value));\n const total = days.reduce((a, d) => a + d.value, 0);\n const hit = days.filter((d) => d.value >= goal).length;\n const H = 150;\n return (\n <ChartCard className={className} title=\"Steps\" value={Math.round(total / days.length).toLocaleString()} caption=\"a day on average\" footer={`Goal met on ${hit} of ${days.length} days.`}>\n <div className=\"relative\" style={{ height: H }}>\n <span aria-hidden className=\"absolute inset-x-0 border-t border-dashed border-muted/60\" style={{ bottom: (goal / max) * H }}>\n <span className=\"absolute -top-2.5 right-0 bg-paper pl-1 text-[11px] text-muted\">{goal.toLocaleString()}</span>\n </span>\n <ul className=\"relative flex h-full items-end gap-2\" aria-label=\"Steps by day\">\n {days.map((d, i) => {\n const over = d.value >= goal;\n return (\n <li\n key={d.label}\n className=\"relative flex h-full flex-1 items-end\"\n onMouseEnter={() => setHover(i)}\n onMouseLeave={() => setHover(null)}\n tabIndex={0}\n onFocus={() => setHover(i)}\n onBlur={() => setHover(null)}\n aria-label={`${d.label}: ${d.value.toLocaleString()} steps`}\n >\n <span\n className={cn(\"vx-rise w-full rounded-[6px] transition-opacity duration-[var(--dur-micro)]\", over ? \"bg-accent [background-image:var(--control-on-image)]\" : \"bg-accent/25\", hover !== null && hover !== i && \"opacity-50\")}\n style={{ height: `${(d.value / max) * 100}%`, transformOrigin: \"bottom\", animationDelay: `${i * 40}ms` }}\n />\n {hover === i && (\n <span className=\"pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 -translate-x-1/2 whitespace-nowrap rounded-input border border-pop-border bg-pop px-2 py-1 text-xs font-medium tabular-nums shadow-pop\">{d.value.toLocaleString()}</span>\n )}\n </li>\n );\n })}\n </ul>\n </div>\n <div aria-hidden className=\"mt-2 flex gap-2 text-center text-[11px] text-muted\">{days.map((d) => <span key={d.label} className=\"flex-1\">{d.label}</span>)}</div>\n </ChartCard>\n );\n}\n","baseSource":null},{"slug":"active-days-card","name":"Most active days","category":"Charts","description":"Seven columns for the week with the busiest day lit and named.","deps":[],"uses":["chart-card"],"usage":"<ActiveDaysCard values={[62, 78, 96, 84, 70, 34, 28]} unit=\"sessions\" />","props":[["values","number[]","a sample week","Monday first."],["unit","string","\"sessions\"","Used in labels."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ChartCard } from \"./chart-card\";\n\nexport interface ActiveDaysCardProps {\n /** Seven values, Monday first. */\n values?: number[];\n /** Unit for the tooltip, such as \"sessions\". */\n unit?: string;\n className?: string;\n}\n\nconst days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst long = [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"];\n\n/** Which weekday is busiest: seven rounded columns with the peak lit and named. */\nexport function ActiveDaysCard({ values = [62, 78, 96, 84, 70, 34, 28], unit = \"sessions\", className }: ActiveDaysCardProps) {\n const [hover, setHover] = React.useState<number | null>(null);\n const max = Math.max(...values, 1);\n const peak = values.indexOf(max);\n const shown = hover ?? peak;\n return (\n <ChartCard className={className} title=\"Most active days\" value={long[peak]} caption={`${max} ${unit} on average`}>\n <div className=\"flex h-36 items-end gap-2\">\n {values.map((v, i) => (\n <button\n key={days[i]}\n type=\"button\"\n aria-label={`${long[i]}: ${v} ${unit}`}\n onMouseEnter={() => setHover(i)}\n onMouseLeave={() => setHover(null)}\n onFocus={() => setHover(i)}\n onBlur={() => setHover(null)}\n className=\"group relative flex h-full flex-1 items-end rounded-[8px] bg-paper-2 focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <span\n className={cn(\"vx-rise w-full rounded-[8px] transition-colors duration-[var(--dur-micro)]\", i === shown ? \"bg-accent [background-image:var(--control-on-image)]\" : \"bg-accent/20 group-hover:bg-accent/35\")}\n style={{ height: `${(v / max) * 100}%`, transformOrigin: \"bottom\", animationDelay: `${i * 40}ms` }}\n />\n </button>\n ))}\n </div>\n <div aria-hidden className=\"mt-2 flex gap-2 text-center text-[11px] text-muted\">\n {days.map((d, i) => <span key={d} className={cn(\"flex-1\", i === shown && \"font-medium text-ink\")}>{d}</span>)}\n </div>\n <p className=\"mt-2 text-center text-xs text-muted\" aria-live=\"polite\">{long[shown]}: <b className=\"font-medium tabular-nums text-ink\">{values[shown]}</b> {unit}</p>\n </ChartCard>\n );\n}\n","baseSource":null}],"templates":[]}
|
package/server.mjs
CHANGED
|
@@ -21,11 +21,17 @@ function closure(slug, seen = new Set()) {
|
|
|
21
21
|
return seen;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** "base" serves the Base UI version of each file that has one; everything else is shared. */
|
|
25
|
+
const libOf = (v) => (String(v ?? "").toLowerCase().startsWith("base") ? "base" : "radix");
|
|
26
|
+
|
|
27
|
+
function componentDoc(c, withSource = true, lib = "radix") {
|
|
25
28
|
const files = [...closure(c.slug)];
|
|
26
|
-
const
|
|
29
|
+
const useBase = (f) => lib === "base" && Boolean(bySlug(reg.components, f)?.baseSource);
|
|
30
|
+
const deps = [...new Set(files.flatMap((f) => (bySlug(reg.components, f)?.deps ?? []).map((d) => (d === "radix-ui" && useBase(f) ? "@base-ui/react" : d))))];
|
|
27
31
|
const props = c.props.map(([n, t, d, desc]) => `| \`${n}\` | \`${t}\` | ${d ? `\`${d}\`` : ""} | ${desc} |`).join("\n");
|
|
28
|
-
let out = `# ${c.name}${c.pro ? " (Pro)" : ""}\n\n${c.description}\n\nCategory: ${c.category}\n\n
|
|
32
|
+
let out = `# ${c.name}${c.pro ? " (Pro)" : ""}\n\n${c.description}\n\nCategory: ${c.category}\n\n`;
|
|
33
|
+
if (c.baseSource) out += lib === "base" ? "Primitives: Base UI. The same component on Radix UI: call again with primitives \"radix\".\n\n" : "Primitives: Radix UI. A Base UI version exists: call again with primitives \"base\".\n\n";
|
|
34
|
+
out += "## Install\n\n";
|
|
29
35
|
out += deps.length ? `\`\`\`bash\nnpm i ${deps.join(" ")}\n\`\`\`\n\n` : "No extra dependencies.\n\n";
|
|
30
36
|
out += `Copy into \`src/components/ui/\`: ${files.map((f) => `\`${f}.tsx\``).join(", ")}.\n\n`;
|
|
31
37
|
out += `## Usage\n\n\`\`\`tsx\nimport { ${pascal(c.slug)} } from "@/components/ui/${c.slug}";\n\n${c.usage}\n\`\`\`\n\n`;
|
|
@@ -34,7 +40,8 @@ function componentDoc(c, withSource = true) {
|
|
|
34
40
|
for (const f of files) {
|
|
35
41
|
const dep = bySlug(reg.components, f);
|
|
36
42
|
out += `## src/components/ui/${f}.tsx\n\n`;
|
|
37
|
-
|
|
43
|
+
const src = useBase(f) ? dep.baseSource : dep?.source;
|
|
44
|
+
out += src ? `\`\`\`tsx\n${src}\n\`\`\`\n\n` : `${PRO_NOTE}\n\n`;
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
return out;
|
|
@@ -61,19 +68,19 @@ const tools = {
|
|
|
61
68
|
},
|
|
62
69
|
},
|
|
63
70
|
get_component: {
|
|
64
|
-
description: "Get one component: install command, every file it needs with full source, usage example and props.",
|
|
65
|
-
inputSchema: { type: "object", properties: { slug: { type: "string", description: "Component slug, such as button or dialog." } }, required: ["slug"] },
|
|
66
|
-
run: ({ slug }) => {
|
|
71
|
+
description: "Get one component: install command, every file it needs with full source, usage example and props. Pass primitives \"base\" for the Base UI version.",
|
|
72
|
+
inputSchema: { type: "object", properties: { slug: { type: "string", description: "Component slug, such as button or dialog." }, primitives: { type: "string", enum: ["radix", "base"], description: "Optional: \"radix\" (default) or \"base\" for the Base UI version, where one exists." } }, required: ["slug"] },
|
|
73
|
+
run: ({ slug, primitives }) => {
|
|
67
74
|
const c = bySlug(reg.components, slug);
|
|
68
|
-
return c ? componentDoc(c) : `No component "${slug}". Call list_components to see the slugs.`;
|
|
75
|
+
return c ? componentDoc(c, true, libOf(primitives)) : `No component "${slug}". Call list_components to see the slugs.`;
|
|
69
76
|
},
|
|
70
77
|
},
|
|
71
78
|
get_component_usage: {
|
|
72
79
|
description: "Get a component's usage example and props without the source. Lighter than get_component.",
|
|
73
|
-
inputSchema: { type: "object", properties: { slug: { type: "string" } }, required: ["slug"] },
|
|
74
|
-
run: ({ slug }) => {
|
|
80
|
+
inputSchema: { type: "object", properties: { slug: { type: "string" }, primitives: { type: "string", enum: ["radix", "base"], description: "Optional: \"radix\" (default) or \"base\" for the Base UI version, where one exists." } }, required: ["slug"] },
|
|
81
|
+
run: ({ slug, primitives }) => {
|
|
75
82
|
const c = bySlug(reg.components, slug);
|
|
76
|
-
return c ? componentDoc(c, false) : `No component "${slug}".`;
|
|
83
|
+
return c ? componentDoc(c, false, libOf(primitives)) : `No component "${slug}".`;
|
|
77
84
|
},
|
|
78
85
|
},
|
|
79
86
|
list_templates: {
|