// chaos-section.jsx — GSAP ScrollTrigger pinned convergence const ChaosSection = () => { const sectionRef = React.useRef(null); const stageRef = React.useRef(null); const titleRef = React.useRef(null); const subRef = React.useRef(null); const eyebrowRef = React.useRef(null); const tilesRef = React.useRef([]); const targetRef = React.useRef(null); const progressFillRef = React.useRef(null); const progressLabelRef = React.useRef(null); const tiles = React.useMemo(() => { const items = [ 'contracts.pdf', 'onboarding.md', 'policies/', 'engineering-wiki', 'sales-playbook', 'notion-pages', 'confluence', 'slack-threads', 'google-drive', 'legacy-cms', 'internal-blog', 'training-docs', 'architecture.md', 'api-reference', 'support-tickets', 'post-mortems', ]; return items.map((label, i) => { const seed = i * 9301 + 49297; const rx = (seed % 233280) / 233280; const ry = ((seed * 3) % 233280) / 233280; const rr = ((seed * 7) % 233280) / 233280; return { label, x: 50 + rx * 45, y: ry * 80 + 8, rot: (rr - 0.5) * 18, }; }); }, []); React.useEffect(() => { if (!window.gsap || !window.ScrollTrigger) return; const gsap = window.gsap; gsap.registerPlugin(window.ScrollTrigger); const section = sectionRef.current; const stage = stageRef.current; if (!section || !stage) return; const ctx = gsap.context(() => { // Entrance — copy fades up before the pin locks in gsap.from([eyebrowRef.current, titleRef.current, subRef.current], { opacity: 0, y: 24, duration: 0.8, stagger: 0.12, ease: 'power2.out', scrollTrigger: { trigger: section, start: 'top 70%', toggleActions: 'play none none reverse', }, }); // Pinned convergence timeline const tl = gsap.timeline({ scrollTrigger: { trigger: section, start: 'top top', end: '+=180%', // scroll distance inside the pin pin: stage, scrub: 0.6, anticipatePin: 1, }, }); // tiles: drift -> converge to target point (72%, 50%) const cx = 72, cy = 50; tilesRef.current.forEach((el, i) => { if (!el) return; const t = tiles[i]; const dx = cx - t.x; const dy = cy - t.y; tl.to(el, { xPercent: dx * 14, // approx: tile moves across its own width grid yPercent: dy * 10, scale: 0.15, opacity: 0, rotate: 0, ease: 'power2.inOut', duration: 1, }, 0 + (i % 5) * 0.02); // tiny stagger }); // target block: fades + scales in tl.fromTo(targetRef.current, { opacity: 0, scale: 0.5 }, { opacity: 1, scale: 1, ease: 'power2.out', duration: 1 }, 0.35); // progress bar + label driven by the same timeline tl.fromTo(progressFillRef.current, { width: '0%' }, { width: '100%', ease: 'none', duration: 1 }, 0); // label counter via onUpdate tl.to({ v: 0 }, { v: 100, duration: 1, ease: 'none', onUpdate: function () { const v = Math.round(this.targets()[0].v); if (progressLabelRef.current) { progressLabelRef.current.textContent = 'Unificando ' + v + '%'; } }, }, 0); }, section); return () => ctx.revert(); }, [tiles]); return (
{/* Tiles layer (right half, masked) */}
{tiles.map((tile, i) => (
{ tilesRef.current[i] = el; }} style={{ position: 'absolute', left: `${tile.x}%`, top: `${tile.y}%`, transform: `translate(-50%, -50%) rotate(${tile.rot}deg)`, border: '1px solid rgba(13,13,13,0.18)', background: '#fff', padding: '8px 14px', fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: 12, color: '#282828', whiteSpace: 'nowrap', boxShadow: '2px 4px 0 rgba(13,13,13,0.08)', willChange: 'transform, opacity', }} > {tile.label}
))} {/* Target */}
INDEXED · UNIFIED · NEXUS
{/* Copy */}
01 · El problema

El conocimiento
de tu empresa
está en todas partes.

Wikis, PDFs, Notion, Drive, repositorios, tickets. Cada agente de IA que abres empieza de cero porque no encuentra nada de lo que ya sabes.

Unificando 0%
); }; Object.assign(window, { ChaosSection });