VeloxUI
interaction·free

Magnetic Button

Button that is pulled toward the cursor with an elastic snap-back.

Installation

terminal
pnpm add gsap @gsap/react

Then copy the component below into src/components/motion/magnetic-button.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).

Source

src/components/motion/magnetic-button.tsx
"use client";

import { useRef, type ButtonHTMLAttributes, type ReactNode } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";

export interface MagneticButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: ReactNode;
  /** How far the button follows the cursor (0–1). */
  strength?: number;
  /** Radius in px within which the pull is active. */
  radius?: number;
}

export function MagneticButton({
  children,
  className,
  strength = 0.35,
  radius = 120,
  ...props
}: MagneticButtonProps) {
  const ref = useRef<HTMLButtonElement>(null);
  const inner = useRef<HTMLSpanElement>(null);

  useGSAP(
    () => {
      const el = ref.current;
      const label = inner.current;
      if (!el || !label) return;
      const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
      const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
      const lxTo = gsap.quickTo(label, "x", { duration: 0.6, ease: "elastic.out(1, 0.4)" });
      const lyTo = gsap.quickTo(label, "y", { duration: 0.6, ease: "elastic.out(1, 0.4)" });

      const onMove = (e: MouseEvent) => {
        const r = el.getBoundingClientRect();
        const cx = r.left + r.width / 2;
        const cy = r.top + r.height / 2;
        const dx = e.clientX - cx;
        const dy = e.clientY - cy;
        const dist = Math.hypot(dx, dy);
        if (dist < radius) {
          xTo(dx * strength);
          yTo(dy * strength);
          lxTo(dx * strength * 0.4);
          lyTo(dy * strength * 0.4);
        } else {
          xTo(0);
          yTo(0);
          lxTo(0);
          lyTo(0);
        }
      };
      window.addEventListener("mousemove", onMove);
      return () => window.removeEventListener("mousemove", onMove);
    },
    { scope: ref, dependencies: [strength, radius] }
  );

  return (
    <button
      ref={ref}
      className={cn(
        "relative inline-flex items-center justify-center rounded-full bg-foreground px-7 py-3 text-sm font-medium text-background will-change-transform",
        className
      )}
      {...props}
    >
      <span ref={inner} className="inline-block will-change-transform">
        {children}
      </span>
    </button>
  );
}