# Magnetic

> Buttons and icons lean toward your pointer as it comes near, labels drifting a little further than their buttons, then spring back the moment you move away.

- Page: https://devl.dev/motion/magnetic
- Kind: motion · Cursor
- Install: `npx shadcn@latest add https://devl.dev/r/magnetic.json`
- Raw source: https://devl.dev/raw/magnetic.tsx
- npm: motion
- License: MIT. The coss ui components it uses (https://coss.com/ui) are AGPL-3.0.

## Usage

```tsx
"use client";

import { ArrowRightIcon, CalendarDaysIcon, HouseIcon, InboxIcon, SearchIcon, Settings2Icon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Magnetic, MagneticField } from "@/components/magnetic";

const dock = [
  { label: "Home", hint: "Back to your overview", icon: HouseIcon },
  { label: "Search", hint: "Find anything ⌘K", icon: SearchIcon },
  { label: "Inbox", hint: "3 unread from Marta", icon: InboxIcon },
  { label: "Calendar", hint: "Standup in 12 min", icon: CalendarDaysIcon },
  { label: "Settings", hint: "Workspace and billing", icon: Settings2Icon },
];

export default function MagneticDemo() {
  const [hovered, setHovered] = useState<number | null>(null);
  const current = hovered === null ? null : dock[hovered];

  return (
    <MagneticField className="flex w-full max-w-sm flex-col items-center gap-12 px-6 py-10">
      <Magnetic max={8} radius={64} strength={0.3}>
        <Button className="h-11 gap-2.5 rounded-full px-5 text-sm transition-[box-shadow,scale] active:scale-[0.97] sm:h-11">
          <Magnetic max={3} radius={48} strength={0.18}>
            Book a demo
          </Magnetic>
          <Magnetic max={5} radius={48} strength={0.3}>
            <ArrowRightIcon />
          </Magnetic>
        </Button>
      </Magnetic>

      <div className="flex flex-col items-center gap-3">
        <nav
          aria-label="App"
          className="flex items-center gap-1 rounded-full border bg-card p-1.5 shadow-xs/5"
          onPointerLeave={() => setHovered(null)}
        >
          {dock.map((item, i) => (
            <Magnetic group="dock" key={item.label} max={6} radius={28} strength={0.4}>
              <button
                aria-label={item.label}
                className="relative grid size-10 cursor-pointer place-items-center rounded-full text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.94] data-[active]:bg-accent data-[active]:text-foreground"
                data-active={hovered === i ? "" : undefined}
                onBlur={() => setHovered(null)}
                onFocus={() => setHovered(i)}
                onPointerEnter={(e) => e.pointerType === "mouse" && setHovered(i)}
                type="button"
              >
                <Magnetic max={3} radius={16} strength={0.2}>
                  <item.icon className="size-[18px]" />
                </Magnetic>
                {item.label === "Inbox" && (
                  <span className="absolute top-2 right-2 size-1.5 rounded-full bg-destructive ring-2 ring-card" />
                )}
              </button>
            </Magnetic>
          ))}
        </nav>
        <div className="relative h-4 w-full text-center text-muted-foreground text-xs">
          <AnimatePresence initial={false} mode="popLayout">
            <motion.p
              animate={{ opacity: 1, y: 0 }}
              className="absolute inset-x-0 whitespace-nowrap"
              exit={{ opacity: 0, y: -3, transition: { duration: 0.12 } }}
              initial={{ opacity: 0, y: 3 }}
              key={current?.label ?? "idle"}
              transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}
            >
              {current ? (
                <>
                  <span className="font-medium text-foreground">{current.label}</span> · {current.hint}
                </>
              ) : (
                <span className="pointer-coarse:invisible">Bring your pointer close</span>
              )}
            </motion.p>
          </AnimatePresence>
        </div>
      </div>
    </MagneticField>
  );
}
```

## Props

| Prop | Type | Default |
| --- | --- | --- |
| `children` | `ReactNode` |  |

Also accepts: `ComponentProps<"div">`.

## Source: components/magnetic.tsx

```tsx
"use client";

import { type MotionValue, motion, useMotionValue, useReducedMotion, useSpring } from "motion/react";
import {
  type ComponentProps,
  createContext,
  type PointerEvent,
  type ReactNode,
  useContext,
  useLayoutEffect,
  useRef,
} from "react";
import { cn } from "@/lib/utils";

interface Magnet {
  anchor: HTMLElement;
  x: MotionValue<number>;
  y: MotionValue<number>;
  strength: number;
  radius: number;
  max: number;
  group?: string;
}

type Registry = Set<Magnet>;

const FieldContext = createContext<Registry | null>(null);

const spring = { stiffness: 260, damping: 17, mass: 0.6 };

export interface MagneticFieldProps extends ComponentProps<"div"> {
  children: ReactNode;
}

export function MagneticField({ children, className, onPointerMove, onPointerLeave, ...props }: MagneticFieldProps) {
  const registry = useRef<Registry>(new Set()).current;
  const reduce = useReducedMotion();

  const move = (event: PointerEvent<HTMLDivElement>) => {
    onPointerMove?.(event);
    if (reduce || event.pointerType !== "mouse") return;
    const pulls: { m: Magnet; dx: number; dy: number; edge: number }[] = [];
    const nearest = new Map<string, number>();
    for (const m of registry) {
      const r = m.anchor.getBoundingClientRect();
      const dx = event.clientX - (r.left + r.width / 2);
      const dy = event.clientY - (r.top + r.height / 2);
      const half = Math.min(r.width, r.height) / 2;
      const wide = r.width >= r.height;
      const along = Math.max(Math.abs(wide ? dx : dy) - (Math.max(r.width, r.height) / 2 - half), 0);
      const edge = Math.max(Math.hypot(along, wide ? dy : dx) - half, 0);
      pulls.push({ m, dx, dy, edge });
      if (m.group) nearest.set(m.group, Math.min(nearest.get(m.group) ?? Infinity, edge));
    }
    for (const { m, dx, dy, edge } of pulls) {
      const t = m.group && edge > (nearest.get(m.group) ?? 0) ? 1 : Math.min(edge / m.radius, 1);
      const pull = 1 - t * t * (3 - 2 * t);
      m.x.set(m.max * Math.tanh((dx * m.strength * pull) / m.max));
      m.y.set(m.max * Math.tanh((dy * m.strength * pull) / m.max));
    }
  };

  const leave = (event: PointerEvent<HTMLDivElement>) => {
    onPointerLeave?.(event);
    for (const m of registry) {
      m.x.set(0);
      m.y.set(0);
    }
  };

  return (
    <FieldContext.Provider value={registry}>
      <div className={className} onPointerLeave={leave} onPointerMove={move} {...props}>
        {children}
      </div>
    </FieldContext.Provider>
  );
}

export interface MagneticProps {
  children: ReactNode;
  strength?: number;
  radius?: number;
  max?: number;
  group?: string;
  className?: string;
}

export function Magnetic({ children, strength = 0.35, radius = 72, max = 10, group, className }: MagneticProps) {
  const registry = useContext(FieldContext);
  const anchor = useRef<HTMLSpanElement>(null);
  const tx = useMotionValue(0);
  const ty = useMotionValue(0);
  const x = useSpring(tx, spring);
  const y = useSpring(ty, spring);

  useLayoutEffect(() => {
    const el = anchor.current;
    if (!registry || !el) return;
    const magnet: Magnet = { anchor: el, x: tx, y: ty, strength, radius, max, group };
    registry.add(magnet);
    return () => {
      registry.delete(magnet);
    };
  }, [registry, tx, ty, strength, radius, max, group]);

  return (
    <span className={cn("inline-flex", className)} ref={anchor}>
      <motion.span className="inline-flex will-change-transform" style={{ x, y }}>
        {children}
      </motion.span>
    </span>
  );
}
```
