@retray-dev/ui-kit 4.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/COMPONENTS.md +1806 -663
  2. package/README.md +14 -10
  3. package/dist/index.d.mts +274 -85
  4. package/dist/index.d.ts +274 -85
  5. package/dist/index.js +1048 -321
  6. package/dist/index.mjs +1046 -324
  7. package/package.json +3 -2
  8. package/src/components/Accordion/Accordion.tsx +1 -1
  9. package/src/components/AlertBanner/AlertBanner.tsx +50 -45
  10. package/src/components/Avatar/Avatar.tsx +61 -17
  11. package/src/components/Badge/Badge.tsx +17 -15
  12. package/src/components/Button/Button.tsx +31 -42
  13. package/src/components/Card/Card.tsx +4 -4
  14. package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
  15. package/src/components/CategoryStrip/index.ts +2 -0
  16. package/src/components/Checkbox/Checkbox.tsx +44 -16
  17. package/src/components/Chip/Chip.tsx +1 -1
  18. package/src/components/ConfirmDialog/ConfirmDialog.tsx +9 -9
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
  21. package/src/components/EmptyState/EmptyState.tsx +9 -9
  22. package/src/components/IconButton/IconButton.tsx +74 -34
  23. package/src/components/Input/Input.tsx +15 -13
  24. package/src/components/LabelValue/LabelValue.tsx +1 -1
  25. package/src/components/ListItem/ListItem.tsx +5 -5
  26. package/src/components/MediaCard/MediaCard.tsx +249 -0
  27. package/src/components/MediaCard/index.ts +2 -0
  28. package/src/components/Pressable/Pressable.tsx +100 -0
  29. package/src/components/Pressable/index.ts +1 -0
  30. package/src/components/Progress/Progress.tsx +14 -7
  31. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  32. package/src/components/Select/Select.tsx +5 -5
  33. package/src/components/Sheet/Sheet.tsx +35 -15
  34. package/src/components/Skeleton/Skeleton.tsx +34 -7
  35. package/src/components/Slider/Slider.tsx +2 -2
  36. package/src/components/Spinner/Spinner.tsx +1 -1
  37. package/src/components/Switch/Switch.tsx +31 -4
  38. package/src/components/Tabs/Tabs.tsx +63 -45
  39. package/src/components/Text/Text.tsx +59 -10
  40. package/src/components/Textarea/Textarea.tsx +4 -3
  41. package/src/components/Toast/Toast.tsx +77 -36
  42. package/src/components/Toggle/Toggle.tsx +3 -3
  43. package/src/index.ts +8 -2
  44. package/src/theme/ThemeProvider.tsx +11 -10
  45. package/src/theme/colorUtils.ts +80 -0
  46. package/src/theme/colors.ts +76 -35
  47. package/src/theme/index.ts +2 -2
  48. package/src/theme/types.ts +27 -13
  49. package/src/tokens.ts +150 -13
  50. package/src/utils/hover.ts +25 -0
package/COMPONENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @retray-dev/ui-kit — Component Reference (v4.0.0)
1
+ # @retray-dev/ui-kit — Component Reference (v5.2.0)
2
2
 
3
3
  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:
4
4
 
@@ -44,7 +44,7 @@ export default function App() {
44
44
 
45
45
  ## Typography — Poppins (Required)
46
46
 
47
- All components use **Poppins** as the font family. You **must** load it before rendering any UI kit component, otherwise text will fall back to the system font.
47
+ All components use **Poppins** as the font family. You **must** load it before rendering any UI kit component.
48
48
 
49
49
  ```tsx
50
50
  import { useFonts } from 'expo-font'
@@ -52,8 +52,7 @@ import { PoppinsFonts } from '@retray-dev/ui-kit/fonts'
52
52
 
53
53
  export default function App() {
54
54
  const [fontsLoaded] = useFonts(PoppinsFonts)
55
- if (!fontsLoaded) return null // or show a splash screen
56
-
55
+ if (!fontsLoaded) return null
57
56
  return (
58
57
  // ... your providers and app
59
58
  )
@@ -66,49 +65,44 @@ export default function App() {
66
65
  3. Metro resolves font files from `node_modules/@retray-dev/ui-kit/src/assets/fonts/` at bundle time
67
66
  4. All library components reference fonts by family name (e.g., `fontFamily: 'Poppins-SemiBold'`)
68
67
 
69
- **Why this pattern:**
70
- - Fonts are NOT bundled into `dist/` — they ship as raw `.ttf` files inside `src/assets/fonts/`
71
- - Metro resolves `require()` calls to these files when it bundles your app
72
- - This prevents font corruption that can occur when `.ttf` files pass through bundlers like esbuild/tsup
73
-
74
- **Setup:**
75
- - Add `expo-font` to your app: `pnpm add expo-font`
76
- - The `if (!fontsLoaded) return null` guard prevents text from rendering before fonts are ready
77
- - Pair with `expo-splash-screen` in production to avoid a flash:
78
- ```tsx
79
- import * as SplashScreen from 'expo-splash-screen'
80
- SplashScreen.preventAutoHideAsync()
81
-
82
- useEffect(() => {
83
- if (fontsLoaded) SplashScreen.hideAsync()
84
- }, [fontsLoaded])
85
- ```
86
-
87
68
  **Included weights:**
88
69
  - Regular: `Poppins-Thin`, `Poppins-ExtraLight`, `Poppins-Light`, `Poppins-Regular`, `Poppins-Medium`, `Poppins-SemiBold`, `Poppins-Bold`, `Poppins-ExtraBold`, `Poppins-Black`
89
70
  - Italic: `Poppins-Italic`, `Poppins-MediumItalic`, `Poppins-SemiBoldItalic`, `Poppins-BoldItalic`
90
71
 
91
- **Total: 13 font files** exported from the package. Additional italic variants exist in `src/assets/fonts/` but are not exported if you need them, open an issue.
72
+ **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.
73
+
74
+ Pair with `expo-splash-screen` in production:
75
+ ```tsx
76
+ import * as SplashScreen from 'expo-splash-screen'
77
+ SplashScreen.preventAutoHideAsync()
78
+
79
+ useEffect(() => {
80
+ if (fontsLoaded) SplashScreen.hideAsync()
81
+ }, [fontsLoaded])
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Theme System
92
87
 
93
88
  ### ThemeProvider Props
94
89
 
95
90
  | Prop | Type | Default | Notes |
96
91
  |------|------|---------|-------|
97
- | colorScheme | `'light' \| 'dark' \| 'system'` | `'system'` | `'system'` auto-detects the device setting and updates when it changes |
98
- | theme | `{ light?: Partial<ThemeColors>, dark?: Partial<ThemeColors> }` | — | Override any subset of color tokens per scheme |
92
+ | colorScheme | `'light' \| 'dark' \| 'system'` | `'system'` | `'system'` auto-detects device setting and updates when it changes |
93
+ | theme | `{ light?: Partial<ThemeColors>, dark?: Partial<ThemeColors> }` | — | Override any subset of the 12 public tokens per scheme |
99
94
 
100
95
  **Custom theme example:**
101
96
  ```tsx
102
97
  const myTheme = {
103
- light: { primary: '#6366f1', primaryForeground: '#ffffff' },
104
- dark: { primary: '#818cf8', primaryForeground: '#ffffff' },
98
+ light: { primary: '#ff385c', primaryForeground: '#ffffff' },
99
+ dark: { primary: '#ff385c', primaryForeground: '#ffffff' },
105
100
  }
106
101
  <ThemeProvider theme={myTheme} colorScheme="system">
107
102
  ```
108
103
 
109
104
  ### useTheme Hook
110
105
 
111
- Access the active color tokens and scheme inside any component:
112
106
  ```tsx
113
107
  import { useTheme } from '@retray-dev/ui-kit'
114
108
 
@@ -118,138 +112,238 @@ function MyComponent() {
118
112
  }
119
113
  ```
120
114
 
115
+ Returns `colors` (full `ResolvedColors` palette) and `colorScheme` (`'light' | 'dark'`).
116
+
121
117
  ---
122
118
 
123
119
  ## Theme Tokens
124
120
 
125
- All 23 tokens are available via `useTheme().colors` and are applied exactly as provided there is no automatic color derivation or forced contrast applied by the library. Consumers may pass partial overrides for `light` and/or `dark` and the values are used as-is.
121
+ ### Public Tokens (ThemeColors) — 12 tokens consumer can override
126
122
 
127
- The `ThemeColors` type (and `Theme`, `ColorScheme`) are exported directly from the package:
128
- ```ts
129
- import type { ThemeColors } from '@retray-dev/ui-kit'
130
- ```
123
+ These are the only values you need to supply when customizing the theme. The library derives all other colors internally.
131
124
 
132
- | Token | Light | Dark | Semantic Role |
133
- |-------|-------|------|---------------|
125
+ | Token | Light Default | Dark Default | Semantic Role |
126
+ |-------|--------------|--------------|---------------|
134
127
  | `background` | `#ffffff` | `#0f0f0f` | Screen / page background |
135
- | `foreground` | `#171717` | `#fafafa` | Primary text color |
136
- | `card` | `#ffffff` | `#1c1c1c` | Card / surface background |
137
- | `cardForeground` | `#171717` | `#fafafa` | Text on cards |
138
- | `primary` | `#1a1a1a` | `#fafafa` | Primary action (buttons, selected states) |
139
- | `primaryForeground` | `#ffffff` | `#0f0f0f` | Text/icon on primary background |
140
- | `secondary` | `#f1f1f1` | `#272727` | Secondary surfaces |
141
- | `secondaryForeground` | `#171717` | `#fafafa` | Text on secondary |
142
- | `muted` | `#f1f1f1` | `#272727` | Muted backgrounds, skeleton fills, track fills |
143
- | `mutedForeground` | `#a2a2a2` | `#9a9a9a` | Placeholder text, helper text, captions |
144
- | `accent` | `#e4e4e4` | `#2e2e2e` | Hover / pressed state fills |
145
- | `accentForeground` | `#171717` | `#fafafa` | Text on accent |
146
- | `destructive` | `#ef4444` | `#dc2626` | Error / danger / delete actions — base color |
147
- | `destructiveForeground` | `#ffffff` | `#ffffff` | Text on destructive |
148
- | `destructiveTint` | `#fff5f5` | `#3b0a0a` | Very light/dark background tint for destructive states |
149
- | `destructiveBorder` | `#fecaca` | `#7f1d1d` | Border color for destructive outlined elements |
150
- | `success` | `#1a7a45` | `#166534` | Success / confirmation / done actions — base color |
151
- | `successForeground` | `#ffffff` | `#ffffff` | Text on success |
152
- | `successTint` | `#f0fdf4` | `#052e16` | Very light/dark background tint for success states |
153
- | `successBorder` | `#bbf7d0` | `#166534` | Border color for success outlined elements |
154
- | `border` | `#e5e5e5` | `#303030` | Borders and dividers |
155
- | `input` | `#e5e5e5` | `#2a2a2a` | Input field border color |
156
- | `ring` | `#1a1a1a` | `#fafafa` | Optional focus ring token (components may use `primary` by default) |
128
+ | `foreground` | `#222222` | `#fafafa` | Primary text deep near-black, not pure black |
129
+ | `card` | `#ffffff` | `#1c1c1c` | Card / elevated surface background |
130
+ | `primary` | `#1a1a1a` | `#fafafa` | Primary action (buttons, selected states, active indicators) |
131
+ | `primaryForeground` | `#ffffff` | `#0f0f0f` | Text/icon placed on primary-colored backgrounds |
132
+ | `border` | `#dddddd` | `#303030` | Borders, dividers, input outlines |
133
+ | `destructive` | `#e53935` | `#ef5350` | Error / danger / delete actions |
134
+ | `destructiveForeground` | `#ffffff` | `#ffffff` | Text/icon on destructive backgrounds |
135
+ | `success` | `#1a7a45` | `#2e7d52` | Success / confirmation states |
136
+ | `successForeground` | `#ffffff` | `#ffffff` | Text/icon on success backgrounds |
137
+ | `warning` | `#e67e00` | `#f57c00` | Warning / caution states |
138
+ | `warningForeground` | `#ffffff` | `#ffffff` | Text/icon on warning backgrounds |
139
+
140
+ ### Derived Tokens (ResolvedColors) read-only via useTheme().colors
141
+
142
+ The full palette components consume. Never supply these directly they are computed by `deriveColors()` from the 12 public tokens above.
143
+
144
+ | Token | Derived From | Purpose |
145
+ |-------|-------------|---------|
146
+ | `foregroundSubtle` | `foreground` @ ~55% | Body text, subtitles, secondary content |
147
+ | `foregroundMuted` | `foreground` @ ~38% | Captions, timestamps, placeholders |
148
+ | `surface` | `background` slightly off-canvas | Chip backgrounds, input fills, tag backgrounds, skeleton |
149
+ | `surfaceStrong` | `background` stronger offset | Pressed/hover fill states |
150
+ | `destructiveTint` | `destructive` blended to bg | Alert banner background, toast background |
151
+ | `destructiveBorder` | `destructive` @ 30% | Alert banner border, badge outline |
152
+ | `successTint` | `success` blended to bg | Success banner background |
153
+ | `successBorder` | `success` @ 30% | Success banner border |
154
+ | `warningTint` | `warning` blended to bg | Warning banner background |
155
+ | `warningBorder` | `warning` @ 30% | Warning banner border |
156
+ | `ring` | `= primary` | Focus ring color (always matches primary) |
157
+ | `input` | `= border` | Input field border (always matches border) |
158
+
159
+ **Usage example — building a custom component using derived tokens:**
160
+ ```tsx
161
+ const { colors } = useTheme()
162
+
163
+ // Text hierarchy
164
+ <Text style={{ color: colors.foreground }}>Primary text</Text>
165
+ <Text style={{ color: colors.foregroundSubtle }}>Secondary text</Text>
166
+ <Text style={{ color: colors.foregroundMuted }}>Caption / timestamp</Text>
167
+
168
+ // Surface fills (unselected chips, inactive backgrounds)
169
+ <View style={{ backgroundColor: colors.surface }}>
170
+
171
+ // Warning alert banner
172
+ <View style={{ backgroundColor: colors.warningTint, borderColor: colors.warningBorder }}>
173
+ <Text style={{ color: colors.warning }}>Warning message</Text>
174
+ ```
175
+
176
+ ### deriveColors export
177
+
178
+ ```tsx
179
+ import { deriveColors } from '@retray-dev/ui-kit'
180
+
181
+ const resolved = deriveColors(myThemeColors, 'light')
182
+ // resolved contains all 24 ResolvedColors tokens
183
+ ```
157
184
 
158
185
  ---
159
186
 
160
187
  ## Design Tokens
161
188
 
162
- Static structural constants exported from the package root — no context or provider needed. Use these instead of hardcoding values.
189
+ Static structural constants — no context or provider needed.
163
190
 
164
191
  ```ts
165
- import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS } from '@retray-dev/ui-kit'
192
+ import { SPACING, ICON_SIZES, RADIUS, SHADOWS, BREAKPOINTS, TYPOGRAPHY } from '@retray-dev/ui-kit'
166
193
  ```
167
194
 
168
- ### SPACING
169
-
170
- 8pt-grid spacing scale.
195
+ ### SPACING — 8pt grid with 2pt micro-step
171
196
 
172
- | Key | Value |
173
- |-----|-------|
174
- | `xs` | 4 |
175
- | `sm` | 8 |
176
- | `md` | 12 |
177
- | `lg` | 16 |
178
- | `xl` | 24 |
179
- | `2xl` | 32 |
180
- | `3xl` | 48 |
197
+ | Key | Value | Use |
198
+ |-----|-------|-----|
199
+ | `xxs` | 2 | Micro gaps (icon to text in badges) |
200
+ | `xs` | 4 | Tight internal gaps |
201
+ | `sm` | 8 | Component internal padding |
202
+ | `md` | 12 | Medium gaps |
203
+ | `base` | 16 | Default content padding |
204
+ | `lg` | 24 | Section internal padding |
205
+ | `xl` | 32 | Between major content blocks |
206
+ | `xxl` | 48 | Section padding |
207
+ | `section` | 64 | Major band separators (Airbnb hero → grid rhythm) |
181
208
 
182
209
  **Types:** `Spacing`, `SpacingKey`
183
210
 
184
211
  ```tsx
185
- <View style={{ gap: SPACING.md, padding: SPACING.lg }} />
212
+ <View style={{ gap: SPACING.md, padding: SPACING.base }} />
186
213
  ```
187
214
 
188
215
  ### ICON_SIZES
189
216
 
190
- Semantic icon size scale aligned to HIG roles.
191
-
192
- | Key | Value |
193
- |-----|-------|
194
- | `sm` | 14 |
195
- | `md` | 18 |
196
- | `lg` | 22 |
197
- | `xl` | 28 |
198
- | `2xl` | 32 |
217
+ | Key | Value | Use |
218
+ |-----|-------|-----|
219
+ | `sm` | 14 | Badge icons, inline micro icons |
220
+ | `md` | 18 | Standard component icons |
221
+ | `lg` | 22 | Larger inline icons |
222
+ | `xl` | 28 | Feature icons |
223
+ | `2xl` | 32 | Display icons |
199
224
 
200
225
  **Types:** `IconSize`, `IconSizeKey`
201
226
 
202
- ```tsx
203
- <Icon name="home" size={ICON_SIZES.md} color={colors.foreground} />
204
- ```
205
-
206
- ### RADIUS
207
-
208
- Border radius scale used throughout the library.
227
+ ### RADIUS — Airbnb shape language
209
228
 
210
229
  | Key | Value | Used in |
211
230
  |-----|-------|---------|
212
- | `sm` | 4 | |
213
- | `md` | 8 | Inputs, Buttons, Checkboxes, Toggles, Tabs |
214
- | `lg` | 12 | Cards, AlertBanner, Toast, EmptyState, Select list |
215
- | `xl` | 16 | Sheet top corners |
216
- | `full` | 9999 | Pills, circular elements |
231
+ | `none` | 0 | No rounding |
232
+ | `xs` | 4 | Micro chips, tags |
233
+ | `sm` | 8 | Inputs, Textarea, Select, Checkbox |
234
+ | `md` | 14 | Cards, MediaCard, AlertBanner, Toast, EmptyState |
235
+ | `lg` | 20 | Sheet top corners |
236
+ | `xl` | 32 | Primary CTA buttons (pill-like) |
237
+ | `full` | 9999 | Circular elements, CategoryStrip chips |
217
238
 
218
239
  **Types:** `Radius`, `RadiusKey`
219
240
 
220
241
  ```tsx
221
- <View style={{ borderRadius: RADIUS.lg }} />
242
+ <View style={{ borderRadius: RADIUS.md }} />
222
243
  ```
223
244
 
224
- ### SHADOWS
225
-
226
- Cross-platform shadow presets (RN `shadow*` properties + `elevation`).
245
+ ### SHADOWS — Cross-platform shadow presets
227
246
 
228
- | Key | Height | Opacity | Radius | Elevation |
229
- |-----|--------|---------|--------|-----------|
230
- | `sm` | 1 | 0.08 | 4 | 2 |
231
- | `md` | 3 | 0.12 | 8 | 5 |
232
- | `lg` | 6 | 0.20 | 16 | 10 |
233
- | `xl` | 12 | 0.28 | 24 | 18 |
247
+ | Key | shadowOffset.height | shadowOpacity | shadowRadius | elevation | Use |
248
+ |-----|---------------------|---------------|--------------|-----------|-----|
249
+ | `sm` | 1 | 0.06 | 4 | 2 | Default card shadow |
250
+ | `md` | 2 | 0.10 | 8 | 5 | Hover float / elevated elements |
251
+ | `lg` | 6 | 0.16 | 16 | 10 | Modals, overlays |
252
+ | `xl` | 12 | 0.24 | 24 | 18 | High-elevation dialogs |
234
253
 
235
254
  ```tsx
236
- <View style={[styles.card, SHADOWS.md]} />
255
+ <View style={[styles.card, SHADOWS.sm]} />
256
+ // Hover state
257
+ <View style={[styles.card, hovered ? SHADOWS.md : SHADOWS.sm]} />
237
258
  ```
238
259
 
239
260
  ### BREAKPOINTS
240
261
 
241
- Layout breakpoints.
262
+ | Key | Value | Use |
263
+ |-----|-------|-----|
264
+ | `wide` | 700 | Tablet / wide layout threshold |
265
+
266
+ ```tsx
267
+ const isWide = useWindowDimensions().width >= BREAKPOINTS.wide
268
+ ```
242
269
 
243
- | Key | Value |
244
- |-----|-------|
245
- | `wide` | 700 |
270
+ ### TYPOGRAPHY 16 Airbnb-aligned variants
271
+
272
+ All components use these tokens for text styling. Import and use in custom components for consistency.
273
+
274
+ | Key | Size | Weight | lineHeight | letterSpacing | Use |
275
+ |-----|------|--------|-----------|---------------|-----|
276
+ | `display-hero` | 64 | 700 | 70 | -1 | Large number display, balance totals |
277
+ | `display-xl` | 28 | 700 | 40 | 0 | Screen titles, hero headings |
278
+ | `display-lg` | 22 | 500 | 26 | -0.44 | Section headings |
279
+ | `display-md` | 21 | 700 | 30 | 0 | Card titles, dialog titles |
280
+ | `display-sm` | 20 | 600 | 24 | -0.18 | Sub-section headings |
281
+ | `title-md` | 16 | 600 | 20 | 0 | Row titles, list item titles |
282
+ | `title-sm` | 16 | 500 | 20 | 0 | Secondary titles |
283
+ | `body-md` | 16 | 400 | 24 | 0 | Primary body copy |
284
+ | `body-sm` | 14 | 400 | 20 | 0 | Secondary body, descriptions |
285
+ | `caption` | 14 | 500 | 18 | 0 | Labels above inputs, item captions |
286
+ | `caption-sm` | 13 | 400 | 16 | 0 | Timestamps, metadata |
287
+ | `badge-text` | 11 | 600 | 13 | 0 | Badge labels, small tags |
288
+ | `micro-label` | 12 | 700 | 16 | 0 | Micro labels, overlines |
289
+ | `uppercase-tag` | 8 | 700 | 10 | 0.32 | Uppercase decorative tags (auto-uppercase) |
290
+ | `button-lg` | 16 | 500 | 20 | 0 | Button labels (md/lg size) |
291
+ | `button-sm` | 14 | 500 | 18 | 0 | Button labels (sm size) |
292
+
293
+ **Types:** `Typography`, `TypographyKey`
246
294
 
247
295
  ```tsx
248
- const isWide = width >= BREAKPOINTS.wide
296
+ import { TYPOGRAPHY } from '@retray-dev/ui-kit'
297
+
298
+ // Use in StyleSheet
299
+ const styles = StyleSheet.create({
300
+ heading: {
301
+ ...TYPOGRAPHY['display-xl'],
302
+ color: colors.foreground,
303
+ },
304
+ })
249
305
  ```
250
306
 
251
307
  ---
252
308
 
309
+ ## Migration Guide: v4 → v5
310
+
311
+ ### Breaking Changes
312
+
313
+ **Button variants renamed:**
314
+ | v4 | v5 |
315
+ |----|-----|
316
+ | `outline` | `secondary` |
317
+ | `ghost` | `text` |
318
+ | `secondary` (filled gray) | removed — use Card/surface instead |
319
+
320
+ **Text variants replaced:**
321
+ | v4 | v5 equivalent |
322
+ |----|---------------|
323
+ | `h1` | `display-xl` |
324
+ | `h2` | `display-lg` or `display-md` |
325
+ | `h3` | `display-sm` |
326
+ | `body` | `body-md` |
327
+ | `label` | `title-sm` or `caption` |
328
+ | `caption` | `caption-sm` |
329
+
330
+ **Theme tokens changed:**
331
+ | v4 | v5 |
332
+ |----|-----|
333
+ | `secondary`, `secondaryForeground` | removed (use `surface` derived token) |
334
+ | `accent`, `accentForeground` | removed (use `surfaceStrong`) |
335
+ | `muted` | removed → `surface` (derived) |
336
+ | `mutedForeground` | removed → `foregroundSubtle` / `foregroundMuted` (derived) |
337
+ | Added | `warning`, `warningForeground` |
338
+ | Added (derived) | `warningTint`, `warningBorder` |
339
+
340
+ **IconButton variant:**
341
+ | v4 | v5 |
342
+ |----|-----|
343
+ | `ghost` | `text` |
344
+
345
+ ---
346
+
253
347
  ## Components
254
348
 
255
349
  ---
@@ -257,28 +351,68 @@ const isWide = width >= BREAKPOINTS.wide
257
351
  ### Text
258
352
 
259
353
  **Import:** `import { Text } from '@retray-dev/ui-kit'`
260
- **When to use:** All text in the app. Replaces React Native's `Text` with semantic variants.
261
- **Extends:** `TextProps` from React Native — all native props pass through.
354
+
355
+ **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.
356
+
357
+ **Extends:** `TextProps` from React Native
262
358
 
263
359
  | Prop | Type | Default | Notes |
264
360
  |------|------|---------|-------|
265
- | variant | `'h1' \| 'h2' \| 'h3' \| 'body' \| 'caption' \| 'label'` | `'body'` | Sets font size, weight, and line height |
266
- | color | `string` | — | Override the color. Defaults to `foreground`, except `caption` which uses `mutedForeground` |
267
-
268
- **Sizes:**
269
- - `h1`: 40px / 700 weight / lineHeight 52
270
- - `h2`: 28px / 700 weight / lineHeight 36
271
- - `h3`: 22px / 600 weight / lineHeight 30
272
- - `body`: 17px / 400 weight / lineHeight 26
273
- - `label`: 15px / 500 weight / lineHeight 22
274
- - `caption`: 13px / 400 weight / lineHeight 20 / `mutedForeground` color by default
275
-
276
- **Example:**
361
+ | variant | `TextVariant` | `'body-md'` | Sets font, size, weight, line height, letter spacing |
362
+ | color | `string` | — | Override text color. Each variant has a semantic default (see table below) |
363
+ | children | `ReactNode` | required | — |
364
+ | style | `TextStyle` | — | Additional styles (merged after variant styles) |
365
+ | (all TextProps) | — | — | `numberOfLines`, `ellipsizeMode`, `onPress`, etc. all pass through |
366
+
367
+ **Variant reference with semantic defaults:**
368
+
369
+ | Variant | Size | Weight | Default Color | When to use |
370
+ |---------|------|--------|--------------|-------------|
371
+ | `display-hero` | 64 | 700 | `foreground` | Large numeric displays — balance totals, stats, hero numbers |
372
+ | `display-xl` | 28 | 700 | `foreground` | Screen-level titles, onboarding hero headings |
373
+ | `display-lg` | 22 | 500 | `foreground` | Section headings, card group labels |
374
+ | `display-md` | 21 | 700 | `foreground` | Card titles, dialog headings |
375
+ | `display-sm` | 20 | 600 | `foreground` | Sub-section titles |
376
+ | `title-md` | 16 | 600 | `foreground` | List row titles, navigation labels |
377
+ | `title-sm` | 16 | 500 | `foreground` | Secondary row titles |
378
+ | `body-md` | 16 | 400 | `foregroundSubtle` | Main body copy, descriptions |
379
+ | `body-sm` | 14 | 400 | `foregroundSubtle` | Secondary descriptions, detail text |
380
+ | `caption` | 14 | 500 | `foreground` | Labels above inputs, item captions |
381
+ | `caption-sm` | 13 | 400 | `foregroundMuted` | Timestamps, metadata, helper text |
382
+ | `badge-text` | 11 | 600 | `foreground` | Badge labels, small status tags |
383
+ | `micro-label` | 12 | 700 | `foreground` | Micro labels, overlines |
384
+ | `uppercase-tag` | 8 | 700 | `foreground` | Decorative uppercase category labels (auto-transforms text) |
385
+ | `button-lg` | 16 | 500 | `foreground` | Internal use in Button component (md/lg) |
386
+ | `button-sm` | 14 | 500 | `foreground` | Internal use in Button component (sm) |
387
+
388
+ **All text has `allowFontScaling={true}` — respects user's font size settings.**
389
+
390
+ **Examples:**
277
391
  ```tsx
278
- <Text variant="h2">Welcome back</Text>
279
- <Text variant="body">Your account is ready.</Text>
280
- <Text variant="caption">Last updated 2 hours ago</Text>
281
- <Text variant="label" color="#6366f1">Pro plan</Text>
392
+ // Screen title
393
+ <Text variant="display-xl">Welcome back</Text>
394
+
395
+ // Financial balance display
396
+ <Text variant="display-hero" color={colors.primary}>$25.000</Text>
397
+
398
+ // Section heading with subtle color
399
+ <Text variant="display-lg" color={colors.foregroundSubtle}>Recent activity</Text>
400
+
401
+ // Body copy
402
+ <Text variant="body-md">Your account is ready to use.</Text>
403
+
404
+ // Caption / metadata
405
+ <Text variant="caption-sm" color={colors.foregroundMuted}>2 hours ago</Text>
406
+
407
+ // Input label (used automatically by Input, but also useful standalone)
408
+ <Text variant="caption">Email address</Text>
409
+
410
+ // Uppercase category tag
411
+ <Text variant="uppercase-tag">New</Text>
412
+
413
+ // Color override
414
+ <Text variant="title-md" color={colors.primary}>Primary colored title</Text>
415
+ <Text variant="body-sm" color={colors.destructive}>Error message in body</Text>
282
416
  ```
283
417
 
284
418
  ---
@@ -286,39 +420,81 @@ const isWide = width >= BREAKPOINTS.wide
286
420
  ### Button
287
421
 
288
422
  **Import:** `import { Button } from '@retray-dev/ui-kit'`
289
- **When to use:** Any interactive action. Use `variant` to communicate intent.
290
- **Extends:** `TouchableOpacityProps` — all RN TouchableOpacity props pass through.
423
+
424
+ **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).
425
+
426
+ **Extends:** `TouchableOpacityProps` from React Native — all native props pass through.
291
427
 
