v0.1 public beta early access on GitHub
Home / Screens / finance-send-money
Screen template

Send Money Screen

Big centered amount entry with a built-in numpad, quick-amount chips, a recipient row, note field, balance guard, and a pinned Send button

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · medium · selection
$ npx lumioui add finance-send-money
import { SendMoneyScreen } from "@/screens/finance/send-money";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
finance/send-money.tsxsource · yours to edit
import * as React from "react";
import { useState } from "react";
import { View, ScrollView, Pressable, TextInput } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Card, Chip, Glass, Icon, Input, KeyboardAvoiding, Text } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, useTheme } from "@lumioui/core";

import { LumioMark, MONO, Mount, Press } from "../shared";
export interface Recipient {
  id: string;
  name: string;
  /** Handle or account line — "@diegoruiz", "Lumio Checking ··4417". */
  handle?: string;
  avatarUrl?: string;
}

export interface SendMoneyScreenProps {
  recipient?: Recipient;
  currency?: string;
  /** Spendable balance — shows under the amount and gates Send. */
  balance?: number;
  /** One-tap amounts in whole units — [10, 25, 50, 100]. */
  quickAmounts?: number[];
  notePlaceholder?: string;
  onSend?: (amount: number, note: string, recipient: Recipient) => void;
  /** Tapping the recipient row — opens the picker. */
  onRecipientPress?: () => void;
  onBack?: () => void;
  /** Prefilled amount/note for demos/previews — production hosts omit them. */
  defaultAmount?: string;
  defaultNote?: string;
}

const DEFAULT_RECIPIENT: Recipient = {
  id: "diego",
  name: "Diego Ruiz",
  handle: "@diegoruiz",
  avatarUrl: "https://i.pravatar.cc/150?img=53",
};

const DEFAULT_QUICK_AMOUNTS = [10, 25, 50, 100];

/** Keep only digits + one decimal point, two places max. */
function sanitizeAmount(text: string) {
  const cleaned = text.replace(/[^0-9.]/g, "");
  const dot = cleaned.indexOf(".");
  if (dot === -1) return cleaned.slice(0, 7);
  // A leading dot gets a zero so the field never shows "$.5".
  const head = cleaned.slice(0, dot).slice(0, 7) || "0";
  const tail = cleaned.slice(dot + 1).replace(/\./g, "").slice(0, 2);
  return `${head}.${tail}`;
}

/** Keypad layout — 1–9, then the decimal point, zero, and backspace. */
const KEYPAD_ROWS = [
  ["1", "2", "3"],
  ["4", "5", "6"],
  ["7", "8", "9"],
  [".", "0", "del"],
] as const;

/** Single numpad key — squishy scale press like the Cash App pad. */
function KeypadKey({ label, onPress }: { label: string; onPress: () => void }) {
  const haptics = useHaptic();
  const press = usePressAnimation({ pressedScale: 0.88, pressedOpacity: 0.6, fast: true });
  const isDelete = label === "del";
  return (
    <AnimatedPressable
      onPress={() => {
        haptics.selection();
        onPress();
      }}
      onPressIn={press.onPressIn}
      onPressOut={press.onPressOut}
      style={press.animatedStyle}
      accessibilityRole="button"
      accessibilityLabel={isDelete ? "Delete" : label === "." ? "Decimal point" : `Digit ${label}`}
      className="h-12 flex-1 items-center justify-center rounded-full"
    >
      {isDelete ? (
        <Icon name="delete" size="md" color="foreground" />
      ) : (
        <Text variant="h3" className="tabular-nums">
          {label}
        </Text>
      )}
    </AnimatedPressable>
  );
}
KeypadKey.displayName = "KeypadKey";

/** Send money — big centered amount entry with quick chips, a recipient
 *  row, a note field, and a pinned Send button (the Cash App idiom). */
