{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "class-variance-authority"],
  "devDependencies": [],
  "files": [
    {
      "path": "button.tsx",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { motion } from \"motion/react\";\nimport {\n  type ButtonHTMLAttributes,\n  forwardRef,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useState,\n} from \"react\";\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 buttonVariantStyles = {\n  default: \"bg-primary text-background hover:bg-primary/90\",\n  destructive: \"bg-red-600 text-white hover:bg-red-600/90\",\n  outline:\n    \"border border-input bg-background text-foreground hover:bg-accent/60 hover:text-accent-foreground\",\n  secondary:\n    \"bg-neutral-200 text-neutral-900 hover:bg-neutral-300 dark:bg-neutral-600 dark:text-neutral-50 dark:hover:bg-neutral-500\",\n  ghost: \"text-foreground hover:bg-accent/60 hover:text-accent-foreground\",\n  link: \"text-primary underline-offset-4 hover:underline\",\n} as const;\n\nconst buttonSizeStyles = {\n  sm: \"min-h-11 px-3.5 text-xs\",\n  md: \"min-h-11 px-5 text-sm\",\n  lg: \"min-h-12 px-8 text-base\",\n  unstyled: \"min-h-11 min-w-11 px-0 py-0 text-sm\",\n} as const;\n\nconst buttonVariants = cva(\n  \"relative inline-flex cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: buttonVariantStyles,\n      size: buttonSizeStyles,\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"md\",\n    },\n  }\n);\n\nconst buttonRootVariants = cva(\n  \"relative inline-flex cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden rounded-lg font-medium transition-[background-color,color,filter] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[pressed=true]:brightness-95\",\n  {\n    variants: {\n      variant: buttonVariantStyles,\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n);\n\nconst buttonContentVariants = cva(\n  \"relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap text-inherit [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      size: buttonSizeStyles,\n    },\n    defaultVariants: {\n      size: \"md\",\n    },\n  }\n);\n\ntype Ripple = { id: string; x: number; y: number; size: number };\n\n/**\n * Prop names that exist on both DOM `button` and `motion.button` but with incompatible types\n * (Motion uses them for gestures / layout animation, not DOM events).\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\nfunction useMeasure<T extends HTMLElement = HTMLElement>(): [\n  (node: T | null) => void,\n  { width: number; height: number },\n] {\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 (!(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]);\n\n  return [ref, bounds];\n}\n\nexport type ButtonProps = ButtonHTMLAttributesForMotion &\n  VariantProps<typeof buttonVariants> & {\n    animateSize?: boolean;\n    children?: ReactNode;\n    disableRipple?: boolean;\n    icon?: ReactNode;\n    iconPosition?: \"start\" | \"end\";\n  };\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      animateSize = false,\n      children,\n      className,\n      disabled,\n      disableRipple = false,\n      icon,\n      iconPosition = \"start\",\n      onBlur,\n      onKeyDown,\n      onKeyUp,\n      onPointerCancel,\n      onPointerDown,\n      onPointerLeave,\n      onPointerUp,\n      size,\n      style,\n      type = \"button\",\n      variant,\n      ...props\n    },\n    ref\n  ) => {\n    const [buttonNode, setButtonNode] = useState<HTMLButtonElement | null>(\n      null\n    );\n    const [contentRef, bounds] = useMeasure<HTMLSpanElement>();\n    const [borderWidth, setBorderWidth] = useState(0);\n    const [isPressed, setIsPressed] = useState(false);\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const canAnimate = !disabled;\n    const allowsRipple = !disableRipple && variant !== \"link\" && canAnimate;\n    const shouldAnimateSize = animateSize && style?.width == null;\n    const animatedWidth =\n      shouldAnimateSize && bounds.width > 0\n        ? Math.ceil(bounds.width + borderWidth)\n        : undefined;\n\n    const setButtonRef = useCallback(\n      (node: HTMLButtonElement | null) => {\n        setButtonNode(node);\n\n        if (typeof ref === \"function\") {\n          ref(node);\n          return;\n        }\n\n        if (ref) {\n          ref.current = node;\n        }\n      },\n      [ref]\n    );\n\n    useLayoutEffect(() => {\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\n      setBorderWidth((current) => (current === next ? current : next));\n    });\n\n    useEffect(() => {\n      if (disabled) {\n        setIsPressed(false);\n      }\n    }, [disabled]);\n\n    const handlePointerDown = (e: PointerEvent<HTMLButtonElement>) => {\n      onPointerDown?.(e);\n\n      if (e.defaultPrevented || disabled) {\n        return;\n      }\n\n      setIsPressed(true);\n\n      if (!(allowsRipple && e.button === 0)) {\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      const id =\n        typeof crypto !== \"undefined\" && crypto.randomUUID\n          ? crypto.randomUUID()\n          : `${Date.now()}-${Math.random()}`;\n\n      setRipples((current) => [...current, { id, x, y, size: rippleSize }]);\n    };\n\n    const handlePointerUp = (e: PointerEvent<HTMLButtonElement>) => {\n      onPointerUp?.(e);\n      setIsPressed(false);\n    };\n\n    const handlePointerLeave = (e: PointerEvent<HTMLButtonElement>) => {\n      onPointerLeave?.(e);\n      setIsPressed(false);\n    };\n\n    const handlePointerCancel = (e: PointerEvent<HTMLButtonElement>) => {\n      onPointerCancel?.(e);\n      setIsPressed(false);\n    };\n\n    const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n      onKeyDown?.(e);\n\n      if (\n        e.defaultPrevented ||\n        disabled ||\n        e.repeat ||\n        (e.key !== \" \" && e.key !== \"Enter\")\n      ) {\n        return;\n      }\n\n      setIsPressed(true);\n    };\n\n    const handleKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {\n      onKeyUp?.(e);\n\n      if (e.key === \" \" || e.key === \"Enter\") {\n        setIsPressed(false);\n      }\n    };\n\n    const renderedIcon = icon ? (\n      <span aria-hidden className=\"inline-flex shrink-0 items-center\">\n        {icon}\n      </span>\n    ) : null;\n\n    return (\n      <motion.button\n        animate={\n          animatedWidth\n            ? {\n                scale: canAnimate && isPressed ? 0.96 : 1,\n                width: animatedWidth,\n              }\n            : {\n                scale: canAnimate && isPressed ? 0.96 : 1,\n              }\n        }\n        className={cn(\n          componentThemeClassName,\n          buttonRootVariants({ variant }),\n          className\n        )}\n        data-pressed={isPressed ? \"true\" : \"false\"}\n        disabled={disabled}\n        initial={false}\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        ref={setButtonRef}\n        style={style}\n        transition={{\n          scale: { type: \"spring\", stiffness: 640, damping: 38, mass: 0.85 },\n          width: {\n            type: \"spring\",\n            stiffness: 420,\n            damping: 34,\n            mass: 0.9,\n          },\n          y: { type: \"spring\", stiffness: 340, damping: 28, mass: 0.8 },\n        }}\n        type={type}\n        whileHover={canAnimate && variant !== \"link\" ? { y: -1 } : undefined}\n        {...props}\n      >\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[inherit]\"\n        >\n          {ripples.map((ripple) => (\n            <motion.span\n              animate={{ opacity: 0, scale: 1 }}\n              className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-current\"\n              initial={{ opacity: 0.22, scale: 0 }}\n              key={ripple.id}\n              onAnimationComplete={() => {\n                setRipples((current) =>\n                  current.filter((item) => item.id !== ripple.id)\n                );\n              }}\n              style={{\n                height: ripple.size,\n                left: ripple.x,\n                top: ripple.y,\n                width: ripple.size,\n              }}\n              transition={{\n                duration: 0.55,\n                ease: [0.22, 1, 0.36, 1],\n              }}\n            />\n          ))}\n        </span>\n        <span\n          className={buttonContentVariants({ size })}\n          ref={shouldAnimateSize ? contentRef : undefined}\n        >\n          {iconPosition === \"start\" ? renderedIcon : null}\n          {children}\n          {iconPosition === \"end\" ? renderedIcon : null}\n        </span>\n      </motion.button>\n    );\n  }\n);\n\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Button",
  "description": "Primary action control with larger default hit targets, spring press feedback, optional intrinsic width animation, and Motion ripples. Forwards ref and native button props."
}
