# @retray-dev/ui-kit — Component Reference (v6.2.0)

This file is the AI reference for this package. It is shipped inside the npm package so consuming projects can import it into their `CLAUDE.md` with:

```markdown
## UI Components
@./node_modules/@retray-dev/ui-kit/COMPONENTS.md
```

---

## Setup (Required)

Wrap your app root with all required providers in this exact order:

```tsx
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { ThemeProvider, BottomSheetModalProvider, ToastProvider } from '@retray-dev/ui-kit'

export default function App() {
  return (
    <SafeAreaProvider initialMetrics={initialWindowMetrics}>
      <GestureHandlerRootView style={{ flex: 1 }}>
        <ThemeProvider colorScheme="system">
          <BottomSheetModalProvider>
            <ToastProvider>
              {/* your app */}
            </ToastProvider>
          </BottomSheetModalProvider>
        </ThemeProvider>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  )
}
```

**Provider order is mandatory:**
- `SafeAreaProvider` must be outermost — required by `@gorhom/bottom-sheet`
- `initialMetrics={initialWindowMetrics}` is required on Android to avoid a "No safe area value available" crash on first render
- `GestureHandlerRootView` must wrap everything — required by `@gorhom/bottom-sheet`
- `BottomSheetModalProvider` must be inside `GestureHandlerRootView`
- `ToastProvider` wraps children and renders `Toaster` (from `sonner-native`) internally

## Typography — Poppins (Required)

All components use **Poppins** as the font family. You **must** load it before rendering any UI kit component.

```tsx
import { useFonts } from 'expo-font'
import { PoppinsFonts } from '@retray-dev/ui-kit/fonts'

export default function App() {
  const [fontsLoaded] = useFonts(PoppinsFonts)
  if (!fontsLoaded) return null
  return (
    // ... your providers and app
  )
}
```

**How it works:**
1. Import `PoppinsFonts` from `@retray-dev/ui-kit/fonts` (separate export path)
2. Pass it to `expo-font`'s `useFonts()` hook at your app root
3. Metro resolves font files from `node_modules/@retray-dev/ui-kit/src/assets/fonts/` at bundle time
4. All library components reference fonts by family name (e.g., `fontFamily: 'Poppins-SemiBold'`)

**Included weights:**
- Regular: `Poppins-Thin`, `Poppins-ExtraLight`, `Poppins-Light`, `Poppins-Regular`, `Poppins-Medium`, `Poppins-SemiBold`, `Poppins-Bold`, `Poppins-ExtraBold`, `Poppins-Black`
- Italic: `Poppins-Italic`, `Poppins-MediumItalic`, `Poppins-SemiBoldItalic`, `Poppins-BoldItalic`

**Total: 13 font files** exported from the package. Font `.ttf` files ship as raw assets in `src/assets/fonts/` — NOT bundled into `dist/`. Metro resolves `require()` calls at build time.

Pair with `expo-splash-screen` in production:
```tsx
import * as SplashScreen from 'expo-splash-screen'
SplashScreen.preventAutoHideAsync()

useEffect(() => {
  if (fontsLoaded) SplashScreen.hideAsync()
}, [fontsLoaded])
```

---

## Theme System

### ThemeProvider Props

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| colorScheme | `'light' \| 'dark' \| 'system'` | `'system'` | `'system'` auto-detects device setting and updates when it changes |
| theme | `{ light?: Partial<ThemeColors>, dark?: Partial<ThemeColors> }` | — | Override any subset of the 12 public tokens per scheme |

**Custom theme example:**
```tsx
const myTheme = {
  light: { primary: '#ff385c', primaryForeground: '#ffffff' },
  dark:  { primary: '#ff385c', primaryForeground: '#ffffff' },
}
<ThemeProvider theme={myTheme} colorScheme="system">
```

### useTheme Hook

```tsx
import { useTheme } from '@retray-dev/ui-kit'

function MyComponent() {
  const { colors, colorScheme } = useTheme()
  return <View style={{ backgroundColor: colors.background }} />
}
```

Returns `colors` (full `ResolvedColors` palette) and `colorScheme` (`'light' | 'dark'`).

---

## Theme Tokens

### Public Tokens (ThemeColors) — 12 tokens consumer can override

These are the only values you need to supply when customizing the theme. The library derives all other colors internally.

| Token | Light Default | Dark Default | Semantic Role |
|-------|--------------|--------------|---------------|
| `background` | `#ffffff` | `#0f0f0f` | Screen / page background |
| `foreground` | `#222222` | `#fafafa` | Primary text — deep near-black, not pure black |
| `card` | `#ffffff` | `#1c1c1c` | Card / elevated surface background |
| `primary` | `#1a1a1a` | `#fafafa` | Primary action (buttons, selected states, active indicators) |
| `primaryForeground` | `#ffffff` | `#0f0f0f` | Text/icon placed on primary-colored backgrounds |
| `border` | `#dddddd` | `#303030` | Borders, dividers, input outlines |
| `destructive` | `#e53935` | `#ef5350` | Error / danger / delete actions |
| `destructiveForeground` | `#ffffff` | `#ffffff` | Text/icon on destructive backgrounds |
| `success` | `#1a7a45` | `#2e7d52` | Success / confirmation states |
| `successForeground` | `#ffffff` | `#ffffff` | Text/icon on success backgrounds |
| `warning` | `#e67e00` | `#f57c00` | Warning / caution states |
| `warningForeground` | `#ffffff` | `#ffffff` | Text/icon on warning backgrounds |
| `overlay` *(optional)* | `rgba(0,0,0,0.45)` | `rgba(0,0,0,0.45)` | Backdrop/overlay color behind sheets and dialogs |
| `accent` *(optional)* | same as `primary` | same as `primary` | Secondary brand accent (e.g. Airbnb coral). Falls back to `primary` |
| `accentForeground` *(optional)* | same as `primaryForeground` | same as `primaryForeground` | Text/icon on accent backgrounds. Falls back to `primaryForeground` |

### Derived Tokens (ResolvedColors) — read-only via useTheme().colors

The full palette components consume. Never supply these directly — they are computed by `deriveColors()` from the 12 public tokens above.

| Token | Derived From | Purpose |
|-------|-------------|---------|
| `foregroundSubtle` | `foreground` @ ~55% | Body text, subtitles, secondary content |
| `foregroundMuted` | `foreground` @ ~38% | Captions, timestamps, placeholders |
| `surface` | `background` slightly off-canvas | Chip backgrounds, input fills, tag backgrounds, skeleton |
| `surfaceStrong` | `background` stronger offset | Pressed/hover fill states |
| `destructiveTint` | `destructive` blended to bg | Alert banner background, toast background |
| `destructiveBorder` | `destructive` @ 30% | Alert banner border, badge outline |
| `successTint` | `success` blended to bg | Success banner background |
| `successBorder` | `success` @ 30% | Success banner border |
| `warningTint` | `warning` blended to bg | Warning banner background |
| `warningBorder` | `warning` @ 30% | Warning banner border |
| `ring` | `= primary` | Focus ring color (always matches primary) |
| `input` | `= border` | Input field border (always matches border) |
| `overlay` | `overlay` token or `rgba(0,0,0,0.45)` | Backdrop behind sheets and dialogs |
| `accentResolved` | `accent` token or `= primary` | Resolved accent color — always present |
| `accentForegroundResolved` | `accentForeground` token or `= primaryForeground` | Resolved text on accent — always present |

**Usage example — building a custom component using derived tokens:**
```tsx
const { colors } = useTheme()

// Text hierarchy
<Text style={{ color: colors.foreground }}>Primary text</Text>
<Text style={{ color: colors.foregroundSubtle }}>Secondary text</Text>
<Text style={{ color: colors.foregroundMuted }}>Caption / timestamp</Text>

// Surface fills (unselected chips, inactive backgrounds)
<View style={{ backgroundColor: colors.surface }}>

// Warning alert banner
<View style={{ backgroundColor: colors.warningTint, borderColor: colors.warningBorder }}>
  <Text style={{ color: colors.warning }}>Warning message</Text>
```

### deriveColors export

```tsx
import { deriveColors } from '@retray-dev/ui-kit'

const resolved = deriveColors(myThemeColors, 'light')
// resolved contains all 24 ResolvedColors tokens
```

---

## Design Tokens

Static structural constants — no context or provider needed.

```ts
import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@retray-dev/ui-kit'
```

### SPACING — 8pt grid with 2pt micro-step

| Key | Value | Use |
|-----|-------|-----|
| `xxs` | 2 | Micro gaps (icon to text in badges) |
| `xs` | 4 | Tight internal gaps |
| `sm` | 8 | Component internal padding |
| `md` | 12 | Medium gaps |
| `base` | 16 | Default content padding |
| `lg` | 24 | Section internal padding |
| `xl` | 32 | Between major content blocks |
| `xxl` | 48 | Section padding |
| `section` | 64 | Major band separators (Airbnb hero → grid rhythm) |

**Types:** `Spacing`, `SpacingKey`

```tsx
<View style={{ gap: SPACING.md, padding: SPACING.base }} />
```

### ICON_SIZES

| Key | Value | Use |
|-----|-------|-----|
| `sm` | 14 | Badge icons, inline micro icons |
| `md` | 18 | Standard component icons |
| `lg` | 22 | Larger inline icons |
| `xl` | 28 | Feature icons |
| `2xl` | 32 | Display icons |

**Types:** `IconSize`, `IconSizeKey`

### RADIUS — Airbnb shape language

| Key | Value | Used in |
|-----|-------|---------|
| `none` | 0 | No rounding |
| `xs` | 4 | Micro chips, tags |
| `sm` | 8 | Inputs, Textarea, Select, Checkbox |
| `md` | 14 | Cards, Buttons (all variants), MediaCard, AlertBanner, Toast, EmptyState |
| `lg` | 20 | Sheet top corners |
| `xl` | 32 | Large decorative elements |
| `full` | 9999 | IconButton (circle), CategoryStrip chips |

**Types:** `Radius`, `RadiusKey`

```tsx
<View style={{ borderRadius: RADIUS.md }} />
```

### SHADOWS — Cross-platform shadow presets

| Key | shadowOffset.height | shadowOpacity | shadowRadius | elevation | Use |
|-----|---------------------|---------------|--------------|-----------|-----|
| `sm` | 1 | 0.06 | 4 | 2 | Default card shadow |
| `md` | 2 | 0.10 | 8 | 5 | Hover float / elevated elements |
| `lg` | 6 | 0.16 | 16 | 10 | Modals, overlays |
| `xl` | 12 | 0.24 | 24 | 18 | High-elevation dialogs |

```tsx
<View style={[styles.card, SHADOWS.sm]} />
// Hover state
<View style={[styles.card, hovered ? SHADOWS.md : SHADOWS.sm]} />
```

### BREAKPOINTS

| Key | Value | Use |
|-----|-------|-----|
| `wide` | 700 | Tablet / wide layout threshold |

```tsx
const isWide = useWindowDimensions().width >= BREAKPOINTS.wide
```

### TYPOGRAPHY — 16 Airbnb-aligned variants

All components use these tokens for text styling. Import and use in custom components for consistency.

| Key | Size | Weight | lineHeight | letterSpacing | Use |
|-----|------|--------|-----------|---------------|-----|
| `display-hero` | 64 | 700 | 70 | -1 | Large number display, balance totals |
| `display-xl` | 28 | 700 | 40 | 0 | Screen titles, hero headings |
| `display-lg` | 22 | 500 | 26 | -0.44 | Section headings |
| `display-md` | 21 | 700 | 30 | 0 | Card titles, dialog titles |
| `display-sm` | 20 | 600 | 24 | -0.18 | Sub-section headings |
| `title-md` | 16 | 600 | 20 | 0 | Row titles, list item titles |
| `title-sm` | 16 | 500 | 20 | 0 | Secondary titles |
| `body-md` | 16 | 400 | 24 | 0 | Primary body copy |
| `body-sm` | 14 | 400 | 20 | 0 | Secondary body, descriptions |
| `caption` | 14 | 500 | 18 | 0 | Labels above inputs, item captions |
| `caption-sm` | 13 | 400 | 16 | 0 | Timestamps, metadata |
| `badge-text` | 11 | 600 | 13 | 0 | Badge labels, small tags |
| `micro-label` | 12 | 700 | 16 | 0 | Micro labels, overlines |
| `uppercase-tag` | 10 | 700 | 13 | 0.8 | Uppercase decorative tags (auto-uppercase) |
| `button-lg` | 16 | 500 | 20 | 0 | Button labels (md/lg size) |
| `button-sm` | 14 | 500 | 18 | 0 | Button labels (sm size) |

**Types:** `Typography`, `TypographyKey`

```tsx
import { TYPOGRAPHY } from '@retray-dev/ui-kit'

// Use in StyleSheet
const styles = StyleSheet.create({
  heading: {
    ...TYPOGRAPHY['display-xl'],
    color: colors.foreground,
  },
})
```

---

## Migration Guide: v4 → v5

### Breaking Changes

**Button variants renamed:**
| v4 | v5 |
|----|-----|
| `outline` | `secondary` |
| `ghost` | `text` |
| `secondary` (filled gray) | removed — use Card/surface instead |

**Text variants replaced:**
| v4 | v5 equivalent |
|----|---------------|
| `h1` | `display-xl` |
| `h2` | `display-lg` or `display-md` |
| `h3` | `display-sm` |
| `body` | `body-md` |
| `label` | `title-sm` or `caption` |
| `caption` | `caption-sm` |

