ZAYX logo
XGitHubDiscordReddit

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.

Open in Github
Open in ChatGPT
Open in Claude
Open in Cursor

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.

npx degit ManasCodeXart/ZAYX/components/payment-reminder components/payment-reminder

Install the needed dependencies

npx expo install react-native-reanimated react-native-worklets expo-haptics react-native-gesture-handler

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>

PropTypeDefaultDescription
contactsreadonly Contact[]9 built-in demo contactsContacts in the create sheet's avatar carousel. Replace with your own before shipping.
initialIndexnumber3Index centered in the avatar carousel on open.
initialRemindersreadonly ReminderItem[][]Reminders already saved, rendered in the morph sheet. Seeded once on mount — not re-read on later prop changes.
onReminderCreate(reminder: ReminderItem) => voidCalled 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>

PropTypeDefaultDescription
contactsreadonly Contact[]Contacts shown in the avatar carousel.
buttonLabelstring'Set Reminder'Label for the built-in trigger button. Hidden when open is provided.
initialIndexnumber0Index centered in the avatar carousel when the sheet opens.
onPillCreate(pill: PillData) => voidCalled once Remind me is confirmed, right as the exit animation completes.
openbooleanOptional. Switches the sheet to controlled mode.
onClose() => voidCalled on backdrop tap, hardware back, or after a successful confirm.

<ReminderMorphSheet>

PropTypeDefaultDescription
pillPillData | nullThe active pill. Pass null before the first reminder is created — the sheet renders nothing.
remindersreadonly ReminderItem[]Full list rendered inside the sheet once the pill is tapped open.

<ReminderPill>

PropTypeDefaultDescription
dataPillData
onPress() => void
translateXSharedValue<number>internal fallbackShare with ReminderMorphSheet so the morph box tracks the pill's live position, drag included.
translateYSharedValue<number>internal fallbackSame as translateX.
draggablebooleantrueSet false while morphed open so the pan gesture doesn't fight the expanded sheet.

<AmountKeypadSheet>

PropTypeDefaultDescription
visibleboolean
amountstringCurrent amount string e.g. '42.50'.
onAmountChange(amount: string) => void
decimalSeparatorstring'.'
hapticsEnabledbooleantrue

<Keypad>

PropTypeDefaultDescription
onKeyPress(key: string) => voidReceives '0'–'9', the decimal separator, or 'delete'.
decimalSeparatorstring'.'
hapticsEnabledbooleantrue
disabledbooleanfalse
containerStyleStyleProp<ViewStyle>
keyStyleStyleProp<ViewStyle>
keyTextStyleStyleProp<TextStyle>

<AvatarCarousel>

PropTypeDefaultDescription
contactsreadonly Contact[]
initialIndexnumber3Clamped internally if contacts.length is smaller.
onContactChange(contact: Contact) => voidFires on scroll snap.
heightnumber120

<DateScrollPicker>

PropTypeDefaultDescription
minnumber1
maxnumber31
valuenumber
widthnumber48
onValueChange(value: number) => voidLoops at both ends — past max wraps to min, and vice versa.

<VerticalTickSlider>

PropTypeDefaultDescription
valuenumber
onValueChange(value: number) => void
minnumber1
maxnumber31
stepnumber1
activeColorstring'rgba(255,255,255,0.85)'
inactiveColorstring'rgba(255,255,255,0.2)'
thumbColorstring'#ffffff75'
widthnumber32
heightnumberauto (fits tick count)

<CircularProgress>

PropTypeDefaultDescription
sizenumber
widthnumberStroke width.
fillnumber0–100, clamped internally.
tintColorstring'#FFFFFF'
backgroundColorstringOptional track ring behind the fill.
rotationnumber0Extra rotation on top of the built-in -90° offset so fill: 0 starts at 12 o'clock.
lineCap'butt' | 'round' | 'square''round'
durationnumber500
styleStyleProp<ViewStyle>Applied to the ring's outer container.
childrenReactNodeRendered centered inside the ring.

<AnimatedCounter>

PropTypeDescription
valuenumber
prefixstring
suffixstring
durationnumber
delaynumber
decimalsnumber
styleStyleProp<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; }

Project Structure

payment-reminder/
├── components/
│ ├── PaymentReminder.tsx # Root orchestrator
│ ├── ReminderMorphSheet.tsx # Pill + morph-box state — pill lives here
│ ├── ReminderCreateSheet.tsx # Modal create flow
│ ├── ReminderPill.tsx # Draggable pill — avatar, amount, countdown
│ ├── AvatarCarousel.tsx # Contact selection carousel
│ ├── DateScrollPicker.tsx # Looping day picker
│ ├── VerticalTickSlider.tsx # Month picker slider
│ ├── AmountKeypadSheet.tsx # Amount entry sheet
│ ├── Keypad.tsx # Custom numeric keypad
│ ├── CircularProgress.tsx # Circular countdown ring (react-native-svg)
│ └── AnimatedCounter.tsx # Shared-value digit counter
├── constants/
│ ├── types.ts # Contact, PillData, ReminderItem types
│ └── scaling.ts # verticalScale helpers
├── hooks/
│ └── useReminderState.ts # Shared reminder orchestration state
├── morph/
│ ├── index.ts # Barrel export
│ ├── types.ts # MorphBox types
│ ├── constants.ts # Spring + timing constants
│ └── ... # MorphContentLayer, MorphMeasureLayer, hooks
├── utils/
│ └── reminder.ts # Reminder ID + progress computation
└── assets/