AnimatedMotion
Magnetic
Wraps anything so it leans toward a hovering mouse pointer and springs back when it leaves.
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/magnetic.jsonAlso installs the theme, cn() and motion-scope. Packages: clsx, tailwind-merge, motion.
Usage
magnetic-demo.tsx
"use client";
import { useState } from "react";
import { Button } from "@/registry/kit/ui/button";
import { Magnetic } from "@/registry/kit/ui/magnetic";
export default function MagneticDemo() {
const [playing, setPlaying] = useState(false);
return (
<div className="flex flex-wrap items-center justify-center gap-10">
<Magnetic>
<Button size="lg" onClick={() => setPlaying((p) => !p)}>
{playing ? "Pause the tour" : "Watch the 2-minute tour"}
</Button>
</Magnetic>
<Magnetic strength={0.5}>
<Button
variant="solid"
size="icon"
aria-label="Play tour"
aria-pressed={playing}
onClick={() => setPlaying((p) => !p)}
className="size-16"
>
<svg viewBox="0 0 24 24" className="size-5 translate-x-px" fill="currentColor" aria-hidden>
{playing ? (
<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" />
) : (
<path d="M8 5.5v13a1 1 0 0 0 1.5.86l11-6.5a1 1 0 0 0 0-1.72l-11-6.5A1 1 0 0 0 8 5.5Z" />
)}
</svg>
</Button>
</Magnetic>
</div>
);
}API
strength= 0.3
numberFraction of the pointer's offset from centre to follow.
childrenReactNodeContent to move.
Source
components/ui/magnetic.tsx
"use client";
import { m, useMotionValue, useSpring } from "motion/react";
import type { PointerEvent } from "react";
import { MotionScope } from "@/registry/kit/lib/motion";
import { cn } from "@/registry/kit/lib/utils";
const spring = { stiffness: 170, damping: 20, mass: 0.5 };
export interface MagneticProps {
children: React.ReactNode;
/** How far the content follows the pointer, as a fraction of its offset from centre. */
strength?: number;
/** Farthest the content moves, in px. Keep it under the gap to the next item so it never slides under a neighbour. */
max?: number;
className?: string;
}
const clamp = (value: number, max: number) => Math.max(-max, Math.min(max, value));
/** Pulls its content toward a hovering mouse pointer and springs back on leave. */
export function Magnetic({ children, strength = 0.18, max = 6, className }: MagneticProps) {
const x = useMotionValue(0);
const y = useMotionValue(0);
const springX = useSpring(x, spring);
const springY = useSpring(y, spring);
function onPointerMove(event: PointerEvent<HTMLSpanElement>) {
if (event.pointerType !== "mouse") return;
const rect = event.currentTarget.getBoundingClientRect();
x.set(clamp((event.clientX - rect.left - rect.width / 2) * strength, max));
y.set(clamp((event.clientY - rect.top - rect.height / 2) * strength, max));
}
function reset() {
x.set(0);
y.set(0);
}
return (
<MotionScope>
<m.span
className={cn("relative inline-block hover:z-10", className)}
style={{ x: springX, y: springY }}
onPointerMove={onPointerMove}
onPointerLeave={reset}
>
{children}
</m.span>
</MotionScope>
);
}