"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useSpring,
} from "framer-motion";
import { cn } from "@/lib/utils";
const MAX_ROTATION = 25;
const STIFF_DAMPING = 30;
const STIFFNESS = 500;
type Props = {
children: React.ReactNode;
outerBackground?: string;
};
/**
* FollowCursorCard component.
*
* This component creates a card that follows the cursor's movement on the screen.
*
* @param {React.ReactNode} props.children - The content of the card.
* @param {string} [props.outerBackground="bg-gradient-to-br from-yellow-500 to-orange-400"] - The background color of the outer card in tailwind classes.
*
*/
export const FollowCursorCard = ({
children,
outerBackground = "bg-gradient-to-br from-yellow-500 to-orange-400",
}: Props) => {
const ref = useRef<HTMLDivElement | null>(null);
const childRef = useRef<HTMLDivElement | null>(null);
const [outerDimensions, setOuterDimensions] = useState({
width: 0,
height: 0,
});
const x = useMotionValue(0);
const y = useMotionValue(0);
const xSpring = useSpring(x, {
stiffness: STIFFNESS,
damping: STIFF_DAMPING,
});
const ySpring = useSpring(y, {
stiffness: STIFFNESS,
damping: STIFF_DAMPING,
});
const transform = useMotionTemplate`rotateX(${xSpring}deg) rotateY(${ySpring}deg)`;
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!ref.current) return;
const width = window.innerWidth;
const height = window.innerHeight;
const mouseX = (e.clientX - width / 2) / width;
const mouseY = (e.clientY - height / 2) / height;
let rX = mouseY * -MAX_ROTATION;
let rY = mouseX * MAX_ROTATION;
rX = Math.max(Math.min(rX, MAX_ROTATION), -MAX_ROTATION);
rY = Math.max(Math.min(rY, MAX_ROTATION), -MAX_ROTATION);
x.set(rX);
y.set(rY);
},
[ref, x, y]
);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove]);
useEffect(() => {
if (childRef.current) {
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
setOuterDimensions({
width: width + 50,
height: height + 50,
});
}
});
resizeObserver.observe(childRef.current);
return () => {
resizeObserver.disconnect();
};
}
}, [children]);
return (
<motion.div
ref={ref}
style={{
transformStyle: "preserve-3d",
transform,
width: outerDimensions.width,
height: outerDimensions.height,
}}
className={cn(
outerBackground,
"relative flex items-center justify-center rounded-lg"
)}
>
<div
ref={childRef}
style={{
transform: "translateZ(75px)",
transformStyle: "preserve-3d",
}}
className="z-10"
>
{children}
</div>
</motion.div>
);
};