"use client";
import { motion, useSpring } from "framer-motion";
import { FC, JSX, useEffect, useRef, useState } from "react";
// Utility function 'cn' (classnames) - implemented directly to resolve import error
function cn(...inputs: (string | undefined | null | boolean)[]) {
return inputs.filter(Boolean).join(" ");
}
interface Position {
x: number;
y: number;
}
export interface SpringConfig {
damping: number;
stiffness: number;
mass: number;
restDelta: number;
}
export interface SmoothCursorProps {
cursor?: JSX.Element;
springConfig?: SpringConfig;
className?: string;
size?: number;
color?: string;
hideOnLeave?: boolean;
trailLength?: number;
showTrail?: boolean;
rotateOnMove?: boolean;
scaleOnClick?: boolean;
glowEffect?: boolean;
magneticDistance?: number;
magneticElements?: string;
onCursorMove?: (position: Position) => void;
onCursorEnter?: () => void;
onCursorLeave?: () => void;
disabled?: boolean;
}
const DefaultCursorSVG: FC<{ size?: number; color?: string; className?: string }> = ({
size = 25,
color = "black",
className
}) => {
return (
);
};
export function SmoothCursor({
cursor,
springConfig = {
damping: 45,
stiffness: 400,
mass: 1,
restDelta: 0.001,
},
className,
size = 25,
color = "black",
hideOnLeave = true,
trailLength = 5,
showTrail = false,
rotateOnMove = true,
scaleOnClick = true,
glowEffect = false,
magneticDistance = 50,
magneticElements = "[data-magnetic]",
onCursorMove,
onCursorEnter,
onCursorLeave,
disabled = false,
}: SmoothCursorProps) {
const [isMoving, setIsMoving] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [isClicking, setIsClicking] = useState(false);
const [trail, setTrail] = useState([]);
const lastMousePos = useRef({ x: 0, y: 0 });
const velocity = useRef({ x: 0, y: 0 });
const lastUpdateTime = useRef(Date.now());
const previousAngle = useRef(0);
const accumulatedRotation = useRef(0);
const cursorX = useSpring(0, springConfig);
const cursorY = useSpring(0, springConfig);
const rotation = useSpring(0, {
...springConfig,
damping: 60,
stiffness: 300,
});
const scale = useSpring(1, {
...springConfig,
stiffness: 500,
damping: 35,
});
const defaultCursor = ;
const cursorElement = cursor || defaultCursor;
useEffect(() => {
if (disabled) return;
const updateVelocity = (currentPos: Position) => {
const currentTime = Date.now();
const deltaTime = currentTime - lastUpdateTime.current;
if (deltaTime > 0) {
velocity.current = {
x: (currentPos.x - lastMousePos.current.x) / deltaTime,
y: (currentPos.y - lastMousePos.current.y) / deltaTime,
};
}
lastUpdateTime.current = currentTime;
lastMousePos.current = currentPos;
};
const updateTrail = (pos: Position) => {
if (!showTrail) return;
setTrail(function (prev) {
var newTrail = [pos].concat(prev.slice(0, trailLength - 1));
return newTrail;
});
};
const findMagneticElement = (x: number, y: number) => {
const elements = document.querySelectorAll(magneticElements);
// Fix: Convert NodeListOf to an array for reliable iteration
for (const element of Array.from(elements)) {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distance = Math.sqrt(
Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)
);
if (distance < magneticDistance) {
return { x: centerX, y: centerY, distance };
}
}
return null;
};
const smoothMouseMove = (e: MouseEvent) => {
let currentPos = { x: e.clientX, y: e.clientY };
// Check for magnetic elements
const magneticTarget = findMagneticElement(currentPos.x, currentPos.y);
if (magneticTarget) {
const strength = 1 - (magneticTarget.distance / magneticDistance);
currentPos = {
x: currentPos.x + (magneticTarget.x - currentPos.x) * strength * 0.3,
y: currentPos.y + (magneticTarget.y - currentPos.y) * strength * 0.3,
};
}
updateVelocity(currentPos);
updateTrail(currentPos);
const speed = Math.sqrt(
Math.pow(velocity.current.x, 2) + Math.pow(velocity.current.y, 2),
);
cursorX.set(currentPos.x);
cursorY.set(currentPos.y);
onCursorMove?.(currentPos);
if (speed > 0.1 && rotateOnMove) {
const currentAngle =
Math.atan2(velocity.current.y, velocity.current.x) * (180 / Math.PI) +
90;
let angleDiff = currentAngle - previousAngle.current;
if (angleDiff > 180) angleDiff -= 360;
if (angleDiff < -180) angleDiff += 360;
accumulatedRotation.current += angleDiff;
rotation.set(accumulatedRotation.current);
previousAngle.current = currentAngle;
scale.set(0.95);
setIsMoving(true);
const timeout = setTimeout(function () {
scale.set(1);
setIsMoving(false);
}, 150);
return function () {
return clearTimeout(timeout);
};
}
};
const handleMouseEnter = function () {
setIsVisible(true);
onCursorEnter?.();
};
const handleMouseLeave = function () {
if (hideOnLeave) {
setIsVisible(false);
}
onCursorLeave?.();
};
const handleMouseDown = function () {
if (scaleOnClick) {
setIsClicking(true);
scale.set(0.8);
}
};
const handleMouseUp = function () {
if (scaleOnClick) {
setIsClicking(false);
scale.set(1);
}
};
let rafId: number;
const throttledMouseMove = function (e: MouseEvent) {
if (rafId) return;
rafId = requestAnimationFrame(function () {
smoothMouseMove(e);
rafId = 0;
});
};
document.body.style.cursor = "none";
window.addEventListener("mousemove", throttledMouseMove);
document.addEventListener("mouseenter", handleMouseEnter);
document.addEventListener("mouseleave", handleMouseLeave);
document.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
return function () {
window.removeEventListener("mousemove", throttledMouseMove);
document.removeEventListener("mouseenter", handleMouseEnter);
document.removeEventListener("mouseleave", handleMouseLeave);
document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "auto";
if (rafId) cancelAnimationFrame(rafId);
};
}, [
cursorX,
cursorY,
rotation,
scale,
disabled,
showTrail,
trailLength,
rotateOnMove,
scaleOnClick,
hideOnLeave,
magneticDistance,
magneticElements,
onCursorMove,
onCursorEnter,
onCursorLeave
]);
if (disabled || !isVisible) return null;
return (
<>
{/* Trail Effect */}
{showTrail && trail.map(function (pos, index) {
return (
);
})}
{/* Main Cursor */}
{cursorElement}
>
);
}
export default SmoothCursor;