{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button-variants",
  "type": "registry:ui",
  "title": "Button Variants",
  "description": "The cva variant map that `button.tsx` imports — variant × size class strings and nothing else. Pure CVA: no React, no client APIs, so a server component can style a plain `<a>` with `buttonVariants({ variant, size })`.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "form",
    "primitive"
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/button-variants.ts",
      "target": "components/ui/button-variants.ts",
      "type": "registry:ui",
      "content": "import { cva, type VariantProps } from 'class-variance-authority';\n\n// @interlace/button-variants v1.3.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/button-variants\n// What changed since: https://ds.interlace.tools/c/button-variants#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — buttonVariants\n *\n * The cva variant map that `button.tsx` imports — variant × size class strings\n * and nothing else. Pure CVA: no React, no client APIs, so a server component\n * can style a plain `<a>` with `buttonVariants({ variant, size })`.\n *\n * This is not a component. It exports a function and a type, renders nothing,\n * and has no props, no slots and no DOM — which is exactly why it is a\n * separate module from the `'use client'` Button that consumes it.\n *\n * ## Anatomy\n *\n *   buttonVariants({ variant, size })\n *     ├─ variant  default | destructive | outline | secondary | ghost | link\n *     └─ size     default | xs | sm | lg | icon | icon-xs | icon-sm | icon-lg\n *\n * Defaults are `variant: 'default'`, `size: 'default'`. `pagination.tsx` is\n * the in-repo example of styling an element rather than nesting a Button: its\n * `<a>` takes `buttonVariants({ variant, size })` directly. (It reaches the\n * function through `button.js`, which re-exports it — that path is\n * `'use client'`; import from this module to keep a server tree server-side.)\n *\n * ## Why half this file is comments\n *\n * Four of the six variants carry a long note recording a measured contrast\n * failure and the rule it produced: a variant that declares its own foreground\n * must paint an opaque surface in the same state, because a button is dropped\n * onto surfaces the design system does not control. `outline` inheriting the\n * page text into a `bg-primary` section measured 1.05:1; `link` with\n * `text-primary` on the same section measured 1.00:1. `composite-contrast-lock`\n * composites every variant over every brandable backdrop, so those notes are\n * checkable rather than folklore.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R8   | Enums, no booleans               | `variant` and `size` are closed string enums                |\n * | R19  | Tokens only                      | `bg-primary`, `bg-destructive`, `bg-accent`, `ring-ring/50` |\n * | R20  | AA contrast                      | every pair composited by `composite-contrast-lock`          |\n * | R25  | Server-safe                      | no React import, no hooks, no `'use client'`                |\n */\n\n// Server-safe button variants. The full `<Button>` component lives in\n// `./button.tsx` and is `'use client'` (Base UI useRender). Variants are a\n// pure CVA function — no React, no client APIs — so they can be called\n// from server components when composing className strings (e.g. on a\n// `<Link className={buttonVariants(...)}>` in a server-rendered page).\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n        // No `dark:bg-destructive/60` here, unlike stock shadcn — see the\n        // matching note in badge.tsx. Our dark destructive token is a light\n        // red tuned for AAA on near-black; muting it to 60% drops the label\n        // to 4.27:1, under AA. Caught by composite-contrast-lock.\n        destructive:\n          'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',\n        // `text-foreground` is NOT redundant, and neither is the ABSENCE of\n        // stock shadcn's `dark:bg-input/30` / `dark:hover:bg-input/50`. Both\n        // halves of that come from the same shipped defect, found twice.\n        //\n        // First half: `outline` paints an opaque surface (`bg-background`)\n        // but was the only such variant that did not also set a foreground,\n        // so it INHERITED one. Dropped inside a section that flips the text\n        // colour — `<CTASection tone=\"primary\">` is `bg-primary\n        // text-primary-foreground` — the button kept the page background and\n        // took the section's white text: 1.05:1, invisible.\n        //\n        // Second half: declaring a foreground only means something if the\n        // surface under it is actually opaque. `dark:bg-input/30` overrode\n        // `bg-background` with a 30% tint, so in dark mode the same\n        // `bg-primary` section bled through — `#6b635a` at 30% over `#fbb99a`\n        // composites to `#d09f87`, and `text-foreground` `#f0ede9` lands on\n        // that at 2.00:1. The hover state was worse-shaped and identical in\n        // result (`dark:hover:bg-input/50` + `hover:text-accent-foreground`,\n        // 2.00:1). Dropping both makes dark match what light already did:\n        // no fill, the control identified by its border — `dark:border-input`\n        // is 3.35:1 on the dark background, clearing SC 1.4.11.\n        //\n        // The rule, stated so it is checkable: a variant that declares its own\n        // foreground must paint an opaque surface in the same state, because\n        // a button is dropped onto surfaces the DS does not control. Locked by\n        // composite-contrast-lock, which composites every variant over every\n        // brandable backdrop rather than over the page background.\n        outline:\n          'border bg-background text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input',\n        secondary:\n          'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        // No `dark:hover:bg-accent/50` here, for the reason spelled out on\n        // `outline`: it declares `hover:text-accent-foreground`, so a 50% hover\n        // surface let a `bg-primary` section through at 3.07:1. Opaque\n        // `bg-accent` carries its own foreground at 10.46:1 in both schemes.\n        // At rest `ghost` declares neither colour and inherits both, which is\n        // the variant's whole point and is safe anywhere.\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        // Inherits its colour, and is underlined at rest rather than only on\n        // hover. Stock shadcn is `text-primary … hover:underline`, which is\n        // the same defect as the two above in its purest form: a declared\n        // foreground over a surface the variant does not paint. `text-primary`\n        // on a `<CTASection tone=\"primary\">` is `#7d350c` on `#7d350c` —\n        // 1.00:1, the button is literally the same colour as the section.\n        //\n        // A variant with no surface of its own cannot name a colour; the only\n        // safe foreground is the inherited one, which is by construction the\n        // pair its surface was measured with. That leaves the underline to\n        // carry the affordance, so it has to be there at rest — which also\n        // retires a colour-only link cue (WCAG 1.4.1) and matches how `Prose`\n        // already renders links. `decoration-from-font` is Prose's too; hover\n        // thickens the rule instead of shifting the hue, because any hue we\n        // could shift to reintroduces the bug.\n        link: 'underline underline-offset-4 decoration-from-font hover:decoration-2',\n      },\n      size: {\n        default: 'h-9 px-4 py-2 has-[>svg]:px-3',\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',\n        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n        icon: 'size-9',\n        'icon-xs': \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        'icon-sm': 'size-8',\n        'icon-lg': 'size-10',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport { buttonVariants };\nexport type { VariantProps };\n"
    }
  ],
  "meta": {
    "tier": "primitive",
    "client": false,
    "minViewport": null,
    "loading": false,
    "version": "1.3.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/button-variants\n\nInstalled to `components/ui/button-variants.ts`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/button-variants';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/button-variants\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
