Magnetic Cursor Dock
macOS-style dock with magnetic cursor effect. Items scale and lift as cursor approaches.
Last updated Mar 5, 2026
Component
"use client";
import {
motion,
useMotionValue,
useTransform,
} from "framer-motion";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface MagneticCursorDockItemProps {
children: React.ReactNode;
mousePosRef: React.MutableRefObject<{
x: number;
y: number;
}>;
dockRef: React.RefObject<HTMLDivElement | null>;
maxDist?: number;
scaleFactor?: number;
liftAmount?: number;
}
function MagneticCursorDockItem({
children,
mousePosRef,
dockRef,
maxDist = 90,
scaleFactor = 0.6,
liftAmount = 16,
}: MagneticCursorDockItemProps) {
const ref = useRef<HTMLDivElement>(null);
const scale = useMotionValue(1);
const y = useMotionValue(0);
const zIndex = useTransform(scale, (s) =>
s > 1 ? Math.round(10 + (s - 1) * 100) : 0,
);
useEffect(() => {
const el = ref.current;
const dock = dockRef.current;
if (!(el && dock)) {
return;
}
const raf = () => {
if (!(el && dock)) {
return;
}
const rect = dock.getBoundingClientRect();
const itemRect = el.getBoundingClientRect();
const itemCx =
itemRect.left - rect.left + itemRect.width / 2;
const itemCy =
itemRect.top - rect.top + itemRect.height / 2;
const mx = mousePosRef.current.x;
const my = mousePosRef.current.y;
const dx = mx - itemCx;
const dy = my - itemCy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < maxDist) {
const factor = 1 - dist / maxDist;
scale.set(1 + factor * scaleFactor);
y.set(-factor * liftAmount);
} else {
scale.set(1);
y.set(0);
}
requestAnimationFrame(raf);
};
const id = requestAnimationFrame(raf);
return () => cancelAnimationFrame(id);
}, [
dockRef,
mousePosRef,
maxDist,
scaleFactor,
liftAmount,
scale.set,
y.set,
]);
return (
<motion.div
ref={ref}
style={{ scale, y, zIndex }}
className="isolate origin-bottom"
>
{children}
</motion.div>
);
}
interface MagneticCursorDockProps {
children: React.ReactNode;
className?: string;
dockClassName?: string;
maxDist?: number;
scaleFactor?: number;
liftAmount?: number;
}
export function MagneticCursorDock({
children,
className,
dockClassName,
maxDist = 90,
scaleFactor = 0.6,
liftAmount = 16,
}: MagneticCursorDockProps) {
const dockRef = useRef<HTMLDivElement>(null);
const mousePos = useRef({ x: -9999, y: -9999 });
useEffect(() => {
const dock = dockRef.current;
if (!dock) {
return;
}
const handleMove = (e: MouseEvent) => {
const rect = dock.getBoundingClientRect();
mousePos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
const handleLeave = () => {
mousePos.current = { x: -9999, y: -9999 };
};
dock.addEventListener("mousemove", handleMove);
dock.addEventListener("mouseleave", handleLeave);
return () => {
dock.removeEventListener("mousemove", handleMove);
dock.removeEventListener("mouseleave", handleLeave);
};
}, []);
return (
<div
ref={dockRef}
className={cn(
"relative flex cursor-default items-end justify-center gap-2 rounded-2xl border border-white/10 bg-black/40 p-4 backdrop-blur-xl",
dockClassName,
className,
)}
>
{React.Children.map(children, (child, i) =>
child ? (
<MagneticCursorDockItem
key={
React.isValidElement(child) &&
child.key != null
? child.key
: `dock-${i}`
}
mousePosRef={mousePos}
dockRef={dockRef}
maxDist={maxDist}
scaleFactor={scaleFactor}
liftAmount={liftAmount}
>
{child}
</MagneticCursorDockItem>
) : null,
)}
</div>
);
}Installation
pnpm dlx shadcn@latest add "https://pulkitxm.com/components/magnetic-cursor-dock.json"1. Install dependencies
pnpm add framer-motion2. Copy the component file
"use client";
import {
motion,
useMotionValue,
useTransform,
} from "framer-motion";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface MagneticCursorDockItemProps {
children: React.ReactNode;
mousePosRef: React.MutableRefObject<{
x: number;
y: number;
}>;
dockRef: React.RefObject<HTMLDivElement | null>;
maxDist?: number;
scaleFactor?: number;
liftAmount?: number;
}
function MagneticCursorDockItem({
children,
mousePosRef,
dockRef,
maxDist = 90,
scaleFactor = 0.6,
liftAmount = 16,
}: MagneticCursorDockItemProps) {
const ref = useRef<HTMLDivElement>(null);
const scale = useMotionValue(1);
const y = useMotionValue(0);
const zIndex = useTransform(scale, (s) =>
s > 1 ? Math.round(10 + (s - 1) * 100) : 0,
);
useEffect(() => {
const el = ref.current;
const dock = dockRef.current;
if (!(el && dock)) {
return;
}
const raf = () => {
if (!(el && dock)) {
return;
}
const rect = dock.getBoundingClientRect();
const itemRect = el.getBoundingClientRect();
const itemCx =
itemRect.left - rect.left + itemRect.width / 2;
const itemCy =
itemRect.top - rect.top + itemRect.height / 2;
const mx = mousePosRef.current.x;
const my = mousePosRef.current.y;
const dx = mx - itemCx;
const dy = my - itemCy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < maxDist) {
const factor = 1 - dist / maxDist;
scale.set(1 + factor * scaleFactor);
y.set(-factor * liftAmount);
} else {
scale.set(1);
y.set(0);
}
requestAnimationFrame(raf);
};
const id = requestAnimationFrame(raf);
return () => cancelAnimationFrame(id);
}, [
dockRef,
mousePosRef,
maxDist,
scaleFactor,
liftAmount,
scale.set,
y.set,
]);
return (
<motion.div
ref={ref}
style={{ scale, y, zIndex }}
className="isolate origin-bottom"
>
{children}
</motion.div>
);
}
interface MagneticCursorDockProps {
children: React.ReactNode;
className?: string;
dockClassName?: string;
maxDist?: number;
scaleFactor?: number;
liftAmount?: number;
}
export function MagneticCursorDock({
children,
className,
dockClassName,
maxDist = 90,
scaleFactor = 0.6,
liftAmount = 16,
}: MagneticCursorDockProps) {
const dockRef = useRef<HTMLDivElement>(null);
const mousePos = useRef({ x: -9999, y: -9999 });
useEffect(() => {
const dock = dockRef.current;
if (!dock) {
return;
}
const handleMove = (e: MouseEvent) => {
const rect = dock.getBoundingClientRect();
mousePos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
const handleLeave = () => {
mousePos.current = { x: -9999, y: -9999 };
};
dock.addEventListener("mousemove", handleMove);
dock.addEventListener("mouseleave", handleLeave);
return () => {
dock.removeEventListener("mousemove", handleMove);
dock.removeEventListener("mouseleave", handleLeave);
};
}, []);
return (
<div
ref={dockRef}
className={cn(
"relative flex cursor-default items-end justify-center gap-2 rounded-2xl border border-white/10 bg-black/40 p-4 backdrop-blur-xl",
dockClassName,
className,
)}
>
{React.Children.map(children, (child, i) =>
child ? (
<MagneticCursorDockItem
key={
React.isValidElement(child) &&
child.key != null
? child.key
: `dock-${i}`
}
mousePosRef={mousePos}
dockRef={dockRef}
maxDist={maxDist}
scaleFactor={scaleFactor}
liftAmount={liftAmount}
>
{child}
</MagneticCursorDockItem>
) : null,
)}
</div>
);
}3. Import and use
import { MagneticCursorDock } from "@/components/magnetic-cursor-dock";
<MagneticCursorDock>{/* dock items */}</MagneticCursorDock>;Usage
Import
Add the MagneticCursorDock import.
import { MagneticCursorDock } from "@/components/magnetic-cursor-dock";Use
Wrap your dock items.
<MagneticCursorDock>{/* items */}</MagneticCursorDock>;Guidelines
- Pass children as dock items. Each child gets the magnetic effect.
- Use maxDist, scaleFactor, liftAmount to tune the effect.
Props
All props are optional unless marked required. Use these to customize every aspect of the component.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional CSS classes for the dock container. |
| dockClassName | string | — | Additional classes for the dock (merged with className). |
| maxDist | number | 90 | Max distance (px) for magnetic effect. |
| scaleFactor | number | 1.2 | Scale factor when cursor is near. |
| liftAmount | number | 8 | Lift amount (px) when cursor is near. |
Examples
Basic
Basic dock with icon items.
import { MagneticCursorDock } from "@/components/magnetic-cursor-dock";
import { Home, Search, Mail } from "lucide-react";
<MagneticCursorDock className="h-24">
<div className="flex size-12 items-center justify-center rounded-xl bg-zinc-800">
<Home className="size-6 text-white" />
</div>
<div className="flex size-12 items-center justify-center rounded-xl bg-zinc-800">
<Search className="size-6 text-white" />
</div>
<div className="flex size-12 items-center justify-center rounded-xl bg-zinc-800">
<Mail className="size-6 text-white" />
</div>
</MagneticCursorDock>;