**Theme tokens changed:**
| v4 | v5 |
|----|-----|
| `secondary`, `secondaryForeground` | removed (use `surface` derived token) |
| `accent`, `accentForeground` | removed (use `surfaceStrong`) |
| `muted` | removed → `surface` (derived) |
| `mutedForeground` | removed → `foregroundSubtle` / `foregroundMuted` (derived) |
| Added | `warning`, `warningForeground` |
| Added (derived) | `warningTint`, `warningBorder` |

**IconButton variant:**
| v4 | v5 |
|----|-----|
| `ghost` | `text` |

---

## Migration Guide: v5 → v6

### New Components

**`MenuItem`** — Navigation row with icon, label, and optional right slot (`rightRender`). Replaces ad-hoc `ListItem` usage for settings/nav menus. Zero horizontal padding by design — consumer controls spacing.

```tsx
import { MenuItem } from '@retray-dev/ui-kit'
// variants: 'plain' (default) | 'card'
<MenuItem label="Profile" subtitle="Edit your details" iconName="user" onPress={() => {}} />
<MenuItem label="Notifications" iconName="bell" rightRender={<Switch value={on} onValueChange={setOn} />} showChevron={false} onPress={() => {}} />
```

### Breaking Changes

**Sheet keyboard defaults changed:**
| Prop | v5 default | v6 default |
|------|-----------|-----------|
| `keyboardBehavior` | `'fillParent'` (Android) / `'interactive'` (iOS) | `'interactive'` (both) |
| `android_keyboardInputMode` | `'adjustResize'` | `'adjustPan'` |

If you relied on `adjustResize`, set it explicitly: `android_keyboardInputMode="adjustResize"`.

**Toast API simplified:**

```tsx
// v5 — hook-only API
const { toast } = useToast()
toast.success('Done')

// v6 — direct import preferred (hook still works for compat)
import { toast } from '@retray-dev/ui-kit'
toast.success('Done')
```

### New Theme Tokens

Three optional `ThemeColors` tokens — fall back gracefully if omitted:

| Token | Default | Purpose |
|-------|---------|---------|
| `overlay` | `rgba(0,0,0,0.45)` | Backdrop color behind sheets and dialogs |
| `accent` | `= primary` | Secondary brand accent color |
| `accentForeground` | `= primaryForeground` | Text on accent backgrounds |

Access resolved values via `useTheme().colors.overlay`, `.accentResolved`, `.accentForegroundResolved`.

### Token Corrections

- `TYPOGRAPHY['uppercase-tag']` — size corrected to `10` (was documented as `8`, actual value was always `10`)

---

## Components

---

### Text

**Import:** `import { Text } from '@retray-dev/ui-kit'`

**When to use:** Every piece of text in your app. Provides Airbnb-aligned typographic hierarchy with correct font, weight, size, and line height. Replaces React Native's `Text` directly — all native `TextProps` pass through.

**Extends:** `TextProps` from React Native

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| variant | `TextVariant` | `'body-md'` | Sets font, size, weight, line height, letter spacing |
| color | `string` | — | Override text color. Each variant has a semantic default (see table below) |
| children | `ReactNode` | required | — |
| style | `TextStyle` | — | Additional styles (merged after variant styles) |
| (all TextProps) | — | — | `numberOfLines`, `ellipsizeMode`, `onPress`, etc. all pass through |

**Variant reference with semantic defaults:**

| Variant | Size | Weight | Default Color | When to use |
|---------|------|--------|--------------|-------------|
| `display-hero` | 64 | 700 | `foreground` | Large numeric displays — balance totals, stats, hero numbers |
| `display-xl` | 28 | 700 | `foreground` | Screen-level titles, onboarding hero headings |
| `display-lg` | 22 | 500 | `foreground` | Section headings, card group labels |
| `display-md` | 21 | 700 | `foreground` | Card titles, dialog headings |
| `display-sm` | 20 | 600 | `foreground` | Sub-section titles |
| `title-md` | 16 | 600 | `foreground` | List row titles, navigation labels |
| `title-sm` | 16 | 500 | `foreground` | Secondary row titles |
| `body-md` | 16 | 400 | `foregroundSubtle` | Main body copy, descriptions |
| `body-sm` | 14 | 400 | `foregroundSubtle` | Secondary descriptions, detail text |
| `caption` | 14 | 500 | `foreground` | Labels above inputs, item captions |
| `caption-sm` | 13 | 400 | `foregroundMuted` | Timestamps, metadata, helper text |
| `badge-text` | 11 | 600 | `foreground` | Badge labels, small status tags |
| `micro-label` | 12 | 700 | `foreground` | Micro labels, overlines |
| `uppercase-tag` | 10 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
| `button-lg` | 16 | 500 | `foreground` | Internal use in Button component (md/lg) |
| `button-sm` | 14 | 500 | `foreground` | Internal use in Button component (sm) |

**All text has `allowFontScaling={true}` — respects user's font size settings.**

**Examples:**
```tsx
// Screen title
<Text variant="display-xl">Welcome back</Text>

// Financial balance display
<Text variant="display-hero" color={colors.primary}>$25.000</Text>

// Section heading with subtle color
<Text variant="display-lg" color={colors.foregroundSubtle}>Recent activity</Text>

// Body copy
<Text variant="body-md">Your account is ready to use.</Text>

// Caption / metadata
<Text variant="caption-sm" color={colors.foregroundMuted}>2 hours ago</Text>

// Input label (used automatically by Input, but also useful standalone)
<Text variant="caption">Email address</Text>

// Uppercase category tag
<Text variant="uppercase-tag">New</Text>

// Color override
<Text variant="title-md" color={colors.primary}>Primary colored title</Text>
<Text variant="body-sm" color={colors.destructive}>Error message in body</Text>
```

---

### Button

**Import:** `import { Button } from '@retray-dev/ui-kit'`

**When to use:** Any interactive action. Use `variant` to communicate intent and visual weight. Primary is the main CTA per screen — use sparingly (one per screen ideally).

**Extends:** `TouchableOpacityProps` from React Native — all native props pass through.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | required | Button text |
| variant | `'primary' \| 'secondary' \| 'text' \| 'destructive'` | `'primary'` | Visual style |
| size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls height, padding, and icon size |
| loading | `boolean` | `false` | Replaces label with spinner, forces disabled state |
| fullWidth | `boolean` | `false` | Stretches to container width (`alignSelf: 'stretch'`) |
| disabled | `boolean` | — | Reduces opacity to 0.5 |
| icon | `React.ReactNode \| ((props: { label, size, variant }) => React.ReactNode)` | — | Icon rendered alongside label |
| iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
| iconColor | `string` | — | Override icon color. Defaults to variant label color |
| iconPosition | `'left' \| 'right'` | `'left'` | Side the icon appears on |
| style | `ViewStyle` | — | Override container style |

**Variants:**

| Variant | Background | Border | Text | When to use |
|---------|-----------|--------|------|-------------|
| `primary` | `primary` | none | `primaryForeground` | Main CTA — save, submit, continue |
| `secondary` | transparent | `primary` (1.5px) | `primary` | Alternative action alongside primary |
| `text` | transparent | none | `primary` | Low-emphasis — cancel, skip, see more |
| `destructive` | `destructive` | none | `destructiveForeground` | Irreversible actions — delete, remove |

**Sizes:**

| Size | Height | Horizontal padding | Icon size | Font |
|------|--------|--------------------|-----------|------|
| `sm` | 40px | 16px | 16px | `button-sm` (14pt/500) |
| `md` | 48px | 24px | 18px | `button-lg` (16pt/500) |
| `lg` | 56px | 28px | 20px | `button-lg` (16pt/500) |

**Shape:** `borderRadius: RADIUS.md = 14px` — Airbnb-aligned rounded rect on all variants. Exception: `IconButton` uses `RADIUS.full` (perfect circle).

**Animations:** `usePressScale` hook — Reanimated v4, UI thread. Scale springs to 0.95 (`SPRINGS.pressIn`) on `onPressIn`, back to 1.0 (`SPRINGS.pressOut`) on `onPressOut`. Web: hover scale 1.02.

**Haptics:** `impactMedium` on every press.

**Examples:**
```tsx
// Standard CTA
<Button label="Continue" onPress={handleContinue} />

// Destructive action
<Button label="Delete account" variant="destructive" onPress={handleDelete} />

// Cancel (low emphasis)
<Button label="Cancel" variant="text" onPress={onCancel} />

// Alternative action
<Button label="Save draft" variant="secondary" onPress={saveDraft} />

// Loading state
<Button label="Saving..." loading onPress={handleSave} />

// Full width with icon
<Button label="Get started" iconName="arrow-right" iconPosition="right" fullWidth />

// Destructive with icon
<Button label="Delete" variant="destructive" iconName="trash-2" size="sm" />

// Large with icon on left
<Button label="Add to trip" iconName="plus" size="lg" />
```

**Composition — action pair (primary + secondary):**
```tsx
<View style={{ gap: SPACING.sm }}>
  <Button label="Book now" fullWidth />
  <Button label="Save for later" variant="secondary" fullWidth />
</View>
```

---

### ButtonGroup

**Import:** `import { ButtonGroup } from '@retray-dev/ui-kit'`

**When to use:** Auto-distribute space equally between buttons in horizontal or vertical layouts. Perfect for side-by-side CTAs (Cancel/Confirm, Back/Next) where both buttons should occupy equal width.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| children | `React.ReactNode` | required | Button components to lay out |
| gap | `number` | `12` | Spacing between buttons (in points) |
| vertical | `boolean` | `false` | Stack buttons vertically instead of horizontally |
| style | `ViewStyle` | — | Override container style |

**Behavior:** Automatically applies `flex: 1` to all direct children. Children share space equally regardless of label length.

**Mobile best practice:** When using 2 buttons with icons in a ButtonGroup, use `size="sm"` to prevent text clipping on narrow screens:
```tsx
// ✅ Good — Small size prevents overflow
<ButtonGroup>
  <Button label="Cancel" variant="secondary" size="sm" iconName="x" onPress={handleCancel} />
  <Button label="Confirm" size="sm" iconName="check" onPress={handleConfirm} />
</ButtonGroup>

// ❌ Avoid — Default size may clip text when icons present
<ButtonGroup>
  <Button label="Cancel" variant="secondary" iconName="x" onPress={handleCancel} />
  <Button label="Confirm" iconName="check" onPress={handleConfirm} />
</ButtonGroup>
```

**Examples:**
```tsx
// Horizontal pair (50%/50%)
<ButtonGroup>
  <Button label="Cancel" variant="secondary" onPress={handleCancel} />
  <Button label="Confirm" onPress={handleConfirm} />
</ButtonGroup>

// Custom gap
<ButtonGroup gap={8}>
  <Button label="Back" variant="text" onPress={handleBack} />
  <Button label="Next" onPress={handleNext} />
</ButtonGroup>

// Vertical stack
<ButtonGroup vertical gap={8}>
  <Button label="Primary Action" onPress={handlePrimary} />
  <Button label="Secondary Action" variant="secondary" onPress={handleSecondary} />
  <Button label="Delete" variant="destructive" onPress={handleDelete} />
</ButtonGroup>

// Three buttons (33%/33%/33%)
<ButtonGroup gap={6}>
  <Button label="1D" variant="secondary" size="sm" onPress={() => setRange('1d')} />
  <Button label="1W" variant="secondary" size="sm" onPress={() => setRange('1w')} />
  <Button label="1M" size="sm" onPress={() => setRange('1m')} />
</ButtonGroup>
```

---

### IconButton

**Import:** `import { IconButton } from '@retray-dev/ui-kit'`

**When to use:** Icon-only pressable actions — toolbars, FABs, navigation actions, close buttons, header actions. Use `Button` when a text label is needed.

**Extends:** `TouchableOpacityProps` from React Native

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
| icon | `ReactNode` | — | Custom icon node when `iconName` is not provided |
| iconColor | `string` | — | Override icon color. Defaults to variant foreground color |
| variant | `'primary' \| 'secondary' \| 'outline' \| 'text' \| 'destructive'` | `'primary'` | Visual style |
| size | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
| loading | `boolean` | `false` | Replaces icon with spinner |
| badge | `boolean \| number` | — | Notification badge overlay |
| disabled | `boolean` | — | Reduces opacity to 0.5 |
| style | `ViewStyle` | — | — |

**Variants:** Same token logic as Button.

| Variant | Background | Border | Icon color | When to use |
|---------|-----------|--------|-----------|-------------|
| `primary` | `primary` | none | `primaryForeground` | Prominent action (FAB, main toolbar action) |
| `secondary` | `surface` | none | `foreground` | Secondary toolbar action |
| `outline` | transparent | `border` | `foreground` | Tertiary action with visible boundary |
| `text` | transparent | none | `foreground` | Inline action, header close/back |
| `destructive` | `destructive` | none | `destructiveForeground` | Delete/remove action |

**Sizes:**

| Size | Dimensions | Icon size |
|------|-----------|-----------|
| `sm` | 32 × 32px | 16px |
| `md` | 44 × 44px | 20px |
| `lg` | 52 × 52px | 24px |

**Badge prop:**
- `true` → 8×8px dot (primary color, top-right corner)
- `number` → 16×16px pill with count, capped at 99 (shows "99+" for higher values)
- Badge is positioned absolutely — does not affect button layout

**Animations:** Scale springs to 0.95 on press.

**Haptics:** `impactLight` on press.

