@retray-dev/ui-kit 4.0.0 → 5.1.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 +1791 -663
  2. package/README.md +4 -3
  3. package/dist/index.d.mts +268 -83
  4. package/dist/index.d.ts +268 -83
  5. package/dist/index.js +1032 -309
  6. package/dist/index.mjs +1029 -311
  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 +3 -3
  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 +2 -2
  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.0.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:**
466
+ ```tsx
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):**
315
493
  ```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" />
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.
341
523
 
342
- **Variants:** Same color logic as `Button` `primary`, `secondary`, `outline`, `ghost`, `destructive`.
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 |
343
531
 
344
- **Animations:** Scale springs to 0.95 on `onPressIn`, back to 1.0 on `onPressOut`. `impactAsync(Light)` haptic on press.
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
544
+
545
+ **Animations:** Scale springs to 0.95 on press.
546
+
547
+ **Haptics:** `impactLight` on press.
345
548
 
346
- **Example:**
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.
1047
+
1048
+ **Badge:** Positioned top-left 8px from edges pass any ReactNode (Badge component recommended).
1049
+
1050
+ **Web hover:** Lifts shadow from `SHADOWS.sm` to `SHADOWS.md` on hover.
1051
+
1052
+ **Press animation:** Scale springs to 0.98.
1053
+
1054
+ **Haptics:** `impactLight` on `onActionPress`.
1055
+
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
+ />
1085
+
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
+ ```
562
1100
 
563
- **Example:**
1101
+ **Composition — listing grid:**
564
1102
  ```tsx
565
- <Skeleton height={20} width="60%" />
566
- <Skeleton height={80} borderRadius={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>
567
1117
  ```
568
1118
 
569
1119
  ---
570
1120
 
571
- ### Progress
1121
+ ### Separator
572
1122
 
573
- **Import:** `import { Progress } from '@retray-dev/ui-kit'`
574
- **When to use:** Show completion percentage for a task or upload.
1123
+ **Import:** `import { Separator } from '@retray-dev/ui-kit'`
1124
+
1125
+ **When to use:** Visual divider between sections, list items, or columns.
575
1126
 
576
1127
  | Prop | Type | Default | Notes |
577
1128
  |------|------|---------|-------|
578
- | value | `number` | `0` | Current value |
579
- | max | `number` | `100` | Maximum value |
1129
+ | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction of the line |
580
1130
  | style | `ViewStyle` | — | — |
581
1131
 
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: '%'`).
1132
+ **Styling:** 1px thickness, `border` token color.
583
1133
 
584
- **Example:**
1134
+ **Examples:**
585
1135
  ```tsx
586
- <Progress value={60} />
587
- <Progress value={3} max={10} />
1136
+ // Section divider
1137
+ <Separator />
1138
+
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>
1145
+
1146
+ // With custom spacing
1147
+ <Separator style={{ marginVertical: SPACING.lg }} />
588
1148
  ```
589
1149
 
590
1150
  ---
591
1151
 
592
- ### CurrencyDisplay
1152
+ ### Spinner
593
1153
 
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.
1154
+ **Import:** `import { Spinner } from '@retray-dev/ui-kit'`
1155
+
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)
596
1159
 
597
1160
  | Prop | Type | Default | Notes |
598
1161
  |------|------|---------|-------|
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 |
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 |
604
1165
 
605
- **Example:**
1166
+ **Examples:**
606
1167
  ```tsx
607
- <CurrencyDisplay value={25000} />
608
- // $25.000
609
-
610
- <CurrencyDisplay value={25000000.5} showDecimals />
611
- // → $25.000.000,50
1168
+ <Spinner />
1169
+ <Spinner size="lg" />
1170
+ <Spinner size="lg" label="Loading..." />
1171
+ <Spinner color={colors.foregroundMuted} />
612
1172
 
613
- <CurrencyDisplay value={1500} prefix="€" />
614
- // 1.500
1173
+ // Full-screen loading overlay
1174
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
1175
+ <Spinner size="lg" label="Fetching data..." />
1176
+ </View>
615
1177
  ```
616
1178
 
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
1179
  ---
623
1180
 
624
- ### CurrencyInputLarge
1181
+ ### Skeleton
1182
+
1183
+ **Import:** `import { Skeleton } from '@retray-dev/ui-kit'`
625
1184
 
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.
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.
628
1186
 
629
1187
  | Prop | Type | Default | Notes |
630
1188
  |------|------|---------|-------|
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 |
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 |
1194
+ | style | `ViewStyle` | | |
642
1195
 
643
- **Example:**
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)
1200
+
1201
+ **Animation:** Shimmer sweep loops every 1200ms. Highlight color adapts to light/dark mode.
1202
+
1203
+ **Examples:**
644
1204
  ```tsx
645
- const [display, setDisplay] = useState('')
646
- const [amount, setAmount] = useState(0)
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
+ ```
647
1223
 
648
- <CurrencyInputLarge
649
- label="Amount"
650
- value={display}
651
- onChangeText={setDisplay}
652
- onChangeValue={setAmount}
653
- />
654
- // Typing "25000" produces "$25.000"
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>
655
1233
  ```
656
1234
 
657
1235
  ---
658
1236
 
659
- ### Card
1237
+ ### Progress
660
1238
 
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.
1239
+ **Import:** `import { Progress } from '@retray-dev/ui-kit'`
663
1240
 
664
- **`Card` Props:**
1241
+ **When to use:** Show completion percentage for tasks, uploads, onboarding steps, multi-step flows.
665
1242
 
666
1243
  | Prop | Type | Default | Notes |
667
1244
  |------|------|---------|-------|
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` | | |
671
- | style | `ViewStyle` | — | |
672
-
673
- All sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`) accept a `style` prop for overrides.
674
-
675
- **Example:**
676
- ```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>
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 |
689
1249
 