export function SendMoneyScreen({
  recipient = DEFAULT_RECIPIENT,
  currency = "$",
  balance = 4280.5,
  quickAmounts = DEFAULT_QUICK_AMOUNTS,
  notePlaceholder = "For Sakura Ramen",
  onSend,
  onRecipientPress,
  onBack,
  defaultAmount,
  defaultNote,
}: SendMoneyScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const { colors } = useTheme();
  const [raw, setRaw] = useState(() => sanitizeAmount(defaultAmount ?? ""));
  const [note, setNote] = useState(defaultNote ?? "");

  const amount = parseFloat(raw) || 0;
  const overBalance = amount > balance;
  const canSend = amount > 0 && !overBalance;

  const send = () => {
    if (!canSend) return;
    // No manual haptic — the send Button fires haptic="medium" itself.
    onSend?.(amount, note.trim(), recipient);
  };

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back disc + centered title pill. */}
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="h-11 w-11" />
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
            <Glass effect="regular" className="absolute inset-0 rounded-full" />
            <LumioMark size={18} />
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Send money
            </Text>
          </View>
        </View>
      </View>

      <KeyboardAvoiding className="flex-1">
        <ScrollView
          className="flex-1"
          showsVerticalScrollIndicator={false}
          contentContainerClassName="gap-6 px-6"
          contentContainerStyle={{ paddingTop: insets.top + 60 }}
          keyboardShouldPersistTaps="handled"
        >
          {/* Amount — one centered field that owns the screen. The currency
              glyph is baked into the value so the digits stay centered. */}
          <Mount className="items-center gap-1">
            <TextInput
              value={raw ? `${currency}${raw}` : ""}
              onChangeText={(text) =>
                setRaw(sanitizeAmount(text.replace(currency, "")))
              }
              placeholder={`${currency}0`}
              // Placeholder color can't be a class — it's a TextInput prop.
              placeholderTextColor={colors.mutedForeground}
              // The on-screen pad below is the primary entry — don't summon
              // the system keyboard on focus (hardware keys still type on web).
              showSoftInputOnFocus={false}
              keyboardType="decimal-pad"
              returnKeyType="done"
              accessibilityLabel="Amount to send"
              accessibilityHint={overBalance ? "Amount exceeds your balance" : undefined}
              className="w-full py-2 text-center text-6xl font-bold tracking-tight text-foreground"
              style={{ fontFamily: MONO, fontVariant: ["tabular-nums"] }}
            />
            <Text
              variant="muted"
              className={cn("text-xs tabular-nums", overBalance && "font-medium text-destructive")}
            >
              {overBalance
                ? "Exceeds your balance"
                : `Balance ${currency}${balance.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} · Lumio Checking`}
            </Text>
            {/* Speed + fee — the two things a send screen must promise. */}
            <View className="flex-row items-center gap-1.5">
              <Icon name="zap" size="sm" color="success" />
              <Text variant="muted" className="text-xs">
                Arrives instantly · no fee
              </Text>
            </View>
          </Mount>

          {/* Quick amounts — one tap fills the field. */}
          <Mount delay={80} className="flex-row justify-center gap-2">
            {quickAmounts.map((v) => {
              const selected = amount === v;
              return (
                <Chip
                  key={v}
                  variant="filter"
                  selected={selected}
                  onPress={() => setRaw(String(v))}
                  accessibilityLabel={`Set amount to ${currency}${v}`}
                  className={`h-11 min-w-16 items-center px-4 ${selected ? "border-transparent bg-[#1B1B26]" : ""}`}
                  textClassName={selected ? "text-white" : undefined}
                >
                  {currency}
                  {v}
                </Chip>
              );
              // Chips fire haptics.selection() internally.
            })}
          </Mount>

          {/* Recipient — tappable row so the destination stays visible
              while entering the amount. */}
          <Mount delay={160}>
            <Card variant="elevated" className="bg-white dark:bg-[#1A1A20]">
              <Pressable
                onPress={() => {
                  haptics.light();
                  onRecipientPress?.();
                }}
                accessibilityRole="button"
                accessibilityLabel={`Sending to ${recipient.name}${recipient.handle ? `, ${recipient.handle}` : ""}. Change recipient`}
                className="flex-row items-center gap-3 p-4"
              >
                <Avatar
                  size="md"
                  source={recipient.avatarUrl ? { uri: recipient.avatarUrl } : undefined}
                  fallback={recipient.name}
                  alt={`${recipient.name}'s avatar`}
                />
                <View className="flex-1 gap-0.5">
                  <Text variant="muted" className="text-xs">
                    To
                  </Text>
                  <Text variant="small" numberOfLines={1} className="font-medium">
                    {recipient.name}
                    {recipient.handle ? (
                      <Text variant="muted" className="text-xs">{`  ${recipient.handle}`}</Text>
                    ) : null}
                  </Text>
                </View>
                <Icon name="chevron-right" size="sm" color="mutedForeground" />
              </Pressable>
            </Card>
          </Mount>

          {/* Note — optional memo line. */}
          <Mount delay={240}>
            <Input
              value={note}
              onChangeText={setNote}
              placeholder={notePlaceholder}
              accessibilityLabel="Add a note"
              leftIcon={<Icon name="edit" size="sm" color="mutedForeground" />}
              returnKeyType="done"
            />
          </Mount>
        </ScrollView>

        {/* Numpad — pinned above Send like Cash App / Revolut: the amount
            field never waits on a system keyboard. */}
        <Mount delay={300} className="gap-1 px-6">
          {KEYPAD_ROWS.map((row, ri) => (
            <View key={ri} className="flex-row gap-1">
              {row.map((key) => (
                <KeypadKey
                  key={key}
                  label={key}
                  onPress={() =>
                    setRaw((prev) =>
                      key === "del" ? prev.slice(0, -1) : sanitizeAmount(prev + key)
                    )
                  }
                />
              ))}
            </View>
          ))}
        </Mount>

        {/* Send — pinned above the home indicator, armed with the amount
            like Cash App's "Pay $25". */}
        <View
          className="px-6 pt-2"
          style={{ paddingBottom: insets.bottom + 12 }}
        >
          <Button
            haptic="medium"
            disabled={!canSend}
            onPress={send}
            accessibilityLabel={
              canSend
                ? `Send ${currency}${amount.toFixed(2)} to ${recipient.name}`
                : "Enter an amount to send"
            }
            accessibilityState={{ disabled: !canSend }}
            className="w-full bg-[#1B1B26]"
            textClassName="text-white"
          >
            {`Send ${currency}${raw || "0"}`}
          </Button>
        </View>
      </KeyboardAvoiding>
    </SafeAreaView>
  );
}
SendMoneyScreen.displayName = "SendMoneyScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Send Money Screen" screen in React Native using LumioUI.

