From b49a5e72b548ea9c5206a7f2b00160ba8f8fa43c Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 30 Jun 2026 23:52:20 -0700 Subject: [PATCH] [eric] onboarding: press-and-hold to launch (warm bloom grows to fill screen, ported from glowup CommitmentView) as the payoff commit transition --- .../Onboarding/flow/HoldToLaunch.tsx | 124 ++++++++++++++++++ .../app/components/Onboarding/flow/Payoff.tsx | 18 +-- 2 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 frontend/src/app/components/Onboarding/flow/HoldToLaunch.tsx diff --git a/frontend/src/app/components/Onboarding/flow/HoldToLaunch.tsx b/frontend/src/app/components/Onboarding/flow/HoldToLaunch.tsx new file mode 100644 index 00000000..e5755dd1 --- /dev/null +++ b/frontend/src/app/components/Onboarding/flow/HoldToLaunch.tsx @@ -0,0 +1,124 @@ +// Press-and-hold to launch, ported from the glowup CommitmentView. Holding fills a progress ring +// AND grows a warm radial bloom from the button until it covers the screen, then hands off (a clean +// "commit -> go" transition into the running agent). Release early and it shrinks back. +// Reduced motion: a plain button, no bloom, instant. + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useReducedMotion } from '@/shared/hooks/useReducedMotion'; +import { useOnboardingSkin } from './onboardingSkin'; + +const HOLD_MS = 1100; + +export const HoldToLaunch: React.FC<{ label: string; onLaunch: () => void }> = ({ label, onLaunch }) => { + const S = useOnboardingSkin(); + const reduce = useReducedMotion(); + const btnRef = useRef(null); + const rafRef = useRef(null); + const startRef = useRef(0); + const [progress, setProgress] = useState(0); + const [committed, setCommitted] = useState(false); + const [origin, setOrigin] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + + const stop = useCallback(() => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + }, []); + + const commit = useCallback(() => { + stop(); + setCommitted(true); + // Let the bloom cover the screen (600ms) before handing off. + window.setTimeout(onLaunch, 600); + }, [stop, onLaunch]); + + const tick = useCallback(() => { + const p = Math.min(1, (performance.now() - startRef.current) / HOLD_MS); + setProgress(p); + if (p >= 1) { commit(); return; } + rafRef.current = requestAnimationFrame(tick); + }, [commit]); + + const begin = useCallback(() => { + if (committed) return; + if (reduce) { commit(); return; } + const r = btnRef.current?.getBoundingClientRect(); + if (r) setOrigin({ x: r.left + r.width / 2, y: r.top + r.height / 2 }); + startRef.current = performance.now(); + rafRef.current = requestAnimationFrame(tick); + }, [committed, reduce, commit, tick]); + + const cancel = useCallback(() => { + if (committed) return; + stop(); + setProgress(0); + }, [committed, stop]); + + useEffect(() => () => stop(), [stop]); + + // Bloom radius: covers the screen from the button center once committed. + const maxR = Math.hypot( + Math.max(origin.x, window.innerWidth - origin.x), + Math.max(origin.y, window.innerHeight - origin.y), + ) * 1.25; + const bloomR = committed ? maxR : progress * maxR; + + return ( + <> + {/* Warm bloom growing from the button center; ignores pointer so the hold isn't interrupted. */} + {(progress > 0 || committed) && ( +
+ )} + + + ); +}; diff --git a/frontend/src/app/components/Onboarding/flow/Payoff.tsx b/frontend/src/app/components/Onboarding/flow/Payoff.tsx index 31437e6e..12e75541 100644 --- a/frontend/src/app/components/Onboarding/flow/Payoff.tsx +++ b/frontend/src/app/components/Onboarding/flow/Payoff.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { useOnboardingSkin } from './onboardingSkin'; import { LineIcon } from './OnboardingIcons'; import { Heading } from './OnboardingAtoms'; +import { HoldToLaunch } from './HoldToLaunch'; import type { PayoffIdea } from './onboardingFlowTypes'; const IdeaRow: React.FC<{ idea: PayoffIdea; onPick: () => void }> = ({ idea, onPick }) => { @@ -75,21 +76,8 @@ export const Payoff: React.FC<{ paddingTop: 14, }} > - a real agent goes and does this, live - - Do it → - + hold to send a real agent on it, live +