// lib.jsx — shared hooks const { useEffect, useRef, useState, useCallback, useMemo, useLayoutEffect } = React; function useCustomCursor() { useEffect(() => { const dot = document.getElementById('cursorDot'); const ring = document.getElementById('cursorRing'); if (!dot || !ring) 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; dot.style.transform = `translate3d(${mx - 4}px, ${my - 4}px, 0)`; }; const tick = () => { rx += (mx - rx) * 0.2; ry += (my - ry) * 0.2; ring.style.transform = `translate3d(${rx - 18}px, ${ry - 18}px, 0)`; raf = requestAnimationFrame(tick); }; const onOver = (e) => { const t = e.target; if (!t || !t.closest) return; if (t.closest('a, button, [data-hover], .clickable')) { ring.classList.add('hover'); } }; const onOut = (e) => { const t = e.target; if (!t || !t.closest) return; if (t.closest('a, button, [data-hover], .clickable')) { ring.classList.remove('hover'); } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseover', onOver); document.addEventListener('mouseout', onOut); raf = requestAnimationFrame(tick); return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseover', onOver); document.removeEventListener('mouseout', onOut); cancelAnimationFrame(raf); }; }, []); } function useReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal, .reveal-word'); const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add('in'); }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); els.forEach((el) => obs.observe(el)); return () => obs.disconnect(); }); } function SplitText({ children, as = 'span', delay = 0, className = '', style = {} }) { const Tag = as; const words = String(children).split(/(\s+)/); return ( {words.map((w, i) => /\s+/.test(w) ? ( {w} ) : ( {w} ) )} ); } function useScrollY() { const [y, setY] = useState(0); useEffect(() => { let raf; const tick = () => { setY(window.scrollY); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); return y; } function useMouse() { const [m, setM] = useState({ x: 0.5, y: 0.5 }); useEffect(() => { const onMove = (e) => setM({ x: e.clientX / window.innerWidth, y: e.clientY / window.innerHeight, }); window.addEventListener('mousemove', onMove); return () => window.removeEventListener('mousemove', onMove); }, []); return m; } function Magnetic({ children, strength = 0.35, className = '', style = {} }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) 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); el.style.transform = `translate3d(${x * strength}px, ${y * strength}px, 0)`; }; const onLeave = () => { el.style.transform = ''; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength]); return ( {children} ); } function useTiltScroll(ref) { const [p, setP] = useState(0); useEffect(() => { let raf; const tick = () => { if (ref.current) { const r = ref.current.getBoundingClientRect(); const vh = window.innerHeight; const total = r.height + vh; const passed = vh - r.top; setP(Math.max(0, Math.min(1, passed / total))); } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [ref]); return p; } /* ────────────────────────────────────────────────────────── Icon — wrapper para Lucide (carga vía CDN en index.html). Renderiza y dispara lucide.createIcons() tras el mount para reemplazar por SVG inline. Reemplaza emojis en toda la plataforma. ────────────────────────────────────────────────────────── */ function Icon({ name, size = 18, color, strokeWidth = 2, style = {}, ariaLabel }) { const ref = useRef(null); useEffect(() => { if (typeof window === 'undefined' || !window.lucide) return; try { window.lucide.createIcons({ icons: window.lucide.icons, attrs: {} }); } catch (e) {} }, [name, size, color]); return ( ); } // Refresca todos los íconos Lucide del DOM (incluye data-icon dispersos) function refreshLucide() { if (typeof window === 'undefined' || !window.lucide) return; document.querySelectorAll('[data-icon]:not([data-lucide])').forEach(el => { el.setAttribute('data-lucide', el.dataset.icon); if (el.dataset.size) el.style.width = el.style.height = el.dataset.size + 'px'; if (el.dataset.color) el.style.color = el.dataset.color; }); try { window.lucide.createIcons(); } catch (e) {} } Object.assign(window, { useCustomCursor, useReveal, SplitText, useScrollY, useMouse, Magnetic, useTiltScroll, Icon, refreshLucide });