Velocity UI
Loading…
Menu

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-cursor

Source 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>
    )
}