150 lines
6.0 KiB
TypeScript
150 lines
6.0 KiB
TypeScript
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import gsap from 'gsap';
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
import processIllustration from '../src/assets/process-illustration.webp';
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
const Process: React.FC = () => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const imgRef = useRef<HTMLImageElement>(null);
|
|
const prefersReducedMotion = useReducedMotion();
|
|
const [supportsFinePointer, setSupportsFinePointer] = useState(false);
|
|
const shouldAnimate = !prefersReducedMotion && supportsFinePointer;
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
|
|
const updateState = () => setSupportsFinePointer(mediaQuery.matches);
|
|
|
|
updateState();
|
|
|
|
if (typeof mediaQuery.addEventListener === 'function') {
|
|
mediaQuery.addEventListener('change', updateState);
|
|
return () => mediaQuery.removeEventListener('change', updateState);
|
|
}
|
|
|
|
mediaQuery.addListener(updateState);
|
|
return () => mediaQuery.removeListener(updateState);
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
if (!shouldAnimate) {
|
|
return;
|
|
}
|
|
|
|
const ctx = gsap.context((self) => {
|
|
// Dramatic Zoom Animation
|
|
if (containerRef.current && imgRef.current) {
|
|
gsap.fromTo(imgRef.current,
|
|
{ scale: 1, transformOrigin: 'center center' },
|
|
{
|
|
scale: 2.0,
|
|
ease: "none",
|
|
scrollTrigger: {
|
|
trigger: containerRef.current,
|
|
start: "top bottom",
|
|
end: "bottom top",
|
|
scrub: true,
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Animate steps - even slower, one by one appearance
|
|
const steps = gsap.utils.selector(containerRef.current)('.process-step');
|
|
steps.forEach((step: any, index: number) => {
|
|
gsap.fromTo(step,
|
|
{ opacity: 0, y: 60 },
|
|
{
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 2,
|
|
ease: "power3.out",
|
|
scrollTrigger: {
|
|
trigger: step,
|
|
start: "top 95%",
|
|
end: "top 40%",
|
|
toggleActions: "play reverse play reverse",
|
|
scrub: 1.5
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
}, containerRef);
|
|
return () => ctx.revert();
|
|
}, [shouldAnimate]);
|
|
|
|
return (
|
|
<section ref={containerRef} className="relative w-full" style={{ clipPath: 'inset(0)' }}>
|
|
{/* Fixed Background Image - constrained to this section via clip-path */}
|
|
<div className={`${shouldAnimate ? 'fixed inset-0 w-full h-screen' : 'absolute inset-0 w-full h-full'} z-0 overflow-hidden`}>
|
|
<img
|
|
ref={imgRef}
|
|
alt="Modern server rack infrastructure"
|
|
className={`w-full h-full object-cover opacity-80 origin-center ${shouldAnimate ? 'will-change-transform' : ''}`}
|
|
src={processIllustration}
|
|
loading="lazy"
|
|
decoding="async"
|
|
fetchPriority="low"
|
|
/>
|
|
{/* Gradient overlay for text readability */}
|
|
<div className="absolute inset-0 bg-gradient-to-r from-black/50 via-black/30 to-black/60" />
|
|
</div>
|
|
|
|
{/* Content - positioned relative, scrolls over the fixed image */}
|
|
<div className="relative z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))]">
|
|
{/* Header - Static on mobile, fixed on desktop */}
|
|
<div className="relative mb-12 lg:mb-0 lg:fixed lg:top-1/2 lg:right-16 lg:-translate-y-1/2 z-20 text-left lg:text-right px-6 lg:px-0" style={{ clipPath: 'none' }}>
|
|
<span className="text-xs font-semibold uppercase tracking-widest text-gray-300 mb-2 block">Process</span>
|
|
<h2 className="font-display text-3xl lg:text-5xl font-medium text-white">
|
|
One consultation to begin,<br />
|
|
<span className="text-gray-400">three steps to clarity.</span>
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Spacer for first screen - shortened */}
|
|
<div className="h-[30vh]" />
|
|
|
|
{/* Steps - LEFT side on desktop, full width on mobile */}
|
|
<div className="min-h-screen px-6 lg:px-16 py-24">
|
|
<div className="w-full lg:w-1/2 space-y-[60vh]">
|
|
{[
|
|
{ num: "1", title: "Audit & Assess", desc: "We dive deep into your current infrastructure to identify vulnerabilities and opportunities for optimization." },
|
|
{ num: "2", title: "Implement & Secure", desc: "Our team deploys the necessary hardware and software solutions with minimal disruption to your daily operations." },
|
|
{ num: "3", title: "Monitor & Maintain", desc: "Ongoing 24/7 monitoring ensures problems are solved before you even notice them." }
|
|
].map((step, i) => (
|
|
<div key={i} className="process-step flex gap-6 group cursor-default bg-black/60 backdrop-blur-md p-8 rounded-2xl border border-white/10">
|
|
<div className="flex-shrink-0 mt-1">
|
|
<motion.span
|
|
whileHover={{ scale: 1.2, borderColor: "#3b82f6", color: "#3b82f6" }}
|
|
className="flex items-center justify-center w-12 h-12 rounded-xl border-2 border-white/30 text-lg font-bold text-white transition-colors"
|
|
>
|
|
{step.num}
|
|
</motion.span>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-2xl lg:text-3xl font-medium text-white group-hover:translate-x-1 transition-transform group-hover:text-blue-400">{step.title}</h3>
|
|
<p className="text-lg text-gray-300 mt-3 leading-relaxed max-w-lg">
|
|
{step.desc}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* End spacer - shortened */}
|
|
<div className="h-[20vh]" />
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Process;
|