/* global React */ const { useEffect, useRef, useState } = React; // ================================================================= // §3 SERVICES JOURNEY — sticky phone, scroll-driven step // ================================================================= const JOURNEY_STEPS = [ { num: "01", id: "click", eyebrow: "LIVE IN UNDER 48 HOURS", ttl: <>Built in 24 hours. Bookings by Day 2., body: "Day 1 we plug in. Day 2 your ads are live. Multiple hyper-targeted ads and content pieces drop in front of the exact people you want to land — not browsers, not tire-kickers. They tap. They qualify, apply, or call. They book before doubt sets in.", points: [ "Full creative + landing page + CRM live in <48 hours", "Multiple ads + content · multiple angles, daily testing", "Lead lands in your CRM in <30 seconds — Sophie's already calling", ], }, { num: "02", id: "followup", eyebrow: "SOPHIE CALLS IN UNDER 30 SECONDS", ttl: <>Sophie calls. Texts. Every lead — constantly., body: "You do nothing. Sophie does everything. Every opt-in, form submission, and phone call — Sophie picks it up in under 30 seconds. No pickup on the call? She texts. She qualifies, books, and filters tire-kickers before they hit your calendar. 60% more bookings. Zero wasted time. Zero missed leads.", points: [ "Trained on your business, offer & objections · male or female voice (your call)", "Picks up every opt-in, form, and phone call · 24/7", "Books only qualified leads · auto-handles reschedules, no-shows, deposits", ], }, { num: "03", id: "booking", eyebrow: "BOOKED & PAID IN UNDER 60 SECONDS", ttl: <>Booked. Paid. Ready to show up., body: "The booking lands on your team's calendar — instantly. Everyone on your team gets pinged with the full file. Deposit auto-charged. Forms auto-sent. Whether it's a consult, sales call, or install — they're qualified, paid, and locked in before the day arrives.", points: [ "Your whole team notified the second they book — Slack + iOS", "Auto-charged deposit · cuts no-shows 70%", "Intake forms / contracts / pre-call docs sent automatically", ], }, { num: "04", id: "flip", eyebrow: "ALWAYS ON — WE NEVER STOP", ttl: <>We work for you. 24/7., body: "Your account isn't set-and-forget — it's set-and-scaled. New ad sets & content every week. Daily optimization. Replies under 60 seconds. We're in your account daily — killing what doesn't work, doubling down on what does. Treating it like our own.", points: [ "New ad sets & content every week · multiple angles, hooks, formats", "Daily optimization · pause losers, scale winners", "Slack replies in <1 minute · daily standups on your account", "You own every ad account · Meta, Google, TikTok, YouTube — forever", ], }, ]; function MetaFrame({ progress }) { // progress 0..1 within step 0 const typed = "555-0142"; const focusedNum = progress > 0.4; const submitting = progress > 0.7; return (
drjoeyalcantara.com/back-pain
🔒
JA
Dr. Joey Alcantara
★ 4.9 · 1,284 reviews · Calgary
FREE BACK PAIN ASSESSMENT

Stop the back pain.
Without surgery.

30-min consult with Dr. Joey. No referral. Same-week openings.

