Most magnetic buttons are broken in the same two ways: they run on touch devices where there is no cursor to be magnetic toward, and they snap back linearly, which feels like a bug rather than a spring.

This one checks for a fine pointer before binding anything, and returns with `elastic.out` so the release reads as physical.

```tsx
"use client";

import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

export function Magnetic({
  children,
  strength = 0.3,
}: {
  children: React.ReactNode;
  strength?: number;
}) {
  const ref = useRef<HTMLSpanElement>(null);

  useGSAP(() => {
    const el = ref.current;
    // No cursor, or motion turned down: bind nothing at all.
    if (!el || !window.matchMedia("(pointer: fine)").matches) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const move = (e: MouseEvent) => {
      const r = el.getBoundingClientRect();
      gsap.to(el, {
        x: (e.clientX - (r.left + r.width / 2)) * strength,
        y: (e.clientY - (r.top + r.height / 2)) * strength,
        duration: 0.5,
        ease: "power3.out",
      });
    };

    const reset = () =>
      gsap.to(el, { x: 0, y: 0, duration: 0.7, ease: "elastic.out(1, 0.4)" });

    el.addEventListener("mousemove", move);
    el.addEventListener("mouseleave", reset);
    return () => {
      el.removeEventListener("mousemove", move);
      el.removeEventListener("mouseleave", reset);
    };
  }, { scope: ref });

  return <span ref={ref} className="inline-block will-change-transform">{children}</span>;
}
```

Two things worth keeping:

- **`pointer: fine` beats a width breakpoint.** A touchscreen laptop is wide *and* has a cursor; an iPad Pro in landscape is wide and doesn't. Asking about the input device answers the actual question.
- **Return the cleanup from `useGSAP`.** Without it, every remount leaves another pair of listeners attached, and the effect compounds until the button starts flying off.