Goal: Big centered amount entry with a built-in numpad, quick-amount chips, a recipient row, note field, balance guard, and a pinned Send button Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add finance-send-money
```

Or pull just the components: `npx lumioui add avatar button card chip icon input keyboard-avoiding text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×1

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1

### `card` — Compound surface — header, title, content and footer slots.
- `<Card>` ×1

### `chip` — Compact selectable pill with press scale and ink overlay.
- `<Chip>` ×1

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5

### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×1

### `keyboard-avoiding` — View that lifts above the keyboard on both platforms.
- `<KeyboardAvoiding>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×7

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/finance/send-money.tsx` exporting SendMoneyScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { useState } from "react";
import { View, ScrollView, Pressable, TextInput } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Card, Chip, Glass, Icon, Input, KeyboardAvoiding, Text } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, useTheme } from "@lumioui/core";

import { LumioMark, MONO, Mount, Press } from "../shared";
export interface Recipient {
  id: string;
  name: string;
  /** Handle or account line — "@diegoruiz", "Lumio Checking ··4417". */
  handle?: string;
  avatarUrl?: string;
}

export interface SendMoneyScreenProps {
  recipient?: Recipient;
  currency?: string;
  /** Spendable balance — shows under the amount and gates Send. */
  balance?: number;
  /** One-tap amounts in whole units — [10, 25, 50, 100]. */
  quickAmounts?: number[];
  notePlaceholder?: string;
  onSend?: (amount: number, note: string, recipient: Recipient) => void;
  /** Tapping the recipient row — opens the picker. */
  onRecipientPress?: () => void;
  onBack?: () => void;
  /** Prefilled amount/note for demos/previews — production hosts omit them. */
  defaultAmount?: string;
  defaultNote?: string;
}

const DEFAULT_RECIPIENT: Recipient = {
  id: "diego",
  name: "Diego Ruiz",
  handle: "@diegoruiz",
  avatarUrl: "https://i.pravatar.cc/150?img=53",
};

const DEFAULT_QUICK_AMOUNTS = [10, 25, 50, 100];

/** Keep only digits + one decimal point, two places max. */
function sanitizeAmount(text: string) {
  const cleaned = text.replace(/[^0-9.]/g, "");
  const dot = cleaned.indexOf(".");
  if (dot === -1) return cleaned.slice(0, 7);
  // A leading dot gets a zero so the field never shows "$.5".
  const head = cleaned.slice(0, dot).slice(0, 7) || "0";
  const tail = cleaned.slice(dot + 1).replace(/\./g, "").slice(0, 2);
  return `${head}.${tail}`;
}