Sarah Mendez
sarah.mendez@gmail.com
{focusedNum ? "(403) " + typed : "Phone number"} {focusedNum && !submitting && }
🔒 HIPAA secure · No spam · Book in 60s
); } function IMessageScreen({ progress }) { const messages = [ { side: "in", text: "Hey Sarah! It's Sophie from Dr. Joey's office.", showAt: 0.0 }, { side: "in", text: "Want me to lock in tomorrow at 2pm or Thursday at 10am for your back assessment?", showAt: 0.15, same: true }, { side: "out", text: "Tomorrow at 2 works 🙏", showAt: 0.45 }, { side: "in", text: "All booked. See you then 🙌", showAt: 0.7 }, { side: "in", text: "I'll text the address + intake form now.", showAt: 0.78, same: true }, ]; const visible = messages.filter(m => progress >= m.showAt); const showTyping = progress > 0.85 && progress < 1; return (
S
Sophie · Ad Scaling AI
iMessage · just now
{visible.map((m, i) => (
{m.text}
))} {showTyping && (
)}
); } function PushScreen({ progress }) { // 4 weeks, mark booked days const days = Array.from({ length: 28 }, (_, i) => i + 1); const booked = [3, 8, 12, 17, 19, 22]; const newBook = 22; const showCard = progress > 0.2; return (
2:14
Tuesday, March 19
{showCard && (
📅
DR. JOEY · BOOKINGS now
📅 New booking · Sarah Mendez
Tomorrow 2pm · Dr. Joey's office · $50 deposit charged
)}
March 47 booked
{days.map(d => (
0.5 ? " new" : "")}> {d}
))}
); } function YourAccount({ progress }) { return (
YOUR ACCOUNT · LIVE ● ON
Dr. Joey Alcantara
Renuva Spine & Wellness · Calgary · April 2026
Leads · April
324
↑ 1.6× vs Feb
CPL · April
$4.70
vs $3.41 in Mar
CPL · trailing 3 months
$3.96 avg
{/* Feb $3.78 → Mar $3.41 (peak/low) → Apr $4.70 — lower is better, so invert */}
AD SPEND · APRIL
$1,524
COST PER LEAD
$4.70
↑ $1.29
CTR
4.86%
REACH
40,093
3-MONTH TRAJECTORY
FEB323 leads · $3.78 CPL$1,222
MAR442 leads · $3.41 CPLPEAK
APR324 leads · $4.70 CPL$1,524
META · MESSAGING ADS SOURCE: META ADS MANAGER
); } function Journey() { const [step, setStep] = useState(0); const [stepProgress, setStepProgress] = useState(0); const stepRefs = useRef([]); const railRef = useRef(null); const phoneStickyRef = useRef(null); // Mobile: per-step animated progress (0→1 driven by IntersectionObserver) const [mobileProgress, setMobileProgress] = useState([0, 0, 0, 0]); const mobileStepRefs = useRef([]); useEffect(() => { const cleanups = mobileStepRefs.current.map((el, i) => { if (!el) return null; let started = false; const obs = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !started) { started = true; let t0 = null; const DURATION = 1500; const tick = (ts) => { if (!t0) t0 = ts; const p = Math.min((ts - t0) / DURATION, 1); setMobileProgress(prev => { const next = [...prev]; next[i] = p; return next; }); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); obs.disconnect(); } }, { threshold: 0.25 }); obs.observe(el); return () => obs.disconnect(); }); return () => cleanups.forEach(c => c && c()); }, []); useEffect(() => { const onScroll = () => { let active = 0; let progress = 0; stepRefs.current.forEach((el, i) => { if (!el) return; const r = el.getBoundingClientRect(); const vh = window.innerHeight; const center = r.top + r.height / 2; if (center < vh * 0.7 && r.bottom > 0) { active = i; const p = (vh * 0.7 - center) / (vh * 0.4); progress = Math.max(0, Math.min(1, p)); } }); setStep(active); setStepProgress(progress); // JS sticky: manually translateY the phone to stay in view the full journey if (railRef.current && phoneStickyRef.current) { const railRect = railRef.current.getBoundingClientRect(); const phoneH = phoneStickyRef.current.offsetHeight; const TOP = 80; // px from viewport top const rawY = TOP - railRect.top; const maxY = railRect.height - phoneH; const translateY = Math.max(0, Math.min(rawY, maxY)); phoneStickyRef.current.style.transform = `translateY(${translateY}px)`; } }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const getPhoneFaceAt = (i, p) => { if (i === 0) return ; if (i === 1) return ; if (i === 2) return ; return ; }; const getPhoneFace = (i) => { const p = i === step ? stepProgress : (i < step ? 1 : 0); return getPhoneFaceAt(i, p); }; return (
ONE REAL CUSTOMER

Our Scaling System In Action.
Watch what happens.

{/* MOBILE layout — shown via CSS at ≤920px */}
{JOURNEY_STEPS.map((s, i) => (
mobileStepRefs.current[i] = el}> {/* 1. STEP HEADER — above phone */}
STEP {s.num} {s.eyebrow}
{/* 2. PHONE — middle */}
{getPhoneFaceAt(i, mobileProgress[i])}
{/* 3. BODY TEXT — below phone */}

{s.ttl}

{s.body}

    {s.points.map((p, j) =>
  • {p}
  • )}
))}
{/* DESKTOP layout — shown via CSS at >920px */}
{getPhoneFace(step)}
{JOURNEY_STEPS.map((s, i) => (
stepRefs.current[i] = el}>
STEP {s.num} {s.eyebrow}

{s.ttl}

{s.body}

    {s.points.map((p, j) =>
  • {p}
  • )}
))}
); } Object.assign(window, { Journey });