"use client"; import { motion, useMotionValue } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; 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 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} )}
); }