690
- // Pressable card
691
- <Card variant="outlined" onPress={() => navigate('detail')}>
692
- <CardContent>
693
- <Text>Tap me</Text>
694
- </CardContent>
695
- </Card>
696
- ```
1250
+ **Variants indicator color:**
1251
+ - `default` `primary` token
1252
+ - `success` → `success` token
1253
+ - `warning` → `warning` token
1254
+ - `destructive` → `destructive` token
697
1255
 
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
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,81 @@ 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
+
1832
+ **Features:**
1833
+ - `enableDynamicSizing` — height auto-fits content, no `snapPoints` needed
1834
+ - `enablePanDownToClose` — swipe down to dismiss
1835
+ - Backdrop press dismisses
1836
+
1837
+ **Styling:** `RADIUS.lg = 20px` top corners, 16px horizontal + 20px bottom padding.
1045
1838
 
1046
- **Example:**
1839
+ **Haptics:** `impactLight` on open.
1840
+
1841
+ **Examples:**
1047
1842
  ```tsx
1048
- <Sheet open={open} onClose={() => setOpen(false)} title="Filters">
1049
- <RadioGroup options={sortOptions} value={sort} onValueChange={setSort} />
1843
+ const [open, setOpen] = useState(false)
1844
+
1845
+ <Sheet open={open} onClose={() => setOpen(false)} title="Sort by">
1846
+ <RadioGroup
1847
+ options={[
1848
+ { label: 'Newest', value: 'newest' },
1849
+ { label: 'Price: Low to High', value: 'price_asc' },
1850
+ { label: 'Price: High to Low', value: 'price_desc' },
1851
+ { label: 'Rating', value: 'rating' },
1852
+ ]}
1853
+ value={sort}
1854
+ onValueChange={(v) => { setSort(v); setOpen(false) }}
1855
+ />
1050
1856
  </Sheet>
1051
- ```
1052
1857
 
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.
1858
+ // Filter sheet
1859
+ <Sheet open={filterOpen} onClose={() => setFilterOpen(false)} title="Filters">
1860
+ <View style={{ gap: SPACING.lg }}>
1861
+ <View>
1862
+ <Text variant="title-sm">Price range</Text>
1863
+ <Slider value={maxPrice} minimumValue={0} maximumValue={1000} step={50} onValueChange={setMaxPrice} showValue formatValue={(v) => `$${v}`} />
1864
+ </View>
1865
+ <Separator />
1866
+ <View>
1867
+ <Text variant="title-sm">Category</Text>
1868
+ <ChipGroup options={categoryOptions} value={selectedCategory} onValueChange={setSelectedCategory} />
1869
+ </View>
1870
+ <Button label="Apply filters" fullWidth onPress={() => { applyFilters(); setFilterOpen(false) }} />
1871
+ </View>
1872
+ </Sheet>
1873
+ ```
1057
1874
 
1058
1875
  ---
1059
1876
 
1060
1877
  ### Toast / useToast
1061
1878
 
1062
1879
  **Import:** `import { ToastProvider, useToast } from '@retray-dev/ui-kit'`
1063
- **When to use:** Ephemeral feedback messages (save success, network error, copy confirmation).
1064
1880
 
1065
- **Required setup**`ToastProvider` must wrap your app inside `SafeAreaProvider` (see Setup section above).
1881
+ **When to use:** Ephemeral feedback messages save confirmations, errors, copy notifications, background process updates. Auto-dismiss after duration.
1066
1882
 
1067
- **Peer dependency:** `react-native-safe-area-context` required for `useSafeAreaInsets` inside `ToastProvider`.
1883
+ **Required setup:** `ToastProvider` must wrap app inside `SafeAreaProvider`. See Setup section above.
1068
1884
 
