About this animation
A fitness challenge pot with arriving avatars and a balance-to-entry flight, with configurable challenges, colors, labels, and fonts.
Why this one
A static prize amount gives no sense of the people behind it. Here each arrival has a visible cause and effect: an avatar flies into the stack, the participant count increases, and the pot gains one stake. Your own entry follows a coin flight and gains a green avatar ring. All financial changes are local display simulations; the host owns payment and persistence.
How it works
- Artwork mounts eagerly at its final view size with memory caching. All cards wait for image display (or a load error), then enter with a 260 ms UI-thread fade/translation staggered by 70 ms. Blur intensity and shadows stay fixed. Exercise loops, live events, and demo autoplay start after this entrance;
onReadylets hosts synchronize their own events. The bundled 477×316 artwork already covers the maximum 150 pt width at 3× density. - Challenge data initializes a transaction snapshot with previous and next amounts, counts, and avatar initials.
- A single arrival shared value drives the rolling characters, avatar flight, and green light along the glass edge. React updates only for transactions, layout, navigation, and the minute clock.
- Each character has an invisible measurement copy and two clipped text layers. Unchanged characters stay still; changed columns end at their exact destination. Punctuation uses its actual glyph width.
- Tapping a card expands its existing surface with a nearly critical spring and lowers the other cards. A downward swipe or the back button closes it.
- Joining reserves the available balance, sends a coin into the pot, then commits the balance and calls
onJoinonce. Repeated entries are ignored. A reset cancels the pending flight and restores initial data. - The demo owns its autoplay schedule: arrival at 0.55 seconds after readiness, second arrival at 2.75 seconds, expand at 3.9 seconds, join at 4.7 seconds, return at 7.2 seconds, reset at 9.5 seconds. Direct input stops autoplay.
Install
npx expo install expo-image expo-blur expo-linear-gradient @shopify/react-native-skia react-native-gesture-handler react-native-reanimated react-native-workletsCopy ChallengePotCard.tsx, icons.ts, and index.ts. Keep the application's root inside GestureHandlerRootView. Load any supplied fonts in the host. For Skia on web, initialize CanvasKit with the host's existing Skia web setup before mounting. The demo artwork is local and separate from the reusable component; its transparent margins have been cropped. Icons: Lucide (ISC).
Usage
import { ChallengePotCard } from './challenge-pot-card';
<ChallengePotCard
challenges={[
{
id: 'daily-reps',
title: '100 Push-ups · 7 days',
image: require('./exercise.png'),
pot: 1840,
progress: 0.6,
participants: 42,
stake: 25,
endsAt: Date.now() + 32 * 3600000,
avatars: ['MK', 'JR', 'SP'],
},
]}
balance={120}
onJoin={(id) => saveChallengeEntry(id)}
/>With external live events and personality controls:
const card = useRef<ChallengePotCardRef>(null);
<ChallengePotCard
ref={card}
challenges={challenges}
balance={120}
liveInterval={0}
arrivalDuration={580}
blurIntensity={48}
fontFamily="YourInterfaceFont"
monoFontFamily="YourNumberFont"
labels={{ heading: 'Your challenges' }}
theme={{ accent: '#8BD94B', button: '#2F7BFF' }}
/>
// Call from an event handler or subscription:
card.current?.simulateJoiner('daily-reps');
card.current?.expand('daily-reps');
card.current?.join('daily-reps');Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
challenges | readonly Challenge[] | required | ID, title, image, initial pot, completion fraction, participant count, stake, end timestamp, avatar initials |
balance | number | required | Initial available funds |
onJoin | (id: string) => void | — | Local entry has settled; host handles persistence |
theme | Partial<ChallengePotTheme> | dark palette | Background, card, glass, border, text, muted, accent, button, button text, live, amber, avatar colors and avatar text |
labels | Partial<ChallengePotLabels> | American English | All structural copy and four navigation labels |
brand | ReactNode | — | Optional host lockup |
fontFamily / headingFontFamily / monoFontFamily | string | system | Interface, heading and numeral fonts |
formatMoney | (value: number) => string | whole US dollars | Currency and locale formatting |
formatRemaining | (minutes: number) => string | days, hours, minutes | Countdown formatting, refreshed every minute |
liveInterval | number | 2600 | Simulated live arrivals; 0 disables them |
joinerInitials | readonly string[] | AL, MK, JR, SP | Incoming avatar initials |
arrivalDuration | number | 580 | Avatar and coin flight duration, minimum 250 ms |
blurTint | BlurTint | dark | Glass tint; pair light with a light palette |
blurIntensity | number | 48 | Glass blur intensity |
onReady | () => void | — | Artwork has settled and the initial card entrance has finished; a failed image does not block the interface |
onInteraction | () => void | — | Stop host autoplay on input |
onTabPress | (index: number) => void | — | Host navigation; this component does not create routes |
announcement | (title, joined) => string | English arrival message | Accessible live announcement |
style / testID | StyleProp<ViewStyle> / string | — / challenge-pot-card | Container styling and stable control IDs |
The ref exposes join(id), simulateJoiner(id), expand(id), collapse(), and reset(). Unknown IDs, expired challenges, duplicate local entries, and entries without sufficient funds are ignored. Incoming arrivals during a pending local coin flight are ignored to keep the transaction callback atomic. Initial financial values are snapshots: after replacing them, call reset() or remount. Use finite nonnegative amounts, unique IDs, and completion fractions from 0 to 1.
Layout and safe areas
Android SDK 31+ uses BlurTargetView and dimezisBlurViewSdk31Plus; older Android versions retain the tinted material fallback.
The root fills its allocated container and centers cards at a maximum width of 520 pt. Short layouts use compact cards and vertical scrolling; expanded content remains scrollable in a 300×400 container or landscape. Artwork never exceeds 150 pt wide. The host owns safe-area insets. The demo leaves the upper-right viewer close-button area empty. At larger accessibility text sizes, scroll to reach the lower content.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses modules bundled in the project's Expo Go setup. Supports iOS, Android, and web. No browser capture or server is created by this delivery.
Accessibility
Tap targets are at least 48 pt. Buttons and cards expose accessible roles, labels, activation actions, and stable test IDs. Joining and live arrivals announce changes. Reduced motion removes coin flights, avatar arcs, edge sweeps, and exercise loops, replacing arrivals with 200 ms fades. A tap or back button provides an alternative to the downward close gesture.



