{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "Action button built on Radix Slot with seven variants, five sizes, two shapes, an optional loading state, and start/end icon slots.",
  "dependencies": [
    "@radix-ui/react-slot",
    "class-variance-authority",
    "@phosphor-icons/react",
    "clsx",
    "tailwind-merge"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "cn",
    "sanitize-href"
  ],
  "files": [
    {
      "path": "@/components/Button/index.tsx",
      "content": "import { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport type { IconComponent, IconProps } from '@/components/shared/types'\n\nimport { buttonSizeHeight, buttonSizeIcon } from '@/components/shared/buttonSizeClasses'\nimport { buttonVariantColors } from '@/components/shared/buttonVariantColors'\nimport { getBorderRadius } from '@/components/shared/getBorderRadius'\nimport { Spinner } from '@/components/shared/Spinner'\nimport { useVariantDefaults } from '@/components/shared/VariantDefaultsContext'\nimport { cn } from '@/utils/cn'\nimport { sanitizeHref } from '@/utils/sanitizeHref'\n\n// Static padding class mapping - ensures all classes are detected by Tailwind\nconst paddingClassMap = {\n  squircle: {\n    xs: {\n      none: 'px-2.5',\n      start: 'pl-2.5 pr-3', // icon at base: 10px, text +2px: 12px\n      end: 'pl-3 pr-2.5',\n    },\n    sm: {\n      none: 'px-3',\n      start: 'pl-3 pr-3.5', // icon at base: 12px, text +2px: 14px\n      end: 'pl-3.5 pr-3',\n    },\n    md: {\n      none: 'px-3.5',\n      start: 'pl-3.5 pr-4', // icon at base: 14px, text +2px: 16px\n      end: 'pl-4 pr-3.5',\n    },\n    lg: {\n      none: 'px-3.5',\n      start: 'pl-3.5 pr-4',\n      end: 'pl-4 pr-3.5',\n    },\n    xl: {\n      none: 'px-4',\n      start: 'pl-4 pr-5', // icon at base: 16px, text +4px: 20px\n      end: 'pl-5 pr-4',\n    },\n  },\n  capsule: {\n    xs: {\n      none: 'px-2',\n      start: 'pl-2 pr-2.5', // icon at base: 8px, text +2px: 10px\n      end: 'pl-2.5 pr-2',\n    },\n    sm: {\n      none: 'px-3',\n      start: 'pl-3 pr-3.5', // icon at base: 12px, text +2px: 14px\n      end: 'pl-3.5 pr-3',\n    },\n    md: {\n      none: 'px-4',\n      start: 'pl-4 pr-5', // icon at base: 16px, text +4px: 20px\n      end: 'pl-5 pr-4',\n    },\n    lg: {\n      none: 'px-4',\n      start: 'pl-4 pr-5', // icon at base: 16px, text +4px: 20px\n      end: 'pl-5 pr-4',\n    },\n    xl: {\n      none: 'px-5',\n      start: 'pl-5 pr-6', // icon at base: 20px, text +4px: 24px\n      end: 'pl-6 pr-5',\n    },\n  },\n  rounded: {\n    xs: {\n      none: 'px-2.5',\n      start: 'pl-2.5 pr-3',\n      end: 'pl-3 pr-2.5',\n    },\n    sm: {\n      none: 'px-3',\n      start: 'pl-3 pr-3.5',\n      end: 'pl-3.5 pr-3',\n    },\n    md: {\n      none: 'px-3.5',\n      start: 'pl-3.5 pr-4',\n      end: 'pl-4 pr-3.5',\n    },\n    lg: {\n      none: 'px-3.5',\n      start: 'pl-3.5 pr-4',\n      end: 'pl-4 pr-3.5',\n    },\n    xl: {\n      none: 'px-4',\n      start: 'pl-4 pr-5',\n      end: 'pl-5 pr-4',\n    },\n  },\n} as const\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center whitespace-nowrap transition-colors focus-ring disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        ...buttonVariantColors,\n        link: 'bg-transparent text-accent underline-offset-4 hover:underline disabled:text-component-button-disabled-text disabled:no-underline cursor-pointer',\n      },\n      size: {\n        xs: `${buttonSizeHeight.xs} gap-button-gap text-body-sm-medium ${buttonSizeIcon.xs}`,\n        sm: `${buttonSizeHeight.sm} gap-button-gap text-body-sm-medium ${buttonSizeIcon.sm}`,\n        md: `${buttonSizeHeight.md} gap-button-gap text-body-sm-medium ${buttonSizeIcon.md}`,\n        lg: `${buttonSizeHeight.lg} gap-button-gap text-body-base-medium ${buttonSizeIcon.lg}`,\n        xl: `${buttonSizeHeight.xl} gap-button-gap text-body-base-medium ${buttonSizeIcon.xl}`,\n      },\n      shape: {\n        rounded: '',\n        squircle: '',\n        capsule: '',\n      },\n      icon: {\n        none: '',\n        start: '',\n        end: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'primary',\n      size: 'md',\n      shape: 'capsule',\n      icon: 'none',\n    },\n  },\n)\n\ninterface ButtonBaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n  state?: 'default' | 'loading'\n  // Optional `link` prop renders an anchor (`<a>`) with the same styles.\n  // We use the name `link` (instead of `href`) to avoid confusion with native DOM attributes.\n  link?: string\n  /** Anchor target attribute (only applies when `link` is set) */\n  target?: React.AnchorHTMLAttributes<HTMLAnchorElement>['target']\n  /** Anchor rel attribute (only applies when `link` is set) */\n  rel?: string\n  type?: 'button' | 'submit' | 'reset'\n}\n\n/**\n * When icon is 'start' or 'end', iconComponent is required.\n * When icon is 'none' (or omitted), iconComponent and iconProps are optional.\n */\nexport type ButtonProps = ButtonBaseProps &\n  (\n    | { icon?: 'none'; iconComponent?: IconComponent; iconProps?: IconProps }\n    | { icon: 'start' | 'end'; iconComponent: IconComponent; iconProps?: IconProps }\n  )\n\nconst Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size = 'md',\n      shape,\n      icon = 'none',\n      link,\n      target,\n      rel,\n      iconComponent: IconComponent,\n      iconProps,\n      asChild = false,\n      children,\n      state = 'default',\n      type = 'button',\n      ...props\n    },\n    ref,\n  ) => {\n    const safeLink = sanitizeHref(link)\n    const Comp: React.ElementType = asChild ? Slot : safeLink ? 'a' : 'button'\n    const variantDefaults = useVariantDefaults()\n\n    const effectiveVariant = variant ?? variantDefaults?.button ?? 'primary'\n    const effectiveShape = shape ?? variantDefaults?.buttonShape ?? 'capsule'\n\n    // Link variant uses no padding or border radius\n    const isLinkVariant = effectiveVariant === 'link'\n\n    // Dynamically calculate padding and border radius with fallbacks\n    const paddingClasses = isLinkVariant ? '' : paddingClassMap[effectiveShape][size || 'md'][icon]\n    const borderRadiusClasses = isLinkVariant ? '' : getBorderRadius(effectiveShape)\n\n    const isLoading = state === 'loading'\n    const hasIcon = IconComponent && icon !== 'none'\n\n    // Compute icon slot: spinner replaces icon when loading, otherwise render icon\n    const iconSlot = isLoading ? <Spinner /> : hasIcon ? <IconComponent {...iconProps} /> : null\n\n    // Compute content: when loading without icon, keep text invisible for width preservation\n    // When iconSlot is null and not loading, pass children directly to preserve Slot (asChild) merging\n    const content =\n      isLoading && !hasIcon ? (\n        <span className='relative inline-flex items-center justify-center'>\n          <span className='absolute inset-0 flex items-center justify-center'>\n            <Spinner />\n          </span>\n          <span className='invisible'>{children}</span>\n        </span>\n      ) : iconSlot ? (\n        <>\n          {iconSlot}\n          {children}\n        </>\n      ) : (\n        children\n      )\n\n    return (\n      <Comp\n        className={cn(\n          buttonVariants({ variant: effectiveVariant, size, shape: effectiveShape, icon }),\n          !isLinkVariant &&\n            'active:scale-[0.98] motion-reduce:active:scale-100 [&[data-slot$=\"-trigger\"]]:active:scale-100',\n          isLinkVariant && 'h-auto',\n          paddingClasses,\n          borderRadiusClasses,\n          icon === 'end' && 'flex-row-reverse',\n          !isLinkVariant && safeLink && 'cursor-pointer',\n          isLoading && 'pointer-events-none',\n          className,\n        )}\n        ref={ref}\n        {...props}\n        {...(safeLink ? { href: safeLink, target, rel } : {})}\n        {...(!safeLink && !asChild ? { type } : {})}\n        {...(isLoading\n          ? {\n              'aria-busy': true,\n              'aria-disabled': true,\n              onClick: (e: React.MouseEvent) => e.preventDefault(),\n            }\n          : {})}\n      >\n        {content}\n      </Comp>\n    )\n  },\n)\nButton.displayName = 'Button'\n\nexport { Button, buttonVariants }\n",
      "type": "registry:component"
    },
    {
      "path": "@/components/shared/VariantDefaultsContext.tsx",
      "content": "import * as React from 'react'\n\n/**\n * Generic context for containers to set default variants for nested components.\n * This allows any container (like HeaderBar) to specify default styling for\n * buttons and other components without those components needing to know about\n * the specific container.\n */\nexport interface VariantDefaults {\n  // 'link' is intentionally excluded — it changes layout behaviour (h-auto, no padding),\n  // not just colour theme, and should not be set as a site-wide default.\n  button?: 'primary' | 'accent' | 'secondary' | 'outline' | 'ghost' | 'translucent' | 'toolbar'\n  iconButton?: 'primary' | 'accent' | 'secondary' | 'outline' | 'ghost' | 'translucent' | 'toolbar'\n  buttonShape?: 'squircle' | 'capsule'\n  iconButtonShape?: 'squircle' | 'capsule'\n}\n\nconst VariantDefaultsContext = React.createContext<VariantDefaults | null>(null)\n\nexport interface VariantDefaultsProviderProps {\n  value?: VariantDefaults\n  children: React.ReactNode\n}\n\n/**\n * Provider that sets default variants for nested Button and IconButton components.\n * Components can still override these defaults by passing explicit variant props.\n *\n * @example\n * <VariantDefaultsProvider value={{ button: 'toolbar', iconButton: 'toolbar' }}>\n *   <Button>Uses toolbar variant</Button>\n *   <Button variant=\"primary\">Uses primary variant (explicit override)</Button>\n * </VariantDefaultsProvider>\n */\nexport const VariantDefaultsProvider: React.FC<VariantDefaultsProviderProps> = ({ value, children }) => {\n  return <VariantDefaultsContext.Provider value={value ?? null}>{children}</VariantDefaultsContext.Provider>\n}\n\n/**\n * Hook to access variant defaults from the nearest VariantDefaultsProvider.\n * Returns null if not inside a provider.\n */\nexport const useVariantDefaults = (): VariantDefaults | null => {\n  return React.useContext(VariantDefaultsContext)\n}\n",
      "type": "registry:component"
    },
    {
      "path": "@/components/shared/types.ts",
      "content": "import type * as React from 'react'\n\n/**\n * Generic icon component type that works with any icon library.\n * Compatible with phosphor-react, lucide-react, react-icons, and custom SVG components.\n * Library-specific props (e.g. phosphor-react's `weight`) can be passed via `iconProps`.\n */\nexport type IconComponent = React.ComponentType<{\n  size?: number | string\n  className?: string\n}>\n\n/**\n * Props passed to icon components. Extends SVGProps for autocomplete on standard\n * SVG attributes (className, style, etc.) while remaining open to library-specific\n * props (e.g. phosphor-react's `weight`, `mirrored`).\n *\n * Note: The index signature `{ [key: string]: unknown }` intentionally loosens\n * type checking to support cross-library icon props. This is a deliberate tradeoff —\n * typos in known SVG props won't be caught at compile time.\n */\nexport type IconProps = React.SVGProps<SVGSVGElement> & { [key: string]: unknown }\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "button"
  ],
  "type": "registry:ui"
}