/** Keypad layout — 1–9, then the decimal point, zero, and backspace. */
const KEYPAD_ROWS = [
  ["1", "2", "3"],
  ["4", "5", "6"],
  ["7", "8", "9"],
  [".", "0", "del"],
] as const;

/** Single numpad key — squishy scale press like the Cash App pad. */
function KeypadKey({ label, onPress }: { label: string; onPress: () => void }) {
  const haptics = useHaptic();
  const press = usePressAnimation({ pressedScale: 0.88, pressedOpacity: 0.6, fast: true });
  const isDelete = label === "del";
  return (
    <AnimatedPressable
      onPress={() => {
        haptics.selection();
        onPress();
      }}
      onPressIn={press.onPressIn}
      onPressOut={press.onPressOut}
      style={press.animatedStyle}
      accessibilityRole="button"
      accessibilityLabel={isDelete ? "Delete" : label === "." ? "Decimal point" : `Digit ${label}`}
      className="h-12 flex-1 items-center justify-center rounded-full"
    >
      {isDelete ? (
        <Icon name="delete" size="md" color="foreground" />
      ) : (
        <Text variant="h3" className="tabular-nums">
          {label}
        </Text>
      )}
    </AnimatedPressable>
  );
}
KeypadKey.displayName = "KeypadKey";

/** Send money — big centered amount entry with quick chips, a recipient
 *  row, a note field, and a pinned Send button (the Cash App idiom). */