292
428
  | Prop | Type | Default | Notes |
293
429
  |------|------|---------|-------|
294
430
  | label | `string` | required | Button text |
295
- | variant | `'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'destructive'` | `'primary'` | Visual style |
296
- | size | `'sm' \| 'md' \| 'lg'` | `'md'` | |
297
- | loading | `boolean` | `false` | Replaces label with a spinner and forces disabled state |
431
+ | variant | `'primary' \| 'secondary' \| 'text' \| 'destructive'` | `'primary'` | Visual style |
432
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls height, padding, and icon size |
433
+ | loading | `boolean` | `false` | Replaces label with spinner, forces disabled state |
298
434
  | fullWidth | `boolean` | `false` | Stretches to container width (`alignSelf: 'stretch'`) |
299
435
  | disabled | `boolean` | — | Reduces opacity to 0.5 |
300
- | icon | `React.ReactNode \| ((props: { label, size, variant }) => React.ReactNode)` | — | Icon rendered alongside the label. Can be a node or a render function |
301
- | iconName | `string` | — | Icon name from `@expo/vector-icons` (e.g. `"arrow-right"`). See [icons.expo.fyi](https://icons.expo.fyi). Takes precedence over `icon` |
436
+ | icon | `React.ReactNode \| ((props: { label, size, variant }) => React.ReactNode)` | — | Icon rendered alongside label |
437
+ | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
302
438
  | iconColor | `string` | — | Override icon color. Defaults to variant label color |
303
439
  | iconPosition | `'left' \| 'right'` | `'left'` | Side the icon appears on |
440
+ | style | `ViewStyle` | — | Override container style |
304
441
 
305
442
  **Variants:**
306
- - `primary`: filled with `primary` token — main actions
307
- - `secondary`: filled with `secondary` token less prominent actions
308
- - `outline`: transparent with `border` — alternative without fill
309
- - `ghost`: fully transparentin-context or low-emphasis actions
310
- - `destructive`: filled with `destructive` token irreversible or dangerous actions
443
+
444
+ | Variant | Background | Border | Text | When to use |
445
+ |---------|-----------|--------|------|-------------|
446
+ | `primary` | `primary` | none | `primaryForeground` | Main CTA save, submit, continue |
447
+ | `secondary` | transparent | `primary` (1.5px) | `primary` | Alternative action alongside primary |
448
+ | `text` | transparent | none | `primary` | Low-emphasis — cancel, skip, see more |
449
+ | `destructive` | `destructive` | none | `destructiveForeground` | Irreversible actions — delete, remove |
450
+
451
+ **Sizes:**
452
+
453
+ | Size | Height | Horizontal padding | Icon size | Font |
454
+ |------|--------|--------------------|-----------|------|
455
+ | `sm` | 40px | 16px | 16px | `button-sm` (14pt/500) |
456
+ | `md` | 48px | 24px | 18px | `button-lg` (16pt/500) |
457
+ | `lg` | 56px | 28px | 20px | `button-lg` (16pt/500) |
458
+
459
+ **Shape:** `borderRadius: RADIUS.xl = 32px` — pill-shaped on all sizes.
311
460
 
312
461
  **Animations:** Scale springs to 0.95 on `onPressIn`, back to 1.0 on `onPressOut`.
313
462
 
314
- **Example:**
463
+ **Haptics:** `impactLight` on every press.
464
+
465
+ **Examples:**
315
466
  ```tsx
316
- <Button label="Save changes" onPress={handleSave} />
317
- <Button label="Cancel" variant="ghost" onPress={onCancel} />
318
- <Button label="Delete" variant="outline" size="sm" />
319
- <Button label="Submitting..." loading fullWidth />
320
- <Button label="Continue" iconName="arrow-right" iconPosition="right" />
321
- <Button label="Delete" variant="destructive" iconName="trash-2" />
467
+ // Standard CTA
468
+ <Button label="Continue" onPress={handleContinue} />
469
+
470
+ // Destructive action
471
+ <Button label="Delete account" variant="destructive" onPress={handleDelete} />
472
+
473
+ // Cancel (low emphasis)
474
+ <Button label="Cancel" variant="text" onPress={onCancel} />
475
+
476
+ // Alternative action
477
+ <Button label="Save draft" variant="secondary" onPress={saveDraft} />
478
+
479
+ // Loading state
480
+ <Button label="Saving..." loading onPress={handleSave} />
481
+
482
+ // Full width with icon
483
+ <Button label="Get started" iconName="arrow-right" iconPosition="right" fullWidth />
484
+
485
+ // Destructive with icon
486
+ <Button label="Delete" variant="destructive" iconName="trash-2" size="sm" />
487
+
488
+ // Large with icon on left
489
+ <Button label="Add to trip" iconName="plus" size="lg" />
490
+ ```
491
+
492
+ **Composition — action pair (primary + secondary):**
493
+ ```tsx
494
+ <View style={{ gap: SPACING.sm }}>
495
+ <Button label="Book now" fullWidth />
496
+ <Button label="Save for later" variant="secondary" fullWidth />
497
+ </View>
322
498
  ```
323
499
 
324
500
  ---
@@ -326,29 +502,81 @@ const isWide = width >= BREAKPOINTS.wide
326
502
  ### IconButton
327
503
 
328
504
  **Import:** `import { IconButton } from '@retray-dev/ui-kit'`
329
- **When to use:** Compact icon-only pressable action — toolbars, FABs, inline icon actions. Use `Button` when a label is needed.
330
- **Extends:** `TouchableOpacityProps`all RN TouchableOpacity props pass through.
505
+
506
+ **When to use:** Icon-only pressable actions toolbars, FABs, navigation actions, close buttons, header actions. Use `Button` when a text label is needed.
507
+
508
+ **Extends:** `TouchableOpacityProps` from React Native
331
509
 
332
510
  | Prop | Type | Default | Notes |
333
511
  |------|------|---------|-------|
334
- | iconName | `string` | — | Icon name from `@expo/vector-icons` (e.g. `"plus"`, `"x"`, `"home"`). Takes precedence over `icon` |
335
- | icon | `React.ReactNode` | — | Custom icon node — used when `iconName` is not provided |
512
+ | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
513
+ | icon | `ReactNode` | — | Custom icon node when `iconName` is not provided |
336
514
  | iconColor | `string` | — | Override icon color. Defaults to variant foreground color |
337
- | variant | `'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'destructive'` | `'primary'` | Visual style — same tokens as `Button` |
338
- | size | `'sm' \| 'md' \| 'lg'` | `'md'` | sm=40pt / md=44pt / lg=52pt (all ≥44pt on md/lg per HIG) |
339
- | loading | `boolean` | `false` | Replaces icon with a spinner and forces disabled state |
515
+ | variant | `'primary' \| 'secondary' \| 'outline' \| 'text' \| 'destructive'` | `'primary'` | Visual style |
516
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
517
+ | loading | `boolean` | `false` | Replaces icon with spinner |
518
+ | badge | `boolean \| number` | — | Notification badge overlay |
340
519
  | disabled | `boolean` | — | Reduces opacity to 0.5 |
520
+ | style | `ViewStyle` | — | — |
521
+
522
+ **Variants:** Same token logic as Button.
523
+
524
+ | Variant | Background | Border | Icon color | When to use |
525
+ |---------|-----------|--------|-----------|-------------|
526
+ | `primary` | `primary` | none | `primaryForeground` | Prominent action (FAB, main toolbar action) |
527
+ | `secondary` | `surface` | none | `foreground` | Secondary toolbar action |
528
+ | `outline` | transparent | `border` | `foreground` | Tertiary action with visible boundary |
529
+ | `text` | transparent | none | `foreground` | Inline action, header close/back |
530
+ | `destructive` | `destructive` | none | `destructiveForeground` | Delete/remove action |
341
531
 
342
- **Variants:** Same color logic as `Button` — `primary`, `secondary`, `outline`, `ghost`, `destructive`.
532
+ **Sizes:**
533
+
534
+ | Size | Dimensions | Icon size |
535
+ |------|-----------|-----------|
536
+ | `sm` | 32 × 32px | 16px |
537
+ | `md` | 44 × 44px | 20px |
538
+ | `lg` | 52 × 52px | 24px |
539
+
540
+ **Badge prop:**
541
+ - `true` → 8×8px dot (primary color, top-right corner)
542
+ - `number` → 16×16px pill with count, capped at 99 (shows "99+" for higher values)
543
+ - Badge is positioned absolutely — does not affect button layout
343
544
 
344
- **Animations:** Scale springs to 0.95 on `onPressIn`, back to 1.0 on `onPressOut`. `impactAsync(Light)` haptic on press.
545
+ **Animations:** Scale springs to 0.95 on press.
345
546
 
346
- **Example:**
547
+ **Haptics:** `impactLight` on press.
548
+
549
+ **Examples:**
347
550
  ```tsx
348
- <IconButton iconName="plus" onPress={handleAdd} />
349
- <IconButton iconName="x" variant="ghost" size="sm" onPress={handleClose} />
551
+ // Close button
552
+ <IconButton iconName="x" variant="text" size="sm" onPress={handleClose} />
553
+
554
+ // FAB
555
+ <IconButton iconName="plus" size="lg" onPress={handleAdd} />
556
+
557
+ // Delete action
350
558
  <IconButton iconName="trash-2" variant="destructive" onPress={handleDelete} />
351
- <IconButton iconName="check" variant="outline" size="lg" loading={saving} />
559
+
560
+ // Notification bell with badge count
561
+ <IconButton iconName="bell" variant="text" badge={3} onPress={() => navigate('notifications')} />
562
+
563
+ // Unread dot (boolean badge)
564
+ <IconButton iconName="mail" variant="outline" badge={true} onPress={() => {}} />
565
+
566
+ // Header back button
567
+ <IconButton iconName="arrow-left" variant="text" onPress={navigation.goBack} />
568
+
569
+ // Loading state while action runs
570
+ <IconButton iconName="save" loading={saving} onPress={handleSave} />
571
+ ```
572
+
573
+ **Composition — toolbar row:**
574
+ ```tsx
575
+ <View style={{ flexDirection: 'row', gap: SPACING.xs }}>
576
+ <IconButton iconName="share" variant="text" onPress={handleShare} />
577
+ <IconButton iconName="bookmark" variant="text" onPress={handleBookmark} />
578
+ <IconButton iconName="more-horizontal" variant="text" onPress={handleMore} />
579
+ </View>
352
580
  ```
353
581
 
354
582
  ---
@@ -356,36 +584,114 @@ const isWide = width >= BREAKPOINTS.wide
356
584
  ### Input
357
585
 
358
586
  **Import:** `import { Input } from '@retray-dev/ui-kit'`
359
- **When to use:** Single-line text entry. Includes built-in label, error, and hint support.
360
- **Extends:** `TextInputProps` from React Native all native props pass through.
587
+
588
+ **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.
589
+
590
+ **Extends:** `TextInputProps` from React Native — all native props pass through (`keyboardType`, `autoCapitalize`, `returnKeyType`, etc.)
361
591
 
362
592
  | Prop | Type | Default | Notes |
363
593
  |------|------|---------|-------|
364
- | label | `string` | — | Label above the input |
365
- | error | `string` | — | Shows error text below; turns border red (`destructive` token) |
594
+ | label | `string` | — | Label above the input field |
595
+ | error | `string` | — | Error text below turns border `destructive` (2px) |
366
596
  | hint | `string` | — | Helper text below (hidden when `error` is set) |
367
- | prefix | `string \| ReactNode` | — | Content rendered before the input field (e.g., `"$"` or an icon) |
368
- | suffix | `string \| ReactNode` | — | Content rendered after the input field (e.g., `"kg"` or a button) |
369
- | prefixStyle | `TextStyle` | — | Style applied to the prefix text when prefix is a string |
370
- | suffixStyle | `TextStyle` | — | Style applied to the suffix text when suffix is a string |
371
- | prefixIcon | `string` | — | Icon name from `@expo/vector-icons` rendered before the input. Takes precedence over `prefix` |
372
- | suffixIcon | `string` | — | Icon name from `@expo/vector-icons` rendered after the input. Takes precedence over `suffix` (unless `type="password"`) |
373
- | prefixIconColor | `string` | — | Override prefix icon color. Defaults to `mutedForeground` |
374
- | suffixIconColor | `string` | — | Override suffix icon color. Defaults to `mutedForeground` |
375
- | type | `'text' \| 'password'` | `'text'` | When `'password'`, shows a visibility toggle button in the suffix slot |
376
- | containerStyle | `ViewStyle` | — | Style for the outer container `View` |
377
-
378
- **Border colors:** `destructive` when `error` is set, `ring` when focused, `border` otherwise.
379
-
380
- **Example:**
597
+ | prefix | `string \| ReactNode` | — | Content before the input text (e.g. `"$"`, icon node) |
598
+ | suffix | `string \| ReactNode` | — | Content after the input text (e.g. `"kg"`, icon node) |
599
+ | prefixStyle | `TextStyle` | — | Style for prefix string (no effect on ReactNode) |
600
+ | suffixStyle | `TextStyle` | — | Style for suffix string (no effect on ReactNode) |
601
+ | prefixIcon | `string` | — | Icon name rendered before input. Takes precedence over `prefix` |
602
+ | suffixIcon | `string` | — | Icon name rendered after input. Takes precedence over `suffix` unless `type="password"` |
603
+ | prefixIconColor | `string` | — | Override prefix icon color. Defaults to `foregroundMuted` |
604
+ | suffixIconColor | `string` | — | Override suffix icon color. Defaults to `foregroundMuted` |
605
+ | type | `'text' \| 'password'` | `'text'` | Password shows eye/eye-off toggle button in suffix slot |
606
+ | containerStyle | `ViewStyle` | — | Outer container View style |
607
+ | inputWrapperStyle | `ViewStyle` | — | The bordered wrapper around the input row |
608
+ | style | `TextStyle` | | TextInput element style |
609
+
610
+ **Dimensions:** 56px height (14px vertical padding), 8px border radius.
611
+
612
+ **Border states:**
613
+ - Default: `border` token (1px)
614
+ - Focused: `primary` (2px)
615
+ - Error: `destructive` (2px)
616
+ - Focused + error: `destructive` (2px)
617
+
618
+ **Examples:**
381
619
  ```tsx
382
- <Input label="Email" placeholder="you@example.com" keyboardType="email-address" />
620
+ // Standard email input
621
+ <Input label="Email" placeholder="you@example.com" keyboardType="email-address" autoCapitalize="none" />
622
+
623
+ // Password with toggle
383
624
  <Input label="Password" type="password" error="Incorrect password" />
384
- <Input label="Username" hint="Must be at least 4 characters" />
385
- <Input label="Amount" prefix="$" placeholder="0.00" keyboardType="numeric" />
386
- <Input label="Weight" suffix="kg" placeholder="0" />
625
+
626
+ // With hint text
627
+ <Input label="Username" hint="4–20 characters, letters and numbers only" />
628
+
629
+ // Currency prefix (string)
630
+ <Input label="Amount" prefix="$" placeholder="0.00" keyboardType="decimal-pad" />
631
+
632
+ // Unit suffix
633
+ <Input label="Weight" suffix="kg" placeholder="0" keyboardType="numeric" />
634
+
635
+ // Search (no label, prefix icon)
387
636
  <Input placeholder="Search..." prefixIcon="search" />
637
+
638
+ // With both icons
388
639
  <Input placeholder="Amount" prefixIcon="dollar-sign" suffixIcon="check" />
640
+
641
+ // With custom ReactNode prefix
642
+ <Input
643
+ label="Phone"
644
+ prefix={<Text variant="body-md">+1</Text>}
645
+ keyboardType="phone-pad"
646
+ />
647
+
648
+ // Controlled with error
649
+ <Input
650
+ label="Email"
651
+ value={email}
652
+ onChangeText={setEmail}
653
+ error={emailError}
654
+ keyboardType="email-address"
655
+ />
656
+ ```
657
+
658
+ **Composition — form fields:**
659
+ ```tsx
660
+ <View style={{ gap: SPACING.md }}>
661
+ <Input label="First name" value={firstName} onChangeText={setFirstName} />
662
+ <Input label="Last name" value={lastName} onChangeText={setLastName} />
663
+ <Input label="Email" value={email} onChangeText={setEmail} error={emailError} keyboardType="email-address" />
664
+ <Input label="Password" type="password" value={password} onChangeText={setPassword} />
665
+ <Button label="Create account" fullWidth onPress={handleSubmit} />
666
+ </View>
667
+ ```
668
+
669
+ ---
670
+
671
+ ### Textarea
672
+
673
+ **Import:** `import { Textarea } from '@retray-dev/ui-kit'`
674
+
675
+ **When to use:** Multi-line text entry — bios, descriptions, notes, reviews. Same visual design as `Input` but taller with multi-line support.
676
+
677
+ **Extends:** `TextInputProps` from React Native — all native props pass through.
678
+
679
+ | Prop | Type | Default | Notes |
680
+ |------|------|---------|-------|
681
+ | label | `string` | — | Label above the textarea |
682
+ | error | `string` | — | Error text below; turns border destructive (2px) |
683
+ | hint | `string` | — | Helper text below (hidden when `error` is set) |
684
+ | rows | `number` | `4` | Minimum row count — each row ≈ 30px. Sets `numberOfLines` |
685
+ | containerStyle | `ViewStyle` | — | Outer container style |
686
+ | style | `TextStyle` | — | TextInput element style |
687
+
688
+ **Border states:** Identical to Input (focused=primary 2px, error=destructive 2px).
689
+
690
+ **Examples:**
691
+ ```tsx
692
+ <Textarea label="Bio" placeholder="Tell us about yourself" rows={5} />
693
+ <Textarea label="Review" hint="Be honest and helpful" />
694
+ <Textarea label="Notes" error="Notes are required" rows={3} />
389
695
  ```
390
696
 
391
697
  ---
@@ -393,29 +699,32 @@ const isWide = width >= BREAKPOINTS.wide
393
699
  ### CurrencyInput
394
700
 
395
701
  **Import:** `import { CurrencyInput } from '@retray-dev/ui-kit'`
396
- **When to use:** Monetary or numeric inputs that need thousands formatting while typing. Wraps `Input` — shares the same visual design, label, error, and hint behavior.
702
+
703
+ **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.
397
704
 
398
705
  | Prop | Type | Default | Notes |
399
706
  |------|------|---------|-------|
400
- | value | `string` | — | Controlled display value (includes prefix, e.g. `'$1,234'`) |
401
- | onChangeText | `(formatted: string) => void` | — | Called with the formatted display string |
707
+ | value | `string` | — | Controlled display value including prefix (e.g. `'$1.234'`) |
708
+ | onChangeText | `(formatted: string) => void` | — | Called with the formatted display string on each keystroke |
402
709
  | onChangeValue | `(raw: number) => void` | — | Called with the parsed number (no separators, no prefix) |
403
- | prefix | `string` | `'$'` | Symbol prepended to the formatted value |
404
- | thousandsSeparator | `'.' \| ','` | `'.'` | Character used to separate groups of three digits |
405
- | size | `'default' \| 'large'` | `'default'` | `'large'` uses 36px font size (previously `CurrencyInputLarge`) |
406
- | label | `string` | — | Label above the input |
407
- | error | `string` | — | Error text below; turns border red |
710
+ | prefix | `string` | `'$'` | Symbol prepended to formatted value |
711
+ | thousandsSeparator | `'.' \| ','` | `'.'` | Character separating groups of three digits |
712
+ | size | `'default' \| 'large'` | `'default'` | `'large'` uses 36px font for prominent payment screens |
713
+ | label | `string` | — | Label above input |
714
+ | error | `string` | — | Error text below |
408
715
  | hint | `string` | — | Helper text below (hidden when `error` is set) |
409
716
  | placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
410
- | editable | `boolean` | — | Pass `false` to disable editing |
411
- | containerStyle | `ViewStyle` | — | Style for the outer container |
717
+ | editable | `boolean` | — | Pass `false` to disable |
718
+ | containerStyle | `ViewStyle` | — | Outer container style |
719
+
720
+ **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.
412
721
 
413
- **Example:**
722
+ **Examples:**
414
723
  ```tsx
415
724
  const [display, setDisplay] = useState('')
416
725
  const [amount, setAmount] = useState(0)
417
726
 
418
- // Default: dot as thousands separator → "$1.234.567"
727
+ // Standard with dot separator ($1.234.567)
419
728
  <CurrencyInput
420
729
  label="Amount"
421
730
  value={display}
@@ -424,7 +733,7 @@ const [amount, setAmount] = useState(0)
424
733
  hint={`Parsed: ${amount}`}
425
734
  />
426
735
 
427
- // Comma as thousands separator → "$1,234,567"
736
+ // Comma separator ($1,234,567)
428
737
  <CurrencyInput
429
738
  prefix="$"
430
739
  thousandsSeparator=","
@@ -432,27 +741,67 @@ const [amount, setAmount] = useState(0)
432
741
  onChangeText={setDisplay}
433
742
  onChangeValue={setAmount}
434
743
  />
744
+
745
+ // Large variant for payment screens
746
+ <CurrencyInput
747
+ size="large"
748
+ label="Transfer amount"
749
+ value={display}
750
+ onChangeText={setDisplay}
751
+ onChangeValue={setAmount}
752
+ />
753
+
754
+ // Euro with error
755
+ <CurrencyInput
756
+ prefix="€"
757
+ label="Price"
758
+ value={display}
759
+ onChangeText={setDisplay}
760
+ onChangeValue={setAmount}
761
+ error={amount < 1000 ? "Minimum amount is €1.000" : undefined}
762
+ />
435
763
  ```
436
764
 
437
765
  ---
438
766
 
439
- ### Textarea
767
+ ### CurrencyDisplay
440
768
 
441
- **Import:** `import { Textarea } from '@retray-dev/ui-kit'`
442
- **When to use:** Multi-line text entry. Same API as `Input` plus `rows`.
443
- **Extends:** `TextInputProps` from React Nativeall native props pass through.
769
+ **Import:** `import { CurrencyDisplay } from '@retray-dev/ui-kit'`
770
+
771
+ **When to use:** Prominent read-only display of monetary values account balances, totals, summary screens. Uses `display-hero` typography (56pt bold) for visual impact.
444
772
 
445
773
  | Prop | Type | Default | Notes |
446
774
  |------|------|---------|-------|
447
- | label | `string` | | Label above |
448
- | error | `string` | | Error text below; red border |
449
- | hint | `string` | | Helper text below |
450
- | rows | `number` | `4` | Sets `minHeight` (each row 30px) and `numberOfLines` |
451
- | containerStyle | `ViewStyle` | — | Style for the outer container `View` |
775
+ | value | `number \| string` | required | Numeric value to display |
776
+ | prefix | `string` | `'$'` | Symbol prepended to formatted value |
777
+ | showDecimals | `boolean` | `false` | Show two decimal places with comma separator (e.g. `$25.000,00`) |
778
+ | textColor | `string` | | Override text color. Defaults to `foreground` token |
779
+ | style | `ViewStyle` | — | Outer container style |
780
+
781
+ **Format:** Dot (`.`) as thousands separator, comma (`,`) as decimal separator — Latin American / European format.
452
782
 
453
- **Example:**
783
+ **Examples:**
454
784
  ```tsx
455
- <Textarea label="Bio" placeholder="Tell us about yourself" rows={5} />
785
+ <CurrencyDisplay value={25000} />
786
+ // → $25.000
787
+
788
+ <CurrencyDisplay value={25000000.5} showDecimals />
789
+ // → $25.000.000,50
790
+
791
+ <CurrencyDisplay value={1500} prefix="€" />
792
+ // → €1.500
793
+
794
+ <CurrencyDisplay value={balance} textColor={balance < 0 ? colors.destructive : colors.foreground} />
795
+ ```
796
+
797
+ **Composition — balance card:**
798
+ ```tsx
799
+ <Card>
800
+ <CardContent>
801
+ <Text variant="caption-sm" color={colors.foregroundMuted}>Available balance</Text>
802
+ <CurrencyDisplay value={12500} />
803
+ </CardContent>
804
+ </Card>
456
805
  ```
457
806
 
458
807
  ---
@@ -460,27 +809,59 @@ const [amount, setAmount] = useState(0)
460
809
  ### Badge
461
810
 
462
811
  **Import:** `import { Badge } from '@retray-dev/ui-kit'`
463
- **When to use:** Status labels, tags, counts.
812
+
813
+ **When to use:** Status labels, counts, category tags, feature flags. Pill-shaped with optional icon.
464
814
 
465
815
  | Prop | Type | Default | Notes |
466
816
  |------|------|---------|-------|
467
- | label | `string` | — | Badge text can be omitted if using `children` |
817
+ | label | `string` | — | Badge text. Can be omitted when using `children` |
468
818
  | children | `ReactNode` | — | Alternative to `label` for custom content |
469
- | variant | `'default' \| 'secondary' \| 'destructive' \| 'outline'` | `'default'` | |
819
+ | variant | `BadgeVariant` | `'default'` | Visual style |
470
820
  | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls padding and font size |
471
- | icon | `ReactNode` | — | Icon rendered before the label/children |
821
+ | icon | `ReactNode` | — | Icon rendered before label |
472
822
  | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
473
823
  | iconColor | `string` | — | Override icon color. Defaults to variant foreground color |
474
824
  | style | `ViewStyle` | — | — |
475
825
 
476
- **Example:**
826
+ **Variants:**
827
+
828
+ | Variant | Background | Text | Border | When to use |
829
+ |---------|-----------|------|--------|-------------|
830
+ | `default` | `primary` | `primaryForeground` | none | Primary status — active, featured |
831
+ | `secondary` | `surface` | `foregroundSubtle` | `border` | Neutral — draft, inactive, secondary status |
832
+ | `destructive` | `destructive` | `destructiveForeground` | none | Error, failed, danger |
833
+ | `outline` | transparent | `foreground` | `border` | Subtle tag, no emphasis |
834
+ | `success` | `success` | `successForeground` | none | Completed, verified, paid |
835
+ | `warning` | `warning` | `warningForeground` | none | Pending, caution, expiring |
836
+ | `successOutline` | `successTint` | `success` | `successBorder` | Soft success (less visual weight) |
837
+ | `destructiveOutline` | `destructiveTint` | `destructive` | `destructiveBorder` | Soft error (less visual weight) |
838
+ | `warningOutline` | `warningTint` | `warning` | `warningBorder` | Soft warning (less visual weight) |
839
+
840
+ **Sizes:**
841
+
842
+ | Size | Padding (V×H) | Font size | Icon size |
843
+ |------|--------------|-----------|-----------|
844
+ | `sm` | 2 × 8 | 11pt | 10px |
845
+ | `md` | 4 × 10 | 13pt | 12px |
846
+ | `lg` | 6 × 12 | 15pt | 14px |
847
+
848
+ **All badges have `borderRadius: 9999` (pill-shaped).**
849
+
850
+ **Examples:**
477
851
  ```tsx
478
852
  <Badge label="New" />
479
853
  <Badge label="Error" variant="destructive" />
480
854
  <Badge label="Draft" variant="secondary" size="sm" />
481
855
  <Badge label="Beta" variant="outline" />
482
- <Badge label="Active" variant="default" iconName="check" size="sm" />
483
- <Badge label="Error" variant="destructive" iconName="alert-circle" />
856
+ <Badge label="Paid" variant="success" iconName="check" />
857
+ <Badge label="Pending" variant="warningOutline" />
858
+ <Badge label="3" variant="destructive" size="sm" />
859
+
860
+ // With status in ListItem
861
+ <ListItem
862
+ title="Payment"
863
+ rightRender={<Badge label="Pending" variant="warningOutline" size="sm" />}
864
+ />
484
865
  ```
485
866
 
486
867
  ---
@@ -488,245 +869,452 @@ const [amount, setAmount] = useState(0)
488
869
  ### Avatar
489
870
 
490
871
  **Import:** `import { Avatar } from '@retray-dev/ui-kit'`
491
- **When to use:** User profile pictures with automatic fallback to initials.
872
+
873
+ **When to use:** User profile pictures with automatic fallback to initials. Works with remote URIs and local images.
492
874
 
493
875
  | Prop | Type | Default | Notes |
494
876
  |------|------|---------|-------|
495
- | src | `string` | — | Image URI |
496
- | fallback | `string` | — | Text shown when image fails or `src` is absent — first 2 chars, uppercased |
497
- | size | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | sm=24, md=32, lg=48, xl=64 (diameter in pt) |
877
+ | src | `string` | — | Image URI (remote or local) |
878
+ | fallback | `string` | — | Text shown when image fails or `src` is absent — first 2 characters shown, uppercased |
879
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Diameter in points |
880
+ | status | `'online' \| 'offline' \| 'busy' \| 'away'` | — | Status indicator dot, bottom-right |
498
881
  | style | `ViewStyle` | — | — |
499
882
 
500
- **Example:**
883
+ **Sizes:**
884
+
885
+ | Size | Diameter | Fallback font | Status dot size |
886
+ |------|---------|--------------|----------------|
887
+ | `sm` | 28px | 12pt | 8px |
888
+ | `md` | 40px | 16pt | 10px |
889
+ | `lg` | 56px | 22pt | 13px |
890
+ | `xl` | 72px | 28pt | 16px |
891
+
892
+ **Status dot colors:**
893
+ - `online` → `#22c55e` (green)
894
+ - `offline` → transparent fill, `border` color outline only
895
+ - `busy` → `destructive` token
896
+ - `away` → `warning` token
897
+
898
+ **Status dot is absolutely positioned at bottom-right with a 2px white border.**
899
+
900
+ **Examples:**
501
901
  ```tsx
502
902
  <Avatar src="https://..." fallback="JC" size="lg" />
503
903
  <Avatar fallback="AN" size="md" />
904
+ <Avatar src={user.avatar} fallback={user.initials} size="xl" status="online" />
905
+ <Avatar fallback="BU" size="sm" status="busy" />
906
+
907
+ // In a ListItem
908
+ <ListItem
909
+ leftRender={<Avatar src={user.avatar} fallback={user.initials} status="online" />}
910
+ title={user.name}
911
+ subtitle={user.role}
912
+ />
504
913
  ```
505
914
 
506
915
  ---
507
916
 
508
- ### Separator
917
+ ### Card
509
918
 
510
- **Import:** `import { Separator } from '@retray-dev/ui-kit'`
511
- **When to use:** Visual dividers between sections.
919
+ **Import:** `import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@retray-dev/ui-kit'`
920
+
921
+ **When to use:** Grouped content with a surface background. Use `Card` as the container and sub-components for structured layout.
922
+
923
+ **`Card` Props:**
512
924
 
513
925
  | Prop | Type | Default | Notes |
514
926
  |------|------|---------|-------|
515
- | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | Vertical requires a parent with a defined height |
927
+ | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | Visual style |
928
+ | onPress | `() => void` | — | Makes card pressable with scale animation + haptics |
929
+ | children | `ReactNode` | required | — |
516
930
  | style | `ViewStyle` | — | — |
517
931
 
518
- **Example:**
932
+ **Variants:**
933
+
934
+ | Variant | Background | Border | Shadow | When to use |
935
+ |---------|-----------|--------|--------|-------------|
936
+ | `elevated` | `card` | `border` (1px) | `SHADOWS.sm` | Default card — standard content groups |
937
+ | `outlined` | `card` | `border` (1px) | none | Flat bordered cards, lists of cards |
938
+ | `filled` | `surfaceStrong` | none | none | Background fill cards, stat blocks, sidebar items |
939
+
940
+ **Sub-components (all accept `style` prop):**
941
+
942
+ | Sub-component | Padding | Notes |
943
+ |--------------|---------|-------|
944
+ | `CardHeader` | 16px all, 0 bottom | Use for title + description |
945
+ | `CardTitle` | — | 18pt / SemiBold / `cardForeground` |
946
+ | `CardDescription` | — | 14pt / Regular / `foregroundMuted` |
947
+ | `CardContent` | 16px all (12px top) | Main content area |
948
+ | `CardFooter` | 16px horizontal, 14px bottom | Row layout, use for buttons |
949
+
950
+ **Shape:** `borderRadius: RADIUS.md = 14px`
951
+
952
+ **Press animation:** Scale springs to 0.98 (subtler than button — appropriate for large targets).
953
+
954
+ **Haptics:** `impactLight` when `onPress` is provided.
955
+
956
+ **Examples:**
519
957
  ```tsx
520
- <Separator />
521
- <View style={{ flexDirection: 'row', height: 40 }}>
522
- <Text>Left</Text>
523
- <Separator orientation="vertical" style={{ marginHorizontal: 12 }} />
524
- <Text>Right</Text>
958
+ // Standard card
959
+ <Card>
960
+ <CardHeader>
961
+ <CardTitle>Account settings</CardTitle>
962
+ <CardDescription>Manage your profile and preferences</CardDescription>
963
+ </CardHeader>
964
+ <CardContent>
965
+ <Input label="Display name" value={name} onChangeText={setName} />
966
+ </CardContent>
967
+ <CardFooter>
968
+ <Button label="Save" fullWidth />
969
+ </CardFooter>
970
+ </Card>
971
+
972
+ // Pressable navigation card
973
+ <Card variant="outlined" onPress={() => navigate('profile')}>
974
+ <CardContent>
975
+ <Text variant="title-md">Profile</Text>
976
+ <Text variant="body-sm">Edit your information</Text>
977
+ </CardContent>
978
+ </Card>
979
+
980
+ // Stat block (filled)
981
+ <Card variant="filled">
982
+ <CardContent>
983
+ <Text variant="caption-sm" color={colors.foregroundMuted}>Total spent</Text>
984
+ <CurrencyDisplay value={totalSpent} />
985
+ </CardContent>
986
+ </Card>
987
+ ```
988
+
989
+ **Composition — dashboard grid:**
990
+ ```tsx
991
+ <View style={{ flexDirection: 'row', gap: SPACING.md }}>
992
+ <Card variant="filled" style={{ flex: 1 }}>
993
+ <CardContent>
994
+ <Text variant="micro-label" color={colors.foregroundMuted}>INCOME</Text>
995
+ <Text variant="display-md" color={colors.success}>$8.500</Text>
996
+ </CardContent>
997
+ </Card>
998
+ <Card variant="filled" style={{ flex: 1 }}>
999
+ <CardContent>
1000
+ <Text variant="micro-label" color={colors.foregroundMuted}>EXPENSES</Text>
1001
+ <Text variant="display-md" color={colors.destructive}>$3.200</Text>
1002
+ </CardContent>
1003
+ </Card>
525
1004
  </View>
526
1005
  ```
527
1006
 
528
1007
  ---
529
1008
 
530
- ### Spinner
1009
+ ### MediaCard
531
1010
 
532
- **Import:** `import { Spinner } from '@retray-dev/ui-kit'`
533
- **When to use:** Loading state indicator. Wraps React Native's `ActivityIndicator`.
534
- **Extends:** `ActivityIndicatorProps` (except `size`).
1011
+ **Import:** `import { MediaCard } from '@retray-dev/ui-kit'`
1012
+
1013
+ **When to use:** Image-first content cards — product listings, properties, experiences, photo-based content. Think Airbnb listing card or marketplace item.
535
1014
 
536
1015
  | Prop | Type | Default | Notes |
537
1016
  |------|------|---------|-------|
538
- | size | `'sm' \| 'md' \| 'lg'` | `'md'` | `sm`/`md` map to RN `'small'`, `lg` maps to `'large'` |
539
- | color | `string` | `primary` token | Override spinner color |
540
- | label | `string` | — | Text displayed below the spinner |
1017
+ | imageSource | `ImageSourcePropType` | | Image `{ uri: '...' }` or `require('./img.jpg')` |
1018
+ | aspectRatio | `MediaCardAspectRatio` | `'4:3'` | Controls image height relative to card width |
1019
+ | badge | `ReactNode` | — | Floating content top-left of image |
1020
+ | actionIcon | `ReactNode` | — | Icon top-right of image (defaults to heart outline) |
1021
+ | actionIconName | `string` | — | Icon name. Overrides `actionIcon` |
1022
+ | actionActive | `boolean` | `false` | Whether action is in active/filled state |
1023
+ | onActionPress | `() => void` | — | Press handler for action icon. Stops event propagation |
1024
+ | title | `string` | — | Below image — primary label |
1025
+ | subtitle | `string` | — | Secondary line below title |
1026
+ | caption | `string` | — | Tertiary metadata line |
1027
+ | footer | `ReactNode` | — | Custom content below caption (price, rating, etc.) |
1028
+ | onPress | `() => void` | — | Press the card body |
1029
+ | style | `ViewStyle` | — | Outer card style |
1030
+ | imageStyle | `ViewStyle` | — | Image container override |
541
1031
 
542
- **Example:**
543
- ```tsx
544
- <Spinner />
545
- <Spinner size="lg" color="#6366f1" />
546
- <Spinner size="lg" label="Loading..." />
547
- ```
1032
+ **Aspect ratios (`MediaCardAspectRatio`):**
548
1033
 
549
- ---
1034
+ | Value | Width:Height | When to use |
1035
+ |-------|-------------|-------------|
1036
+ | `'1:1'` | Square | Profile cards, avatars, square products |
1037
+ | `'4:3'` | Classic | Default — general listings |
1038
+ | `'16:9'` | Wide | Videos, wide landscape imagery |
1039
+ | `'4:5'` | Portrait | Tall product photos, fashion |
1040
+ | `'3:2'` | Photo | Standard photography ratio |
550
1041
 
551
- ### Skeleton
1042
+ **Image implementation:** Uses `paddingTop` percentage for cross-platform consistent aspect ratios with `position: absolute` image fill — no fixed height needed.
552
1043
 
553
- **Import:** `import { Skeleton } from '@retray-dev/ui-kit'`
554
- **When to use:** Placeholder while content is loading. Pulses with a looping opacity animation.
1044
+ **Shape:** `borderRadius: RADIUS.md = 14px`, `overflow: hidden`.
555
1045
 
556
- | Prop | Type | Default | Notes |
557
- |------|------|---------|-------|
558
- | width | `number \| string` | `'100%'` | — |
559
- | height | `number` | `16` | — |
560
- | borderRadius | `number` | `6` | — |
561
- | style | `ViewStyle` | — | — |
1046
+ **Action button:** 32×32px circle, semi-transparent background (24% opacity), top-right corner 8px from edges.
562
1047
 
563
- **Example:**
564
- ```tsx
565
- <Skeleton height={20} width="60%" />
566
- <Skeleton height={80} borderRadius={10} />
567
- ```
1048
+ **Badge:** Positioned top-left 8px from edges — pass any ReactNode (Badge component recommended).
568
1049
 
569
- ---
1050
+ **Web hover:** Lifts shadow from `SHADOWS.sm` to `SHADOWS.md` on hover.
570
1051
 
571
- ### Progress
1052
+ **Press animation:** Scale springs to 0.98.
572
1053
 
573
- **Import:** `import { Progress } from '@retray-dev/ui-kit'`
574
- **When to use:** Show completion percentage for a task or upload.
1054
+ **Haptics:** `impactLight` on `onActionPress`.
575
1055
 
576
- | Prop | Type | Default | Notes |
577
- |------|------|---------|-------|
578
- | value | `number` | `0` | Current value |
579
- | max | `number` | `100` | Maximum value |
580
- | style | `ViewStyle` | — | — |
1056
+ **Examples:**
1057
+ ```tsx
1058
+ // Basic listing card
1059
+ <MediaCard
1060
+ imageSource={{ uri: listing.photo }}
1061
+ title={listing.name}
1062
+ subtitle={listing.location}
1063
+ caption={`$${listing.price} / night`}
1064
+ onPress={() => navigate('listing', { id: listing.id })}
1065
+ />
1066
+
1067
+ // With save action and aspect ratio
1068
+ <MediaCard
1069
+ imageSource={{ uri: product.image }}
1070
+ aspectRatio="4:5"
1071
+ title={product.name}
1072
+ subtitle={product.brand}
1073
+ actionActive={saved}
1074
+ onActionPress={() => toggleSave(product.id)}
1075
+ onPress={() => navigate('product', { id: product.id })}
1076
+ />
1077
+
1078
+ // With badge overlay
1079
+ <MediaCard
1080
+ imageSource={{ uri: item.photo }}
1081
+ badge={<Badge label="New" size="sm" />}
1082
+ title={item.name}
1083
+ onPress={() => {}}
1084
+ />
581
1085
 
582
- **Animation:** Spring-animates the fill width on `value` changes (JS thread — `useNativeDriver: false`). Uses `onLayout` to capture track pixel width and interpolates to pixels (cannot animate `width: '%'`).
1086
+ // With custom footer (price + rating)
1087
+ <MediaCard
1088
+ imageSource={{ uri: place.photo }}
1089
+ aspectRatio="4:3"
1090
+ title={place.name}
1091
+ footer={
1092
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
1093
+ <Text variant="title-sm">$120 / night</Text>
1094
+ <Text variant="caption-sm">★ 4.8</Text>
1095
+ </View>
1096
+ }
1097
+ onPress={() => navigate('place', { id: place.id })}
1098
+ />
1099
+ ```
583
1100
 
584
- **Example:**
1101
+ **Composition — listing grid:**
585
1102
  ```tsx
586
- <Progress value={60} />
587
- <Progress value={3} max={10} />
1103
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.md }}>
1104
+ {listings.map((item) => (
1105
+ <MediaCard
1106
+ key={item.id}
1107
+ style={{ width: (screenWidth - SPACING.base * 2 - SPACING.md) / 2 }}
1108
+ imageSource={{ uri: item.photo }}
1109
+ title={item.name}
1110
+ caption={item.price}
1111
+ actionActive={savedIds.includes(item.id)}
1112
+ onActionPress={() => toggleSave(item.id)}
1113
+ onPress={() => navigate('listing', { id: item.id })}
1114
+ />
1115
+ ))}
1116
+ </View>
588
1117
  ```
589
1118
 
590
1119
  ---
591
1120
 
592
- ### CurrencyDisplay
1121
+ ### Separator
593
1122
 
594
- **Import:** `import { CurrencyDisplay } from '@retray-dev/ui-kit'`
595
- **When to use:** Prominent display of monetary values (account balances, totals). Large, attention-grabbing typographic treatment with dot-separated thousands and optional comma-decimal.
1123
+ **Import:** `import { Separator } from '@retray-dev/ui-kit'`
1124
+
1125
+ **When to use:** Visual divider between sections, list items, or columns.
596
1126
 
597
1127
  | Prop | Type | Default | Notes |
598
1128
  |------|------|---------|-------|
599
- | value | `number \| string` | required | Numeric value to display |
600
- | prefix | `string` | `'$'` | Symbol prepended to the formatted value |
601
- | showDecimals | `boolean` | `false` | When `true`, shows two decimal places separated by a comma (e.g. `$25.000,00`) |
602
- | textColor | `string` | — | Override the color of the formatted text. Defaults to the `foreground` token |
603
- | style | `ViewStyle` | — | Style override for outer container |
1129
+ | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction of the line |
1130
+ | style | `ViewStyle` | | |
1131
+
1132
+ **Styling:** 1px thickness, `border` token color.
604
1133
 
605
- **Example:**
1134
+ **Examples:**
606
1135
  ```tsx
607
- <CurrencyDisplay value={25000} />
608
- // → $25.000
1136
+ // Section divider
1137
+ <Separator />
609
1138
 
610
- <CurrencyDisplay value={25000000.5} showDecimals />
611
- // $25.000.000,50
1139
+ // Vertical divider (parent must have defined height)
1140
+ <View style={{ flexDirection: 'row', height: 40, alignItems: 'center' }}>
1141
+ <Text>Left</Text>
1142
+ <Separator orientation="vertical" style={{ marginHorizontal: 12 }} />
1143
+ <Text>Right</Text>
1144
+ </View>
612
1145
 
613
- <CurrencyDisplay value={1500} prefix="€" />
614
- // €1.500
1146
+ // With custom spacing
1147
+ <Separator style={{ marginVertical: SPACING.lg }} />
615
1148
  ```
616
1149
 
617
- **Notes:**
618
- - Uses dot (`.`) as thousands separator and comma (`,`) as decimal separator — Latin American / European format.
619
- - Intended as a display-only component (not editable).
620
- - `allowFontScaling={true}` is set on the inner `Text`.
621
-
622
1150
  ---
623
1151
 
624
- ### CurrencyInputLarge
1152
+ ### Spinner
1153
+
1154
+ **Import:** `import { Spinner } from '@retray-dev/ui-kit'`
625
1155
 
626
- **Import:** `import { CurrencyInputLarge } from '@retray-dev/ui-kit'`
627
- **When to use:** Large, form-style monetary input for prominent entry screens (payments, transfers). Same formatting behavior as `CurrencyInput` but with 36px font size.
1156
+ **When to use:** Loading state indicator. Use inline (within buttons or content areas) or full-screen.
1157
+
1158
+ **Extends:** `ActivityIndicatorProps` (except `size` — use the string size prop instead)
628
1159
 
629
1160
  | Prop | Type | Default | Notes |
630
1161
  |------|------|---------|-------|
631
- | value | `string` | | Controlled display value (includes prefix, e.g. `'$1.234'`) |
632
- | onChangeText | `(formatted: string) => void` | | Called with the formatted display string |
633
- | onChangeValue | `(raw: number) => void` | — | Called with the parsed number (no separators, no prefix) |
634
- | label | `string` | — | Label above the input |
635
- | prefix | `string` | `'$'` | Currency prefix |
636
- | thousandsSeparator | `'.' \| ','` | `'.'` | Separator used to format groups of three digits |
637
- | error | `string` | — | Error text below; turns border red |
638
- | hint | `string` | — | Helper text below (hidden when `error` is set) |
639
- | placeholder | `string` | `'$0'` | Defaults to `prefix + '0'` |
640
- | editable | `boolean` | — | Pass `false` to disable editing |
641
- | containerStyle | `ViewStyle` | — | Style for the outer container |
1162
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | `sm`/`md` RN `'small'`, `lg` RN `'large'` |
1163
+ | color | `string` | `primary` token | Override spinner color |
1164
+ | label | `string` | — | Text displayed below spinner |
642
1165
 
643
- **Example:**
1166
+ **Examples:**
644
1167
  ```tsx
645
- const [display, setDisplay] = useState('')
646
- const [amount, setAmount] = useState(0)
1168
+ <Spinner />
1169
+ <Spinner size="lg" />
1170
+ <Spinner size="lg" label="Loading..." />
1171
+ <Spinner color={colors.foregroundMuted} />
647
1172
 
648
- <CurrencyInputLarge
649
- label="Amount"
650
- value={display}
651
- onChangeText={setDisplay}
652
- onChangeValue={setAmount}
653
- />
654
- // Typing "25000" produces "$25.000"
1173
+ // Full-screen loading overlay
1174
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
1175
+ <Spinner size="lg" label="Fetching data..." />
1176
+ </View>
655
1177
  ```
656
1178
 
657
1179
  ---
658
1180
 
659
- ### Card
1181
+ ### Skeleton
660
1182
 
661
- **Import:** `import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@retray-dev/ui-kit'`
662
- **When to use:** Grouped content with a surface background, border, and shadow.
1183
+ **Import:** `import { Skeleton } from '@retray-dev/ui-kit'`
663
1184
 
664
- **`Card` Props:**
1185
+ **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.
665
1186
 
666
1187
  | Prop | Type | Default | Notes |
667
1188
  |------|------|---------|-------|
668
- | variant | `'elevated' \| 'outlined' \| 'filled'` | `'elevated'` | `elevated`: shadow + border; `outlined`: border only; `filled`: background fill, no border |
669
- | onPress | `() => void` | | Makes the card pressable with scale animation + haptics |
670
- | children | `ReactNode` | | |
1189
+ | width | `number \| string` | `'100%'` | Width of placeholder |
1190
+ | height | `number` | `16` | Height of placeholder |
1191
+ | borderRadius | `number` | `6` | Corner radius |
1192
+ | preset | `'base' \| 'circle' \| 'text'` | `'base'` | Convenience preset |
1193
+ | diameter | `number` | `40` | Used by `'circle'` preset — overrides width/height |
671
1194
  | style | `ViewStyle` | — | — |
672
1195
 
673
- All sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`) accept a `style` prop for overrides.
1196
+ **Presets:**
1197
+ - `'base'` — Uses `width`, `height`, `borderRadius` exactly as specified
1198
+ - `'circle'` — Forces square dimensions from `diameter`, `borderRadius: 9999`
1199
+ - `'text'` — 60% width, 14px height, 4px border radius (typical text line shape)
674
1200
 
675
- **Example:**
1201
+ **Animation:** Shimmer sweep loops every 1200ms. Highlight color adapts to light/dark mode.
1202
+
1203
+ **Examples:**
676
1204
  ```tsx
677
- <Card variant="elevated">
678
- <CardHeader>
679
- <CardTitle>Account</CardTitle>
680
- <CardDescription>Manage your profile settings</CardDescription>
681
- </CardHeader>
682
- <CardContent>
683
- <Input label="Name" />
684
- </CardContent>
685
- <CardFooter>
686
- <Button label="Save" fullWidth />
687
- </CardFooter>
688
- </Card>
1205
+ // Text line placeholders
1206
+ <Skeleton height={20} width="80%" />
1207
+ <Skeleton height={16} width="60%" style={{ marginTop: 8 }} />
1208
+
1209
+ // Circle avatar placeholder
1210
+ <Skeleton preset="circle" diameter={40} />
1211
+
1212
+ // Text preset (auto-sized)
1213
+ <Skeleton preset="text" />
1214
+
1215
+ // Custom card skeleton
1216
+ <View style={{ gap: SPACING.sm }}>
1217
+ <Skeleton height={180} borderRadius={14} /> {/* image */}
1218
+ <Skeleton height={18} width="70%" /> {/* title */}
1219
+ <Skeleton preset="text" /> {/* subtitle */}
1220
+ <Skeleton height={14} width="40%" /> {/* caption */}
1221
+ </View>
1222
+ ```
689
1223
 
690
- // Pressable card
691
- <Card variant="outlined" onPress={() => navigate('detail')}>
692
- <CardContent>
693
- <Text>Tap me</Text>
694
- </CardContent>
695
- </Card>
1224
+ **Composition skeleton for a ListItem:**
1225
+ ```tsx
1226
+ <View style={{ flexDirection: 'row', gap: SPACING.md, padding: SPACING.base }}>
1227
+ <Skeleton preset="circle" diameter={40} />
1228
+ <View style={{ flex: 1, gap: SPACING.xs }}>
1229
+ <Skeleton height={16} width="60%" />
1230
+ <Skeleton preset="text" />
1231
+ </View>
1232
+ </View>
696
1233
  ```
697
1234
 
698
- **Notes:**
699
- - `CardHeader` and `CardContent` have `padding: 24`. Override via the `style` prop: `<CardContent style={{ padding: 16 }}>`
700
- - `CardFooter` has `paddingTop: 0` so it connects naturally to `CardContent`
701
- - `CardTitle`: 20px / 600 weight / lineHeight 28, `cardForeground` color
702
- - `CardDescription`: 15px / lineHeight 22, `mutedForeground` color
703
- - Press animation scales to 0.98 via `Animated.spring` when `onPress` is provided
1235
+ ---
1236
+
1237
+ ### Progress
1238
+
1239
+ **Import:** `import { Progress } from '@retray-dev/ui-kit'`
1240
+
1241
+ **When to use:** Show completion percentage for tasks, uploads, onboarding steps, multi-step flows.
1242
+
1243
+ | Prop | Type | Default | Notes |
1244
+ |------|------|---------|-------|
1245
+ | value | `number` | `0` | Current progress value |
1246
+ | max | `number` | `100` | Maximum value — `value/max` determines fill percentage |
1247
+ | variant | `'default' \| 'success' \| 'warning' \| 'destructive'` | `'default'` | Indicator color |
1248
+ | style | `ViewStyle` | — | Outer container style |
1249
+
1250
+ **Variants — indicator color:**
1251
+ - `default` → `primary` token
1252
+ - `success` → `success` token
1253
+ - `warning` → `warning` token
1254
+ - `destructive` → `destructive` token
1255
+
1256
+ **Styling:** 8px height, `borderRadius: 9999` (pill). Track uses `surface` background.
1257
+
1258
+ **Animation:** Spring-animates fill width on `value` changes (JS thread). Uses `onLayout` to capture pixel width.
1259
+
1260
+ **Examples:**
1261
+ ```tsx
1262
+ <Progress value={60} />
1263
+ <Progress value={3} max={10} />
1264
+ <Progress value={uploadProgress} variant="success" />
1265
+ <Progress value={daysRemaining} max={30} variant="warning" />
1266
+ <Progress value={errors} max={100} variant="destructive" />
1267
+
1268
+ // Labeled progress
1269
+ <View style={{ gap: SPACING.xs }}>
1270
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
1271
+ <Text variant="caption">Profile completion</Text>
1272
+ <Text variant="caption">{progress}%</Text>
1273
+ </View>
1274
+ <Progress value={progress} />
1275
+ </View>
1276
+ ```
704
1277
 
705
1278
  ---
706
1279
 
707
1280
  ### AlertBanner
708
1281
 
709
1282
  **Import:** `import { AlertBanner } from '@retray-dev/ui-kit'`
710
- **When to use:** Inline feedback messages (info, success, error). Not for transient toasts — use `Toast` for ephemeral feedback.
1283
+
1284
+ **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.
711
1285
 
712
1286
  > **Note:** Named `AlertBanner` (not `Alert`) to avoid collision with React Native's built-in `Alert` module.
713
1287
 
714
1288
  | Prop | Type | Default | Notes |
715
1289
  |------|------|---------|-------|
716
1290
  | title | `string` | required | Bold heading |
717
- | description | `string` | — | Optional detail text below the title |
718
- | variant | `'default' \| 'destructive' \| 'success'` | `'default'` | Controls border/text color |
719
- | icon | `ReactNode` | — | Icon placed to the left of the text content. Defaults to a variant-appropriate symbol (`ℹ`, `⚠`, `✓`) |
720
- | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` (but still falls back to the variant default when neither is set) |
721
- | iconColor | `string` | — | Override icon color. Defaults to the variant title color |
1291
+ | description | `string` | — | Supporting detail text below title |
1292
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | Controls colors and default icon |
1293
+ | icon | `ReactNode` | — | Override default icon with custom ReactNode |
1294
+ | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon`, but still falls back to variant default when neither is set |
1295
+ | iconColor | `string` | — | Override icon color. Defaults to variant title color |
722
1296
  | style | `ViewStyle` | — | — |
723
1297
 
724
- **Example:**
1298
+ **Variants:**
1299
+
1300
+ | Variant | Background | Border | Icon | When to use |
1301
+ |---------|-----------|--------|------|-------------|
1302
+ | `default` | `card` | `border` | ℹ️ info | Neutral notices, informational messages |
1303
+ | `destructive` | `destructiveTint` | `destructiveBorder` | ⚠️ error | Form errors, action failures |
1304
+ | `success` | `successTint` | `successBorder` | ✓ check | Confirmations, completed states |
1305
+ | `warning` | `warningTint` | `warningBorder` | ⚠️ warning | Cautions, expiring states, pending actions |
1306
+
1307
+ **Styling:** 12px border radius, 14px horizontal + 12px vertical padding, 1px border.
1308
+
1309
+ **Examples:**
725
1310
  ```tsx
726
- <AlertBanner title="Info" description="Your session will expire in 5 minutes." />
727
- <AlertBanner variant="destructive" title="Error" description="Failed to save. Try again." />
728
- <AlertBanner variant="success" title="Done" description="Your changes have been saved." />
729
- <AlertBanner iconName="bell" title="Reminder" description="Complete your profile." />
1311
+ <AlertBanner title="Your session expires in 5 minutes" />
1312
+ <AlertBanner variant="destructive" title="Payment failed" description="Please check your card details." />
1313
+ <AlertBanner variant="success" title="Profile updated" description="Your changes have been saved." />
1314
+ <AlertBanner variant="warning" title="ID verification required" description="Complete verification to unlock transfers." />
1315
+
1316
+ // Custom icon
1317
+ <AlertBanner iconName="bell" title="Reminder" description="Complete your profile to get started." />
730
1318
  ```
731
1319
 
732
1320
  ---
@@ -734,28 +1322,48 @@ All sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardContent`,
734
1322
  ### EmptyState
735
1323
 
736
1324
  **Import:** `import { EmptyState } from '@retray-dev/ui-kit'`
737
- **When to use:** When a list or section has no content yet.
1325
+
1326
+ **When to use:** When a list, section, or screen has no content. Provides a centered illustration/icon, message, and optional CTA.
738
1327
 
739
1328
  | Prop | Type | Default | Notes |
740
1329
  |------|------|---------|-------|
741
- | title | `string` | required | |
742
- | description | `string` | — | |
743
- | icon | `ReactNode` | — | Shown in a 48×48 muted square above the text |
744
- | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon`. Sized automatically (48 default, 32 compact) |
745
- | iconColor | `string` | — | Override icon color. Defaults to `mutedForeground` |
746
- | action | `ReactNode` | — | Usually a `Button`, placed below the text |
747
- | size | `'default' \| 'compact'` | `'default'` | `compact` hides description/action, uses a 36×36 icon, smaller title (15px), and tighter spacing |
1330
+ | title | `string` | required | Primary message |
1331
+ | description | `string` | — | Supporting text (hidden in `compact` size) |
1332
+ | icon | `ReactNode` | — | Icon in the top circle container |
1333
+ | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
1334
+ | iconColor | `string` | — | Override icon color. Defaults to `foregroundMuted` |
1335
+ | action | `ReactNode` | — | CTA below text — typically a `Button`. Hidden in `compact` size |
1336
+ | size | `'default' \| 'compact'` | `'default'` | `compact` is smaller, hides description and action |
748
1337
  | style | `ViewStyle` | — | — |
749
1338
 
750
- **Example:**
1339
+ **Sizes:**
1340
+
1341
+ | Size | Icon container | Title font | Description | Action |
1342
+ |------|---------------|-----------|-------------|--------|
1343
+ | `default` | 48×48px, 12px radius | 18pt / Medium | shown | shown |
1344
+ | `compact` | 36×36px, 8px radius | 15pt / Medium | hidden | hidden |
1345
+
1346
+ **Styling:** Centered layout, dashed border container, `surface` icon background, 32px padding.
1347
+
1348
+ **Examples:**
751
1349
  ```tsx
1350
+ // Full empty state with action
752
1351
  <EmptyState
1352
+ iconName="inbox"
753
1353
  title="No notifications"
754
- description="You're all caught up!"
755
- action={<Button label="Refresh" variant="outline" size="sm" />}
1354
+ description="You're all caught up! Check back later."
1355
+ action={<Button label="Refresh" variant="secondary" size="sm" onPress={refresh} />}
756
1356
  />
757
- <EmptyState iconName="inbox" title="No messages" description="You're all caught up." />
758
- <EmptyState iconName="search" title="No results" size="compact" />
1357
+
1358
+ // Search no results
1359
+ <EmptyState
1360
+ iconName="search"
1361
+ title="No results for &quot;coffee&quot;"
1362
+ description="Try a different search term."
1363
+ />
1364
+
1365
+ // Compact inline empty state
1366
+ <EmptyState iconName="file-text" title="No transactions" size="compact" />
759
1367
  ```
760
1368
 
761
1369
  ---
@@ -764,18 +1372,39 @@ All sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardContent`,
764
1372
 
765
1373
  **Import:** `import { Checkbox } from '@retray-dev/ui-kit'`
766
1374
 
1375
+ **When to use:** Binary boolean choices in forms — terms acceptance, feature toggles, multi-select item selection.
1376
+
767
1377
  | Prop | Type | Default | Notes |
768
1378
  |------|------|---------|-------|
769
- | checked | `boolean` | `false` | |
770
- | onCheckedChange | `(checked: boolean) => void` | — | |
771
- | label | `string` | — | Text to the right of the box |
772
- | disabled | `boolean` | — | |
1379
+ | checked | `boolean` | `false` | Controlled checked state |
1380
+ | onCheckedChange | `(checked: boolean) => void` | — | Called with new boolean on press |
1381
+ | label | `string` | — | Text to the right of the checkbox |
1382
+ | disabled | `boolean` | — | Reduces opacity to 0.5, disables interaction |
773
1383
  | style | `ViewStyle` | — | — |
774
1384
 
775
- **Example:**
1385
+ **Styling:** 24×24px box, 4px border radius, 1.5px border. Checkmark: 12×7px L-shape.
1386
+
1387
+ **Animation:** Scale spring on press (0.95).
1388
+
1389
+ **Haptics:** `selectionAsync` on change.
1390
+
1391
+ **Examples:**
776
1392
  ```tsx
777
1393
  const [accepted, setAccepted] = useState(false)
778
- <Checkbox checked={accepted} onCheckedChange={setAccepted} label="I agree to the terms" />
1394
+
1395
+ <Checkbox checked={accepted} onCheckedChange={setAccepted} label="I agree to the terms and conditions" />
1396
+ <Checkbox checked={rememberMe} onCheckedChange={setRememberMe} label="Remember me" />
1397
+ <Checkbox checked={true} onCheckedChange={() => {}} label="Can't uncheck this" disabled />
1398
+
1399
+ // Multi-select list
1400
+ {options.map((opt) => (
1401
+ <Checkbox
1402
+ key={opt.id}
1403
+ checked={selected.includes(opt.id)}
1404
+ onCheckedChange={(checked) => toggleSelected(opt.id, checked)}
1405
+ label={opt.label}
1406
+ />
1407
+ ))}
779
1408
  ```
780
1409
 
781
1410
  ---
@@ -783,22 +1412,47 @@ const [accepted, setAccepted] = useState(false)
783
1412
  ### Switch
784
1413
 
785
1414
  **Import:** `import { Switch } from '@retray-dev/ui-kit'`
786
- **When to use:** Binary on/off settings.
1415
+
1416
+ **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.
787
1417
 
788
1418
  | Prop | Type | Default | Notes |
789
1419
  |------|------|---------|-------|
790
- | checked | `boolean` | `false` | |
791
- | onCheckedChange | `(checked: boolean) => void` | — | |
792
- | disabled | `boolean` | — | Reduces opacity to 0.45 |
1420
+ | checked | `boolean` | `false` | Controlled state |
1421
+ | onCheckedChange | `(checked: boolean) => void` | — | Called with new state on press |
1422
+ | disabled | `boolean` | — | Reduces opacity to 0.45, disables interaction |
793
1423
  | style | `ViewStyle` | — | — |
794
1424
 
795
- **Dimensions:** Track 60×36pt, Thumb 28×28pt with 4pt offset from edges (`top: 4, left: 4`). Thumb uses `position: 'absolute'` inside the track.
1425
+ **Dimensions:** Track 52×30px (pill), Thumb 24×24px with 3px offset.
796
1426
 
797
- **Animation:** Thumb translates via spring (bounciness: 4); track color transitions via opacity timing (150ms).
1427
+ **Animation:** Thumb translates via spring; track color transitions via opacity (150ms).
798
1428
 
799
- **Example:**
1429
+ **Haptics:** `selectionAsync` on toggle.
1430
+
1431
+ **Examples:**
800
1432
  ```tsx
801
1433
  <Switch checked={notifications} onCheckedChange={setNotifications} />
1434
+
1435
+ // In a settings row
1436
+ <ListItem
1437
+ title="Push notifications"
1438
+ subtitle="Get alerts for new activity"
1439
+ rightRender={<Switch checked={push} onCheckedChange={setPush} />}
1440
+ />
1441
+
1442
+ // Full settings list
1443
+ {settings.map((setting) => (
1444
+ <ListItem
1445
+ key={setting.key}
1446
+ title={setting.label}
1447
+ rightRender={
1448
+ <Switch
1449
+ checked={prefs[setting.key]}
1450
+ onCheckedChange={(v) => updatePref(setting.key, v)}
1451
+ />
1452
+ }
1453
+ showSeparator
1454
+ />
1455
+ ))}
802
1456
  ```
803
1457
 
804
1458
  ---
@@ -806,36 +1460,64 @@ const [accepted, setAccepted] = useState(false)
806
1460
  ### Toggle
807
1461
 
808
1462
  **Import:** `import { Toggle } from '@retray-dev/ui-kit'`
809
- **When to use:** Toggleable button (e.g., bold/italic in a toolbar). Looks like a button, unlike `Switch`.
1463
+
1464
+ **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.
810
1465
 
811
1466
  | Prop | Type | Default | Notes |
812
1467
  |------|------|---------|-------|
813
- | pressed | `boolean` | `false` | |
814
- | onPressedChange | `(pressed: boolean) => void` | — | |
815
- | variant | `'default' \| 'outline'` | `'default'` | `outline` adds a border when unpressed |
816
- | size | `'sm' \| 'md' \| 'lg'` | `'md'` | sm=minH 40pt, md=minH 44pt, lg=minH 48pt |
1468
+ | pressed | `boolean` | `false` | Controlled pressed state |
1469
+ | onPressedChange | `(pressed: boolean) => void` | — | Called with new state on press |
1470
+ | variant | `'default' \| 'outline'` | `'default'` | `outline` adds border when unpressed |
1471
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls minimum height and padding |
817
1472
  | label | `string` | — | Text label |
818
- | icon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon shown when not pressed — can be combined with `label` |
819
- | activeIcon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon shown when pressed. If omitted, a default `✓` check mark is shown |
820
- | iconName | `string` | — | Icon name from `@expo/vector-icons` shown when not pressed. Takes precedence over `icon` |
821
- | activeIconName | `string` | — | Icon name shown when pressed. Takes precedence over `activeIcon` |
822
- | iconColor | `string` | — | Override inactive icon color. Defaults to `mutedForeground` |
823
- | activeIconColor | `string` | — | Override active icon color. Defaults to `primary` |
1473
+ | icon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon when not pressed |
1474
+ | activeIcon | `ReactNode \| ((pressed: boolean) => ReactNode)` | — | Icon when pressed. Defaults to check-circle |
1475
+ | iconName | `string` | — | Icon name when not pressed. Takes precedence over `icon` |
1476
+ | activeIconName | `string` | — | Icon name when pressed. Takes precedence over `activeIcon` |
1477
+ | iconColor | `string` | — | Inactive icon color. Defaults to `foregroundMuted` |
1478
+ | activeIconColor | `string` | — | Active icon color. Defaults to `primary` |
1479
+ | disabled | `boolean` | — | — |
1480
+ | style | `ViewStyle` | — | — |
1481
+
1482
+ **Sizes:**
1483
+
1484
+ | Size | Min height | Horizontal padding | Icon size |
1485
+ |------|-----------|-------------------|-----------|
1486
+ | `sm` | 40px | 12px | 16px |
1487
+ | `md` | 44px | 16px | 18px |
1488
+ | `lg` | 48px | 20px | 20px |
824
1489
 
825
- **Animation:** `borderColor` and `backgroundColor` animate via `Animated.timing` (150ms, `Easing.out`) on press state change. `borderWidth` stays fixed at 2pt to prevent layout jumps.
1490
+ **Animation:** `borderColor`, `backgroundColor`, `textColor` animate via `Animated.timing` (150ms) on state change. `borderWidth` stays fixed at 2pt to prevent layout shifts.
826
1491
 
827
- **Example:**
1492
+ **Haptics:** `selectionAsync` on press.
1493
+
1494
+ **Examples:**
828
1495
  ```tsx
829
1496
  <Toggle pressed={bold} onPressedChange={setBold} label="Bold" variant="outline" />
830
- <Toggle pressed={muted} onPressedChange={setMuted} iconName="volume-x" activeIconName="volume" />
831
1497
 
832
- // With custom active icon (ReactNode)
1498
+ // With icon names
1499
+ <Toggle
1500
+ pressed={muted}
1501
+ onPressedChange={setMuted}
1502
+ iconName="volume-x"
1503
+ activeIconName="volume"
1504
+ />
1505
+
1506
+ // Bookmark
833
1507
  <Toggle
834
- pressed={favorited}
835
- onPressedChange={setFavorited}
836
- icon={<HeartIcon size={18} />}
837
- activeIcon={<HeartIcon size={18} color={colors.primary} />}
1508
+ pressed={saved}
1509
+ onPressedChange={setSaved}
1510
+ iconName="bookmark"
1511
+ activeIconName="bookmark"
1512
+ activeIconColor={colors.primary}
838
1513
  />
1514
+
1515
+ // Toolbar row
1516
+ <View style={{ flexDirection: 'row', gap: SPACING.xs }}>
1517
+ <Toggle pressed={bold} onPressedChange={setBold} iconName="bold" variant="outline" size="sm" />
1518
+ <Toggle pressed={italic} onPressedChange={setItalic} iconName="italic" variant="outline" size="sm" />
1519
+ <Toggle pressed={underline} onPressedChange={setUnderline} iconName="underline" variant="outline" size="sm" />
1520
+ </View>
839
1521
  ```
840
1522
 
841
1523
  ---
@@ -844,15 +1526,28 @@ const [accepted, setAccepted] = useState(false)
844
1526
 
845
1527
  **Import:** `import { RadioGroup } from '@retray-dev/ui-kit'`
846
1528
 
1529
+ **When to use:** Single selection from a small set of options (2–6). When options are mutually exclusive and all should be visible simultaneously.
1530
+
847
1531
  | Prop | Type | Default | Notes |
848
1532
  |------|------|---------|-------|
849
- | options | `RadioOption[]` | required | Each option: `{ label: string, value: string, disabled?: boolean }` |
1533
+ | options | `RadioOption[]` | required | `{ label: string, value: string, disabled?: boolean }` |
850
1534
  | value | `string` | — | Currently selected value |
851
- | onValueChange | `(value: string) => void` | — | |
852
- | orientation | `'vertical' \| 'horizontal'` | `'vertical'` | |
1535
+ | onValueChange | `(value: string) => void` | — | Called on selection change |
1536
+ | orientation | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
853
1537
  | style | `ViewStyle` | — | — |
854
1538
 
855
- **Example:**
1539
+ **RadioOption type:**
1540
+ ```ts
1541
+ { label: string; value: string; disabled?: boolean }
1542
+ ```
1543
+
1544
+ **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).
1545
+
1546
+ **Animation:** Scale spring on press.
1547
+
1548
+ **Haptics:** `selectionAsync` on selection.
1549
+
1550
+ **Examples:**
856
1551
  ```tsx
857
1552
  <RadioGroup
858
1553
  options={[
@@ -863,6 +1558,14 @@ const [accepted, setAccepted] = useState(false)
863
1558
  value={plan}
864
1559
  onValueChange={setPlan}
865
1560
  />
1561
+
1562
+ // Horizontal orientation
1563
+ <RadioGroup
1564
+ options={[{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }]}
1565
+ value={answer}
1566
+ onValueChange={setAnswer}
1567
+ orientation="horizontal"
1568
+ />
866
1569
  ```
