// ============================================================ // PACE — Primitivas de motion (Reveal, CountUp, Marquee, // Magnetic, Parallax, ScrollProgress, Cursor) // ============================================================ const { useState, useEffect, useRef, useCallback } = React; // ---- useInView hook (rect-based: fiable en iframes y capturas) ---- console.log("pace-motion v2 (rect-based reveals)"); function useInView(opts) { const ref = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; let done = false; const check = () => { if (done || !ref.current) return; const r = ref.current.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < vh * 0.92 && r.bottom > 0) { done = true; setInView(true); window.removeEventListener("scroll", check); window.removeEventListener("resize", check); } }; // chequeo inicial tras el primer layout const t = setTimeout(check, 60); window.addEventListener("scroll", check, { passive: true }); window.addEventListener("resize", check); return () => { clearTimeout(t); window.removeEventListener("scroll", check); window.removeEventListener("resize", check); }; }, []); return [ref, inView]; } // ---- Reveal ---- function Reveal({ children, delay, as, className, style }) { const [ref, inView] = useInView(); const Tag = as || "div"; return ( {children} ); } // ---- CountUp ---- function CountUp({ end, prefix, suffix, duration }) { const [ref, inView] = useInView({ threshold: 0.6 }); const [val, setVal] = useState(0); useEffect(() => { if (!inView) return; const dur = duration || 1600; const t0 = performance.now(); let raf; const tick = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 4); setVal(Math.round(end * eased)); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [inView, end]); return ( {(prefix || "") + val.toLocaleString("es-CO") + (suffix || "")} ); } // ---- Marquee ---- function Marquee({ words, dark }) { const items = []; for (let r = 0; r < 3; r++) { words.forEach((w, i) => { items.push( {w} / ); }); } return (
{items}
); } // ---- Magnetic button wrapper ---- function Magnetic({ children, strength }) { const ref = useRef(null); const s = strength == null ? 0.25 : strength; const onMove = useCallback( (e) => { const el = ref.current; if (!el || window.matchMedia("(pointer: coarse)").matches) return; 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 = "translate(" + x * s + "px," + y * s + "px)"; }, [s] ); const onLeave = useCallback(() => { const el = ref.current; if (!el) return; el.style.transition = "transform 0.6s cubic-bezier(0.22,1,0.36,1)"; el.style.transform = "translate(0,0)"; setTimeout(() => { if (ref.current) ref.current.style.transition = ""; }, 600); }, []); return ( {children} ); } // ---- Parallax image ---- function ParallaxImg({ src, alt, className, style, strength }) { const wrapRef = useRef(null); const imgRef = useRef(null); const k = strength == null ? 0.07 : strength; useEffect(() => { let raf = null; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; const wrap = wrapRef.current; const img = imgRef.current; if (!wrap || !img) return; const r = wrap.getBoundingClientRect(); const center = r.top + r.height / 2 - window.innerHeight / 2; img.style.transform = "translateY(" + center * -k + "px) scale(" + (1 + k * 2.2) + ")"; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, [k]); return (
{alt
); } // ---- Scroll progress bar ---- function ScrollProgress() { const ref = useRef(null); useEffect(() => { const onScroll = () => { const el = ref.current; if (!el) return; const h = document.documentElement; const p = h.scrollTop / (h.scrollHeight - h.clientHeight); el.style.width = p * 100 + "%"; }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return
; } // ---- Custom cursor ---- function CustomCursor({ enabled }) { const dotRef = useRef(null); const ringRef = useRef(null); useEffect(() => { if (!enabled || window.matchMedia("(pointer: coarse)").matches) { document.body.classList.remove("cursor-on"); return; } document.body.classList.add("cursor-on"); let rx = -100, ry = -100, tx = -100, ty = -100; let raf; const onMove = (e) => { tx = e.clientX; ty = e.clientY; if (dotRef.current) dotRef.current.style.transform = "translate(" + (tx - 3.5) + "px," + (ty - 3.5) + "px)"; const el = e.target.closest && e.target.closest("a, button, .goal-tab"); if (ringRef.current) ringRef.current.classList.toggle("hot", !!el); }; const loop = () => { rx += (tx - rx) * 0.16; ry += (ty - ry) * 0.16; if (ringRef.current) { const w = ringRef.current.offsetWidth; ringRef.current.style.transform = "translate(" + (rx - w / 2) + "px," + (ry - w / 2) + "px)"; } raf = requestAnimationFrame(loop); }; window.addEventListener("mousemove", onMove, { passive: true }); raf = requestAnimationFrame(loop); return () => { window.removeEventListener("mousemove", onMove); cancelAnimationFrame(raf); document.body.classList.remove("cursor-on"); }; }, [enabled]); if (!enabled) return null; return (
); } // ---- Speed lines decorativas (eco del logo) ---- function SpeedLines({ count, width, style, animate }) { const [ref, inView] = useInView({ threshold: 0.4 }); const n = count || 5; const lines = []; for (let i = 0; i < n; i++) { lines.push( ); } return (
{lines}
); } Object.assign(window, { useInView, Reveal, CountUp, Marquee, Magnetic, ParallaxImg, ScrollProgress, CustomCursor, SpeedLines, });