Payment Reminder
A draggable reminder pill that morphs seamlessly into a full reminders sheet. Set a payment reminder in a modal sheet — pick a contact, date, month, and amount, hit 'Remind me' and it collapses into a floating draggable pill. Tap the pill and it grows directly into the reminders list using a headless morph-box primitive — no second modal, no popover, just the pill expanding into the sheet it already is.
Morph, not modal
Tapping the pill grows it directly into the reminders list via a headless morph-box primitive; the sheet always expands out of exactly where the pill is sitting, even after dragging.
Draggable pill
The reminder collapses into a floating pill you can pan anywhere on screen; dragging is disabled automatically while the sheet is open so the gesture never fights the morph.
Snapping avatar carousel
Contact picker with scale, opacity, and translate all driven by scroll position, haptic tick on every snap.
Race-safe confirm
A ref-backed guard blocks double-tap from creating two reminders off a single confirm.
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 { StyleSheet, View } from 'react-native'
import { PaymentReminder } from '../../components/payment-reminder/components'
export default function Index() {
return (
<View style={styles.container}>
<PaymentReminder/>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1 },
})API
<PaymentReminder>
| Prop | Type | Default | Description |
|---|---|---|---|
| contacts | readonly Contact[] | 9 built-in demo contacts | Contacts in the create sheet's avatar carousel. Replace with your own before shipping. |
| initialIndex | number | 3 | Index centered in the avatar carousel on open. |
| initialReminders | readonly ReminderItem[] | [] | Reminders already saved, rendered in the morph sheet. Seeded once on mount — not re-read on later prop changes. |
| onReminderCreate | (reminder: ReminderItem) => void | — | Called with the fully-built ReminderItem right after a new reminder is created — persist it here. |
If you load reminders asynchronously, wait for that data before mounting PaymentReminder, or force a remount with a key prop (e.g. key={userId}) once it arrives.
<ReminderCreateSheet>
| Prop | Type | Default | Description |
|---|---|---|---|
| contacts | readonly Contact[] | — | Contacts shown in the avatar carousel. |
| buttonLabel | string | 'Set Reminder' | Label for the built-in trigger button. Hidden when open is provided. |
| initialIndex | number | 0 | Index centered in the avatar carousel when the sheet opens. |
| onPillCreate | (pill: PillData) => void | — | Called once Remind me is confirmed, right as the exit animation completes. |
| open | boolean | — | Optional. Switches the sheet to controlled mode. |
| onClose | () => void | — | Called on backdrop tap, hardware back, or after a successful confirm. |
<ReminderMorphSheet>
| Prop | Type | Default | Description |
|---|---|---|---|
| pill | PillData | null | — | The active pill. Pass null before the first reminder is created — the sheet renders nothing. |
| reminders | readonly ReminderItem[] | — | Full list rendered inside the sheet once the pill is tapped open. |
<ReminderPill>
| Prop | Type | Default | Description |
|---|---|---|---|
| data | PillData | — | — |
| onPress | () => void | — | — |
| translateX | SharedValue<number> | internal fallback | Share with ReminderMorphSheet so the morph box tracks the pill's live position, drag included. |
| translateY | SharedValue<number> | internal fallback | Same as translateX. |
| draggable | boolean | true | Set false while morphed open so the pan gesture doesn't fight the expanded sheet. |
<AmountKeypadSheet>
| Prop | Type | Default | Description |
|---|---|---|---|
| visible | boolean | — | — |
| amount | string | — | Current amount string e.g. '42.50'. |
| onAmountChange | (amount: string) => void | — | — |
| decimalSeparator | string | '.' | — |
| hapticsEnabled | boolean | true | — |
<Keypad>
| Prop | Type | Default | Description |
|---|---|---|---|
| onKeyPress | (key: string) => void | — | Receives '0'–'9', the decimal separator, or 'delete'. |
| decimalSeparator | string | '.' | — |
| hapticsEnabled | boolean | true | — |
| disabled | boolean | false | — |
| containerStyle | StyleProp<ViewStyle> | — | — |
| keyStyle | StyleProp<ViewStyle> | — | — |
| keyTextStyle | StyleProp<TextStyle> | — | — |
<AvatarCarousel>
| Prop | Type | Default | Description |
|---|---|---|---|
| contacts | readonly Contact[] | — | — |
| initialIndex | number | 3 | Clamped internally if contacts.length is smaller. |
| onContactChange | (contact: Contact) => void | — | Fires on scroll snap. |
| height | number | 120 | — |
<DateScrollPicker>
| Prop | Type | Default | Description |
|---|---|---|---|
| min | number | 1 | — |
| max | number | 31 | — |
| value | number | — | — |
| width | number | 48 | — |
| onValueChange | (value: number) => void | — | Loops at both ends — past max wraps to min, and vice versa. |
<VerticalTickSlider>
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | — |
| onValueChange | (value: number) => void | — | — |
| min | number | 1 | — |
| max | number | 31 | — |
| step | number | 1 | — |
| activeColor | string | 'rgba(255,255,255,0.85)' | — |
| inactiveColor | string | 'rgba(255,255,255,0.2)' | — |
| thumbColor | string | '#ffffff75' | — |
| width | number | 32 | — |
| height | number | auto (fits tick count) | — |
<CircularProgress>
| Prop | Type | Default | Description |
|---|---|---|---|
| size | number | — | — |
| width | number | — | Stroke width. |
| fill | number | — | 0–100, clamped internally. |
| tintColor | string | '#FFFFFF' | — |
| backgroundColor | string | — | Optional track ring behind the fill. |
| rotation | number | 0 | Extra rotation on top of the built-in -90° offset so fill: 0 starts at 12 o'clock. |
| lineCap | 'butt' | 'round' | 'square' | 'round' | — |
| duration | number | 500 | — |
| style | StyleProp<ViewStyle> | — | Applied to the ring's outer container. |
| children | ReactNode | — | Rendered centered inside the ring. |
<AnimatedCounter>
| Prop | Type | Description |
|---|---|---|
| value | number | — |
| prefix | string | — |
| suffix | string | — |
| duration | number | — |
| delay | number | — |
| decimals | number | — |
| style | StyleProp<TextStyle> | — |
Types
interface Contact {
readonly id: string;
readonly avatar: ImageSourcePropType;
readonly handle: string;
}
interface PillData {
readonly contact: Contact;
readonly amount: string;
readonly dateLabel: string;
readonly countdownLabel: string;
readonly daysRemaining: number;
}
interface ReminderItem extends PillData {
readonly id: string;
readonly progress: number;
}