# Magnetic Icon Buttons in a Floating Toolbar

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/magnetic-icon-buttons-in-a-floating-toolbar).

- Page, with a live preview: https://shadcn.exchange/r/weinc/magnetic-icon-buttons-in-a-floating-toolbar
- Screenshot: https://shadcn.exchange/shot/weinc/magnetic-icon-buttons-in-a-floating-toolbar/light.png
- Registry item: https://we.inc/r/magnetic-icon-buttons-in-a-floating-toolbar.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/magnetic-icon-buttons-in-a-floating-toolbar.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/magnetic-icon-buttons-in-a-floating-toolbar.tsx`

```tsx
import { useEffect, useRef } from "react";

const PULL = 0.4; // share of the pointer distance each icon moves toward it
const RADIUS = 60; // px from an icon's centre where the pull starts

const tools = [
  { label: "Notes", d: "M5 4h10l4 4v12H5z M15 4v4h4 M8 12h8 M8 16h6" },
  { label: "Search", d: "M11 4a7 7 0 1 1 0 14a7 7 0 0 1 0-14z M16 16l4 4" },
  { label: "Calendar", d: "M4 6h16v14H4z M4 10h16 M8 3v5 M16 3v5" },
  { label: "Inbox", d: "M4 13l3-8h10l3 8v6H4z M4 13h5l1 2h4l1-2h5" },
  { label: "Settings", d: "M12 9a3 3 0 1 1 0 6a3 3 0 0 1 0-6z M12 3v3 M12 18v3 M3 12h3 M18 12h3 M5.6 5.6l2.1 2.1 M16.3 16.3l2.1 2.1 M5.6 18.4l2.1-2.1 M16.3 7.7l2.1-2.1" },
];

export default function MagneticIconButtonsInAFloatingToolbar() {
  const bar = useRef<HTMLElement>(null);
  const btns = useRef<(HTMLAnchorElement | null)[]>([]);

  useEffect(() => {
    const el = bar.current;
    if (!el) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const fine = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
    const list = btns.current.filter(Boolean) as HTMLAnchorElement[];
    const pos = list.map(() => ({ x: 0, y: 0 }));
    let px = 0, py = 0, live = false, frame = 0, visible = true;
    const move = (e: PointerEvent) => { if (e.pointerType === "mouse") { live = true; px = e.clientX; py = e.clientY; } };
    const leave = () => { live = false; };
    const tick = (now: number) => {
      const b = el.getBoundingClientRect();
      if (!live && el.contains(document.activeElement)) { px = -9999; py = -9999; }
      else if (!live) {
        // idle demo: a pretend pointer glides along the bar
        px = b.left + b.width / 2 + Math.sin(now / 1600) * b.width * 0.42;
        py = b.top + b.height / 2 - 10;
      }
      let best = -1, bestD = RADIUS * 0.55;
      list.forEach((btn, i) => {
        const r = btn.getBoundingClientRect();
        const cx = r.left + r.width / 2 - pos[i].x, cy = r.top + r.height / 2 - pos[i].y;
        const dx = px - cx, dy = py - cy;
        const near = Math.hypot(dx, dy) < RADIUS;
        pos[i].x += ((near ? dx * PULL : 0) - pos[i].x) * 0.15;
        pos[i].y += ((near ? dy * PULL : 0) - pos[i].y) * 0.15;
        btn.style.transform = `translate(${pos[i].x.toFixed(2)}px, ${pos[i].y.toFixed(2)}px)`;
        if (Math.hypot(dx, dy) < bestD) { bestD = Math.hypot(dx, dy); best = i; }
      });
      list.forEach((btn, i) => btn.toggleAttribute("data-near", i === best));
      frame = requestAnimationFrame(tick);
    };
    const play = () => { if (!frame && visible) frame = requestAnimationFrame(tick); };
    const stop = () => { cancelAnimationFrame(frame); frame = 0; };
    const io = new IntersectionObserver(([en]) => { visible = en.isIntersecting; if (visible) play(); else stop(); });
    io.observe(el);
    if (fine) { el.addEventListener("pointermove", move); el.addEventListener("pointerleave", leave); }
    play();
    return () => {
      stop();
      io.disconnect();
      el.removeEventListener("pointermove", move);
      el.removeEventListener("pointerleave", leave);
    };
  }, []);

  return (
    <section className="bg-[#e9edf1] px-5 py-16 sm:py-20">
      <style>{`
        .w3nav-btn { will-change: transform; }
        .w3nav-bg, .w3nav-tip { opacity: 0; transition: opacity .25s ease, transform .25s ease; }
        .w3nav-tip { transform: translate(-50%, 6px); }
        .w3nav-btn[data-near] .w3nav-bg, .w3nav-btn:focus-visible .w3nav-bg { opacity: 1; }
        .w3nav-btn[data-near] .w3nav-tip, .w3nav-btn:focus-visible .w3nav-tip { opacity: 1; transform: translate(-50%, 0); }
        .w3nav-btn:focus-visible { outline: 2px solid #1e293b; outline-offset: 3px; }
        @media (hover: hover) and (pointer: fine) {
          .w3nav-btn:hover .w3nav-bg { opacity: 1; }
          .w3nav-btn:hover .w3nav-tip { opacity: 1; transform: translate(-50%, 0); }
        }
        @media (prefers-reduced-motion: reduce) { .w3nav-btn { transform: none !important; will-change: auto; } .w3nav-bg, .w3nav-tip { transition: none; } }
      `}</style>
      <div className="mx-auto max-w-xl rounded-3xl bg-white px-6 pb-8 pt-8 shadow-[0_1px_2px_rgba(15,23,42,0.06),0_12px_40px_rgba(15,23,42,0.08)] sm:px-10">
        <p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-600">Thursday, 14 March</p>
        <h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-900">Site visit, Larch Street</h2>
        <p className="mt-3 text-base leading-relaxed text-slate-600">Measure the back stairs again before ordering treads. Ask Mira about the lead time on the oak.</p>
        <div className="mt-6 space-y-2" aria-hidden="true">
          <div className="h-2.5 w-11/12 rounded-full bg-slate-100" />
          <div className="h-2.5 w-3/4 rounded-full bg-slate-100" />
        </div>
        <nav ref={bar} aria-label="Workspace" className="mx-auto mt-12 flex w-fit items-center gap-1 rounded-full border border-slate-200 bg-slate-50 p-2 sm:gap-2">
          {tools.map((t, i) => (
            <a
              key={t.label}
              ref={(n) => { btns.current[i] = n; }}
              href="#"
              aria-label={t.label}
              aria-current={i === 0 ? "page" : undefined}
              className="w3nav-btn relative flex h-12 w-12 items-center justify-center rounded-full text-slate-700"
            >
              <span aria-hidden="true" className="w3nav-bg absolute inset-0 rounded-full bg-white shadow-[0_4px_14px_rgba(15,23,42,0.14)]" />
              {i === 0 && <span aria-hidden="true" className="absolute bottom-1 h-1 w-1 rounded-full bg-slate-900" />}
              <svg aria-hidden="true" viewBox="0 0 24 24" className="relative h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d={t.d} /></svg>
              <span aria-hidden="true" className="w3nav-tip pointer-events-none absolute -top-9 left-1/2 whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs font-medium text-white">{t.label}</span>
            </a>
          ))}
        </nav>
      </div>
    </section>
  );
}
```
