// waiv-home.jsx — homepage shell: Direction-B hero + scroll-reveal + nav stick.
const { useState: useStateH, useRef: useRefH, useEffect: useEffectH } = React;
function HomeHero() {
const [finish, setFinish] = useStateH('midnightgold');
const [revealed, setRevealed] = useStateH(false);
const tiltRef = useRefH(null);
const revealedRef = useRefH(false);
revealedRef.current = revealed;
// cursor-driven 3D tilt with spring easing
useEffectH(() => {
const el = tiltRef.current; if (!el) return;
const cur = { rx: 7, ry: -23 }, tgt = { rx: 7, ry: -23 };
let raf;
const onMove = (e) => {
const nx = e.clientX / window.innerWidth - 0.5;
const ny = e.clientY / window.innerHeight - 0.5;
tgt.ry = (revealedRef.current ? -12 : -23) + nx * 17;
tgt.rx = (revealedRef.current ? 5 : 7) - ny * 13;
};
const loop = () => {
cur.rx += (tgt.rx - cur.rx) * 0.08; cur.ry += (tgt.ry - cur.ry) * 0.08;
el.style.transform = `rotateX(${cur.rx.toFixed(2)}deg) rotateY(${cur.ry.toFixed(2)}deg) rotateZ(2deg)`;
raf = requestAnimationFrame(loop);
};
window.addEventListener('pointermove', onMove); raf = requestAnimationFrame(loop);
return () => { window.removeEventListener('pointermove', onMove); cancelAnimationFrame(raf); };
}, []);
useEffectH(() => {
const ev = new PointerEvent('pointermove', { clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 });
window.dispatchEvent(ev);
}, [revealed]);
const cfg = WAIV_FINISHES[finish];
return (
SHARE YOUR STORY · NFC
Make the first moveunforgettable.
One premium, UV-printed card with a secure NFC chip inside. Tap any phone and your whole world arrives — contacts, socials, payments, portfolio. No app required.
{WAIV_FINISH_ORDER.map(id => (
setFinish(id)}
aria-label={WAIV_FINISHES[id].name} title={WAIV_FINISHES[id].name} />
))}
Finish
{cfg.name}
setRevealed(v => !v)}>
{revealed ? 'Hide internals' : 'See inside the card'}
);
}
function Nav() {
const [stuck, setStuck] = useStateH(false);
const progRef = useRefH(null);
useEffectH(() => {
const onScroll = () => {
setStuck(window.scrollY > 40);
const max = document.documentElement.scrollHeight - window.innerHeight;
if (progRef.current) progRef.current.style.width = (max > 0 ? (window.scrollY / max) * 100 : 0) + '%';
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
Get your waiv
);
}
function App() {
// scroll reveal
useEffectH(() => {
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } });
}, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
const observeAll = () => document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(el => io.observe(el));
observeAll();
// re-scan for elements mounted later (e.g. gallery Wall, modal content)
const mo = new MutationObserver(observeAll);
mo.observe(document.body, { childList: true, subtree: true });
return () => { io.disconnect(); mo.disconnect(); };
}, []);
// scroll parallax — gentle drift on decorative layers
useEffectH(() => {
const els = [...document.querySelectorAll('[data-parallax]')].map(el => ({
el, speed: parseFloat(el.getAttribute('data-parallax')) || 0.08,
base: el.getAttribute('data-parallax-base') || '',
}));
if (!els.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let raf;
const loop = () => {
const h = window.innerHeight;
els.forEach(({ el, speed, base }) => {
const r = el.getBoundingClientRect();
const rel = (r.top + r.height / 2) - h / 2;
el.style.transform = `${base} translate3d(0, ${(-rel * speed).toFixed(1)}px, 0)`.trim();
});
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
// magnetic buttons
useEffectH(() => {
const onMove = (e) => {
document.querySelectorAll('.wv-magnetic').forEach(b => {
const r = b.getBoundingClientRect();
const dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
const d = Math.hypot(dx, dy), radius = 130;
if (d < radius) { const p = (1 - d / radius) * 10; b.style.transform = `translate(${(dx / d) * p || 0}px, ${(dy / d) * p - 2 || -2}px)`; }
else b.style.transform = '';
});
};
window.addEventListener('pointermove', onMove);
return () => window.removeEventListener('pointermove', onMove);
}, []);
// custom cursor: lagging ring + dot, reacts to links & draggables
useEffectH(() => {
if (!window.matchMedia('(hover:hover) and (pointer:fine)').matches) return;
document.body.classList.add('wv-cursor-on');
const wrap = document.createElement('div'); wrap.className = 'wv-cur';
const ring = document.createElement('div'); ring.className = 'wv-cur-ring';
const dot = document.createElement('div'); dot.className = 'wv-cur-dot';
wrap.append(ring, dot); document.body.appendChild(wrap);
let mx = innerWidth / 2, my = innerHeight / 2, rx = mx, ry = my, raf;
const linkSel = 'a, button, .wv-btn, .pg-card, .pg-dot, .so-bubble, .iw-step, .iw-replay, .ol-feat, .hb-finish-btn, .wv-finish-chip, .coll-arrow, [role="button"]';
const onMove = (e) => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px'; dot.style.top = my + 'px';
const t = e.target.closest ? e.target.closest(linkSel) : null;
const grab = e.target.closest ? e.target.closest('.bud') : null;
ring.classList.toggle('is-link', !!t && !grab);
dot.classList.toggle('is-link', !!t && !grab);
ring.classList.toggle('is-grab', !!grab);
};
const onLeave = () => wrap.classList.add('is-hidden');
const onEnter = () => wrap.classList.remove('is-hidden');
const loop = () => { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; ring.style.left = rx + 'px'; ring.style.top = ry + 'px'; raf = requestAnimationFrame(loop); };
window.addEventListener('pointermove', onMove);
document.addEventListener('mouseleave', onLeave);
document.addEventListener('mouseenter', onEnter);
raf = requestAnimationFrame(loop);
return () => { window.removeEventListener('pointermove', onMove); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); cancelAnimationFrame(raf); wrap.remove(); document.body.classList.remove('wv-cursor-on'); };
}, []);
return (
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );