{
  "$schema": "https://zuilib.com/r/schema.json",
  "version": "0.1.0",
  "name": "button",
  "type": "component",
  "title": "Button",
  "description": "Primary actions",
  "dependencies": [
    "@headlessui/react@^2.2.0"
  ],
  "registryDependencies": [
    "lib-cn",
    "lib-focus",
    "lib-reset",
    "lib-sizes",
    "lib-spinner",
    "zui-styles"
  ],
  "files": [
    {
      "path": "src/button.tsx",
      "target": "components/zui/button.tsx",
      "content": "'use client'\n\nimport { Button as HeadlessButton, type ButtonProps as HeadlessButtonProps } from '@headlessui/react'\nimport { type ElementType, type MouseEvent, type ReactNode, type Ref, forwardRef } from 'react'\nimport { cn } from './lib/cn'\nimport { nativeControlResetClasses } from './lib/reset'\nimport { focusRingClasses } from './lib/focus'\nimport { type ControlSize } from './lib/sizes'\nimport { Spinner } from './lib/spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'\n/** The shared scale plus `icon`: a square the height of an `md` control. */\nexport type ButtonSize = ControlSize | 'icon'\n\nconst DEFAULT_TAG = 'button'\n\nexport interface ButtonOwnProps {\n  variant?: ButtonVariant\n  size?: ButtonSize\n  fullWidth?: boolean\n  /**\n   * Shows a spinner, sets `aria-busy` / `aria-disabled` and swallows clicks\n   * (and Enter / Space, which are clicks on a button). The button stays\n   * focusable so keyboard focus is not lost when a click flips it to loading.\n   */\n  loading?: boolean\n  /** Text announced with the spinner while `loading` (visually hidden). */\n  loadingText?: string\n  /** Rendered before the children (replaced by the spinner while `loading`). */\n  leadingIcon?: ReactNode\n  /** Rendered after the children. */\n  trailingIcon?: ReactNode\n  /** Merged last, after the variant and size classes. */\n  className?: string\n}\n\n/**\n * Polymorphic: `<Button as=\"a\" href=\"...\">` types `href` from the anchor's\n * props, `<Button as={Link} to=\"...\">` from the component's.\n */\nexport type ButtonProps<TTag extends ElementType = typeof DEFAULT_TAG> = Omit<\n  HeadlessButtonProps<TTag>,\n  keyof ButtonOwnProps\n> &\n  ButtonOwnProps\n\nconst variantClasses: Record<ButtonVariant, string> = {\n  primary: 'bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80',\n  secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70',\n  destructive:\n    'bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80',\n  outline:\n    'border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-accent/90',\n  ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-accent/90',\n  link: 'text-primary underline-offset-4 hover:underline active:text-primary/80',\n}\n\nconst sizeClasses: Record<ButtonSize, string> = {\n  sm: 'h-(--control-height-sm) px-(--button-padding-x-sm) text-sm',\n  md: 'h-(--control-height-md) px-(--button-padding-x-md) text-base',\n  lg: 'h-(--control-height-lg) px-(--button-padding-x-lg) text-lg',\n  /** Square box the height of an `md` control, for a lone icon. */\n  icon: 'size-(--control-height-md) p-0 text-base',\n}\n\n/** Link buttons are inline text: no control box, no horizontal padding. */\nconst linkSizeClasses: Record<ButtonSize, string> = {\n  sm: 'h-auto px-0 text-sm',\n  md: 'h-auto px-0 text-base',\n  lg: 'h-auto px-0 text-lg',\n  icon: 'h-auto p-0 text-base',\n}\n\nfunction swallowClick(event: MouseEvent) {\n  event.preventDefault()\n  event.stopPropagation()\n}\n\nfunction ButtonFn(\n  {\n    variant = 'primary',\n    size = 'md',\n    fullWidth = false,\n    loading = false,\n    loadingText = 'Loading',\n    leadingIcon,\n    trailingIcon,\n    disabled = false,\n    className,\n    children,\n    onClick,\n    ...props\n  }: ButtonProps,\n  ref: Ref<HTMLElement>\n) {\n  const isNativeButton = props.as === undefined || props.as === DEFAULT_TAG\n  /**\n   * `disabled` is only a real attribute on <button>. A disabled anchor or\n   * router Link gets `aria-disabled` and leaves the tab order instead; a\n   * loading button of any tag gets `aria-disabled` and keeps its focus.\n   */\n  const nativeDisabled = isNativeButton && disabled\n  const ariaDisabled = (disabled && !isNativeButton) || (loading && !disabled)\n  const sizes = variant === 'link' ? linkSizeClasses : sizeClasses\n  /** An icon-only button has no room for both the spinner and the icon, so\n   *  the children go visually hidden; they still name the button. */\n  const hideContent = size === 'icon' && loading\n\n  const content =\n    typeof children === 'function' ? (\n      children\n    ) : (\n      <>\n        {loading ? (\n          <>\n            <Spinner data-slot=\"button-spinner\" />\n            <span className=\"sr-only\">{loadingText}</span>\n          </>\n        ) : leadingIcon ? (\n          <span data-slot=\"button-leading-icon\" className=\"inline-flex shrink-0\" aria-hidden=\"true\">\n            {leadingIcon}\n          </span>\n        ) : null}\n        {hideContent ? (\n          <span data-slot=\"button-content\" className=\"sr-only\">\n            {children}\n          </span>\n        ) : (\n          children\n        )}\n        {trailingIcon && !hideContent ? (\n          <span data-slot=\"button-trailing-icon\" className=\"inline-flex shrink-0\" aria-hidden=\"true\">\n            {trailingIcon}\n          </span>\n        ) : null}\n      </>\n    )\n\n  const shared = {\n    ref,\n    'data-slot': 'button',\n    'data-variant': variant,\n    'data-size': size,\n    'data-loading': loading || undefined,\n    'aria-busy': loading || undefined,\n    'aria-disabled': ariaDisabled || undefined,\n    className: cn(\n      nativeControlResetClasses,\n      'cursor-pointer inline-flex items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap shrink-0',\n      'transition-colors duration-(--duration-fast)',\n      focusRingClasses,\n      'disabled:pointer-events-none disabled:opacity-50',\n      'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      'aria-disabled:pointer-events-none aria-disabled:opacity-50',\n      \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[1em]\",\n      variantClasses[variant],\n      sizes[size],\n      fullWidth && 'w-full',\n      className\n    ),\n    onClick: loading || disabled ? swallowClick : onClick,\n  }\n\n  if (!isNativeButton) {\n    /**\n     * Headless UI's Button hard-codes `type=\"button\"` and forwards `disabled`\n     * to whatever tag it renders, which is invalid on an anchor or a router\n     * Link, so anything that is not a <button> is rendered directly.\n     */\n    const { as, tabIndex, ...rest } = props as Record<string, unknown>\n    const Tag = as as ElementType\n    /** Render-prop bag for a function child; a plain tag has no hover/press tracking. */\n    const bag = { disabled, hover: false, focus: false, active: false, autofocus: false }\n    return (\n      <Tag {...shared} {...rest} tabIndex={disabled ? -1 : (tabIndex as number | undefined)}>\n        {typeof content === 'function' ? content(bag) : content}\n      </Tag>\n    )\n  }\n\n  return (\n    <HeadlessButton {...shared} {...props} disabled={nativeDisabled || undefined}>\n      {content}\n    </HeadlessButton>\n  )\n}\n\nexport interface ButtonComponent {\n  <TTag extends ElementType = typeof DEFAULT_TAG>(\n    props: ButtonProps<TTag> & { ref?: Ref<HTMLElement> }\n  ): React.JSX.Element\n  displayName: string\n}\n\nconst Button = forwardRef(ButtonFn) as unknown as ButtonComponent\n\nButton.displayName = 'Button'\n\nexport default Button\n",
      "sha256": "97b1d5273664a15ff83c1d1fd77281e91989e537eaf578f54725850b2920a552"
    }
  ],
  "docs": "import Button from '@/components/zui/button'",
  "meta": {
    "subpath": "@zuilib/components/button",
    "exportName": "Button"
  }
}
