# Magnetic button

Button that pulls toward the cursor with spring physics while its label drifts slightly further for depth.

- Type: component
- Category: Buttons
- Tier: Free
- Dependencies: motion

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/magnetic-button.json
```

## Usage

```tsx
import { MagneticButton } from "@/components/facade/magnetic-button";

export default function Page() {
  return <MagneticButton />;
}
```

## Source

```tsx
"use client";

import * as React from "react";
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react";
import { cn } from "@/lib/utils";

export type MagneticButtonVariant = "brand" | "solid" | "outline";

export interface MagneticButtonProps
  extends Omit<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd"
  > {
  /** How strongly the shell follows the cursor, 0 to 1. Defaults to 0.35. */
  strength?: number;
  /** Extra multiplier applied to the inner label. Defaults to 1.6. */
  labelStrength?: number;
  /** Distance in px outside the button where the pull starts. Defaults to 40. */
  radius?: number;
  variant?: MagneticButtonVariant;
  /** Pass `href` to render an `<a>` instead of a `<button>`. */
  href?: string;
  className?: string;
  children?: React.ReactNode;
}

const variants: Record<MagneticButtonVariant, string> = {
  brand:
    "bg-brand text-brand-foreground shadow-[0_12px_32px_-12px_color-mix(in_oklch,var(--brand)_80%,transparent),inset_0_1px_0_0_color-mix(in_oklch,var(--brand-foreground)_25%,transparent)]",
  solid:
    "bg-primary text-primary-foreground shadow-[0_12px_28px_-14px_color-mix(in_oklch,var(--foreground)_55%,transparent),inset_0_1px_0_0_color-mix(in_oklch,var(--primary-foreground)_15%,transparent)]",
  outline: "border border-border bg-background text-foreground hover:bg-muted/60",
};

const spring = { stiffness: 320, damping: 22, mass: 0.6 };

export function MagneticButton({
  strength = 0.35,
  labelStrength = 1.6,
  radius = 40,
  variant = "brand",
  href,
  className,
  children,
  type = "button",
  ...rest
}: MagneticButtonProps) {
  const reduce = useReducedMotion();
  const zoneRef = React.useRef<HTMLSpanElement>(null);

  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const sx = useSpring(x, spring);
  const sy = useSpring(y, spring);
  const lx = useTransform(sx, (v) => v * labelStrength);
  const ly = useTransform(sy, (v) => v * labelStrength);

  const handleMove = (e: React.PointerEvent<HTMLSpanElement>) => {
    if (reduce || e.pointerType === "touch") return;
    const el = zoneRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    x.set((e.clientX - (rect.left + rect.width / 2)) * strength);
    y.set((e.clientY - (rect.top + rect.height / 2)) * strength);
  };

  const reset = () => {
    x.set(0);
    y.set(0);
  };

  const shellClass = cn(
    "relative z-[1] inline-flex h-12 items-center justify-center rounded-full px-7 text-[15px] font-medium tracking-[-0.01em] will-change-transform",
    "transition-[background-color,box-shadow] duration-300",
    "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",
    variants[variant],
    className,
  );

  const label = (
    <motion.span style={{ x: lx, y: ly }} className="relative inline-flex items-center gap-2 [&_svg]:size-4">
      {children}
    </motion.span>
  );

  return (
    <span
      ref={zoneRef}
      onPointerMove={handleMove}
      onPointerLeave={reset}
      className="relative inline-flex"
      style={{ padding: radius, margin: -radius }}
    >
      {href !== undefined ? (
        <motion.a
          href={href}
          style={{ x: sx, y: sy }}
          className={shellClass}
          onBlur={reset}
          {...(rest as Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd">)}
        >
          {label}
        </motion.a>
      ) : (
        <motion.button
          type={type}
          style={{ x: sx, y: sy }}
          className={shellClass}
          whileTap={reduce ? undefined : { scale: 0.97 }}
          onBlur={reset}
          {...rest}
        >
          {label}
        </motion.button>
      )}
    </span>
  );
}

export default MagneticButton;
```