**Examples:**
```tsx
// Close button
<IconButton iconName="x" variant="text" size="sm" onPress={handleClose} />

// FAB
<IconButton iconName="plus" size="lg" onPress={handleAdd} />

// Delete action
<IconButton iconName="trash-2" variant="destructive" onPress={handleDelete} />

// Notification bell with badge count
<IconButton iconName="bell" variant="text" badge={3} onPress={() => navigate('notifications')} />

// Unread dot (boolean badge)
<IconButton iconName="mail" variant="outline" badge={true} onPress={() => {}} />

// Header back button
<IconButton iconName="arrow-left" variant="text" onPress={navigation.goBack} />

// Loading state while action runs
<IconButton iconName="save" loading={saving} onPress={handleSave} />
```

**Composition — toolbar row:**
```tsx
<View style={{ flexDirection: 'row', gap: SPACING.xs }}>
  <IconButton iconName="share" variant="text" onPress={handleShare} />
  <IconButton iconName="bookmark" variant="text" onPress={handleBookmark} />
  <IconButton iconName="more-horizontal" variant="text" onPress={handleMore} />
</View>
```

---

### Input

**Import:** `import { Input } from '@retray-dev/ui-kit'`

**When to use:** Single-line text entry in forms. Includes label, error, hint, and icon/prefix/suffix support. Covers email, phone, search, password, and custom numeric inputs.

**Extends:** `TextInputProps` from React Native — all native props pass through (`keyboardType`, `autoCapitalize`, `returnKeyType`, etc.)

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | — | Label above the input field |
| error | `string` | — | Error text below — turns border `destructive` (2px) |
| hint | `string` | — | Helper text below (hidden when `error` is set) |
| prefix | `string \| ReactNode` | — | Content before the input text (e.g. `"$"`, icon node) |
| suffix | `string \| ReactNode` | — | Content after the input text (e.g. `"kg"`, icon node) |
| prefixStyle | `TextStyle` | — | Style for prefix string (no effect on ReactNode) |
| suffixStyle | `TextStyle` | — | Style for suffix string (no effect on ReactNode) |
| prefixIcon | `string` | — | Icon name rendered before input. Takes precedence over `prefix` |
| suffixIcon | `string` | — | Icon name rendered after input. Takes precedence over `suffix` unless `type="password"` |
| prefixIconColor | `string` | — | Override prefix icon color. Defaults to `foregroundMuted` |
| suffixIconColor | `string` | — | Override suffix icon color. Defaults to `foregroundMuted` |
| type | `'text' \| 'password'` | `'text'` | Password shows eye/eye-off toggle button in suffix slot |
| containerStyle | `ViewStyle` | — | Outer container View style |
| inputWrapperStyle | `ViewStyle` | — | The bordered wrapper around the input row |
| style | `TextStyle` | — | TextInput element style |

**Dimensions:** 56px height (14px vertical padding), 8px border radius.

**Border states:**
- Default: `border` token (1px)
- Focused: `primary` (2px)
- Error: `destructive` (2px)
- Focused + error: `destructive` (2px)

**Examples:**
```tsx
// Standard email input
<Input label="Email" placeholder="you@example.com" keyboardType="email-address" autoCapitalize="none" />

// Password with toggle
<Input label="Password" type="password" error="Incorrect password" />

// With hint text
<Input label="Username" hint="4–20 characters, letters and numbers only" />

// Currency prefix (string)
<Input label="Amount" prefix="$" placeholder="0.00" keyboardType="decimal-pad" />

// Unit suffix
<Input label="Weight" suffix="kg" placeholder="0" keyboardType="numeric" />

// Search (no label, prefix icon)
<Input placeholder="Search..." prefixIcon="search" />

// With both icons
<Input placeholder="Amount" prefixIcon="dollar-sign" suffixIcon="check" />

// With custom ReactNode prefix
<Input
  label="Phone"
  prefix={<Text variant="body-md">+1</Text>}
  keyboardType="phone-pad"
/>

// Controlled with error
<Input
  label="Email"
  value={email}
  onChangeText={setEmail}
  error={emailError}
  keyboardType="email-address"
/>
```

**Composition — form fields:**
```tsx
<View style={{ gap: SPACING.md }}>
  <Input label="First name" value={firstName} onChangeText={setFirstName} />
  <Input label="Last name" value={lastName} onChangeText={setLastName} />
  <Input label="Email" value={email} onChangeText={setEmail} error={emailError} keyboardType="email-address" />
  <Input label="Password" type="password" value={password} onChangeText={setPassword} />
  <Button label="Create account" fullWidth onPress={handleSubmit} />
</View>
```

---

### Textarea

**Import:** `import { Textarea } from '@retray-dev/ui-kit'`

**When to use:** Multi-line text entry — bios, descriptions, notes, reviews. Same visual design as `Input` but taller with multi-line support.

**Extends:** `TextInputProps` from React Native — all native props pass through.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | — | Label above the textarea |
| error | `string` | — | Error text below; turns border destructive (2px) |
| hint | `string` | — | Helper text below (hidden when `error` is set) |
| rows | `number` | `4` | Minimum row count — each row ≈ 30px. Sets `numberOfLines` |
| prefixIcon | `string` | — | Icon name from `@expo/vector-icons` rendered inside top-left corner |
| prefixIconNode | `ReactNode` | — | Custom icon node rendered top-left |
| prefixIconColor | `string` | — | Override prefix icon color. Defaults to `foregroundMuted` |
| containerStyle | `ViewStyle` | — | Outer container style |
| style | `TextStyle` | — | TextInput element style |

**Border states:** Identical to Input (focused=primary 2px, error=destructive 2px).

**Examples:**
```tsx
<Textarea label="Bio" placeholder="Tell us about yourself" rows={5} prefixIcon="user" />
<Textarea label="Review" hint="Be honest and helpful" prefixIcon="message-circle" />
<Textarea label="Notes" error="Notes are required" rows={3} prefixIcon="edit-3" />
```

---

### CurrencyInput

**Import:** `import { CurrencyInput } from '@retray-dev/ui-kit'`

**When to use:** Monetary or numeric entries that need thousands-separator formatting while typing. Amount entry, price fields, transfer amounts. Wraps `Input` — shares the same label, error, hint, and visual design.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| value | `string` | — | Controlled display value including prefix (e.g. `'$1.234'`) |
| onChangeText | `(formatted: string) => void` | — | Called with the formatted display string on each keystroke |
| onChangeValue | `(raw: number) => void` | — | Called with the parsed number (no separators, no prefix) |
| prefix | `string` | `'$'` | Symbol prepended to formatted value |
| thousandsSeparator | `'.' \| ','` | `'.'` | Character separating groups of three digits |
| size | `'default' \| 'large'` | `'default'` | `'large'` uses 36px font for prominent payment screens |
| label | `string` | — | Label above input |
| error | `string` | — | Error text below |
| hint | `string` | — | Helper text below (hidden when `error` is set) |
| placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
| editable | `boolean` | — | Pass `false` to disable |
| containerStyle | `ViewStyle` | — | Outer container style |

**Formatting behavior:** Strips all non-digit characters from input, then re-applies thousands separator every 3 digits from the right, then prepends `prefix`. Decimal point input is not supported — this is for whole-number monetary amounts.

**Examples:**
```tsx
const [display, setDisplay] = useState('')
const [amount, setAmount] = useState(0)

// Standard with dot separator ($1.234.567)
<CurrencyInput
  label="Amount"
  value={display}
  onChangeText={setDisplay}
  onChangeValue={setAmount}
  hint={`Parsed: ${amount}`}
/>

// Comma separator ($1,234,567)
<CurrencyInput
  prefix="$"
  thousandsSeparator=","
  value={display}
  onChangeText={setDisplay}
  onChangeValue={setAmount}
/>

// Large variant for payment screens
<CurrencyInput
  size="large"
  label="Transfer amount"
  value={display}
  onChangeText={setDisplay}
  onChangeValue={setAmount}
/>

// Euro with error
<CurrencyInput
  prefix="€"
  label="Price"
  value={display}
  onChangeText={setDisplay}
  onChangeValue={setAmount}
  error={amount < 1000 ? "Minimum amount is €1.000" : undefined}
/>
```

---

### CurrencyDisplay

**Import:** `import { CurrencyDisplay } from '@retray-dev/ui-kit'`

**When to use:** Prominent read-only display of monetary values — account balances, totals, summary screens. Uses `display-hero` typography (56pt bold) for visual impact.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| value | `number \| string` | required | Numeric value to display |
| prefix | `string` | `'$'` | Symbol prepended to formatted value |
| showDecimals | `boolean` | `false` | Show two decimal places with comma separator (e.g. `$25.000,00`) |
| textColor | `string` | — | Override text color. Defaults to `foreground` token |
| variant | `'hero' \| 'large' \| 'medium' \| 'small'` | — | Predefined size (48/32/18/14pt). Overrides default 56pt |
| autoScale | `boolean` | `false` | Enable `adjustsFontSizeToFit` — long values shrink to fit one line |
| maxFontSize | `number` | — | Max font size when `autoScale` true. Defaults to variant size or 56pt |
| style | `ViewStyle` | — | Outer container style |

**Format:** Dot (`.`) as thousands separator, comma (`,`) as decimal separator — Latin American / European format.

**Examples:**
```tsx
<CurrencyDisplay value={25000} />
// → $25.000

<CurrencyDisplay value={25000000.5} showDecimals />
// → $25.000.000,50

<CurrencyDisplay value={1500} prefix="€" />
// → €1.500

<CurrencyDisplay value={balance} textColor={balance < 0 ? colors.destructive : colors.foreground} />
```

**Composition — balance card:**
```tsx
<Card>
  <CardContent>
    <Text variant="caption-sm" color={colors.foregroundMuted}>Available balance</Text>
    <CurrencyDisplay value={12500} />
  </CardContent>
</Card>
```

---

### Badge

**Import:** `import { Badge } from '@retray-dev/ui-kit'`

**When to use:** Status labels, counts, category tags, feature flags. Pill-shaped with optional icon.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | — | Badge text. Can be omitted when using `children` |
| children | `ReactNode` | — | Alternative to `label` for custom content |
| variant | `BadgeVariant` | `'default'` | Visual style |
| size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls padding and font size |
| icon | `ReactNode` | — | Icon rendered before label |
| iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
| iconColor | `string` | — | Override icon color. Defaults to variant foreground color |
| style | `ViewStyle` | — | — |

**Variants:**

| Variant | Background | Text | Border | When to use |
|---------|-----------|------|--------|-------------|
| `default` | `primary` | `primaryForeground` | none | Primary status — active, featured |
| `secondary` | `surface` | `foregroundSubtle` | `border` | Neutral — draft, inactive, secondary status |
| `destructive` | `destructive` | `destructiveForeground` | none | Error, failed, danger |
| `outline` | transparent | `foreground` | `border` | Subtle tag, no emphasis |
| `success` | `success` | `successForeground` | none | Completed, verified, paid |
| `warning` | `warning` | `warningForeground` | none | Pending, caution, expiring |
| `successOutline` | `successTint` | `success` | `successBorder` | Soft success (less visual weight) |
| `destructiveOutline` | `destructiveTint` | `destructive` | `destructiveBorder` | Soft error (less visual weight) |
| `warningOutline` | `warningTint` | `warning` | `warningBorder` | Soft warning (less visual weight) |

**Sizes:**

| Size | Padding (V×H) | Font size | Icon size |
|------|--------------|-----------|-----------|
| `sm` | 2 × 8 | 11pt | 10px |
| `md` | 4 × 10 | 13pt | 12px |
| `lg` | 6 × 12 | 15pt | 14px |

**All badges have `borderRadius: 9999` (pill-shaped).**

**Examples:**
```tsx
<Badge label="New" />
<Badge label="Error" variant="destructive" />
<Badge label="Draft" variant="secondary" size="sm" />
<Badge label="Beta" variant="outline" />
<Badge label="Paid" variant="success" iconName="check" />
<Badge label="Pending" variant="warningOutline" />
<Badge label="3" variant="destructive" size="sm" />

// With status in ListItem
<ListItem
  title="Payment"
  rightRender={<Badge label="Pending" variant="warningOutline" size="sm" />}
/>
```

---

### Avatar

**Import:** `import { Avatar } from '@retray-dev/ui-kit'`

**When to use:** User profile pictures with automatic fallback to initials. Works with remote URIs and local images.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| src | `string \| null` | — | Image URI (remote or local). `null` forces fallback |
| fallback | `string` | — | Manual initials (max 2 chars, uppercased) |
| fallbackText | `string` | — | Full name — extracts up to 2 initials (e.g. `"Julian Cruz"` → `"JC"`) |
| size | `'sm' \| 'md' \| 'lg' \| 'xl' \| number` | `'md'` | Named size or custom diameter in points |
| status | `'online' \| 'offline' \| 'busy' \| 'away'` | — | Status indicator dot, bottom-right |
| style | `ViewStyle` | — | — |

**Sizes:**

| Size | Diameter | Fallback font | Status dot size |
|------|---------|--------------|----------------|
| `sm` | 28px | 12pt | 8px |
| `md` | 40px | 16pt | 10px |
| `lg` | 56px | 22pt | 13px |
| `xl` | 72px | 28pt | 16px |

**Status dot colors:**
- `online` → `#22c55e` (green)
- `offline` → transparent fill, `border` color outline only
- `busy` → `destructive` token
- `away` → `warning` token

**Status dot is absolutely positioned at bottom-right with a 2px white border.**

**Examples:**
```tsx
<Avatar src="https://..." fallback="JC" size="lg" />
<Avatar fallback="AN" size="md" />
<Avatar src={user.avatar} fallback={user.initials} size="xl" status="online" />
<Avatar fallback="BU" size="sm" status="busy" />
<Avatar fallbackText="Julian Cruz" size="md" />  // → "JC"
<Avatar src={avatarUrl} size={64} />              // custom numeric size

// In a ListItem
<ListItem
  leftRender={<Avatar src={user.avatar} fallback={user.initials} status="online" />}
  title={user.name}
  subtitle={user.role}
/>
```