1885
+ **Usage:**
1069
1886
  ```tsx
1070
- import { useToast } from '@retray-dev/ui-kit'
1071
-
1072
1887
  function MyComponent() {
1073
1888
  const { toast, dismiss } = useToast()
1074
1889
 
@@ -1077,10 +1892,7 @@ function MyComponent() {
1077
1892
  label="Save"
1078
1893
  onPress={async () => {
1079
1894
  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' })
1895
+ toast({ title: 'Saved', variant: 'success' })
1084
1896
  }}
1085
1897
  />
1086
1898
  )
@@ -1092,90 +1904,223 @@ function MyComponent() {
1092
1904
  | Field | Type | Default | Notes |
1093
1905
  |-------|------|---------|-------|
1094
1906
  | 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 |
1907
+ | description | `string` | — | Detail text below title |
1908
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | Background and icon color |
1909
+ | duration | `number` (ms) | `3000` | Auto-dismiss delay. Pass `Infinity` to prevent auto-dismiss |
1910
+ | icon | `ReactNode` | — | Custom icon. Defaults to variant symbol |
1099
1911
  | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
1100
- | iconColor | `string` | — | Override icon color. Defaults to variant text color |
1912
+ | iconColor | `string` | — | Override icon color |
1913
+ | action | `{ label: string, onPress: () => void }` | — | Optional action button beside dismiss |
1914
+
1915
+ **`dismiss(id)`:** Dismiss a specific toast programmatically. `id` is returned by the `toast()` call.
1916
+
1917
+ **Variant details:**
1918
+ - `default` — dark/primary background, `impactLight` haptic
1919
+ - `success` — `success` token background, `notificationSuccess` haptic
1920
+ - `destructive` — `destructive` token background, `notificationError` haptic
1921
+ - `warning` — `warning` token background, `notificationError` haptic
1922
+
1923
+ **Behavior:**
1924
+ - Max 3 toasts shown simultaneously — oldest removed when 4th arrives
1925
+ - Appear at top of screen below status bar (dynamic safe area inset)
1926
+ - Swipe left or right to dismiss early (threshold: 80px or 800pt/s velocity)
1927
+ - **Web:** 400px max width, centered
1928
+
1929
+ **Animation:** Entrance: `withTiming(120ms, Easing.out(Easing.exp))` slide-down + opacity. Exit: `withTiming(200ms)` slide-up + opacity fade.
1930
+
1931
+ **Examples:**
1932
+ ```tsx
1933
+ const { toast, dismiss } = useToast()
1934
+
1935
+ // Basic variants
1936
+ toast({ title: 'Saved', variant: 'success' })
1937
+ toast({ title: 'Connection error', variant: 'destructive' })
1938
+ toast({ title: 'Check your email', variant: 'warning' })
1939
+ toast({ title: 'Link copied' })
1940
+
1941
+ // With description
1942
+ toast({
1943
+ title: 'Payment sent',
1944
+ description: '$250 sent to John Doe',
1945
+ variant: 'success',
1946
+ iconName: 'check-circle',
1947
+ })
1948
+
1949
+ // With action button
1950
+ toast({
1951
+ title: 'Message deleted',
1952
+ action: { label: 'Undo', onPress: () => restoreMessage() },
1953
+ })
1954
+
1955
+ // Custom duration (longer)
1956
+ toast({ title: 'Processing your request...', duration: 8000 })
1957
+
1958
+ // Programmatic dismiss
1959
+ const id = toast({ title: 'Uploading...', duration: Infinity })
1960
+ await upload()
1961
+ dismiss(id)
1962
+ toast({ title: 'Upload complete', variant: 'success' })
1963
+ ```
1101
1964
 
1102
- **`dismiss(id)`:** Dismiss a toast programmatically. The `id` is returned by the `toast()` call — store it if you need programmatic dismissal.
1965
+ ---
1966
+
1967
+ ### ConfirmDialog
1968
+
1969
+ **Import:** `import { ConfirmDialog } from '@retray-dev/ui-kit'`
1970
+
1971
+ **When to use:** Confirmation prompts before irreversible or destructive actions — delete, send money, discard changes. Always confirm before actions that can't be undone.
1972
+
1973
+ **Requires:** `BottomSheetModalProvider` at app root (same as `Sheet`).
1974
+
1975
+ | Prop | Type | Default | Notes |
1976
+ |------|------|---------|-------|
1977
+ | visible | `boolean` | required | Controls dialog visibility |
1978
+ | title | `string` | required | Dialog heading |
1979
+ | description | `string` | — | Supporting text — describe what exactly will happen |
1980
+ | confirmLabel | `string` | `'Confirm'` | Confirm button text |
1981
+ | cancelLabel | `string` | `'Cancel'` | Cancel button text |
1982
+ | confirmVariant | `'primary' \| 'destructive'` | `'primary'` | Use `'destructive'` for delete/remove actions |
1983
+ | onConfirm | `() => void` | required | Called when confirm is tapped |
1984
+ | onCancel | `() => void` | required | Called when cancel is tapped or backdrop pressed |
1103
1985
 
1104
1986
  **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)
1987
+ - Powered by `@gorhom/bottom-sheet` with `enableDynamicSizing`
1988
+ - Buttons are full-width, stacked vertically (confirm on top)
1989
+ - Cancel shows X icon, confirm shows check (primary) or trash-2 (destructive) icon
1990
+ - Swipe down or backdrop press calls `onCancel`
1991
+
1992
+ **Haptics:** `impactLight` on open.
1993
+
1994
+ **Examples:**
1995
+ ```tsx
1996
+ // Delete confirmation
1997
+ <ConfirmDialog
1998
+ visible={showDelete}
1999
+ title="Delete transaction?"
2000
+ description="$45.000 · Mercado · 12 mar · This action cannot be undone."
2001
+ confirmLabel="Delete"
2002
+ confirmVariant="destructive"
2003
+ onConfirm={handleDelete}
2004
+ onCancel={() => setShowDelete(false)}
2005
+ />
2006
+
2007
+ // Send money confirmation
2008
+ <ConfirmDialog
2009
+ visible={showConfirm}
2010
+ title="Send payment?"
2011
+ description={`Send $${amount} to ${recipientName}`}
2012
+ confirmLabel="Send"
2013
+ onConfirm={handleSend}
2014
+ onCancel={() => setShowConfirm(false)}
2015
+ />
2016
+
2017
+ // Discard changes
2018
+ <ConfirmDialog
2019
+ visible={showDiscard}
2020
+ title="Discard changes?"
2021
+ description="All unsaved changes will be lost."
2022
+ confirmLabel="Discard"
2023
+ confirmVariant="destructive"
2024
+ onConfirm={() => { setShowDiscard(false); goBack() }}
2025
+ onCancel={() => setShowDiscard(false)}
2026
+ />
2027
+ ```
1111
2028
 
1112
2029
  ---
1113
2030
 
1114
2031
  ### ListItem
1115
2032
 
1116
2033
  **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.
2034
+
2035
+ **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
2036
 
1119
2037
  | Prop | Type | Default | Notes |
1120
2038
  |------|------|---------|-------|
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 |
2039
+ | title | `string` | required | Primary text |
2040
+ | subtitle | `string` | — | Secondary line below title |
2041
+ | caption | `string` | — | Tertiary line below subtitle |
2042
+ | leftRender | `ReactNode` | — | Content in the fixed 44×44pt left slot |
2043
+ | rightRender | `string \| ReactNode` | — | Content on the right edge. Strings auto-styled as muted text |
2044
+ | leftIcon | `string` | — | Icon name for left slot. Takes precedence over `leftRender` |
2045
+ | rightIcon | `string` | — | Icon name for right slot. Takes precedence over `rightRender` |
2046
+ | leftIconColor | `string` | — | Override left icon color (default: `foreground`) |
2047
+ | rightIconColor | `string` | — | Override right icon color (default: `foregroundMuted`) |
2048
+ | variant | `'plain' \| 'card'` | `'plain'` | `plain`: no background. `card`: surface with border and shadow |
2049
+ | showChevron | `boolean` | `false` | Right-pointing chevron. Ignored when `rightRender` is set |
2050
+ | showSeparator | `boolean` | `false` | Hairline separator at bottom. Useful for stacking plain items |
2051
+ | onPress | `() => void` | — | Makes row pressable (scale animation + haptics) |
1134
2052
  | 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 |
2053
+ | style | `ViewStyle` | — | Outer container style |
2054
+ | titleStyle | `TextStyle` | — | Override title text style |
2055
+ | subtitleStyle | `TextStyle` | — | Override subtitle text style |
2056
+ | captionStyle | `TextStyle` | — | Override caption text style |
1139
2057
  | icon | `ReactNode` | — | **Deprecated** — use `leftRender` |
1140
2058
  | trailing | `string \| ReactNode` | — | **Deprecated** — use `rightRender` |
1141
2059
 
1142
- **Animation:** Scale springs to 0.97 on press-in, back to 1.0 on press-out (only when `onPress` is provided).
2060
+ **Slots:**
2061
+ - `leftRender` / `leftIcon` — 44×44pt fixed container, centered. Good for Avatar, icons, thumbnails
2062
+ - `rightRender` / `rightIcon` — max 160pt wide, right-aligned. Good for Badge, price text, Switch
2063
+ - `showChevron` — `›` chevron, 24pt, `foregroundMuted` color. Only shows when `rightRender` is absent
2064
+
2065
+ **Separator inset:** Aligns to text block — `marginLeft` adjusts to skip over left slot when present.
2066
+
2067
+ **Animation:** Scale springs to 0.97 on press (subtler than Button's 0.95).
1143
2068
 
1144
- **Example:**
2069
+ **Haptics:** `selectionAsync` on press.
2070
+
2071
+ **Examples:**
1145
2072
  ```tsx
