"use client"; import { useRef, useLayoutEffect, useEffect, useState } from "react"; import { motion } from "framer-motion"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { section } from "framer-motion/client"; // Register GSAP plugins if (typeof window !== "undefined") { gsap.registerPlugin(ScrollTrigger); } interface Service { icon: React.ReactNode; title: string; description: string; features: string[]; } interface CardMouseState { x: number; y: number; isHovering: boolean; } const services: Service[] = [ { icon: ( ), title: "Machine Learning", description: "Building and deploying predictive models that uncover patterns and drive intelligent decision-making.", features: ["TensorFlow / PyTorch", "Model Optimization", "MLOps"], }, { icon: ( ), title: "Data Visualization", description: "Transforming complex datasets into clear, compelling visual stories that inform and persuade.", features: ["Tableau / Power BI", "Matplotlib / Seaborn", "Dashboard Design"], }, { icon: ( ), title: "Deep Learning & NLP", description: "Leveraging neural networks and language models to solve complex problems in vision and text understanding.", features: ["Natural Language Processing", "Computer Vision", "Neural Networks"], }, { icon: ( ), title: "Data Engineering & Python Backend", description: "End-to-end data pipelines and robust Python backend systems from ingestion to production APIs.", features: ["SQL / NoSQL", "ETL Pipelines", "FastAPI / Flask"], }, ]; export default function ServicesSection() { const sectionRef = useRef(null); const headerRef = useRef(null); const cardsRef = useRef(null); const cardRefs = useRef<(HTMLDivElement | null)[]>([]); const [mounted, setMounted] = useState(false); const [isMobile, setIsMobile] = useState(false); const [cardMouseStates, setCardMouseStates] = useState( services.map(() => ({ x: 0, y: 0, isHovering: false })) ); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 768); checkMobile(); window.addEventListener('resize', checkMobile); // Set mounted AFTER isMobile is determined setMounted(true); return () => window.removeEventListener('resize', checkMobile); }, []); // Track mouse position per card for 3D tilt effects const handleMouseMove = (e: React.MouseEvent, cardIndex: number) => { const card = e.currentTarget; const rect = card.getBoundingClientRect(); const x = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2); const y = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2); setCardMouseStates(prev => { const newStates = [...prev]; newStates[cardIndex] = { x, y, isHovering: true }; return newStates; }); }; const handleMouseEnter = (cardIndex: number) => { setCardMouseStates(prev => { const newStates = [...prev]; newStates[cardIndex] = { ...newStates[cardIndex], isHovering: true }; return newStates; }); }; const handleMouseLeave = (cardIndex: number) => { setCardMouseStates(prev => { const newStates = [...prev]; newStates[cardIndex] = { x: 0, y: 0, isHovering: false }; return newStates; }); }; useLayoutEffect(() => { if (!mounted) return; // On mobile, skip ALL GSAP animations — mobile layout is pure CSS if (isMobile) return; const ctx = gsap.context(() => { // Header animation gsap.fromTo( ".services-label", { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.8, ease: "power2.out", scrollTrigger: { trigger: headerRef.current, start: "top 90%", end: "top 70%", scrub: 1, }, } ); // Heading words animation if (headerRef.current) { const words = headerRef.current.querySelectorAll(".word"); gsap.fromTo( words, { opacity: 0, y: 40, rotateX: -15 }, { opacity: 1, y: 0, rotateX: 0, duration: 1, stagger: 0.08, ease: "power3.out", scrollTrigger: { trigger: headerRef.current, start: "top 85%", end: "top 55%", scrub: 1, }, } ); } // Decorative line animation gsap.fromTo( ".services-line", { scaleX: 0, transformOrigin: "left center" }, { scaleX: 1, duration: 1.2, ease: "power3.inOut", scrollTrigger: { trigger: sectionRef.current, start: "top 80%", end: "top 50%", scrub: 1, }, } ); // Horizontal scroll animation if (cardsRef.current && sectionRef.current) { const scrollContainer = cardsRef.current; const cards = scrollContainer.querySelectorAll(".service-card") as NodeListOf; const numCards = cards.length; if (numCards > 0) { requestAnimationFrame(() => { requestAnimationFrame(() => { scrollContainer.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportCenter = viewportWidth / 2; const cardWidth = cards[0].getBoundingClientRect().width; const gap = 32; const paddingLeft = viewportWidth * 0.08; const firstCardCenterInitial = paddingLeft + (cardWidth / 2); const initialOffset = firstCardCenterInitial - viewportCenter; const lastCardLeftEdge = paddingLeft + (numCards - 1) * (cardWidth + gap); const lastCardCenterInitial = lastCardLeftEdge + (cardWidth / 2); const scrollAmount = lastCardCenterInitial - firstCardCenterInitial; const scrollDistancePerCard = Math.max(400, viewportWidth * 0.4); const totalScrollDistance = scrollDistancePerCard * numCards; cards.forEach((card, index) => { gsap.set(card, { willChange: "transform, opacity", force3D: true }); if (index === 0) { gsap.set(card, { scale: 1.2, opacity: 1 }); } else { gsap.set(card, { scale: 0.7, opacity: 0.4 }); } }); gsap.set(scrollContainer, { x: -initialOffset, willChange: "transform", force3D: true }); gsap.fromTo( scrollContainer, { x: -initialOffset }, { x: -initialOffset - scrollAmount, ease: "none", immediateRender: false, scrollTrigger: { trigger: sectionRef.current, start: "top top", end: () => `+=${totalScrollDistance}`, pin: true, pinSpacing: true, anticipatePin: 1, invalidateOnRefresh: true, refreshPriority: -1, scrub: 1, markers: false, fastScrollEnd: true, onUpdate: (self) => { const progress = Math.min(1, Math.max(0, self.progress)); const currentX = -initialOffset - (progress * scrollAmount); gsap.set(scrollContainer, { x: currentX }); const progressPerCard = 1 / (numCards - 1 || 1); cards.forEach((card, index) => { const cardCenterProgress = index * progressPerCard; const distanceFromCenter = Math.abs(progress - cardCenterProgress); const normalizedDistance = distanceFromCenter / progressPerCard; const easeOut = (t: number) => 1 - (1 - t) * (1 - t); const centeredness = easeOut(Math.max(0, 1 - normalizedDistance)); const scale = 0.7 + (0.5 * centeredness); const opacity = 0.4 + (0.6 * centeredness); gsap.set(card, { scale, opacity }); }); }, onLeave: () => { gsap.set(cards[numCards - 1], { scale: 1.2, opacity: 1 }); gsap.set(scrollContainer, { x: -initialOffset - scrollAmount }); }, onEnterBack: () => { gsap.set(cards[0], { scale: 1.2, opacity: 1 }); for (let i = 1; i < numCards; i++) { gsap.set(cards[i], { scale: 0.7, opacity: 0.4 }); } gsap.set(scrollContainer, { x: -initialOffset }); }, onRefresh: (self) => { const progress = self.progress || 0; if (progress === 0) { gsap.set(cards[0], { scale: 1.2, opacity: 1 }); gsap.set(scrollContainer, { x: -initialOffset }); } else if (progress >= 0.99) { gsap.set(cards[numCards - 1], { scale: 1.2, opacity: 1 }); gsap.set(scrollContainer, { x: -initialOffset - scrollAmount }); } }, }, } ); let resizeTimeout: NodeJS.Timeout; const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { ScrollTrigger.refresh(); }, 300); }; window.addEventListener("resize", handleResize); (sectionRef.current as any)._scrollCleanup = () => { window.removeEventListener("resize", handleResize); cards.forEach(card => { gsap.set(card, { clearProps: "willChange" }); }); gsap.set(scrollContainer, { clearProps: "willChange" }); ScrollTrigger.getAll().forEach(trigger => { if (trigger.vars.trigger === sectionRef.current) { trigger.kill(); } }); }; }); }); } } }, sectionRef); return () => { ctx.revert(); if (sectionRef.current && (sectionRef.current as any)._scrollCleanup) { (sectionRef.current as any)._scrollCleanup(); } }; }, [mounted, isMobile]); const headingText = "What I"; const headingText2 = "Bring to the Table"; return ( {/* Full-screen dark overlay to hide other sections */} {/* Background gradient orb */} {/* ─── MOBILE: Vertical stacked cards (visible < 768px) ─── */} {/* Header */} Services What I{" "} Bring to the Table {/* Vertical card grid */} {services.map((service) => ( {/* Icon */} {service.icon} {/* Text */} {service.title} {service.description} {service.features.map((feature) => ( {feature} ))} ))} {/* ─── DESKTOP: Horizontal scroll with GSAP pin (visible >= 768px) ─── */} {/* Header */} Services {headingText.split(" ").map((word, i) => ( {word} ))}{" "} {headingText2.split(" ").map((word, i) => ( {word} ))} {services.map((service, index) => { const mouseState = cardMouseStates[index]; return ( { cardRefs.current[index] = el; }} className="service-card group relative p-6 bg-white/[0.02] backdrop-blur-sm border border-white/5 rounded-3xl overflow-hidden cursor-pointer flex-shrink-0 w-[320px] lg:w-[350px] h-auto" onMouseMove={(e) => handleMouseMove(e, index)} onMouseEnter={() => handleMouseEnter(index)} onMouseLeave={() => handleMouseLeave(index)} style={{ transformStyle: "preserve-3d", transform: mouseState.isHovering ? `perspective(1000px) rotateX(${-mouseState.y * 10}deg) rotateY(${mouseState.x * 10}deg) translateZ(20px)` : "perspective(1000px) rotateX(0) rotateY(0) translateZ(0)", transition: mouseState.isHovering ? "transform 0.1s ease-out" : "transform 0.5s ease-out", }} > {/* Icon */} {service.icon} {/* Title */} {service.title} {/* Description */} {service.description} {/* Features */} {service.features.map((feature) => ( {feature} ))} ); })} {/* Scroll indicator */} ); }
Services
{service.description}