357 lines
9.8 KiB
TypeScript
357 lines
9.8 KiB
TypeScript
'use client';
|
|
|
|
import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
|
import { useRef, useState, type ReactNode, type MouseEvent } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const cardVariants: Variants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
y: 30,
|
|
scale: 0.95,
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
},
|
|
},
|
|
};
|
|
|
|
interface InkCardProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
hoverScale?: number;
|
|
hoverRotate?: number;
|
|
inkColor?: string;
|
|
showInkOnHover?: boolean;
|
|
}
|
|
|
|
export function InkCard({
|
|
children,
|
|
className = '',
|
|
hoverScale = 1.02,
|
|
hoverRotate = 0,
|
|
inkColor = 'rgba(196, 30, 58, 0.05)',
|
|
showInkOnHover = true,
|
|
...props
|
|
}: InkCardProps) {
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
const [inkPosition, setInkPosition] = useState({ x: 50, y: 50 });
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
|
if (!cardRef.current) return;
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
|
setInkPosition({ x, y });
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={cardRef}
|
|
variants={cardVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
whileHover={{
|
|
scale: hoverScale,
|
|
rotate: hoverRotate,
|
|
y: -4,
|
|
}}
|
|
onHoverStart={() => setIsHovered(true)}
|
|
onHoverEnd={() => setIsHovered(false)}
|
|
onMouseMove={handleMouseMove}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 300,
|
|
damping: 20,
|
|
}}
|
|
className={cn(
|
|
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl transition-shadow duration-300',
|
|
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{showInkOnHover && (
|
|
<motion.div
|
|
className="absolute inset-0 pointer-events-none"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: isHovered ? 1 : 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<div
|
|
className="absolute w-[200%] h-[200%] rounded-full"
|
|
style={{
|
|
background: `radial-gradient(circle, ${inkColor} 0%, transparent 70%)`,
|
|
left: `${inkPosition.x - 100}%`,
|
|
top: `${inkPosition.y - 100}%`,
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
<div className="relative z-10">{children}</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface GeometricCardProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
cornerColor?: string;
|
|
}
|
|
|
|
export function GeometricCard({
|
|
children,
|
|
className = '',
|
|
cornerColor = '#C41E3A',
|
|
...props
|
|
}: GeometricCardProps) {
|
|
return (
|
|
<motion.div
|
|
variants={cardVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
whileHover={{
|
|
y: -4,
|
|
transition: { type: 'spring', stiffness: 300, damping: 20 },
|
|
}}
|
|
className={cn(
|
|
'relative bg-white border border-[#E5E5E5] rounded-xl p-6 transition-all duration-300',
|
|
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
|
'group',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 rounded-tl-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
|
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 rounded-tr-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
|
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 rounded-bl-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
|
<div className="absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 rounded-br-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface FlipCardProps {
|
|
front: ReactNode;
|
|
back: ReactNode;
|
|
className?: string;
|
|
frontClassName?: string;
|
|
backClassName?: string;
|
|
}
|
|
|
|
export function FlipCard({
|
|
front,
|
|
back,
|
|
className = '',
|
|
frontClassName = '',
|
|
backClassName = '',
|
|
}: FlipCardProps) {
|
|
const [isFlipped, setIsFlipped] = useState(false);
|
|
|
|
return (
|
|
<motion.div
|
|
className={cn('relative cursor-pointer perspective-1000', className)}
|
|
onClick={() => setIsFlipped(!isFlipped)}
|
|
style={{ perspective: 1000 }}
|
|
>
|
|
<motion.div
|
|
className="relative w-full h-full"
|
|
initial={false}
|
|
animate={{ rotateY: isFlipped ? 180 : 0 }}
|
|
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
|
style={{ transformStyle: 'preserve-3d' }}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 backface-hidden',
|
|
frontClassName
|
|
)}
|
|
style={{ backfaceVisibility: 'hidden' }}
|
|
>
|
|
{front}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 backface-hidden',
|
|
backClassName
|
|
)}
|
|
style={{
|
|
backfaceVisibility: 'hidden',
|
|
transform: 'rotateY(180deg)',
|
|
}}
|
|
>
|
|
{back}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface TiltCardProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
maxTilt?: number;
|
|
scale?: number;
|
|
}
|
|
|
|
export function TiltCard({
|
|
children,
|
|
className = '',
|
|
maxTilt = 10,
|
|
scale = 1.02,
|
|
...props
|
|
}: TiltCardProps) {
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
const [rotateX, setRotateX] = useState(0);
|
|
const [rotateY, setRotateY] = useState(0);
|
|
|
|
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
|
if (!cardRef.current) return;
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const centerX = rect.left + rect.width / 2;
|
|
const centerY = rect.top + rect.height / 2;
|
|
const mouseX = e.clientX - centerX;
|
|
const mouseY = e.clientY - centerY;
|
|
|
|
const rotateXValue = (mouseY / (rect.height / 2)) * -maxTilt;
|
|
const rotateYValue = (mouseX / (rect.width / 2)) * maxTilt;
|
|
|
|
setRotateX(rotateXValue);
|
|
setRotateY(rotateYValue);
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
setRotateX(0);
|
|
setRotateY(0);
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={cardRef}
|
|
className={cn('relative', className)}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
style={{ transformStyle: 'preserve-3d' }}
|
|
animate={{
|
|
rotateX,
|
|
rotateY,
|
|
scale: rotateX || rotateY ? scale : 1,
|
|
}}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface GlowCardProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
glowColor?: string;
|
|
}
|
|
|
|
export function GlowCard({
|
|
children,
|
|
className = '',
|
|
glowColor = 'rgba(196, 30, 58, 0.15)',
|
|
...props
|
|
}: GlowCardProps) {
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
const [glowPosition, setGlowPosition] = useState({ x: 50, y: 50 });
|
|
|
|
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
|
if (!cardRef.current) return;
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
|
setGlowPosition({ x, y });
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={cardRef}
|
|
variants={cardVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
whileHover={{ y: -4 }}
|
|
onMouseMove={handleMouseMove}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
className={cn(
|
|
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl',
|
|
'transition-shadow duration-300',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<motion.div
|
|
className="absolute inset-0 pointer-events-none opacity-0 hover:opacity-100 transition-opacity duration-300"
|
|
style={{
|
|
background: `radial-gradient(circle at ${glowPosition.x}% ${glowPosition.y}%, ${glowColor} 0%, transparent 50%)`,
|
|
}}
|
|
/>
|
|
<div className="relative z-10">{children}</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
interface ExpandCardProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode;
|
|
className?: string;
|
|
expandedContent?: ReactNode;
|
|
}
|
|
|
|
export function ExpandCard({
|
|
children,
|
|
className = '',
|
|
expandedContent,
|
|
...props
|
|
}: ExpandCardProps) {
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
return (
|
|
<motion.div
|
|
variants={cardVariants}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, amount: 0.2 }}
|
|
whileHover={{ y: -4 }}
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
className={cn(
|
|
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl cursor-pointer',
|
|
'transition-shadow duration-300',
|
|
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="p-6">{children}</div>
|
|
{expandedContent && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{
|
|
height: isExpanded ? 'auto' : 0,
|
|
opacity: isExpanded ? 1 : 0,
|
|
}}
|
|
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="px-6 pb-6 border-t border-[#E5E5E5] pt-4">
|
|
{expandedContent}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
}
|