174 lines
3.7 KiB
TypeScript
174 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useEffect, type ReactNode } from "react";
|
|
|
|
interface CinematicSectionProps {
|
|
children: ReactNode;
|
|
className?: string;
|
|
effect?: "fade-scale" | "clip-reveal" | "parallax-depth" | "rotate-in";
|
|
id?: string;
|
|
}
|
|
|
|
export default function CinematicSection({
|
|
children,
|
|
className = "",
|
|
effect = "fade-scale",
|
|
id,
|
|
}: CinematicSectionProps) {
|
|
const sectionRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = sectionRef.current;
|
|
if (!el) return;
|
|
|
|
// Check reduced motion preference
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
let cleanup: (() => void) | undefined;
|
|
|
|
const setup = async () => {
|
|
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
|
|
import("gsap"),
|
|
import("gsap/ScrollTrigger"),
|
|
]);
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
switch (effect) {
|
|
case "fade-scale": {
|
|
gsap.set(el, {
|
|
opacity: 0,
|
|
scale: 0.92,
|
|
transformOrigin: "center center",
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
scrollTrigger: {
|
|
trigger: el,
|
|
start: "top 85%",
|
|
end: "top 20%",
|
|
scrub: 0.8,
|
|
},
|
|
});
|
|
|
|
tl.to(el, {
|
|
opacity: 1,
|
|
scale: 1,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
cleanup = () => {
|
|
tl.scrollTrigger?.kill();
|
|
tl.kill();
|
|
};
|
|
break;
|
|
}
|
|
|
|
case "clip-reveal": {
|
|
gsap.set(el, {
|
|
clipPath: "inset(15% 5% 15% 5%)",
|
|
opacity: 0.3,
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
scrollTrigger: {
|
|
trigger: el,
|
|
start: "top 80%",
|
|
end: "top 15%",
|
|
scrub: 1,
|
|
},
|
|
});
|
|
|
|
tl.to(el, {
|
|
clipPath: "inset(0% 0% 0% 0%)",
|
|
opacity: 1,
|
|
ease: "power3.out",
|
|
});
|
|
|
|
cleanup = () => {
|
|
tl.scrollTrigger?.kill();
|
|
tl.kill();
|
|
};
|
|
break;
|
|
}
|
|
|
|
case "parallax-depth": {
|
|
gsap.set(el, {
|
|
y: 80,
|
|
opacity: 0,
|
|
rotateX: 4,
|
|
transformPerspective: 1200,
|
|
transformOrigin: "center bottom",
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
scrollTrigger: {
|
|
trigger: el,
|
|
start: "top 90%",
|
|
end: "top 30%",
|
|
scrub: 0.6,
|
|
},
|
|
});
|
|
|
|
tl.to(el, {
|
|
y: 0,
|
|
opacity: 1,
|
|
rotateX: 0,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
cleanup = () => {
|
|
tl.scrollTrigger?.kill();
|
|
tl.kill();
|
|
};
|
|
break;
|
|
}
|
|
|
|
case "rotate-in": {
|
|
gsap.set(el, {
|
|
opacity: 0,
|
|
rotateY: -5,
|
|
x: -60,
|
|
transformPerspective: 1000,
|
|
transformOrigin: "left center",
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
scrollTrigger: {
|
|
trigger: el,
|
|
start: "top 80%",
|
|
end: "top 25%",
|
|
scrub: 0.8,
|
|
},
|
|
});
|
|
|
|
tl.to(el, {
|
|
opacity: 1,
|
|
rotateY: 0,
|
|
x: 0,
|
|
ease: "power3.out",
|
|
});
|
|
|
|
cleanup = () => {
|
|
tl.scrollTrigger?.kill();
|
|
tl.kill();
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
void setup();
|
|
|
|
return () => {
|
|
cleanup?.();
|
|
};
|
|
}, [effect]);
|
|
|
|
return (
|
|
<div ref={sectionRef} className={className} id={id}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|