{"slug":"button","name":"Button","description":"Spring-pressed Button plus StatefulButton (idle → loading → success / error) and MagneticButton.","category":"motion","source_url":"https://beui.saura3h.xyz/r/button/raw","detail_url":"https://beui.saura3h.xyz/r/button","raw_url":"https://beui.saura3h.xyz/r/button/raw","page_url":"https://beui.saura3h.xyz/components/motion/button","dependencies":[],"internal":[],"files":[{"path":"components/motion/button/index.tsx","type":"component","content":"export { Button } from \"./base\";\nexport type { ButtonProps, ButtonVariant, ButtonSize } from \"./base\";\n\nexport { StatefulButton } from \"./stateful\";\nexport type { StatefulButtonProps, ButtonState } from \"./stateful\";\n\nexport { MagneticButton } from \"./magnetic\";\nexport type { MagneticButtonProps } from \"./magnetic\";\n"},{"path":"components/motion/button/base.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useReducedMotion, type HTMLMotionProps } from \"motion/react\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<HTMLMotionProps<\"button\">, \"children\"> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-(--color-fg) text-(--color-bg) hover:bg-(--color-fg)/90\",\n  secondary:\n    \"border border-(--color-border) bg-(--color-bg-elev) text-(--color-fg) hover:border-(--color-border-strong)\",\n  ghost: \"text-(--color-fg-muted) hover:text-(--color-fg) hover:bg-(--color-fg)/5\",\n  outline: \"border border-(--color-border) bg-transparent text-(--color-fg) hover:bg-(--color-fg)/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const PRESS_SPRING = { type: \"spring\" as const, stiffness: 500, damping: 30, mass: 0.6 };\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n  { variant = \"primary\", size = \"md\", pressScale = 0.93, className, children, ...rest },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  return (\n    <motion.button\n      ref={ref}\n      type=\"button\"\n      whileTap={reduce ? undefined : { scale: pressScale }}\n      whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n      transition={PRESS_SPRING}\n      className={cn(\n        \"inline-flex items-center justify-center font-medium select-none\",\n        \"transition-colors\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        VARIANT_CLASS[variant],\n        SIZE_CLASS[size],\n        className,\n      )}\n      {...rest}\n    >\n      {children}\n    </motion.button>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"component","content":"\"use client\";\n\nimport { AnimatePresence, motion, type HTMLMotionProps } from \"motion/react\";\nimport { Check, Loader2, X } from \"lucide-react\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ---------- Base Button (kept in-file so this snippet is self-contained) ---------- */\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\ntype ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\ninterface ButtonProps extends Omit<HTMLMotionProps<\"button\">, \"children\"> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-(--color-fg) text-(--color-bg) hover:bg-(--color-fg)/90\",\n  secondary:\n    \"border border-(--color-border) bg-(--color-bg-elev) text-(--color-fg) hover:border-(--color-border-strong)\",\n  ghost: \"text-(--color-fg-muted) hover:text-(--color-fg) hover:bg-(--color-fg)/5\",\n  outline: \"border border-(--color-border) bg-transparent text-(--color-fg) hover:bg-(--color-fg)/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nconst PRESS_SPRING = { type: \"spring\" as const, stiffness: 500, damping: 30, mass: 0.6 };\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n  { variant = \"primary\", size = \"md\", pressScale = 0.93, className, children, ...rest },\n  ref,\n) {\n  return (\n    <motion.button\n      ref={ref}\n      type=\"button\"\n      whileTap={{ scale: pressScale }}\n      whileHover={{ scale: 1.02 }}\n      transition={PRESS_SPRING}\n      className={cn(\n        \"inline-flex items-center justify-center font-medium select-none transition-colors\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        VARIANT_CLASS[variant],\n        SIZE_CLASS[size],\n        className,\n      )}\n      {...rest}\n    >\n      {children}\n    </motion.button>\n  );\n});\n\n/* ---------- StatefulButton ---------- */\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst SWAP_SPRING = { type: \"spring\" as const, stiffness: 460, damping: 30, mass: 0.55 };\n\nfunction Slot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  return (\n    <motion.span\n      key={keyId}\n      initial={{ y: 14, opacity: 0, filter: \"blur(6px)\" }}\n      animate={{ y: 0, opacity: 1, filter: \"blur(0px)\" }}\n      exit={{ y: -14, opacity: 0, filter: \"blur(6px)\" }}\n      transition={SWAP_SPRING}\n      className=\"inline-flex items-center gap-2 whitespace-nowrap\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} {...rest}>\n      <motion.span\n        layout\n        transition={SWAP_SPRING}\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {state === \"idle\" ? (\n            <Slot keyId=\"idle\">\n              {children}\n              {icon}\n            </Slot>\n          ) : null}\n          {state === \"loading\" ? (\n            <Slot keyId=\"loading\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n              {loadingText}\n            </Slot>\n          ) : null}\n          {state === \"success\" ? (\n            <Slot keyId=\"success\">\n              <Check className=\"h-4 w-4\" />\n              {successText}\n            </Slot>\n          ) : null}\n          {state === \"error\" ? (\n            <Slot keyId=\"error\">\n              <X className=\"h-4 w-4\" />\n              {errorText}\n            </Slot>\n          ) : null}\n        </AnimatePresence>\n      </motion.span>\n    </Button>\n  );\n});\n"},{"path":"components/motion/button/magnetic.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useMotionValue, useSpring, type HTMLMotionProps } from \"motion/react\";\nimport { forwardRef, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ---------- Base Button (kept in-file so this snippet is self-contained) ---------- */\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\ntype ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\ninterface ButtonProps extends Omit<HTMLMotionProps<\"button\">, \"children\"> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-(--color-fg) text-(--color-bg) hover:bg-(--color-fg)/90\",\n  secondary:\n    \"border border-(--color-border) bg-(--color-bg-elev) text-(--color-fg) hover:border-(--color-border-strong)\",\n  ghost: \"text-(--color-fg-muted) hover:text-(--color-fg) hover:bg-(--color-fg)/5\",\n  outline: \"border border-(--color-border) bg-transparent text-(--color-fg) hover:bg-(--color-fg)/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nconst PRESS_SPRING = { type: \"spring\" as const, stiffness: 500, damping: 30, mass: 0.6 };\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n  { variant = \"primary\", size = \"md\", pressScale = 0.93, className, children, ...rest },\n  ref,\n) {\n  return (\n    <motion.button\n      ref={ref}\n      type=\"button\"\n      whileTap={{ scale: pressScale }}\n      whileHover={{ scale: 1.02 }}\n      transition={PRESS_SPRING}\n      className={cn(\n        \"inline-flex items-center justify-center font-medium select-none transition-colors\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        VARIANT_CLASS[variant],\n        SIZE_CLASS[size],\n        className,\n      )}\n      {...rest}\n    >\n      {children}\n    </motion.button>\n  );\n});\n\n/* ---------- MagneticButton ---------- */\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, { stiffness: 200, damping: 15, mass: 0.3 });\n  const sy = useSpring(y, { stiffness: 200, damping: 15, mass: 0.3 });\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = wrapRef.current;\n    if (!el) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={wrapRef}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", magneticClassName)}\n    >\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </motion.div>\n  );\n});\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"util","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"lib/utils.ts","type":"util","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"}]}