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