/* Hooks compartidos · Golden Hall - useLang: idioma global con localStorage - useReveal: animación on-scroll - useCounter: contador animado - t(): traducción rápida */ const LangCtx = React.createContext({ lang: 'es', setLang: () => {} }); function LangProvider({ children }) { const [lang, setLang] = React.useState(() => { try { return localStorage.getItem('gh-lang') || 'es'; } catch { return 'es'; } }); React.useEffect(() => { try { localStorage.setItem('gh-lang', lang); } catch {} document.documentElement.lang = lang; }, [lang]); return React.createElement(LangCtx.Provider, { value: { lang, setLang } }, children); } const useLang = () => React.useContext(LangCtx); // t('clave') — saca la traducción del bundle const useT = () => { const { lang } = useLang(); return React.useCallback((key) => { const entry = window.GH_T[key]; if (!entry) return key; return entry[lang] ?? entry.es ?? key; }, [lang]); }; // reveal on scroll usando IntersectionObserver function Reveal({ as = 'div', delay = 0, className = '', children, ...rest }) { const ref = React.useRef(null); const Comp = as; React.useEffect(() => { const el = ref.current; if (!el) return; // Inmediato: si ya está en viewport al montar, revelar al toque const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < vh && r.bottom > 0) { el.classList.add('in'); return; } const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { el.classList.add('in'); obs.unobserve(el); } }); }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); obs.observe(el); // Salvaguarda: si el observer no se dispara, mostrar tras 1.2s const safety = setTimeout(() => el.classList.add('in'), 1200); return () => { obs.disconnect(); clearTimeout(safety); }; }, []); const delayClass = delay ? `delay-${delay}` : ''; return React.createElement( Comp, { ref, className: `reveal ${delayClass} ${className}`.trim(), ...rest }, children ); } // Contador animado al entrar en viewport function Counter({ to, suffix = '', duration = 1800, decimals = 0 }) { const ref = React.useRef(null); const [val, setVal] = React.useState(0); React.useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (!e.isIntersecting) return; obs.unobserve(el); const start = performance.now(); const tick = (now) => { const p = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - p, 3); // easeOutCubic setVal(to * eased); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }); }, { threshold: 0.3 }); obs.observe(el); return () => obs.disconnect(); }, [to, duration]); const display = decimals ? val.toFixed(decimals) : Math.round(val).toLocaleString(); return React.createElement('span', { ref }, display, suffix); } Object.assign(window, { LangCtx, LangProvider, useLang, useT, Reveal, Counter });