{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "A simple button component.",
  "dependencies": [
    "class-variance-authority",
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "files": [
    {
      "path": "registry/default/button/button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Button as BaseButton } from \"@base-ui/react/button\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\n// Package root, not a subpath. Since 4.3 the root is a barrel of per-icon\n// re-exports that the `optimizePackageImports` entry in next.config.mjs rewrites\n// to direct imports, so only the icons in use get parsed. It is also the only\n// entry point that ships types; the per-icon subpaths have no .d.ts. Next does\n// not optimize this package by default, so an app installing this component\n// should add \"@hugeicons/core-free-icons\" to its own `optimizePackageImports`.\nimport { Loading03Icon } from \"@hugeicons/core-free-icons\";\nimport { cn } from \"@/lib/utils\";\n\n// The button's fill and border render on a ::before pseudo-element that\n// scales down on press (the pill shrinks, the label stays put). Because the\n// pseudo travels with the classes, every consumer of the recipe — <Button>\n// and flat elements styled via `buttonVariants` (pagination links, calendar\n// nav, toolbar buttons) — gets identical paint and press behavior with no\n// extra DOM. ButtonGroup collapses borders between segments with `before:`\n// rules, inert on children that don't carry the recipe.\n//\n// Paint: variants set per-state tokens (--btn-bg, --btn-bg-hover,\n// --btn-bg-active, --btn-border), the state machine below resolves them into\n// --btn-paint, and buttonPaint renders it on the pseudo (custom properties\n// inherit into pseudo-elements). Consumers recolor by overriding the tokens\n// (className=\"[--btn-bg:...]\") and restyle the border via `before:` classes\n// (className=\"before:border-dashed\").\nconst buttonBase = cn(\n  \"relative isolate inline-flex items-center cursor-pointer justify-center whitespace-nowrap rounded-lg font-medium data-disabled:pointer-events-none data-disabled:opacity-60 data-disabled:focus-visible:outline-ring [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 focus-visible:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color,scale,opacity,background-color,color] duration-100 ease-out outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 aria-invalid:outline-destructive/50 aria-invalid:outline-2 aria-invalid:outline-offset-2 aria-invalid:outline-solid\",\n  // State machine: unset tokens fall through to transparent. Pressing shows\n  // the active paint — except on popup triggers (aria-haspopup), which skip\n  // pressed feedback entirely (mirroring the press-scale guard) and simply\n  // hold their hover paint while open (data-popup-open, stamped by Base UI).\n  \"[--btn-paint:var(--btn-bg,transparent)] hover:[--btn-paint:var(--btn-bg-hover,var(--btn-bg,transparent))] active:not-aria-[haspopup]:[--btn-paint:var(--btn-bg-active,var(--btn-bg-hover,var(--btn-bg,transparent)))] data-popup-open:[--btn-paint:var(--btn-bg-hover,var(--btn-bg,transparent))]\",\n);\n\n// The paint pseudo-element: 1px border + fill from the resolved vars,\n// transparent when unset. -z-10 keeps it under the content (the root's\n// `isolate` contains it), and pressing scales only the pseudo — except on\n// popup triggers (aria-haspopup), which skip press feedback and hold their\n// hover paint while open instead. Reduced motion keeps the press tint only.\nconst buttonPaint =\n  \"before:content-[''] before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] before:border before:border-[color:var(--btn-border,transparent)] before:bg-[var(--btn-paint,transparent)] before:transition-[background-color,border-color,scale] before:duration-100 before:ease-out motion-safe:active:not-aria-[haspopup]:before:scale-[0.98]\";\n\n// Text classes plus the paint tokens the state machine reads.\nconst buttonVariantClasses = {\n  primary:\n    \"text-primary-foreground [--btn-bg:var(--primary)] [--btn-bg-hover:var(--primary-hover)] [--btn-bg-active:var(--primary-active)]\",\n  \"primary-soft\":\n    \"text-(--primary-soft-foreground) [--btn-bg:var(--secondary)] [--btn-bg-hover:var(--secondary-hover)] [--btn-bg-active:var(--secondary-active)]\",\n  neutral:\n    \"text-neutral-foreground [--btn-bg:var(--neutral)] [--btn-bg-hover:var(--neutral-hover)] [--btn-bg-active:var(--neutral-active)]\",\n  destructive:\n    \"text-destructive-foreground [--btn-border:rgb(0_0_0/0.05)] dark:[--btn-border:rgb(255_255_255/0.05)] [--btn-bg:var(--destructive)] [--btn-bg-hover:var(--destructive-hover)] [--btn-bg-active:var(--destructive-active)]\",\n  \"destructive-soft\":\n    \"text-(--destructive-soft-foreground) [--btn-bg:color-mix(in_oklab,var(--destructive)_12%,transparent)] [--btn-bg-hover:color-mix(in_oklab,var(--destructive)_20%,transparent)] [--btn-bg-active:color-mix(in_oklab,var(--destructive)_25%,transparent)]\",\n  // bg-clip-padding keeps the card fill out from under the translucent\n  // border. Deliberately NOT in the shared paint: with the default\n  // transparent border it would inset every solid fill by 1px.\n  outline:\n    \"[--btn-border:var(--border)] [--btn-bg:var(--card)] [--btn-bg-hover:var(--outline-hover)] [--btn-bg-active:var(--outline-active)] before:bg-clip-padding\",\n  // Outline's border with ghost's transparent fill, for elevated substrates\n  // (Cards, Dialogs) where outline's solid card fill would look mismatched.\n  \"outline-ghost\":\n    \"[--btn-border:var(--border)] [--btn-bg-hover:var(--surface-hover)] [--btn-bg-active:var(--surface-active)]\",\n  secondary:\n    \"text-secondary-foreground [--btn-bg:var(--secondary)] [--btn-bg-hover:var(--secondary-hover)] [--btn-bg-active:var(--secondary-active)]\",\n  ghost:\n    \"text-muted-foreground hover:text-foreground data-popup-open:text-foreground [--btn-bg-hover:var(--surface-hover)] [--btn-bg-active:var(--surface-active)]\",\n  link: \"text-primary underline-offset-4 hover:underline\",\n};\n\n// Heights match the Input/Select ramp (36px desktop default) so buttons pair\n// with form fields. Below the sm breakpoint each size is one step taller for\n// touch targets.\nconst buttonSizeVariantClasses = {\n  default: \"h-10 sm:not-pointer-coarse:h-9 px-3.5 gap-1.5 text-sm\",\n  xs: \"h-8 sm:not-pointer-coarse:h-7 px-2.5 gap-1 text-xs rounded-md [&_svg:not([class*='size-'])]:size-3.5\",\n  sm: \"h-9 sm:not-pointer-coarse:h-8 px-3 gap-1.5 text-sm\",\n  lg: \"h-11 sm:not-pointer-coarse:h-10 px-3.5 gap-1.5 text-base [&_svg:not([class*='size-'])]:size-5\",\n  icon: \"size-10 sm:not-pointer-coarse:size-9 text-sm\",\n  icon_xs:\n    \"size-8 sm:not-pointer-coarse:size-7 rounded-md text-xs [&_svg:not([class*='size-'])]:size-3.5\",\n  icon_sm: \"size-9 sm:not-pointer-coarse:size-8 text-sm\",\n  icon_lg:\n    \"size-11 sm:not-pointer-coarse:size-10 text-base [&_svg:not([class*='size-'])]:size-5\",\n};\n\n// Sizes with no text slot. For these, children render bare (no text-box\n// wrapper) and leadingIcon/trailingIcon are ignored — a fixed square has no\n// room for an icon beside a label.\nconst iconOnlySizes = new Set<keyof typeof buttonSizeVariantClasses>([\n  \"icon\",\n  \"icon_xs\",\n  \"icon_sm\",\n  \"icon_lg\",\n]);\n\n// Fills the root and mirrors its justification so root-level overrides like\n// justify-between keep working.\nconst buttonContentLayout =\n  \"inline-flex w-full items-center [justify-content:inherit] gap-[inherit]\";\n\n// The one button recipe, used by <Button> and by flat elements (links,\n// calendar nav, toolbar buttons) alike — paint, states, and press behavior\n// are identical in both. iconLeft/iconRight tighten padding on the icon side\n// (an icon is visually lighter than a text edge); <Button> sets them from\n// its icon props, flat consumers can pass them explicitly.\nconst buttonVariants = cva(cn(buttonBase, buttonPaint), {\n  variants: {\n    variant: buttonVariantClasses,\n    size: buttonSizeVariantClasses,\n    iconLeft: { true: \"\" },\n    iconRight: { true: \"\" },\n  },\n  compoundVariants: [\n    { size: \"default\", iconLeft: true, className: \"pl-2.5\" },\n    { size: \"default\", iconRight: true, className: \"pr-2.5\" },\n    { size: \"sm\", iconLeft: true, className: \"pl-2\" },\n    { size: \"sm\", iconRight: true, className: \"pr-2\" },\n    { size: \"xs\", iconLeft: true, className: \"pl-2\" },\n    { size: \"xs\", iconRight: true, className: \"pr-2\" },\n    { size: \"lg\", iconLeft: true, className: \"pl-2.5\" },\n    { size: \"lg\", iconRight: true, className: \"pr-2.5\" },\n  ],\n  defaultVariants: {\n    variant: \"primary\",\n    size: \"default\",\n  },\n});\n\nexport type ButtonProps = BaseButton.Props &\n  Omit<VariantProps<typeof buttonVariants>, \"iconLeft\" | \"iconRight\"> & {\n    loading?: boolean;\n    leadingIcon?: React.ReactNode;\n    trailingIcon?: React.ReactNode;\n  };\n\nfunction Button({\n  className,\n  variant,\n  size,\n  loading,\n  children,\n  disabled,\n  focusableWhenDisabled,\n  leadingIcon,\n  trailingIcon,\n  ...props\n}: ButtonProps) {\n  const isIconOnly = size != null && iconOnlySizes.has(size);\n\n  const content = isIconOnly ? (\n    children\n  ) : (\n    <>\n      {leadingIcon}\n      {/* text-box trims ascent/descent whitespace for optical centering;\n          unsupported browsers fall back to metrics centering. */}\n      <span className=\"[text-box:trim-both_cap_alphabetic]\">{children}</span>\n      {trailingIcon}\n    </>\n  );\n\n  return (\n    <BaseButton\n      data-slot=\"button\"\n      data-size={size}\n      data-variant={variant}\n      className={cn(\n        buttonVariants({\n          variant,\n          size,\n          iconLeft: !!leadingIcon,\n          iconRight: !!trailingIcon,\n        }),\n        className,\n      )}\n      disabled={disabled || loading}\n      aria-busy={loading || undefined}\n      {...props}\n      focusableWhenDisabled={focusableWhenDisabled ?? loading}\n    >\n      <span\n        data-slot=\"button-content\"\n        className={cn(\"relative\", buttonContentLayout)}\n      >\n        {loading ? (\n          <>\n            {/* Real content stays laid out (invisible) so the size holds. */}\n            <span className={cn(buttonContentLayout, \"opacity-0\")}>\n              {content}\n            </span>\n            <span\n              aria-hidden\n              className=\"absolute inset-0 flex items-center justify-center\"\n            >\n              <HugeiconsIcon\n                icon={Loading03Icon}\n                className=\"animate-spin motion-reduce:animate-pulse\"\n                strokeWidth={2}\n              />\n            </span>\n            {/* No sr-only live region here, deliberately. `sr-only` clips\n                rather than hides, so its text joins name-from-content and the\n                button would rename itself mid-request (\"Save\" -> \"Save\n                Loading\"). And it would not announce anyway: `role=\"button\"` is\n                Children Presentational, and a region that mounts already\n                holding its text has not changed. `aria-busy` and `disabled`\n                above are the signals that work. Callers who want words swap\n                their own label, or render their own live region OUTSIDE the\n                button — see CopyButton, which does exactly that. */}\n          </>\n        ) : (\n          content\n        )}\n      </span>\n    </BaseButton>\n  );\n}\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/button.tsx"
    }
  ],
  "type": "registry:ui"
}