Magnetic Cursor
Magnetic Cursor component with smooth animations and modern UI.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add magnetic-cursorSource Code
magnetic-cursor.tsx
'use client'
import React, { useEffect, useState, useRef } from 'react'
import { motion, useMotionValue, useSpring, AnimatePresence } from 'framer-motion'
import { cn } from '@/lib/utils'
export default function MagneticCursorPreview() {
return (
<div className="min-h-[600px] w-full bg-neutral-100 dark:bg-neutral-950 flex flex-col items-center justify-center relative overflow-hidden cursor-none">
<MagneticCursor />
<div className="z-10 flex flex-col items-center gap-12">
<div className="text-center space-y-2">
<h2 className="text-3xl font-bold text-neutral-800 dark:text-white" data-magnetic>Magnetic Cursor</h2>
<p className="text-neutral-500 max-w-md mx-auto">
A premium cursor interaction that snaps to interactive elements.
Add <code className="bg-neutral-200 dark:bg-neutral-800 px-1 rounded">data-magnetic</code> to any element.
</p>
</div>
<div className="flex gap-8 items-center">
<button
data-magnetic
className="px-8 py-3 rounded-full bg-indigo-600 text-white font-medium hover:bg-indigo-700 transition-colors"
>
Hover Me
</button>
<button
data-magnetic
className="h-16 w-16 rounded-full bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 flex items-center justify-center shadow-lg"
>
<span className="text-2xl">✨</span>
</button>
<a href="#" data-magnetic className="text-indigo-600 font-medium underline underline-offset-4">
Link Item
</a>
</div>
<div className="grid grid-cols-2 gap-4">
{[1, 2, 3, 4].map(i => (
<div key={i} data-magnetic className="w-24 h-24 bg-white dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 flex items-center justify-center hover:scale-95 transition-transform duration-300">
{i}
</div>
))}
</div>
</div>
{/* Grid Background */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
</div>
)
}
function MagneticCursor() {
const cursorX = useMotionValue(-100)
const cursorY = useMotionValue(-100)
const springConfig = { damping: 25, stiffness: 700 }
const cursorXSpring = useSpring(cursorX, springConfig)
const cursorYSpring = useSpring(cursorY, springConfig)
const [isHovering, setIsHovering] = useState(false)
const [hoverTarget, setHoverTarget] = useState<DOMRect | null>(null)
useEffect(() => {
const moveCursor = (e: MouseEvent) => {
// Check if hovering over a magnetic element
const target = e.target as HTMLElement
const magneticElement = target.closest('[data-magnetic]')
if (magneticElement) {
const rect = magneticElement.getBoundingClientRect()
// Snap to center of element if it's small enough, otherwise just track mouse but expanded
// For this implementation, we'll make it expand around the element
// const centerX = rect.left + rect.width / 2
// const centerY = rect.top + rect.height / 2
// cursorX.set(centerX)
// cursorY.set(centerY)
// Better approach: Cursor follows mouse but state changes
cursorX.set(e.clientX)
cursorY.set(e.clientY)
setIsHovering(true)
setHoverTarget(rect)
} else {
cursorX.set(e.clientX)
cursorY.set(e.clientY)
setIsHovering(false)
setHoverTarget(null)
}
}
window.addEventListener('mousemove', moveCursor)
return () => {
window.removeEventListener('mousemove', moveCursor)
}
}, [cursorX, cursorY])
return (
<div className="fixed top-0 left-0 w-full h-full pointer-events-none z-[9999] mix-blend-difference">
<motion.div
className={cn(
"absolute top-0 left-0 rounded-full bg-white transition-all duration-200",
isHovering ? "h-8 w-8 -ml-4 -mt-4 opacity-50" : "h-4 w-4 -ml-2 -mt-2 opacity-100"
)}
style={{
x: cursorXSpring,
y: cursorYSpring,
}}
/>
<motion.div
className={cn(
"absolute top-0 left-0 rounded-full border border-white transition-all duration-300 ease-out",
isHovering ? "border-2" : "border"
)}
style={{
x: cursorXSpring,
y: cursorYSpring,
width: isHovering ? 60 : 32,
height: isHovering ? 60 : 32,
translateX: isHovering ? -30 : -16,
translateY: isHovering ? -30 : -16,
}}
/>
</div>
)
}