867
1570
 
868
1571
  ---
@@ -870,27 +1573,47 @@ const [accepted, setAccepted] = useState(false)
870
1573
  ### Select
871
1574
 
872
1575
  **Import:** `import { Select } from '@retray-dev/ui-kit'`
873
- **When to use:** Dropdown picker. Uses the native system picker UI on each platform — wheel modal on iOS (confirm with "Done"), spinner dialog on Android, native `<select>` on web (full keyboard support). Requires `@react-native-picker/picker` installed in the consuming app.
1576
+
1577
+ **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.
1578
+
1579
+ **Peer dependency:** `@react-native-picker/picker` must be installed.
874
1580
 
875
1581
  | Prop | Type | Default | Notes |
876
1582
  |------|------|---------|-------|
877
- | options | `SelectOption[]` | required | Each option: `{ label: string, value: string, disabled?: boolean }` |
878
- | value | `string` | — | Selected value |
1583
+ | options | `SelectOption[]` | required | `{ label: string, value: string, disabled?: boolean }` |
1584
+ | value | `string` | — | Currently selected value |
879
1585
  | onValueChange | `(value: string) => void` | — | — |
880
1586
  | placeholder | `string` | `'Select an option'` | Shown when no value is selected |
881
1587
  | label | `string` | — | Label above the trigger |
