Files
gov/components/SectionWrapper.tsx

208 lines
7.0 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:04:31 +05:00
import React, { useRef } from 'react';
import { motion, useScroll, useTransform, Variants, useInView } from 'framer-motion';
import { WarpOverlay, SandOverlay } from './VisualEffects';
export type TransitionType =
| 'warp'
| 'wipe-right'
| 'shutters'
| 'paint-ripple'
| 'sand'
| 'slide'
| 'gradient';
interface Props {
children: React.ReactNode;
id: string;
className?: string;
transparent?: boolean;
transitionEffect?: TransitionType;
fitScreen?: boolean;
}
const SectionWrapper: React.FC<Props> = ({
children,
id,
className = "",
transparent = false,
transitionEffect = 'slide',
fitScreen = true
}) => {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { amount: 0.2 });
// Parallax Logic
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], ["5%", "-5%"]);
const smoothEase: [number, number, number, number] = [0.645, 0.045, 0.355, 1.000];
// --- RENDER OVERLAYS BASED ON TYPE ---
const renderOverlay = () => {
switch (transitionEffect) {
case 'warp':
return (
<motion.div
className="absolute inset-0 z-50 pointer-events-none"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.5 }}
>
{/* Only render WarpOverlay when in view to restart the animation/canvas clock */}
{isInView && <WarpOverlay />}
<motion.div
className="absolute inset-0 bg-[#0e0e10]"
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
/>
</motion.div>
);
case 'wipe-right':
return (
<>
<motion.div className="absolute inset-0 z-50 bg-[#ff4b4b] pointer-events-none"
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase }} />
<motion.div className="absolute inset-0 z-40 bg-[#20e3b2] pointer-events-none"
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase, delay: 0.1 }} />
</>
);
case 'shutters':
return (
<div className="absolute inset-0 z-50 pointer-events-none flex flex-row">
{[0, 1, 2, 3].map((i) => (
<motion.div
key={i}
className={`relative h-full w-1/4 bg-[#1c1c1f] border-x border-[#333]/50 ${i % 2 === 0 ? 'border-b-4 border-b-[#20e3b2]' : 'border-t-4 border-t-[#ff4b4b]'}`}
initial={{ scaleY: 1 }}
whileInView={{ scaleY: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{
duration: 0.8,
ease: [0.76, 0, 0.24, 1], // expoOut
delay: i * 0.05
}}
style={{ originY: i % 2 === 0 ? 0 : 1 }} // Evens shrink to top, Odds shrink to bottom
/>
))}
</div>
);
case 'paint-ripple':
return (
<>
{/* The Drop */}
<motion.div
className="absolute left-1/2 top-0 z-[60] w-3 h-12 bg-white rounded-full pointer-events-none -translate-x-1/2"
initial={{ y: "-10vh", opacity: 1 }}
whileInView={{ y: "50vh", opacity: [1, 1, 0] }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.6, ease: "easeIn" }}
/>
{/* The Ripple */}
<motion.div
className="absolute inset-0 z-0 bg-[#e3e1db] pointer-events-none flex items-center justify-center overflow-hidden"
initial={{ clipPath: "circle(0% at 50% 50%)" }}
whileInView={{ clipPath: "circle(150% at 50% 50%)" }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.8, ease: "circOut", delay: 0.55 }}
/>
</>
);
case 'gradient':
return (
<motion.div
className="absolute inset-0 z-[20] pointer-events-none"
style={{
background: "linear-gradient(to bottom, #e3e1db 0%, transparent 100%)"
}}
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
viewport={{ once: true, amount: 0.1 }} // Trigger almost immediately on mobile to prevent blocking text
transition={{ duration: 1.5 }}
/>
);
case 'sand':
return (
<motion.div
className="absolute inset-0 z-0 overflow-hidden"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 1 }}
>
<SandOverlay />
</motion.div>
)
case 'slide':
return (
<motion.div
className="absolute inset-0 z-50 bg-[#ebe9e4] pointer-events-none"
initial={{ y: "0%" }}
whileInView={{ y: "100%" }}
viewport={{ once: true, amount: 0.01 }} // Trigger almost immediately
transition={{ duration: 1.0, ease: [0.76, 0, 0.24, 1] }}
/>
);
default:
return null;
}
};
// --- CONTENT VARIANTS ---
const getContentVariants = (): Variants | undefined => {
if (transitionEffect === 'sand') {
return {
hidden: { opacity: 0, scale: 0.95, filter: "blur(10px)" },
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: { duration: 1.2, ease: "easeOut" }
}
};
}
return undefined;
};
return (
<section
ref={ref}
id={id}
className={`w-full relative overflow-hidden snap-start shrink-0 flex flex-col
min-h-[100dvh]
${transparent ? 'bg-transparent' : 'bg-theme-main'}
${transitionEffect === 'gradient' ? 'bg-[#0e0e10]' : ''}
border-b border-theme/20 ${className}`}
>
{renderOverlay()}
<motion.div
style={transitionEffect !== 'slide' ? { y } : undefined}
initial={transitionEffect === 'sand' ? "hidden" : undefined}
whileInView={transitionEffect === 'sand' ? "visible" : undefined}
viewport={{ once: true, amount: 0.2 }}
variants={getContentVariants()}
className="w-full flex-grow flex flex-col items-center justify-center px-4 md:px-12 py-20 md:py-28 relative z-10"
>
{children}
</motion.div>
</section>
);
};
export default SectionWrapper;