1146
- // Simple row with chevron
2073
+ // Simple settings row
2074
+ <ListItem title="Profile" showChevron onPress={() => navigate('profile')} />
2075
+
2076
+ // With subtitle and chevron
1147
2077
  <ListItem
1148
- title="Profile"
1149
- subtitle="Manage your account"
2078
+ title="Notifications"
2079
+ subtitle="Push, email, and SMS"
1150
2080
  showChevron
1151
- onPress={() => navigate('profile')}
2081
+ onPress={() => navigate('notifications')}
1152
2082
  />
1153
2083
 
1154
- // Rich row: avatar + title/subtitle/caption + price + badge
2084
+ // Icon left slot + chevron
2085
+ <ListItem title="Profile" leftIcon="user" showChevron onPress={() => navigate('profile')} />
2086
+
2087
+ // Icon left + badge right
1155
2088
  <ListItem
1156
- leftRender={<Avatar src={item.avatar} fallback={item.initials} size="md" />}
1157
- title={item.name}
1158
- subtitle={item.date}
1159
- caption={item.category}
2089
+ leftIcon="bell"
2090
+ title="Notifications"
2091
+ subtitle="3 unread"
2092
+ rightRender={<Badge label="3" variant="destructive" size="sm" />}
2093
+ onPress={() => navigate('notifications')}
2094
+ />
2095
+
2096
+ // Avatar + full text stack + right content
2097
+ <ListItem
2098
+ leftRender={<Avatar src={user.avatar} fallback={user.initials} status="online" />}
2099
+ title={user.name}
2100
+ subtitle={user.role}
2101
+ caption="Active 2 min ago"
1160
2102
  rightRender={
1161
2103
  <View style={{ alignItems: 'flex-end', gap: 4 }}>
1162
- <Text variant="label">${item.amount}</Text>
1163
- <Badge label={item.status} variant="secondary" size="sm" />
2104
+ <Text variant="title-sm">${amount}</Text>
2105
+ <Badge label={status} variant="warningOutline" size="sm" />
1164
2106
  </View>
1165
2107
  }
1166
- onPress={() => navigate('detail', { id: item.id })}
2108
+ onPress={() => navigate('user', { id: user.id })}
1167
2109
  />
1168
2110
 
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 />
2111
+ // Switch in right slot
2112
+ <ListItem
2113
+ leftIcon="moon"
2114
+ title="Dark mode"
2115
+ rightRender={<Switch checked={darkMode} onCheckedChange={setDarkMode} />}
2116
+ />
1172
2117
 
1173
2118
  // Card variant (standalone surface)
1174
2119
  <ListItem
1175
2120
  variant="card"
1176
2121
  leftIcon="credit-card"
1177
- title="Balance"
1178
- subtitle="Available funds"
2122
+ title="Checking account"
2123
+ subtitle="Available balance"
1179
2124
  rightRender="$12.500"
1180
2125
  />
1181
2126
 