882
- | error | `string` | — | Error text below the trigger |
1588
+ | error | `string` | — | Error text below trigger |
883
1589
  | disabled | `boolean` | — | — |
884
- | style | `ViewStyle` | — | |
1590
+ | style | `ViewStyle` | — | Outer container style |
1591
+
1592
+ **Platform behavior:**
1593
+ - **iOS:** Modal sheet with wheel `Picker`, "Done" button, transparent backdrop, opens on trigger press
1594
+ - **Android:** Native dialog picker opened via `TextInput.focus()`
1595
+ - **Web:** Native HTML `<select>` element — full keyboard navigation
1596
+
1597
+ **Styling:** 8px border radius trigger, 14px horizontal + 11px vertical padding. Error state: 2px destructive border.
1598
+
1599
+ **Haptics:** `selectionAsync` on open and confirm.
885
1600
 
886
- **Example:**
1601
+ **Examples:**
887
1602
  ```tsx
888
1603
  <Select
889
1604
  label="Country"
890
- options={[{ label: 'Argentina', value: 'AR' }, { label: 'Spain', value: 'ES' }]}
1605
+ options={countries.map((c) => ({ label: c.name, value: c.code }))}
891
1606
  value={country}
892
1607
  onValueChange={setCountry}
893
- placeholder="Pick a country"
1608
+ placeholder="Select a country"
1609
+ />
1610
+
1611
+ <Select
1612
+ label="Category"
1613
+ options={categories}
1614
+ value={category}
1615
+ onValueChange={setCategory}
1616
+ error={categoryError}
894
1617
  />
895
1618
  ```
