{
  "$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\";\r\n\r\nimport * as React from \"react\";\r\nimport { cva, type VariantProps } from \"class-variance-authority\";\r\nimport { Button as BaseButton } from \"@base-ui/react/button\";\r\nimport { HugeiconsIcon } from \"@hugeicons/react\";\r\nimport { Loading03Icon } from \"@hugeicons/core-free-icons\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// The button's fill and border render on a ::before pseudo-element that\r\n// scales down on press (the pill shrinks, the label stays put). Because the\r\n// pseudo travels with the classes, every consumer of the recipe — <Button>\r\n// and flat elements styled via `buttonVariants` (pagination links, calendar\r\n// nav, toolbar buttons) — gets identical paint and press behavior with no\r\n// extra DOM. ButtonGroup collapses borders between segments with `before:`\r\n// rules, inert on children that don't carry the recipe.\r\n//\r\n// Paint: variants set per-state tokens (--btn-bg, --btn-bg-hover,\r\n// --btn-bg-active, --btn-border), the state machine below resolves them into\r\n// --btn-paint, and buttonPaint renders it on the pseudo (custom properties\r\n// inherit into pseudo-elements). Consumers recolor by overriding the tokens\r\n// (className=\"[--btn-bg:...]\") and restyle the border via `before:` classes\r\n// (className=\"before:border-dashed\").\r\nconst buttonBase = cn(\r\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\",\r\n  // State machine: unset tokens fall through to transparent. Pressing shows\r\n  // the active paint — except on popup triggers (aria-haspopup), which skip\r\n  // pressed feedback entirely (mirroring the press-scale guard) and simply\r\n  // hold their hover paint while open (data-popup-open, stamped by Base UI).\r\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))]\",\r\n);\r\n\r\n// The paint pseudo-element: 1px border + fill from the resolved vars,\r\n// transparent when unset. -z-10 keeps it under the content (the root's\r\n// `isolate` contains it), and pressing scales only the pseudo — except on\r\n// popup triggers (aria-haspopup), which skip press feedback and hold their\r\n// hover paint while open instead.\r\nconst buttonPaint =\r\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 active:not-aria-[haspopup]:before:scale-[0.98]\";\r\n\r\n// Text classes plus the paint tokens the state machine reads.\r\nconst buttonVariantClasses = {\r\n  primary:\r\n    \"text-primary-foreground [--btn-bg:var(--primary)] [--btn-bg-hover:var(--primary-hover)] [--btn-bg-active:var(--primary-active)]\",\r\n  \"primary-soft\":\r\n    \"text-(--primary-soft-foreground) [--btn-bg:var(--secondary)] [--btn-bg-hover:var(--secondary-hover)] [--btn-bg-active:var(--secondary-active)]\",\r\n  neutral:\r\n    \"text-neutral-foreground [--btn-bg:var(--neutral)] [--btn-bg-hover:var(--neutral-hover)] [--btn-bg-active:var(--neutral-active)]\",\r\n  destructive:\r\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)]\",\r\n  \"destructive-soft\":\r\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)]\",\r\n  // bg-clip-padding keeps the card fill out from under the translucent\r\n  // border. Deliberately NOT in the shared paint: with the default\r\n  // transparent border it would inset every solid fill by 1px.\r\n  outline:\r\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\",\r\n  // Outline's border with ghost's transparent fill, for elevated substrates\r\n  // (Cards, Dialogs) where outline's solid card fill would look mismatched.\r\n  \"outline-ghost\":\r\n    \"[--btn-border:var(--border)] [--btn-bg-hover:var(--surface-hover)] [--btn-bg-active:var(--surface-active)]\",\r\n  secondary:\r\n    \"text-secondary-foreground [--btn-bg:var(--secondary)] [--btn-bg-hover:var(--secondary-hover)] [--btn-bg-active:var(--secondary-active)]\",\r\n  ghost:\r\n    \"text-muted-foreground hover:text-foreground data-popup-open:text-foreground [--btn-bg-hover:var(--surface-hover)] [--btn-bg-active:var(--surface-active)]\",\r\n  link: \"text-primary underline-offset-4 hover:underline\",\r\n};\r\n\r\n// Heights match the Input/Select ramp (36px desktop default) so buttons pair\r\n// with form fields. Below the sm breakpoint each size is one step taller for\r\n// touch targets.\r\nconst buttonSizeVariantClasses = {\r\n  default: \"h-10 sm:h-9 px-3.5 gap-1.5 text-sm\",\r\n  xs: \"h-8 sm:h-7 px-2.5 gap-1 text-xs rounded-md [&_svg:not([class*='size-'])]:size-3.5\",\r\n  sm: \"h-9 sm:h-8 px-3 gap-1.5 text-sm\",\r\n  lg: \"h-11 sm:h-10 px-3.5 gap-1.5 text-base [&_svg:not([class*='size-'])]:size-5\",\r\n  icon: \"size-10 sm:size-9 text-sm\",\r\n  icon_xs:\r\n    \"size-8 sm:size-7 rounded-md text-xs [&_svg:not([class*='size-'])]:size-3.5\",\r\n  icon_sm: \"size-9 sm:size-8 text-[13px]\",\r\n  icon_lg: \"size-11 sm:size-10 text-base [&_svg:not([class*='size-'])]:size-5\",\r\n};\r\n\r\n// Sizes with no text slot. For these, children render bare (no text-box\r\n// wrapper) and leadingIcon/trailingIcon are ignored — a fixed square has no\r\n// room for an icon beside a label.\r\nconst iconOnlySizes = new Set<keyof typeof buttonSizeVariantClasses>([\r\n  \"icon\",\r\n  \"icon_xs\",\r\n  \"icon_sm\",\r\n  \"icon_lg\",\r\n]);\r\n\r\n// Fills the root and mirrors its justification so root-level overrides like\r\n// justify-between keep working.\r\nconst buttonContentLayout =\r\n  \"inline-flex w-full items-center [justify-content:inherit] gap-[inherit]\";\r\n\r\n// The one button recipe, used by <Button> and by flat elements (links,\r\n// calendar nav, toolbar buttons) alike — paint, states, and press behavior\r\n// are identical in both. iconLeft/iconRight tighten padding on the icon side\r\n// (an icon is visually lighter than a text edge); <Button> sets them from\r\n// its icon props, flat consumers can pass them explicitly.\r\nconst buttonVariants = cva(cn(buttonBase, buttonPaint), {\r\n  variants: {\r\n    variant: buttonVariantClasses,\r\n    size: buttonSizeVariantClasses,\r\n    iconLeft: { true: \"\" },\r\n    iconRight: { true: \"\" },\r\n  },\r\n  compoundVariants: [\r\n    { size: \"default\", iconLeft: true, className: \"pl-2.5\" },\r\n    { size: \"default\", iconRight: true, className: \"pr-2.5\" },\r\n    { size: \"sm\", iconLeft: true, className: \"pl-2\" },\r\n    { size: \"sm\", iconRight: true, className: \"pr-2\" },\r\n    { size: \"xs\", iconLeft: true, className: \"pl-2\" },\r\n    { size: \"xs\", iconRight: true, className: \"pr-2\" },\r\n    { size: \"lg\", iconLeft: true, className: \"pl-2.5\" },\r\n    { size: \"lg\", iconRight: true, className: \"pr-2.5\" },\r\n  ],\r\n  defaultVariants: {\r\n    variant: \"primary\",\r\n    size: \"default\",\r\n  },\r\n});\r\n\r\nexport type ButtonProps = BaseButton.Props &\r\n  Omit<VariantProps<typeof buttonVariants>, \"iconLeft\" | \"iconRight\"> & {\r\n    loading?: boolean;\r\n    leadingIcon?: React.ReactNode;\r\n    trailingIcon?: React.ReactNode;\r\n  };\r\n\r\nfunction Button({\r\n  className,\r\n  variant,\r\n  size,\r\n  loading,\r\n  children,\r\n  disabled,\r\n  focusableWhenDisabled,\r\n  leadingIcon,\r\n  trailingIcon,\r\n  ...props\r\n}: ButtonProps) {\r\n  const isIconOnly = size != null && iconOnlySizes.has(size);\r\n\r\n  const content = isIconOnly ? (\r\n    children\r\n  ) : (\r\n    <>\r\n      {leadingIcon}\r\n      {/* text-box trims ascent/descent whitespace for optical centering;\r\n          unsupported browsers fall back to metrics centering. */}\r\n      <span className=\"[text-box:trim-both_cap_alphabetic]\">{children}</span>\r\n      {trailingIcon}\r\n    </>\r\n  );\r\n\r\n  return (\r\n    <BaseButton\r\n      data-slot=\"button\"\r\n      data-size={size}\r\n      data-variant={variant}\r\n      className={cn(\r\n        buttonVariants({\r\n          variant,\r\n          size,\r\n          iconLeft: !!leadingIcon,\r\n          iconRight: !!trailingIcon,\r\n        }),\r\n        className,\r\n      )}\r\n      disabled={disabled || loading}\r\n      aria-busy={loading || undefined}\r\n      {...props}\r\n      focusableWhenDisabled={focusableWhenDisabled ?? loading}\r\n    >\r\n      <span\r\n        data-slot=\"button-content\"\r\n        className={cn(\"relative\", buttonContentLayout)}\r\n      >\r\n        {loading ? (\r\n          <>\r\n            {/* Real content stays laid out (invisible) so the size holds. */}\r\n            <span className={cn(buttonContentLayout, \"opacity-0\")}>\r\n              {content}\r\n            </span>\r\n            <span\r\n              aria-hidden\r\n              className=\"absolute inset-0 flex items-center justify-center\"\r\n            >\r\n              <HugeiconsIcon\r\n                icon={Loading03Icon}\r\n                className=\"animate-spin motion-reduce:animate-pulse\"\r\n                strokeWidth={2}\r\n              />\r\n            </span>\r\n            <span role=\"status\" className=\"sr-only\">\r\n              Loading\r\n            </span>\r\n          </>\r\n        ) : (\r\n          content\r\n        )}\r\n      </span>\r\n    </BaseButton>\r\n  );\r\n}\r\n\r\nexport { Button, buttonVariants };\r\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/button.tsx"
    }
  ],
  "type": "registry:ui"
}