---

### Card

**Import:** `import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@retray-dev/ui-kit'`

**When to use:** Grouped content with a surface background. Use `Card` as the container and sub-components for structured layout.

**`Card` Props:**

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Visual style |
| onPress | `() => void` | — | Makes card pressable with scale animation + haptics |
| children | `ReactNode` | required | — |
| style | `ViewStyle` | — | — |

**Variants:**

| Variant | Background | Border | Shadow | When to use |
|---------|-----------|--------|--------|-------------|
| `elevated` | `card` | `border` (1px) | `SHADOWS.sm` | Default card — standard content groups |
| `outlined` | `card` | `border` (1px) | none | Flat bordered cards, lists of cards |
| `filled` | `surfaceStrong` | none | none | Background fill cards, stat blocks, sidebar items |

**Sub-components (all accept `style` prop):**

| Sub-component | Padding | Notes |
|--------------|---------|-------|
| `CardHeader` | 16px all, 0 bottom | Use for title + description |
| `CardTitle` | — | 18pt / SemiBold / `cardForeground` |
| `CardDescription` | — | 14pt / Regular / `foregroundMuted` |
| `CardContent` | 16px all (12px top) | Main content area |
| `CardFooter` | 16px horizontal, 14px bottom | Row layout, use for buttons |

**Shape:** `borderRadius: RADIUS.md = 14px`

**Press animation:** Scale springs to 0.98 (subtler than button — appropriate for large targets).

**Haptics:** `impactLight` when `onPress` is provided.

**Examples:**
```tsx
// Standard card
<Card>
  <CardHeader>
    <CardTitle>Account settings</CardTitle>
    <CardDescription>Manage your profile and preferences</CardDescription>
  </CardHeader>
  <CardContent>
    <Input label="Display name" value={name} onChangeText={setName} />
  </CardContent>
  <CardFooter>
    <Button label="Save" fullWidth />
  </CardFooter>
</Card>

// Pressable navigation card
<Card variant="outlined" onPress={() => navigate('profile')}>
  <CardContent>
    <Text variant="title-md">Profile</Text>
    <Text variant="body-sm">Edit your information</Text>
  </CardContent>
</Card>

// Stat block (filled)
<Card variant="filled">
  <CardContent>
    <Text variant="caption-sm" color={colors.foregroundMuted}>Total spent</Text>
    <CurrencyDisplay value={totalSpent} />
  </CardContent>
</Card>
```

**Composition — dashboard grid:**
```tsx
<View style={{ flexDirection: 'row', gap: SPACING.md }}>
  <Card variant="filled" style={{ flex: 1 }}>
    <CardContent>
      <Text variant="micro-label" color={colors.foregroundMuted}>INCOME</Text>
      <Text variant="display-md" color={colors.success}>$8.500</Text>
    </CardContent>
  </Card>
  <Card variant="filled" style={{ flex: 1 }}>
    <CardContent>
      <Text variant="micro-label" color={colors.foregroundMuted}>EXPENSES</Text>
      <Text variant="display-md" color={colors.destructive}>$3.200</Text>
    </CardContent>
  </Card>
</View>
```

---

### MediaCard

**Import:** `import { MediaCard } from '@retray-dev/ui-kit'`

**When to use:** Image-first content cards — product listings, properties, experiences, photo-based content. Think Airbnb listing card or marketplace item.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| imageSource | `ImageSourcePropType` | — | Image — `{ uri: '...' }` or `require('./img.jpg')` |
| aspectRatio | `MediaCardAspectRatio` | `'4:3'` | Controls image height relative to card width |
| badge | `ReactNode` | — | Floating content top-left of image |
| actionIcon | `ReactNode` | — | Icon top-right of image (defaults to heart outline) |
| actionIconName | `string` | — | Icon name. Overrides `actionIcon` |
| actionActive | `boolean` | `false` | Whether action is in active/filled state |
| onActionPress | `() => void` | — | Press handler for action icon. Stops event propagation |
| title | `string` | — | Below image — primary label |
| subtitle | `string` | — | Secondary line below title |
| caption | `string` | — | Tertiary metadata line |
| footer | `ReactNode` | — | Custom content below caption (price, rating, etc.) |
| onPress | `() => void` | — | Press the card body |
| style | `ViewStyle` | — | Outer card style |
| imageStyle | `ViewStyle` | — | Image container override |

**Aspect ratios (`MediaCardAspectRatio`):**

| Value | Width:Height | When to use |
|-------|-------------|-------------|
| `'1:1'` | Square | Profile cards, avatars, square products |
| `'4:3'` | Classic | Default — general listings |
| `'16:9'` | Wide | Videos, wide landscape imagery |
| `'4:5'` | Portrait | Tall product photos, fashion |
| `'3:2'` | Photo | Standard photography ratio |

**Image implementation:** Uses `paddingTop` percentage for cross-platform consistent aspect ratios with `position: absolute` image fill — no fixed height needed.

**Shape:** `borderRadius: RADIUS.md = 14px`, `overflow: hidden`.

**Action button:** 32×32px circle, semi-transparent background (24% opacity), top-right corner 8px from edges.

**Badge:** Positioned top-left 8px from edges — pass any ReactNode (Badge component recommended).

**Web hover:** Lifts shadow from `SHADOWS.sm` to `SHADOWS.md` on hover.

**Press animation:** Scale springs to 0.98.

**Haptics:** `impactLight` on `onActionPress`.

**Examples:**
```tsx
// Basic listing card
<MediaCard
  imageSource={{ uri: listing.photo }}
  title={listing.name}
  subtitle={listing.location}
  caption={`$${listing.price} / night`}
  onPress={() => navigate('listing', { id: listing.id })}
/>

// With save action and aspect ratio
<MediaCard
  imageSource={{ uri: product.image }}
  aspectRatio="4:5"
  title={product.name}
  subtitle={product.brand}
  actionActive={saved}
  onActionPress={() => toggleSave(product.id)}
  onPress={() => navigate('product', { id: product.id })}
/>

// With badge overlay
<MediaCard
  imageSource={{ uri: item.photo }}
  badge={<Badge label="New" size="sm" />}
  title={item.name}
  onPress={() => {}}
/>

// With custom footer (price + rating)
<MediaCard
  imageSource={{ uri: place.photo }}
  aspectRatio="4:3"
  title={place.name}
  footer={
    <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
      <Text variant="title-sm">$120 / night</Text>
      <Text variant="caption-sm">★ 4.8</Text>
    </View>
  }
  onPress={() => navigate('place', { id: place.id })}
/>
```

**Composition — listing grid:**
```tsx
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.md }}>
  {listings.map((item) => (
    <MediaCard
      key={item.id}
      style={{ width: (screenWidth - SPACING.base * 2 - SPACING.md) / 2 }}
      imageSource={{ uri: item.photo }}
      title={item.name}
      caption={item.price}
      actionActive={savedIds.includes(item.id)}
      onActionPress={() => toggleSave(item.id)}
      onPress={() => navigate('listing', { id: item.id })}
    />
  ))}
</View>
```

---

### Separator

**Import:** `import { Separator } from '@retray-dev/ui-kit'`

**When to use:** Visual divider between sections, list items, or columns.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction of the line |
| style | `ViewStyle` | — | — |

**Styling:** 1px thickness, `border` token color.

**Examples:**
```tsx
// Section divider
<Separator />

// Vertical divider (parent must have defined height)
<View style={{ flexDirection: 'row', height: 40, alignItems: 'center' }}>
  <Text>Left</Text>
  <Separator orientation="vertical" style={{ marginHorizontal: 12 }} />
  <Text>Right</Text>
</View>

// With custom spacing
<Separator style={{ marginVertical: SPACING.lg }} />
```

---

### Spinner

**Import:** `import { Spinner } from '@retray-dev/ui-kit'`

**When to use:** Loading state indicator. Use inline (within buttons or content areas) or full-screen.

**Extends:** `ActivityIndicatorProps` (except `size` — use the string size prop instead)

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| size | `'sm' \| 'md' \| 'lg'` | `'md'` | `sm`/`md` → RN `'small'`, `lg` → RN `'large'` |
| color | `string` | `primary` token | Override spinner color |
| label | `string` | — | Text displayed below spinner |

**Examples:**
```tsx
<Spinner />
<Spinner size="lg" />
<Spinner size="lg" label="Loading..." />
<Spinner color={colors.foregroundMuted} />

// Full-screen loading overlay
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
  <Spinner size="lg" label="Fetching data..." />
</View>
```

---

### Skeleton

**Import:** `import { Skeleton } from '@retray-dev/ui-kit'`

**When to use:** Placeholder while content is loading. Use to match the shape of the content that will appear — creates a visual loading skeleton that prevents layout shift.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| width | `number \| string` | `'100%'` | Width of placeholder |
| height | `number` | `16` | Height of placeholder |
| borderRadius | `number` | `6` | Corner radius |
| preset | `'base' \| 'circle' \| 'text'` | `'base'` | Convenience preset |
| diameter | `number` | `40` | Used by `'circle'` preset — overrides width/height |
| style | `ViewStyle` | — | — |

**Presets:**
- `'base'` — Uses `width`, `height`, `borderRadius` exactly as specified
- `'circle'` — Forces square dimensions from `diameter`, `borderRadius: 9999`
- `'text'` — 60% width, 14px height, 4px border radius (typical text line shape)

**Animation:** Shimmer sweep loops every 1200ms. Highlight color adapts to light/dark mode.

**Examples:**
```tsx
// Text line placeholders
<Skeleton height={20} width="80%" />
<Skeleton height={16} width="60%" style={{ marginTop: 8 }} />

// Circle avatar placeholder
<Skeleton preset="circle" diameter={40} />

// Text preset (auto-sized)
<Skeleton preset="text" />

// Custom card skeleton
<View style={{ gap: SPACING.sm }}>
  <Skeleton height={180} borderRadius={14} />   {/* image */}
  <Skeleton height={18} width="70%" />           {/* title */}
  <Skeleton preset="text" />                     {/* subtitle */}
  <Skeleton height={14} width="40%" />           {/* caption */}
</View>
```

**Composition — skeleton for a ListItem:**
```tsx
<View style={{ flexDirection: 'row', gap: SPACING.md, padding: SPACING.base }}>
  <Skeleton preset="circle" diameter={40} />
  <View style={{ flex: 1, gap: SPACING.xs }}>
    <Skeleton height={16} width="60%" />
    <Skeleton preset="text" />
  </View>
</View>
```

---

### Progress

**Import:** `import { Progress } from '@retray-dev/ui-kit'`

**When to use:** Show completion percentage for tasks, uploads, onboarding steps, multi-step flows.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| value | `number` | `0` | Current progress value |
| max | `number` | `100` | Maximum value — `value/max` determines fill percentage |
| variant | `'default' \| 'success' \| 'warning' \| 'destructive'` | `'default'` | Indicator color |
| style | `ViewStyle` | — | Outer container style |

**Variants — indicator color:**
- `default` → `primary` token
- `success` → `success` token
- `warning` → `warning` token
- `destructive` → `destructive` token

**Styling:** 8px height, `borderRadius: 9999` (pill). Track uses `surface` background.

**Animation:** Spring-animates fill width on `value` changes (JS thread). Uses `onLayout` to capture pixel width.

**Examples:**
```tsx
<Progress value={60} />
<Progress value={3} max={10} />
<Progress value={uploadProgress} variant="success" />
<Progress value={daysRemaining} max={30} variant="warning" />
<Progress value={errors} max={100} variant="destructive" />

// Labeled progress
<View style={{ gap: SPACING.xs }}>
  <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
    <Text variant="caption">Profile completion</Text>
    <Text variant="caption">{progress}%</Text>
  </View>
  <Progress value={progress} />
</View>
```

---

### AlertBanner

**Import:** `import { AlertBanner } from '@retray-dev/ui-kit'`

**When to use:** Inline feedback messages that persist in the UI — info notices, form-level errors, feature announcements, status alerts. Not for ephemeral feedback — use `Toast` for temporary messages.

> **Note:** Named `AlertBanner` (not `Alert`) to avoid collision with React Native's built-in `Alert` module.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| title | `string` | required | Bold heading |
| description | `string` | — | Supporting detail text below title |
| variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | Controls colors and default icon |
| icon | `ReactNode` | — | Override default icon with custom ReactNode |
| iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon`, but still falls back to variant default when neither is set |
| iconColor | `string` | — | Override icon color. Defaults to variant title color |
| style | `ViewStyle` | — | — |

**Variants:**

| Variant | Background | Border | Icon | When to use |
|---------|-----------|--------|------|-------------|
| `default` | `card` | `border` | ℹ️ info | Neutral notices, informational messages |
| `destructive` | `destructiveTint` | `destructiveBorder` | ⚠️ error | Form errors, action failures |
| `success` | `successTint` | `successBorder` | ✓ check | Confirmations, completed states |
| `warning` | `warningTint` | `warningBorder` | ⚠️ warning | Cautions, expiring states, pending actions |

**Styling:** 12px border radius, 14px horizontal + 12px vertical padding, 1px border.

**Examples:**
```tsx
<AlertBanner title="Your session expires in 5 minutes" />
<AlertBanner variant="destructive" title="Payment failed" description="Please check your card details." />
<AlertBanner variant="success" title="Profile updated" description="Your changes have been saved." />
<AlertBanner variant="warning" title="ID verification required" description="Complete verification to unlock transfers." />