896
1619
 
@@ -899,37 +1622,55 @@ const [accepted, setAccepted] = useState(false)
899
1622
  ### Slider
900
1623
 
901
1624
  **Import:** `import { Slider } from '@retray-dev/ui-kit'`
902
- **When to use:** Select a numeric value within a range by dragging.
1625
+
1626
+ **When to use:** Selecting a numeric value in a continuous range by dragging — volume, brightness, price range, percentage.
1627
+
1628
+ **Peer dependency:** `@react-native-community/slider` must be installed.
903
1629
 
904
1630
  | Prop | Type | Default | Notes |
905
1631
  |------|------|---------|-------|
906
- | value | `number` | `0` | |
1632
+ | value | `number` | `0` | Controlled current value |
907
1633
  | minimumValue | `number` | `0` | — |
908
1634
  | maximumValue | `number` | `1` | — |
909
- | step | `number` | `0` | `0` means continuous (no snapping) |
1635
+ | step | `number` | `0` | `0` = continuous. Any positive number = snapping + haptic per step |
910
1636
  | onValueChange | `(value: number) => void` | — | Fires while dragging |
911
1637
  | onSlidingComplete | `(value: number) => void` | — | Fires on finger release |
1638
+ | label | `string` | — | Label text above slider |
1639
+ | showValue | `boolean` | `false` | Displays current value top-right |
1640
+ | formatValue | `(value: number) => string` | `toFixed(2)` | Custom display formatter |
1641
+ | accessibilityLabel | `string` | — | Screen reader label |
912
1642
  | disabled | `boolean` | — | — |
