87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { motion, useMotionValue, useSpring } from "framer-motion";
|
|
|
|
export default function CustomCursor() {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
// Motion values for smooth cursor tracking
|
|
const cursorX = useMotionValue(-100);
|
|
const cursorY = useMotionValue(-100);
|
|
|
|
// Spring physics for trailing effect on the outer circle
|
|
const springConfig = { damping: 30, stiffness: 220, mass: 0.6 };
|
|
const cursorXSpring = useSpring(cursorX, springConfig);
|
|
const cursorYSpring = useSpring(cursorY, springConfig);
|
|
|
|
useEffect(() => {
|
|
const moveCursor = (e: MouseEvent) => {
|
|
cursorX.set(e.clientX);
|
|
cursorY.set(e.clientY);
|
|
};
|
|
|
|
const handleMouseOver = (e: MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if (
|
|
target.tagName === "A" ||
|
|
target.tagName === "BUTTON" ||
|
|
target.closest("a") ||
|
|
target.closest("button") ||
|
|
target.classList.contains("clickable") ||
|
|
target.closest(".clickable")
|
|
) {
|
|
setIsHovered(true);
|
|
} else {
|
|
setIsHovered(false);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("mousemove", moveCursor);
|
|
window.addEventListener("mouseover", handleMouseOver);
|
|
|
|
return () => {
|
|
window.removeEventListener("mousemove", moveCursor);
|
|
window.removeEventListener("mouseover", handleMouseOver);
|
|
};
|
|
}, [cursorX, cursorY]);
|
|
|
|
return (
|
|
<div className="custom-cursor-container pointer-events-none fixed inset-0 z-[9999] hidden lg:block">
|
|
{/* Outer Spring Ring */}
|
|
<motion.div
|
|
className="custom-cursor pointer-events-none fixed left-0 top-0"
|
|
style={{
|
|
x: cursorXSpring,
|
|
y: cursorYSpring,
|
|
translateX: "-50%",
|
|
translateY: "-50%",
|
|
}}
|
|
animate={{
|
|
width: isHovered ? 64 : 24,
|
|
height: isHovered ? 64 : 24,
|
|
backgroundColor: isHovered ? "rgba(212, 175, 55, 0.15)" : "rgba(212, 175, 55, 0)",
|
|
borderColor: isHovered ? "#F3E5AB" : "#D4AF37",
|
|
boxShadow: isHovered ? "0 0 20px rgba(225, 177, 44, 0.4)" : "none",
|
|
}}
|
|
transition={{ type: "tween", ease: "backOut", duration: 0.2 }}
|
|
/>
|
|
{/* Inner Pinpoint Dot */}
|
|
<motion.div
|
|
className="custom-cursor-dot pointer-events-none fixed left-0 top-0"
|
|
style={{
|
|
x: cursorX,
|
|
y: cursorY,
|
|
translateX: "-50%",
|
|
translateY: "-50%",
|
|
}}
|
|
animate={{
|
|
scale: isHovered ? 0 : 1,
|
|
backgroundColor: isHovered ? "#F3E5AB" : "#D4AF37",
|
|
}}
|
|
transition={{ duration: 0.1 }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|