// Custom icon
<AlertBanner iconName="bell" title="Reminder" description="Complete your profile to get started." />
```

---

### EmptyState

**Import:** `import { EmptyState } from '@retray-dev/ui-kit'`

**When to use:** When a list, section, or screen has no content. Provides a centered illustration/icon, message, and optional CTA.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| title | `string` | required | Primary message |
| description | `string` | — | Supporting text (hidden in `compact` size) |
| icon | `ReactNode` | — | Icon in the top circle container |
| iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
| iconColor | `string` | — | Override icon color. Defaults to `foregroundMuted` |
| action | `ReactNode` | — | CTA below text — typically a `Button`. Hidden in `compact` size |
| size | `'default' \| 'compact'` | `'default'` | `compact` is smaller, hides description and action |
| style | `ViewStyle` | — | — |

**Sizes:**

| Size | Icon container | Title font | Description | Action |
|------|---------------|-----------|-------------|--------|
| `default` | 48×48px, 12px radius | 18pt / Medium | shown | shown |
| `compact` | 36×36px, 8px radius | 15pt / Medium | hidden | hidden |

**Styling:** Centered layout, dashed border container, `surface` icon background, 32px padding.

**Examples:**
```tsx
// Full empty state with action
<EmptyState
  iconName="inbox"
  title="No notifications"
  description="You're all caught up! Check back later."
  action={<Button label="Refresh" variant="secondary" size="sm" onPress={refresh} />}
/>

// Search no results
<EmptyState
  iconName="search"
  title="No results for &quot;coffee&quot;"
  description="Try a different search term."
/>

// Compact inline empty state
<EmptyState iconName="file-text" title="No transactions" size="compact" />
```

---

### Checkbox

**Import:** `import { Checkbox } from '@retray-dev/ui-kit'`

**When to use:** Binary boolean choices in forms — terms acceptance, feature toggles, multi-select item selection.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| checked | `boolean` | `false` | Controlled checked state |
| onCheckedChange | `(checked: boolean) => void` | — | Called with new boolean on press |
| label | `string` | — | Text to the right of the checkbox |
| disabled | `boolean` | — | Reduces opacity to 0.5, disables interaction |
| style | `ViewStyle` | — | — |

**Styling:** 24×24px box, 4px border radius, 1.5px border. Checkmark: 12×7px L-shape.

**Animation:** Scale spring on press (0.95).

**Haptics:** `selectionAsync` on change.

**Examples:**
```tsx
const [accepted, setAccepted] = useState(false)

<Checkbox checked={accepted} onCheckedChange={setAccepted} label="I agree to the terms and conditions" />
<Checkbox checked={rememberMe} onCheckedChange={setRememberMe} label="Remember me" />
<Checkbox checked={true} onCheckedChange={() => {}} label="Can't uncheck this" disabled />

// Multi-select list
{options.map((opt) => (
  <Checkbox
    key={opt.id}
    checked={selected.includes(opt.id)}
    onCheckedChange={(checked) => toggleSelected(opt.id, checked)}
    label={opt.label}
  />
))}
```

---

### Switch

**Import:** `import { Switch } from '@retray-dev/ui-kit'`

**When to use:** Binary on/off settings that take effect immediately — notifications, dark mode, feature flags. When the action is immediate (no form submit), prefer Switch over Checkbox.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| checked | `boolean` | `false` | Controlled state |
| onCheckedChange | `(checked: boolean) => void` | — | Called with new state on press |
| disabled | `boolean` | — | Reduces opacity to 0.45, disables interaction |
| style | `ViewStyle` | — | — |

**Dimensions:** Track 52×30px (pill), Thumb 24×24px with 3px offset.

**Animation:** Thumb translates via spring; track color transitions via opacity (150ms).

**Haptics:** `selectionAsync` on toggle.

**Examples:**
```tsx
<Switch checked={notifications} onCheckedChange={setNotifications} />

// In a settings row
<ListItem
  title="Push notifications"
  subtitle="Get alerts for new activity"
  rightRender={<Switch checked={push} onCheckedChange={setPush} />}
/>

// Full settings list
{settings.map((setting) => (
  <ListItem
    key={setting.key}
    title={setting.label}
    rightRender={
      <Switch
        checked={prefs[setting.key]}
        onCheckedChange={(v) => updatePref(setting.key, v)}
      />
    }
    showSeparator
  />
))}
```

---

### Toggle

**Import:** `import { Toggle } from '@retray-dev/ui-kit'`

**When to use:** Toggleable button that shows its active state visually — bold/italic/underline in toolbars, favorite/bookmark actions, filter toggles. Looks like a button (not a switch). Use `Switch` when a pure on/off control is needed.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| pressed | `boolean` | `false` | Controlled pressed state |
| onPressedChange | `(pressed: boolean) => void` | — | Called with new state on press |
| variant | `'default' \| 'outline'` | `'default'` | `outline` adds border when unpressed |
| size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls minimum height and padding |
| label | `string` | — | Text label |
| icon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon when not pressed |
| activeIcon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon when pressed. Defaults to check-circle |
| iconName | `string` | — | Icon name when not pressed. Takes precedence over `icon` |
| activeIconName | `string` | — | Icon name when pressed. Takes precedence over `activeIcon` |
| iconColor | `string` | — | Inactive icon color. Defaults to `foregroundMuted` |
| activeIconColor | `string` | — | Active icon color. Defaults to `primary` |
| disabled | `boolean` | — | — |
| style | `ViewStyle` | — | — |

**Sizes:**

| Size | Min height | Horizontal padding | Icon size |
|------|-----------|-------------------|-----------|
| `sm` | 40px | 12px | 16px |
| `md` | 44px | 16px | 18px |
| `lg` | 48px | 20px | 20px |

**Animation:** `borderColor`, `backgroundColor`, `textColor` animate via `Animated.timing` (150ms) on state change. `borderWidth` stays fixed at 2pt to prevent layout shifts.

**Haptics:** `selectionAsync` on press.

**Examples:**
```tsx
<Toggle pressed={bold} onPressedChange={setBold} label="Bold" variant="outline" />

// With icon names
<Toggle
  pressed={muted}
  onPressedChange={setMuted}
  iconName="volume-x"
  activeIconName="volume"
/>

// Bookmark
<Toggle
  pressed={saved}
  onPressedChange={setSaved}
  iconName="bookmark"
  activeIconName="bookmark"
  activeIconColor={colors.primary}
/>

// Toolbar row
<View style={{ flexDirection: 'row', gap: SPACING.xs }}>
  <Toggle pressed={bold} onPressedChange={setBold} iconName="bold" variant="outline" size="sm" />
  <Toggle pressed={italic} onPressedChange={setItalic} iconName="italic" variant="outline" size="sm" />
  <Toggle pressed={underline} onPressedChange={setUnderline} iconName="underline" variant="outline" size="sm" />
</View>
```

---

### RadioGroup

**Import:** `import { RadioGroup } from '@retray-dev/ui-kit'`

**When to use:** Single selection from a small set of options (2–6). When options are mutually exclusive and all should be visible simultaneously.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| options | `RadioOption[]` | required | `{ label: string, value: string, disabled?: boolean }` |
| value | `string` | — | Currently selected value |
| onValueChange | `(value: string) => void` | — | Called on selection change |
| orientation | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
| style | `ViewStyle` | — | — |

**RadioOption type:**
```ts
{ label: string; value: string; disabled?: boolean }
```

**Styling:** Radio circle 24×24px, 1.5px border. Filled dot 10×10px when selected. 12px gap between radio and label. 8px gap between options (vertical), 16px (horizontal).

**Animation:** Scale spring on press.

**Haptics:** `selectionAsync` on selection.

**Examples:**
```tsx
<RadioGroup
  options={[
    { label: 'Free', value: 'free' },
    { label: 'Pro', value: 'pro' },
    { label: 'Enterprise', value: 'enterprise', disabled: true },
  ]}
  value={plan}
  onValueChange={setPlan}
/>

// Horizontal orientation
<RadioGroup
  options={[{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }]}
  value={answer}
  onValueChange={setAnswer}
  orientation="horizontal"
/>
```

---

### Select

**Import:** `import { Select } from '@retray-dev/ui-kit'`

**When to use:** Dropdown selection from a list too long for RadioGroup (7+ options), or options that don't need to all be visible at once. Uses platform-native picker UI.

**Peer dependency:** `@react-native-picker/picker` must be installed.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| options | `SelectOption[]` | required | `{ label: string, value: string, disabled?: boolean }` |
| value | `string` | — | Currently selected value |
| onValueChange | `(value: string) => void` | — | — |
| placeholder | `string` | `'Select an option'` | Shown when no value is selected |
| label | `string` | — | Label above the trigger |
| error | `string` | — | Error text below trigger |
| disabled | `boolean` | — | — |
| style | `ViewStyle` | — | Outer container style |

**Platform behavior:**
- **iOS:** Modal sheet with wheel `Picker`, "Done" button, transparent backdrop, opens on trigger press
- **Android:** Native dialog picker opened via `TextInput.focus()`
- **Web:** Native HTML `<select>` element — full keyboard navigation

**Styling:** 8px border radius trigger, 14px horizontal + 11px vertical padding. Error state: 2px destructive border.

**Haptics:** `selectionAsync` on open and confirm.

**Examples:**
```tsx
<Select
  label="Country"
  options={countries.map((c) => ({ label: c.name, value: c.code }))}
  value={country}
  onValueChange={setCountry}
  placeholder="Select a country"
/>

<Select
  label="Category"
  options={categories}
  value={category}
  onValueChange={setCategory}
  error={categoryError}
/>
```

---

### Slider

**Import:** `import { Slider } from '@retray-dev/ui-kit'`

**When to use:** Selecting a numeric value in a continuous range by dragging — volume, brightness, price range, percentage.

**Peer dependency:** `@react-native-community/slider` must be installed.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| value | `number` | `0` | Controlled current value |
| minimumValue | `number` | `0` | — |
| maximumValue | `number` | `1` | — |
| step | `number` | `0` | `0` = continuous. Any positive number = snapping + haptic per step |
| onValueChange | `(value: number) => void` | — | Fires while dragging |
| onSlidingComplete | `(value: number) => void` | — | Fires on finger release |
| label | `string` | — | Label text above slider |
| showValue | `boolean` | `false` | Displays current value top-right |
| formatValue | `(value: number) => string` | `toFixed(2)` | Custom display formatter |
| accessibilityLabel | `string` | — | Screen reader label |
| disabled | `boolean` | — | — |
| style | `ViewStyle` | — | — |

**Haptics:** `selectionAsync` on each step (only when `step > 0`).

**Theming:** `minimumTrackTintColor = primary`, `maximumTrackTintColor = surface`, `thumbTintColor = primary`.

**Examples:**
```tsx
<Slider value={volume} minimumValue={0} maximumValue={100} step={1} onValueChange={setVolume} />

<Slider
  label="Volume"
  showValue
  value={volume}
  minimumValue={0}
  maximumValue={100}
  step={5}
  formatValue={(v) => `${v}%`}
  onValueChange={setVolume}
/>

<Slider
  label="Price range"
  showValue
  value={price}
  minimumValue={50}
  maximumValue={500}
  step={10}
  formatValue={(v) => `$${v}`}
  onValueChange={setPrice}
  onSlidingComplete={applyFilter}
/>
```

---

### Tabs

**Import:** `import { Tabs, TabsContent } from '@retray-dev/ui-kit'`

**When to use:** Switching between categorized sections on the same screen — profile/activity/settings, different data views, content filters.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| tabs | `TabItem[]` | required | Tab definitions |
| variant | `'pill' \| 'underline'` | `'pill'` | Visual style |
| value | `string` | — | Controlled active tab |
| onValueChange | `(value: string) => void` | — | — |
| children | `ReactNode` | — | `TabsContent` components |
| style | `ViewStyle` | — | — |

**TabItem type:**
```ts
{ label: string; value: string; icon?: ReactNode | ((isActive: boolean) => ReactNode) }
```

**Variants:**
- `pill` — Animated background pill slides to active tab. Full-width distributed tabs. Active: filled `primary` + `primaryForeground` text. Default for top-of-screen navigation.
- `underline` — 2px bottom border on active tab, no background. Tabs don't stretch — natural width. Airbnb-style filter tabs.

**`TabsContent` Props:**

| Prop | Type | Notes |
|------|------|-------|
| value | `string` | Must match a tab value in `tabs` |
| activeValue | `string` | Pass the current active tab — content hidden when not active |
| children | `ReactNode` | — |
| style | `ViewStyle` | — |

**Animation (pill variant):** Absolutely-positioned pill slides via spring (speed: 20, bounciness: 0) to track active tab layout.

**Haptics:** `selectionAsync` on tab change.

**Examples:**
```tsx
const [tab, setTab] = useState('profile')

// Pill variant (default)
<Tabs
  tabs={[
    { label: 'Profile', value: 'profile' },
    { label: 'Activity', value: 'activity' },
    { label: 'Settings', value: 'settings' },
  ]}
  value={tab}
  onValueChange={setTab}
>
  <TabsContent value="profile" activeValue={tab}>
    <Text>Profile content</Text>
  </TabsContent>
  <TabsContent value="activity" activeValue={tab}>
    <Text>Activity content</Text>
  </TabsContent>
  <TabsContent value="settings" activeValue={tab}>
    <Text>Settings content</Text>
  </TabsContent>
</Tabs>

// Underline variant with icons
<Tabs
  variant="underline"
  tabs={[
    { label: 'All', value: 'all' },
    { label: 'Trips', value: 'trips' },
    { label: 'Experiences', value: 'experiences' },
  ]}
  value={filter}
  onValueChange={setFilter}
/>