913
- | label | `string` | — | Label text displayed above the slider |
914
- | showValue | `boolean` | `false` | When `true`, displays the current value at the top right |
915
- | formatValue | `(value: number) => string` | — | Custom formatter for the displayed value |
916
- | accessibilityLabel | `string` | — | Accessibility label for screen readers |
917
1643
  | style | `ViewStyle` | — | — |
918
1644
 
919
- **Notes:** Uses `@react-native-community/slider` natively theming applied via `minimumTrackTintColor` (primary), `maximumTrackTintColor` (muted), `thumbTintColor` (primary). Requires `@react-native-community/slider` installed in the consuming app.
1645
+ **Haptics:** `selectionAsync` on each step (only when `step > 0`).
1646
+
1647
+ **Theming:** `minimumTrackTintColor = primary`, `maximumTrackTintColor = surface`, `thumbTintColor = primary`.
920
1648
 
921
- **Example:**
1649
+ **Examples:**
922
1650
  ```tsx
923
1651
  <Slider value={volume} minimumValue={0} maximumValue={100} step={1} onValueChange={setVolume} />
924
- <Slider
925
- label="Volume"
926
- showValue
927
- value={volume}
928
- minimumValue={0}
929
- maximumValue={100}
1652
+
1653
+ <Slider
1654
+ label="Volume"
1655
+ showValue
1656
+ value={volume}
1657
+ minimumValue={0}
1658
+ maximumValue={100}
930
1659
  step={5}
931
1660
  formatValue={(v) => `${v}%`}
932
- onValueChange={setVolume}
1661
+ onValueChange={setVolume}
1662
+ />
1663
+
1664
+ <Slider
1665
+ label="Price range"
1666
+ showValue
1667
+ value={price}
1668
+ minimumValue={50}
1669
+ maximumValue={500}
1670
+ step={10}
1671
+ formatValue={(v) => `$${v}`}
1672
+ onValueChange={setPrice}
1673
+ onSlidingComplete={applyFilter}
933
1674
  />
934
1675
  ```
935
1676
 
@@ -938,43 +1679,81 @@ const [accepted, setAccepted] = useState(false)
938
1679
  ### Tabs
939
1680
 
940
1681
  **Import:** `import { Tabs, TabsContent } from '@retray-dev/ui-kit'`
941
- **When to use:** Switching between categorized sections on the same screen.
1682
+
1683
+ **When to use:** Switching between categorized sections on the same screen — profile/activity/settings, different data views, content filters.
942
1684
 
943
1685
  | Prop | Type | Default | Notes |
944
1686
  |------|------|---------|-------|
945
- | tabs | `TabItem[]` | required | Each item: `{ label: string, value: string, icon?: ReactNode \| ((active: boolean) => ReactNode) }` |
1687
+ | tabs | `TabItem[]` | required | Tab definitions |
1688
+ | variant | `'pill' \| 'underline'` | `'pill'` | Visual style |
946
1689
  | value | `string` | — | Controlled active tab |
947
1690
  | onValueChange | `(value: string) => void` | — | — |
948
1691
  | children | `ReactNode` | — | `TabsContent` components |
949
1692
  | style | `ViewStyle` | — | — |
950
1693
 
1694
+ **TabItem type:**
1695
+ ```ts
1696
+ { label: string; value: string; icon?: ReactNode | ((isActive: boolean) => ReactNode) }
1697
+ ```
1698
+
1699
+ **Variants:**
1700
+ - `pill` — Animated background pill slides to active tab. Full-width distributed tabs. Active: filled `primary` + `primaryForeground` text. Default for top-of-screen navigation.
1701
+ - `underline` — 2px bottom border on active tab, no background. Tabs don't stretch — natural width. Airbnb-style filter tabs.
1702
+
951
1703
  **`TabsContent` Props:**
952
1704
 
953
1705
  | Prop | Type | Notes |
954
1706
  |------|------|-------|
955
1707
  | value | `string` | Must match a tab value in `tabs` |
956
- | activeValue | `string` | Pass the current active tab — content is hidden when not active |
1708
+ | activeValue | `string` | Pass the current active tab — content hidden when not active |
957
1709
  | children | `ReactNode` | — |
958
1710
  | style | `ViewStyle` | — |
959
1711
 
960
- **Animation:** An absolutely-positioned pill slides and resizes via spring (speed: 20, bounciness: 0) to track the active tab.
1712
+ **Animation (pill variant):** Absolutely-positioned pill slides via spring (speed: 20, bounciness: 0) to track active tab layout.
1713
+
1714
+ **Haptics:** `selectionAsync` on tab change.
961
1715
 
962
- **Example:**
1716
+ **Examples:**
963
1717
  ```tsx
964
1718
  const [tab, setTab] = useState('profile')
965
1719
 
1720
+ // Pill variant (default)
966
1721
  <Tabs
967
- tabs={[{ label: 'Profile', value: 'profile' }, { label: 'Security', value: 'security' }]}
1722
+ tabs={[
1723
+ { label: 'Profile', value: 'profile' },
1724
+ { label: 'Activity', value: 'activity' },
1725
+ { label: 'Settings', value: 'settings' },
1726
+ ]}
968
1727
  value={tab}
969
1728
  onValueChange={setTab}
970
1729
  >
971
1730
  <TabsContent value="profile" activeValue={tab}>
972
1731
  <Text>Profile content</Text>
973
1732
  </TabsContent>
974
- <TabsContent value="security" activeValue={tab}>
975
- <Text>Security content</Text>
1733
+ <TabsContent value="activity" activeValue={tab}>
1734
+ <Text>Activity content</Text>
1735
+ </TabsContent>
1736
+ <TabsContent value="settings" activeValue={tab}>
1737
+ <Text>Settings content</Text>
976
1738
  </TabsContent>
977
1739
  </Tabs>
1740
+
1741
+ // Underline variant with icons
1742
+ <Tabs
1743
+ variant="underline"
1744
+ tabs={[
1745
+ { label: 'All', value: 'all' },
1746
+ { label: 'Trips', value: 'trips' },
1747
+ { label: 'Experiences', value: 'experiences' },
1748
+ ]}
1749
+ value={filter}
1750
+ onValueChange={setFilter}
1751
+ />
1752
+
1753
+ // Content-only (no TabsContent — manage content externally)
1754
+ <Tabs tabs={tabs} value={tab} onValueChange={setTab} />
1755
+ {tab === 'profile' && <ProfileSection />}
1756
+ {tab === 'activity' && <ActivitySection />}
978
1757
  ```
979
1758
 
980
1759
  ---
@@ -982,26 +1761,47 @@ const [tab, setTab] = useState('profile')
982
1761
  ### Accordion
983
1762
 
984
1763
  **Import:** `import { Accordion } from '@retray-dev/ui-kit'`
985
- **When to use:** FAQs, collapsible sections. Animated expand/collapse.
1764
+
1765
+ **When to use:** FAQs, expandable settings sections, collapsible help content. Animated expand/collapse.
986
1766
 
987
1767
  | Prop | Type | Default | Notes |
988
1768
  |------|------|---------|-------|
989
- | items | `AccordionItem[]` | required | Each: `{ value: string, trigger: string, content: ReactNode }` |
990
- | type | `'single' \| 'multiple'` | `'single'` | `single`: only one open at a time. `multiple`: any number can be open |
991
- | defaultValue | `string \| string[]` | — | Initially open item(s). Use `string[]` with `type='multiple'` |
1769
+ | items | `AccordionItem[]` | required | Accordion entries |
1770
+ | type | `'single' \| 'multiple'` | `'single'` | `single`: one item open at a time. `multiple`: any number |
1771
+ | defaultValue | `string \| string[]` | — | Initially open items. Use `string[]` with `type='multiple'` |
992
1772
  | style | `ViewStyle` | — | — |
993
1773
 
994
- **Animation:** All animation is handled exclusively by `react-native-reanimated` (`withTiming` for height/rotation, `withSpring` for press scale, `useSharedValue`, `useAnimatedStyle`) on the UI thread at 60 fps. `Easing.out(Easing.ease)` for expand, `Easing.in(Easing.ease)` for collapse (220ms). Press scale springs to 0.95 on press-in and back to 1.0 on press-out.
1774
+ **AccordionItem type:**
1775
+ ```ts
1776
+ { value: string; trigger: string; content: ReactNode }
1777
+ ```
1778
+
1779
+ **Animation:** `react-native-reanimated` `withTiming` (220ms) for height and chevron rotation (180°). Press scale uses `withSpring`. Runs on UI thread at 60fps.
1780
+
1781
+ **Haptics:** `selectionAsync` on toggle.
995
1782
 
996
- **Example:**
1783
+ **Examples:**
997
1784
  ```tsx
998
1785
  <Accordion
999
- type="single"
1000
1786
  items={[
1001
- { value: 'q1', trigger: 'What is this?', content: <Text>It's a UI kit.</Text> },
1002
- { value: 'q2', trigger: 'Is it free?', content: <Text>Yes.</Text> },
1787
+ {
1788
+ value: 'q1',
1789
+ trigger: 'What payment methods do you accept?',
1790
+ content: <Text variant="body-sm">We accept Visa, Mastercard, and bank transfers.</Text>,
1791
+ },
1792
+ {
1793
+ value: 'q2',
1794
+ trigger: 'Can I cancel my booking?',
1795
+ content: <Text variant="body-sm">Yes — free cancellation within 48 hours of booking.</Text>,
1796
+ },
1003
1797
  ]}
1004
1798
  />
1799
+
1800
+ // Multiple open simultaneously
1801
+ <Accordion type="multiple" defaultValue={['q1']} items={faqItems} />
1802
+
1803
+ // Pre-opened
1804
+ <Accordion defaultValue="getting-started" items={helpSections} />
1005
1805
  ```
1006
1806
 
1007
1807
  ---
@@ -1009,66 +1809,96 @@ const [tab, setTab] = useState('profile')
1009
1809
  ### Sheet
1010
1810
 
1011
1811
  **Import:** `import { Sheet, BottomSheetModalProvider } from '@retray-dev/ui-kit'`
1012
- **When to use:** Bottom sheet that automatically sizes to its content. No snap points needed — just put children inside and it fits. Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`.
1013
1812
 
1014
- **Required setup** add to your app root (see Setup section above):
1015
- ```tsx
1016
- import { GestureHandlerRootView } from 'react-native-gesture-handler'
1017
- import { BottomSheetModalProvider } from '@retray-dev/ui-kit'
1813
+ **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.
1018
1814
 
1019
- <GestureHandlerRootView style={{ flex: 1 }}>
1020
- <BottomSheetModalProvider>
1021
- {/* rest of app */}
1022
- </BottomSheetModalProvider>
1023
- </GestureHandlerRootView>
1024
- ```
1815
+ **Required setup** — `BottomSheetModalProvider` must wrap app at root (inside `GestureHandlerRootView`). See Setup section above.
1025
1816
 
1026
- **Peer dependencies** (install in your app):
1817
+ **Peer dependencies:**
1027
1818
  ```bash
1028
1819
  pnpm add @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler react-native-worklets
1029
1820
  ```
1030
- Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to your `babel.config.js` plugins.
1031
-
1032
- > **Important — Expo managed workflow:** Expo's install tool may downgrade `@gorhom/bottom-sheet` to an incompatible version (v4). Pin it in `app.json` to prevent this:
1033
- > ```json
1034
- > { "expo": { "install": { "exclude": ["@gorhom/bottom-sheet"] } } }
1035
- > ```
1821
+ Add `react-native-worklets/plugin` (not `react-native-reanimated/plugin`) to `babel.config.js`.
1036
1822
 
1037
1823
  | Prop | Type | Default | Notes |
1038
1824
  |------|------|---------|-------|
1039
1825
  | open | `boolean` | required | `true` presents the sheet, `false` dismisses it |
1040
1826
  | onClose | `() => void` | required | Called on swipe-dismiss or backdrop press |
1041
- | title | `string` | — | |
1042
- | description | `string` | — | |
1043
- | children | `ReactNode` | — | |
1044
- | style | `ViewStyle` | — | Applied to the inner content container |
1827
+ | title | `string` | — | Sheet heading |
1828
+ | description | `string` | — | Supporting text below title |
1829
+ | children | `ReactNode` | — | Sheet content |
1830
+ | style | `ViewStyle` | — | Inner content container style |
1831
+ | scrollable | `boolean` | `false` | Wraps children in `BottomSheetScrollView` — fixes gesture conflict on both platforms when content needs to scroll |
1832
+ | maxHeight | `number` | — | Caps sheet height (dp). Automatically enables scrolling when content exceeds this value |
1833
+
1834
+ **Features:**
1835
+ - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed
1836
+ - `enablePanDownToClose` — swipe down to dismiss
1837
+ - Backdrop press dismisses
1838
+ - **Scrollable content:** use `scrollable` prop (explicit) or `maxHeight` prop (capped height). Both use `BottomSheetScrollView` internally — do NOT use plain `ScrollView` inside Sheet, it breaks gesture handling on iOS
1839
+
1840
+ **Styling:** `RADIUS.lg = 20px` top corners, 16px horizontal + 20px bottom padding.
1045
1841
 
1046
- **Example:**
1842
+ **Haptics:** `impactLight` on open.
1843
+
1844
+ **Examples:**
1047
1845
  ```tsx
1048
- <Sheet open={open} onClose={() => setOpen(false)} title="Filters">
1049
- <RadioGroup options={sortOptions} value={sort} onValueChange={setSort} />
1846
+ const [open, setOpen] = useState(false)
1847
+
1848
+ <Sheet open={open} onClose={() => setOpen(false)} title="Sort by">
1849
+ <RadioGroup
1850
+ options={[
1851
+ { label: 'Newest', value: 'newest' },
1852
+ { label: 'Price: Low to High', value: 'price_asc' },
1853
+ { label: 'Price: High to Low', value: 'price_desc' },
1854
+ { label: 'Rating', value: 'rating' },
1855
+ ]}
1856
+ value={sort}
1857
+ onValueChange={(v) => { setSort(v); setOpen(false) }}
1858
+ />
1050
1859
  </Sheet>
1051
- ```
1052
1860
 
1053
- **Notes:**
1054
- - Height is calculated automatically from content via `enableDynamicSizing` (gorhom v5 feature). No `snapPoints` prop.
1055
- - Swipe down to dismiss (`enablePanDownToClose`). Backdrop press also dismisses.
1056
- - `impactAsync(Light)` haptic fires on open.
1861
+ // Scrollable sheet — long list
1862
+ <Sheet open={open} onClose={() => setOpen(false)} title="Logs" scrollable>
1863
+ {logs.map((log) => (
1864
+ <Text key={log.id}>{log.message}</Text>
1865
+ ))}
1866
+ </Sheet>
1867
+
1868
+ // Capped height — scroll kicks in when content overflows 400dp
1869
+ <Sheet open={open} onClose={() => setOpen(false)} title="Results" maxHeight={400}>
1870
+ {results.map((r) => <ListItem key={r.id} title={r.name} />)}
1871
+ </Sheet>
1872
+
1873
+ // Filter sheet
1874
+ <Sheet open={filterOpen} onClose={() => setFilterOpen(false)} title="Filters">
1875
+ <View style={{ gap: SPACING.lg }}>
1876
+ <View>
1877
+ <Text variant="title-sm">Price range</Text>
1878
+ <Slider value={maxPrice} minimumValue={0} maximumValue={1000} step={50} onValueChange={setMaxPrice} showValue formatValue={(v) => `$${v}`} />
1879
+ </View>
1880
+ <Separator />
1881
+ <View>
1882
+ <Text variant="title-sm">Category</Text>
1883
+ <ChipGroup options={categoryOptions} value={selectedCategory} onValueChange={setSelectedCategory} />
1884
+ </View>
1885
+ <Button label="Apply filters" fullWidth onPress={() => { applyFilters(); setFilterOpen(false) }} />
1886
+ </View>
1887
+ </Sheet>
1888
+ ```
1057
1889
 
1058
1890
  ---
1059
1891
 
1060
1892
  ### Toast / useToast
1061
1893
 
1062
1894
  **Import:** `import { ToastProvider, useToast } from '@retray-dev/ui-kit'`
1063
- **When to use:** Ephemeral feedback messages (save success, network error, copy confirmation).
1064
1895
 
1065
- **Required setup**`ToastProvider` must wrap your app inside `SafeAreaProvider` (see Setup section above).
1896
+ **When to use:** Ephemeral feedback messages save confirmations, errors, copy notifications, background process updates. Auto-dismiss after duration.
1066
1897
 
1067
- **Peer dependency:** `react-native-safe-area-context` required for `useSafeAreaInsets` inside `ToastProvider`.
1898
+ **Required setup:** `ToastProvider` must wrap app inside `SafeAreaProvider`. See Setup section above.
1068
1899
 
