"use client"; import { useId, useState, type MouseEvent } from "react"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react"; export type MagneticMenuItem = { label: string; href: string }; type MagneticMenuProps = { items: MagneticMenuItem[]; /** href of the active link at start. Defaults to the first item. */ active?: string; /** Called on click. Call e.preventDefault() to stop the browser following the link. */ onSelect?: (item: MagneticMenuItem, e: MouseEvent) => void; orientation?: "horizontal" | "vertical"; /** How far a link follows the pointer, 0 to 1. */ strength?: number; /** Colour of the hover blob. */ color?: string; /** Colour of the dot under the active link. */ accent?: string; /** Name read out by screen readers. */ label?: string; className?: string; }; const SPRING = { stiffness: 260, damping: 18, mass: 0.4 }; /* MagneticMenu: links lean toward the pointer, and a soft blob glides to whichever link is under it. */ export function MagneticMenu({ items, active, onSelect, orientation = "horizontal", strength = 0.3, color = "rgba(167,139,250,0.18)", accent = "#a78bfa", label = "Menu", className = "text-sm text-white/70", }: MagneticMenuProps) { const id = useId(); const [hover, setHover] = useState(null); const [current, setCurrent] = useState(active ?? items[0]?.href); return ( ); } function MagnetLink({ item, on, hovered, blobId, strength, color, accent, onHover, onPick, }: { item: MagneticMenuItem; on: boolean; hovered: boolean; blobId: string; strength: number; color: string; accent: string; onHover: () => void; onPick: (e: MouseEvent) => void; }) { const reduce = useReducedMotion(); const x = useMotionValue(0); const y = useMotionValue(0); const sx = useSpring(x, SPRING); const sy = useSpring(y, SPRING); // The label moves a little further than its pill, so it feels pulled. const lx = useTransform(sx, (v) => v * 0.5); const ly = useTransform(sy, (v) => v * 0.5); return (
  • { if (reduce) return; const r = e.currentTarget.getBoundingClientRect(); x.set((e.clientX - (r.left + r.width / 2)) * strength); y.set((e.clientY - (r.top + r.height / 2)) * strength); }} onPointerLeave={() => { x.set(0); y.set(0); }} style={{ x: sx, y: sy }} className={`relative block whitespace-nowrap rounded-full px-4 py-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-white/70 ${ on || hovered ? "text-white" : "" }`} > {hovered && ( )} {item.label} {on && ( )}
  • ); }