// ============ SHARED HELPERS + CURSOR + NAV + BOOT ============ const { useState, useEffect, useRef, useCallback, useMemo, createContext, useContext } = React; // ---- Language context ---- const LangCtx = createContext({ lang: 'es', setLang: () => {}, t: window.CONTENT.es }); function LangProvider({ children }) { const [lang, setLangState] = useState(() => { try { return localStorage.getItem('lang') || 'es'; } catch { return 'es'; } }); const setLang = (v) => { setLangState(v); try { localStorage.setItem('lang', v); } catch {} }; const t = window.CONTENT[lang]; return {children}; } function useLang() { return useContext(LangCtx); } // ---- reveal-on-scroll ---- function useReveal() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { el.classList.add('in'); el.querySelectorAll('.fade-up').forEach(n => n.classList.add('in')); io.disconnect(); } }); }, { threshold: 0.12 }); io.observe(el); return () => io.disconnect(); }, []); return ref; } // ---- reduced motion ---- function useReducedMotion() { const [r, setR] = useState(() => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches); useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); const h = () => setR(mq.matches); mq.addEventListener?.('change', h); return () => mq.removeEventListener?.('change', h); }, []); return r; } // ---- Custom cursor ---- function CustomCursor() { const dotRef = useRef(null); const ringRef = useRef(null); const reduce = useReducedMotion(); useEffect(() => { if (reduce) return; if (matchMedia('(pointer:coarse)').matches) return; let mx = window.innerWidth / 2, my = window.innerHeight / 2; let rx = mx, ry = my; let raf; const onMove = (e) => { mx = e.clientX; my = e.clientY; if (dotRef.current) { dotRef.current.style.left = mx + 'px'; dotRef.current.style.top = my + 'px'; } }; const loop = () => { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; if (ringRef.current) { ringRef.current.style.left = rx + 'px'; ringRef.current.style.top = ry + 'px'; } raf = requestAnimationFrame(loop); }; const onOver = (e) => { const r = ringRef.current; if (!r) return; const target = e.target; if (target.closest('a, button, .os-icon, .chip, .sq, [data-hover]')) r.classList.add('hover'); else r.classList.remove('hover'); if (target.matches('p, h1, h2, h3, h4, li, span, input, textarea')) r.classList.add('text'); else r.classList.remove('text'); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseover', onOver); raf = requestAnimationFrame(loop); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseover', onOver); cancelAnimationFrame(raf); }; }, [reduce]); if (reduce) return null; return ( <>
); } // ---- Magnetic button ---- function useMagnetic(strength = 12) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || matchMedia('(pointer:coarse)').matches) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const x = e.clientX - (r.left + r.width / 2); const y = e.clientY - (r.top + r.height / 2); const dist = Math.hypot(x, y); const max = 80; if (dist < max) { const factor = (1 - dist / max) * strength; el.style.transform = `translate(${x / max * strength}px, ${y / max * strength}px)`; } else { el.style.transform = ''; } }; const onLeave = () => { el.style.transform = ''; }; const parent = el.parentElement; parent?.addEventListener('mousemove', onMove); parent?.addEventListener('mouseleave', onLeave); return () => { parent?.removeEventListener('mousemove', onMove); parent?.removeEventListener('mouseleave', onLeave); }; }, [strength]); return ref; } // ---- spotlight effect (mouse position CSS vars) ---- function useSpotlight() { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); el.style.setProperty('--mx', `${e.clientX - r.left}px`); el.style.setProperty('--my', `${e.clientY - r.top}px`); }; el.addEventListener('mousemove', onMove); return () => el.removeEventListener('mousemove', onMove); }, []); return ref; } // ---- Nav ---- function Nav() { const { lang, setLang, t } = useLang(); const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const h = () => setScrolled(window.scrollY > 30); window.addEventListener('scroll', h, { passive: true }); return () => window.removeEventListener('scroll', h); }, []); const links = [ { id: 'about', label: t.nav.about }, { id: 'os', label: t.nav.os }, { id: 'projects', label: t.nav.projects }, { id: 'aura', label: t.nav.aura }, { id: 'cafe', label: t.nav.cafe }, { id: 'contact', label: t.nav.contact }, ]; return ( <>
setOpen(false)}> {links.map(l => {l.label})}
); } // ---- Boot screen ---- function BootScreen() { const [lines, setLines] = useState([]); const [gone, setGone] = useState(false); const reduce = useReducedMotion(); useEffect(() => { if (reduce) { setGone(true); return; } const seq = [ "> booting angelo.os", "> loading modules...", "> mounting filesystem [ok]", "> warm-up render pipeline...", "> ready" ]; let i = 0; const tick = () => { if (i >= seq.length) { setTimeout(() => setGone(true), 280); return; } setLines(L => [...L, seq[i]]); i++; setTimeout(tick, 220); }; tick(); }, [reduce]); return (
        {lines.map((l, idx) => 
{l}
)}
); } // ---- Typing rotator ---- function TypingRotator({ items, interval = 3800 }) { const reduce = useReducedMotion(); const [idx, setIdx] = useState(0); const [text, setText] = useState(items[0]); const [phase, setPhase] = useState('hold'); useEffect(() => { if (reduce) { setText(items[idx]); return; } let to; if (phase === 'hold') { to = setTimeout(() => setPhase('out'), interval); } else if (phase === 'out') { const target = items[idx]; let len = target.length; const erase = () => { len -= 1; setText(target.slice(0, Math.max(0, len))); if (len <= 0) { setPhase('in'); setIdx((idx + 1) % items.length); } else to = setTimeout(erase, 18); }; to = setTimeout(erase, 30); } else if (phase === 'in') { const target = items[idx]; let len = 0; const type = () => { len += 1; setText(target.slice(0, len)); if (len >= target.length) { setPhase('hold'); } else to = setTimeout(type, 32); }; to = setTimeout(type, 40); } return () => clearTimeout(to); }, [phase, idx, items, interval, reduce]); return (
{text}
); } // expose Object.assign(window, { LangProvider, useLang, useReveal, useReducedMotion, CustomCursor, useMagnetic, useSpotlight, Nav, BootScreen, TypingRotator });