// final-hooks.jsx — animaciones reutilizables para la landing final // Smooth scroll-driven progress (0..1) for an element relative to viewport function useScrollProgress(ref, mode = 'enter-leave') { const [p, setP] = React.useState(0); React.useEffect(() => { const el = ref.current; if (!el) return; const onScroll = () => { const r = el.getBoundingClientRect(); const vh = window.innerHeight || 800; let prog; if (mode === 'pin') { prog = Math.max(0, Math.min(1, -r.top / (r.height - vh))); } else { // 0 when element bottom enters bottom of viewport, 1 when top exits top prog = Math.max(0, Math.min(1, (vh - r.top) / (vh + r.height))); } setP(prog); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, [mode]); return p; } // Track viewport scroll as global percentage (0..1 of total page) function useGlobalScroll() { const [y, setY] = React.useState(0); React.useEffect(() => { const onScroll = () => setY(window.scrollY); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); return y; } // Letter-by-letter reveal — each char wraps in a span with stagger delay function RevealText({ text, delay = 0, stagger = 30, start = true, as = 'span', style = {}, className = '', italic = false, gradient = false }) { const Tag = as; const words = text.split(' '); let globalIndex = 0; return ( {words.map((word, wi) => { const chars = Array.from(word); const startIndex = globalIndex; globalIndex += chars.length + 1; return ( {chars.map((ch, i) => ( {ch} ))} {wi < words.length - 1 ? '\u00A0' : ''} ); })} ); } // Word-by-word reveal — words slide up + fade function RevealWords({ text, delay = 0, stagger = 80, start = true, style = {}, className = '' }) { const words = text.split(' '); return ( {words.map((w, i) => ( {w} ))} ); } // Parallax wrapper — translates Y by speed * scroll-delta-since-enter function Parallax({ speed = 0.2, children, style = {}, className = '' }) { const ref = React.useRef(null); const [y, setY] = React.useState(0); React.useEffect(() => { const el = ref.current; if (!el) return; const onScroll = () => { const r = el.getBoundingClientRect(); const vh = window.innerHeight; // -1 (entering) → 1 (leaving) const center = (r.top + r.height / 2 - vh / 2) / vh; setY(center * speed * 100); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, [speed]); return (
{children}
); } // Magnetic hover — element drifts toward cursor (respects context disable flag) function Magnetic({ strength = 0.4, children, style = {}, ...rest }) { const ctx = (window.TweaksCtx && React.useContext(window.TweaksCtx)) || {}; const enabled = ctx.magneticHover !== false; const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el || !enabled) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const dx = e.clientX - (r.left + r.width / 2); const dy = e.clientY - (r.top + r.height / 2); el.style.transform = `translate3d(${dx * strength}px, ${dy * strength}px, 0)`; }; const onLeave = () => { el.style.transform = 'translate3d(0,0,0)'; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; }, [strength, enabled]); return
{children}
; } // Inject keyframes once if (typeof document !== 'undefined' && !document.getElementById('final-keyframes')) { const s = document.createElement('style'); s.id = 'final-keyframes'; s.textContent = ` @keyframes revealChar { to { opacity: 1; transform: translateY(0) rotateX(0); } } @keyframes revealWord { to { opacity: 1; transform: translateY(0); } } @keyframes finalPulse { 0%,100% { opacity:.55 } 50% { opacity:1 } } @keyframes finalBlink { 0%,49% { opacity:1 } 50%,100% { opacity:0 } } @keyframes finalMarquee { from { transform: translateX(0) } to { transform: translateX(-50%) } } @keyframes finalGridPan { from { background-position: 0 0; } to { background-position: 80px 80px; } } @keyframes finalScan { 0% { top: -2px; opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { top: 100%; opacity: 0; } } @keyframes finalGlow { 0%,100% { box-shadow: 0 0 32px rgba(33,153,234,0.45); } 50% { box-shadow: 0 0 56px rgba(33,153,234,0.7); } } @keyframes finalFadeUp { from { opacity:0; transform: translateY(40px) } to { opacity:1; transform: none } } @keyframes pfWordCycle { from { opacity: 0; filter: blur(8px); transform: translateY(10px); } to { opacity: 1; filter: blur(0); transform: none; } } @keyframes finalFloat { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-12px) } } @keyframes finalRotate { from { transform: rotate(0) } to { transform: rotate(360deg) } } @keyframes heroNumBreath { 0%,100% { opacity: 0.18; -webkit-text-stroke-color: rgba(120,140,210,0.10); text-shadow: none; } 40% { opacity: 0.38; -webkit-text-stroke-color: rgba(33,153,234,0.35); text-shadow: 0 0 120px rgba(33,153,234,0.12); } 70% { opacity: 0.26; -webkit-text-stroke-color: rgba(120,200,255,0.22); text-shadow: 0 0 200px rgba(120,200,255,0.08); } } .final-fadeup { opacity: 0; } .final-fadeup.in { animation: finalFadeUp 1s cubic-bezier(.16,.84,.3,1) forwards; } `; document.head.appendChild(s); } // Rotating word — smooth blur/fade crossfade between words on an interval. // (Previously a character-scramble; replaced with a calmer transition that // matches the brand's restrained motion language and can't collide visually // with the sibling text.) function GlitchWord({ words, interval = 3500, gradient = false, style = {} }) { const [idx, setIdx] = React.useState(0); const [tick, setTick] = React.useState(0); const wordsRef = React.useRef(words); wordsRef.current = words; React.useEffect(() => { const iv = setInterval(() => { setIdx(i => (i + 1) % wordsRef.current.length); setTick(t => t + 1); }, interval); return () => clearInterval(iv); }, [words.join('|'), interval]); const gradientStyle = gradient ? { background: 'linear-gradient(135deg, #2199ea 0%, #63b7f0 60%, #e7ecff 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' } : {}; return ( {words[idx] || ''} ); } Object.assign(window, { useScrollProgress, useGlobalScroll, RevealText, RevealWords, Parallax, Magnetic, GlitchWord });