// 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
});