@plankui/mcp 0.1.2 → 0.1.4
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/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":"textarea-counter","name":"Textarea counter","category":"Forms","description":"A multiline field that shows the character count and enforces a length limit.","deps":[],"uses":["textarea"],"usage":"<TextareaCounter aria-label=\"Release note\" value={note} onChange={(event) => setNote(event.currentTarget.value)} maxLength={120} />","props":[["value","string","required","Current field value."],["onChange","ChangeEventHandler<HTMLTextAreaElement>","","Update the value as the user types."],["maxLength","number","required","Maximum length, using the browser’s character counting rules."]],"pro":false,"source":"import { useId, type ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Textarea } from \"./textarea\";\n\nexport interface TextareaCounterProps extends Omit<ComponentPropsWithoutRef<typeof Textarea>, \"defaultValue\" | \"maxLength\" | \"value\"> {\n value: string;\n maxLength: number;\n}\n\nexport function TextareaCounter({ value, maxLength, className, \"aria-describedby\": description, ...props }: TextareaCounterProps) {\n const counterId = useId();\n return (\n <div className=\"grid gap-1.5\">\n <Textarea\n {...props}\n value={value}\n maxLength={maxLength}\n aria-describedby={description ? `${description} ${counterId}` : counterId}\n className={cn(\"w-full\", className)}\n />\n <span id={counterId} className=\"justify-self-end text-xs tabular-nums text-muted\">\n {value.length} / {maxLength}<span className=\"sr-only\"> characters</span>\n </span>\n </div>\n );\n}\n","baseSource":null},{"slug":"status-indicator","name":"Status indicator","category":"Feedback","description":"A colored dot and text label for service health, availability or job state.","deps":[],"uses":[],"usage":"<StatusIndicator label=\"Operational\" tone=\"success\" />","props":[["label","string","required","Visible status text."],["tone","\"neutral\" | \"success\" | \"warning\" | \"danger\"","\"neutral\"","Color of the dot."]],"pro":false,"source":"import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nconst tones = {\n neutral: \"bg-muted\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-danger\",\n};\n\nexport interface StatusIndicatorProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n label: string;\n tone?: keyof typeof tones;\n}\n\nexport function StatusIndicator({ label, tone = \"neutral\", className, ...props }: StatusIndicatorProps) {\n return (\n <span className={cn(\"inline-flex items-center gap-2 text-sm font-medium text-ink\", className)} {...props}>\n <span aria-hidden=\"true\" className={cn(\"size-2 shrink-0 rounded-full\", tones[tone])} />\n {label}\n </span>\n );\n}\n","baseSource":null},{"slug":"scroll-area","name":"Scroll area","category":"Display","description":"A keyboard-focusable region for scrolling lists or panels within a limited height.","deps":[],"uses":[],"usage":"<ScrollArea aria-label=\"Project activity\" className=\"max-h-36\">{activity}</ScrollArea>","props":[["aria-label","string","required","Accessible name for the scroll region."],["className","string","","Override the default maximum height or style the region."],["children","ReactNode","","Content inside the scroll region."]],"pro":false,"source":"import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ScrollAreaProps extends ComponentPropsWithoutRef<\"div\"> {\n \"aria-label\": string;\n}\n\nexport function ScrollArea({ className, ...props }: ScrollAreaProps) {\n return (\n <div\n role=\"region\"\n tabIndex={0}\n className={cn(\"max-h-48 overflow-auto rounded-card border border-rule bg-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\", className)}\n {...props}\n />\n );\n}\n","baseSource":null},{"slug":"native-select","name":"Native select","category":"Forms","description":"A styled browser select that keeps the platform’s own picker and keyboard behavior.","deps":["lucide-react"],"uses":[],"usage":"<NativeSelect aria-label=\"Time zone\" defaultValue=\"utc\">\n <option value=\"utc\">Coordinated Universal Time</option>\n <option value=\"kolkata\">Asia/Kolkata</option>\n</NativeSelect>","props":[["size","\"sm\" | \"md\"","\"md\"","Control height."],["wrapperClassName","string","","Styles the wrapper around the select and chevron."]],"pro":false,"source":"import * as React from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface NativeSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, \"size\"> {\n /** Classes for the wrapper; className styles the select itself. */\n wrapperClassName?: string;\n size?: \"sm\" | \"md\";\n}\n\n/**\n * The browser's own select, restyled. The arrow is ours, inset from the edge with room to breathe,\n * so it never crowds the border the way the default arrow does.\n */\nexport const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(\n ({ className, wrapperClassName, size = \"md\", children, ...props }, ref) => (\n <span className={cn(\"relative inline-flex\", wrapperClassName)}>\n <select\n ref={ref}\n className={cn(\n \"vx-glass w-full cursor-pointer appearance-none rounded-input border border-field-border bg-field text-ink shadow-field\",\n \"outline-none focus-visible:field-focus disabled:cursor-not-allowed disabled:opacity-50\",\n size === \"md\" ? \"h-9 pl-3 pr-10 text-sm\" : \"h-8 pl-2.5 pr-9 text-sm\",\n className,\n )}\n {...props}\n >\n {children}\n </select>\n <ChevronDown\n aria-hidden\n className={cn(\"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-muted\", size === \"md\" ? \"right-3.5\" : \"right-3\")}\n />\n </span>\n ),\n);\nNativeSelect.displayName = \"NativeSelect\";\n","baseSource":null},{"slug":"disclosure","name":"Disclosure","category":"Display","description":"A single expandable section for shipping details, notes or other supporting information.","deps":["lucide-react"],"uses":[],"usage":"<Disclosure summary=\"Shipping details\">Orders ship within two business days.</Disclosure>","props":[["summary","ReactNode","required","Visible label for the toggle."],["open","boolean","false","Shows the panel expanded when the attribute is present."],["children","ReactNode","","Content inside the panel."]],"pro":false,"source":"import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface DisclosureProps extends ComponentPropsWithoutRef<\"details\"> {\n summary: ReactNode;\n}\n\n/** A single expandable section with native keyboard and focus behavior. */\nexport function Disclosure({ summary, children, className, ...props }: DisclosureProps) {\n return (\n <details className={cn(\"group rounded-card border border-rule bg-card text-sm text-ink\", className)} {...props}>\n <summary className=\"flex cursor-pointer list-none items-center justify-between gap-3 rounded-card px-4 py-3 font-medium focus-visible:outline-2 focus-visible:outline-focus [&::-webkit-details-marker]:hidden\">\n {summary}\n <ChevronDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted group-open:rotate-180\" />\n </summary>\n <div className=\"border-t border-rule px-4 py-3 text-ink-2\">{children}</div>\n </details>\n );\n}\n","baseSource":null},{"slug":"description-list","name":"Description list","category":"Display","description":"A responsive list of labeled values for account details, specifications or summaries.","deps":[],"uses":[],"usage":"<DescriptionList items={[{ id: \"plan\", term: \"Plan\", description: \"Studio\" }]} />","props":[["items","{ id: string; term: ReactNode; description: ReactNode }[]","required","Rows with stable IDs, labels and values."]],"pro":false,"source":"import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface DescriptionListProps extends Omit<ComponentPropsWithoutRef<\"dl\">, \"children\"> {\n items: readonly { id: string; term: ReactNode; description: ReactNode }[];\n}\n\n/** A list of labeled values, grouped with native definition-list semantics. */\nexport function DescriptionList({ items, className, ...props }: DescriptionListProps) {\n return (\n <dl className={cn(\"divide-y divide-rule rounded-card border border-rule bg-card px-4 text-sm\", className)} {...props}>\n {items.map(({ id, term, description }) => (\n <div key={id} className=\"grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-3 py-3 [overflow-wrap:anywhere]\">\n <dt className=\"text-muted\">{term}</dt>\n <dd className=\"text-ink\">{description}</dd>\n </div>\n ))}\n </dl>\n );\n}\n","baseSource":null},{"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":"toggle","name":"Toggle","category":"Actions","description":"A two-state button for formatting and view options.","deps":["radix-ui","class-variance-authority"],"uses":[],"usage":"<Toggle aria-label=\"Bold\"><Bold /></Toggle>","props":[["pressed / defaultPressed","boolean","","State."],["onPressedChange","(pressed: boolean) => void","","Called on toggle."],["variant","\"ghost\" | \"outline\"","\"ghost\"","Visual style."],["size","\"sm\" | \"md\" | \"lg\"","\"md\"","Height."]],"pro":false,"source":"import * as React from \"react\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleVariants = cva(\n [\n \"inline-flex items-center justify-center gap-1.5 rounded-input text-sm font-medium text-ink-2\",\n \"transition-[background-color,color,box-shadow,border-color] duration-[var(--dur-micro)] hover:bg-paper-3 hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:pointer-events-none disabled:opacity-50\",\n \"data-[state=on]:bg-accent-wash data-[state=on]:text-accent-wash-ink [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n ghost: \"\",\n outline: \"vx-glass border border-[color:var(--key-border)] bg-key-face shadow-key data-[state=on]:border-accent/40\",\n },\n size: { sm: \"h-8 min-w-8 px-2\", md: \"h-9 min-w-9 px-2.5\", lg: \"h-11 min-w-11 px-3\" },\n },\n defaultVariants: { variant: \"ghost\", size: \"md\" },\n },\n);\n\nexport function Toggle({\n className,\n variant,\n size,\n ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) {\n return <TogglePrimitive.Root className={cn(toggleVariants({ variant, size }), className)} {...props} />;\n}\n","baseSource":"import * as React from \"react\";\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleVariants = cva(\n [\n \"inline-flex items-center justify-center gap-1.5 rounded-input text-sm font-medium text-ink-2\",\n \"transition-[background-color,color,box-shadow,border-color] duration-[var(--dur-micro)] hover:bg-paper-3 hover:text-ink\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n \"data-[pressed]:bg-accent-wash data-[pressed]:text-accent-wash-ink [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n ghost: \"\",\n outline: \"vx-glass border border-[color:var(--key-border)] bg-key-face shadow-key data-[pressed]:border-accent/40\",\n },\n size: { sm: \"h-8 min-w-8 px-2\", md: \"h-9 min-w-9 px-2.5\", lg: \"h-11 min-w-11 px-3\" },\n },\n defaultVariants: { variant: \"ghost\", size: \"md\" },\n },\n);\n\nexport function Toggle({\n className,\n variant,\n size,\n ...props\n}: Omit<React.ComponentProps<typeof TogglePrimitive>, \"className\"> & { className?: string } & VariantProps<typeof toggleVariants>) {\n return <TogglePrimitive className={cn(toggleVariants({ variant, size }), className)} {...props} />;\n}\n"},{"slug":"command","name":"Command","category":"Actions","description":"A searchable, keyboard-driven list of actions. CommandDialog wraps it in a modal with a hotkey.","deps":["radix-ui","lucide-react"],"uses":["kbd"],"usage":"<Command groups={[{ heading: \"Settings\", items: [{ value: \"profile\", label: \"Profile\" }] }]} />\n<CommandDialog open={open} onOpenChange={setOpen} groups={groups} />","props":[["groups","{ heading?: string; items: CommandItem[] }[]","required","Grouped items."],["CommandItem","{ value, label, icon?, shortcut?, keywords?, disabled?, onSelect? }","","One row."],["onSelect","(item) => void","","Called when an item is chosen."],["CommandDialog.hotkey","string | false","\"k\"","Letter that opens it with ⌘ or Ctrl."]],"pro":false,"source":"import * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport { Search } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuCheck, menuItem, menuLabel, menuPanel } from \"./menu-styles\";\nimport { Kbd } from \"./kbd\";\n\nexport interface CommandItem {\n value: string;\n label: string;\n icon?: React.ReactNode;\n shortcut?: string;\n /** Extra words the filter should match. */\n keywords?: string[];\n disabled?: boolean;\n onSelect?: () => void;\n}\nexport interface CommandGroup {\n heading?: string;\n items: CommandItem[];\n}\n\nexport interface CommandProps {\n groups: CommandGroup[];\n placeholder?: string;\n emptyText?: React.ReactNode;\n /** Called after an item's own onSelect. */\n onSelect?: (item: CommandItem) => void;\n className?: string;\n autoFocus?: boolean;\n}\n\nconst match = (item: CommandItem, q: string) =>\n [item.label, item.value, ...(item.keywords ?? [])].some((s) => s.toLowerCase().includes(q.toLowerCase()));\n\n/** A searchable, keyboard-driven list of actions. */\nexport function Command({ groups, placeholder = \"Type a command or search\", emptyText = \"No results.\", onSelect, className, autoFocus }: CommandProps) {\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const listId = React.useId();\n const filtered = groups\n .map((g) => ({ ...g, items: g.items.filter((i) => match(i, q)) }))\n .filter((g) => g.items.length);\n const flat = filtered.flatMap((g) => g.items).filter((i) => !i.disabled);\n const activeItem = flat[Math.min(active, flat.length - 1)];\n\n React.useEffect(() => {\n setActive(0);\n }, [q]);\n\n const choose = (item?: CommandItem) => {\n if (!item || item.disabled) return;\n item.onSelect?.();\n onSelect?.(item);\n };\n\n return (\n <div className={cn(\"vx-glass flex flex-col overflow-hidden rounded-card border border-pop-border bg-pop text-ink\", className)}>\n <div className=\"flex items-center gap-2 border-b border-rule px-3\">\n <Search className=\"size-4 shrink-0 text-muted\" />\n <input\n autoFocus={autoFocus}\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") { e.preventDefault(); setActive((a) => Math.min(a + 1, flat.length - 1)); }\n if (e.key === \"ArrowUp\") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }\n if (e.key === \"Enter\") { e.preventDefault(); choose(activeItem); }\n }}\n placeholder={placeholder}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={activeItem ? `${listId}-${activeItem.value}` : undefined}\n className=\"h-11 flex-1 bg-transparent text-sm outline-none placeholder:text-muted\"\n />\n </div>\n <div id={listId} role=\"listbox\" className=\"max-h-72 overflow-y-auto p-1.5\">\n {filtered.length === 0 && <p className=\"px-3 py-6 text-center text-sm text-muted\">{emptyText}</p>}\n {filtered.map((g, gi) => (\n <div key={gi} role=\"group\" aria-label={g.heading} className=\"pb-1\">\n {g.heading && <p className={menuLabel}>{g.heading}</p>}\n {g.items.map((item) => {\n const isActive = item === activeItem;\n return (\n <div\n key={item.value}\n id={`${listId}-${item.value}`}\n role=\"option\"\n aria-selected={isActive}\n aria-disabled={item.disabled}\n onMouseMove={() => !item.disabled && setActive(flat.indexOf(item))}\n onClick={() => choose(item)}\n data-active={isActive}\n data-disabled={item.disabled || undefined}\n className={menuItem}\n >\n {item.icon}\n <span className=\"flex-1\">{item.label}</span>\n {item.shortcut && <Kbd>{item.shortcut}</Kbd>}\n </div>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/** Command inside a modal, opened with ⌘K by default. */\nexport function CommandDialog({\n open,\n onOpenChange,\n hotkey = \"k\",\n ...props\n}: CommandProps & { open: boolean; onOpenChange: (open: boolean) => void; hotkey?: string | false }) {\n React.useEffect(() => {\n if (!hotkey) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key.toLowerCase() === hotkey && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n onOpenChange(!open);\n }\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [hotkey, open, onOpenChange]);\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <DialogPrimitive.Content aria-describedby={undefined} className=\"vx-pop fixed left-1/2 top-[14vh] z-[400] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 outline-none\">\n <DialogPrimitive.Title className=\"sr-only\">Command menu</DialogPrimitive.Title>\n <Command\n autoFocus\n {...props}\n className={cn(\"shadow-pop\", props.className)}\n onSelect={(i) => {\n props.onSelect?.(i);\n onOpenChange(false);\n }}\n />\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n );\n}\n","baseSource":"import * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { Search } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuCheck, menuItem, menuLabel, menuPanel } from \"./menu-styles\";\nimport { Kbd } from \"./kbd\";\n\nexport interface CommandItem {\n value: string;\n label: string;\n icon?: React.ReactNode;\n shortcut?: string;\n /** Extra words the filter should match. */\n keywords?: string[];\n disabled?: boolean;\n onSelect?: () => void;\n}\nexport interface CommandGroup {\n heading?: string;\n items: CommandItem[];\n}\n\nexport interface CommandProps {\n groups: CommandGroup[];\n placeholder?: string;\n emptyText?: React.ReactNode;\n /** Called after an item's own onSelect. */\n onSelect?: (item: CommandItem) => void;\n className?: string;\n autoFocus?: boolean;\n}\n\nconst match = (item: CommandItem, q: string) =>\n [item.label, item.value, ...(item.keywords ?? [])].some((s) => s.toLowerCase().includes(q.toLowerCase()));\n\n/** A searchable, keyboard-driven list of actions. */\nexport function Command({ groups, placeholder = \"Type a command or search\", emptyText = \"No results.\", onSelect, className, autoFocus }: CommandProps) {\n const [q, setQ] = React.useState(\"\");\n const [active, setActive] = React.useState(0);\n const listId = React.useId();\n const filtered = groups\n .map((g) => ({ ...g, items: g.items.filter((i) => match(i, q)) }))\n .filter((g) => g.items.length);\n const flat = filtered.flatMap((g) => g.items).filter((i) => !i.disabled);\n const activeItem = flat[Math.min(active, flat.length - 1)];\n\n React.useEffect(() => {\n setActive(0);\n }, [q]);\n\n const choose = (item?: CommandItem) => {\n if (!item || item.disabled) return;\n item.onSelect?.();\n onSelect?.(item);\n };\n\n return (\n <div className={cn(\"vx-glass flex flex-col overflow-hidden rounded-card border border-pop-border bg-pop text-ink\", className)}>\n <div className=\"flex items-center gap-2 border-b border-rule px-3\">\n <Search className=\"size-4 shrink-0 text-muted\" />\n <input\n autoFocus={autoFocus}\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") { e.preventDefault(); setActive((a) => Math.min(a + 1, flat.length - 1)); }\n if (e.key === \"ArrowUp\") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }\n if (e.key === \"Enter\") { e.preventDefault(); choose(activeItem); }\n }}\n placeholder={placeholder}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={activeItem ? `${listId}-${activeItem.value}` : undefined}\n className=\"h-11 flex-1 bg-transparent text-sm outline-none placeholder:text-muted\"\n />\n </div>\n <div id={listId} role=\"listbox\" className=\"max-h-72 overflow-y-auto p-1.5\">\n {filtered.length === 0 && <p className=\"px-3 py-6 text-center text-sm text-muted\">{emptyText}</p>}\n {filtered.map((g, gi) => (\n <div key={gi} role=\"group\" aria-label={g.heading} className=\"pb-1\">\n {g.heading && <p className={menuLabel}>{g.heading}</p>}\n {g.items.map((item) => {\n const isActive = item === activeItem;\n return (\n <div\n key={item.value}\n id={`${listId}-${item.value}`}\n role=\"option\"\n aria-selected={isActive}\n aria-disabled={item.disabled}\n onMouseMove={() => !item.disabled && setActive(flat.indexOf(item))}\n onClick={() => choose(item)}\n data-active={isActive}\n data-disabled={item.disabled || undefined}\n className={menuItem}\n >\n {item.icon}\n <span className=\"flex-1\">{item.label}</span>\n {item.shortcut && <Kbd>{item.shortcut}</Kbd>}\n </div>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/** Command inside a modal, opened with ⌘K by default. */\nexport function CommandDialog({\n open,\n onOpenChange,\n hotkey = \"k\",\n ...props\n}: CommandProps & { open: boolean; onOpenChange: (open: boolean) => void; hotkey?: string | false }) {\n React.useEffect(() => {\n if (!hotkey) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key.toLowerCase() === hotkey && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n onOpenChange(!open);\n }\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [hotkey, open, onOpenChange]);\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Backdrop className=\"vx-fade fixed inset-0 z-[400] bg-overlay backdrop-blur-[2px]\" />\n <DialogPrimitive.Popup className=\"vx-pop fixed left-1/2 top-[14vh] z-[400] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 outline-none\">\n <DialogPrimitive.Title className=\"sr-only\">Command menu</DialogPrimitive.Title>\n <Command\n autoFocus\n {...props}\n className={cn(\"shadow-pop\", props.className)}\n onSelect={(i) => {\n props.onSelect?.(i);\n onOpenChange(false);\n }}\n />\n </DialogPrimitive.Popup>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\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":"number-input","name":"Number input","category":"Forms","description":"A number field with step keys. Arrow keys step by one, Shift by ten.","deps":["lucide-react"],"uses":[],"usage":"<NumberInput defaultValue={5} min={1} max={100} />","props":[["value / defaultValue","number","0","Value."],["min / max / step","number","-∞ / ∞ / 1","Bounds and increment."],["format","(n: number) => string","","Display format while not editing."],["onValueChange","(n: number) => void","","Called on commit."]],"pro":false,"source":"import * as React from \"react\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface NumberInputProps {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n /** Format the shown value, e.g. (n) => `${n} GB`. Editing shows the raw number. */\n format?: (n: number) => string;\n}\n\nconst clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));\n\n/** A number field with step keys. Arrow keys step, Shift steps by ten. */\nexport function NumberInput({\n value, defaultValue = 0, onValueChange, min = -Infinity, max = Infinity, step = 1, disabled, className, format, ...rest\n}: NumberInputProps) {\n const [inner, setInner] = React.useState(defaultValue);\n const [draft, setDraft] = React.useState<string | null>(null);\n const current = value ?? inner;\n const commit = (n: number) => {\n if (Number.isNaN(n)) return;\n const v = clamp(Math.round(n / step) * step, min, max);\n setInner(v);\n onValueChange?.(v);\n };\n const key =\n \"grid h-full w-9 place-items-center text-muted transition-[background-color,color] duration-[var(--dur-micro)] hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-3.5\";\n return (\n <div\n className={cn(\n \"inline-flex h-9 items-stretch overflow-hidden rounded-input border border-field-border bg-field shadow-field vx-glass\",\n \"focus-within:field-focus\",\n disabled && \"opacity-60\",\n className,\n )}\n >\n <button type=\"button\" tabIndex={-1} aria-label=\"Decrease\" className={cn(key, \"border-r border-rule\")} disabled={disabled || current <= min} onClick={() => commit(current - step)}>\n <Minus />\n </button>\n <input\n id={rest.id}\n role=\"spinbutton\"\n aria-label={rest[\"aria-label\"]}\n aria-valuenow={current}\n aria-valuemin={Number.isFinite(min) ? min : undefined}\n aria-valuemax={Number.isFinite(max) ? max : undefined}\n inputMode=\"decimal\"\n disabled={disabled}\n value={draft ?? (format ? format(current) : String(current))}\n onFocus={() => setDraft(String(current))}\n onChange={(e) => setDraft(e.target.value)}\n onBlur={() => { if (draft !== null) commit(parseFloat(draft)); setDraft(null); }}\n onKeyDown={(e) => {\n const big = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\") { e.preventDefault(); commit(current + step * big); setDraft(null); }\n if (e.key === \"ArrowDown\") { e.preventDefault(); commit(current - step * big); setDraft(null); }\n if (e.key === \"Enter\") (e.target as HTMLInputElement).blur();\n }}\n className=\"w-16 min-w-0 flex-1 bg-transparent text-center text-sm tabular-nums text-ink outline-none\"\n />\n <button type=\"button\" tabIndex={-1} aria-label=\"Increase\" className={cn(key, \"border-l border-rule\")} disabled={disabled || current >= max} onClick={() => commit(current + step)}>\n <Plus />\n </button>\n </div>\n );\n}\n","baseSource":null},{"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":"hover-card","name":"Hover card","category":"Overlays","description":"A preview of a linked person or page, shown on hover or focus.","deps":["radix-ui"],"uses":[],"usage":"<HoverCard>\n <HoverCardTrigger href=\"/ana\">@ana</HoverCardTrigger>\n <HoverCardContent>…</HoverCardContent>\n</HoverCard>","props":[["openDelay / closeDelay","number","700 / 300","Milliseconds before showing and hiding."],["HoverCardContent.align","\"start\" | \"center\" | \"end\"","\"center\"","Alignment."]],"pro":false,"source":"import * as React from \"react\";\nimport { HoverCard as HC } from \"radix-ui\";\nimport { cn } from \"@/lib/cn\";\n\nexport const HoverCard = HC.Root;\nexport const HoverCardTrigger = HC.Trigger;\n\nexport function HoverCardContent({ className, align = \"center\", sideOffset = 8, ...props }: React.ComponentProps<typeof HC.Content>) {\n return (\n <HC.Portal>\n <HC.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 </HC.Portal>\n );\n}\n","baseSource":"import * as React from \"react\";\nimport { PreviewCard as HC } from \"@base-ui/react/preview-card\";\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 return { render: children as React.ReactElement<Record<string, unknown>> };\n}\n\nconst Delays = React.createContext<{ openDelay?: number; closeDelay?: number }>({});\n\n/** Same props as the Radix version: openDelay and closeDelay sit on the root. */\nexport function HoverCard({\n openDelay,\n closeDelay,\n ...props\n}: React.ComponentProps<typeof HC.Root> & { openDelay?: number; closeDelay?: number }) {\n return (\n <Delays.Provider value={{ openDelay, closeDelay }}>\n <HC.Root {...props} />\n </Delays.Provider>\n );\n}\n\nexport function HoverCardTrigger({ asChild, children, ...props }: Omit<React.ComponentProps<typeof HC.Trigger>, \"render\"> & { asChild?: boolean }) {\n const { openDelay, closeDelay } = React.useContext(Delays);\n return <HC.Trigger delay={openDelay} closeDelay={closeDelay} {...props} {...slot(asChild, children)} />;\n}\n\ntype Positioner = React.ComponentProps<typeof HC.Positioner>;\n\nexport function HoverCardContent({\n className,\n side = \"bottom\",\n align = \"center\",\n sideOffset = 8,\n ...props\n}: Omit<React.ComponentProps<typeof HC.Popup>, \"className\"> & { className?: string; side?: Positioner[\"side\"]; align?: Positioner[\"align\"]; sideOffset?: number }) {\n return (\n <HC.Portal>\n <HC.Positioner side={side} align={align} sideOffset={sideOffset} className=\"z-[100]\">\n <HC.Popup 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)} {...props} />\n </HC.Positioner>\n </HC.Portal>\n );\n}\n"},{"slug":"empty-state","name":"Empty state","category":"Feedback","description":"What a list or page shows before it has content, with a way forward.","deps":[],"uses":[],"usage":"<EmptyState icon={<FolderPlus />} title=\"No projects yet\" action={<Button>New project</Button>} />","props":[["title","ReactNode","required","Headline."],["description","ReactNode","","Supporting line."],["icon","ReactNode","","Shown on a key."],["action","ReactNode","","Buttons."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface EmptyStateProps {\n icon?: React.ReactNode;\n title: React.ReactNode;\n description?: React.ReactNode;\n action?: React.ReactNode;\n className?: string;\n}\n\n/** What a list or page shows when it has nothing in it yet. */\nexport function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {\n return (\n <div className={cn(\"flex flex-col items-center gap-3 rounded-panel border border-dashed border-rule-2 px-6 py-12 text-center\", className)}>\n {icon && (\n <span className=\"mb-1 grid size-11 place-items-center rounded-card border border-[color:var(--key-border)] bg-key-face text-muted shadow-key [&_svg]:size-5\">\n {icon}\n </span>\n )}\n <p className=\"font-display text-lg font-medium tracking-tight text-ink\">{title}</p>\n {description && <p className=\"max-w-sm text-sm text-muted\">{description}</p>}\n {action && <div className=\"mt-2 flex flex-wrap justify-center gap-2\">{action}</div>}\n </div>\n );\n}\n","baseSource":null},{"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":"stat","name":"Stat","category":"Display","description":"One number with its label, change against a previous period and a sparkline.","deps":["lucide-react"],"uses":["sparkline"],"usage":"<Stat label=\"Revenue\" value=\"$48,210\" delta={12.4} deltaLabel=\"vs last month\" trend={series} />","props":[["label / value","ReactNode","required","What and how much."],["delta","number","","Percent change."],["invert","boolean","false","Treat a fall as good."],["trend","number[]","","Series for the sparkline."]],"pro":false,"source":"import * as React from \"react\";\nimport { TrendingDown, TrendingUp } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Sparkline } from \"./sparkline\";\n\nexport interface StatProps {\n label: React.ReactNode;\n value: React.ReactNode;\n /** Change versus the previous period, e.g. 12.4 or -3. */\n delta?: number;\n /** Text after the delta, e.g. \"vs last week\". */\n deltaLabel?: React.ReactNode;\n /** Set when a fall is good, e.g. error rate. */\n invert?: boolean;\n /** Optional series drawn as a sparkline. */\n trend?: number[];\n className?: string;\n}\n\n/** One number, with its label, change and trend. */\nexport function Stat({ label, value, delta, deltaLabel, invert, trend, className }: StatProps) {\n const good = delta === undefined ? undefined : invert ? delta < 0 : delta >= 0;\n const Icon = delta !== undefined && delta < 0 ? TrendingDown : TrendingUp;\n return (\n <div className={cn(\"vx-glass grid gap-2 rounded-panel border border-card-border bg-card p-5 shadow-card\", className)}>\n <p className=\"text-sm text-muted\">{label}</p>\n <div className=\"flex items-end justify-between gap-3\">\n <p className=\"shrink-0 font-display text-3xl font-semibold tabular-nums tracking-tight text-ink\">{value}</p>\n {trend && (\n <div className=\"flex min-w-0 max-w-28 flex-1 justify-end pb-1\">\n <Sparkline values={trend} width={112} height={32} className=\"h-auto w-full\" />\n </div>\n )}\n </div>\n {delta !== undefined && (\n <p className=\"flex items-center gap-1.5 text-sm\">\n <span className={cn(\"inline-flex items-center gap-1 font-medium tabular-nums\", good ? \"text-success-wash-ink\" : \"text-danger-wash-ink\")}>\n <Icon className=\"size-3.5\" />\n {delta > 0 ? \"+\" : \"\"}\n {delta}%\n </span>\n {deltaLabel && <span className=\"text-muted\">{deltaLabel}</span>}\n </p>\n )}\n </div>\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":"stepper","name":"Stepper","category":"Navigation","description":"Progress through a multi-step flow, horizontal or vertical.","deps":["lucide-react"],"uses":[],"usage":"<Stepper current={1} steps={[{ title: \"Account\" }, { title: \"Billing\" }]} />","props":[["steps","{ title: ReactNode; description?: ReactNode }[]","required","The steps."],["current","number","required","Zero-based current step."],["orientation","\"horizontal\" | \"vertical\"","\"horizontal\"","Layout."]],"pro":false,"source":"import * as React from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface StepperProps {\n steps: { title: React.ReactNode; description?: React.ReactNode }[];\n /** Zero-based index of the current step. */\n current: number;\n orientation?: \"horizontal\" | \"vertical\";\n className?: string;\n}\n\n/** Progress through a multi-step flow. */\nexport function Stepper({ steps, current, orientation = \"horizontal\", className }: StepperProps) {\n const vertical = orientation === \"vertical\";\n return (\n <ol className={cn(\"flex\", vertical ? \"flex-col\" : \"w-full items-start\", className)}>\n {steps.map((s, i) => {\n const state = i < current ? \"done\" : i === current ? \"current\" : \"todo\";\n const last = i === steps.length - 1;\n return (\n <li\n key={i}\n aria-current={state === \"current\" ? \"step\" : undefined}\n className={cn(\"relative flex gap-3\", vertical ? \"pb-6 last:pb-0\" : cn(\"flex-col\", last ? \"flex-none\" : \"min-w-0 flex-1\"))}\n >\n <div className={cn(\"flex items-center\", vertical ? \"flex-col self-stretch\" : \"w-full\")}>\n <span\n className={cn(\n \"grid size-7 shrink-0 place-items-center rounded-full border text-xs font-medium tabular-nums\",\n state === \"done\" && \"border-accent bg-accent text-accent-ink\",\n state === \"current\" && \"border-accent bg-paper text-accent shadow-[0_0_0_4px_var(--ring)]\",\n state === \"todo\" && \"border-rule-2 bg-paper text-muted\",\n )}\n >\n {state === \"done\" ? <Check className=\"size-3.5\" strokeWidth={3} /> : i + 1}\n </span>\n {!last && (\n <span\n aria-hidden\n className={cn(vertical ? \"mt-1 w-px flex-1\" : \"mx-2 h-px flex-1\", i < current ? \"bg-accent\" : \"bg-rule-2\")}\n />\n )}\n </div>\n <div className={cn(\"grid gap-0.5\", !vertical && !last && \"pr-4\")}>\n <span className={cn(\"text-sm font-medium\", state === \"todo\" ? \"text-muted\" : \"text-ink\")}>{s.title}</span>\n {s.description && <span className=\"text-sm text-muted\">{s.description}</span>}\n </div>\n </li>\n );\n })}\n </ol>\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":"calendar","name":"Calendar","category":"Forms","description":"A month grid with event dots, disabled days and full keyboard navigation.","deps":["lucide-react"],"uses":[],"usage":"<Calendar value={date} onValueChange={setDate} events={[new Date()]} />","props":[["value / onValueChange","Date / (date) => void","","Selected day."],["events","Date[]","[]","Days that show a dot."],["isDisabled","(date) => boolean","","Days that cannot be picked."],["weekStartsOn","0 | 1","1","Sunday or Monday first."]],"pro":false,"source":"import * as React from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface DateRange {\n from?: Date;\n to?: Date;\n}\n\ninterface BaseProps {\n /** Days that carry an event dot. */\n events?: Date[];\n /** Days that cannot be picked. */\n isDisabled?: (date: Date) => boolean;\n /** 0 is Sunday, 1 is Monday. */\n weekStartsOn?: 0 | 1;\n /** The month shown first. Defaults to the selection or today. */\n defaultMonth?: Date;\n className?: string;\n}\n\nexport interface CalendarSingleProps extends BaseProps {\n mode?: \"single\";\n value?: Date | null;\n defaultValue?: Date;\n onValueChange?: (date: Date) => void;\n}\n\nexport interface CalendarRangeProps extends BaseProps {\n mode: \"range\";\n range?: DateRange;\n defaultRange?: DateRange;\n onRangeChange?: (range: DateRange) => void;\n}\n\nexport type CalendarProps = CalendarSingleProps | CalendarRangeProps;\n\nconst day = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\nconst same = (a?: Date | null, b?: Date | null) => !!a && !!b && day(a) === day(b);\nconst names = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\n\n/**\n * A month grid. Single mode picks one day; range mode picks a start and an end, previewing on hover.\n * Keyboard: arrows move a day or a week, Page Up and Page Down change month, Enter or Space picks.\n */\nexport function Calendar(props: CalendarProps) {\n const { events = [], isDisabled, weekStartsOn = 1, className } = props;\n const isRange = props.mode === \"range\";\n\n const [innerOne, setInnerOne] = React.useState<Date | null>(!isRange ? props.defaultValue ?? null : null);\n const one = !isRange ? (props.value === undefined ? innerOne : props.value) : null;\n const [innerRange, setInnerRange] = React.useState<DateRange>(isRange ? props.defaultRange ?? {} : {});\n const range = isRange ? props.range ?? innerRange : {};\n const [hover, setHover] = React.useState<Date | null>(null);\n\n const anchor = props.defaultMonth ?? one ?? range.from ?? new Date();\n const [month, setMonth] = React.useState(() => new Date(anchor.getFullYear(), anchor.getMonth(), 1));\n const [focus, setFocus] = React.useState<Date>(anchor);\n const grid = React.useRef<HTMLDivElement>(null);\n const today = new Date();\n\n const offset = (month.getDay() - weekStartsOn + 7) % 7;\n const days = Array.from({ length: 42 }, (_, i) => new Date(month.getFullYear(), month.getMonth(), i - offset + 1));\n\n const pick = (d: Date) => {\n if (isDisabled?.(d)) return;\n if (!isRange) {\n setInnerOne(d);\n props.onValueChange?.(d);\n return;\n }\n const next: DateRange = !range.from || range.to || day(d) < day(range.from) ? { from: d, to: undefined } : { from: range.from, to: d };\n setInnerRange(next);\n props.onRangeChange?.(next);\n };\n const move = (d: Date) => {\n setFocus(d);\n if (d.getMonth() !== month.getMonth() || d.getFullYear() !== month.getFullYear()) setMonth(new Date(d.getFullYear(), d.getMonth(), 1));\n requestAnimationFrame(() => grid.current?.querySelector<HTMLButtonElement>(`[data-day=\"${d.toDateString()}\"]`)?.focus());\n };\n const shift = (n: number) => setMonth(new Date(month.getFullYear(), month.getMonth() + n, 1));\n\n const end = range.to ?? (range.from && hover && day(hover) > day(range.from) ? hover : undefined);\n const inRange = (d: Date) => !!range.from && !!end && day(d) > day(range.from) && day(d) < day(end);\n const isStart = (d: Date) => same(d, range.from);\n const isEnd = (d: Date) => same(d, end);\n\n return (\n <div className={cn(\"w-[280px] select-none\", className)}>\n <div className=\"mb-3 flex items-center justify-between\">\n <button type=\"button\" onClick={() => shift(-1)} aria-label=\"Previous month\" className=\"grid size-8 place-items-center rounded-input text-ink-2 hover:bg-paper-3 focus-visible:outline-2 focus-visible:outline-focus\">\n <ChevronLeft className=\"size-4\" />\n </button>\n <p className=\"text-sm font-semibold\" aria-live=\"polite\">\n {month.toLocaleDateString(undefined, { month: \"long\", year: \"numeric\" })}\n </p>\n <button type=\"button\" onClick={() => shift(1)} aria-label=\"Next month\" className=\"grid size-8 place-items-center rounded-input text-ink-2 hover:bg-paper-3 focus-visible:outline-2 focus-visible:outline-focus\">\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n <div className=\"grid grid-cols-7 pb-1 text-center text-xs text-muted\">\n {Array.from({ length: 7 }, (_, i) => <span key={i}>{names[(i + weekStartsOn) % 7]}</span>)}\n </div>\n <div ref={grid} role=\"grid\" className=\"grid grid-cols-7 gap-y-1\" onMouseLeave={() => setHover(null)}>\n {days.map((d) => {\n const out = d.getMonth() !== month.getMonth();\n const dis = isDisabled?.(d);\n const hasEvent = events.some((e) => same(e, d));\n const sel = isRange ? isStart(d) || isEnd(d) : same(d, one);\n const band = isRange && (inRange(d) || ((isStart(d) || isEnd(d)) && !!end && !!range.from && !same(range.from, end)));\n return (\n <div\n key={d.toISOString()}\n className={cn(\n \"relative\",\n band && \"bg-accent-wash\",\n band && isStart(d) && \"rounded-l-input\",\n band && isEnd(d) && \"rounded-r-input\",\n )}\n >\n <button\n type=\"button\"\n data-day={d.toDateString()}\n tabIndex={same(d, focus) ? 0 : -1}\n disabled={dis}\n aria-pressed={sel}\n aria-label={d.toLocaleDateString(undefined, { weekday: \"long\", day: \"numeric\", month: \"long\", year: \"numeric\" })}\n onClick={() => { pick(d); setFocus(d); }}\n onMouseEnter={() => isRange && setHover(d)}\n onKeyDown={(e) => {\n const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];\n if (step) { e.preventDefault(); move(new Date(d.getFullYear(), d.getMonth(), d.getDate() + step)); }\n if (e.key === \"PageUp\") { e.preventDefault(); move(new Date(d.getFullYear(), d.getMonth() - 1, d.getDate())); }\n if (e.key === \"PageDown\") { e.preventDefault(); move(new Date(d.getFullYear(), d.getMonth() + 1, d.getDate())); }\n }}\n className={cn(\n \"relative grid aspect-square w-full place-items-center rounded-input text-sm tabular-nums transition-[background-color,color] duration-[var(--dur-micro)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-focus disabled:pointer-events-none disabled:opacity-30\",\n sel\n ? \"border border-[color:var(--btn-border)] bg-[color:var(--btn-bg)] [background-image:var(--btn-image)] font-semibold text-accent-ink shadow-key-accent\"\n : band\n ? \"text-accent-wash-ink hover:bg-accent/15\"\n : out\n ? \"text-muted/60 hover:bg-paper-3\"\n : \"text-ink hover:bg-paper-3\",\n !sel && same(d, today) && \"font-semibold text-accent-wash-ink ring-1 ring-inset ring-accent/40\",\n )}\n >\n {d.getDate()}\n {hasEvent && <span aria-hidden className={cn(\"absolute bottom-1 size-1 rounded-full\", sel ? \"bg-accent-ink\" : \"bg-accent\")} />}\n </button>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","baseSource":null},{"slug":"announcement","name":"Announcement","category":"Feedback","description":"A dismissible card for product news, with one action.","deps":["lucide-react"],"uses":[],"usage":"<Announcement title=\"Two new components\" onDismiss={hide}>Calendar is ready.</Announcement>","props":[["title","ReactNode","required","Headline."],["icon","ReactNode","megaphone","Shown on an accent key."],["action","ReactNode","","A button under the text."],["onDismiss","() => void","","Shows a close button."]],"pro":false,"source":"import * as React from \"react\";\nimport { Megaphone, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface AnnouncementProps {\n title: React.ReactNode;\n children?: React.ReactNode;\n icon?: React.ReactNode;\n action?: React.ReactNode;\n /** Shows a close button. The component does not remember dismissal; store it if you need to. */\n onDismiss?: () => void;\n className?: string;\n}\n\n/** A dismissible card for product news. */\nexport function Announcement({ title, children, icon = <Megaphone />, action, onDismiss, className }: AnnouncementProps) {\n return (\n <div role=\"status\" className={cn(\"vx-glass relative grid w-full max-w-sm gap-3 rounded-panel border border-card-border bg-card p-4 shadow-card\", className)}>\n <div className=\"flex items-start gap-3 pr-6\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-input border border-[color:var(--btn-border)] bg-[color:var(--btn-bg)] [background-image:var(--btn-image)] text-accent-ink shadow-key-accent [&_svg]:size-4\">\n {icon}\n </span>\n <div className=\"grid gap-1\">\n <p className=\"text-sm font-semibold text-ink\">{title}</p>\n {children && <p className=\"text-sm text-muted\">{children}</p>}\n </div>\n </div>\n {action}\n {onDismiss && (\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className=\"absolute right-3 top-3 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 </button>\n )}\n </div>\n );\n}\n","baseSource":null},{"slug":"chip","name":"Chip","category":"Display","description":"A compact, removable token for filters and selections.","deps":["class-variance-authority","lucide-react"],"uses":[],"usage":"<Chip onRemove={() => remove(\"Design\")}>Design</Chip>","props":[["variant","\"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\"","\"neutral\"","Tone."],["leading","ReactNode","","Icon or avatar before the label."],["onRemove","() => void","","Shows a remove button."]],"pro":false,"source":"import * as React from \"react\";\nimport { X } from \"lucide-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nconst chipVariants = cva(\n \"inline-flex h-6 items-center gap-1 rounded-[calc(6px*var(--radius-factor))] border pl-1.5 text-[13px] font-medium [&_svg]:size-3.5\",\n {\n variants: {\n variant: {\n neutral: \"border-transparent bg-paper-3 text-ink-2\",\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 },\n },\n defaultVariants: { variant: \"neutral\" },\n },\n);\n\nexport interface ChipProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof chipVariants> {\n /** Leading icon or avatar. */\n leading?: React.ReactNode;\n /** Shows a remove button when set. */\n onRemove?: () => void;\n}\n\n/** A compact, removable token for filters and selections. */\nexport function Chip({ className, variant, leading, onRemove, children, ...props }: ChipProps) {\n return (\n <span className={cn(chipVariants({ variant }), onRemove ? \"pr-0.5\" : \"pr-1.5\", className)} {...props}>\n {leading}\n {children}\n {onRemove && (\n <button\n type=\"button\"\n onClick={onRemove}\n aria-label={`Remove ${typeof children === \"string\" ? children : \"item\"}`}\n className=\"grid size-5 place-items-center rounded-[calc(4px*var(--radius-factor))] opacity-60 hover:bg-ink/10 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X />\n </button>\n )}\n </span>\n );\n}\n","baseSource":null},{"slug":"count-up","name":"Count up","category":"Display","description":"A number that climbs to its new value. Still under reduced motion.","deps":[],"uses":[],"usage":"<CountUp value={48210} format={(n) => \"$\" + Math.round(n).toLocaleString()} />","props":[["value","number","required","Target value."],["duration","number","900","Milliseconds."],["format","(n: number) => string","toLocaleString","Formats each frame."]],"pro":false,"source":"import * as React from \"react\";\n\nexport interface CountUpProps {\n value: number;\n /** Milliseconds for the climb. */\n duration?: number;\n /** Format each frame, e.g. (n) => `$${n.toFixed(0)}`. */\n format?: (n: number) => string;\n className?: string;\n}\n\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 3);\n\n/** A number that counts from its previous value to the new one. Static under reduced motion. */\nexport function CountUp({ value, duration = 900, format = (n) => Math.round(n).toLocaleString(), className }: CountUpProps) {\n const [shown, setShown] = React.useState(value);\n const from = React.useRef(0);\n React.useEffect(() => {\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n setShown(value);\n from.current = value;\n return;\n }\n const start = performance.now();\n const a = from.current;\n let raf = 0;\n const tick = (now: number) => {\n const t = Math.min(1, (now - start) / duration);\n setShown(a + (value - a) * easeOut(t));\n if (t < 1) raf = requestAnimationFrame(tick);\n else from.current = value;\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [value, duration]);\n return <span className={className} style={{ fontVariantNumeric: \"tabular-nums\" }}>{format(shown)}</span>;\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":"tag-input","name":"Tag input","category":"Forms","description":"Type and press Enter or comma to add tags, with suggestions and a limit.","deps":[],"uses":["chip"],"usage":"<TagInput defaultValue={[\"React\"]} suggestions={[\"TypeScript\", \"Figma\"]} max={6} />","props":[["value / onValueChange","string[] / (tags) => void","","Tags."],["suggestions","string[]","[]","Offered while typing."],["max","number","","Most tags allowed."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { menuCheck, menuItem, menuLabel, menuPanel } from \"./menu-styles\";\nimport { Chip } from \"./chip\";\n\nexport interface TagInputProps {\n value?: string[];\n defaultValue?: string[];\n onValueChange?: (tags: string[]) => void;\n placeholder?: string;\n /** Most tags allowed. */\n max?: number;\n /** Offered below the field while typing. */\n suggestions?: string[];\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** Type and press Enter or comma to add a tag; Backspace on an empty field removes the last one. */\nexport function TagInput({ value, defaultValue = [], onValueChange, placeholder = \"Add a tag\", max, suggestions = [], className, ...rest }: TagInputProps) {\n const [inner, setInner] = React.useState(defaultValue);\n const tags = value ?? inner;\n const [text, setText] = React.useState(\"\");\n const input = React.useRef<HTMLInputElement>(null);\n const full = max !== undefined && tags.length >= max;\n const commit = (next: string[]) => {\n setInner(next);\n onValueChange?.(next);\n };\n const add = (raw: string) => {\n const t = raw.trim().replace(/,$/, \"\");\n if (!t || full || tags.some((x) => x.toLowerCase() === t.toLowerCase())) return;\n commit([...tags, t]);\n setText(\"\");\n };\n const open = text.length > 0 ? suggestions.filter((s) => s.toLowerCase().includes(text.toLowerCase()) && !tags.includes(s)).slice(0, 5) : [];\n return (\n <div className={cn(\"relative grid gap-1.5\", className)}>\n <div\n onClick={() => input.current?.focus()}\n className={cn(\n \"vx-glass flex min-h-9 flex-wrap items-center gap-1.5 rounded-input border border-field-border bg-field px-1.5 py-1 shadow-field\",\n \"focus-within:field-focus\",\n )}\n >\n {tags.map((t) => (\n <Chip key={t} className=\"h-6 text-xs\" onRemove={() => commit(tags.filter((x) => x !== t))}>{t}</Chip>\n ))}\n <input\n ref={input}\n value={text}\n disabled={full}\n onChange={(e) => (e.target.value.endsWith(\",\") ? add(e.target.value) : setText(e.target.value))}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") { e.preventDefault(); add(open[0] && !tags.includes(text) && open[0].toLowerCase().startsWith(text.toLowerCase()) ? open[0] : text); }\n if (e.key === \"Backspace\" && !text && tags.length) commit(tags.slice(0, -1));\n }}\n placeholder={full ? `${max} tags maximum` : tags.length ? \"\" : placeholder}\n aria-label={rest[\"aria-label\"] ?? placeholder}\n className=\"h-6 min-w-24 flex-1 bg-transparent px-1.5 text-sm text-ink outline-none placeholder:text-muted disabled:cursor-not-allowed\"\n />\n </div>\n {open.length > 0 && (\n <ul role=\"listbox\" className={cn(menuPanel, \"absolute inset-x-0 top-full mt-1 grid gap-0.5\")}>\n {open.map((s) => (\n <li key={s}>\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n onMouseDown={(e) => { e.preventDefault(); add(s); }}\n className={cn(menuItem, \"w-full text-left hover:bg-paper-3\")}\n >\n {s}\n </button>\n </li>\n ))}\n </ul>\n )}\n {max !== undefined && <p className=\"text-xs text-muted\">{tags.length} of {max}</p>}\n </div>\n );\n}\n","baseSource":null},{"slug":"rating","name":"Rating","category":"Forms","description":"Stars as a radio group with hover preview, arrow keys and a read-only mode.","deps":["lucide-react"],"uses":[],"usage":"<Rating value={stars} onValueChange={setStars} />","props":[["value / onValueChange","number / (n) => void","0","Stars chosen."],["max","number","5","Number of stars."],["readOnly","boolean","false","Display only."],["size","\"sm\" | \"md\" | \"lg\"","\"md\"","Star size."]],"pro":false,"source":"import * as React from \"react\";\nimport { Star } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface RatingProps {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n max?: number;\n readOnly?: boolean;\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n \"aria-label\"?: string;\n}\n\nconst sizes = { sm: \"size-4\", md: \"size-5\", lg: \"size-7\" };\n\n/** Stars as a radio group: hover to preview, arrow keys to change, click a set star again to clear. */\nexport function Rating({ value, defaultValue = 0, onValueChange, max = 5, readOnly, size = \"md\", className, ...rest }: RatingProps) {\n const [inner, setInner] = React.useState(defaultValue);\n const current = value ?? inner;\n const [hover, setHover] = React.useState<number | null>(null);\n const shown = hover ?? current;\n const set = (n: number) => {\n if (readOnly) return;\n setInner(n);\n onValueChange?.(n);\n };\n return (\n <div\n role=\"radiogroup\"\n aria-label={rest[\"aria-label\"] ?? \"Rating\"}\n aria-readonly={readOnly || undefined}\n className={cn(\"inline-flex items-center gap-0.5\", className)}\n onMouseLeave={() => setHover(null)}\n >\n {Array.from({ length: max }, (_, i) => i + 1).map((n) => (\n <button\n key={n}\n type=\"button\"\n role=\"radio\"\n aria-checked={current === n}\n aria-label={`${n} of ${max}`}\n tabIndex={readOnly ? -1 : current === n || (current === 0 && n === 1) ? 0 : -1}\n disabled={readOnly}\n onMouseEnter={() => !readOnly && setHover(n)}\n onClick={() => set(current === n ? 0 : n)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") { e.preventDefault(); set(Math.min(max, current + 1)); }\n if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") { e.preventDefault(); set(Math.max(0, current - 1)); }\n }}\n className=\"rounded-sm p-0.5 transition-transform duration-[var(--dur-micro)] enabled:hover:scale-110 focus-visible:outline-2 focus-visible:outline-focus disabled:cursor-default\"\n >\n <Star className={cn(sizes[size], n <= shown ? \"fill-warning text-warning\" : \"fill-transparent text-rule-2\")} />\n </button>\n ))}\n </div>\n );\n}\n","baseSource":null},{"slug":"context-menu","name":"Context menu","category":"Overlays","description":"The menu that opens on right-click or a long press.","deps":["radix-ui","lucide-react"],"uses":[],"usage":"<ContextMenu>\n <ContextMenuTrigger>Right-click me</ContextMenuTrigger>\n <ContextMenuContent><ContextMenuItem>Rename</ContextMenuItem></ContextMenuContent>\n</ContextMenu>","props":[["ContextMenuItem.shortcut","string","","Key hint."],["ContextMenuItem.destructive","boolean","false","Danger colouring."]],"pro":false,"source":"import * as React from \"react\";\nimport { ContextMenu 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 ContextMenu = Menu.Root;\nexport const ContextMenuTrigger = Menu.Trigger;\nexport const ContextMenuGroup = Menu.Group;\nexport const ContextMenuSub = Menu.Sub;\n\nconst panel = menuPanel;\nconst item = menuItem;\n\n/** The menu that opens on right-click or a long press. */\nexport function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof Menu.Content>) {\n return (\n <Menu.Portal>\n <Menu.Content className={cn(panel, className)} {...props} />\n </Menu.Portal>\n );\n}\n\nexport function ContextMenuItem({\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 className={cn(item, destructive && \"text-danger data-[highlighted]:bg-danger-wash [&_svg]:text-danger\", className)} {...props}>\n {children}\n {shortcut && <span className=\"ml-auto font-mono text-xs text-muted\">{shortcut}</span>}\n </Menu.Item>\n );\n}\n\nexport function ContextMenuCheckboxItem({ 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><Check className=\"!text-accent\" /></Menu.ItemIndicator>\n </span>\n {children}\n </Menu.CheckboxItem>\n );\n}\n\nexport function ContextMenuLabel({ className, ...props }: React.ComponentProps<typeof Menu.Label>) {\n return <Menu.Label className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof Menu.Separator>) {\n return <Menu.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n\nexport function ContextMenuSubTrigger({ 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 ContextMenuSubContent({ 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 { ContextMenu as Menu } from \"@base-ui/react/context-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 return { render: children as React.ReactElement<Record<string, unknown>> };\n}\n\nexport const ContextMenu = Menu.Root;\nexport const ContextMenuGroup = Menu.Group;\nexport const ContextMenuSub = Menu.SubmenuRoot;\n\nexport function ContextMenuTrigger({ asChild, children, ...props }: Omit<React.ComponentProps<typeof Menu.Trigger>, \"render\"> & { asChild?: boolean }) {\n return <Menu.Trigger {...props} {...slot(asChild, children)} />;\n}\n\nconst panel = menuPanel;\nconst item = menuItem;\n\n/** The menu that opens on right-click or a long press. */\nexport function ContextMenuContent({ className, ...props }: Omit<React.ComponentProps<typeof Menu.Popup>, \"className\"> & { className?: string }) {\n return (\n <Menu.Portal>\n <Menu.Positioner className=\"z-[100]\">\n <Menu.Popup className={cn(panel, className)} {...props} />\n </Menu.Positioner>\n </Menu.Portal>\n );\n}\n\nexport function ContextMenuItem({\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 ContextMenuCheckboxItem({ className, children, ...props }: 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><Check className=\"!text-accent\" /></Menu.CheckboxItemIndicator>\n </span>\n {children}\n </Menu.CheckboxItem>\n );\n}\n\n/** A heading inside the menu. A plain element, so it works inside or outside a group. */\nexport function ContextMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return <div role=\"presentation\" className={cn(menuLabel, className)} {...props} />;\n}\n\nexport function ContextMenuSeparator({ className, ...props }: Omit<React.ComponentProps<typeof Menu.Separator>, \"className\"> & { className?: string }) {\n return <Menu.Separator className={cn(menuSeparator, className)} {...props} />;\n}\n\nexport function ContextMenuSubTrigger({ className, children, ...props }: 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 ContextMenuSubContent({ 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":"timeline","name":"Timeline","category":"Display","description":"A vertical list of events joined by a line, with icons and tones.","deps":[],"uses":[],"usage":"<Timeline items={[{ title: \"Deployed\", time: \"2 min ago\", tone: \"accent\" }]} />","props":[["items","{ title; time?; body?; icon?; tone? }[]","required","Events, newest first."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface TimelineItem {\n title: React.ReactNode;\n time?: React.ReactNode;\n body?: React.ReactNode;\n /** An icon in the marker; a dot when omitted. */\n icon?: React.ReactNode;\n tone?: \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\";\n}\n\nconst tones = {\n neutral: \"border-rule-2 bg-paper text-muted\",\n accent: \"border-transparent bg-accent text-accent-ink\",\n success: \"border-transparent bg-success text-white\",\n warning: \"border-transparent bg-warning text-ink\",\n danger: \"border-transparent bg-danger text-white\",\n};\n\n/** A vertical list of events joined by a line. */\nexport function Timeline({ items, className }: { items: TimelineItem[]; className?: string }) {\n return (\n <ol className={cn(\"grid\", className)}>\n {items.map((it, i) => (\n <li key={i} className=\"relative grid grid-cols-[28px_minmax(0,1fr)] gap-3 pb-6 last:pb-0\">\n {i < items.length - 1 && <span aria-hidden className=\"absolute bottom-0 left-[13.5px] top-8 w-px bg-rule\" />}\n <span className={cn(\"relative z-10 grid size-7 place-items-center rounded-full border [&_svg]:size-3.5\", tones[it.tone ?? \"neutral\"])}>\n {it.icon ?? <span className=\"size-2 rounded-full bg-current\" />}\n </span>\n <div className=\"grid gap-1 pt-0.5\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3\">\n <p className=\"text-sm font-medium text-ink\">{it.title}</p>\n {it.time && <time className=\"text-xs text-muted\">{it.time}</time>}\n </div>\n {it.body && <div className=\"text-sm text-muted\">{it.body}</div>}\n </div>\n </li>\n ))}\n </ol>\n );\n}\n","baseSource":null},{"slug":"progress-circle","name":"Progress circle","category":"Feedback","description":"A ring that fills to a value, with the percentage or your own content inside.","deps":[],"uses":[],"usage":"<ProgressCircle value={72} size={96} label=\"Storage used\" />","props":[["value","number","required","0 to 100."],["size / thickness","number","96 / 8","Pixels."],["tone","\"accent\" | \"success\" | \"warning\" | \"danger\"","\"accent\"","Fill colour."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ProgressCircleProps {\n value: number;\n size?: number;\n /** Ring thickness in pixels. */\n thickness?: number;\n tone?: \"accent\" | \"success\" | \"warning\" | \"danger\";\n /** Centre content. Defaults to the percentage. */\n children?: React.ReactNode;\n label?: string;\n className?: string;\n}\n\nconst stroke = { accent: \"var(--accent)\", success: \"var(--success)\", warning: \"var(--warning)\", danger: \"var(--danger)\" };\n\n/** A ring that fills to a value. The fill eases between values. */\nexport function ProgressCircle({ value, size = 96, thickness = 8, tone = \"accent\", children, label, className }: ProgressCircleProps) {\n const v = Math.min(100, Math.max(0, value));\n const r = (size - thickness) / 2;\n const c = 2 * Math.PI * r;\n return (\n <div\n role=\"progressbar\"\n aria-valuenow={Math.round(v)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n className={cn(\"relative inline-grid place-items-center\", className)}\n style={{ width: size, height: size }}\n >\n <svg width={size} height={size} className=\"-rotate-90\" aria-hidden>\n <circle cx={size / 2} cy={size / 2} r={r} fill=\"none\" stroke=\"var(--paper-3)\" strokeWidth={thickness} />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke={stroke[tone]}\n strokeWidth={thickness}\n strokeLinecap=\"round\"\n strokeDasharray={c}\n strokeDashoffset={c * (1 - v / 100)}\n className=\"transition-[stroke-dashoffset] duration-700 ease-[var(--ease-out)]\"\n />\n </svg>\n <span className=\"absolute inset-0 grid place-content-center text-center\">\n {children ?? <span className=\"font-semibold tabular-nums tracking-tight\" style={{ fontSize: size * 0.24 }}>{Math.round(v)}%</span>}\n </span>\n </div>\n );\n}\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":"search-field","name":"Search field","category":"Forms","description":"A search input with a clear button, a focus shortcut and a loading state. Escape clears it.","deps":["lucide-react"],"uses":["input","kbd"],"usage":"<SearchField value={query} onValueChange={setQuery} shortcut=\"/\" />","props":[["value / defaultValue","string","\"\"","Controlled or initial text."],["onValueChange","(value: string) => void","","Called on every change, and with an empty string on clear."],["shortcut","string","","A key that focuses the field from anywhere, shown as a hint while empty."],["loading","boolean","false","Swaps the search icon for a spinner."],["size","\"sm\" | \"md\" | \"lg\"","\"md\"","Height."]],"pro":false,"source":"import * as React from \"react\";\nimport { LoaderCircle, Search, X } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { fieldBase } from \"./input\";\nimport { Kbd } from \"./kbd\";\n\nexport interface SearchFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"size\"> {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** A single key that focuses the field from anywhere, such as \"/\". Shown as a hint while empty. */\n shortcut?: string;\n /** Shows a spinner in place of the search icon. */\n loading?: boolean;\n size?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst sizes = { sm: \"h-8 pl-8 text-sm\", md: \"h-9 pl-9\", lg: \"h-11 pl-10 text-base\" };\n\n/** A search input with a clear button, an optional focus shortcut and a loading state. Escape clears it. */\nexport const SearchField = React.forwardRef<HTMLInputElement, SearchFieldProps>(\n ({ value, defaultValue = \"\", onValueChange, shortcut, loading, size = \"md\", className, placeholder = \"Search\", ...props }, ref) => {\n const [inner, setInner] = React.useState(defaultValue);\n const current = value ?? inner;\n const local = React.useRef<HTMLInputElement>(null);\n React.useImperativeHandle(ref, () => local.current!);\n const set = (v: string) => {\n setInner(v);\n onValueChange?.(v);\n };\n React.useEffect(() => {\n if (!shortcut) return;\n const onKey = (e: KeyboardEvent) => {\n const t = e.target as HTMLElement;\n if (e.key !== shortcut || e.metaKey || e.ctrlKey || t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) return;\n e.preventDefault();\n local.current?.focus();\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [shortcut]);\n const Icon = loading ? LoaderCircle : Search;\n return (\n <div className={cn(\"relative flex items-center\", className)}>\n <Icon aria-hidden className={cn(\"pointer-events-none absolute left-3 size-4 text-muted\", loading && \"animate-spin motion-reduce:animate-none\", size === \"sm\" && \"left-2.5 size-3.5\")} />\n <input\n ref={local}\n type=\"search\"\n value={current}\n placeholder={placeholder}\n onChange={(e) => set(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && current) {\n e.preventDefault();\n set(\"\");\n }\n }}\n className={cn(fieldBase, sizes[size], \"pr-10 [&::-webkit-search-cancel-button]:hidden\")}\n {...props}\n />\n <span className=\"absolute right-2 flex items-center\">\n {current ? (\n <button\n type=\"button\"\n aria-label=\"Clear search\"\n onClick={() => {\n set(\"\");\n local.current?.focus();\n }}\n className=\"grid size-6 place-items-center rounded-sm text-muted hover:bg-paper-3 hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\"\n >\n <X className=\"size-3.5\" />\n </button>\n ) : (\n shortcut && <Kbd className=\"mr-0.5\">{shortcut}</Kbd>\n )}\n </span>\n </div>\n );\n },\n);\nSearchField.displayName = \"SearchField\";\n","baseSource":null},{"slug":"time-field","name":"Time field","category":"Forms","description":"A time input in segments. Arrows step, digits type, left and right move between hours, minutes and AM/PM.","deps":["lucide-react"],"uses":[],"usage":"<TimeField value={time} onValueChange={setTime} hour12 step={15} />","props":[["value / defaultValue","{ hours: number; minutes: number }","9:00","Hours are 0 to 23."],["onValueChange","(time) => void","","Called when a segment changes."],["hour12","boolean","false","Show a 12-hour clock with AM/PM."],["step","number","1","Minutes per arrow press."]],"pro":false,"source":"import * as React from \"react\";\nimport { Clock } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface Time {\n hours: number;\n minutes: number;\n}\n\nexport interface TimeFieldProps {\n value?: Time;\n defaultValue?: Time;\n onValueChange?: (time: Time) => void;\n /** 12-hour clock with an AM/PM segment. */\n hour12?: boolean;\n /** Minutes moved per arrow press. */\n step?: number;\n disabled?: boolean;\n className?: string;\n \"aria-label\"?: string;\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n\n/** A time input made of segments. Arrow up and down change a segment, digits type into it, left and right move between them. */\nexport function TimeField({ value, defaultValue = { hours: 9, minutes: 0 }, onValueChange, hour12, step = 1, disabled, className, ...rest }: TimeFieldProps) {\n const [inner, setInner] = React.useState(defaultValue);\n const t = value ?? inner;\n const typed = React.useRef(\"\");\n const refs = React.useRef<(HTMLSpanElement | null)[]>([]);\n const set = (next: Time) => {\n setInner(next);\n onValueChange?.(next);\n };\n const pm = t.hours >= 12;\n const shownHours = hour12 ? (t.hours % 12 || 12) : t.hours;\n\n type Seg = \"hours\" | \"minutes\" | \"period\";\n const segs: Seg[] = hour12 ? [\"hours\", \"minutes\", \"period\"] : [\"hours\", \"minutes\"];\n\n const bump = (seg: Seg, dir: 1 | -1) => {\n if (seg === \"hours\") set({ ...t, hours: (t.hours + dir + 24) % 24 });\n if (seg === \"minutes\") set({ ...t, minutes: (t.minutes + dir * step + 60) % 60 });\n if (seg === \"period\") set({ ...t, hours: (t.hours + 12) % 24 });\n };\n const type = (seg: Seg, digit: string, i: number) => {\n const next = (typed.current + digit).slice(-2);\n const n = Number(next);\n if (seg === \"hours\") {\n const max = hour12 ? 12 : 23;\n if (n > max) typed.current = digit;\n else typed.current = next;\n let h = Number(typed.current);\n if (hour12) h = (h % 12) + (pm ? 12 : 0);\n set({ ...t, hours: h });\n } else {\n typed.current = n > 59 ? digit : next;\n set({ ...t, minutes: Number(typed.current) });\n }\n if (typed.current.length === 2 || Number(typed.current) > (seg === \"hours\" ? (hour12 ? 1 : 2) : 5)) {\n typed.current = \"\";\n refs.current[i + 1]?.focus();\n }\n };\n\n const segClass =\n \"rounded-sm px-1 tabular-nums outline-none caret-transparent focus:bg-accent focus:text-accent-ink data-[placeholder]:text-muted\";\n return (\n <div\n role=\"group\"\n aria-label={rest[\"aria-label\"] ?? \"Time\"}\n aria-disabled={disabled || undefined}\n className={cn(\n \"vx-glass inline-flex h-9 items-center gap-0.5 rounded-input border border-field-border bg-field pl-3 pr-3 text-sm text-ink shadow-field\",\n \"focus-within:field-focus\",\n disabled && \"pointer-events-none opacity-60\",\n className,\n )}\n >\n <Clock aria-hidden className=\"mr-1.5 size-4 text-muted\" />\n {segs.map((seg, i) => {\n const label = seg === \"hours\" ? \"Hours\" : seg === \"minutes\" ? \"Minutes\" : \"AM or PM\";\n const text = seg === \"hours\" ? pad(shownHours) : seg === \"minutes\" ? pad(t.minutes) : pm ? \"PM\" : \"AM\";\n return (\n <React.Fragment key={seg}>\n {seg === \"minutes\" && <span aria-hidden className=\"text-muted\">:</span>}\n {seg === \"period\" && <span aria-hidden className=\"w-1\" />}\n <span\n ref={(el) => {\n refs.current[i] = el;\n }}\n role=\"spinbutton\"\n tabIndex={disabled ? -1 : 0}\n aria-label={label}\n aria-valuenow={seg === \"hours\" ? t.hours : seg === \"minutes\" ? t.minutes : pm ? 1 : 0}\n aria-valuetext={text}\n aria-valuemin={0}\n aria-valuemax={seg === \"hours\" ? 23 : seg === \"minutes\" ? 59 : 1}\n className={segClass}\n onBlur={() => (typed.current = \"\")}\n onKeyDown={(e) => {\n if (e.key === \"ArrowUp\") bump(seg, 1);\n else if (e.key === \"ArrowDown\") bump(seg, -1);\n else if (e.key === \"ArrowRight\") refs.current[i + 1]?.focus();\n else if (e.key === \"ArrowLeft\") refs.current[i - 1]?.focus();\n else if (seg === \"period\" && /^[ap]$/i.test(e.key)) {\n if ((e.key.toLowerCase() === \"p\") !== pm) bump(\"period\", 1);\n } else if (seg !== \"period\" && /^\\d$/.test(e.key)) type(seg, e.key, i);\n else return;\n e.preventDefault();\n }}\n >\n {text}\n </span>\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n","baseSource":null},{"slug":"theme-toggle","name":"Theme toggle","category":"Actions","description":"Switch light and dark: a sliding switch, a three-way segmented control with system, or one icon button.","deps":["lucide-react"],"uses":[],"usage":"<ThemeToggle value={mode} onValueChange={(m, e) => setMode(m)} variant=\"switch\" />","props":[["value / defaultValue","\"light\" | \"dark\" | \"system\"","\"light\"","Current mode."],["onValueChange","(mode, event) => void","","The click event lets you start a reveal from the pointer."],["variant","\"switch\" | \"segmented\" | \"icon\"","\"switch\"","Shape of the control."]],"pro":false,"source":"import * as React from \"react\";\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\n\nexport type ColorMode = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeToggleProps {\n value?: ColorMode;\n defaultValue?: ColorMode;\n onValueChange?: (mode: ColorMode, event: React.MouseEvent) => void;\n /** \"switch\" slides a thumb between sun and moon; \"segmented\" adds a system option; \"icon\" is a single button. */\n variant?: \"switch\" | \"segmented\" | \"icon\";\n className?: string;\n}\n\nconst icon = \"size-4 transition-[transform,opacity] duration-[var(--dur-short)] ease-[var(--ease-out)] motion-reduce:transition-none\";\n\n/** Switch between light and dark, with an optional system choice. Pass the click event on to start a reveal from the pointer. */\nexport function ThemeToggle({ value, defaultValue = \"light\", onValueChange, variant = \"switch\", className }: ThemeToggleProps) {\n const [inner, setInner] = React.useState<ColorMode>(defaultValue);\n const mode = value ?? inner;\n const set = (m: ColorMode, e: React.MouseEvent) => {\n setInner(m);\n onValueChange?.(m, e);\n };\n const dark = mode === \"dark\";\n\n if (variant === \"icon\") {\n return (\n <button\n type=\"button\"\n aria-label={dark ? \"Switch to light mode\" : \"Switch to dark mode\"}\n onClick={(e) => set(dark ? \"light\" : \"dark\", e)}\n className={cn(\n \"relative grid size-9 place-items-center rounded-input border border-[color:var(--key-border)] bg-key-face text-ink shadow-key\",\n \"hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n className,\n )}\n >\n <Sun aria-hidden className={cn(icon, \"absolute\", dark ? \"rotate-90 scale-50 opacity-0\" : \"rotate-0 opacity-100\")} />\n <Moon aria-hidden className={cn(icon, \"absolute\", dark ? \"rotate-0 opacity-100\" : \"-rotate-90 scale-50 opacity-0\")} />\n </button>\n );\n }\n\n if (variant === \"segmented\") {\n const opts: [ColorMode, typeof Sun, string][] = [[\"light\", Sun, \"Light\"], [\"dark\", Moon, \"Dark\"], [\"system\", Monitor, \"System\"]];\n return (\n <div role=\"radiogroup\" aria-label=\"Colour mode\" className={cn(\"inline-flex gap-0.5 rounded-input border border-tray-border bg-tray p-0.5\", className)}>\n {opts.map(([m, Icon, label]) => (\n <button\n key={m}\n type=\"button\"\n role=\"radio\"\n aria-checked={mode === m}\n aria-label={label}\n title={label}\n onClick={(e) => set(m, e)}\n className={cn(\n \"grid size-7 place-items-center rounded-sm text-muted transition-[color,background-color] duration-[var(--dur-micro)]\",\n \"focus-visible:outline-2 focus-visible:outline-focus [&_svg]:size-4\",\n mode === m ? \"bg-key-face text-ink shadow-key\" : \"hover:text-ink\",\n )}\n >\n <Icon aria-hidden />\n </button>\n ))}\n </div>\n );\n }\n\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={dark}\n aria-label=\"Dark mode\"\n onClick={(e) => set(dark ? \"light\" : \"dark\", e)}\n className={cn(\n \"relative inline-flex h-8 w-[60px] shrink-0 items-center rounded-full border border-tray-border bg-tray p-0.5\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus\",\n className,\n )}\n >\n <Sun aria-hidden className=\"absolute left-2 size-3.5 text-muted\" />\n <Moon aria-hidden className=\"absolute right-2 size-3.5 text-muted\" />\n <span\n className={cn(\n \"relative z-10 grid size-[26px] place-items-center rounded-full bg-key-face text-ink shadow-key\",\n \"transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)] motion-reduce:transition-none\",\n dark && \"translate-x-[28px]\",\n )}\n >\n {dark ? <Moon aria-hidden className=\"size-3.5\" /> : <Sun aria-hidden className=\"size-3.5\" />}\n </span>\n </button>\n );\n}\n","baseSource":null},{"slug":"divider","name":"Divider","category":"Display","description":"A rule with an optional label, such as “or” between sign-in options or a date between messages.","deps":[],"uses":[],"usage":"<Divider>or</Divider>\n<Divider align=\"start\" variant=\"dashed\">Yesterday</Divider>","props":[["children","ReactNode","","The label. Leave empty for a plain rule."],["align","\"start\" | \"center\" | \"end\"","\"center\"","Where the label sits."],["variant","\"solid\" | \"dashed\"","\"solid\"","Line style."]],"pro":false,"source":"import * as React from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface DividerProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Where the label sits on the line. */\n align?: \"start\" | \"center\" | \"end\";\n /** \"dashed\" for softer breaks inside a card. */\n variant?: \"solid\" | \"dashed\";\n}\n\n/** A rule with an optional label, such as \"or\" between sign-in options, or a date between messages. */\nexport function Divider({ align = \"center\", variant = \"solid\", className, children, ...props }: DividerProps) {\n const line = cn(\"h-px flex-1\", variant === \"dashed\" ? \"bg-[length:6px_1px] bg-repeat-x [background-image:linear-gradient(to_right,var(--rule-2)_50%,transparent_0)]\" : \"bg-rule\");\n if (!children) return <div role=\"separator\" className={cn(line, \"w-full\", className)} {...props} />;\n return (\n <div role=\"separator\" className={cn(\"flex w-full items-center gap-3 text-xs text-muted\", className)} {...props}>\n {align !== \"start\" && <span aria-hidden className={line} />}\n <span className=\"shrink-0\">{children}</span>\n {align !== \"end\" && <span aria-hidden className={line} />}\n </div>\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":"task-list","name":"Task list","category":"Blocks","description":"A checklist with progress, priorities, add and delete. Done tasks strike through.","deps":["lucide-react"],"uses":["checkbox"],"usage":"<TaskList title=\"Launch week\" tasks={[{ id: \"1\", title: \"Write the changelog\", priority: \"high\" }]} />","props":[["tasks","{ id; title; done?; meta?; priority? }[]","required","The tasks."],["onTasksChange","(tasks) => void","","Pass to control the list yourself."],["addable","boolean","true","Show the add field."]],"pro":false,"source":"import * as React from \"react\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Checkbox } from \"./checkbox\";\n\nexport interface Task {\n id: string;\n title: string;\n done?: boolean;\n /** Short context such as a due date or a project. */\n meta?: string;\n priority?: \"low\" | \"medium\" | \"high\";\n}\n\nexport interface TaskListProps {\n tasks: Task[];\n onTasksChange?: (tasks: Task[]) => void;\n title?: string;\n /** Show the add field at the bottom. */\n addable?: boolean;\n className?: string;\n}\n\nconst tone = { low: \"text-muted bg-paper-3\", medium: \"text-warning bg-warning/12\", high: \"text-danger bg-danger/12\" };\n\n/** A checklist with progress, priorities, add and delete. Completed tasks fade and strike through. */\nexport function TaskList({ tasks: controlled, onTasksChange, title = \"Tasks\", addable = true, className }: TaskListProps) {\n const [inner, setInner] = React.useState(controlled);\n const tasks = onTasksChange ? controlled : inner;\n const set = (next: Task[]) => {\n setInner(next);\n onTasksChange?.(next);\n };\n const [draft, setDraft] = React.useState(\"\");\n const done = tasks.filter((t) => t.done).length;\n const pct = tasks.length ? (done / tasks.length) * 100 : 0;\n const id = React.useId();\n return (\n <section aria-labelledby={`${id}-t`} className={cn(\"vx-glass grid w-full max-w-md gap-3 rounded-panel border border-card-border bg-card p-4 text-ink shadow-card\", className)}>\n <header className=\"grid gap-2\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 id={`${id}-t`} className=\"font-semibold\">{title}</h3>\n <span className=\"text-xs tabular-nums text-muted\">{done} of {tasks.length} done</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-paper-3\" role=\"progressbar\" aria-label=\"Tasks done\" aria-valuenow={Math.round(pct)} aria-valuemin={0} aria-valuemax={100}>\n <div className=\"h-full origin-left rounded-full bg-accent transition-transform duration-[var(--dur-short)] ease-[var(--ease-out)]\" style={{ transform: `scaleX(${pct / 100})` }} />\n </div>\n </header>\n <ul className=\"grid gap-0.5\">\n {tasks.map((t) => (\n <li key={t.id} className=\"group flex items-center gap-3 rounded-input px-2 py-2 hover:bg-paper-2\">\n <Checkbox\n id={`${id}-${t.id}`}\n checked={!!t.done}\n onCheckedChange={(v) => set(tasks.map((x) => (x.id === t.id ? { ...x, done: v === true } : x)))}\n />\n <label htmlFor={`${id}-${t.id}`} className=\"grid min-w-0 flex-1 cursor-pointer\">\n <span className={cn(\"truncate text-sm transition-colors duration-[var(--dur-micro)]\", t.done ? \"text-muted line-through\" : \"text-ink\")}>{t.title}</span>\n {t.meta && <span className=\"truncate text-xs text-muted\">{t.meta}</span>}\n </label>\n {t.priority && !t.done && <span className={cn(\"rounded-sm px-1.5 py-0.5 text-[11px] font-medium capitalize\", tone[t.priority])}>{t.priority}</span>}\n <button\n type=\"button\"\n aria-label={`Delete ${t.title}`}\n onClick={() => set(tasks.filter((x) => x.id !== t.id))}\n className=\"grid size-7 place-items-center rounded-sm text-muted opacity-0 hover:bg-paper-3 hover:text-danger focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-focus group-hover:opacity-100\"\n >\n <Trash2 className=\"size-3.5\" />\n </button>\n </li>\n ))}\n </ul>\n {addable && (\n <form\n className=\"flex items-center gap-2 rounded-input border border-dashed border-rule-2 px-2 focus-within:border-accent\"\n onSubmit={(e) => {\n e.preventDefault();\n const title = draft.trim();\n if (!title) return;\n set([...tasks, { id: `${Date.now()}`, title }]);\n setDraft(\"\");\n }}\n >\n <Plus aria-hidden className=\"size-4 text-muted\" />\n <input\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Add a task and press Enter\"\n aria-label=\"New task\"\n className=\"h-9 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted\"\n />\n </form>\n )}\n </section>\n );\n}\n","baseSource":null},{"slug":"stat-cards","name":"Stat cards","category":"Blocks","description":"KPI cards: a grey shell, a lit icon tile, the number and a white footer with the change.","deps":["lucide-react"],"uses":["tooltip"],"usage":"<StatCards>\n <StatCard label=\"Total revenue\" value=\"$152,313\" delta={16} icon={<DollarSign />} />\n</StatCards>","props":[["label / value","string / ReactNode","required","What it measures and the number."],["delta","number","","Percent change, shown in the footer."],["comparison","string","\"From last month\"","Footer text."],["icon / color","ReactNode / string","accent","Icon tile and its colour."],["info","string","","Tooltip on the info button."],["invert","boolean","false","Set when a fall is good."]],"pro":false,"source":"import * as React from \"react\";\nimport { Info, TrendingDown, TrendingUp } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Tooltip } from \"./tooltip\";\n\nexport interface StatCardProps {\n label: string;\n value: React.ReactNode;\n /** Percent change against the comparison period. */\n delta?: number;\n /** Text in the footer, such as \"From last month\". */\n comparison?: string;\n icon?: React.ReactNode;\n /** Any CSS colour for the icon tile. */\n color?: string;\n /** Shown in a tooltip on the info button. */\n info?: string;\n /** Set when a fall is good, such as refunds. */\n invert?: boolean;\n className?: string;\n}\n\n/** A KPI card: a grey shell, a coloured icon, the number, and a white footer with the change. */\nexport function StatCard({ label, value, delta, comparison = \"From last month\", icon, color = \"var(--accent)\", info, invert, className }: StatCardProps) {\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={label} className={cn(\"vx-glass flex min-w-0 flex-col gap-4 rounded-panel border border-card-border bg-card p-2 text-ink shadow-card\", className)}>\n <div className=\"grid gap-3 px-2 pt-2\">\n <div className=\"flex items-start justify-between\">\n {icon && (\n <span className=\"grid size-8 place-items-center rounded-input text-[oklch(100%_0_0)] shadow-[inset_0_1.5px_0_oklch(100%_0_0/0.25)] [&_svg]:size-4\" style={{ background: `linear-gradient(${color}, color-mix(in oklab, ${color} 82%, oklch(0% 0 0)))` }}>\n {icon}\n </span>\n )}\n {info && (\n <Tooltip content={info}>\n <button type=\"button\" aria-label={`About ${label}`} className=\"grid size-6 place-items-center rounded-full text-muted hover:text-ink focus-visible:outline-2 focus-visible:outline-focus\">\n <Info className=\"size-4\" />\n </button>\n </Tooltip>\n )}\n </div>\n <div className=\"grid gap-0.5\">\n <span className=\"text-sm text-muted\">{label}</span>\n <span className=\"text-[28px] font-semibold leading-tight tracking-[-0.02em] tabular-nums\">{value}</span>\n </div>\n </div>\n {delta !== undefined && (\n <div className=\"mt-auto flex items-center justify-between gap-2 rounded-input bg-paper py-1.5 pl-2.5 pr-1.5 text-sm text-muted shadow-[0_1px_1px_oklch(0%_0_0/0.05)]\">\n {comparison}\n <span className={cn(\"flex items-center gap-1 rounded-full py-0.5 pl-1 pr-2 text-xs font-semibold\", good ? \"bg-success-wash text-success-wash-ink\" : \"bg-danger-wash text-danger-wash-ink\")}>\n <Trend className=\"size-3.5\" /> {Math.abs(delta)}%\n </span>\n </div>\n )}\n </section>\n );\n}\n\n/** A responsive grid of stat cards. */\nexport function StatCards({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn(\"grid w-full gap-3 sm:grid-cols-2 xl:grid-cols-4\", className)}>{children}</div>;\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},{"slug":"agent-limits","name":"Agent limits","category":"Blocks","description":"Usage against plan limits. Bars turn amber at 80% and red at the limit, with a note.","deps":["lucide-react"],"uses":["button"],"usage":"<AgentLimits plan=\"Team\" resets=\"in 3 days\" limits={[{ label: \"Messages\", used: 412, max: 500 }]} />","props":[["limits","{ label; used; max; unit? }[]","required","One bar each."],["plan / resets","string","","Header text."],["onUpgrade","() => void","","Shows an Upgrade button."]],"pro":false,"source":"import * as React from \"react\";\nimport { TriangleAlert, Zap } from \"lucide-react\";\nimport { cn } from \"@/lib/cn\";\nimport { Button } from \"./button\";\n\nexport interface Limit {\n label: string;\n used: number;\n max: number;\n /** Shown after the numbers, such as \"messages\". */\n unit?: string;\n}\n\nexport interface AgentLimitsProps {\n plan?: string;\n limits: Limit[];\n /** When the allowance resets, such as \"in 3 days\". */\n resets?: string;\n onUpgrade?: () => void;\n className?: string;\n}\n\nconst tone = (pct: number) => (pct >= 1 ? \"bg-danger\" : pct >= 0.8 ? \"bg-warning\" : \"bg-accent [background-image:var(--control-on-image)]\");\n\n/** Usage against plan limits. Bars turn amber at 80% and red when a limit is reached. */\nexport function AgentLimits({ plan = \"Free\", limits, resets, onUpgrade, className }: AgentLimitsProps) {\n const worst = Math.max(...limits.map((l) => l.used / l.max));\n return (\n <section aria-label=\"Usage\" className={cn(\"vx-glass grid w-full max-w-sm gap-2 rounded-panel border border-card-border bg-card p-2 text-ink shadow-card\", className)}>\n <header className=\"flex items-center justify-between gap-2 px-2.5 pt-2\">\n <div>\n <p className=\"text-sm font-medium\">Usage</p>\n <p className=\"text-xs text-muted\">{plan} plan{resets && `, resets ${resets}`}</p>\n </div>\n {onUpgrade && <Button size=\"xs\" onClick={onUpgrade}><Zap /> Upgrade</Button>}\n </header>\n <ul className=\"grid gap-3.5 rounded-input bg-paper p-3.5 shadow-[0_1px_1px_oklch(0%_0_0/0.05)]\">\n {limits.map((l) => {\n const pct = Math.min(1, l.used / l.max);\n return (\n <li key={l.label} className=\"grid gap-1.5\">\n <div className=\"flex items-baseline justify-between gap-2 text-sm\">\n <span className=\"font-medium\">{l.label}</span>\n <span className=\"tabular-nums text-muted\"><b className=\"font-medium text-ink\">{l.used.toLocaleString()}</b> / {l.max.toLocaleString()}{l.unit && ` ${l.unit}`}</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-[color:var(--track-off)]\" role=\"meter\" aria-label={l.label} aria-valuenow={l.used} aria-valuemin={0} aria-valuemax={l.max}>\n <div className={cn(\"h-full origin-left rounded-full transition-transform duration-[var(--dur-short)]\", tone(pct))} style={{ transform: `scaleX(${pct})` }} />\n </div>\n </li>\n );\n })}\n </ul>\n {worst >= 0.8 && (\n <p className={cn(\"flex items-start gap-2 rounded-input px-3 py-2 text-xs\", worst >= 1 ? \"bg-danger-wash text-danger-wash-ink\" : \"bg-warning-wash text-warning-wash-ink\")}>\n <TriangleAlert className=\"mt-px size-3.5 shrink-0\" />\n {worst >= 1 ? \"You have reached a limit. New runs wait until it resets.\" : \"You are close to a limit. Runs may pause soon.\"}\n </p>\n )}\n </section>\n );\n}\n","baseSource":null}],"templates":[]}
|