{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "Token-backed native Button component for Dethink Components.",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/button/button.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/button/button.tsx",
      "content": "import {\n  Children,\n  cloneElement,\n  forwardRef,\n  isValidElement,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  type ButtonHTMLAttributes,\n  type ForwardedRef,\n  type MouseEventHandler,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport type ButtonVariant =\n  \"solid\" | \"soft\" | \"outline\" | \"ghost\" | \"link\" | \"destructive\";\n\nexport type ButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"icon\";\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  \"data-slot\"?: string;\n  asChild?: boolean;\n  leftIcon?: ReactNode;\n  loadingIndicator?: ReactNode;\n  rightIcon?: ReactNode;\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  loading?: boolean;\n}\n\nconst buttonBaseClasses =\n  \"relative inline-flex shrink-0 items-center justify-center gap-density-gap whitespace-nowrap rounded-md border border-transparent font-medium motion-safe:transition-[color,background-color,border-color,opacity,scale] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control motion-safe:[&:not(:disabled):not([data-disabled=true]):active]:scale-[var(--dt-control-press-scale,0.98)] motion-safe:active:duration-[var(--dt-motion-press)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[loading=true]:cursor-wait data-[loading=true]:opacity-80\";\n\nconst buttonVariantClasses: Record<ButtonVariant, string> = {\n  solid:\n    \"bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80\",\n  soft: \"bg-primary/10 text-foreground hover:bg-primary/15 active:bg-primary/20\",\n  outline:\n    \"border-border bg-background text-foreground hover:bg-muted active:bg-muted/80\",\n  ghost: \"bg-transparent text-foreground hover:bg-muted active:bg-muted/80\",\n  link: \"border-transparent bg-transparent text-primary underline-offset-4 hover:underline active:text-primary/80\",\n  destructive:\n    \"bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80\",\n};\n\nconst buttonSizeClasses: Record<ButtonSize, string> = {\n  xs: \"h-7 px-[var(--dt-space-2)] text-xs\",\n  sm: \"h-8 px-[var(--dt-space-3)] text-sm\",\n  md: \"h-density-control px-[var(--dt-space-4)] text-sm\",\n  lg: \"h-11 px-[var(--dt-space-5)] text-base\",\n  xl: \"h-12 px-[var(--dt-space-6)] text-base\",\n  icon: \"h-density-control w-density-control p-0 text-sm\",\n};\n\nconst buttonIconClasses =\n  \"pointer-events-none inline-flex size-4 shrink-0 items-center justify-center empty:hidden [&>svg]:size-4\";\n\nconst buttonSpinnerClasses =\n  \"pointer-events-none size-4 shrink-0 rounded-full border-2 border-current border-r-transparent animate-spin motion-reduce:animate-none\";\n\nconst buttonLoadingIndicatorClasses =\n  \"pointer-events-none inline-flex size-4 shrink-0 items-center justify-center\";\n\ntype ButtonSlotProps = Record<string, unknown> & {\n  children?: ReactNode;\n  className?: string;\n  onClick?: MouseEventHandler<HTMLElement>;\n  ref?: Ref<HTMLElement>;\n};\n\nexport function buttonClassNames({\n  variant = \"solid\",\n  size = \"md\",\n  className,\n}: Pick<ButtonProps, \"variant\" | \"size\" | \"className\"> = {}) {\n  return cn(\n    buttonBaseClasses,\n    buttonSizeClasses[size],\n    buttonVariantClasses[variant],\n    className,\n  );\n}\n\nfunction setRef<T>(ref: Ref<T> | undefined, node: T | null) {\n  if (typeof ref === \"function\") {\n    ref(node);\n    return;\n  }\n\n  if (ref) {\n    (ref as { current: T | null }).current = node;\n  }\n}\n\nfunction composeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      setRef(ref, node);\n    }\n  };\n}\n\nfunction composeClickHandlers(\n  componentHandler: MouseEventHandler<HTMLElement>,\n  childHandler: MouseEventHandler<HTMLElement> | undefined,\n) {\n  return (event: React.MouseEvent<HTMLElement>) => {\n    componentHandler(event);\n\n    if (!event.defaultPrevented) {\n      childHandler?.(event);\n    }\n  };\n}\n\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nfunction ButtonContent({\n  children,\n  leftIcon,\n  loading,\n  loadingIndicator,\n  rightIcon,\n}: Pick<\n  ButtonProps,\n  \"children\" | \"leftIcon\" | \"loading\" | \"loadingIndicator\" | \"rightIcon\"\n>) {\n  const contentRef = useRef<HTMLSpanElement>(null);\n\n  useIsomorphicLayoutEffect(() => {\n    const content = contentRef.current;\n    if (!content || loading) return;\n\n    // Measure only at rest. Parent press transforms must not affect the size\n    // reserved while a consumer swaps the label during an async action.\n    const measure = () => {\n      const width = content.offsetWidth;\n      if (width)\n        content.style.setProperty(\"--dt-button-idle-width\", `${width}px`);\n    };\n    measure();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(content);\n    return () => observer.disconnect();\n  }, [loading]);\n\n  const overlaySpinner = loading && !leftIcon;\n  return (\n    <span\n      ref={contentRef}\n      data-slot=\"button-content\"\n      className=\"gap-density-gap relative inline-flex max-w-full min-w-0 items-center justify-center\"\n      style={{\n        inlineSize: loading ? \"var(--dt-button-idle-width, auto)\" : undefined,\n      }}\n    >\n      {loading ? (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none inline-flex shrink-0 items-center justify-center motion-safe:animate-[dt-scrim-in_120ms_ease-out]\",\n            overlaySpinner && \"absolute inset-0\",\n          )}\n        >\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"button-spinner\"\n            className={\n              loadingIndicator\n                ? buttonLoadingIndicatorClasses\n                : buttonSpinnerClasses\n            }\n          >\n            {loadingIndicator}\n          </span>\n        </span>\n      ) : leftIcon ? (\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"button-left-icon\"\n          className={buttonIconClasses}\n        >\n          {leftIcon}\n        </span>\n      ) : null}\n      <span\n        data-slot=\"button-label\"\n        className={cn(\n          \"min-w-0 truncate motion-safe:transition-opacity motion-safe:duration-[var(--dt-motion-fast)]\",\n          overlaySpinner && \"opacity-0\",\n        )}\n      >\n        {children}\n      </span>\n      {rightIcon ? (\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"button-right-icon\"\n          className={cn(buttonIconClasses, overlaySpinner && \"opacity-0\")}\n        >\n          {rightIcon}\n        </span>\n      ) : null}\n    </span>\n  );\n}\n\nfunction getChildRef(child: ReactElement<ButtonSlotProps>) {\n  return child.props.ref;\n}\n\nexport const Button = forwardRef<HTMLElement, ButtonProps>(\n  (\n    {\n      \"data-slot\": dataSlot = \"button\",\n      \"aria-busy\": ariaBusy,\n      asChild = false,\n      children,\n      className,\n      disabled = false,\n      leftIcon,\n      loading = false,\n      loadingIndicator,\n      onClick,\n      rightIcon,\n      type = \"button\",\n      variant = \"solid\",\n      size = \"md\",\n      ...props\n    },\n    ref,\n  ) => {\n    const isDisabled = disabled || loading;\n    const handleClick: MouseEventHandler<HTMLElement> = (event) => {\n      if (isDisabled) {\n        event.preventDefault();\n        return;\n      }\n\n      onClick?.(event as React.MouseEvent<HTMLButtonElement>);\n    };\n    const classes = buttonClassNames({ variant, size, className });\n\n    if (asChild) {\n      const child = Children.only(children);\n\n      if (!isValidElement<ButtonSlotProps>(child)) {\n        throw new Error(\n          \"Button with asChild expects a single React element child.\",\n        );\n      }\n\n      return cloneElement(\n        child,\n        // eslint-disable-next-line react-hooks/refs -- React forwards this ref during commit; createElement/cloneElement does not read ref.current.\n        {\n          ...props,\n          ...child.props,\n          ref: (node) => {\n            composeRefs(ref, getChildRef(child))(node);\n          },\n          \"aria-busy\": loading ? true : ariaBusy,\n          \"aria-disabled\": isDisabled ? true : child.props[\"aria-disabled\"],\n          \"data-slot\": dataSlot,\n          \"data-variant\": variant,\n          \"data-size\": size,\n          \"data-disabled\": isDisabled ? \"true\" : undefined,\n          \"data-loading\": loading ? \"true\" : undefined,\n          className: cn(classes, child.props.className),\n          onClick: composeClickHandlers(handleClick, child.props.onClick),\n        },\n        <ButtonContent\n          leftIcon={leftIcon}\n          loading={loading}\n          loadingIndicator={loadingIndicator}\n          rightIcon={rightIcon}\n        >\n          {child.props.children}\n        </ButtonContent>,\n      );\n    }\n\n    return (\n      <button\n        {...props}\n        ref={ref as ForwardedRef<HTMLButtonElement>}\n        type={type}\n        disabled={isDisabled}\n        aria-busy={loading ? true : ariaBusy}\n        data-slot={dataSlot}\n        data-variant={variant}\n        data-size={size}\n        data-disabled={isDisabled ? \"true\" : undefined}\n        data-loading={loading ? \"true\" : undefined}\n        className={classes}\n        onClick={handleClick as MouseEventHandler<HTMLButtonElement>}\n      >\n        <ButtonContent\n          leftIcon={leftIcon}\n          loading={loading}\n          loadingIndicator={loadingIndicator}\n          rightIcon={rightIcon}\n        >\n          {children}\n        </ButtonContent>\n      </button>\n    );\n  },\n);\n\nButton.displayName = \"Button\";\n"
    },
    {
      "path": "packages/components/src/components/button/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/button/index.ts",
      "content": "export {\n  Button,\n  buttonClassNames,\n  type ButtonProps,\n  type ButtonSize,\n  type ButtonVariant,\n} from \"./button\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
