Files
ArtSplash/app/components/Categories.tsx
2026-02-21 19:51:12 +05:30

281 lines
7.9 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { Palette, Monitor, FileImage, ArrowRight, LucideIcon } from "lucide-react";
import Link from "next/link";
import gsap from "gsap";
const categories = [
{
icon: Monitor,
title: "Digital Art",
description:
"Create stunning digital masterpieces using any software of your choice.",
color: "from-violet-500 to-purple-600",
bgColor: "bg-violet-500",
},
{
icon: Palette,
title: "Paintings",
description:
"Traditional paintings in any medium - oil, acrylic, watercolor.",
color: "from-orange-500 to-red-500",
bgColor: "bg-orange-500",
},
{
icon: FileImage,
title: "Poster Making",
description:
"Design impactful posters that communicate powerful messages.",
color: "from-lime-500 to-green-500",
bgColor: "bg-lime-500",
},
];
// BounceCards Component for Category Cards
interface CategoryCard {
icon: LucideIcon;
title: string;
description: string;
color: string;
bgColor: string;
}
interface BounceCardsProps {
className?: string;
cards: CategoryCard[];
containerWidth?: number;
containerHeight?: number;
animationDelay?: number;
animationStagger?: number;
easeType?: string;
transformStyles?: string[];
enableHover?: boolean;
}
function BounceCards({
className = "",
cards,
containerWidth = 500,
containerHeight = 400,
animationDelay = 0.5,
animationStagger = 0.08,
easeType = "elastic.out(1, 0.8)",
transformStyles = [
"rotate(-8deg) translate(-200px)",
"rotate(0deg)",
"rotate(8deg) translate(200px)",
],
enableHover = true,
}: BounceCardsProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.fromTo(
".card",
{ scale: 0 },
{
scale: 1,
stagger: animationStagger,
ease: easeType,
delay: animationDelay,
}
);
}, containerRef);
return () => ctx.revert();
}, [animationDelay, animationStagger, easeType]);
const getNoRotationTransform = (transformStr: string): string => {
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
if (hasRotate) {
return transformStr.replace(/rotate\([\s\S]*?\)/, "rotate(0deg)");
} else if (transformStr === "none") {
return "rotate(0deg)";
} else {
return `${transformStr} rotate(0deg)`;
}
};
const getPushedTransform = (
baseTransform: string,
offsetX: number
): string => {
const translateRegex = /translate\(([-0-9.]+)px\)/;
const match = baseTransform.match(translateRegex);
if (match) {
const currentX = parseFloat(match[1]);
const newX = currentX + offsetX;
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
} else {
return baseTransform === "none"
? `translate(${offsetX}px)`
: `${baseTransform} translate(${offsetX}px)`;
}
};
const pushSiblings = (hoveredIdx: number) => {
const q = gsap.utils.selector(containerRef);
if (!enableHover || !containerRef.current) return;
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
if (i === hoveredIdx) {
const noRotation = getNoRotationTransform(baseTransform);
gsap.to(selector, {
transform: noRotation,
zIndex: 10,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
} else {
const offsetX = i < hoveredIdx ? -80 : 80;
const pushedTransform = getPushedTransform(baseTransform, offsetX);
const distance = Math.abs(hoveredIdx - i);
const delay = distance * 0.05;
gsap.to(selector, {
transform: pushedTransform,
zIndex: 1,
duration: 0.4,
ease: "back.out(1.4)",
delay,
overwrite: "auto",
});
}
});
};
const resetSiblings = () => {
if (!enableHover || !containerRef.current) return;
const q = gsap.utils.selector(containerRef);
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
gsap.to(selector, {
transform: baseTransform,
zIndex: cards.length - i,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
});
};
return (
<div
className={`relative flex items-center justify-center ${className}`}
ref={containerRef}
style={{
width: containerWidth,
height: containerHeight,
}}
>
{cards.map((card, idx) => (
<div
key={idx}
className={`card card-${idx} absolute w-[220px] h-[280px] border-4 border-zinc-800 rounded-[24px] overflow-hidden bg-zinc-900 p-6 flex flex-col cursor-pointer`}
style={{
boxShadow: "0 8px 40px rgba(0, 0, 0, 0.5)",
transform: transformStyles[idx] || "none",
zIndex: cards.length - idx,
}}
onMouseEnter={() => pushSiblings(idx)}
onMouseLeave={resetSiblings}
>
{/* Icon */}
<div
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center mb-4 flex-shrink-0`}
>
<card.icon className="w-7 h-7 text-white" />
</div>
{/* Title */}
<h3 className="text-xl font-bold text-white mb-2">{card.title}</h3>
{/* Description */}
<p className="text-zinc-400 text-sm leading-relaxed flex-grow">
{card.description}
</p>
{/* Submit Link */}
<div className="mt-4 pt-4 border-t border-zinc-800">
<Link
href="/submit"
className="inline-flex items-center gap-2 text-lime-400 text-sm font-semibold hover:text-lime-300 transition-colors"
>
Submit Now
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
))}
</div>
);
}
export default function Categories() {
const transformStyles = [
"rotate(-8deg) translate(-220px)",
"rotate(0deg)",
"rotate(8deg) translate(220px)",
];
return (
<section className="py-24 bg-black overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-4">
CATEGORIES
</span>
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Three Ways to
<span className="text-lime-400"> Express</span>
</h2>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Choose your canvas, unleash your creativity. Each category offers a
unique way to showcase your artistic vision.
</p>
</motion.div>
{/* BounceCards with Category Cards */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex justify-center"
>
<BounceCards
cards={categories}
containerWidth={700}
containerHeight={380}
animationDelay={0.3}
animationStagger={0.1}
easeType="elastic.out(1, 0.8)"
transformStyles={transformStyles}
enableHover={true}
/>
</motion.div>
</div>
</section>
);
}