"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { motion, useMotionValue, useSpring } from "framer-motion"; export default function CustomCursor() { const [isHovering, setIsHovering] = useState(false); const [isClicking, setIsClicking] = useState(false); const [hoverText, setHoverText] = useState(""); const [isVisible, setIsVisible] = useState(false); const cursorX = useMotionValue(0); const cursorY = useMotionValue(0); const springConfig = { damping: 25, stiffness: 250, mass: 0.5 }; const x = useSpring(cursorX, springConfig); const y = useSpring(cursorY, springConfig); const ringSpringConfig = { damping: 20, stiffness: 150, mass: 0.8 }; const ringX = useSpring(cursorX, ringSpringConfig); const ringY = useSpring(cursorY, ringSpringConfig); const rafRef = useRef(0); const handleMouseMove = useCallback( (e: MouseEvent) => { if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(() => { cursorX.set(e.clientX); cursorY.set(e.clientY); if (!isVisible) setIsVisible(true); }); }, [cursorX, cursorY, isVisible] ); useEffect(() => { // Only show custom cursor on desktop with fine pointer const hasFinePointer = window.matchMedia("(pointer: fine)").matches; if (!hasFinePointer) return; const handleDown = () => setIsClicking(true); const handleUp = () => setIsClicking(false); const handleLeave = () => setIsVisible(false); const handleEnter = () => setIsVisible(true); window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mousedown", handleDown); window.addEventListener("mouseup", handleUp); document.addEventListener("mouseleave", handleLeave); document.addEventListener("mouseenter", handleEnter); // Detect hoverable elements const observer = new MutationObserver(() => { attachHoverListeners(); }); observer.observe(document.body, { childList: true, subtree: true }); attachHoverListeners(); function attachHoverListeners() { const hoverables = document.querySelectorAll( 'a, button, [role="button"], input, textarea, select, [data-cursor]' ); for (const el of hoverables) { el.addEventListener("mouseenter", handleElementEnter as EventListener); el.addEventListener("mouseleave", handleElementLeave as EventListener); } } function handleElementEnter(e: Event) { const el = e.currentTarget as HTMLElement; setIsHovering(true); const cursorData = el.getAttribute("data-cursor"); if (cursorData) setHoverText(cursorData); } function handleElementLeave() { setIsHovering(false); setHoverText(""); } return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mousedown", handleDown); window.removeEventListener("mouseup", handleUp); document.removeEventListener("mouseleave", handleLeave); document.removeEventListener("mouseenter", handleEnter); observer.disconnect(); if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [handleMouseMove]); // Don't render on touch devices if (typeof window !== "undefined" && !window.matchMedia("(pointer: fine)").matches) { return null; } return ( <> {/* Dot cursor */}
{/* Ring cursor */}
{hoverText && ( {hoverText} )}
); }