Piggy Bank
A gravity-driven savings drop animated coins fall from your quick-amount pad into a reactive piggy bank.
Physics-y coin fall
Staggered delay, rotateY flip loop, sideways drift, spring-settle, scale+fade absorb.
Self-measuring coin slot
Piggy bank measures its own slot position via UI-thread measure(), no hardcoded coordinates.
Imperative piggy reactions
jiggle() per coin, proudPuff() (spring overshoot + success haptic) on full deposit.
Race-safe save sync
Success only fires once both the coin animation and your onSave promise resolve; a 3s grace timer catches slow API calls without ever double-firing.
Installation
Copy the command directly into your project.
Install the needed dependencies
Use in your app
Built to match the preview exactly — for the complete implementation, check the repo on GitHub.
import { useCallback, useState } from 'react'
import GravitySavings from '../../components/piggy-bank/components/GravitySavings'
const DEMO_AVATAR = require('../../components/piggy-bank/assets/images/Avatar.png')
const DEMO_INITIAL_SAVINGS = 3600
const SAVE_SIMULATION_DELAY = 1200
export default function GravitySavingsDemo() {
const [currentSavings, setCurrentSavings] = useState(DEMO_INITIAL_SAVINGS)
const handleSave = useCallback(async (amount: number) => {
// Replace with a real deposit/payment API call.
await new Promise<void>((resolve) => setTimeout(resolve, SAVE_SIMULATION_DELAY))
setCurrentSavings((prev) => prev + amount)
}, [])
const handleError = useCallback((error: unknown) => {
console.warn('Save failed:', error)
}, [])
return (
<GravitySavings
currentSavings={currentSavings}
userName="ManasCodeXart"
userAvatar={DEMO_AVATAR}
onSave={handleSave}
onError={handleError}
/>
)
}API
<GravitySavings>
| Prop | Type | Default | Description |
|---|---|---|---|
| currentSavings | number | — | Balance shown at the top. Controlled — update it yourself inside onSave. |
| userName | string | — | Shown on the success sheet. |
| userAvatar | ImageSourcePropType | — | Optional. Shown next to the balance and on the success sheet. |
| currencySymbol | string | '$' | Prefix used everywhere an amount is shown. |
| quickAmounts | readonly QuickAmountPill[] | $100 / $500 / $1000 | Pills above the keypad. |
| hapticsEnabled | boolean | true | Enables keypad, jiggle, and success haptics. |
| onSave | (amount: number) => void | Promise<void> | — | Required. Called on Save tap. Both the coin animation and this promise must resolve before success shows. |
| onDone | () => void | — | Called when the user dismisses the success sheet's Done button. |
| onReturnHome | () => void | — | Optional. Adds a secondary Return to Home link to the success sheet. |
| onError | (error: unknown) => void | — | Called if onSave rejects or times out after 3 seconds. |
currentSavings is a controlled prop — GravitySavings never updates your balance internally. Add the saved amount to your own state inside onSave.
Types
interface QuickAmountPill {
readonly label: string;
readonly value: number;
}
type CoinDropState = 'idle' | 'dropping' | 'success' | 'error';