// growth-sections.jsx — secciones premium de la landing "Growth"
// Reusa F, Magnetic, Parallax, RevealText, RevealWords, GlitchWord, useScrollProgress,
// useInView de shared.jsx / final-hooks.jsx / final-sections.jsx.
// Pieza nueva de esta landing: GrowthChart — un gráfico de crecimiento animado con
// contadores en vivo, distinto de la terminal de MVP y la red de nodos de Empresas.
function useCountUp(target, active, duration = 1600) {
const [val, setVal] = React.useState(0);
React.useEffect(() => {
if (!active) return;
let raf, start;
const step = (t) => {
if (!start) start = t;
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(target * eased);
if (p < 1) raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [active, target, duration]);
return val;
}
function GrowthChart() {
const ref = React.useRef(null);
const inView = useInView(ref, { threshold: 0.35 });
const svgRef = React.useRef(null);
const [dash, setDash] = React.useState(1000);
const pathD = 'M0,150 C40,148 60,140 90,132 C130,122 150,110 190,100 C230,90 250,60 290,52 C320,46 340,20 380,8';
const points = [
{ x: 0, y: 150 }, { x: 90, y: 132 }, { x: 190, y: 100 }, { x: 290, y: 52 }, { x: 380, y: 8 }
];
React.useEffect(() => {
const p = svgRef.current;
if (p) setDash(p.getTotalLength());
}, []);
const clientes = useCountUp(312, inView, 1800);
const roas = useCountUp(4.2, inView, 1800);
const cac = useCountUp(38, inView, 1800);
return (
ADQUISICIÓN · ÚLTIMOS 90 DÍAS
EN VIVO
{Math.round(clientes)}
CLIENTES NUEVOS
{roas.toFixed(1)}x
RETORNO SOBRE PAUTA
-{Math.round(cac)}%
COSTO POR CLIENTE
);
}
// ───────────── HERO ─────────────
function GHero({ content }) {
const heroRef = React.useRef(null);
const progress = useScrollProgress(heroRef);
const [mouse, setMouse] = React.useState({ x: 0.5, y: 0.5 });
React.useEffect(() => {
if (document.getElementById('growth-hero-styles')) return;
const s = document.createElement('style');
s.id = 'growth-hero-styles';
s.textContent = `
@property --grBorderAngle { syntax: ''; inherits: false; initial-value: 0deg; }
@keyframes grBorderRotate { to { --grBorderAngle: 360deg; } }
@keyframes grBtnShimmer {
0%, 75% { background-position: 200% center; opacity: 0; }
80% { opacity: 1; } 95%, 100% { background-position: -100% center; opacity: 0; }
}
@keyframes grArrow { 0%, 60%, 100% { transform: translateX(0); } 80% { transform: translateX(5px); } }
.gr-cta-wrap {
position: relative; display: inline-block; border-radius: 14px; padding: 2px;
background: conic-gradient(from var(--grBorderAngle), transparent 0%, #2199ea 12%, #63b7f0 22%, #137dc5 32%, transparent 44%, transparent 100%);
animation: grBorderRotate 2.5s linear infinite;
transition: transform 0.3s cubic-bezier(.2,.8,.3,1);
}
.gr-cta-wrap:hover { transform: scale(1.04); }
.gr-cta-inner {
position: relative; overflow: hidden; padding: 20px 36px;
background: linear-gradient(135deg, #2199ea 20%, #137dc5); color: #fff; border: none; border-radius: 12px;
font-family: var(--font-heading); font-size: 15px; font-weight: 500; letter-spacing: 0.02em;
cursor: pointer; display: flex; align-items: center; gap: 8px;
}
.gr-cta-inner::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%);
background-size: 200% 100%; animation: grBtnShimmer 9s ease-in-out infinite; pointer-events: none;
}
.gr-arrow { display: inline-block; animation: grArrow 3s ease-in-out infinite; }
`;
document.head.appendChild(s);
}, []);
React.useEffect(() => {
const onMove = (e) => {
const el = heroRef.current; if (!el) return;
const r = el.getBoundingClientRect();
setMouse({ x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height });
};
const el = heroRef.current; if (!el) return;
el.addEventListener('mousemove', onMove);
return () => el.removeEventListener('mousemove', onMove);
}, []);
const px = (mouse.x - 0.5) * 20;
const py = (mouse.y - 0.5) * 20;
return (
EUREKANT.GROWTH.MARKETING
{content.hero.sub}
{ e.preventDefault(); const t = document.getElementById('modulos'); if (t) window.scrollTo({ top: t.getBoundingClientRect().top + window.scrollY - 80, behavior: 'smooth' }); }} style={{
padding: '20px 36px', background: 'transparent', color: F.text, textDecoration: 'none',
border: `1px solid ${F.lineStrong}`, borderRadius: 12, fontFamily: 'var(--font-heading)',
fontSize: 15, fontWeight: 500, cursor: 'pointer', display: 'inline-block'
}}>{content.hero.cta2}
●{content.hero.meta}
);
}
function SectionEyebrowHead({ eyebrow, title, sub, align = 'left', maxTitle = 900 }) {
return (
{eyebrow}
{title}
{sub &&
{sub}
}
);
}
// ───────────── COSTO DE SER INVISIBLE ─────────────
function GCost({ content }) {
const ref = React.useRef(null);
const inView = useInView(ref, { threshold: 0.2 });
return (
01
{content.cost.items.map((c, i) => (
{c.stat}
{c.label}
{c.desc}
))}
);
}
// ───────────── DOLORES ─────────────
function GPains({ content }) {
const ref = React.useRef(null);
const inView = useInView(ref, { threshold: 0.15 });
return (
{content.pains.items.map((p, i) => (
e.currentTarget.style.borderColor = F.accent}
onMouseLeave={(e) => e.currentTarget.style.borderColor = F.line}
style={{
background: F.surface, border: `1px solid ${F.line}`, borderRadius: 24, padding: 40,
position: 'relative', overflow: 'hidden',
opacity: inView ? 1 : 0, transform: inView ? 'none' : 'translateY(28px)',
transitionProperty: 'border-color, opacity, transform',
transitionDuration: `.35s, .8s, .8s`, transitionDelay: `0s, ${i * 0.12}s, ${i * 0.12}s`,
transitionTimingFunction: 'ease, cubic-bezier(.16,.84,.3,1), cubic-bezier(.16,.84,.3,1)'
}}>
{p.n}
{p.title}
{p.desc}
))}
);
}
// ───────────── QUÉ INCLUYE (sticky scroll) ─────────────
function GModules({ content }) {
const [active, setActive] = React.useState(0);
const ref = React.useRef(null);
const progress = useScrollProgress(ref);
React.useEffect(() => {
if (progress > 0.1 && progress < 0.9) {
const idx = Math.min(3, Math.floor(progress * 4));
if (idx !== active) setActive(idx);
}
}, [progress]);
const step = content.modules.steps[active];
return (
02
{content.modules.steps.map((s, i) => (
setActive(i)} onClick={() => setActive(i)}
style={{
padding: '32px 0', cursor: 'pointer',
borderTop: `1px solid ${active === i ? F.accent : F.line}`,
opacity: active === i ? 1 : 0.4,
transform: active === i ? 'translateX(8px)' : 'translateX(0)',
transition: 'opacity .4s, border-color .4s, transform .4s'
}}>
{s.n}
{s.title}
{s.dur}
))}
{step.n}
CANAL {step.n} / 04 · {step.dur.toUpperCase()}
{step.title}
{step.desc}
{content.modules.steps.map((_, i) => (
))}
);
}
// ───────────── CÓMO TRABAJAMOS ─────────────
function GProcess({ content }) {
const ref = React.useRef(null);
const inView = useInView(ref, { threshold: 0.15 });
return (
03
{content.process.items.map((st, i) => (
0{i + 1}
{st.dur.toUpperCase()}
{st.title}
{st.desc}
))}
);
}
Object.assign(window, { GrowthChart, GHero, GCost, GPains, GModules, GProcess });