// CasesGrid.jsx — featured case studies // Pure-text cards. The mock-site embed was removed in favour of focused copy // + animated counters and reveal staggers. Champagne trails drift behind the // content to keep the cards alive without competing with the typography. const CASES = [ { cls: 'case--lg case--navy on-navy', meta: 'E-COMMERCE · ACME · 2025', kpi: { prefix: '', value: 3.2, decimals: 1, unit: '×', trailing: 'mais leads' }, title: ['Migração da loja em ', { em: '60 dias' }], }, { cls: '', meta: 'INSTITUCIONAL · NORTHWIND · 2025', kpi: { prefix: '+', value: 45, decimals: 0, unit: '%', trailing: 'NPS' }, title: ['Reposicionamento ', { em: 'premium' }], }, { cls: 'case--navy on-navy', meta: 'SAAS · HELVETIA · 2025', kpi: { prefix: '−', value: 40, decimals: 0, unit: '%', trailing: 'churn em 30d' }, title: ['Onboarding ', { em: 'refeito' }], }, { cls: '', meta: 'FINTECH · VERIDIAN · 2025', kpi: { prefix: '', value: 1.1, decimals: 1, unit: 's', trailing: 'carga média' }, title: ['Dashboard que ', { em: 'respira' }], }, { cls: '', meta: 'VAREJO · LUMEN · 2025', kpi: { prefix: '+', value: 72, decimals: 0, unit: '%', trailing: 'busca interna' }, title: ['Catálogo com ', { em: 'fôlego' }], }, ]; const Cases = () => { const gridRef = React.useRef(null); React.useEffect(() => { if (!window.gsap || !gridRef.current) return; const grid = gridRef.current; const cards = grid.querySelectorAll('.case'); const observers = []; cards.forEach((card) => { const target = parseFloat(card.dataset.kpi || '0'); const decimals = parseInt(card.dataset.decimals || '0', 10); const numEl = card.querySelector('.case__kpi-num'); const ruleEl = card.querySelector('.case__rule'); const titleNodes = card.querySelectorAll('.case__meta, .case__kpi, .case__title'); const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; // 1) Reveal stagger — meta, KPI, title slide up + fade in gsap.from(titleNodes, { y: 24, opacity: 0, duration: 0.6, stagger: 0.1, ease: 'power3.out', }); // 2) Counter — animate 0 → target. Format with comma decimal. if (numEl && !Number.isNaN(target)) { const obj = { v: 0 }; gsap.to(obj, { v: target, duration: 1.6, delay: 0.4, ease: 'power3.out', onUpdate: () => { numEl.textContent = obj.v.toFixed(decimals).replace('.', ','); }, }); } // 3) Champagne underline that draws from 0 → 100% under the KPI if (ruleEl) { gsap.fromTo(ruleEl, { scaleX: 0, transformOrigin: 'left center' }, { scaleX: 1, duration: 1.2, delay: 0.5, ease: 'expo.out' } ); } io.unobserve(card); }); }, { threshold: 0.3 }); io.observe(card); observers.push(io); }); return () => observers.forEach((io) => io.disconnect()); }, []); const renderTitle = (parts) => parts.map((p, i) => typeof p === 'string' ? {p} : {p.em} ); return (
Cases selecionados

Cases que convertem

{CASES.map((c, i) => (
{c.meta}
{c.kpi.prefix} 0 {c.kpi.unit} {c.kpi.trailing ? ` ${c.kpi.trailing}` : null}

{renderTitle(c.title)}

))}
); }; window.Cases = Cases;