// Content-only (no TabsContent — manage content externally)
<Tabs tabs={tabs} value={tab} onValueChange={setTab} />
{tab === 'profile' && <ProfileSection />}
{tab === 'activity' && <ActivitySection />}
```

---

### Accordion

**Import:** `import { Accordion } from '@retray-dev/ui-kit'`

**When to use:** FAQs, expandable settings sections, collapsible help content. Animated expand/collapse.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| items | `AccordionItem[]` | required | Accordion entries |
| type | `'single' \| 'multiple'` | `'single'` | `single`: one item open at a time. `multiple`: any number |
| defaultValue | `string \| string[]` | — | Initially open items. Use `string[]` with `type='multiple'` |
| style | `ViewStyle` | — | — |

**AccordionItem type:**
```ts
{
  value: string
  trigger: string
  content: ReactNode
  iconName?: string          // Icon name from @expo/vector-icons
  icon?: ReactNode           // Custom icon node
  iconColor?: string         // Override icon color (defaults to foregroundMuted)
}
```

**Animation:** `react-native-reanimated` `withTiming` (220ms) for height and chevron rotation (180°). Press scale uses `withSpring`. Runs on UI thread at 60fps.

**Haptics:** `selectionAsync` on toggle.

**Examples:**
```tsx
<Accordion
  items={[
    {
      value: 'q1',
      trigger: 'What payment methods do you accept?',
      iconName: 'credit-card',
      content: <Text variant="body-sm">We accept Visa, Mastercard, and bank transfers.</Text>,
    },
    {
      value: 'q2',
      trigger: 'Can I cancel my booking?',
      iconName: 'x-circle',
      content: <Text variant="body-sm">Yes — free cancellation within 48 hours of booking.</Text>,
    },
  ]}
/>

// Multiple open simultaneously
<Accordion type="multiple" defaultValue={['q1']} items={faqItems} />

// Pre-opened
<Accordion defaultValue="getting-started" items={helpSections} />
```

---

### Sheet

**Import:** `import { Sheet, SheetTextInput, BottomSheetModalProvider } from '@retray-dev/ui-kit'`

**When to use:** Bottom sheet for contextual actions, filters, pickers, or detail views that don't need a full screen. Auto-sizes to content — no snap points needed.

**Required setup** — `BottomSheetModalProvider` must wrap app at root (inside `GestureHandlerRootView`). See Setup section above.

**Peer dependencies:**
```bash
pnpm add @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler react-native-worklets
```
Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `babel.config.js`.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| open | `boolean` | required | `true` presents the sheet, `false` dismisses it |
| onClose | `() => void` | required | Called on swipe-dismiss or backdrop press |
| title | `string` | — | Sheet heading |
| subtitle | `string` | — | Supporting text below title (replaces deprecated `description`) |
| showCloseButton | `boolean` | `false` | Show X close button in the header |
| children | `ReactNode` | — | Sheet content |
| style | `ViewStyle` | — | Inner scroll/content container style |
| contentStyle | `ViewStyle` | — | Outer content wrapper style |
| scrollable | `boolean` | `false` | Wraps children in `BottomSheetScrollView` |
| maxHeight | `number` | `~85% screen` | Caps sheet height (dp). Defaults to 85% of screen height. Enables scrolling when content overflows |
| keyboardBehavior | `'extend' \| 'fillParent' \| 'interactive'` | `'interactive'` | How sheet responds to keyboard. `'interactive'` (default) offsets sheet by keyboard size — works on both platforms. `'fillParent'` extends to fill parent view. `'extend'` extends to max snap point |
| keyboardBlurBehavior | `'none' \| 'restore'` | `'restore'` | What happens when keyboard dismisses: `'restore'` → return to pre-keyboard position (recommended), `'none'` → stay at keyboard-adjusted position |
| enableBlurKeyboardOnGesture | `boolean` | `true` | Dismiss keyboard when user starts dragging sheet down (recommended for better UX) |
| android_keyboardInputMode | `'adjustPan' \| 'adjustResize'` | `'adjustPan'` | Android-only: `'adjustPan'` moves the window (default — fixes restore issues with dynamic sizing). `'adjustResize'` resizes the container (can cause transparent gap when keyboard dismisses) |
| footer | `ReactNode` | — | Sticky footer below scroll area (sticky above keyboard) |
| snapPoints | `(string \| number)[]` | — | Optional snap points (e.g., `['50%', '85%']`). When omitted, uses dynamic sizing (auto-fits content) |

**Features:**
- `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed (default behavior when `snapPoints` is omitted)
- `snapPoints` — optionally provide custom snap points (e.g., `['50%', '85%']`). Disables dynamic sizing when provided
- `enablePanDownToClose` — swipe down to dismiss
- Backdrop press dismisses
- **Scrollable content:** use `scrollable` prop or `maxHeight`. Both use `BottomSheetScrollView` — do NOT use plain `ScrollView` inside Sheet
- **Keyboard handling:** Full keyboard awareness via `@gorhom/bottom-sheet` v5. Professional defaults:
  - `keyboardBehavior="interactive"` — sheet offsets by keyboard size on both platforms
  - `android_keyboardInputMode="adjustPan"` — moves window instead of resizing (fixes restore issues on Android)
  - `keyboardBlurBehavior="restore"` — returns to pre-keyboard position when keyboard dismisses
  - `enableBlurKeyboardOnGesture={true}` — dismisses keyboard when dragging sheet down
  - `topInset` — automatically applied from safe area context to prevent going above notch
