Spaces:
Running
Running
| "use client"; | |
| import { useRef, useEffect, useState } from "react"; | |
| interface TextRevealItem { | |
| title: string; | |
| hoverTitle: string; | |
| description: string; | |
| } | |
| const textItems: TextRevealItem[] = [ | |
| { | |
| title: "DATA SCIENCE", | |
| hoverTitle: "INSIGHT DRIVEN", | |
| description: "Extracting actionable insights from complex datasets with statistical rigor.", | |
| }, | |
| { | |
| title: "MACHINE LEARNING", | |
| hoverTitle: "DEEP LEARNING", | |
| description: "Building predictive models that learn, adapt, and scale.", | |
| }, | |
| { | |
| title: "DATA ANALYSIS", | |
| hoverTitle: "VISUALIZATION", | |
| description: "Turning raw numbers into compelling visual narratives and dashboards.", | |
| }, | |
| { | |
| title: "AI / ML", | |
| hoverTitle: "INTELLIGENT", | |
| description: "Designing AI systems that solve real-world problems with precision.", | |
| }, | |
| { | |
| title: "PYTHON", | |
| hoverTitle: "BACKEND", | |
| description: "Engineering robust Python backends and scalable data pipelines.", | |
| }, | |
| ]; | |
| const baseFontStyle: React.CSSProperties = { | |
| fontSize: "clamp(1.6rem, 7vw, 8.5rem)", | |
| fontWeight: 800, | |
| letterSpacing: "-0.04em", | |
| lineHeight: 1, | |
| textTransform: "uppercase", | |
| margin: 0, | |
| fontFamily: "var(--font-sans)", | |
| }; | |
| export default function AchievementsSection() { | |
| const sectionRef = useRef<HTMLElement>(null); | |
| const textRefs = useRef<(HTMLHeadingElement | null)[]>([]); | |
| const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); | |
| const [tappedIndex, setTappedIndex] = useState<number | null>(null); | |
| const [isMobile, setIsMobile] = useState(false); | |
| useEffect(() => { | |
| const checkMobile = () => setIsMobile(window.innerWidth < 768); | |
| checkMobile(); | |
| window.addEventListener('resize', checkMobile); | |
| return () => window.removeEventListener('resize', checkMobile); | |
| }, []); | |
| // Native scroll-driven color sweep — guaranteed to work | |
| useEffect(() => { | |
| const section = sectionRef.current; | |
| if (!section) return; | |
| let ticking = false; | |
| const onScroll = () => { | |
| if (!ticking) { | |
| requestAnimationFrame(() => { | |
| const rect = section.getBoundingClientRect(); | |
| const vh = window.innerHeight; | |
| // Slow sweep: takes ~2 viewport heights of scrolling | |
| // Progress 0 → 1 mapped across a wider scroll range | |
| const rawProgress = (vh - rect.top) / (vh * 1.8); | |
| const progress = Math.max(0, Math.min(1, rawProgress)); | |
| textRefs.current.forEach((el, i) => { | |
| if (!el) return; | |
| // Small stagger per row for gradual cascade | |
| const rowStart = i * 0.07; | |
| const rowDuration = 0.6; | |
| const rowEnd = rowStart + rowDuration; | |
| let rowProgress: number; | |
| if (progress <= rowStart) { | |
| rowProgress = 0; | |
| } else if (progress >= rowEnd) { | |
| rowProgress = 1; | |
| } else { | |
| rowProgress = (progress - rowStart) / rowDuration; | |
| } | |
| // Move background position from 100% (dim) to 0% (bright) | |
| const bgPos = 100 - rowProgress * 100; | |
| el.style.backgroundPosition = `${bgPos}% 0%`; | |
| }); | |
| ticking = false; | |
| }); | |
| ticking = true; | |
| } | |
| }; | |
| // Listen on window for Lenis scroll events | |
| window.addEventListener("scroll", onScroll, { passive: true }); | |
| // Also run once on mount | |
| onScroll(); | |
| return () => window.removeEventListener("scroll", onScroll); | |
| }, []); | |
| return ( | |
| <section | |
| id="clients" | |
| ref={sectionRef} | |
| className="relative overflow-hidden" | |
| style={{ | |
| background: "#050505", | |
| minHeight: isMobile ? "auto" : "100vh", | |
| display: "flex", | |
| flexDirection: "column", | |
| justifyContent: "center", | |
| paddingTop: isMobile ? "4vh" : "0", | |
| paddingBottom: isMobile ? "4vh" : "15vh", | |
| }} | |
| > | |
| {/* Text rows */} | |
| <div style={{ padding: "0 clamp(20px, 5vw, 80px)" }}> | |
| {textItems.map((item, index) => { | |
| const isHovered = isMobile ? tappedIndex === index : hoveredIndex === index; | |
| return ( | |
| <div | |
| key={index} | |
| className="text-reveal-row" | |
| onMouseEnter={() => !isMobile && setHoveredIndex(index)} | |
| onMouseLeave={() => !isMobile && setHoveredIndex(null)} | |
| onClick={() => isMobile && setTappedIndex(tappedIndex === index ? null : index)} | |
| style={{ | |
| position: "relative", | |
| overflow: "hidden", | |
| borderBottom: | |
| index < textItems.length - 1 | |
| ? "1px solid rgba(255,255,255,0.06)" | |
| : "none", | |
| touchAction: "manipulation", | |
| }} | |
| > | |
| {/* Base text — gradient background-clip, position driven by scroll */} | |
| <div | |
| style={{ | |
| position: "relative", | |
| padding: "clamp(8px, 1.4vw, 18px) 0", | |
| display: "flex", | |
| alignItems: "center", | |
| }} | |
| > | |
| <h3 | |
| ref={(el) => { textRefs.current[index] = el; }} | |
| style={{ | |
| ...baseFontStyle, | |
| background: "linear-gradient(to right, rgba(255,255,255,0.9) 50%, rgba(255,255,255,0.15) 50%)", | |
| backgroundSize: "200% 100%", | |
| backgroundPosition: "100% 0%", | |
| WebkitBackgroundClip: "text", | |
| WebkitTextFillColor: "transparent", | |
| backgroundClip: "text", | |
| }} | |
| > | |
| {item.title} | |
| </h3> | |
| </div> | |
| {/* Hover reveal layer */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| inset: 0, | |
| background: "#ffffff", | |
| clipPath: isHovered | |
| ? "inset(0 0 0 0)" | |
| : "inset(50% 0 50% 0)", | |
| transition: "clip-path 0.45s cubic-bezier(0.4, 0, 0.2, 1)", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "space-between", | |
| padding: `clamp(8px, 1.4vw, 18px) clamp(20px, 5vw, 80px)`, | |
| gap: "2rem", | |
| zIndex: 10, | |
| pointerEvents: "none", | |
| }} | |
| > | |
| <h3 style={{ ...baseFontStyle, color: "#000000" }}> | |
| {item.hoverTitle} | |
| </h3> | |
| <p | |
| className="hidden md:block" | |
| style={{ | |
| fontSize: "clamp(0.7rem, 1vw, 0.9rem)", | |
| color: "rgba(0,0,0,0.55)", | |
| maxWidth: "260px", | |
| lineHeight: 1.5, | |
| margin: 0, | |
| textAlign: "right", | |
| flexShrink: 0, | |
| opacity: isHovered ? 1 : 0, | |
| transition: "opacity 0.3s ease 0.1s", | |
| }} | |
| > | |
| {item.description} | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Bottom gradient fade */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 0, | |
| left: 0, | |
| right: 0, | |
| height: "15vh", | |
| background: "linear-gradient(to bottom, transparent, #050505)", | |
| pointerEvents: "none", | |
| zIndex: 5, | |
| }} | |
| /> | |
| </section> | |
| ); | |
| } | |