"use client"; import { useEffect, useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { ArrowRight } from "@/components/ui/icons"; import AnimatedButton from "@/components/ui/AnimatedButton"; /** * Thumbnail-slider hero. Full-bleed background, content left, preview deck * bottom-right. Slide changes reveal the new image through an expanding * circle anchored to the clicked card — every image layer stays full-bleed * and is only clipped/uniformly transformed, so distortion is impossible. * * Swap the slide images in /public/images/opt (optimized WebP — run * scripts/optimize-images.mjs after adding new source images). */ const SLIDES = [ { id: "game", image: "/images/opt/Pillar1.webp", tag: "Game Development", title: "Game Development", desc: "Immersive 2D, 3D, AR/VR and multiplayer games — from concept and art to live ops.", }, { id: "data", image: "/images/opt/HeroBanner3.png", tag: "Data Analytics & AI", title: "Data Analytics & AI", desc: "BI, predictive analytics and AI that turn raw data into decisions and automation.", }, { id: "web", image: "/images/opt/HeroBanner1.webp", tag: "Web Development", title: "Web Development", desc: "Web apps, custom software and cloud — engineered on a modern stack to scale.", }, { id: "media", image: "/images/opt/HeroBanner2.webp", tag: "Media & Creative", title: "Media & Creative", desc: "Branding, video, motion and social creative that commands attention.", }, ]; const EASE = [0.76, 0, 0.24, 1]; export default function HeroSlider() { const [active, setActive] = useState(0); const [committed, setCommitted] = useState(0); // base layer slide const [origin, setOrigin] = useState({ x: 80, y: 78 }); // reveal origin (%) const sectionRef = useRef(null); const firstCardRef = useRef(null); const len = SLIDES.length; const slide = SLIDES[active]; const revealing = active !== committed; // upcoming preview cards (next 3, wrapping) const upcoming = [1, 2, 3].map((o) => (active + o) % len); /** Jump to a slide; the circle reveal grows out of `el` (or the 1st card). */ const go = (idx, el) => { if (idx === active) return; // If a reveal is still mid-flight, lock its image in as the base first — // otherwise aborting it would flash the older slide underneath. if (revealing) setCommitted(active); const cardEl = el || firstCardRef.current; const s = sectionRef.current?.getBoundingClientRect(); if (cardEl && s) { const c = cardEl.getBoundingClientRect(); setOrigin({ x: ((c.left + c.width / 2 - s.left) / s.width) * 100, y: ((c.top + c.height / 2 - s.top) / s.height) * 100, }); } setActive(idx); }; const next = () => go((active + 1) % len); const prev = () => go((active - 1 + len) % len); // Warm all slide images once so the first reveals never wipe in a // not-yet-decoded (blank) layer on a cold cache. useEffect(() => { SLIDES.forEach((s) => { const img = new window.Image(); img.src = s.image; }); }, []); // auto-advance; resets whenever active changes useEffect(() => { const id = setTimeout(next, 5200); return () => clearTimeout(id); }, [active]); return (
{/* Base layer — committed slide (pushes back during reveal, Ken Burns idle). On commit it snaps back instantly — the reveal layer covers the swap, so no visible zoom/brightness shift on the new image. */}
{/* Reveal layer — full-bleed new slide, clipped by an expanding circle. The layer itself never resizes → no distortion, ever. */} {revealing && ( setCommitted(active)} className="absolute inset-0 -z-20 will-change-[clip-path]" style={{ backgroundImage: `url(${slide.image})`, backgroundSize: "cover", backgroundPosition: "center", }} /> )} {/* Legibility overlays */}
{/* Content */}
Build. Analyze. Create. Grow.

Building digital experiences that{" "} drive growth.

{slide.tag}

{slide.desc}

Start a project
{/* Bottom-right preview deck */}
{upcoming.map((idx, pos) => { const s = SLIDES[idx]; return ( go(idx, e.currentTarget)} aria-label={`Show ${s.title}`} className="group relative h-24 w-32 shrink-0 overflow-hidden rounded-2xl border border-white/20 shadow-xl shadow-black/50 sm:h-36 sm:w-52" > {s.tag} ); })} {/* Arrows */}
{/* Progress dots */}
{SLIDES.map((s, i) => (
); }