1900
+ **Usage:**
1069
1901
  ```tsx
1070
- import { useToast } from '@retray-dev/ui-kit'
1071
-
1072
1902
  function MyComponent() {
1073
1903
  const { toast, dismiss } = useToast()
1074
1904
 
@@ -1077,10 +1907,7 @@ function MyComponent() {
1077
1907
  label="Save"
1078
1908
  onPress={async () => {
1079
1909
  await save()
1080
- toast({ title: 'Saved', description: 'Your changes were saved.', variant: 'success' })
1081
- // With custom icon name
1082
- toast({ title: 'Saved', variant: 'success', iconName: 'check-circle' })
1083
- toast({ title: 'Oops', variant: 'destructive', iconName: 'x-circle' })
1910
+ toast({ title: 'Saved', variant: 'success' })
1084
1911
  }}
1085
1912
  />
1086
1913
  )
@@ -1092,90 +1919,223 @@ function MyComponent() {
1092
1919
  | Field | Type | Default | Notes |
1093
1920
  |-------|------|---------|-------|
1094
1921
  | title | `string` | — | Bold heading |
1095
- | description | `string` | — | Detail text |
1096
- | variant | `'default' \| 'destructive' \| 'success'` | `'default'` | `default`: dark background. `destructive`: red. `success`: green |
1097
- | duration | `number` (ms) | `3000` | Auto-dismiss after this delay |
1098
- | icon | `ReactNode` | — | Custom icon node. Defaults to a variant-appropriate symbol (`✓`, `✖`, `ℹ`) at 22px |
1922
+ | description | `string` | — | Detail text below title |
1923
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | Background and icon color |
1924
+ | duration | `number` (ms) | `3000` | Auto-dismiss delay. Pass `Infinity` to prevent auto-dismiss |
1925
+ | icon | `ReactNode` | — | Custom icon. Defaults to variant symbol |
1099
1926
  | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
1100
- | iconColor | `string` | — | Override icon color. Defaults to variant text color |
1927
+ | iconColor | `string` | — | Override icon color |
1928
+ | action | `{ label: string, onPress: () => void }` | — | Optional action button beside dismiss |
1929
+
1930
+ **`dismiss(id)`:** Dismiss a specific toast programmatically. `id` is returned by the `toast()` call.
1931
+
1932
+ **Variant details:**
1933
+ - `default` — dark/primary background, `impactLight` haptic
1934
+ - `success` — `success` token background, `notificationSuccess` haptic
1935
+ - `destructive` — `destructive` token background, `notificationError` haptic
1936
+ - `warning` — `warning` token background, `notificationError` haptic
1937
+
1938
+ **Behavior:**
1939
+ - Max 3 toasts shown simultaneously — oldest removed when 4th arrives
1940
+ - Appear at top of screen below status bar (dynamic safe area inset)
1941
+ - Swipe left or right to dismiss early (threshold: 80px or 800pt/s velocity)
1942
+ - **Web:** 400px max width, centered
1943
+
1944
+ **Animation:** Entrance: `withTiming(120ms, Easing.out(Easing.exp))` slide-down + opacity. Exit: `withTiming(200ms)` slide-up + opacity fade.
1945
+
1946
+ **Examples:**
1947
+ ```tsx
1948
+ const { toast, dismiss } = useToast()
1949
+
1950
+ // Basic variants
1951
+ toast({ title: 'Saved', variant: 'success' })
1952
+ toast({ title: 'Connection error', variant: 'destructive' })
1953
+ toast({ title: 'Check your email', variant: 'warning' })
1954
+ toast({ title: 'Link copied' })
1955
+
1956
+ // With description
1957
+ toast({
1958
+ title: 'Payment sent',
1959
+ description: '$250 sent to John Doe',
1960
+ variant: 'success',
1961
+ iconName: 'check-circle',
1962
+ })
1963
+
1964
+ // With action button
1965
+ toast({
1966
+ title: 'Message deleted',
1967
+ action: { label: 'Undo', onPress: () => restoreMessage() },
1968
+ })
1969
+
1970
+ // Custom duration (longer)
1971
+ toast({ title: 'Processing your request...', duration: 8000 })
1972
+
1973
+ // Programmatic dismiss
1974
+ const id = toast({ title: 'Uploading...', duration: Infinity })
1975
+ await upload()
1976
+ dismiss(id)
1977
+ toast({ title: 'Upload complete', variant: 'success' })
1978
+ ```
1101
1979
 
1102
- **`dismiss(id)`:** Dismiss a toast programmatically. The `id` is returned by the `toast()` call — store it if you need programmatic dismissal.
1980
+ ---
1981
+
1982
+ ### ConfirmDialog
1983
+
1984
+ **Import:** `import { ConfirmDialog } from '@retray-dev/ui-kit'`
1985
+
1986
+ **When to use:** Confirmation prompts before irreversible or destructive actions — delete, send money, discard changes. Always confirm before actions that can't be undone.
1987
+
1988
+ **Requires:** `BottomSheetModalProvider` at app root (same as `Sheet`).
1989
+
1990
+ | Prop | Type | Default | Notes |
1991
+ |------|------|---------|-------|
1992
+ | visible | `boolean` | required | Controls dialog visibility |
1993
+ | title | `string` | required | Dialog heading |
1994
+ | description | `string` | — | Supporting text — describe what exactly will happen |
1995
+ | confirmLabel | `string` | `'Confirm'` | Confirm button text |
1996
+ | cancelLabel | `string` | `'Cancel'` | Cancel button text |
1997
+ | confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Use `'destructive'` for delete/remove actions |
1998
+ | onConfirm | `() => void` | required | Called when confirm is tapped |
1999
+ | onCancel | `() => void` | required | Called when cancel is tapped or backdrop pressed |
1103
2000
 
1104
2001
  **Notes:**
1105
- - Max 3 toasts shown simultaneously (oldest is removed when a 4th arrives)
1106
- - Toasts appear at the top of the screen, below the status bar (dynamic safe area inset)
1107
- - **On web:** constrained to 400px wide and centered (`alignSelf: 'center'`) not full-width
1108
- - Entrance: `withTiming(120ms, Easing.out(Easing.exp))` slide-down + opacity fade fast, sharp feel
1109
- - Exit: `withTiming(200ms)` slide-up + opacity fade
1110
- - Swipe left or right to dismiss early (threshold: 80px or 800 pt/s velocity)
2002
+ - Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`
2003
+ - Buttons are full-width, stacked vertically (confirm on top)
2004
+ - Cancel shows X icon, confirm shows check (primary) or trash-2 (destructive) icon
2005
+ - Swipe down or backdrop press calls `onCancel`
2006
+
2007
+ **Haptics:** `impactLight` on open.
2008
+
2009
+ **Examples:**
2010
+ ```tsx
2011
+ // Delete confirmation
2012
+ <ConfirmDialog
2013
+ visible={showDelete}
2014
+ title="Delete transaction?"
2015
+ description="$45.000 · Mercado · 12 mar · This action cannot be undone."
2016
+ confirmLabel="Delete"
2017
+ confirmVariant="destructive"
2018
+ onConfirm={handleDelete}
2019
+ onCancel={() => setShowDelete(false)}
2020
+ />
2021
+
2022
+ // Send money confirmation
2023
+ <ConfirmDialog
2024
+ visible={showConfirm}
2025
+ title="Send payment?"
2026
+ description={`Send $${amount} to ${recipientName}`}
2027
+ confirmLabel="Send"
2028
+ onConfirm={handleSend}
2029
+ onCancel={() => setShowConfirm(false)}
2030
+ />
2031
+
2032
+ // Discard changes
2033
+ <ConfirmDialog
2034
+ visible={showDiscard}
2035
+ title="Discard changes?"
2036
+ description="All unsaved changes will be lost."
2037
+ confirmLabel="Discard"
2038
+ confirmVariant="destructive"
2039
+ onConfirm={() => { setShowDiscard(false); goBack() }}
2040
+ onCancel={() => setShowDiscard(false)}
2041
+ />
2042
+ ```
1111
2043
 
1112
2044
  ---
1113
2045
 
1114
2046
  ### ListItem
1115
2047
 
1116
2048
  **Import:** `import { ListItem } from '@retray-dev/ui-kit'`
1117
- **When to use:** Rows in a list, feed, or menu. Composes a left slot (avatar, icon, image), title/subtitle/caption text block, and a right slot (price, badge, chevron, switch, etc.) in a standard horizontal layout. Supports both plain and card visual variants.
2049
+
2050
+ **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.
1118
2051
 
1119
2052
  | Prop | Type | Default | Notes |
1120
2053
  |------|------|---------|-------|
1121
- | title | `string` | required | Primary text (17px / 500 weight) |
1122
- | subtitle | `string` | — | Secondary line below the title (14px / 400 weight) |
1123
- | caption | `string` | — | Tertiary / caption line below the subtitle (12px / 400 weight, 70% opacity) |
1124
- | leftRender | `ReactNode` | — | Arbitrary content in a fixed 44×44pt left slot (avatar, icon, image, etc.) |
1125
- | rightRender | `string \| ReactNode` | — | Content on the right edge. Strings are styled as muted text (15px); pass ReactNode for Badges, prices, switches, etc. |
1126
- | leftIcon | `string` | — | Icon name from `@expo/vector-icons` in the left slot. Takes precedence over `leftRender`. Size 24, color `foreground` |
1127
- | rightIcon | `string` | — | Icon name from `@expo/vector-icons` in the right slot. Takes precedence over `rightRender`. Size 24, color `mutedForeground` |
1128
- | leftIconColor | `string` | — | Override left icon color |
1129
- | rightIconColor | `string` | — | Override right icon color |
1130
- | variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background, sits inside a parent surface. `card`: standalone surface with background, border, and shadow |
1131
- | showChevron | `boolean` | `false` | Shows a `›` chevron on the far right. Ignored when `rightRender` is set |
1132
- | showSeparator | `boolean` | `false` | Renders a hairline separator at the bottom. Useful when stacking multiple plain items in a list |
1133
- | onPress | `() => void` | — | Makes the row pressable (scale spring + haptics). No animation or haptics when omitted |
2054
+ | title | `string` | required | Primary text |
2055
+ | subtitle | `string` | — | Secondary line below title |
2056
+ | caption | `string` | — | Tertiary line below subtitle |
2057
+ | leftRender | `ReactNode` | — | Content in the fixed 44×44pt left slot |
2058
+ | rightRender | `string \| ReactNode` | — | Content on the right edge. Strings auto-styled as muted text |
2059
+ | leftIcon | `string` | — | Icon name for left slot. Takes precedence over `leftRender` |
2060
+ | rightIcon | `string` | — | Icon name for right slot. Takes precedence over `rightRender` |
2061
+ | leftIconColor | `string` | — | Override left icon color (default: `foreground`) |
2062
+ | rightIconColor | `string` | — | Override right icon color (default: `foregroundMuted`) |
2063
+ | variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background. `card`: surface with border and shadow |
2064
+ | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightRender` is set |
2065
+ | showSeparator | `boolean` | `false` | Hairline separator at bottom. Useful for stacking plain items |
2066
+ | onPress | `() => void` | — | Makes row pressable (scale animation + haptics) |
1134
2067
  | disabled | `boolean` | — | Reduces opacity to 0.45 |
1135
- | style | `ViewStyle` | — | Style applied to the outer container |
1136
- | titleStyle | `TextStyle` | — | Style override for the title text |
1137
- | subtitleStyle | `TextStyle` | — | Style override for the subtitle text |
1138
- | captionStyle | `TextStyle` | — | Style override for the caption text |
2068
+ | style | `ViewStyle` | — | Outer container style |
2069
+ | titleStyle | `TextStyle` | — | Override title text style |
2070
+ | subtitleStyle | `TextStyle` | — | Override subtitle text style |
2071
+ | captionStyle | `TextStyle` | — | Override caption text style |
1139
2072
  | icon | `ReactNode` | — | **Deprecated** — use `leftRender` |
1140
2073
  | trailing | `string \| ReactNode` | — | **Deprecated** — use `rightRender` |
1141
2074
 
1142
- **Animation:** Scale springs to 0.97 on press-in, back to 1.0 on press-out (only when `onPress` is provided).
2075
+ **Slots:**
2076
+ - `leftRender` / `leftIcon` — 44×44pt fixed container, centered. Good for Avatar, icons, thumbnails
2077
+ - `rightRender` / `rightIcon` — max 160pt wide, right-aligned. Good for Badge, price text, Switch
2078
+ - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender` is absent
2079
+
2080
+ **Separator inset:** Aligns to text block — `marginLeft` adjusts to skip over left slot when present.
2081
+
2082
+ **Animation:** Scale springs to 0.97 on press (subtler than Button's 0.95).
1143
2083
 
1144
- **Example:**
2084
+ **Haptics:** `selectionAsync` on press.
2085
+
2086
+ **Examples:**
1145
2087
  ```tsx
1146
- // Simple row with chevron
2088
+ // Simple settings row
2089
+ <ListItem title="Profile" showChevron onPress={() => navigate('profile')} />
2090
+
2091
+ // With subtitle and chevron
1147
2092
  <ListItem
1148
- title="Profile"
1149
- subtitle="Manage your account"
2093
+ title="Notifications"
2094
+ subtitle="Push, email, and SMS"
1150
2095
  showChevron
1151
- onPress={() => navigate('profile')}
2096
+ onPress={() => navigate('notifications')}
1152
2097
  />
1153
2098
 
1154
- // Rich row: avatar + title/subtitle/caption + price + badge
2099
+ // Icon left slot + chevron
2100
+ <ListItem title="Profile" leftIcon="user" showChevron onPress={() => navigate('profile')} />
2101
+
2102
+ // Icon left + badge right
1155
2103
  <ListItem
1156
- leftRender={<Avatar src={item.avatar} fallback={item.initials} size="md" />}
1157
- title={item.name}
1158
- subtitle={item.date}
1159
- caption={item.category}
2104
+ leftIcon="bell"
2105
+ title="Notifications"
2106
+ subtitle="3 unread"
2107
+ rightRender={<Badge label="3" variant="destructive" size="sm" />}
2108
+ onPress={() => navigate('notifications')}
2109
+ />
2110
+
2111
+ // Avatar + full text stack + right content
2112
+ <ListItem
2113
+ leftRender={<Avatar src={user.avatar} fallback={user.initials} status="online" />}
2114
+ title={user.name}
2115
+ subtitle={user.role}
2116
+ caption="Active 2 min ago"
1160
2117
  rightRender={
1161
2118
  <View style={{ alignItems: 'flex-end', gap: 4 }}>
1162
- <Text variant="label">${item.amount}</Text>
1163
- <Badge label={item.status} variant="secondary" size="sm" />
2119
+ <Text variant="title-sm">${amount}</Text>
2120
+ <Badge label={status} variant="warningOutline" size="sm" />
1164
2121
  </View>
1165
2122
  }
1166
- onPress={() => navigate('detail', { id: item.id })}
2123
+ onPress={() => navigate('user', { id: user.id })}
1167
2124
  />
1168
2125
 
1169
- // Icon name props (no manual imports needed)
1170
- <ListItem title="Profile" leftIcon="user" rightIcon="chevron-right" onPress={() => {}} />
1171
- <ListItem title="Notifications" leftIcon="bell" subtitle="3 unread" showChevron />
2126
+ // Switch in right slot
2127
+ <ListItem
2128
+ leftIcon="moon"
2129
+ title="Dark mode"
2130
+ rightRender={<Switch checked={darkMode} onCheckedChange={setDarkMode} />}
2131
+ />
1172
2132
 
1173
2133
  // Card variant (standalone surface)
1174
2134
  <ListItem
1175
2135
  variant="card"
1176
2136
  leftIcon="credit-card"
1177
- title="Balance"
1178
- subtitle="Available funds"
2137
+ title="Checking account"
2138
+ subtitle="Available balance"
1179
2139
  rightRender="$12.500"
1180
2140
  />
1181
2141
 
@@ -1183,114 +2143,173 @@ function MyComponent() {
1183
2143
  {transactions.map((t, i) => (
1184
2144
  <ListItem
1185
2145
  key={t.id}
1186
- title={t.name}
2146
+ leftRender={<Avatar fallback={t.merchant[0]} size="sm" />}
2147
+ title={t.merchant}
1187
2148
  subtitle={t.date}
1188
- rightRender={t.amount}
2149
+ rightRender={
2150
+ <Text variant="title-sm" color={t.amount < 0 ? colors.destructive : colors.success}>
2151
+ {t.amount < 0 ? '-' : '+'}${Math.abs(t.amount)}
2152
+ </Text>
2153
+ }
1189
2154
  showSeparator={i < transactions.length - 1}
1190
- onPress={() => navigate('detail', { id: t.id })}
2155
+ onPress={() => navigate('transaction', { id: t.id })}
1191
2156
  />
1192
2157
  ))}
1193
2158
  ```
1194
2159
 
1195
- **Notes:**
1196
- - `leftRender` renders inside a fixed 44×44pt container (centered) — no need to wrap in a sized View
1197
- - `rightRender` container has `maxWidth: 160` and aligns content to the right edge
1198
- - The separator inset aligns to the text block: `marginLeft: 16 + 44 + 12` when `leftRender` is set, `marginLeft: 16` otherwise
1199
-
1200
2160
  ---
1201
2161
 
1202
2162
  ### Chip / ChipGroup
1203
2163
 
1204
2164
  **Import:** `import { Chip, ChipGroup } from '@retray-dev/ui-kit'`
1205
- **When to use:** Filter pills, single-select options displayed inline. Use `ChipGroup` for managed single-selection; use `Chip` standalone for custom logic.
2165
+
2166
+ **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).
1206
2167
 
1207
2168
  **`Chip` Props:**
1208
2169
 
1209
2170
  | Prop | Type | Default | Notes |
1210
2171
  |------|------|---------|-------|
1211
2172
  | label | `string` | required | — |
1212
- | selected | `boolean` | `false` | Controls fill color |
2173
+ | selected | `boolean` | `false` | Controls fill style |
1213
2174
  | onPress | `() => void` | — | — |
1214
- | icon | `ReactNode` | — | Custom icon rendered before the label |
1215
- | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
2175
+ | icon | `ReactNode` | — | Custom icon before label |
2176
+ | iconName | `string` | — | Icon name. Takes precedence over `icon` |
1216
2177
  | style | `ViewStyle` | — | — |
1217
2178
 
1218
2179
  **`ChipGroup` Props:**
1219
2180
 
1220
2181
  | Prop | Type | Default | Notes |
1221
2182
  |------|------|---------|-------|
1222
- | options | `ChipOption[]` | required | Each: `{ label: string, value: string \| number }` |
1223
- | value | `string \| number \| (string \| number)[]` | — | Currently selected value (single mode) or array of selected values (multi mode) |
1224
- | onValueChange | `(value: string \| number \| (string \| number)[]) => void` | — | |
1225
- | multiSelect | `boolean` | `false` | When `true`, allows multiple chips to be selected simultaneously |
1226
- | style | `ViewStyle` | — | Applied to the wrapping row |
2183
+ | options | `ChipOption[]` | required | `{ label: string, value: string \| number }` |
2184
+ | value | `string \| number \| (string \| number)[]` | — | Selected value(s) |
2185
+ | onValueChange | `(value: ...) => void` | — | Returns single value or array depending on `multiSelect` |
2186
+ | multiSelect | `boolean` | `false` | Allow multiple chips selected simultaneously |
2187
+ | style | `ViewStyle` | — | Wrapping row style |
2188
+
2189
+ **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.
1227
2190
 
1228
- **Animation:** Background, text, and border colors animate via `Animated.timing` (150ms) between unselected (`secondary`/`border`/`foreground`) and selected (`primary`/`primary`/`primaryForeground`) states.
2191
+ **Animation:** Spring scale 0.95 on press; background/text/border color transitions (150ms timing) on selection change.
1229
2192
 
1230
- **Example:**
2193
+ **Haptics:** `selectionAsync` on change.
2194
+
2195
+ **Examples:**
1231
2196
  ```tsx
1232
- // Single select
2197
+ // Single select percentage
1233
2198
  const [pct, setPct] = useState(50)
1234
-
1235
2199
  <ChipGroup
1236
- options={[
1237
- { label: '40%', value: 40 },
1238
- { label: '50%', value: 50 },
1239
- { label: '100%', value: 100 },
1240
- ]}
2200
+ options={[{ label: '25%', value: 25 }, { label: '50%', value: 50 }, { label: '100%', value: 100 }]}
1241
2201
  value={pct}
1242
2202
  onValueChange={setPct}
1243
2203
  />
1244
2204
 
1245
- // Multi select
2205
+ // Multi select categories
1246
2206
  const [categories, setCategories] = useState<number[]>([1, 3])
1247
-
1248
2207
  <ChipGroup
1249
2208
  multiSelect
1250
2209
  options={[
1251
2210
  { label: 'Food', value: 1 },
1252
2211
  { label: 'Transport', value: 2 },
1253
2212
  { label: 'Entertainment', value: 3 },
2213
+ { label: 'Health', value: 4 },
1254
2214
  ]}
1255
2215
  value={categories}
1256
2216
  onValueChange={setCategories}
1257
2217
  />
2218
+
2219
+ // Standalone chips (custom logic)
2220
+ <View style={{ flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap' }}>
2221
+ {filters.map((f) => (
2222
+ <Chip
2223
+ key={f.value}
2224
+ label={f.label}
2225
+ selected={activeFilter === f.value}
2226
+ onPress={() => setActiveFilter(f.value)}
2227
+ />
2228
+ ))}
2229
+ </View>
1258
2230
  ```
1259
2231
 
1260
2232
  ---
1261
2233
 
1262
- ### ConfirmDialog
2234
+ ### CategoryStrip
1263
2235
 
1264
- **Import:** `import { ConfirmDialog } from '@retray-dev/ui-kit'`
1265
- **When to use:** Confirmation prompts before irreversible or destructive actions (delete, send, discard).
2236
+ **Import:** `import { CategoryStrip } from '@retray-dev/ui-kit'`
2237
+
2238
+ **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.
1266
2239
 
1267
2240
  | Prop | Type | Default | Notes |
1268
2241
  |------|------|---------|-------|
1269
- | visible | `boolean` | required | Controls modal visibility |
1270
- | title | `string` | required | Dialog heading |
1271
- | description | `string` | — | Supporting text below the title |
1272
- | confirmLabel | `string` | `'Confirm'` | Label for the confirm button |
1273
- | cancelLabel | `string` | `'Cancel'` | Label for the cancel button |
1274
- | confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Button variant for the confirm action |
1275
- | onConfirm | `() => void` | required | Called when confirm is tapped |
1276
- | onCancel | `() => void` | required | Called when cancel is tapped or backdrop is pressed |
2242
+ | categories | `CategoryItem[]` | required | Category definitions |
2243
+ | value | `string \| string[]` | | Selected category value(s) |
2244
+ | onValueChange | `(value: string \| string[]) => void` | — | Called with new selection |
2245
+ | multiSelect | `boolean` | `false` | Allow multiple simultaneous selections |
2246
+ | style | `ViewStyle` | | ScrollView content container style |
2247
+ | itemStyle | `ViewStyle` | | Style applied to each chip's wrapper |
2248
+
2249
+ **CategoryItem type:**
2250
+ ```ts
2251
+ {
2252
+ label: string
2253
+ value: string
2254
+ icon?: ReactNode | string // Icon or icon name (16px, auto-colored)
2255
+ badge?: number // Count badge on chip, capped at 99
2256
+ }
2257
+ ```
1277
2258
 
1278
- **Notes:**
1279
- - Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing` — height auto-fits content, no snap points.
1280
- - Swipe down or tap backdrop to cancel (calls `onCancel`).
1281
- - Cancel and confirm buttons are full-width stacked vertically.
1282
- - Requires `BottomSheetModalProvider` at app root (same as `Sheet`).
2259
+ **Single select behavior:** Pressing selected item again deselects it (value becomes `''`).
2260
+
2261
+ **Multi select behavior:** Toggles item in/out of value array.
1283
2262
 
1284
- **Example:**
2263
+ **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.
2264
+
2265
+ **Animation:** Scale spring 0.95 on press.
2266
+
2267
+ **Haptics:** `selectionAsync` on change.
2268
+
2269
+ **Web:** Horizontal scroll with `showsHorizontalScrollIndicator={false}`.
2270
+
2271
+ **Examples:**
1285
2272
  ```tsx