export function SendMoneyScreen({
  recipient = DEFAULT_RECIPIENT,
  currency = "$",
  balance = 4280.5,
  quickAmounts = DEFAULT_QUICK_AMOUNTS,
  notePlaceholder = "For Sakura Ramen",
  onSend,
  onRecipientPress,
  onBack,
  defaultAmount,
  defaultNote,
}: SendMoneyScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const { colors } = useTheme();
  const [raw, setRaw] = useState(() => sanitizeAmount(defaultAmount ?? ""));
  const [note, setNote] = useState(defaultNote ?? "");

  const amount = parseFloat(raw) || 0;
  const overBalance = amount > balance;
  const canSend = amount > 0 && !overBalance;

  const send = () => {
    if (!canSend) return;
    // No manual haptic — the send Button fires haptic="medium" itself.
    onSend?.(amount, note.trim(), recipient);
  };

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back disc + centered title pill. */}
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="h-11 w-11" />
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <View className="h-11 flex-row items-center justify-center gap-2 rounded-full pl-3.5 pr-5">
            <Glass effect="regular" className="absolute inset-0 rounded-full" />
            <LumioMark size={18} />
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              Send money
            </Text>
          </View>
        </View>
      </View>

      <KeyboardAvoiding className="flex-1">
        <ScrollView
          className="flex-1"
          showsVerticalScrollIndicator={false}
          contentContainerClassName="gap-6 px-6"
          contentContainerStyle={{ paddingTop: insets.top + 60 }}
          keyboardShouldPersistTaps="handled"
        >
          {/* Amount — one centered field that owns the screen. The currency
              glyph is baked into the value so the digits stay centered. */}
          <Mount className="items-center gap-1">
            <TextInput
              value={raw ? `${currency}${raw}` : ""}
              onChangeText={(text) =>
                setRaw(sanitizeAmount(text.replace(currency, "")))
              }
              placeholder={`${currency}0`}
              // Placeholder color can't be a class — it's a TextInput prop.
              placeholderTextColor={colors.mutedForeground}
              // The on-screen pad below is the primary entry — don't summon
              // the system keyboard on focus (hardware keys still type on web).
              showSoftInputOnFocus={false}
              keyboardType="decimal-pad"
              returnKeyType="done"
              accessibilityLabel="Amount to send"
              accessibilityHint={overBalance ? "Amount exceeds your balance" : undefined}
              className="w-full py-2 text-center text-6xl font-bold tracking-tight text-foreground"
              style={{ fontFamily: MONO, fontVariant: ["tabular-nums"] }}
            />
            <Text
              variant="muted"
              className={cn("text-xs tabular-nums", overBalance && "font-medium text-destructive")}
            >
              {overBalance
                ? "Exceeds your balance"
                : `Balance ${currency}${balance.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} · Lumio Checking`}
            </Text>
            {/* Speed + fee — the two things a send screen must promise. */}
            <View className="flex-row items-center gap-1.5">
              <Icon name="zap" size="sm" color="success" />
              <Text variant="muted" className="text-xs">
                Arrives instantly · no fee
              </Text>
            </View>
          </Mount>

          {/* Quick amounts — one tap fills the field. */}
          <Mount delay={80} className="flex-row justify-center gap-2">
            {quickAmounts.map((v) => {
              const selected = amount === v;
              return (
                <Chip
                  key={v}
                  variant="filter"
                  selected={selected}
                  onPress={() => setRaw(String(v))}
                  accessibilityLabel={`Set amount to ${currency}${v}`}
                  className={`h-11 min-w-16 items-center px-4 ${selected ? "border-transparent bg-[#1B1B26]" : ""}`}
                  textClassName={selected ? "text-white" : undefined}
                >
                  {currency}
                  {v}
                </Chip>
              );
              // Chips fire haptics.selection() internally.
            })}
          </Mount>

          {/* Recipient — tappable row so the destination stays visible
              while entering the amount. */}
          <Mount delay={160}>
            <Card variant="elevated" className="bg-white dark:bg-[#1A1A20]">
              <Pressable
                onPress={() => {
                  haptics.light();
                  onRecipientPress?.();
                }}
                accessibilityRole="button"
                accessibilityLabel={`Sending to ${recipient.name}${recipient.handle ? `, ${recipient.handle}` : ""}. Change recipient`}
                className="flex-row items-center gap-3 p-4"
              >
                <Avatar
                  size="md"
                  source={recipient.avatarUrl ? { uri: recipient.avatarUrl } : undefined}
                  fallback={recipient.name}
                  alt={`${recipient.name}'s avatar`}
                />
                <View className="flex-1 gap-0.5">
                  <Text variant="muted" className="text-xs">
                    To
                  </Text>
                  <Text variant="small" numberOfLines={1} className="font-medium">
                    {recipient.name}
                    {recipient.handle ? (
                      <Text variant="muted" className="text-xs">{`  ${recipient.handle}`}</Text>
                    ) : null}
                  </Text>
                </View>
                <Icon name="chevron-right" size="sm" color="mutedForeground" />
              </Pressable>
            </Card>
          </Mount>

          {/* Note — optional memo line. */}
          <Mount delay={240}>
            <Input
              value={note}
              onChangeText={setNote}
              placeholder={notePlaceholder}
              accessibilityLabel="Add a note"
              leftIcon={<Icon name="edit" size="sm" color="mutedForeground" />}
              returnKeyType="done"
            />
          </Mount>
        </ScrollView>

        {/* Numpad — pinned above Send like Cash App / Revolut: the amount
            field never waits on a system keyboard. */}
        <Mount delay={300} className="gap-1 px-6">
          {KEYPAD_ROWS.map((row, ri) => (
            <View key={ri} className="flex-row gap-1">
              {row.map((key) => (
                <KeypadKey
                  key={key}
                  label={key}
                  onPress={() =>
                    setRaw((prev) =>
                      key === "del" ? prev.slice(0, -1) : sanitizeAmount(prev + key)
                    )
                  }
                />
              ))}
            </View>
          ))}
        </Mount>

        {/* Send — pinned above the home indicator, armed with the amount
            like Cash App's "Pay $25". */}
        <View
          className="px-6 pt-2"
          style={{ paddingBottom: insets.bottom + 12 }}
        >
          <Button
            haptic="medium"
            disabled={!canSend}
            onPress={send}
            accessibilityLabel={
              canSend
                ? `Send ${currency}${amount.toFixed(2)} to ${recipient.name}`
                : "Enter an amount to send"
            }
            accessibilityState={{ disabled: !canSend }}
            className="w-full bg-[#1B1B26]"
            textClassName="text-white"
          >
            {`Send ${currency}${raw || "0"}`}
          </Button>
        </View>
      </KeyboardAvoiding>
    </SafeAreaView>
  );
}
SendMoneyScreen.displayName = "SendMoneyScreen";
```

Screen: https://lumioui.dev/screens/finance-send-money
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