Effects & Borders

Magnetic Area

Restrained magnetic hover that pulls its child a few pixels toward the cursor and springs back on leave, fine-pointer only.

Install

npx shadcn@latest add @paragon/magnetic-area

magnetic-area.tsx

"use client";

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

export interface MagneticAreaProps extends React.ComponentProps<"div"> {
  /** Max translation toward the cursor in px. Clamped to 0–8. */
  strength?: number;
  /** Disables the pull entirely. */
  static?: boolean;
}

/**
 * Restrained magnetic hover for icon buttons and CTAs: the child translates
 * toward the cursor — a few px at most, this is a lean, not a chase — and
 * springs back to center on leave. Mouse pointers only (touch and coarse
 * pointers see nothing), and reduced motion disables it. The pull scales
 * with cursor distance from center, so it eases in rather than jumping.
 */
export function MagneticArea({
  strength = 4,
  static: isStatic = false,
  className,
  children,
  onPointerMove,
  onPointerLeave,
  ...props
}: MagneticAreaProps) {
  const reducedMotion = useReducedMotion();
  const max = Math.min(Math.max(strength, 0), 8);
  const enabled = !isStatic && !reducedMotion && max > 0;

  const x = useSpring(0, { stiffness: 350, damping: 30 });
  const y = useSpring(0, { stiffness: 350, damping: 30 });

  return (
    <div
      data-slot="magnetic-area"
      className={cn("inline-flex", className)}
      onPointerMove={(event) => {
        onPointerMove?.(event);
        if (!enabled || event.pointerType !== "mouse") return;
        const rect = event.currentTarget.getBoundingClientRect();
        const nx = (event.clientX - (rect.left + rect.width / 2)) / (rect.width / 2);
        const ny = (event.clientY - (rect.top + rect.height / 2)) / (rect.height / 2);
        x.set(Math.max(-1, Math.min(1, nx)) * max);
        y.set(Math.max(-1, Math.min(1, ny)) * max);
      }}
      onPointerLeave={(event) => {
        onPointerLeave?.(event);
        x.set(0);
        y.set(0);
      }}
      {...props}
    >
      <motion.div className="inline-flex" style={{ x, y }}>
        {children}
      </motion.div>
    </div>
  );
}