1286
- <ConfirmDialog
1287
- visible={showDelete}
1288
- title="¿Eliminar gasto?"
1289
- description="$45.000 · Mercado · 12 mar"
1290
- confirmLabel="Eliminar"
1291
- confirmVariant="destructive"
1292
- onConfirm={handleDelete}
1293
- onCancel={() => setShowDelete(false)}
2273
+ // Basic category filter
2274
+ <CategoryStrip
2275
+ categories={[
2276
+ { label: 'All', value: '' },
2277
+ { label: 'Trips', value: 'trips' },
2278
+ { label: 'Experiences', value: 'experiences' },
2279
+ { label: 'Restaurants', value: 'restaurants' },
2280
+ ]}
2281
+ value={category}
2282
+ onValueChange={setCategory}
2283
+ />
2284
+
2285
+ // With icons
2286
+ <CategoryStrip
2287
+ categories={[
2288
+ { label: 'Nearby', value: 'nearby', icon: 'map-pin' },
2289
+ { label: 'Popular', value: 'popular', icon: 'trending-up' },
2290
+ { label: 'New', value: 'new', icon: 'star' },
2291
+ ]}
2292
+ value={filter}
2293
+ onValueChange={setFilter}
2294
+ />
2295
+
2296
+ // With badge counts
2297
+ <CategoryStrip
2298
+ categories={[
2299
+ { label: 'Inbox', value: 'inbox', badge: 3 },
2300
+ { label: 'Sent', value: 'sent' },
2301
+ { label: 'Archived', value: 'archived' },
2302
+ ]}
2303
+ value={tab}
2304
+ onValueChange={setTab}
2305
+ />
2306
+
2307
+ // Multi-select
2308
+ <CategoryStrip
2309
+ multiSelect
2310
+ categories={amenities}
2311
+ value={selectedAmenities}
2312
+ onValueChange={setSelectedAmenities}
1294
2313
  />
1295
2314
  ```
1296
2315
 
@@ -1299,19 +2318,32 @@ const [categories, setCategories] = useState<number[]>([1, 3])
1299
2318
  ### LabelValue
1300
2319
 
1301
2320
  **Import:** `import { LabelValue } from '@retray-dev/ui-kit'`
1302
- **When to use:** Key-value display rows in receipts, summaries, and detail screens. Label on the left (muted caption), value on the right (medium weight).
2321
+
2322
+ **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).
1303
2323
 
1304
2324
  | Prop | Type | Default | Notes |
1305
2325
  |------|------|---------|-------|
1306
- | label | `string` | required | Caption text on the left |
1307
- | value | `string \| ReactNode` | required | Value on the right. Strings are auto-styled; pass `ReactNode` for custom rendering |
2326
+ | label | `string` | required | Caption label on the left |
2327
+ | value | `string \| ReactNode` | required | Value on the right. Strings auto-styled; pass `ReactNode` for custom content |
1308
2328
  | style | `ViewStyle` | — | — |
1309
2329
 
1310
- **Example:**
2330
+ **Styling:** Row layout, `justifyContent: 'space-between'`. Label: 13pt / Regular / `foregroundMuted`. Value: 14pt / Medium / `foreground`. 12px gap.
2331
+
2332
+ **Examples:**
1311
2333
  ```tsx
1312
- <LabelValue label="Fecha" value="12 mar 2025" />
1313
- <LabelValue label="Categoría" value="Mercado" />
1314
- <LabelValue label="Estado" value={<Badge label="Pendiente" variant="secondary" />} />
2334
+ <LabelValue label="Date" value="12 mar 2025" />
2335
+ <LabelValue label="Category" value="Food & drink" />
2336
+ <LabelValue label="Status" value={<Badge label="Pending" variant="warningOutline" size="sm" />} />
2337
+ <LabelValue label="Amount" value="$45.000" />
2338
+
2339
+ // Transaction detail
2340
+ <View style={{ gap: SPACING.sm }}>
2341
+ <LabelValue label="Merchant" value={transaction.merchant} />
2342
+ <LabelValue label="Date" value={formatDate(transaction.date)} />
2343
+ <LabelValue label="Category" value={transaction.category} />
2344
+ <Separator />
2345
+ <LabelValue label="Amount" value={`$${transaction.amount}`} />
2346
+ </View>
1315
2347
  ```
1316
2348
 
1317
2349
  ---
@@ -1319,42 +2351,121 @@ const [categories, setCategories] = useState<number[]>([1, 3])
1319
2351
  ### MonthPicker
1320
2352
 
1321
2353
  **Import:** `import { MonthPicker } from '@retray-dev/ui-kit'`
1322
- **When to use:** Compact month/year selector with previous/next navigation. Common in finance apps for filtering by period.
2354
+
2355
+ **When to use:** Month/year navigation — finance apps filtering by period, date pickers, reporting periods. Compact left/right navigation UI.
1323
2356
 
1324
2357
  | Prop | Type | Default | Notes |
1325
2358
  |------|------|---------|-------|
1326
- | value | `MonthPickerValue` | required | `{ month: number, year: number }` — `month` is 1–12 |
1327
- | onChange | `(value: MonthPickerValue) => void` | required | Called on each navigation step |
2359
+ | value | `MonthPickerValue` | required | `{ month: 1–12, year: number }` |
2360
+ | onChange | `(value: MonthPickerValue) => void` | required | Called on navigation |
1328
2361
  | style | `ViewStyle` | — | — |
1329
2362
 
1330
- **`MonthPickerValue`:** `{ month: number, year: number }` — month is 1-indexed (January = 1).
2363
+ **MonthPickerValue type:**
2364
+ ```ts
2365
+ { month: number; year: number } // month is 1-indexed (January = 1)
2366
+ ```
2367
+
2368
+ **Navigation:** Year wraps correctly at boundaries (December → January increments year, January → December decrements year). Fully controlled — no internal state.
1331
2369
 
1332
- **Navigation:** Arrows wrap correctly at year boundaries (December → January increments the year; January → December decrements it). The component is fully controlled no internal state.
2370
+ **Styling:** Centered row with `←` / `→` buttons (44×44px each). Label: 17pt / Medium / 160px min-width, centered.
1333
2371
 
1334
- **Example:**
2372
+ **Haptics:** `selectionAsync` on navigation.
2373
+
2374
+ **Examples:**
1335
2375
  ```tsx
1336
- const [period, setPeriod] = useState({ month: new Date().getMonth() + 1, year: new Date().getFullYear() })
2376
+ const [period, setPeriod] = useState({
2377
+ month: new Date().getMonth() + 1,
2378
+ year: new Date().getFullYear(),
2379
+ })
1337
2380
 
1338
2381
  <MonthPicker value={period} onChange={setPeriod} />
1339
- // Displays: "April 2026"
2382
+ // Displays: "May 2026"
2383
+
2384
+ // In a transactions header
2385
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
2386
+ <Text variant="display-md">Transactions</Text>
2387
+ <MonthPicker value={period} onChange={setPeriod} />
2388
+ </View>
1340
2389
  ```
1341
2390
 
1342
2391
  ---
1343
2392
 
1344
- ## Icon System
2393
+ ### Pressable
1345
2394
 
1346
- The library ships a built-in icon resolver so you can pass a plain icon name string to any component — no manual imports, no size guessing, no color math.
2395
+ **Import:** `import { Pressable } from '@retray-dev/ui-kit'`
1347
2396
 
1348
- ### Supported families
2397
+ **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.
1349
2398
 
1350
- Icons are resolved by checking each family's glyph map in priority order (first match wins):
2399
+ **Extends:** `TouchableOpacityProps` (all native props pass through except `activeOpacity`)
2400
+
2401
+ | Prop | Type | Default | Notes |
2402
+ |------|------|---------|-------|
2403
+ | children | `ReactNode` | required | Content to render inside the pressable |
2404
+ | onPress | `() => void` | — | Press handler |
2405
+ | pressScale | `number` | `0.98` | Scale value on press (MediaCard-style) |
2406
+ | bounciness | `number` | `4` | Spring bounciness on release |
2407
+ | haptics | `boolean` | `true` | Enable haptic feedback on press |
2408
+ | hoverScale | `number` | `1.02` | Hover scale (web only). Set to `1` to disable |
2409
+ | disabled | `boolean` | `false` | Disable interaction |
2410
+ | style | `ViewStyle` | — | Animated wrapper style |
2411
+
2412
+ **Behavior:**
2413
+ - Press: springs to `pressScale` (default 0.98) with `speed: 40, bounciness: 0`
2414
+ - Release: springs back to 1.0 with `speed: 40, bounciness: 4`
2415
+ - Web: optional hover scale (default 1.02)
2416
+ - Haptics: `impactLight` on press (unless `haptics={false}`)
2417
+
2418
+ **Use cases:**
2419
+ - Custom card layouts
2420
+ - Complex pressable rows that need consistent feel
2421
+ - Non-standard interactive elements that aren't buttons
2422
+ - Wrapping groups of elements as a single pressable unit
2423
+
2424
+ **Examples:**
2425
+ ```tsx
2426
+ // Custom card
2427
+ <Pressable onPress={() => navigate('detail')} style={{ padding: 16, borderRadius: 12, backgroundColor: colors.card }}>
2428
+ <Text variant="title-md">Custom Card</Text>
2429
+ <Text variant="body-sm">Press me for beautiful bounce effect</Text>
2430
+ </Pressable>
2431
+
2432
+ // Wrapping complex layout
2433
+ <Pressable onPress={handleSelect} pressScale={0.96} bounciness={8}>
2434
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
2435
+ <Avatar src={user.avatar} size="md" />
2436
+ <View style={{ flex: 1 }}>
2437
+ <Text variant="title-sm">{user.name}</Text>
2438
+ <Text variant="caption-sm">{user.role}</Text>
2439
+ </View>
2440
+ <Icon name="chevron-right" size={20} color={colors.foregroundMuted} />
2441
+ </View>
2442
+ </Pressable>
2443
+
2444
+ // Disable haptics
2445
+ <Pressable onPress={handleQuickAction} haptics={false}>
2446
+ {/* content */}
2447
+ </Pressable>
2448
+
2449
+ // Custom press scale (deeper press)
2450
+ <Pressable onPress={handlePress} pressScale={0.92} bounciness={6}>
2451
+ {/* content */}
2452
+ </Pressable>
2453
+ ```
2454
+
2455
+ ---
2456
+
2457
+ ## Icon System
2458
+
2459
+ The library ships a built-in icon resolver — pass any icon name string to components without manual imports.
2460
+
2461
+ ### Supported families (priority order — first match wins)
1351
2462
 
1352
2463
  | Priority | Family | Best for |
1353
2464
  |---|---|---|
1354
2465
  | 1 (highest) | `Feather` | Clean line icons, UI essentials |
1355
2466
  | 2 | `AntDesign` | Semantic UI icons |
1356
2467
  | 3 | `Entypo` | Social, media, navigation icons |
1357
- | 4 | `FontAwesome5` | Wide coverage of named icons |
2468
+ | 4 | `FontAwesome5` | Wide coverage |
1358
2469
  | 5 | `MaterialIcons` | Material-style icons |
1359
2470
  | 6 (lowest) | `Ionicons` | Fallback |
1360
2471
 
@@ -1365,87 +2476,119 @@ Browse all available icons: **https://icons.expo.fyi**
1365
2476
  ```tsx
1366
2477
  import { Icon } from '@retray-dev/ui-kit'
1367
2478
 
1368
- <Icon name="home" size={24} color="#000" />
2479
+ <Icon name="home" size={24} color={colors.foreground} />
1369
2480
  <Icon name="star" size={20} color={colors.primary} />
1370
2481
 
1371
- // Force a specific family when the same name exists in multiple families:
2482
+ // Force a specific family when same name exists in multiple families:
1372
2483
  <Icon name="heart" size={24} color="red" family="FontAwesome5" />
1373
2484
  ```
1374
2485
 
1375
2486
  **Props:**
1376
2487
 
1377
- | Prop | Type | Required | Description |
1378
- |---|---|---|---|
1379
- | `name` | `string` | Yes | Icon name (e.g. `"home"`, `"star"`, `"arrow-right"`) |
1380
- | `size` | `number` | Yes | Icon size in points |
1381
- | `color` | `string` | Yes | Icon color |
1382
- | `family` | `IconFamily` | No | Override resolved family |
2488
+ | Prop | Type | Required | Notes |
2489
+ |------|------|----------|-------|
2490
+ | name | `string` | yes | Icon name (e.g. `"home"`, `"arrow-right"`) |
2491
+ | size | `number` | yes | Icon size in points |
2492
+ | color | `string` | yes | Icon color |
2493
+ | family | `IconFamily` | no | Force a specific family |
1383
2494
 
1384
- Returns `null` (no crash) if the icon name is not found in any family.
2495
+ Returns `null` (no crash) if name not found in any family.
1385
2496
 
1386
2497
  ### `renderIcon` helper
1387
2498
 
1388
2499
  ```tsx
1389
2500
  import { renderIcon } from '@retray-dev/ui-kit'
1390
2501
 
1391
- const node = renderIcon('check', 18, colors.primary)
2502
+ // Returns ReactNode or null
2503
+ const icon = renderIcon('check', 18, colors.primary)
1392
2504
  ```
1393
2505
 
1394
2506
  ### `iconName` props on components
1395
2507
 
1396
- Each component that accepts icon slots now has a corresponding `iconName` prop. Pass the icon name as a string and the library resolves size and color automatically.
2508
+ All components with icon slots accept `iconName` auto-resolved size and color:
2509
+
2510
+ | Component | Prop(s) | Slot | Default color |
2511
+ |-----------|---------|------|---------------|
2512
+ | `Button` | `iconName`, `iconColor` | Left or right of label | Variant label color |
2513
+ | `IconButton` | `iconName`, `iconColor` | Center | Variant foreground |
2514
+ | `Input` | `prefixIcon`, `prefixIconColor` | Before input | `foregroundMuted` |
2515
+ | `Input` | `suffixIcon`, `suffixIconColor` | After input | `foregroundMuted` |
2516
+ | `ListItem` | `leftIcon`, `leftIconColor` | Left 44×44 slot | `foreground` |
2517
+ | `ListItem` | `rightIcon`, `rightIconColor` | Right slot | `foregroundMuted` |
2518
+ | `Badge` | `iconName`, `iconColor` | Before label | Variant foreground |
2519
+ | `Toggle` | `iconName`, `iconColor` | When not pressed | `foregroundMuted` |
2520
+ | `Toggle` | `activeIconName`, `activeIconColor` | When pressed | `primary` |
2521
+ | `AlertBanner` | `iconName`, `iconColor` | Left of content | Variant title color |
2522
+ | `EmptyState` | `iconName`, `iconColor` | Center icon slot | `foregroundMuted` |
2523
+ | `Toast` | `iconName`, `iconColor` | Left of message | Variant text color |
2524
+ | `MediaCard` | `actionIconName` | Top-right of image | `#ffffff` |
2525
+ | `Chip` | `iconName` | Before label | Variant foreground |
2526
+
2527
+ **Precedence:** `iconName` always takes precedence over the corresponding `ReactNode` prop when both are supplied.
1397
2528
 
1398
- | Component | Prop(s) | Slot | Default size | Default color |
1399
- |---|---|---|---|---|
1400
- | `Button` | `iconName`, `iconColor` | Left or right of label | sm=16 / md=18 / lg=20 | Variant label color |
1401
- | `IconButton` | `iconName`, `iconColor` | Center (icon-only) | sm=18 / md=20 / lg=24 | Variant foreground color |
1402
- | `Input` | `prefixIcon`, `prefixIconColor` | Before input text | 20 | `mutedForeground` |
1403
- | `Input` | `suffixIcon`, `suffixIconColor` | After input text | 20 | `mutedForeground` |
1404
- | `ListItem` | `leftIcon`, `leftIconColor` | Left 44×44 slot | 24 | `foreground` |
1405
- | `ListItem` | `rightIcon`, `rightIconColor` | Right slot | 24 | `mutedForeground` |
1406
- | `Badge` | `iconName`, `iconColor` | Before label | sm=10 / md=12 / lg=14 | Variant foreground |
1407
- | `Toggle` | `iconName`, `iconColor` | When not pressed | sm=16 / md=18 / lg=20 | `mutedForeground` |
1408
- | `Toggle` | `activeIconName`, `activeIconColor` | When pressed | sm=16 / md=18 / lg=20 | `primary` |
1409
- | `AlertBanner` | `iconName`, `iconColor` | Left of content | 18 | Variant title color |
1410
- | `EmptyState` | `iconName`, `iconColor` | Center icon slot | default=48 / compact=32 | `mutedForeground` |
1411
- | `Toast` | `iconName`, `iconColor` | Left of message | 22 | Variant text color |
1412
-
1413
- **Precedence:** `iconName` takes precedence over the corresponding ReactNode prop (`icon`, `prefix`, `suffix`, etc.) when both are supplied.
2529
+ ---
1414
2530
 
1415
- **Backward compatibility:** All existing `icon`, `prefix`, `suffix`, `leftRender`, `rightRender` ReactNode props continue to work exactly as before.
2531
+ ## Hover Support (Web)
1416
2532
 
1417
- **Example — Button with icon:**
1418
2533
  ```tsx
1419
- <Button label="Continue" iconName="arrow-right" iconPosition="right" />
1420
- <Button label="Delete" variant="destructive" iconName="trash-2" />
1421
- ```
2534
+ import { useHover } from '@retray-dev/ui-kit'
1422
2535
 
1423
- **Example Input with icons:**
1424
- ```tsx
1425
- <Input placeholder="Search..." prefixIcon="search" />
1426
- <Input placeholder="Amount" prefixIcon="dollar-sign" suffixIcon="check" />
1427
- ```
2536
+ function MyHoverableComponent() {
2537
+ const { hovered, hoverHandlers } = useHover()
1428
2538
 
1429
- **Example — ListItem with icons:**
1430
- ```tsx
1431
- <ListItem title="Profile" leftIcon="user" rightIcon="chevron-right" />
1432
- <ListItem title="Notifications" leftIcon="bell" subtitle="3 unread" showChevron />
2539
+ return (
2540
+ <View
2541
+ {...hoverHandlers}
2542
+ style={[
2543
+ styles.container,
2544
+ hovered && { backgroundColor: colors.surfaceStrong }
2545
+ ]}
2546
+ />
2547
+ )
2548
+ }
1433
2549
  ```
1434
2550
 
1435
- **Example Badge with icon:**
1436
- ```tsx
1437
- <Badge label="Active" variant="default" iconName="check" size="sm" />
1438
- <Badge label="Error" variant="destructive" iconName="alert-circle" />
1439
- ```
2551
+ - **Web:** Returns `{ hovered: boolean, hoverHandlers: { onMouseEnter, onMouseLeave } }`
2552
+ - **Native:** Always returns `{ hovered: false, hoverHandlers: {} }` — no-op, no crashes
1440
2553
 
1441
- **Example Toast with icon:**
1442
- ```tsx
1443
- const { toast } = useToast()
1444
- toast({ title: "Saved", variant: "success", iconName: "check-circle" })
1445
- toast({ title: "Oops", variant: "destructive", iconName: "x-circle" })
1446
- ```
2554
+ Built-in web hover: `MediaCard` (shadow lift), interactive components use this internally.
1447
2555
 
1448
- **Example — EmptyState with icon:**
1449
- ```tsx
1450
- <EmptyState iconName="inbox" title="No messages" description="You're all caught up." />
1451
- ```
2556
+ ---
2557
+
2558
+ ## Design System Conventions
2559
+
2560
+ ### Touch targets
2561
+ All interactive elements maintain ≥44pt touch height per Apple HIG:
2562
+ - Button md: 48px, sm: 40px, lg: 56px
2563
+ - Input / Textarea: 56px (14px vertical padding)
2564
+ - IconButton md: 44px, lg: 52px
2565
+ - Checkbox: 24×24px box
2566
+ - Switch: 30px track height
2567
+ - MonthPicker arrows: 44×44px
2568
+
2569
+ ### Haptic patterns
2570
+ - `impactLight` — Button, Card (press), Sheet open, Toast, MediaCard action
2571
+ - `selectionAsync` — Checkbox, Switch, Toggle, RadioGroup, Select, Slider steps, Accordion, ListItem, Chip, CategoryStrip, Tabs, MonthPicker
2572
+ - `notificationSuccess` — Toast `success`
2573
+ - `notificationError` — Toast `destructive`, Toast `warning`
2574
+
2575
+ ### Animation press scales
2576
+ - `Button` → 0.95 (strong spring feedback)
2577
+ - `Card` → 0.98 (subtle, appropriate for large surfaces)
2578
+ - `ListItem` → 0.97 (between — medium row targets)
2579
+ - `MediaCard` → 0.98 (large surface)
2580
+ - `IconButton` → 0.95
2581
+ - `Chip`, `CategoryStrip chip` → 0.95
2582
+
2583
+ ### Platform differences
2584
+ - `useNativeDriver` — `Platform.OS !== 'web'` everywhere (native driver disabled on web)
2585
+ - `Select` — wheel modal on iOS, dialog on Android, `<select>` on web
2586
+ - `Toast` — full width on mobile, 400px centered on web
2587
+ - Hover states — web only via `useHover()`
2588
+ - `Skeleton` shimmer highlight — adapts opacity for light/dark mode
2589
+
2590
+ ### Dynamic Type
2591
+ All `Text` and `TextInput` components have `allowFontScaling={true}` — respects user font size accessibility settings.
2592
+
2593
+ ### Scaling utilities (internal)
2594
+ Used internally — not exported. `s()` horizontal scale, `vs()` vertical scale, `ms()` moderate scale relative to 350×680 base.