# Magnetic Button (`magnetic-button`)

> A button that translates toward the cursor with spring physics, with the label counter-drifting to create a sense of mass.

- **Docs:** https://www.mellowui.com/components/magnetic-button
- **Markdown:** https://www.mellowui.com/components/magnetic-button.md
- **Registry:** https://www.mellowui.com/r/magnetic-button.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/magnetic-button
- **Categories:** button, interactive, animation
- **Dependencies:** motion

## AI prompt

Add a MagneticButton from the mellow library. The button body springs toward the cursor position (translation, not rotation), while the label drifts slightly counter to the pull — creating a parallax sense of mass. On press it snaps to center and springs back. Use variant='default', variant='accent' for purple, or variant='destructive' for red delete/danger actions. The strength prop (default 0.45) controls pull intensity.

## Install

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

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `children` | `React.ReactNode` | — | Button label content. |
| `variant` | `"default" \| "accent" \| "destructive"` | `"default"` | Color scheme — default is neutral glass, accent is purple, destructive is red for delete/danger actions. |
| `strength` | `number` | `0.45` | Multiplier controlling how far the button pulls toward the cursor. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, {
  useRef,
  useCallback,
  type ButtonHTMLAttributes,
} from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
} from "motion/react";

export interface MagneticButtonProps
  extends Omit<
    ButtonHTMLAttributes<HTMLButtonElement>,
    | "onDrag"
    | "onDragStart"
    | "onDragEnd"
    | "onAnimationStart"
    | "onAnimationEnd"
  > {
  children: React.ReactNode;
  variant?: "default" | "accent" | "destructive";
  strength?: number;
}

export function MagneticButton({
  children,
  variant = "default",
  strength = 0.45,
  className,
  style,
  disabled,
  onMouseMove,
  onMouseLeave,
  onMouseDown,
  onMouseUp,
  ...props
}: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null);

  const mx = useMotionValue(0);
  const my = useMotionValue(0);
  const sc = useMotionValue(1);

  const springCfg = { stiffness: 180, damping: 16, mass: 0.9 };
  const springX = useSpring(mx, springCfg);
  const springY = useSpring(my, springCfg);
  const springScale = useSpring(sc, { stiffness: 380, damping: 26 });

  const labelX = useTransform(springX, (v) => -v * 0.3);
  const labelY = useTransform(springY, (v) => -v * 0.3);

  const isAccent = variant === "accent";
  const isDestructive = variant === "destructive";

  const handleMouseMove = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!ref.current || disabled) return;
      const rect = ref.current.getBoundingClientRect();
      mx.set((e.clientX - (rect.left + rect.width / 2)) * strength);
      my.set((e.clientY - (rect.top + rect.height / 2)) * strength);
      onMouseMove?.(e);
    },
    [disabled, strength, mx, my, onMouseMove]
  );

  const handleMouseLeave = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      mx.set(0);
      my.set(0);
      sc.set(1);
      onMouseLeave?.(e);
    },
    [mx, my, sc, onMouseLeave]
  );

  const handleMouseDown = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!disabled) {
        mx.set(0);
        my.set(0);
        sc.set(0.93);
      }
      onMouseDown?.(e);
    },
    [disabled, mx, my, sc, onMouseDown]
  );

  const handleMouseUp = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      sc.set(1);
      onMouseUp?.(e);
    },
    [sc, onMouseUp]
  );

  return (
    <motion.button
      ref={ref}
      disabled={disabled}
      className={[
        "relative inline-flex items-center justify-center px-5 py-[0.6rem] rounded-[8px]",
        "[font-family:var(--font-sans)] text-sm font-medium tracking-[-0.01em] select-none",
        disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
        isAccent
          ? "border border-[oklch(0.62_0.25_250/0.6)] bg-[oklch(0.50_0.22_250)] text-white"
          : isDestructive
          ? "border border-[oklch(0.62_0.2_25/0.6)] bg-[oklch(0.50_0.19_25)] text-white"
          : "border border-[rgba(var(--ink-rgb),0.14)] bg-[rgba(var(--ink-rgb),0.07)] text-[var(--ink)]",
        className,
      ].filter(Boolean).join(" ")}
      style={{
        x: springX,
        y: springY,
        scale: springScale,
        willChange: "transform",
        ...(style as object),
      }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      {...props}
    >
      <motion.span
        className="relative pointer-events-none"
        style={{ x: labelX, y: labelY }}
      >
        {children}
      </motion.span>
    </motion.button>
  );
}

export default MagneticButton;

```

## Demo

```tsx
"use client";

import React from "react";
import { MagneticButton } from "../mellow/magnetic-button";

export default function MagneticButtonDemo() {
  return (
    <div className="flex flex-col items-center gap-8 p-8">
      <div className="flex flex-wrap items-center justify-center gap-10">
        <MagneticButton>Explore</MagneticButton>
        <MagneticButton>Connect</MagneticButton>
        <MagneticButton variant="accent">Attract</MagneticButton>
        <MagneticButton variant="destructive">Delete</MagneticButton>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-10">
        <MagneticButton strength={0.7}>Strong</MagneticButton>
        <MagneticButton variant="accent" strength={0.2}>Subtle</MagneticButton>
        <MagneticButton disabled>Inert</MagneticButton>
      </div>
      <p className="text-[rgba(var(--ink-rgb),0.35)] text-[0.8125rem]">
        Hover to feel the pull · press to snap back
      </p>
    </div>
  );
}

```
