// waiv-journey.jsx — one continuous scroll story. A single card persists across // three beats: HERO (right) → X-RAY (center, surface dissolves) → TAP (card // shrinks onto a phone) → ONELYNK (profile builds in). All driven by scroll. function WaivJourney() { const { useState, useRef, useEffect } = React; const [finish, setFinish] = useState('midnightgold'); const [revealed, setRevealed] = useState(false); const revRef = useRef(false); const trackRef = useRef(null); const stickyRef = useRef(null); const posRef = useRef(null); // card position wrapper const tiltRef = useRef(null); // wv-card3d-tilt (rotation) const phoneRef = useRef(null); const screenRef = useRef(null); const copyRef = useRef(null); const cueRef = useRef(null); const stat1Ref = useRef(null); const stat2Ref = useRef(null); const capRefs = useRef([]); const dotRefs = useRef([]); useEffect(() => { const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); const ease = t => t * t * (3 - 2 * t); const es = (p, a, b) => ease(clamp((p - a) / (b - a), 0, 1)); const mix = (a, b, t) => a + (b - a) * t; const mobile = () => window.matchMedia('(max-width:1080px)').matches; let raf; const tick = (now) => { const track = trackRef.current, sticky = stickyRef.current; if (track && sticky && !mobile()) { const r = track.getBoundingClientRect(); const vh = window.innerHeight; const p = clamp(-r.top / (r.height - vh), 0, 1); const t = now / 1000; // ── segments ── const sHC = es(p, 0.16, 0.42); // hero → center const sCT = es(p, 0.46, 0.64); // center → tap const sTP = es(p, 0.66, 0.82); // tap → park // ── card position / scale ── let cx = mix(20, 0, sHC); cx = mix(cx, 0, sCT); cx = mix(cx, 27, sTP); let cy = mix(0, 2, sHC); cy = mix(cy, -13, sCT); cy = mix(cy, -30, sTP); let cs = mix(1, 1.06, sHC); cs = mix(cs, 0.42, sCT); cs = mix(cs, 0.26, sTP); cy += Math.sin(t * 1.1) * 0.55 * (1 - sHC); // idle bob in hero only if (posRef.current) posRef.current.style.transform = `translate(-50%,-50%) translate3d(${cx.toFixed(2)}vw,${cy.toFixed(2)}vh,0) scale(${cs.toFixed(3)})`; // ── card rotation ── let rx = mix(7, 4, sHC); rx = mix(rx, 17, sCT); rx = mix(rx, 8, sTP); let ry = mix(-22, 6, sHC); ry = mix(ry, -12, sCT); ry = mix(ry, -20, sTP); let rz = mix(2, 1, sHC); rz = mix(rz, -7, sCT); rz = mix(rz, 8, sTP); if (tiltRef.current) tiltRef.current.style.transform = `rotateX(${rx.toFixed(2)}deg) rotateY(${ry.toFixed(2)}deg) rotateZ(${rz.toFixed(2)}deg)`; // ── x-ray reveal window (face dissolves to internals) ── const wantReveal = p > 0.24 && p < 0.45; if (wantReveal !== revRef.current) { revRef.current = wantReveal; setRevealed(wantReveal); } // ── phone enter ── const pe = es(p, 0.50, 0.66); if (phoneRef.current) { const px = mix(120, 0, pe), ps = mix(0.95, 1, pe); phoneRef.current.style.transform = `translate(-50%,-50%) translate3d(${px.toFixed(2)}vw,0,0) scale(${ps.toFixed(3)})`; phoneRef.current.style.opacity = pe.toFixed(3); } // profile build-in if (screenRef.current) screenRef.current.classList.toggle('is-built', p > 0.72); // floating stats const so = es(p, 0.80, 0.96); if (stat1Ref.current) { stat1Ref.current.style.opacity = so.toFixed(3); stat1Ref.current.style.transform = `translateY(${(1 - so) * 16}px)`; } if (stat2Ref.current) { stat2Ref.current.style.opacity = so.toFixed(3); stat2Ref.current.style.transform = `translateY(${(1 - so) * 16}px)`; } // tap pulse sticky.classList.toggle('is-tapping', p > 0.60 && p < 0.80); // hero copy + cue fade const heroOp = 1 - es(p, 0.08, 0.18); if (copyRef.current) { copyRef.current.style.opacity = heroOp.toFixed(3); copyRef.current.style.pointerEvents = heroOp < 0.1 ? 'none' : 'auto'; copyRef.current.style.transform = `translateY(calc(-50% + ${(-(1 - heroOp) * 30).toFixed(1)}px))`; } if (cueRef.current) cueRef.current.style.opacity = heroOp.toFixed(3); // ── captions + dots ── const phase = p >= 0.74 ? 2 : p >= 0.55 ? 1 : p >= 0.26 ? 0 : -1; capRefs.current.forEach((el, i) => el && el.classList.toggle('is-on', i === phase)); dotRefs.current.forEach((el, i) => el && el.classList.toggle('is-on', phase >= 0 && i <= phase)); } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); const cfg = WAIV_FINISHES[finish]; const caps = [ <>Beneath the finish — a copper antenna and a secure NTAG 424 chip.>, <>Hold it to any phone. No app. No scanning.>, <>And your whole world arrives — in a single tap.>, ]; return ( {/* Beat A — hero copy */} SHARE YOUR STORY · NFC Make thefirst moveunforgettable. One premium card with a secure NFC chip inside. Tap any phone and your whole world arrives — contacts, socials, payments, portfolio. Design your card Browse the range → {WAIV_FINISH_ORDER.map(id => ( setFinish(id)} aria-label={WAIV_FINISHES[id].name} title={WAIV_FINISHES[id].name} /> ))} Finish {cfg.name} {/* The single shared card */} {/* Phone + tap pulse */} Maya Okafor Product Designer · Studio North ⚡ via waiv Portfoliomaya.design Book a coffeecal.com/maya Instagram@maya.makes Save contact 1.2kprofileviews +318contactssaved {/* captions */} {caps.map((c, i) => capRefs.current[i] = el}>{c})} {[0, 1, 2].map(i => dotRefs.current[i] = el} />)} Scroll ); } window.WaivJourney = WaivJourney;
One premium card with a secure NFC chip inside. Tap any phone and your whole world arrives — contacts, socials, payments, portfolio.
Product Designer · Studio North
capRefs.current[i] = el}>{c}