86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef } from 'react';
|
|
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: string;
|
|
className?: string;
|
|
}
|
|
|
|
export default function StatCard({ title, value, className = "" }: StatCardProps) {
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
const x = useMotionValue(0);
|
|
const y = useMotionValue(0);
|
|
|
|
// Spring animations for smoother movement
|
|
const springX = useSpring(x, { stiffness: 150, damping: 20 });
|
|
const springY = useSpring(y, { stiffness: 150, damping: 20 });
|
|
|
|
// Transform mouse movement to rotation values
|
|
const rotateXOutput = useTransform(springY, [-0.5, 0.5], ["7deg", "-7deg"]);
|
|
const rotateYOutput = useTransform(springX, [-0.5, 0.5], ["-7deg", "7deg"]);
|
|
|
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!cardRef.current) return;
|
|
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
|
|
// Calculate mouse position relative to card (0-1)
|
|
const mouseX = (e.clientX - rect.left) / rect.width;
|
|
const mouseY = (e.clientY - rect.top) / rect.height;
|
|
|
|
// Convert to -0.5 to 0.5 range
|
|
x.set(mouseX - 0.5);
|
|
y.set(mouseY - 0.5);
|
|
};
|
|
|
|
const handleMouseEnter = () => {
|
|
setIsHovered(true);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
// Reset to neutral position when mouse leaves
|
|
x.set(0);
|
|
y.set(0);
|
|
setIsHovered(false);
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={cardRef}
|
|
className={`relative overflow-hidden rounded-xl p-6 sm:p-8 bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 shadow-lg flex flex-col justify-center group ${className}`}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
style={{
|
|
rotateX: rotateXOutput,
|
|
rotateY: rotateYOutput,
|
|
transformStyle: "preserve-3d",
|
|
}}
|
|
whileHover={{ scale: 1.03 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
{/* Ambient light reflection */}
|
|
<div
|
|
className={`absolute inset-0 w-full h-full bg-gradient-to-tr from-pink-500/10 to-purple-500/5 transition-opacity duration-300 ${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
|
/>
|
|
|
|
{/* Main content with 3D effect */}
|
|
<div style={{ transform: "translateZ(20px)" }} className="relative z-10">
|
|
<p className="text-gray-400 text-xs sm:text-sm mb-1 font-medium">{title}</p>
|
|
<p className="text-2xl sm:text-3xl md:text-4xl font-bold text-white">{value}</p>
|
|
</div>
|
|
|
|
{/* Shiny shimmer effect */}
|
|
<div className="absolute inset-0 rounded-xl overflow-hidden">
|
|
<div
|
|
className={`absolute inset-0 w-[200%] bg-gradient-to-r from-transparent via-pink-500/20 to-transparent -translate-x-[100%] animate-shimmer transition-opacity duration-300 ${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|