- **Text inputs inside sheet:** **MUST use `SheetTextInput`** (re-exported `BottomSheetTextInput`) — handles focus/blur internally, communicates with sheet's keyboard system. **Never use regular `TextInput`** inside sheets — keyboard handling will break
- **Custom TextInput:** If using custom input components, you must copy `handleOnFocus`/`handleOnBlur` from [BottomSheetTextInput source](https://github.com/gorhom/react-native-bottom-sheet/blob/master/src/components/bottomSheetTextInput/BottomSheetTextInput.tsx)

**Haptics:** `impactMedium` on open.

**Examples:**
```tsx
const [open, setOpen] = useState(false)

// Basic
<Sheet open={open} onClose={() => setOpen(false)} title="Sort by">
  <RadioGroup options={sortOptions} value={sort} onValueChange={setSort} />
</Sheet>

// Scrollable list
<Sheet open={open} onClose={() => setOpen(false)} title="Results" scrollable maxHeight={500}>
  {items.map((r) => <ListItem key={r.id} title={r.name} showSeparator />)}
</Sheet>

// With text input — keyboard handling works automatically (platform-optimized defaults)
<Sheet 
  open={open} 
  onClose={() => setOpen(false)} 
  title="Add note"
  subtitle="Keyboard handling is automatic with platform-optimized defaults" 
>
  <SheetTextInput 
    placeholder="Write your note..." 
    multiline
    style={{ 
      borderWidth: 1, 
      borderColor: '#ddd',
      borderRadius: 8,
      padding: 12,
      minHeight: 80,
    }} 
  />
  <Button label="Save" fullWidth style={{ marginTop: 12 }} onPress={() => setOpen(false)} />
</Sheet>

// With custom snap points (manual sizing)
<Sheet 
  open={open} 
  onClose={() => setOpen(false)} 
  title="Filters"
  snapPoints={['50%', '90%']}
>
  <RadioGroup options={filterOptions} value={filter} onValueChange={setFilter} />
</Sheet>

// With sticky footer
<Sheet open={open} onClose={() => setOpen(false)} title="Filters" scrollable
  footer={<View style={{ padding: 16 }}><Button label="Apply" fullWidth onPress={() => setOpen(false)} /></View>}>
  {/* long filter content */}
</Sheet>

// With close button
<Sheet open={open} onClose={() => setOpen(false)} title="Details" subtitle="Product info" showCloseButton>
  <Text>Content here...</Text>
</Sheet>
```

**Keyboard handling notes:**
- No `KeyboardAvoidingView` needed — `@gorhom/bottom-sheet` handles everything
- Always use `SheetTextInput` (not plain `TextInput`) for auto-focus/blur handling
- Default `keyboardBehavior="interactive"` works on both platforms
- Default `android_keyboardInputMode="adjustPan"` fixes the transparent gap that occurs with `adjustResize` when keyboard dismisses
- `enableBlurKeyboardOnGesture={true}` (default) dismisses keyboard when dragging sheet

---

### Toast / useToast

**Import:** `import { toast, ToastProvider, useToast } from '@retray-dev/ui-kit'`

**When to use:** Ephemeral feedback messages — save confirmations, errors, copy notifications, background process updates. Auto-dismiss after duration.

**Powered by:** `sonner-native` — white/black background, colored icon marks the variant (not the background), swipe-to-dismiss, promise support, stacking.

**Required setup:** `ToastProvider` must wrap the app. See Setup section above. Also requires `react-native-svg` and `react-native-screens` as peer deps.

**API — direct function (preferred):**
```tsx
import { toast } from '@retray-dev/ui-kit'

// Variants:
toast.success('Saved successfully')
toast.error('Connection error', { description: 'Check your network' })
toast.warning('Slow connection detected')
toast('Neutral message')
toast.loading('Processing...')

// Promise — auto-resolves:
toast.promise(saveData(), {
  loading: 'Saving...',
  success: 'Saved',
  error: 'Failed to save',
})

// Programmatic dismiss:
const id = toast.loading('Uploading...')
await upload()
toast.dismiss(id)
toast.success('Upload complete')
```

**API — hook (backward compat):**
```tsx
const { toast, dismiss } = useToast()
toast.success('Saved')
dismiss(id)
```

**Behavior:**
- Max 3 toasts shown simultaneously
- Appear top-center with safe area insets (respects notch/status bar)
- Swipe up to dismiss early
- Auto-dismiss after 4000ms
- Follows `colorScheme` from `ThemeProvider` (light/dark background)

**Notes:**
- `richColors: false` — background stays white (light) / black (dark). Variant shown via icon color only.
- `closeButton: false` — swipe-to-dismiss is the gesture.

---

### ConfirmDialog

**Import:** `import { ConfirmDialog } from '@retray-dev/ui-kit'`

**When to use:** Confirmation prompts before irreversible or destructive actions — delete, send money, discard changes. Always confirm before actions that can't be undone.

**Requires:** `BottomSheetModalProvider` at app root (same as `Sheet`).

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| visible | `boolean` | required | Controls dialog visibility |
| title | `string` | required | Dialog heading |
| description | `string` | — | Supporting text — describe what exactly will happen |
| confirmLabel | `string` | `'Confirm'` | Confirm button text |
| cancelLabel | `string` | `'Cancel'` | Cancel button text |
| confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Use `'destructive'` for delete/remove actions |
| onConfirm | `() => void` | required | Called when confirm is tapped |
| onCancel | `() => void` | required | Called when cancel is tapped or backdrop pressed |

**Notes:**
- Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`
- Buttons are full-width, stacked vertically (confirm on top)
- Cancel shows X icon, confirm shows check (primary) or trash-2 (destructive) icon
- Swipe down or backdrop press calls `onCancel`

**Haptics:** `impactLight` on open.

**Examples:**
```tsx
// Delete confirmation
<ConfirmDialog
  visible={showDelete}
  title="Delete transaction?"
  description="$45.000 · Mercado · 12 mar · This action cannot be undone."
  confirmLabel="Delete"
  confirmVariant="destructive"
  onConfirm={handleDelete}
  onCancel={() => setShowDelete(false)}
/>

// Send money confirmation
<ConfirmDialog
  visible={showConfirm}
  title="Send payment?"
  description={`Send $${amount} to ${recipientName}`}
  confirmLabel="Send"
  onConfirm={handleSend}
  onCancel={() => setShowConfirm(false)}
/>

// Discard changes
<ConfirmDialog
  visible={showDiscard}
  title="Discard changes?"
  description="All unsaved changes will be lost."
  confirmLabel="Discard"
  confirmVariant="destructive"
  onConfirm={() => { setShowDiscard(false); goBack() }}
  onCancel={() => setShowDiscard(false)}
/>
```

---

### ListItem

**Import:** `import { ListItem } from '@retray-dev/ui-kit'`

**When to use:** Rows in lists, feeds, menus, settings screens. The most compositional component — composes left slot, text block, and right slot in a standard horizontal layout. Supports plain (list items) and card (standalone cards) variants.

**Important:** ListItem has **zero horizontal padding** by design — the consumer controls spacing. Wrap in a container with padding for standalone use, or use `variant="card"` for auto-styled surfaces.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| title | `string` | required | Primary text |
| subtitle | `string` | — | Secondary line below title |
| caption | `string` | — | Tertiary line below subtitle |
| leftRender | `ReactNode` | — | Content in the fixed 44×44pt left slot |
| rightRender | `string \| ReactNode` | — | Content on the right edge. Strings auto-styled as muted text |
| leftIcon | `string` | — | Icon name for left slot. Takes precedence over `leftRender` |
| rightIcon | `string` | — | Icon name for right slot. Takes precedence over `rightRender` |
| leftIconColor | `string` | — | Override left icon color (default: `foreground`) |
| rightIconColor | `string` | — | Override right icon color (default: `foregroundMuted`) |
| variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background. `card`: surface with border and shadow |
| showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightRender` is set |
| showSeparator | `boolean` | `false` | Hairline separator at bottom. Useful for stacking plain items |
| onPress | `() => void` | — | Makes row pressable (scale animation + haptics) |
| disabled | `boolean` | — | Reduces opacity to 0.45 |
| style | `ViewStyle` | — | Outer container style |
| titleStyle | `TextStyle` | — | Override title text style |
| subtitleStyle | `TextStyle` | — | Override subtitle text style |
| captionStyle | `TextStyle` | — | Override caption text style |
| icon | `ReactNode` | — | **Deprecated** — use `leftRender` |
| trailing | `string \| ReactNode` | — | **Deprecated** — use `rightRender` |

**Slots:**
- `leftRender` / `leftIcon` — 44×44pt fixed container, centered. Good for Avatar, icons, thumbnails
- `rightRender` / `rightIcon` — max 160pt wide, right-aligned. Good for Badge, price text, Switch
- `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender` is absent

**Separator inset:** Aligns to text block — `marginLeft` adjusts to skip over left slot when present.

**Animation:** Scale springs to 0.97 on press (subtler than Button's 0.95).

**Haptics:** `selectionAsync` on press.

**Examples:**
```tsx
// Simple settings row
<ListItem title="Profile" showChevron onPress={() => navigate('profile')} />

// With subtitle and chevron
<ListItem
  title="Notifications"
  subtitle="Push, email, and SMS"
  showChevron
  onPress={() => navigate('notifications')}
/>

// Icon left slot + chevron
<ListItem title="Profile" leftIcon="user" showChevron onPress={() => navigate('profile')} />

// Icon left + badge right
<ListItem
  leftIcon="bell"
  title="Notifications"
  subtitle="3 unread"
  rightRender={<Badge label="3" variant="destructive" size="sm" />}
  onPress={() => navigate('notifications')}
/>

// Avatar + full text stack + right content
<ListItem
  leftRender={<Avatar src={user.avatar} fallback={user.initials} status="online" />}
  title={user.name}
  subtitle={user.role}
  caption="Active 2 min ago"
  rightRender={
    <View style={{ alignItems: 'flex-end', gap: 4 }}>
      <Text variant="title-sm">${amount}</Text>
      <Badge label={status} variant="warningOutline" size="sm" />
    </View>
  }
  onPress={() => navigate('user', { id: user.id })}
/>

// Switch in right slot
<ListItem
  leftIcon="moon"
  title="Dark mode"
  rightRender={<Switch checked={darkMode} onCheckedChange={setDarkMode} />}
/>

// Card variant (standalone surface)
<ListItem
  variant="card"
  leftIcon="credit-card"
  title="Checking account"
  subtitle="Available balance"
  rightRender="$12.500"
/>

// List with separators
{transactions.map((t, i) => (
  <ListItem
    key={t.id}
    leftRender={<Avatar fallback={t.merchant[0]} size="sm" />}
    title={t.merchant}
    subtitle={t.date}
    rightRender={
      <Text variant="title-sm" color={t.amount < 0 ? colors.destructive : colors.success}>
        {t.amount < 0 ? '-' : '+'}${Math.abs(t.amount)}
      </Text>
    }
    showSeparator={i < transactions.length - 1}
    onPress={() => navigate('transaction', { id: t.id })}
  />
))}
```

---

### MenuItem

**Import:** `import { MenuItem } from '@retray-dev/ui-kit'`

**When to use:** Settings screens, navigation lists, menu entries — icon + label + optional subtitle + optional right content. Purpose-built for Airbnb-style settings rows. Use `ListItem` when you need caption, metadata rows, or more complex layouts beyond label+subtitle.

**Important:** MenuItem has **zero horizontal padding** by design — the consumer controls spacing. Wrap in a container with padding for standalone use, or use `variant="card"` for auto-styled surfaces.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | required | Row label |
| subtitle | `string` | — | Secondary text below label. Uses `mutedForeground` color |
| iconName | `string` | — | Icon from `@expo/vector-icons`. Auto-resolved across all 6 families |
| icon | `ReactNode` | — | Custom icon (used if `iconName` not set) |
| iconColor | `string` | — | Override icon color. Defaults to `foreground` |
| rightRender | `ReactNode` | — | Custom content on the right. Replaces default chevron. Use for Switch, Checkbox, Badge, etc. |
| showChevron | `boolean` | `true` | Show chevron on the right. Ignored when `rightRender` is set |
| onPress | `() => void` | required | Press handler |
| variant | `'plain' \| 'card'` | `'plain'` | `plain` sits inside parent surface. `card` has own background + border |
| disabled | `boolean` | `false` | Dims and disables press |
| showSeparator | `boolean` | `false` | Hairline separator at bottom |
| style | `ViewStyle` | — | Container style override |
| labelStyle | `TextStyle` | — | Label text style override |

**Features:**
- Height: 54dp — consistent settings row
- Chevron: shown by default (`showChevron={true}`). Can be replaced via `rightRender` prop
- `rightRender` — arbitrary right slot content (Switch, Checkbox, Badge, etc). When set, chevron is hidden
- Press scale: 0.97 (`PRESS_SCALE.row`) via `usePressScale` with `SPRINGS.surfacePressIn/Out` — Reanimated v4, UI thread
- Haptics: `selectionAsync` on press

**Example:**
```tsx
// Settings screen (wrap in container with padding)
<View style={{ padding: 16, backgroundColor: colors.card, borderRadius: 12 }}>
  <MenuItem label="Personal info" subtitle="Name, email, phone" iconName="user" onPress={() => navigate('profile')} showSeparator />
  <MenuItem label="Security" iconName="shield" onPress={() => navigate('security')} showSeparator />
  <MenuItem label="Privacy" iconName="lock" onPress={() => navigate('privacy')} showSeparator />
  <MenuItem 
    label="Notifications" 
    iconName="bell" 
    onPress={() => setNotifs(!notifs)}
    rightRender={<Switch value={notifs} onValueChange={setNotifs} />}
    showSeparator 
  />
  <MenuItem 
    label="Dark Mode" 
    iconName="moon" 
    onPress={() => setDark(!dark)}
    rightRender={<Checkbox checked={dark} onPress={() => setDark(!dark)} />}
    showSeparator 
  />
  <MenuItem 
    label="Payments" 
    iconName="credit-card" 
    onPress={() => navigate('payments')}
    rightRender={<Badge label="New" variant="warning" size="sm" />}
  />
</View>

// Card variant (standalone)
<MenuItem
  variant="card"
  label="Refer a friend"
  iconName="gift"
  onPress={() => navigate('referral')}
/>

// No chevron (custom right content)
<MenuItem
  label="Language"
  iconName="globe"
  rightRender={<Text>English</Text>}
  onPress={() => navigate('language')}
/>
```

---

### Chip / ChipGroup

**Import:** `import { Chip, ChipGroup } from '@retray-dev/ui-kit'`

**When to use:** Inline filter options, quick selections, multi-select toggles. Use `Chip` for standalone custom logic; use `ChipGroup` for managed selection (single or multi).

**`Chip` Props:**

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | required | — |
| selected | `boolean` | `false` | Controls fill style |
| onPress | `() => void` | — | — |
| icon | `ReactNode` | — | Custom icon before label |
| iconName | `string` | — | Icon name. Takes precedence over `icon` |
| style | `ViewStyle` | — | — |

**`ChipGroup` Props:**

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| options | `ChipOption[]` | required | `{ label, value, iconName?, iconColor?, disabled? }` |
| value | `string \| number \| (string \| number)[]` | — | Selected value(s) |
| onValueChange | `(value: ...) => void` | — | Returns single value or array depending on `multiSelect` |
| multiSelect | `boolean` | `false` | Allow multiple chips selected simultaneously |
| style | `ViewStyle` | — | Wrapping row style |

**Styling:** Pill-shaped (borderRadius: 9999), 14px horizontal + 5px vertical padding, 1px border. Unselected: `surface` bg / `foreground` text / `border` border. Selected: `primary` bg / `primaryForeground` text / `primary` border.

**Animation:** Spring scale 0.95 on press; background/text/border color transitions (150ms timing) on selection change.

**Haptics:** `selectionAsync` on change.

**Examples:**
```tsx
// Single select percentage
const [pct, setPct] = useState(50)
<ChipGroup
  options={[{ label: '25%', value: 25 }, { label: '50%', value: 50 }, { label: '100%', value: 100 }]}
  value={pct}
  onValueChange={setPct}
/>

// Multi select categories
const [categories, setCategories] = useState<number[]>([1, 3])
<ChipGroup
  multiSelect
  options={[
    { label: 'Food', value: 1 },
    { label: 'Transport', value: 2 },
    { label: 'Entertainment', value: 3 },
    { label: 'Health', value: 4 },
  ]}
  value={categories}
  onValueChange={setCategories}
/>

// Standalone chips (custom logic)
<View style={{ flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap' }}>
  {filters.map((f) => (
    <Chip
      key={f.value}
      label={f.label}
      selected={activeFilter === f.value}
      onPress={() => setActiveFilter(f.value)}
    />
  ))}
</View>
```

---

### CategoryStrip

**Import:** `import { CategoryStrip } from '@retray-dev/ui-kit'`

**When to use:** Horizontal scrollable filter/category bar at the top of browse screens — marketplace categories, content type filters, location tabs. Airbnb-style pill chips that scroll horizontally.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| categories | `CategoryItem[]` | required | Category definitions |
| value | `string \| string[]` | — | Selected category value(s) |
| onValueChange | `(value: string \| string[]) => void` | — | Called with new selection |
| multiSelect | `boolean` | `false` | Allow multiple simultaneous selections |
| style | `ViewStyle` | — | ScrollView content container style |
| itemStyle | `ViewStyle` | — | Style applied to each chip's wrapper |

**CategoryItem type:**
```ts
{
  label: string
  value: string
  icon?: ReactNode | string   // Icon or icon name (16px, auto-colored)
  badge?: number              // Count badge on chip, capped at 99
}
```

**Single select behavior:** Pressing selected item again deselects it (value becomes `''`).

**Multi select behavior:** Toggles item in/out of value array.

**Styling:** Pill-shaped chips (`borderRadius: RADIUS.full = 9999`), 14px horizontal + 8px vertical padding, 8px gap between chips. Selected: `primary` bg + `primaryForeground` text. Unselected: `surface` bg + `foregroundSubtle` text + `border` border.

**Animation:** Scale spring 0.95 on press.

**Haptics:** `selectionAsync` on change.

**Web:** Horizontal scroll with `showsHorizontalScrollIndicator={false}`.

**Examples:**
```tsx
// Basic category filter
<CategoryStrip
  categories={[
    { label: 'All', value: '' },
    { label: 'Trips', value: 'trips' },
    { label: 'Experiences', value: 'experiences' },
    { label: 'Restaurants', value: 'restaurants' },
  ]}
  value={category}
  onValueChange={setCategory}
/>

// With icons
<CategoryStrip
  categories={[
    { label: 'Nearby', value: 'nearby', icon: 'map-pin' },
    { label: 'Popular', value: 'popular', icon: 'trending-up' },
    { label: 'New', value: 'new', icon: 'star' },
  ]}
  value={filter}
  onValueChange={setFilter}
/>

// With badge counts
<CategoryStrip
  categories={[
    { label: 'Inbox', value: 'inbox', badge: 3 },
    { label: 'Sent', value: 'sent' },
    { label: 'Archived', value: 'archived' },
  ]}
  value={tab}
  onValueChange={setTab}
/>

// Multi-select
<CategoryStrip
  multiSelect
  categories={amenities}
  value={selectedAmenities}
  onValueChange={setSelectedAmenities}
/>
```

---

### LabelValue

**Import:** `import { LabelValue } from '@retray-dev/ui-kit'`

**When to use:** Key-value display rows in receipts, transaction details, order summaries, profile information. Label on left (muted caption), value on right (medium weight).

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string` | required | Caption label on the left |
| value | `string \| ReactNode` | required | Value on the right. Strings auto-styled; pass `ReactNode` for custom content |
| iconName | `string` | — | Icon name from `@expo/vector-icons` rendered left of label |
| iconColor | `string` | — | Override icon color. Defaults to `foregroundMuted` |
| style | `ViewStyle` | — | — |

**Styling:** Row layout, `justifyContent: 'space-between'`. Label: 13pt / Regular / `foregroundMuted`. Value: 14pt / Medium / `foreground`. 12px gap.

**Examples:**
```tsx
<LabelValue label="Date" value="12 mar 2025" iconName="calendar" />
<LabelValue label="Category" value="Food & drink" iconName="tag" />
<LabelValue label="Status" value={<Badge label="Pending" variant="warningOutline" size="sm" />} iconName="clock" />
<LabelValue label="Amount" value="$45.000" iconName="dollar-sign" />

// Transaction detail
<View style={{ gap: SPACING.sm }}>
  <LabelValue label="Merchant" value={transaction.merchant} iconName="shopping-bag" />
  <LabelValue label="Date" value={formatDate(transaction.date)} iconName="calendar" />
  <LabelValue label="Category" value={transaction.category} iconName="tag" />
  <Separator />
  <LabelValue label="Amount" value={`$${transaction.amount}`} iconName="dollar-sign" />
</View>
```

---

### DetailRow

**Import:** `import { DetailRow } from '@retray-dev/ui-kit'`

**When to use:** "Label ··· Value" ticket/receipt rows — financial summaries, split bills, transaction breakdowns. Dotted separator fills the space between label and value.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| label | `string \| ReactNode` | required | Left side. Strings auto-styled in `foregroundMuted` caption |
| value | `string` | required | Right side value text |
| separator | `'dotted' \| 'solid' \| 'dashed' \| 'none'` | `'dotted'` | Fill line between label and value |
| labelWeight | `'normal' \| 'medium' \| 'semibold' \| 'bold'` | `'normal'` | Font weight of label text |
| valueColor | `string` | — | Override value text color (hex or theme token value) |
| leftIcon | `ReactNode` | — | Node rendered left of label (e.g. `Avatar`, `Icon`) |
| leftIconName | `string` | — | Icon name from `@expo/vector-icons` rendered left of label. Takes precedence over `leftIcon` |
| leftIconColor | `string` | — | Override left icon color. Defaults to `foregroundMuted` |
| rightIconName | `string` | — | Icon name from `@expo/vector-icons` rendered right of value |
| rightIconColor | `string` | — | Override right icon color. Defaults to `foregroundMuted` |
| style | `ViewStyle` | — | Row container style |
| labelStyle | `TextStyle` | — | Label text style override |
| valueStyle | `TextStyle` | — | Value text style override |

**Examples:**
```tsx
// Basic
<DetailRow label="Total" value="$50.000" />

// With icons
<DetailRow label="Food" value="$380.000" leftIconName="coffee" />
<DetailRow label="Transport" value="$95.000" leftIconName="truck" />
<DetailRow label="Total" value="$1.250.000" labelWeight="bold" rightIconName="trending-up" />

// Dotted separator with bold total
<DetailRow label="Total" value="$50.000" labelWeight="bold" />

// With avatar left icon
<DetailRow
  label="Juliana"
  value="$25.000"
  leftIcon={<Avatar src={avatarUrl} size={20} />}
/>

// Destructive value
<DetailRow label="Deuda" value="$150.000" valueColor={colors.destructive} />

// No separator
<DetailRow label="Subtotal" value="$45.000" separator="none" />

// Full receipt block
<View style={{ gap: SPACING.xs }}>
  <DetailRow label="Juliana" value="$25.000" leftIcon={<Avatar fallback="J" size={20} />} />
  <DetailRow label="Julián" value="$25.000" leftIcon={<Avatar fallback="JC" size={20} />} />
  <Separator />
  <DetailRow label="Total" value="$50.000" labelWeight="bold" />
</View>
```

**Output:**
```
Juliana ············ $25.000
Julián ············· $25.000
─────────────────────────────
Total ·············· $50.000
```

---

### MonthPicker

**Import:** `import { MonthPicker } from '@retray-dev/ui-kit'`

**When to use:** Month/year navigation — finance apps filtering by period, date pickers, reporting periods. Compact left/right navigation UI.

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| value | `MonthPickerValue` | required | `{ month: 1–12, year: number }` |
| onChange | `(value: MonthPickerValue) => void` | required | Called on navigation |
| locale | `string` | `'en'` | BCP 47 locale. Built-in: `'en'`, `'es'`, `'pt'`, `'fr'`. Other locales: use `formatLabel` |
| formatLabel | `(value: MonthPickerValue) => string` | — | Custom label formatter. Takes precedence over `locale` |
| style | `ViewStyle` | — | — |

**MonthPickerValue type:**
```ts
{ month: number; year: number }  // month is 1-indexed (January = 1)
```

**Navigation:** Year wraps correctly at boundaries (December → January increments year, January → December decrements year). Fully controlled — no internal state.

**Styling:** Centered row with `←` / `→` buttons (44×44px each). Label: 17pt / Medium / 160px min-width, centered.

**Haptics:** `selectionAsync` on navigation.

**Examples:**
```tsx
const [period, setPeriod] = useState({
  month: new Date().getMonth() + 1,
  year: new Date().getFullYear(),
})

<MonthPicker value={period} onChange={setPeriod} />
// Displays: "May 2026"

<MonthPicker value={period} onChange={setPeriod} locale="es" />
// Displays: "mayo 2026"

<MonthPicker
  value={period}
  onChange={setPeriod}
  formatLabel={({ month, year }) => `${month}/${year}`}
/>
// Displays: "5/2026"

// In a transactions header
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
  <Text variant="display-md">Transactions</Text>
  <MonthPicker value={period} onChange={setPeriod} />
</View>
```

---

### Pressable

**Import:** `import { Pressable } from '@retray-dev/ui-kit'`

**When to use:** Custom interactive content that needs beautiful spring bounce effect matching MediaCard. Use when you need a pressable wrapper around custom layouts that aren't covered by existing button components.

**Extends:** `TouchableOpacityProps` (all native props pass through except `activeOpacity`)

| Prop | Type | Default | Notes |
|------|------|---------|-------|
| children | `ReactNode` | required | Content to render inside the pressable |
| onPress | `() => void` | — | Press handler |
| pressScale | `number` | `0.98` | Scale value on press (MediaCard-style) |
| bounciness | `number` | `4` | Spring bounciness on release |
| haptics | `boolean` | `true` | Enable haptic feedback on press |
| hoverScale | `number` | `1.02` | Hover scale (web only). Set to `1` to disable |
| disabled | `boolean` | `false` | Disable interaction |
| style | `ViewStyle` | — | Animated wrapper style |

**Behavior:**
- Press: springs to `pressScale` (default 0.98) with `speed: 40, bounciness: 0`
- Release: springs back to 1.0 with `speed: 40, bounciness: 4`
- Web: optional hover scale (default 1.02)
- Haptics: `impactLight` on press (unless `haptics={false}`)

**Use cases:**
- Custom card layouts
- Complex pressable rows that need consistent feel
- Non-standard interactive elements that aren't buttons
- Wrapping groups of elements as a single pressable unit

**Examples:**
```tsx
// Custom card
<Pressable onPress={() => navigate('detail')} style={{ padding: 16, borderRadius: 12, backgroundColor: colors.card }}>
  <Text variant="title-md">Custom Card</Text>
  <Text variant="body-sm">Press me for beautiful bounce effect</Text>
</Pressable>

// Wrapping complex layout
<Pressable onPress={handleSelect} pressScale={0.96} bounciness={8}>
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
    <Avatar src={user.avatar} size="md" />
    <View style={{ flex: 1 }}>
      <Text variant="title-sm">{user.name}</Text>
      <Text variant="caption-sm">{user.role}</Text>
    </View>
    <Icon name="chevron-right" size={20} color={colors.foregroundMuted} />
  </View>
</Pressable>

// Disable haptics
<Pressable onPress={handleQuickAction} haptics={false}>
  {/* content */}
</Pressable>

// Custom press scale (deeper press)
<Pressable onPress={handlePress} pressScale={0.92} bounciness={6}>
  {/* content */}
</Pressable>
```

---

## Icon System

The library ships a built-in icon resolver — pass any icon name string to components without manual imports.

### Supported families (priority order — first match wins)

| Priority | Family | Best for |
|---|---|---|
| 1 (highest) | `Feather` | Clean line icons, UI essentials |
| 2 | `AntDesign` | Semantic UI icons |
| 3 | `Entypo` | Social, media, navigation icons |
| 4 | `FontAwesome5` | Wide coverage |
| 5 | `MaterialIcons` | Material-style icons |
| 6 (lowest) | `Ionicons` | Fallback |

Browse all available icons: **https://icons.expo.fyi**

### Standalone `Icon` component

```tsx
import { Icon } from '@retray-dev/ui-kit'

<Icon name="home" size={24} color={colors.foreground} />
<Icon name="star" size={20} color={colors.primary} />

// Force a specific family when same name exists in multiple families:
<Icon name="heart" size={24} color="red" family="FontAwesome5" />
```

**Props:**

| Prop | Type | Required | Notes |
|------|------|----------|-------|
| name | `string` | yes | Icon name (e.g. `"home"`, `"arrow-right"`) |
| size | `number` | yes | Icon size in points |
| color | `string` | yes | Icon color |
| family | `IconFamily` | no | Force a specific family |

Returns `null` (no crash) if name not found in any family.

### `renderIcon` helper

```tsx
import { renderIcon } from '@retray-dev/ui-kit'

// Returns ReactNode or null
const icon = renderIcon('check', 18, colors.primary)
```

### `iconName` props on components

All components with icon slots accept `iconName` — auto-resolved size and color:

| Component | Prop(s) | Slot | Default color |
|-----------|---------|------|---------------|
| `Button` | `iconName`, `iconColor` | Left or right of label | Variant label color |
| `IconButton` | `iconName`, `iconColor` | Center | Variant foreground |
| `Input` | `prefixIcon`, `prefixIconColor` | Before input | `foregroundMuted` |
| `Input` | `suffixIcon`, `suffixIconColor` | After input | `foregroundMuted` |
| `ListItem` | `leftIcon`, `leftIconColor` | Left 44×44 slot | `foreground` |
| `ListItem` | `rightIcon`, `rightIconColor` | Right slot | `foregroundMuted` |
| `Badge` | `iconName`, `iconColor` | Before label | Variant foreground |
| `Toggle` | `iconName`, `iconColor` | When not pressed | `foregroundMuted` |
| `Toggle` | `activeIconName`, `activeIconColor` | When pressed | `primary` |
| `AlertBanner` | `iconName`, `iconColor` | Left of content | Variant title color |
| `EmptyState` | `iconName`, `iconColor` | Center icon slot | `foregroundMuted` |
| `Toast` | `iconName`, `iconColor` | Left of message | Variant text color |
| `MediaCard` | `actionIconName` | Top-right of image | `#ffffff` |
| `Chip` | `iconName` | Before label | Variant foreground |

**Precedence:** `iconName` always takes precedence over the corresponding `ReactNode` prop when both are supplied.

---

### `getResponsiveFontSize` utility

Calculates a stepped-down font size for long numeric strings (currency amounts, large numbers) that would overflow at full size.

```tsx
import { getResponsiveFontSize } from '@retray-dev/ui-kit'

// Default steps: ≤10 chars → max, ≤12 → max-4, ≤14 → max-6, >14 → max-8
const fontSize = getResponsiveFontSize(formatCOP(amount), 48)

<Text style={{ fontSize }} numberOfLines={1} adjustsFontSizeToFit>
  {formatCOP(amount)}
</Text>
```

**Signature:**
```ts
getResponsiveFontSize(
  text: string,
  maxSize: number,
  steps?: { maxLen: number; subtract: number }[]
): number
```

Custom steps example:
```tsx
getResponsiveFontSize(text, 48, [
  { maxLen: 8,  subtract: 0 },
  { maxLen: 11, subtract: 6 },
  { maxLen: 14, subtract: 10 },
])
```

**Alternative:** Use `<CurrencyDisplay autoScale maxFontSize={48} />` for currency values — no manual sizing needed.

---

## Hover Support (Web)

```tsx
import { useHover } from '@retray-dev/ui-kit'

function MyHoverableComponent() {
  const { hovered, hoverHandlers } = useHover()

  return (
    <View
      {...hoverHandlers}
      style={[
        styles.container,
        hovered && { backgroundColor: colors.surfaceStrong }
      ]}
    />
  )
}
```

- **Web:** Returns `{ hovered: boolean, hoverHandlers: { onMouseEnter, onMouseLeave } }`
- **Native:** Always returns `{ hovered: false, hoverHandlers: {} }` — no-op, no crashes

Built-in web hover: `MediaCard` (shadow lift), interactive components use this internally.

---

## Design System Conventions

### Touch targets
All interactive elements maintain ≥44pt touch height per Apple HIG:
- Button md: 48px, sm: 40px, lg: 56px
- Input / Textarea: 56px (14px vertical padding)
- IconButton md: 44px, lg: 52px
- Checkbox: 24×24px box
- Switch: 30px track height
- MonthPicker arrows: 44×44px

### Haptic patterns
- `impactLight` — Button, Card (press), Sheet open, Toast, MediaCard action
- `selectionAsync` — Checkbox, Switch, Toggle, RadioGroup, Select, Slider steps, Accordion, ListItem, Chip, CategoryStrip, Tabs, MonthPicker
- `notificationSuccess` — Toast `success`
- `notificationError` — Toast `destructive`, Toast `warning`

### Animation press scales
- `Button` → 0.95 (strong spring feedback)
- `Card` → 0.98 (subtle, appropriate for large surfaces)
- `ListItem` → 0.97 (between — medium row targets)
- `MediaCard` → 0.98 (large surface)
- `IconButton` → 0.95
- `Chip`, `CategoryStrip chip` → 0.95

### Platform differences
- `useNativeDriver` — `Platform.OS !== 'web'` everywhere (native driver disabled on web)
- `Select` — wheel modal on iOS, dialog on Android, `<select>` on web
- `Toast` — full width on mobile, 400px centered on web
- Hover states — web only via `useHover()`
- `Skeleton` shimmer highlight — adapts opacity for light/dark mode

### Dynamic Type
All `Text` and `TextInput` components have `allowFontScaling={true}` — respects user font size accessibility settings.

### Scaling utilities (internal)
Used internally — not exported. `s()` horizontal scale, `vs()` vertical scale, `ms()` moderate scale relative to 350×680 base.

---

## Full Composition Examples

The package includes **EXAMPLES.md** with complete, working code for 3 real-world app screens:

1. **Finance Dashboard** — MonthPicker, CurrencyDisplay, Progress, CategoryStrip, ListItem, DetailRow
2. **Edit Profile** — Avatar, Badge, Input, Select, Switch, Card, AlertBanner
3. **Onboarding Flow** — Badge, Progress, Input, RadioGroup, EmptyState (multi-step wizard)

**For AI agents:** Import EXAMPLES.md from the package for full source code and patterns:

```markdown
## UI Kit Composition Examples
@./node_modules/@retray-dev/ui-kit/EXAMPLES.md
```

Each example includes:
- Complete component code (copy-paste ready)
- State management setup
- Proper imports and theme usage
- Common patterns (spacing, colors, navigation, toast feedback)
- StyleSheet definitions

**For human developers:** Examples are also live in the `example/` app. Clone the repo, run `pnpm install && pnpm build`, then `cd example && pnpm start` to see them in action. Access via "🖥 App Screen Compositions" accordion.
