{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "b-button",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority",
    "lucide-react",
    "motion"
  ],
  "devDependencies": [],
  "files": [
    {
      "path": "b-button.tsx",
      "content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type AnchorHTMLAttributes,\n  type ButtonHTMLAttributes,\n  cloneElement,\n  type FocusEventHandler,\n  forwardRef,\n  isValidElement,\n  type KeyboardEvent,\n  type KeyboardEventHandler,\n  memo,\n  type PointerEvent,\n  type PointerEventHandler,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst buttonThemeClassName =\n  \"[--button-destructive:#dc2626] [--button-input:#e3e7ec] [--button-primary-foreground:#ffffff] [--button-ring:rgba(17,17,17,0.16)] [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] [--button-secondary:#eceff3] [--button-secondary-foreground:#111111] dark:[--button-destructive:#f87171] dark:[--button-input:#2b2a25] dark:[--button-primary-foreground:#111111] dark:[--button-ring:rgba(246,243,236,0.18)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_30%)] dark:[--button-secondary:#2a2a27] dark:[--button-secondary-foreground:#f6f3ec]\";\n\nconst buttonCornerClassName =\n  \"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]\";\n\nconst buttonCornerXsClassName =\n  \"rounded-[min(var(--radius-md),10px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]\";\n\nconst buttonCornerSmClassName =\n  \"rounded-[min(var(--radius-md),12px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[14px]\";\n\nconst buttonGroupCornerClassName =\n  \"in-data-[slot=button-group]:rounded-lg in-data-[slot=button-group]:supports-[corner-shape:squircle]:corner-squircle in-data-[slot=button-group]:supports-[corner-shape:squircle]:rounded-[11px]\";\n\nconst buttonVariants = cva(\n  cn(\n    componentThemeClassName,\n    buttonThemeClassName,\n    buttonCornerClassName,\n    \"group/button relative isolate inline-flex shrink-0 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden whitespace-nowrap border border-transparent bg-clip-border font-medium text-sm no-underline outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:border-[color:var(--button-ring)] focus-visible:ring-3 focus-visible:ring-[color:color-mix(in_oklch,var(--button-ring),transparent_50%)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-[color:var(--button-destructive)] aria-invalid:ring-3 aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:aria-invalid:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_50%)] dark:aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n  ),\n  {\n    variants: {\n      variant: {\n        default:\n          \"visited:!text-primary-foreground hover:!text-primary-foreground focus-visible:!text-primary-foreground active:!text-primary-foreground bg-primary text-primary-foreground [--button-ripple:color-mix(in_oklch,var(--primary-foreground),transparent_45%)] hover:bg-primary dark:[--button-ripple:color-mix(in_oklch,#111111,transparent_10%)] [&_svg]:text-primary-foreground\",\n        outline:\n          \"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground border-border bg-background text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:border-[color:var(--button-input)] dark:bg-[color-mix(in_oklch,var(--button-input),transparent_70%)] dark:hover:bg-[color-mix(in_oklch,var(--button-input),transparent_50%)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground\",\n        secondary:\n          \"visited:!text-[color:var(--button-secondary-foreground)] hover:!text-[color:var(--button-secondary-foreground)] focus-visible:!text-[color:var(--button-secondary-foreground)] active:!text-[color:var(--button-secondary-foreground)] aria-expanded:!text-[color:var(--button-secondary-foreground)] bg-[color:var(--button-secondary)] text-[color:var(--button-secondary-foreground)] [--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_52%)] hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_7%)] aria-expanded:bg-[color:var(--button-secondary)] dark:hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_11%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_20%)] [&_svg]:text-[color:var(--button-secondary-foreground)]\",\n        ghost:\n          \"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:hover:bg-muted/50 dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground\",\n        destructive:\n          \"visited:!text-[color:var(--button-destructive)] hover:!text-[color:var(--button-destructive)] focus-visible:!text-[color:var(--button-destructive)] active:!text-[color:var(--button-destructive)] bg-[color-mix(in_oklch,var(--button-destructive),transparent_90%)] text-[color:var(--button-destructive)] [--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_40%)] hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] focus-visible:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] dark:hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_70%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_20%)] [&_svg]:text-[color:var(--button-destructive)]\",\n        link: \"\",\n      },\n      linkUnderline: {\n        motion: \"\",\n        static: \"\",\n      },\n      size: {\n        default:\n          \"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n        xs: cn(\n          \"h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n          buttonGroupCornerClassName,\n          buttonCornerXsClassName\n        ),\n        sm: cn(\n          \"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n          buttonGroupCornerClassName,\n          buttonCornerSmClassName\n        ),\n        lg: \"h-10 gap-1.5 px-4 text-base leading-5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5\",\n        icon: \"size-8\",\n        \"icon-xs\": cn(\n          \"size-6 [&_svg:not([class*='size-'])]:size-3\",\n          buttonGroupCornerClassName,\n          buttonCornerXsClassName\n        ),\n        \"icon-sm\": cn(\n          \"size-7\",\n          buttonGroupCornerClassName,\n          buttonCornerSmClassName\n        ),\n        \"icon-lg\": \"size-9\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"link\",\n        linkUnderline: \"motion\",\n        class:\n          \"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground shadow-none active:translate-y-0 [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]\",\n      },\n      {\n        variant: \"link\",\n        linkUnderline: \"static\",\n        class:\n          \"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground underline-offset-4 hover:underline [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]\",\n      },\n      {\n        variant: \"link\",\n        size: \"default\",\n        class: \"h-8 px-0 text-sm leading-5\",\n      },\n      {\n        variant: \"link\",\n        size: \"lg\",\n        class: \"h-10 px-0 text-base leading-5\",\n      },\n      {\n        variant: \"link\",\n        size: \"sm\",\n        class: \"h-7 px-0 text-[0.8rem]\",\n      },\n      {\n        variant: \"link\",\n        size: \"xs\",\n        class: \"h-6 px-0 text-xs\",\n      },\n    ],\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n      linkUnderline: \"motion\",\n    },\n  }\n);\n\nconst linkUnderlineFillTransition = {\n  type: \"spring\" as const,\n  stiffness: 420,\n  damping: 36,\n  mass: 0.65,\n};\n\nconst MAX_RIPPLES = 3;\nconst REL_TOKEN_SPLIT = /\\s+/;\n\nconst LinkUnderlineLabel = memo(function LinkUnderlineLabel({\n  children,\n  enabled,\n}: {\n  children: ReactNode;\n  enabled: boolean;\n}) {\n  if (!enabled) {\n    return <>{children}</>;\n  }\n\n  return (\n    <span className=\"relative inline-block pb-px font-medium text-[1em] leading-[inherit]\">\n      <span className=\"relative z-10\" data-link-label>\n        {children}\n      </span>\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px bg-muted-foreground/50\"\n      />\n      <motion.span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px origin-left bg-foreground\"\n        transition={linkUnderlineFillTransition}\n        variants={{\n          rest: { scaleX: 0 },\n          hover: { scaleX: 1 },\n        }}\n      />\n    </span>\n  );\n});\n\nconst buttonContentClassName =\n  \"relative z-10 inline-flex items-center justify-center [gap:inherit] whitespace-nowrap text-inherit\";\n\ntype Ripple = { id: string; x: number; y: number; size: number };\n\ntype AnchorHTMLAttributesForMotion = Omit<\n  AnchorHTMLAttributes<HTMLAnchorElement>,\n  | \"onAnimationEnd\"\n  | \"onAnimationIteration\"\n  | \"onAnimationStart\"\n  | \"onDrag\"\n  | \"onDragEnd\"\n  | \"onDragEnter\"\n  | \"onDragExit\"\n  | \"onDragLeave\"\n  | \"onDragOver\"\n  | \"onDragStart\"\n  | \"onDrop\"\n>;\n\ntype ButtonHTMLAttributesForMotion = Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  | \"onAnimationEnd\"\n  | \"onAnimationIteration\"\n  | \"onAnimationStart\"\n  | \"onDrag\"\n  | \"onDragEnd\"\n  | \"onDragEnter\"\n  | \"onDragExit\"\n  | \"onDragLeave\"\n  | \"onDragOver\"\n  | \"onDragStart\"\n  | \"onDrop\"\n>;\n\ntype ButtonRenderProps = React.HTMLAttributes<HTMLButtonElement> & {\n  className?: string;\n  ref?: React.Ref<HTMLButtonElement>;\n  style?: React.CSSProperties;\n};\n\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n\n  if (ref) {\n    (ref as React.MutableRefObject<T>).current = value;\n  }\n}\n\nfunction useMeasure<T extends HTMLElement = HTMLElement>(\n  enabled: boolean\n): [(node: T | null) => void, { width: number; height: number }] {\n  const [element, setElement] = useState<T | null>(null);\n  const [bounds, setBounds] = useState({ width: 0, height: 0 });\n\n  const ref = useCallback((node: T | null) => {\n    setElement(node);\n  }, []);\n\n  useEffect(() => {\n    if (!(enabled && element && typeof ResizeObserver !== \"undefined\")) {\n      return;\n    }\n\n    const observer = new ResizeObserver(([entry]) => {\n      setBounds((current) => {\n        const next = {\n          width: entry.contentRect.width,\n          height: entry.contentRect.height,\n        };\n\n        if (current.width === next.width && current.height === next.height) {\n          return current;\n        }\n\n        return next;\n      });\n    });\n\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [element, enabled]);\n\n  return [ref, enabled ? bounds : { width: 0, height: 0 }];\n}\n\nconst MotionAnchor = motion.create(\"a\");\n\nconst buttonTransition = {\n  scale: {\n    type: \"spring\",\n    stiffness: 640,\n    damping: 38,\n    mass: 0.85,\n  },\n  width: {\n    type: \"spring\",\n    stiffness: 420,\n    damping: 34,\n    mass: 0.9,\n  },\n  opacity: {\n    type: \"spring\",\n    stiffness: 380,\n    damping: 32,\n    mass: 0.8,\n  },\n  y: { type: \"spring\", stiffness: 340, damping: 28, mass: 0.8 },\n} as const;\n\nconst loadingIconTransition = {\n  type: \"spring\" as const,\n  stiffness: 400,\n  damping: 30,\n  mass: 0.76,\n};\n\nconst loadingLabelExitTransition = {\n  type: \"spring\" as const,\n  stiffness: 420,\n  damping: 38,\n  mass: 0.84,\n};\n\nconst loadingLabelEnterTransition = {\n  type: \"spring\" as const,\n  stiffness: 280,\n  damping: 30,\n  mass: 0.98,\n};\n\nconst loadingLayoutTransition = {\n  type: \"spring\" as const,\n  stiffness: 360,\n  damping: 36,\n  mass: 0.94,\n};\n\nconst loadingLabelVariants = {\n  animate: {\n    filter: \"blur(0px)\",\n    opacity: 1,\n    scale: 1,\n    transition: loadingLabelEnterTransition,\n    y: 0,\n  },\n  exit: {\n    filter: \"blur(3px)\",\n    opacity: 0,\n    scale: 0.96,\n    transition: loadingLabelExitTransition,\n    y: -4,\n  },\n  initial: {\n    filter: \"blur(3px)\",\n    opacity: 0,\n    scale: 0.96,\n    y: 4,\n  },\n} as const;\n\nfunction getLabelMotionKey(children: ReactNode, loading: boolean) {\n  if (typeof children === \"string\" || typeof children === \"number\") {\n    return `${loading ? \"busy\" : \"idle\"}:${children}`;\n  }\n\n  return loading ? \"busy\" : \"idle\";\n}\n\nfunction withLoadingSpin(node: ReactNode, spinning: boolean) {\n  if (!(spinning && isValidElement<{ className?: string }>(node))) {\n    return node;\n  }\n\n  return cloneElement(node, {\n    className: cn(node.props.className, \"animate-spin\"),\n  });\n}\n\nfunction getButtonAnimate(\n  canAnimate: boolean,\n  isPressed: boolean,\n  animatedWidth: number | undefined,\n  reduceMotion: boolean | null\n) {\n  const shouldReduceMotion = reduceMotion === true;\n  const pressScale = canAnimate && isPressed && !shouldReduceMotion ? 0.96 : 1;\n\n  if (animatedWidth) {\n    return {\n      scale: pressScale,\n      width: animatedWidth,\n    };\n  }\n\n  return {\n    scale: pressScale,\n  };\n}\n\nfunction omitLinkMotionConflicts<T extends Record<string, unknown>>(props: T) {\n  const {\n    onAnimationEnd: _onAnimationEnd,\n    onAnimationIteration: _onAnimationIteration,\n    onAnimationStart: _onAnimationStart,\n    onDrag: _onDrag,\n    onDragEnd: _onDragEnd,\n    onDragEnter: _onDragEnter,\n    onDragExit: _onDragExit,\n    onDragLeave: _onDragLeave,\n    onDragOver: _onDragOver,\n    onDragStart: _onDragStart,\n    onDrop: _onDrop,\n    ...rest\n  } = props;\n\n  return rest;\n}\n\nfunction resolvePrimitiveButtonProps(buttonProps: ButtonRenderProps) {\n  const {\n    children: _buttonChildren,\n    className: primitiveClassName,\n    onAnimationEnd: _onAnimationEnd,\n    onAnimationIteration: _onAnimationIteration,\n    onAnimationStart: _onAnimationStart,\n    onDrag: _onDrag,\n    onDragEnd: _onDragEnd,\n    onDragEnter: _onDragEnter,\n    onDragExit: _onDragExit,\n    onDragLeave: _onDragLeave,\n    onDragOver: _onDragOver,\n    onDragStart: _onDragStart,\n    onDrop: _onDrop,\n    ref: primitiveRef,\n    style: primitiveStyle,\n    ...resolvedButtonProps\n  } = buttonProps;\n\n  return {\n    primitiveClassName,\n    primitiveRef,\n    primitiveStyle,\n    resolvedButtonProps,\n  };\n}\n\nfunction resolveAnchorRel(target?: string, rel?: string) {\n  if (!target?.includes(\"_blank\")) {\n    return rel;\n  }\n\n  const tokens = new Set((rel ?? \"\").split(REL_TOKEN_SPLIT).filter(Boolean));\n  tokens.add(\"noopener\");\n  tokens.add(\"noreferrer\");\n  return [...tokens].join(\" \");\n}\n\nfunction isIconOnlySize(\n  size:\n    | \"default\"\n    | \"xs\"\n    | \"sm\"\n    | \"lg\"\n    | \"icon\"\n    | \"icon-xs\"\n    | \"icon-sm\"\n    | \"icon-lg\"\n    | null\n    | undefined\n) {\n  return (\n    size === \"icon\" ||\n    size === \"icon-xs\" ||\n    size === \"icon-sm\" ||\n    size === \"icon-lg\"\n  );\n}\n\nfunction useIconOnlyA11yWarning(\n  size:\n    | \"default\"\n    | \"xs\"\n    | \"sm\"\n    | \"lg\"\n    | \"icon\"\n    | \"icon-xs\"\n    | \"icon-sm\"\n    | \"icon-lg\"\n    | null\n    | undefined,\n  children: ReactNode,\n  iconLabel: string | undefined,\n  ariaLabel: string | undefined,\n  loading: boolean\n) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\") {\n      return;\n    }\n\n    const hasVisibleLabel =\n      children != null && children !== false && children !== \"\";\n    const needsAccessibleName =\n      isIconOnlySize(size) || (loading && !hasVisibleLabel);\n\n    if (!needsAccessibleName || iconLabel || ariaLabel) {\n      return;\n    }\n\n    console.warn(\n      \"[Button] Icon-only and loading-only buttons need `iconLabel` or `aria-label` for accessibility.\"\n    );\n  }, [ariaLabel, children, iconLabel, loading, size]);\n}\n\nfunction getIconIdleAnimate(showSpinner: boolean, noMotion: boolean) {\n  if (noMotion) {\n    return { opacity: showSpinner ? 0 : 1 };\n  }\n\n  return {\n    filter: showSpinner ? \"blur(3px)\" : \"blur(0px)\",\n    opacity: showSpinner ? 0 : 1,\n    rotate: showSpinner ? -56 : 0,\n    scale: showSpinner ? 0.82 : 1,\n  };\n}\n\nfunction getIconSpinnerAnimate(showSpinner: boolean, noMotion: boolean) {\n  if (noMotion) {\n    return { opacity: showSpinner ? 1 : 0 };\n  }\n\n  return {\n    filter: showSpinner ? \"blur(0px)\" : \"blur(3px)\",\n    opacity: showSpinner ? 1 : 0,\n    rotate: showSpinner ? 0 : 56,\n    scale: showSpinner ? 1 : 0.82,\n  };\n}\n\nfunction buildMotionSurfaceProps({\n  canAnimate,\n  isPressed,\n  motionAnimate,\n  reduceMotion,\n  useLinkMotionUnderline,\n  variant,\n}: {\n  canAnimate: boolean;\n  isPressed: boolean;\n  motionAnimate: ReturnType<typeof getButtonAnimate>;\n  reduceMotion: boolean | null;\n  useLinkMotionUnderline: boolean;\n  variant: VariantProps<typeof buttonVariants>[\"variant\"];\n}) {\n  const shouldReduceMotion = reduceMotion === true;\n  const surfaceTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : buttonTransition;\n  const shared = {\n    animate: motionAnimate,\n    \"data-pressed\": isPressed ? \"true\" : \"false\",\n    transition: surfaceTransition,\n  };\n\n  if (useLinkMotionUnderline) {\n    return {\n      ...shared,\n      initial: shouldReduceMotion ? (false as const) : (\"rest\" as const),\n      variants: shouldReduceMotion ? undefined : { rest: {}, hover: {} },\n      whileHover: shouldReduceMotion ? undefined : (\"hover\" as const),\n    };\n  }\n\n  const hoverLift =\n    canAnimate && variant !== \"link\" && !shouldReduceMotion\n      ? { y: -1 }\n      : undefined;\n\n  return {\n    ...shared,\n    initial: false as const,\n    whileHover: hoverLift,\n  };\n}\n\nconst ButtonIconSlot = memo(function ButtonIconSlot({\n  accessibleName,\n  dataIcon,\n  hasIcon,\n  hasLabel,\n  icon,\n  iconTransition,\n  layoutTransition,\n  noMotion,\n  showSpinner,\n  spinner,\n}: {\n  accessibleName?: string;\n  dataIcon: \"inline-start\" | \"inline-end\";\n  hasIcon: boolean;\n  hasLabel: boolean;\n  icon?: ReactNode;\n  iconTransition: { duration: number } | typeof loadingIconTransition;\n  layoutTransition: { duration: number } | typeof loadingLayoutTransition;\n  noMotion: boolean;\n  showSpinner: boolean;\n  spinner: ReactNode;\n}) {\n  return (\n    <motion.span\n      aria-hidden={hasLabel || accessibleName ? true : undefined}\n      className=\"relative inline-flex size-[1em] shrink-0 items-center justify-center\"\n      data-icon={dataIcon}\n      layout={noMotion ? false : \"position\"}\n      transition={layoutTransition}\n    >\n      {hasIcon ? (\n        <motion.span\n          animate={getIconIdleAnimate(showSpinner, noMotion)}\n          className=\"absolute inset-0 inline-flex items-center justify-center\"\n          initial={false}\n          transition={iconTransition}\n        >\n          {icon}\n        </motion.span>\n      ) : null}\n      <motion.span\n        animate={getIconSpinnerAnimate(showSpinner, noMotion)}\n        className=\"absolute inset-0 inline-flex items-center justify-center\"\n        initial={false}\n        transition={iconTransition}\n      >\n        {withLoadingSpin(spinner, showSpinner && !noMotion)}\n      </motion.span>\n    </motion.span>\n  );\n});\n\nconst ButtonLoadingLabel = memo(function ButtonLoadingLabel({\n  children,\n  layoutTransition,\n  linkUnderlineEnabled,\n  loading,\n  noMotion,\n}: {\n  children: ReactNode;\n  layoutTransition: { duration: number } | typeof loadingLayoutTransition;\n  linkUnderlineEnabled: boolean;\n  loading: boolean;\n  noMotion: boolean;\n}) {\n  return (\n    <motion.span\n      className=\"relative inline-grid min-w-0 items-center leading-[inherit] [grid-template-columns:max-content] [grid-template-rows:minmax(0,1.25em)]\"\n      layout=\"position\"\n      transition={layoutTransition}\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={noMotion ? { opacity: 1 } : \"animate\"}\n          className=\"col-start-1 row-start-1 inline-flex min-w-0 items-center whitespace-nowrap\"\n          exit={noMotion ? { opacity: 0 } : \"exit\"}\n          initial={noMotion ? false : \"initial\"}\n          key={getLabelMotionKey(children, loading)}\n          layout=\"position\"\n          transition={{ layout: layoutTransition }}\n          variants={noMotion ? undefined : loadingLabelVariants}\n        >\n          <LinkUnderlineLabel enabled={linkUnderlineEnabled}>\n            {children}\n          </LinkUnderlineLabel>\n        </motion.span>\n      </AnimatePresence>\n    </motion.span>\n  );\n});\n\nconst ButtonSpinnerPresence = memo(function ButtonSpinnerPresence({\n  iconSlot,\n  layoutTransition,\n  noMotion,\n  visible,\n}: {\n  iconSlot: ReactNode;\n  layoutTransition: { duration: number } | typeof loadingLayoutTransition;\n  noMotion: boolean;\n  visible: boolean;\n}) {\n  return (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      {visible ? (\n        <motion.span\n          animate={noMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n          className=\"inline-flex shrink-0\"\n          exit={\n            noMotion\n              ? { opacity: 0 }\n              : {\n                  filter: \"blur(2px)\",\n                  opacity: 0,\n                  scale: 0.86,\n                  transition: loadingLabelExitTransition,\n                }\n          }\n          initial={noMotion ? false : { opacity: 0, scale: 0.86 }}\n          key=\"loading-spinner\"\n          layout=\"position\"\n          transition={layoutTransition}\n        >\n          {iconSlot}\n        </motion.span>\n      ) : null}\n    </AnimatePresence>\n  );\n});\n\nconst defaultLoadingIcon = <Loader2 aria-hidden strokeWidth={2.25} />;\n\nconst ButtonLoadingContent = memo(function ButtonLoadingContent({\n  accessibleName,\n  children,\n  contentRef,\n  icon,\n  iconPosition,\n  linkUnderlineEnabled,\n  loading,\n  loadingIcon,\n  reduceMotion,\n}: {\n  accessibleName?: string;\n  children: ReactNode;\n  contentRef?: React.Ref<HTMLSpanElement>;\n  icon?: ReactNode;\n  iconPosition: \"start\" | \"end\";\n  linkUnderlineEnabled: boolean;\n  loading: boolean;\n  loadingIcon?: ReactNode;\n  reduceMotion: boolean | null;\n}) {\n  const hasIcon = Boolean(icon);\n  const hasLabel = children != null && children !== false && children !== \"\";\n  const noMotion = reduceMotion === true;\n  const iconTransition = noMotion ? { duration: 0 } : loadingIconTransition;\n  const layoutTransition = noMotion ? { duration: 0 } : loadingLayoutTransition;\n  const spinner = loadingIcon ?? defaultLoadingIcon;\n\n  const iconAtStart = loading || (hasIcon && iconPosition === \"start\");\n  const iconAtEnd = hasIcon && !loading && iconPosition === \"end\";\n  const spinnerOnlyAtStart = loading && !hasIcon;\n\n  const iconSlotProps = {\n    accessibleName,\n    hasLabel,\n    iconTransition,\n    layoutTransition,\n    noMotion,\n    spinner,\n  };\n\n  return (\n    <motion.span\n      animate={loading && !noMotion ? { opacity: 0.96 } : { opacity: 1 }}\n      className={buttonContentClassName}\n      initial={false}\n      layout={!noMotion}\n      ref={contentRef}\n      transition={layoutTransition}\n    >\n      <ButtonSpinnerPresence\n        iconSlot={\n          <ButtonIconSlot\n            {...iconSlotProps}\n            dataIcon=\"inline-start\"\n            hasIcon={hasIcon}\n            icon={icon}\n            showSpinner\n          />\n        }\n        layoutTransition={layoutTransition}\n        noMotion={noMotion}\n        visible={spinnerOnlyAtStart}\n      />\n      {hasIcon && iconAtStart ? (\n        <ButtonIconSlot\n          {...iconSlotProps}\n          dataIcon=\"inline-start\"\n          hasIcon={hasIcon}\n          icon={icon}\n          showSpinner={loading}\n        />\n      ) : null}\n      {hasLabel ? (\n        <ButtonLoadingLabel\n          layoutTransition={layoutTransition}\n          linkUnderlineEnabled={linkUnderlineEnabled}\n          loading={loading}\n          noMotion={noMotion}\n        >\n          {children}\n        </ButtonLoadingLabel>\n      ) : null}\n      {hasIcon && iconAtEnd ? (\n        <ButtonIconSlot\n          {...iconSlotProps}\n          dataIcon=\"inline-end\"\n          hasIcon={hasIcon}\n          icon={icon}\n          showSpinner={loading}\n        />\n      ) : null}\n    </motion.span>\n  );\n});\n\nconst rippleTransition = {\n  duration: 0.65,\n  ease: [0.22, 1, 0.36, 1] as const,\n  opacity: {\n    duration: 0.65,\n    ease: [0.4, 0, 0.2, 1] as const,\n    times: [0, 0.2, 1] as const,\n  },\n} as const;\n\nconst RippleLayer = memo(function RippleLayer({\n  onRippleComplete,\n  ripples,\n}: {\n  ripples: Ripple[];\n  onRippleComplete: (id: string) => void;\n}) {\n  return (\n    <span aria-hidden className=\"pointer-events-none absolute inset-0 z-[1]\">\n      {ripples.map((ripple) => (\n        <motion.span\n          animate={{ opacity: [0.36, 0.2, 0], scale: 1 }}\n          className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--button-ripple)]\"\n          initial={{ opacity: 0.36, scale: 0 }}\n          key={ripple.id}\n          onAnimationComplete={() => onRippleComplete(ripple.id)}\n          style={{\n            height: ripple.size,\n            left: ripple.x,\n            top: ripple.y,\n            width: ripple.size,\n          }}\n          transition={rippleTransition}\n        />\n      ))}\n    </span>\n  );\n});\n\ntype ButtonInteractionElement = HTMLButtonElement | HTMLAnchorElement;\n\ntype ButtonInteractionHandlers = {\n  onBlur?: FocusEventHandler<ButtonInteractionElement>;\n  onKeyDown?: KeyboardEventHandler<ButtonInteractionElement>;\n  onKeyUp?: KeyboardEventHandler<ButtonInteractionElement>;\n  onPointerCancel?: PointerEventHandler<ButtonInteractionElement>;\n  onPointerDown?: PointerEventHandler<ButtonInteractionElement>;\n  onPointerLeave?: PointerEventHandler<ButtonInteractionElement>;\n  onPointerUp?: PointerEventHandler<ButtonInteractionElement>;\n};\n\nexport type ButtonProps = Omit<\n  ButtonHTMLAttributesForMotion,\n  keyof ButtonInteractionHandlers\n> &\n  ButtonInteractionHandlers &\n  VariantProps<typeof buttonVariants> & {\n    animateSize?: boolean;\n    children?: ReactNode;\n    disableRipple?: boolean;\n    href?: string;\n    icon?: ReactNode;\n    iconLabel?: string;\n    iconPosition?: \"start\" | \"end\";\n    loading?: boolean;\n    loadingIcon?: ReactNode;\n  } & Pick<\n    AnchorHTMLAttributes<HTMLAnchorElement>,\n    | \"download\"\n    | \"hrefLang\"\n    | \"media\"\n    | \"ping\"\n    | \"referrerPolicy\"\n    | \"rel\"\n    | \"target\"\n  >;\n\nconst ButtonInner = forwardRef<\n  HTMLButtonElement | HTMLAnchorElement,\n  ButtonProps\n>(\n  (\n    {\n      animateSize = false,\n      \"aria-label\": ariaLabel,\n      children,\n      className,\n      disabled,\n      disableRipple = false,\n      href,\n      icon,\n      iconLabel,\n      iconPosition = \"start\",\n      linkUnderline,\n      loading = false,\n      loadingIcon,\n      onBlur,\n      onKeyDown,\n      onKeyUp,\n      onPointerCancel,\n      onPointerDown,\n      onPointerLeave,\n      onPointerUp,\n      rel,\n      size,\n      style,\n      target,\n      type = \"button\",\n      variant,\n      ...props\n    },\n    ref\n  ) => {\n    const isLocked = Boolean(disabled || loading);\n    const accessibleName = iconLabel ?? ariaLabel;\n    const reduceMotion = useReducedMotion();\n\n    useIconOnlyA11yWarning(size, children, iconLabel, ariaLabel, loading);\n    const buttonNodeRef = useRef<HTMLButtonElement | HTMLAnchorElement | null>(\n      null\n    );\n    const rippleIdRef = useRef(0);\n    const wasLoadingRef = useRef(loading);\n    const [horizontalInset, setHorizontalInset] = useState(0);\n    const [isPressed, setIsPressed] = useState(false);\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const shouldAnimateSize = animateSize && style?.width == null;\n    const [contentRef, bounds] = useMeasure<HTMLSpanElement>(shouldAnimateSize);\n    const lastContentWidthRef = useRef(0);\n    const resolvedLinkUnderline =\n      variant === \"link\" ? (linkUnderline ?? \"motion\") : undefined;\n    const useLinkMotionUnderline = resolvedLinkUnderline === \"motion\";\n\n    const canAnimate = !isLocked && reduceMotion !== true;\n    const allowsRipple =\n      !(disableRipple || loading) &&\n      variant !== \"link\" &&\n      !isLocked &&\n      reduceMotion !== true;\n    useLayoutEffect(() => {\n      if (bounds.width > 0) {\n        lastContentWidthRef.current = bounds.width;\n      }\n    }, [bounds.width]);\n\n    const contentWidth =\n      bounds.width > 0 ? bounds.width : lastContentWidthRef.current;\n    const animatedWidth =\n      shouldAnimateSize && contentWidth > 0\n        ? Math.ceil(contentWidth + horizontalInset)\n        : undefined;\n\n    const setButtonRef = useCallback(\n      (node: HTMLButtonElement | HTMLAnchorElement | null) => {\n        buttonNodeRef.current = node;\n        setRef(ref, node);\n      },\n      [ref]\n    );\n\n    const measureHorizontalInset = useCallback(\n      (measuredContentWidth: number) => {\n        if (!(measuredContentWidth > 0)) {\n          return;\n        }\n\n        const buttonNode = buttonNodeRef.current;\n        if (!(buttonNode && animateSize)) {\n          return;\n        }\n\n        const styles = window.getComputedStyle(buttonNode);\n        const next =\n          (Number.parseFloat(styles.borderLeftWidth) || 0) +\n          (Number.parseFloat(styles.borderRightWidth) || 0) +\n          (Number.parseFloat(styles.paddingLeft) || 0) +\n          (Number.parseFloat(styles.paddingRight) || 0);\n\n        setHorizontalInset((current) => (current === next ? current : next));\n      },\n      [animateSize]\n    );\n\n    useLayoutEffect(() => {\n      measureHorizontalInset(contentWidth);\n    }, [contentWidth, measureHorizontalInset]);\n\n    const handleRippleComplete = useCallback((id: string) => {\n      setRipples((current) => {\n        if (!current.some((item) => item.id === id)) {\n          return current;\n        }\n\n        return current.filter((item) => item.id !== id);\n      });\n    }, []);\n\n    useEffect(() => {\n      if (isLocked) {\n        setIsPressed(false);\n      }\n    }, [isLocked]);\n\n    useLayoutEffect(() => {\n      const wasLoading = wasLoadingRef.current;\n      if (loading || wasLoading) {\n        setRipples([]);\n      }\n      if (loading) {\n        setIsPressed(false);\n      }\n      wasLoadingRef.current = loading;\n    }, [loading]);\n\n    const handlePointerDown = (\n      e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>\n    ) => {\n      onPointerDown?.(e);\n\n      if (e.defaultPrevented || isLocked) {\n        return;\n      }\n\n      setIsPressed(true);\n\n      if (!(allowsRipple && e.button === 0)) {\n        return;\n      }\n\n      if (e.pointerType === \"mouse\" && e.buttons !== 1) {\n        return;\n      }\n\n      const rect = e.currentTarget.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n      const rippleSize =\n        2 *\n        Math.max(\n          Math.hypot(x, y),\n          Math.hypot(rect.width - x, y),\n          Math.hypot(x, rect.height - y),\n          Math.hypot(rect.width - x, rect.height - y)\n        );\n      rippleIdRef.current += 1;\n      const id = `ripple-${rippleIdRef.current}`;\n\n      setRipples((current) => {\n        const next = [...current, { id, x, y, size: rippleSize }];\n        return next.length > MAX_RIPPLES\n          ? next.slice(next.length - MAX_RIPPLES)\n          : next;\n      });\n    };\n\n    const handlePointerUp = (\n      e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>\n    ) => {\n      onPointerUp?.(e);\n      setIsPressed(false);\n    };\n\n    const handlePointerLeave = (\n      e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>\n    ) => {\n      onPointerLeave?.(e);\n      setIsPressed(false);\n    };\n\n    const handlePointerCancel = (\n      e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>\n    ) => {\n      onPointerCancel?.(e);\n      setIsPressed(false);\n    };\n\n    const handleKeyDown = (e: KeyboardEvent<ButtonInteractionElement>) => {\n      onKeyDown?.(e);\n\n      if (\n        e.defaultPrevented ||\n        isLocked ||\n        e.repeat ||\n        (e.key !== \" \" && e.key !== \"Enter\")\n      ) {\n        return;\n      }\n\n      setIsPressed(true);\n    };\n\n    const handleKeyUp = (e: KeyboardEvent<ButtonInteractionElement>) => {\n      onKeyUp?.(e);\n\n      if (e.key === \" \" || e.key === \"Enter\") {\n        setIsPressed(false);\n      }\n    };\n\n    const rootClassName = cn(\n      buttonVariants({\n        className,\n        linkUnderline: resolvedLinkUnderline,\n        size,\n        variant,\n      }),\n      isLocked && \"pointer-events-none\",\n      disabled && \"opacity-50\"\n    );\n\n    const buildButtonSurface = (labelContent: ReactNode) => (\n      <>\n        {allowsRipple ? (\n          <RippleLayer\n            onRippleComplete={handleRippleComplete}\n            ripples={ripples}\n          />\n        ) : null}\n        <ButtonLoadingContent\n          accessibleName={accessibleName}\n          contentRef={shouldAnimateSize ? contentRef : undefined}\n          icon={icon}\n          iconPosition={iconPosition}\n          linkUnderlineEnabled={resolvedLinkUnderline === \"motion\"}\n          loading={loading}\n          loadingIcon={loadingIcon}\n          reduceMotion={reduceMotion}\n        >\n          {labelContent}\n        </ButtonLoadingContent>\n      </>\n    );\n\n    const buttonSurface = buildButtonSurface(children);\n\n    const motionAnimate = useMemo(\n      () =>\n        getButtonAnimate(canAnimate, isPressed, animatedWidth, reduceMotion),\n      [animatedWidth, canAnimate, isPressed, reduceMotion]\n    );\n\n    const motionSurfaceProps = useMemo(\n      () =>\n        buildMotionSurfaceProps({\n          canAnimate,\n          isPressed,\n          motionAnimate,\n          reduceMotion,\n          useLinkMotionUnderline,\n          variant,\n        }),\n      [\n        canAnimate,\n        isPressed,\n        motionAnimate,\n        reduceMotion,\n        useLinkMotionUnderline,\n        variant,\n      ]\n    );\n\n    const sharedRootProps = {\n      \"aria-busy\": loading || undefined,\n      \"aria-label\": accessibleName,\n      className: rootClassName,\n      \"data-slot\": \"button\" as const,\n    };\n\n    if (href?.trim()) {\n      const safeHref = href.trim();\n      const {\n        onClick,\n        type: _type,\n        ...anchorProps\n      } = omitLinkMotionConflicts(props as AnchorHTMLAttributesForMotion);\n      const safeRel = resolveAnchorRel(target, rel);\n\n      return (\n        <MotionAnchor\n          {...motionSurfaceProps}\n          {...anchorProps}\n          {...sharedRootProps}\n          aria-disabled={isLocked || undefined}\n          href={safeHref}\n          onBlur={(e) => {\n            onBlur?.(e);\n            setIsPressed(false);\n          }}\n          onClick={(e) => {\n            if (isLocked) {\n              e.preventDefault();\n            }\n            onClick?.(e as React.MouseEvent<HTMLAnchorElement>);\n          }}\n          onKeyDown={(e) => {\n            onKeyDown?.(e);\n\n            if (\n              e.defaultPrevented ||\n              isLocked ||\n              e.repeat ||\n              e.key !== \"Enter\"\n            ) {\n              return;\n            }\n\n            setIsPressed(true);\n          }}\n          onKeyUp={(e) => {\n            onKeyUp?.(e);\n\n            if (e.key === \"Enter\") {\n              setIsPressed(false);\n            }\n          }}\n          onPointerCancel={handlePointerCancel}\n          onPointerDown={handlePointerDown}\n          onPointerLeave={handlePointerLeave}\n          onPointerUp={handlePointerUp}\n          ref={setButtonRef}\n          rel={safeRel}\n          style={style}\n          tabIndex={isLocked ? -1 : undefined}\n          target={target}\n        >\n          {buttonSurface}\n        </MotionAnchor>\n      );\n    }\n\n    return (\n      <ButtonPrimitive\n        {...sharedRootProps}\n        disabled={isLocked}\n        onBlur={(e) => {\n          onBlur?.(e);\n          setIsPressed(false);\n        }}\n        onKeyDown={handleKeyDown}\n        onKeyUp={handleKeyUp}\n        onPointerCancel={handlePointerCancel}\n        onPointerDown={handlePointerDown}\n        onPointerLeave={handlePointerLeave}\n        onPointerUp={handlePointerUp}\n        render={(buttonProps) => {\n          const {\n            primitiveClassName,\n            primitiveRef,\n            primitiveStyle,\n            resolvedButtonProps,\n          } = resolvePrimitiveButtonProps(buttonProps);\n\n          return (\n            <motion.button\n              {...resolvedButtonProps}\n              {...motionSurfaceProps}\n              className={primitiveClassName}\n              ref={(node) => {\n                setButtonRef(node);\n                setRef(primitiveRef, node);\n              }}\n              style={primitiveStyle}\n              type={type}\n            >\n              {buttonSurface}\n            </motion.button>\n          );\n        }}\n        style={style}\n        type={type}\n        {...props}\n      />\n    );\n  }\n);\n\nButtonInner.displayName = \"Button\";\n\nconst Button = memo(ButtonInner);\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Button (Base UI)",
  "description": "Button with embedded Iconiq theme tokens, optional loading, href link rendering, ripple, press spring, hover lift, and intrinsic-width animation over the Base UI button primitive."
}