@@ -1183,114 +2128,173 @@ function MyComponent() {
1183
2128
  {transactions.map((t, i) => (
1184
2129
  <ListItem
1185
2130
  key={t.id}
1186
- title={t.name}
2131
+ leftRender={<Avatar fallback={t.merchant[0]} size="sm" />}
2132
+ title={t.merchant}
1187
2133
  subtitle={t.date}
1188
- rightRender={t.amount}
2134
+ rightRender={
2135
+ <Text variant="title-sm" color={t.amount < 0 ? colors.destructive : colors.success}>
2136
+ {t.amount < 0 ? '-' : '+'}${Math.abs(t.amount)}
2137
+ </Text>
2138
+ }
1189
2139
  showSeparator={i < transactions.length - 1}
1190
- onPress={() => navigate('detail', { id: t.id })}
2140
+ onPress={() => navigate('transaction', { id: t.id })}
1191
2141
  />
1192
2142
  ))}
1193
2143
  ```
1194
2144
 
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
2145
  ---
1201
2146
 
1202
2147
  ### Chip / ChipGroup
1203
2148
 
1204
2149
  **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.
2150
+
2151
+ **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
2152
 
1207
2153
  **`Chip` Props:**
1208
2154
 
1209
2155
  | Prop | Type | Default | Notes |
1210
2156
  |------|------|---------|-------|
1211
2157
  | label | `string` | required | — |
1212
- | selected | `boolean` | `false` | Controls fill color |
2158
+ | selected | `boolean` | `false` | Controls fill style |
1213
2159
  | onPress | `() => void` | — | — |
1214
- | icon | `ReactNode` | — | Custom icon rendered before the label |
1215
- | iconName | `string` | — | Icon name from `@expo/vector-icons`. Takes precedence over `icon` |
2160
+ | icon | `ReactNode` | — | Custom icon before label |
2161
+ | iconName | `string` | — | Icon name. Takes precedence over `icon` |
1216
2162
  | style | `ViewStyle` | — | — |
1217
2163
 
1218
2164
  **`ChipGroup` Props:**
1219
2165
 
1220
2166
  | Prop | Type | Default | Notes |
1221
2167
  |------|------|---------|-------|
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 |
2168
+ | options | `ChipOption[]` | required | `{ label: string, value: string \| number }` |
2169
+ | value | `string \| number \| (string \| number)[]` | — | Selected value(s) |
2170
+ | onValueChange | `(value: ...) => void` | — | Returns single value or array depending on `multiSelect` |
2171
+ | multiSelect | `boolean` | `false` | Allow multiple chips selected simultaneously |
2172
+ | style | `ViewStyle` | — | Wrapping row style |
2173
+
2174
+ **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
2175
 
1228
- **Animation:** Background, text, and border colors animate via `Animated.timing` (150ms) between unselected (`secondary`/`border`/`foreground`) and selected (`primary`/`primary`/`primaryForeground`) states.
2176
+ **Animation:** Spring scale 0.95 on press; background/text/border color transitions (150ms timing) on selection change.
1229
2177
 
1230
- **Example:**
2178
+ **Haptics:** `selectionAsync` on change.
2179
+
2180
+ **Examples:**
1231
2181
  ```tsx
1232
- // Single select
2182
+ // Single select percentage
1233
2183
  const [pct, setPct] = useState(50)
1234
-
1235
2184
  <ChipGroup
1236
- options={[
1237
- { label: '40%', value: 40 },
1238
- { label: '50%', value: 50 },
1239
- { label: '100%', value: 100 },
1240
- ]}
2185
+ options={[{ label: '25%', value: 25 }, { label: '50%', value: 50 }, { label: '100%', value: 100 }]}
1241
2186
  value={pct}
1242
2187
  onValueChange={setPct}
1243
2188
  />
1244
2189
 
1245
- // Multi select
2190
+ // Multi select categories
1246
2191
  const [categories, setCategories] = useState<number[]>([1, 3])
1247
-
1248
2192
  <ChipGroup
1249
2193
  multiSelect
1250
2194
  options={[
1251
2195
  { label: 'Food', value: 1 },
1252
2196
  { label: 'Transport', value: 2 },
1253
2197
  { label: 'Entertainment', value: 3 },
2198
+ { label: 'Health', value: 4 },
1254
2199
  ]}
1255
2200
  value={categories}
1256
2201
  onValueChange={setCategories}
1257
2202
  />
2203
+
2204
+ // Standalone chips (custom logic)
2205
+ <View style={{ flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap' }}>
2206
+ {filters.map((f) => (
2207
+ <Chip
2208
+ key={f.value}
2209
+ label={f.label}
2210
+ selected={activeFilter === f.value}
2211
+ onPress={() => setActiveFilter(f.value)}
2212
+ />
2213
+ ))}
2214
+ </View>
1258
2215
  ```
1259
2216
 
1260
2217
  ---
1261
2218
 
1262
- ### ConfirmDialog
2219
+ ### CategoryStrip
1263
2220
 
1264
- **Import:** `import { ConfirmDialog } from '@retray-dev/ui-kit'`
1265
- **When to use:** Confirmation prompts before irreversible or destructive actions (delete, send, discard).
2221
+ **Import:** `import { CategoryStrip } from '@retray-dev/ui-kit'`
2222
+
2223
+ **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
2224
 
1267
2225
  | Prop | Type | Default | Notes |
1268
2226
  |------|------|---------|-------|
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 |
2227
+ | categories | `CategoryItem[]` | required | Category definitions |
2228
+ | value | `string \| string[]` | | Selected category value(s) |
2229
+ | onValueChange | `(value: string \| string[]) => void` | — | Called with new selection |
2230
+ | multiSelect | `boolean` | `false` | Allow multiple simultaneous selections |
2231
+ | style | `ViewStyle` | | ScrollView content container style |
2232
+ | itemStyle | `ViewStyle` | | Style applied to each chip's wrapper |
2233
+
2234
+ **CategoryItem type:**
2235
+ ```ts
2236
+ {
2237
+ label: string
2238
+ value: string
2239
+ icon?: ReactNode | string // Icon or icon name (16px, auto-colored)
2240
+ badge?: number // Count badge on chip, capped at 99
2241
+ }
2242
+ ```
1277
2243
 
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`).
2244
+ **Single select behavior:** Pressing selected item again deselects it (value becomes `''`).
2245
+
2246
+ **Multi select behavior:** Toggles item in/out of value array.
1283
2247
 
1284
- **Example:**
2248
+ **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.
2249
+
2250
+ **Animation:** Scale spring 0.95 on press.
2251
+
2252
+ **Haptics:** `selectionAsync` on change.
2253
+
2254
+ **Web:** Horizontal scroll with `showsHorizontalScrollIndicator={false}`.
2255
+
2256
+ **Examples:**
1285
2257
  ```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)}
2258
+ // Basic category filter
2259
+ <CategoryStrip
2260
+ categories={[
2261
+ { label: 'All', value: '' },
2262
+ { label: 'Trips', value: 'trips' },
2263
+ { label: 'Experiences', value: 'experiences' },
2264
+ { label: 'Restaurants', value: 'restaurants' },
2265
+ ]}
2266
+ value={category}
2267
+ onValueChange={setCategory}
2268
+ />
2269
+
2270
+ // With icons
2271
+ <CategoryStrip
2272
+ categories={[
2273
+ { label: 'Nearby', value: 'nearby', icon: 'map-pin' },
2274
+ { label: 'Popular', value: 'popular', icon: 'trending-up' },
2275
+ { label: 'New', value: 'new', icon: 'star' },
2276
+ ]}
2277
+ value={filter}
2278
+ onValueChange={setFilter}
2279
+ />
2280
+
2281
+ // With badge counts
2282
+ <CategoryStrip
2283
+ categories={[
2284
+ { label: 'Inbox', value: 'inbox', badge: 3 },
2285
+ { label: 'Sent', value: 'sent' },
2286
+ { label: 'Archived', value: 'archived' },
2287
+ ]}
2288
+ value={tab}
2289
+ onValueChange={setTab}
2290
+ />
2291
+
2292
+ // Multi-select
2293
+ <CategoryStrip
2294
+ multiSelect
2295
+ categories={amenities}
2296
+ value={selectedAmenities}
2297
+ onValueChange={setSelectedAmenities}
1294
2298
  />
1295
2299
  ```
1296
2300
 
@@ -1299,19 +2303,32 @@ const [categories, setCategories] = useState<number[]>([1, 3])
1299
2303
  ### LabelValue
1300
2304
 
1301
2305
  **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).
2306
+
2307
+ **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
2308
 
1304
2309
  | Prop | Type | Default | Notes |
1305
2310
  |------|------|---------|-------|
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 |
2311
+ | label | `string` | required | Caption label on the left |
2312
+ | value | `string \| ReactNode` | required | Value on the right. Strings auto-styled; pass `ReactNode` for custom content |
1308
2313
  | style | `ViewStyle` | — | — |
1309
2314
 
1310
- **Example:**
2315
+ **Styling:** Row layout, `justifyContent: 'space-between'`. Label: 13pt / Regular / `foregroundMuted`. Value: 14pt / Medium / `foreground`. 12px gap.
2316
+
2317
+ **Examples:**
1311
2318
  ```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" />} />
2319
+ <LabelValue label="Date" value="12 mar 2025" />
2320
+ <LabelValue label="Category" value="Food & drink" />
2321
+ <LabelValue label="Status" value={<Badge label="Pending" variant="warningOutline" size="sm" />} />
2322
+ <LabelValue label="Amount" value="$45.000" />
2323
+
2324
+ // Transaction detail
2325
+ <View style={{ gap: SPACING.sm }}>
2326
+ <LabelValue label="Merchant" value={transaction.merchant} />
2327
+ <LabelValue label="Date" value={formatDate(transaction.date)} />
2328
+ <LabelValue label="Category" value={transaction.category} />
2329
+ <Separator />
2330
+ <LabelValue label="Amount" value={`$${transaction.amount}`} />
2331
+ </View>
1315
2332
  ```
1316
2333
 
1317
2334
  ---
@@ -1319,42 +2336,121 @@ const [categories, setCategories] = useState<number[]>([1, 3])
1319
2336
  ### MonthPicker
1320
2337
 
1321
2338
  **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.
2339
+
2340
+ **When to use:** Month/year navigation — finance apps filtering by period, date pickers, reporting periods. Compact left/right navigation UI.
1323
2341
 
1324
2342
  | Prop | Type | Default | Notes |
1325
2343
  |------|------|---------|-------|
1326
- | value | `MonthPickerValue` | required | `{ month: number, year: number }` — `month` is 1–12 |
1327
- | onChange | `(value: MonthPickerValue) => void` | required | Called on each navigation step |
2344
+ | value | `MonthPickerValue` | required | `{ month: 1–12, year: number }` |
2345
+ | onChange | `(value: MonthPickerValue) => void` | required | Called on navigation |
1328
2346
  | style | `ViewStyle` | — | — |
1329
2347
 
1330
- **`MonthPickerValue`:** `{ month: number, year: number }` — month is 1-indexed (January = 1).
2348
+ **MonthPickerValue type:**
2349
+ ```ts
2350
+ { month: number; year: number } // month is 1-indexed (January = 1)
2351
+ ```
2352
+
2353
+ **Navigation:** Year wraps correctly at boundaries (December → January increments year, January → December decrements year). Fully controlled — no internal state.
1331
2354
 
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.
2355
+ **Styling:** Centered row with `←` / `→` buttons (44×44px each). Label: 17pt / Medium / 160px min-width, centered.
1333
2356
 
1334
- **Example:**
2357
+ **Haptics:** `selectionAsync` on navigation.
2358
+
2359
+ **Examples:**
1335
2360
  ```tsx
1336
- const [period, setPeriod] = useState({ month: new Date().getMonth() + 1, year: new Date().getFullYear() })
2361
+ const [period, setPeriod] = useState({
2362
+ month: new Date().getMonth() + 1,
2363
+ year: new Date().getFullYear(),
2364
+ })
1337
2365
 
1338
2366
  <MonthPicker value={period} onChange={setPeriod} />
1339
- // Displays: "April 2026"
2367
+ // Displays: "May 2026"
2368
+
2369
+ // In a transactions header
2370
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
2371
+ <Text variant="display-md">Transactions</Text>
2372
+ <MonthPicker value={period} onChange={setPeriod} />
2373
+ </View>
1340
2374
  ```
1341
2375
 
1342
2376
  ---
1343
2377
 
1344
- ## Icon System
2378
+ ### Pressable
1345
2379
 
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.
2380
+ **Import:** `import { Pressable } from '@retray-dev/ui-kit'`
1347
2381
 
1348
- ### Supported families
2382
+ **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
2383
 
1350
- Icons are resolved by checking each family's glyph map in priority order (first match wins):
2384
+ **Extends:** `TouchableOpacityProps` (all native props pass through except `activeOpacity`)
2385
+
2386
+ | Prop | Type | Default | Notes |
2387
+ |------|------|---------|-------|
2388
+ | children | `ReactNode` | required | Content to render inside the pressable |
2389
+ | onPress | `() => void` | — | Press handler |
2390
+ | pressScale | `number` | `0.98` | Scale value on press (MediaCard-style) |
2391
+ | bounciness | `number` | `4` | Spring bounciness on release |
2392
+ | haptics | `boolean` | `true` | Enable haptic feedback on press |
2393
+ | hoverScale | `number` | `1.02` | Hover scale (web only). Set to `1` to disable |
2394
+ | disabled | `boolean` | `false` | Disable interaction |
2395
+ | style | `ViewStyle` | — | Animated wrapper style |
2396
+
2397
+ **Behavior:**
2398
+ - Press: springs to `pressScale` (default 0.98) with `speed: 40, bounciness: 0`
2399
+ - Release: springs back to 1.0 with `speed: 40, bounciness: 4`
2400
+ - Web: optional hover scale (default 1.02)
2401
+ - Haptics: `impactLight` on press (unless `haptics={false}`)
2402
+
2403
+ **Use cases:**
2404
+ - Custom card layouts
2405
+ - Complex pressable rows that need consistent feel
2406
+ - Non-standard interactive elements that aren't buttons
2407
+ - Wrapping groups of elements as a single pressable unit
2408
+
2409
+ **Examples:**
2410
+ ```tsx
2411
+ // Custom card
2412
+ <Pressable onPress={() => navigate('detail')} style={{ padding: 16, borderRadius: 12, backgroundColor: colors.card }}>
2413
+ <Text variant="title-md">Custom Card</Text>
2414
+ <Text variant="body-sm">Press me for beautiful bounce effect</Text>
2415
+ </Pressable>
2416
+
2417
+ // Wrapping complex layout
2418
+ <Pressable onPress={handleSelect} pressScale={0.96} bounciness={8}>
2419
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
2420
+ <Avatar src={user.avatar} size="md" />
2421
+ <View style={{ flex: 1 }}>
2422
+ <Text variant="title-sm">{user.name}</Text>
2423
+ <Text variant="caption-sm">{user.role}</Text>
2424
+ </View>
2425
+ <Icon name="chevron-right" size={20} color={colors.foregroundMuted} />
2426
+ </View>
2427
+ </Pressable>
2428
+
2429
+ // Disable haptics
2430
+ <Pressable onPress={handleQuickAction} haptics={false}>
2431
+ {/* content */}
2432
+ </Pressable>
2433
+
2434
+ // Custom press scale (deeper press)
2435
+ <Pressable onPress={handlePress} pressScale={0.92} bounciness={6}>
2436
+ {/* content */}
2437
+ </Pressable>
2438
+ ```
2439
+
2440
+ ---
2441
+
2442
+ ## Icon System
2443
+
2444
+ The library ships a built-in icon resolver — pass any icon name string to components without manual imports.
2445
+
2446
+ ### Supported families (priority order — first match wins)
1351
2447
 
1352
2448
  | Priority | Family | Best for |
1353
2449
  |---|---|---|
1354
2450
  | 1 (highest) | `Feather` | Clean line icons, UI essentials |
1355
2451
  | 2 | `AntDesign` | Semantic UI icons |
1356
2452
  | 3 | `Entypo` | Social, media, navigation icons |
1357
- | 4 | `FontAwesome5` | Wide coverage of named icons |
2453
+ | 4 | `FontAwesome5` | Wide coverage |
1358
2454
  | 5 | `MaterialIcons` | Material-style icons |
1359
2455
  | 6 (lowest) | `Ionicons` | Fallback |
1360
2456
 
@@ -1365,87 +2461,119 @@ Browse all available icons: **https://icons.expo.fyi**
1365
2461
  ```tsx
1366
2462
  import { Icon } from '@retray-dev/ui-kit'
1367
2463
 
1368
- <Icon name="home" size={24} color="#000" />
2464
+ <Icon name="home" size={24} color={colors.foreground} />
1369
2465
  <Icon name="star" size={20} color={colors.primary} />
1370
2466
 
1371
- // Force a specific family when the same name exists in multiple families:
2467
+ // Force a specific family when same name exists in multiple families:
1372
2468
  <Icon name="heart" size={24} color="red" family="FontAwesome5" />
1373
2469
  ```
1374
2470
 
1375
2471
  **Props:**
1376
2472
 
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 |
2473
+ | Prop | Type | Required | Notes |
2474
+ |------|------|----------|-------|
2475
+ | name | `string` | yes | Icon name (e.g. `"home"`, `"arrow-right"`) |
2476
+ | size | `number` | yes | Icon size in points |
2477
+ | color | `string` | yes | Icon color |
2478
+ | family | `IconFamily` | no | Force a specific family |
1383
2479
 
1384
- Returns `null` (no crash) if the icon name is not found in any family.
2480
+ Returns `null` (no crash) if name not found in any family.
1385
2481
 
1386
2482
  ### `renderIcon` helper
1387
2483
 
1388
2484
  ```tsx
1389
2485
  import { renderIcon } from '@retray-dev/ui-kit'
1390
2486
 
1391
- const node = renderIcon('check', 18, colors.primary)
2487
+ // Returns ReactNode or null
2488
+ const icon = renderIcon('check', 18, colors.primary)
1392
2489
  ```
1393
2490
 
1394
2491
  ### `iconName` props on components
1395
2492
 
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.
2493
+ All components with icon slots accept `iconName` auto-resolved size and color:
2494
+
2495
+ | Component | Prop(s) | Slot | Default color |
2496
+ |-----------|---------|------|---------------|
2497
+ | `Button` | `iconName`, `iconColor` | Left or right of label | Variant label color |
2498
+ | `IconButton` | `iconName`, `iconColor` | Center | Variant foreground |
2499
+ | `Input` | `prefixIcon`, `prefixIconColor` | Before input | `foregroundMuted` |
2500
+ | `Input` | `suffixIcon`, `suffixIconColor` | After input | `foregroundMuted` |
2501
+ | `ListItem` | `leftIcon`, `leftIconColor` | Left 44×44 slot | `foreground` |
2502
+ | `ListItem` | `rightIcon`, `rightIconColor` | Right slot | `foregroundMuted` |
2503
+ | `Badge` | `iconName`, `iconColor` | Before label | Variant foreground |
2504
+ | `Toggle` | `iconName`, `iconColor` | When not pressed | `foregroundMuted` |
2505
+ | `Toggle` | `activeIconName`, `activeIconColor` | When pressed | `primary` |
2506
+ | `AlertBanner` | `iconName`, `iconColor` | Left of content | Variant title color |
2507
+ | `EmptyState` | `iconName`, `iconColor` | Center icon slot | `foregroundMuted` |
2508
+ | `Toast` | `iconName`, `iconColor` | Left of message | Variant text color |
2509
+ | `MediaCard` | `actionIconName` | Top-right of image | `#ffffff` |
2510
+ | `Chip` | `iconName` | Before label | Variant foreground |
2511
+
2512
+ **Precedence:** `iconName` always takes precedence over the corresponding `ReactNode` prop when both are supplied.
1397
2513
 
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.
2514
+ ---
1414
2515
 
1415
- **Backward compatibility:** All existing `icon`, `prefix`, `suffix`, `leftRender`, `rightRender` ReactNode props continue to work exactly as before.
2516
+ ## Hover Support (Web)
1416
2517
 
1417
- **Example — Button with icon:**
1418
2518
  ```tsx
1419
- <Button label="Continue" iconName="arrow-right" iconPosition="right" />
1420
- <Button label="Delete" variant="destructive" iconName="trash-2" />
1421
- ```
2519
+ import { useHover } from '@retray-dev/ui-kit'
1422
2520
 
1423
- **Example Input with icons:**
1424
- ```tsx
1425
- <Input placeholder="Search..." prefixIcon="search" />
1426
- <Input placeholder="Amount" prefixIcon="dollar-sign" suffixIcon="check" />
1427
- ```
2521
+ function MyHoverableComponent() {
2522
+ const { hovered, hoverHandlers } = useHover()
1428
2523
 
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 />
2524
+ return (
2525
+ <View
2526
+ {...hoverHandlers}
2527
+ style={[
2528
+ styles.container,
2529
+ hovered && { backgroundColor: colors.surfaceStrong }
2530
+ ]}
2531
+ />
2532
+ )
2533
+ }
1433
2534
  ```
1434
2535
 
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
- ```
2536
+ - **Web:** Returns `{ hovered: boolean, hoverHandlers: { onMouseEnter, onMouseLeave } }`
2537
+ - **Native:** Always returns `{ hovered: false, hoverHandlers: {} }` — no-op, no crashes
1440
2538
 
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
- ```
2539
+ Built-in web hover: `MediaCard` (shadow lift), interactive components use this internally.
1447
2540
 
1448
- **Example — EmptyState with icon:**
1449
- ```tsx
1450
- <EmptyState iconName="inbox" title="No messages" description="You're all caught up." />
1451
- ```
2541
+ ---
2542
+
2543
+ ## Design System Conventions
2544
+
2545
+ ### Touch targets
2546
+ All interactive elements maintain ≥44pt touch height per Apple HIG:
2547
+ - Button md: 48px, sm: 40px, lg: 56px
2548
+ - Input / Textarea: 56px (14px vertical padding)
2549
+ - IconButton md: 44px, lg: 52px
2550
+ - Checkbox: 24×24px box
2551
+ - Switch: 30px track height
2552
+ - MonthPicker arrows: 44×44px
2553
+
2554
+ ### Haptic patterns
2555
+ - `impactLight` — Button, Card (press), Sheet open, Toast, MediaCard action
2556
+ - `selectionAsync` — Checkbox, Switch, Toggle, RadioGroup, Select, Slider steps, Accordion, ListItem, Chip, CategoryStrip, Tabs, MonthPicker
2557
+ - `notificationSuccess` — Toast `success`
2558
+ - `notificationError` — Toast `destructive`, Toast `warning`
2559
+
2560
+ ### Animation press scales
2561
+ - `Button` → 0.95 (strong spring feedback)
2562
+ - `Card` → 0.98 (subtle, appropriate for large surfaces)
2563
+ - `ListItem` → 0.97 (between — medium row targets)
2564
+ - `MediaCard` → 0.98 (large surface)
2565
+ - `IconButton` → 0.95
2566
+ - `Chip`, `CategoryStrip chip` → 0.95
2567
+
2568
+ ### Platform differences
2569
+ - `useNativeDriver` — `Platform.OS !== 'web'` everywhere (native driver disabled on web)
2570
+ - `Select` — wheel modal on iOS, dialog on Android, `<select>` on web
2571
+ - `Toast` — full width on mobile, 400px centered on web
2572
+ - Hover states — web only via `useHover()`
2573
+ - `Skeleton` shimmer highlight — adapts opacity for light/dark mode
2574
+
2575
+ ### Dynamic Type
2576
+ All `Text` and `TextInput` components have `allowFontScaling={true}` — respects user font size accessibility settings.
2577
+
2578
+ ### Scaling utilities (internal)
2579
+ Used internally — not exported. `s()` horizontal scale, `vs()` vertical scale, `ms()` moderate scale relative to 350×680 base.