@expokit/cli 0.0.0-stage → 0.3.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/bin/expo-kit.js +1266 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +345 -0
  6. package/package.json +49 -4
  7. package/registry/_docs/accordion.json +115 -0
  8. package/registry/_docs/action-sheet.json +138 -0
  9. package/registry/_docs/alert.json +155 -0
  10. package/registry/_docs/avatar.json +227 -0
  11. package/registry/_docs/badge.json +148 -0
  12. package/registry/_docs/bottom-nav.json +143 -0
  13. package/registry/_docs/bottom-sheet.json +186 -0
  14. package/registry/_docs/bullet.json +27 -0
  15. package/registry/_docs/button-3d.json +138 -0
  16. package/registry/_docs/button.json +131 -0
  17. package/registry/_docs/calendar.json +198 -0
  18. package/registry/_docs/card-stack.json +124 -0
  19. package/registry/_docs/card.json +131 -0
  20. package/registry/_docs/carousel.json +122 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +109 -0
  23. package/registry/_docs/checkout.json +105 -0
  24. package/registry/_docs/chip-group.json +132 -0
  25. package/registry/_docs/color-swatch.json +82 -0
  26. package/registry/_docs/confetti.json +102 -0
  27. package/registry/_docs/coupon.json +192 -0
  28. package/registry/_docs/cube-pager.json +100 -0
  29. package/registry/_docs/dashboard.json +123 -0
  30. package/registry/_docs/date-picker.json +269 -0
  31. package/registry/_docs/dialog.json +186 -0
  32. package/registry/_docs/drawer.json +199 -0
  33. package/registry/_docs/fab.json +144 -0
  34. package/registry/_docs/flip-card.json +127 -0
  35. package/registry/_docs/floating-input.json +211 -0
  36. package/registry/_docs/gift-card.json +124 -0
  37. package/registry/_docs/glass-button.json +142 -0
  38. package/registry/_docs/glass-nav-bar.json +102 -0
  39. package/registry/_docs/glass-search-bar.json +114 -0
  40. package/registry/_docs/glass-segmented.json +94 -0
  41. package/registry/_docs/glass-sheet.json +136 -0
  42. package/registry/_docs/glass-tab-bar.json +100 -0
  43. package/registry/_docs/glass.json +94 -0
  44. package/registry/_docs/inbox.json +75 -0
  45. package/registry/_docs/isometric-bar.json +75 -0
  46. package/registry/_docs/kit.json +14 -0
  47. package/registry/_docs/kpi-card.json +14 -0
  48. package/registry/_docs/like-button.json +92 -0
  49. package/registry/_docs/liquidglass-tab.json +30 -0
  50. package/registry/_docs/list-item.json +216 -0
  51. package/registry/_docs/loyalty-card.json +124 -0
  52. package/registry/_docs/morph-button.json +128 -0
  53. package/registry/_docs/number-ticker.json +112 -0
  54. package/registry/_docs/offers.json +81 -0
  55. package/registry/_docs/onboarding.json +69 -0
  56. package/registry/_docs/otp-input.json +158 -0
  57. package/registry/_docs/pagination-dots.json +108 -0
  58. package/registry/_docs/parallax-layers.json +104 -0
  59. package/registry/_docs/parallax-scroll.json +210 -0
  60. package/registry/_docs/password-input.json +112 -0
  61. package/registry/_docs/payment-card.json +169 -0
  62. package/registry/_docs/pixel-bar.json +69 -0
  63. package/registry/_docs/polar-area.json +81 -0
  64. package/registry/_docs/profile.json +87 -0
  65. package/registry/_docs/progress-steps.json +116 -0
  66. package/registry/_docs/progress.json +152 -0
  67. package/registry/_docs/radio-group.json +112 -0
  68. package/registry/_docs/rating.json +114 -0
  69. package/registry/_docs/ring-carousel.json +122 -0
  70. package/registry/_docs/scratch-card.json +110 -0
  71. package/registry/_docs/search-bar.json +160 -0
  72. package/registry/_docs/search-results.json +123 -0
  73. package/registry/_docs/segmented-control.json +123 -0
  74. package/registry/_docs/select.json +277 -0
  75. package/registry/_docs/settings.json +75 -0
  76. package/registry/_docs/shimmer-button.json +98 -0
  77. package/registry/_docs/sign-in.json +201 -0
  78. package/registry/_docs/skeleton.json +162 -0
  79. package/registry/_docs/slide-to-confirm.json +108 -0
  80. package/registry/_docs/slider.json +177 -0
  81. package/registry/_docs/spin-wheel.json +138 -0
  82. package/registry/_docs/spinner.json +121 -0
  83. package/registry/_docs/stateful-button.json +184 -0
  84. package/registry/_docs/stepper.json +184 -0
  85. package/registry/_docs/swipe-deck.json +118 -0
  86. package/registry/_docs/swipeable-row.json +95 -0
  87. package/registry/_docs/switch.json +124 -0
  88. package/registry/_docs/tabs.json +129 -0
  89. package/registry/_docs/tag-input.json +148 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +191 -0
  92. package/registry/_docs/ticket-card.json +194 -0
  93. package/registry/_docs/ticket-shape.json +14 -0
  94. package/registry/_docs/tilt-card.json +157 -0
  95. package/registry/_docs/time-picker.json +269 -0
  96. package/registry/_docs/timeline-chart.json +33 -0
  97. package/registry/_docs/timeline.json +172 -0
  98. package/registry/_docs/toast.json +185 -0
  99. package/registry/_docs/treemap.json +33 -0
  100. package/registry/_docs/waterfall.json +39 -0
  101. package/registry/_docs/wheel-list.json +122 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  103. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  104. package/registry/accordion.json +17 -0
  105. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  106. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  107. package/registry/action-sheet.json +18 -0
  108. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  109. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  110. package/registry/alert/files/src/lib/kit.tsx +219 -0
  111. package/registry/alert.json +20 -0
  112. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  113. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  114. package/registry/avatar.json +18 -0
  115. package/registry/badge/files/src/components/ui/badge.tsx +626 -0
  116. package/registry/badge/files/src/lib/kit.tsx +219 -0
  117. package/registry/badge.json +17 -0
  118. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  119. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-nav.json +18 -0
  121. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  122. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  123. package/registry/bottom-sheet.json +17 -0
  124. package/registry/bullet.json +14 -0
  125. package/registry/button/files/src/components/ui/button.tsx +274 -0
  126. package/registry/button/files/src/lib/kit.tsx +219 -0
  127. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  128. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  129. package/registry/button-3d.json +18 -0
  130. package/registry/button.json +18 -0
  131. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  132. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  133. package/registry/calendar.json +17 -0
  134. package/registry/card/files/src/components/ui/card.tsx +300 -0
  135. package/registry/card/files/src/lib/kit.tsx +219 -0
  136. package/registry/card-stack.json +15 -0
  137. package/registry/card.json +17 -0
  138. package/registry/carousel.json +15 -0
  139. package/registry/chat.json +18 -0
  140. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  141. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  142. package/registry/checkbox.json +17 -0
  143. package/registry/checkout.json +26 -0
  144. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  145. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  146. package/registry/chip-group.json +17 -0
  147. package/registry/color-swatch.json +17 -0
  148. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  149. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  150. package/registry/confetti.json +15 -0
  151. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  152. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  153. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  154. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  155. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  156. package/registry/coupon.json +23 -0
  157. package/registry/cube-pager.json +15 -0
  158. package/registry/dashboard.json +32 -0
  159. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  160. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  161. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  162. package/registry/date-picker.json +19 -0
  163. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  164. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  165. package/registry/dialog.json +18 -0
  166. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  167. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  168. package/registry/drawer.json +18 -0
  169. package/registry/fab.json +18 -0
  170. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  171. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  172. package/registry/flip-card.json +15 -0
  173. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  174. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  175. package/registry/floating-input.json +17 -0
  176. package/registry/gift-card.json +21 -0
  177. package/registry/glass-button.json +22 -0
  178. package/registry/glass-nav-bar.json +21 -0
  179. package/registry/glass-search-bar.json +21 -0
  180. package/registry/glass-segmented.json +22 -0
  181. package/registry/glass-sheet.json +22 -0
  182. package/registry/glass-tab-bar.json +22 -0
  183. package/registry/glass.json +19 -0
  184. package/registry/inbox.json +30 -0
  185. package/registry/isometric-bar.json +14 -0
  186. package/registry/kit/files/src/lib/kit.tsx +219 -0
  187. package/registry/kit.json +11 -0
  188. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  189. package/registry/kpi-card.json +14 -0
  190. package/registry/like-button.json +18 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  192. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  193. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  194. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  195. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  196. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  197. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  198. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  199. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  200. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  201. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  202. package/registry/liquidglass-tab.json +23 -0
  203. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  204. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  205. package/registry/list-item.json +17 -0
  206. package/registry/loyalty-card.json +23 -0
  207. package/registry/morph-button.json +18 -0
  208. package/registry/number-ticker.json +15 -0
  209. package/registry/offers.json +24 -0
  210. package/registry/onboarding.json +17 -0
  211. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  212. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  213. package/registry/otp-input.json +17 -0
  214. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  215. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  216. package/registry/pagination-dots.json +15 -0
  217. package/registry/parallax-layers.json +15 -0
  218. package/registry/parallax-scroll.json +17 -0
  219. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  220. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  221. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  222. package/registry/password-input.json +19 -0
  223. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  224. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  225. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  226. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  227. package/registry/payment-card.json +21 -0
  228. package/registry/pixel-bar.json +14 -0
  229. package/registry/polar-area.json +14 -0
  230. package/registry/profile.json +17 -0
  231. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  232. package/registry/progress/files/src/lib/kit.tsx +219 -0
  233. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  234. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  235. package/registry/progress-steps.json +18 -0
  236. package/registry/progress.json +17 -0
  237. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  238. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  239. package/registry/radio-group.json +18 -0
  240. package/registry/rating.json +18 -0
  241. package/registry/ring-carousel.json +15 -0
  242. package/registry/scratch-card.json +18 -0
  243. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  244. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  245. package/registry/search-bar.json +17 -0
  246. package/registry/search-results.json +30 -0
  247. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  248. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  249. package/registry/segmented-control.json +18 -0
  250. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  251. package/registry/select/files/src/lib/kit.tsx +219 -0
  252. package/registry/select.json +17 -0
  253. package/registry/settings.json +26 -0
  254. package/registry/shimmer-button.json +17 -0
  255. package/registry/sign-in.json +26 -0
  256. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  257. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  258. package/registry/skeleton.json +17 -0
  259. package/registry/slide-to-confirm.json +18 -0
  260. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  261. package/registry/slider/files/src/lib/kit.tsx +219 -0
  262. package/registry/slider.json +17 -0
  263. package/registry/spin-wheel.json +18 -0
  264. package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
  265. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  266. package/registry/spinner.json +17 -0
  267. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  268. package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
  269. package/registry/stateful-button.json +18 -0
  270. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  271. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  272. package/registry/stepper.json +17 -0
  273. package/registry/swipe-deck.json +15 -0
  274. package/registry/swipeable-row.json +15 -0
  275. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  276. package/registry/switch/files/src/lib/kit.tsx +219 -0
  277. package/registry/switch.json +18 -0
  278. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  279. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  280. package/registry/tabs.json +17 -0
  281. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  282. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  283. package/registry/tag-input.json +17 -0
  284. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  285. package/registry/text-animation/files/src/lib/kit.tsx +219 -0
  286. package/registry/text-animation.json +15 -0
  287. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  288. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  289. package/registry/textarea.json +17 -0
  290. package/registry/ticket-card.json +24 -0
  291. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  292. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  293. package/registry/ticket-shape.json +17 -0
  294. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  295. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  296. package/registry/tilt-card.json +17 -0
  297. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  298. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  299. package/registry/time-picker.json +17 -0
  300. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  301. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  302. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  303. package/registry/timeline-chart.json +14 -0
  304. package/registry/timeline.json +20 -0
  305. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  306. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  307. package/registry/toast/files/src/lib/kit.tsx +219 -0
  308. package/registry/toast.json +20 -0
  309. package/registry/treemap.json +14 -0
  310. package/registry/waterfall.json +14 -0
  311. package/registry/wheel-list.json +17 -0
  312. package/schema/components.schema.json +13 -0
@@ -0,0 +1,530 @@
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ View,
11
+ type StyleProp,
12
+ type TextInputProps,
13
+ type ViewStyle,
14
+ } from 'react-native';
15
+ import Svg, { Circle, Path } from 'react-native-svg';
16
+
17
+ import { alpha, motion, useKit } from '@/lib/kit';
18
+
19
+ export type SearchBarVariant = 'default' | 'filled' | 'minimal';
20
+ export type SearchBarSize = 'sm' | 'md' | 'lg';
21
+
22
+ export interface SearchSuggestion {
23
+ key: string;
24
+ label: string;
25
+ /** Muted text on the right, e.g. a category or result count. */
26
+ hint?: string;
27
+ /** Leading icon (default 'search'). */
28
+ icon?: 'recent' | 'trending' | 'search';
29
+ }
30
+
31
+ export interface SearchBarProps extends Omit<TextInputProps, 'style'> {
32
+ /** 'default' outlined pill · 'filled' iOS-style grey field · 'minimal' icon that expands into a field. */
33
+ variant?: SearchBarVariant;
34
+ /** 'sm' 36pt · 'md' 44pt · 'lg' 52pt. */
35
+ size?: SearchBarSize;
36
+ /** Swaps the magnifier for a spinning arc (e.g. while results load). */
37
+ loading?: boolean;
38
+ /** Slide a Cancel button in from the right while searching (default: true, except 'minimal'). */
39
+ showCancel?: boolean;
40
+ cancelLabel?: string;
41
+ /** Called after Cancel clears the query and dismisses the keyboard. */
42
+ onCancel?: () => void;
43
+ /** Called when the clear button empties the query. */
44
+ onClear?: () => void;
45
+ /** Focus colour (default: theme accent). */
46
+ accent?: string;
47
+ /** 'minimal' only: which side the collapsed icon rests on. */
48
+ align?: 'left' | 'right';
49
+ /** Shown in a dropdown panel under the bar while it is focused. */
50
+ suggestions?: SearchSuggestion[];
51
+ /** Called when a suggestion is picked (the query is set to its label first). */
52
+ onSelectSuggestion?: (item: SearchSuggestion) => void;
53
+ /** Filter suggestions by the query (default true). Turn off if you filter them yourself. */
54
+ filterSuggestions?: boolean;
55
+ /** Most suggestions to show (default 6). */
56
+ maxSuggestions?: number;
57
+ /** Rendered inside the field on the right, e.g. a mic or filter button. */
58
+ trailing?: React.ReactNode;
59
+ style?: StyleProp<ViewStyle>;
60
+ }
61
+
62
+ /** Every dimension comes from one table so the sizes read clearly different. */
63
+ const SIZES: Record<SearchBarSize, { h: number; font: number; icon: number; slot: number; r: number; clear: number; end: number }> = {
64
+ sm: { h: 34, font: 14, icon: 14, slot: 30, r: 9, clear: 16, end: 6 },
65
+ md: { h: 44, font: 16, icon: 18, slot: 40, r: 12, clear: 20, end: 8 },
66
+ lg: { h: 56, font: 19, icon: 22, slot: 52, r: 16, clear: 24, end: 12 },
67
+ };
68
+
69
+ /** react-native-web draws the browser focus outline; our border + halo is the focus indicator. */
70
+ const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
71
+
72
+ /**
73
+ * Search field with a focus halo: the magnifier pops on focus, Cancel slides in as the field
74
+ * springs narrower, a clear button scales in, a spinning arc shows loading and a suggestion
75
+ * panel drops down while focused. The 'minimal' variant rests as a round icon and expands
76
+ * into a full field when tapped.
77
+ */
78
+ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBar(
79
+ {
80
+ variant = 'default',
81
+ size = 'md',
82
+ loading = false,
83
+ showCancel,
84
+ cancelLabel = 'Cancel',
85
+ onCancel,
86
+ onClear,
87
+ accent,
88
+ align = 'left',
89
+ suggestions,
90
+ onSelectSuggestion,
91
+ filterSuggestions = true,
92
+ maxSuggestions = 6,
93
+ trailing,
94
+ style,
95
+ value,
96
+ defaultValue,
97
+ onChangeText,
98
+ onFocus,
99
+ onBlur,
100
+ placeholder = 'Search',
101
+ editable = true,
102
+ autoFocus,
103
+ ...props
104
+ },
105
+ ref
106
+ ) {
107
+ const { colors, radius } = useKit();
108
+ const s = SIZES[size];
109
+ const tint = accent ?? colors.accent;
110
+ const minimal = variant === 'minimal';
111
+ const cancelEnabled = !minimal && (showCancel ?? true);
112
+
113
+ const inputRef = useRef<TextInput>(null);
114
+ useImperativeHandle(ref, () => inputRef.current as TextInput);
115
+
116
+ const [focused, setFocused] = useState(false);
117
+ const [inner, setInner] = useState(defaultValue ?? '');
118
+ const text = value ?? inner;
119
+ const [expanded, setExpanded] = useState(!!text || !!autoFocus);
120
+ const [rowW, setRowW] = useState(0);
121
+ const [cancelW, setCancelW] = useState(0);
122
+
123
+ const open = !minimal || expanded;
124
+ const cancelShown = cancelEnabled && (focused || text.length > 0);
125
+
126
+ // JS driver: layout (widths) and colours.
127
+ const focusJS = useRef(new Animated.Value(0)).current;
128
+ const cancelJS = useRef(new Animated.Value(0)).current;
129
+ const openJS = useRef(new Animated.Value(open ? 1 : 0)).current;
130
+ // Native driver: opacity and transforms, on different views.
131
+ const halo = useRef(new Animated.Value(0)).current;
132
+ const cancelN = useRef(new Animated.Value(0)).current;
133
+ const openN = useRef(new Animated.Value(open ? 1 : 0)).current;
134
+ const icon = useRef(new Animated.Value(1)).current;
135
+ const clear = useRef(new Animated.Value(text ? 1 : 0)).current;
136
+ const busy = useRef(new Animated.Value(loading ? 1 : 0)).current;
137
+ const spin = useRef(new Animated.Value(0)).current;
138
+
139
+ useEffect(() => {
140
+ const to = focused ? 1 : 0;
141
+ Animated.timing(focusJS, { toValue: to, duration: motion.fast, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
142
+ Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
143
+ if (focused) {
144
+ icon.setValue(0.6);
145
+ Animated.spring(icon, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
146
+ }
147
+ }, [focused, focusJS, halo, icon]);
148
+
149
+ useEffect(() => {
150
+ const to = cancelShown ? 1 : 0;
151
+ Animated.spring(cancelJS, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
152
+ Animated.spring(cancelN, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
153
+ }, [cancelShown, cancelJS, cancelN]);
154
+
155
+ useEffect(() => {
156
+ const to = open ? 1 : 0;
157
+ Animated.spring(openJS, { toValue: to, useNativeDriver: false, ...motion.gentle }).start();
158
+ Animated.timing(openN, { toValue: to, duration: open ? motion.normal : motion.fast, delay: open ? 80 : 0, useNativeDriver: true }).start();
159
+ }, [open, openJS, openN]);
160
+
161
+ useEffect(() => {
162
+ Animated.spring(clear, { toValue: text && editable && open ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
163
+ }, [text, editable, open, clear]);
164
+
165
+ useEffect(() => {
166
+ Animated.timing(busy, { toValue: loading ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
167
+ if (!loading) return;
168
+ spin.setValue(0);
169
+ const loop = Animated.loop(Animated.timing(spin, { toValue: 1, duration: 750, easing: Easing.linear, useNativeDriver: true }));
170
+ loop.start();
171
+ return () => loop.stop();
172
+ }, [loading, busy, spin]);
173
+
174
+ // The panel closes a beat after blur so a tap on a suggestion still lands (web blurs on mousedown).
175
+ const [panelOpen, setPanelOpen] = useState(false);
176
+ const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
177
+ useEffect(() => {
178
+ if (closeTimer.current) clearTimeout(closeTimer.current);
179
+ if (focused) setPanelOpen(true);
180
+ else closeTimer.current = setTimeout(() => setPanelOpen(false), 160);
181
+ return () => {
182
+ if (closeTimer.current) clearTimeout(closeTimer.current);
183
+ };
184
+ }, [focused]);
185
+
186
+ const query = text.trim().toLowerCase();
187
+ const visible = (suggestions ?? [])
188
+ .filter((item) => !filterSuggestions || !query || item.label.toLowerCase().includes(query))
189
+ .slice(0, maxSuggestions);
190
+
191
+ const setText = (next: string) => {
192
+ if (value === undefined) setInner(next);
193
+ onChangeText?.(next);
194
+ };
195
+
196
+ const expand = () => {
197
+ setExpanded(true);
198
+ requestAnimationFrame(() => inputRef.current?.focus());
199
+ };
200
+
201
+ const cancel = () => {
202
+ setText('');
203
+ inputRef.current?.blur();
204
+ if (minimal) setExpanded(false);
205
+ onCancel?.();
206
+ };
207
+
208
+ const select = (item: SearchSuggestion) => {
209
+ setText(item.label);
210
+ setPanelOpen(false);
211
+ inputRef.current?.blur();
212
+ onSelectSuggestion?.(item);
213
+ };
214
+
215
+ const look: Record<SearchBarVariant, { bg: string; rest: string; r: number }> = {
216
+ default: { bg: colors.card, rest: colors.input, r: radius.capsule(s.h) },
217
+ filled: { bg: colors.muted, rest: alpha(colors.muted, 0), r: Math.round(s.r * (radius.md / 12)) },
218
+ minimal: { bg: colors.muted, rest: alpha(colors.muted, 0), r: radius.capsule(s.h) },
219
+ };
220
+ const { bg, rest, r } = look[variant];
221
+ const borderColor = focusJS.interpolate({ inputRange: [0, 1], outputRange: [rest, tint] });
222
+ const fieldW = minimal ? openJS.interpolate({ inputRange: [0, 1], outputRange: [s.h, Math.max(s.h, rowW)] }) : undefined;
223
+ const slot = minimal ? s.h : s.slot;
224
+ const iconColor = focused ? tint : minimal && !open ? colors.foreground : colors.mutedForeground;
225
+
226
+ return (
227
+ <View style={[styles.root, !editable && styles.disabled, style]}>
228
+ <View
229
+ onLayout={(e) => setRowW(e.nativeEvent.layout.width)}
230
+ style={[styles.row, minimal && { justifyContent: align === 'right' ? 'flex-end' : 'flex-start' }]}
231
+ >
232
+ <Animated.View style={minimal ? { width: fieldW, height: s.h } : [styles.flex, { height: s.h }]}>
233
+ <Animated.View
234
+ pointerEvents="none"
235
+ style={[
236
+ styles.halo,
237
+ {
238
+ borderRadius: r + 4,
239
+ backgroundColor: alpha(tint, 0.18),
240
+ opacity: halo,
241
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }],
242
+ },
243
+ ]}
244
+ />
245
+ <Animated.View style={[styles.field, { borderRadius: r, backgroundColor: bg, borderColor }]}>
246
+ <View style={[styles.slot, { width: slot }]}>
247
+ <Animated.View
248
+ style={{
249
+ opacity: busy.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
250
+ transform: [
251
+ { scale: icon },
252
+ { rotate: icon.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
253
+ ],
254
+ }}
255
+ >
256
+ <Icon name="search" size={s.icon} color={iconColor} />
257
+ </Animated.View>
258
+ <Animated.View
259
+ pointerEvents="none"
260
+ style={[
261
+ styles.center,
262
+ { opacity: busy, transform: [{ rotate: spin.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] }) }] },
263
+ ]}
264
+ >
265
+ <Svg width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none">
266
+ <Circle cx={12} cy={12} r={8.5} stroke={alpha(tint, 0.2)} strokeWidth={2.2} />
267
+ <Path d="M12 3.5a8.5 8.5 0 0 1 8.5 8.5" stroke={tint} strokeWidth={2.2} strokeLinecap="round" />
268
+ </Svg>
269
+ </Animated.View>
270
+ </View>
271
+ <Animated.View style={[styles.flex, { opacity: openN }]} pointerEvents={open ? 'auto' : 'none'}>
272
+ <TextInput
273
+ ref={inputRef}
274
+ returnKeyType="search"
275
+ autoCorrect={false}
276
+ accessibilityRole="search"
277
+ accessibilityLabel={props.accessibilityLabel ?? placeholder}
278
+ {...props}
279
+ autoFocus={autoFocus}
280
+ editable={editable}
281
+ value={text}
282
+ onChangeText={setText}
283
+ placeholder={placeholder}
284
+ placeholderTextColor={colors.mutedForeground}
285
+ selectionColor={tint}
286
+ cursorColor={tint}
287
+ onFocus={(e) => {
288
+ setFocused(true);
289
+ onFocus?.(e);
290
+ }}
291
+ onBlur={(e) => {
292
+ setFocused(false);
293
+ if (minimal && !text) setExpanded(false);
294
+ onBlur?.(e);
295
+ }}
296
+ style={[styles.input, { color: colors.foreground, fontSize: s.font, height: s.h - 3 }, webNoOutline]}
297
+ />
298
+ </Animated.View>
299
+ <Animated.View
300
+ pointerEvents={text && open ? 'auto' : 'none'}
301
+ style={[styles.clear, { width: s.clear + 8, opacity: clear, transform: [{ scale: clear }] }]}
302
+ >
303
+ <Pressable
304
+ hitSlop={12}
305
+ accessibilityRole="button"
306
+ accessibilityLabel="Clear search"
307
+ onPress={() => {
308
+ setText('');
309
+ onClear?.();
310
+ inputRef.current?.focus();
311
+ }}
312
+ style={[
313
+ styles.clearDot,
314
+ { width: s.clear, height: s.clear, borderRadius: s.clear / 2, backgroundColor: alpha(colors.mutedForeground, 0.22) },
315
+ ]}
316
+ >
317
+ <Svg width={s.clear / 2} height={s.clear / 2} viewBox="0 0 24 24" fill="none">
318
+ <Path d="M6 6l12 12M18 6L6 18" stroke={colors.foreground} strokeWidth={3} strokeLinecap="round" />
319
+ </Svg>
320
+ </Pressable>
321
+ </Animated.View>
322
+ {trailing ? (
323
+ <Animated.View style={[styles.trailing, { paddingRight: s.end - 2, opacity: openN }]} pointerEvents={open ? 'auto' : 'none'}>
324
+ {trailing}
325
+ </Animated.View>
326
+ ) : (
327
+ <View style={{ width: s.end }} />
328
+ )}
329
+ {minimal && !open ? (
330
+ <Pressable
331
+ accessibilityRole="button"
332
+ accessibilityLabel={placeholder}
333
+ onPress={expand}
334
+ disabled={!editable}
335
+ style={StyleSheet.absoluteFill}
336
+ />
337
+ ) : null}
338
+ </Animated.View>
339
+ </Animated.View>
340
+
341
+ {cancelEnabled ? (
342
+ <>
343
+ <Text
344
+ pointerEvents="none"
345
+ onLayout={(e) => setCancelW(Math.ceil(e.nativeEvent.layout.width) + 14)}
346
+ style={[styles.cancelText, styles.measure, { fontSize: s.font }]}
347
+ >
348
+ {cancelLabel}
349
+ </Text>
350
+ <Animated.View
351
+ style={{ height: s.h, overflow: 'hidden', width: cancelJS.interpolate({ inputRange: [0, 1], outputRange: [0, cancelW] }) }}
352
+ >
353
+ <Animated.View
354
+ style={[
355
+ styles.cancel,
356
+ {
357
+ width: cancelW,
358
+ opacity: cancelN,
359
+ transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }],
360
+ },
361
+ ]}
362
+ >
363
+ <Pressable
364
+ hitSlop={{ top: 10, bottom: 10, left: 6, right: 10 }}
365
+ accessibilityRole="button"
366
+ accessibilityLabel={cancelLabel}
367
+ onPress={cancel}
368
+ >
369
+ <Text numberOfLines={1} style={[styles.cancelText, { color: tint, fontSize: s.font }]}>
370
+ {cancelLabel}
371
+ </Text>
372
+ </Pressable>
373
+ </Animated.View>
374
+ </Animated.View>
375
+ </>
376
+ ) : null}
377
+ </View>
378
+
379
+ {suggestions ? (
380
+ <SuggestionPanel open={panelOpen && open && visible.length > 0} items={visible} query={query} tint={tint} onSelect={select} />
381
+ ) : null}
382
+ </View>
383
+ );
384
+ });
385
+
386
+ /** Dropdown list under the bar: drops in, rows show an icon, bolded match and a hint. */
387
+ function SuggestionPanel({
388
+ open,
389
+ items,
390
+ query,
391
+ tint,
392
+ onSelect,
393
+ }: {
394
+ open: boolean;
395
+ items: SearchSuggestion[];
396
+ query: string;
397
+ tint: string;
398
+ onSelect: (item: SearchSuggestion) => void;
399
+ }) {
400
+ const { colors, radius } = useKit();
401
+ const [mounted, setMounted] = useState(open);
402
+ const enter = useRef(new Animated.Value(0)).current;
403
+ const last = useRef(items);
404
+ if (items.length) last.current = items;
405
+
406
+ useEffect(() => {
407
+ if (open) {
408
+ setMounted(true);
409
+ Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
410
+ } else {
411
+ Animated.timing(enter, { toValue: 0, duration: motion.fast, useNativeDriver: true }).start(({ finished }) => {
412
+ if (finished) setMounted(false);
413
+ });
414
+ }
415
+ }, [open, enter]);
416
+
417
+ if (!mounted) return null;
418
+ return (
419
+ <Animated.View
420
+ accessibilityRole="list"
421
+ style={[
422
+ styles.panel,
423
+ {
424
+ backgroundColor: colors.popover,
425
+ borderColor: colors.border,
426
+ borderRadius: radius.lg,
427
+ opacity: enter,
428
+ transform: [
429
+ { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [-6, 0] }) },
430
+ { scale: enter.interpolate({ inputRange: [0, 1], outputRange: [0.98, 1] }) },
431
+ ],
432
+ },
433
+ ]}
434
+ >
435
+ {last.current.map((item) => (
436
+ <Pressable
437
+ key={item.key}
438
+ accessibilityRole="button"
439
+ accessibilityLabel={item.hint ? `${item.label}, ${item.hint}` : item.label}
440
+ onPress={() => onSelect(item)}
441
+ style={({ pressed }) => [styles.option, pressed && { backgroundColor: alpha(tint, 0.1) }]}
442
+ >
443
+ <View style={[styles.optionIcon, { backgroundColor: colors.muted }]}>
444
+ <Icon name={item.icon ?? 'search'} size={15} color={item.icon === 'trending' ? tint : colors.mutedForeground} />
445
+ </View>
446
+ <Text numberOfLines={1} style={[styles.optionText, { color: colors.mutedForeground }]}>
447
+ <Highlight label={item.label} query={query} color={colors.foreground} />
448
+ </Text>
449
+ {item.hint ? (
450
+ <Text numberOfLines={1} style={[styles.hint, { color: colors.mutedForeground }]}>
451
+ {item.hint}
452
+ </Text>
453
+ ) : null}
454
+ </Pressable>
455
+ ))}
456
+ </Animated.View>
457
+ );
458
+ }
459
+
460
+ /** Bolds the part of `label` that matches the query (the whole label when there is no query). */
461
+ function Highlight({ label, query, color }: { label: string; query: string; color: string }) {
462
+ const at = query ? label.toLowerCase().indexOf(query) : -1;
463
+ if (!query) return <Text style={{ color, fontWeight: '400' }}>{label}</Text>;
464
+ if (at < 0) return <Text>{label}</Text>;
465
+ return (
466
+ <>
467
+ {label.slice(0, at)}
468
+ <Text style={{ color, fontWeight: '400' }}>{label.slice(at, at + query.length)}</Text>
469
+ {label.slice(at + query.length)}
470
+ </>
471
+ );
472
+ }
473
+
474
+ function Icon({ name, size, color }: { name: 'search' | 'recent' | 'trending'; size: number; color: string }) {
475
+ const p = { stroke: color, strokeWidth: 1.8, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
476
+ return (
477
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
478
+ {name === 'search' ? (
479
+ <>
480
+ <Circle cx={10.8} cy={10.8} r={6.6} {...p} strokeWidth={2.1} />
481
+ <Path d="M15.8 15.8 20 20" {...p} strokeWidth={2.1} />
482
+ </>
483
+ ) : name === 'recent' ? (
484
+ <>
485
+ <Path d="M4.6 12a7.6 7.6 0 1 0 2.2-5.4" {...p} />
486
+ <Path d="M4.5 4.4v3.3h3.3" {...p} />
487
+ <Path d="M12 8.2V12l2.6 1.7" {...p} />
488
+ </>
489
+ ) : (
490
+ <>
491
+ <Path d="M3.5 16.5 9.2 10.8l3.6 3.6L20.5 6.7" {...p} />
492
+ <Path d="M15.3 6.7h5.2v5.2" {...p} />
493
+ </>
494
+ )}
495
+ </Svg>
496
+ );
497
+ }
498
+
499
+ const styles = StyleSheet.create({
500
+ root: { alignSelf: 'stretch' },
501
+ disabled: { opacity: 0.5 },
502
+ row: { flexDirection: 'row', alignItems: 'center' },
503
+ flex: { flex: 1 },
504
+ halo: { position: 'absolute', top: -4, left: -4, right: -4, bottom: -4 },
505
+ field: { flex: 1, flexDirection: 'row', alignItems: 'center', borderWidth: 1.5, overflow: 'hidden' },
506
+ slot: { height: '100%', alignItems: 'center', justifyContent: 'center' },
507
+ center: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center', justifyContent: 'center' },
508
+ input: { paddingVertical: 0, paddingLeft: 0, paddingRight: 4 },
509
+ clear: { alignItems: 'center', justifyContent: 'center' },
510
+ clearDot: { alignItems: 'center', justifyContent: 'center' },
511
+ trailing: { paddingLeft: 2, alignItems: 'center', justifyContent: 'center' },
512
+ cancel: { position: 'absolute', left: 0, top: 0, bottom: 0, justifyContent: 'center', paddingLeft: 14 },
513
+ cancelText: { fontWeight: '400' },
514
+ measure: { position: 'absolute', opacity: 0, left: 0, top: 0 },
515
+ panel: {
516
+ marginTop: 10,
517
+ paddingVertical: 6,
518
+ borderWidth: StyleSheet.hairlineWidth * 2,
519
+ overflow: 'hidden',
520
+ shadowColor: '#000',
521
+ shadowOpacity: 0.08,
522
+ shadowRadius: 16,
523
+ shadowOffset: { width: 0, height: 8 },
524
+ elevation: 4,
525
+ },
526
+ option: { flexDirection: 'row', alignItems: 'center', gap: 12, minHeight: 44, paddingHorizontal: 12 },
527
+ optionIcon: { width: 28, height: 28, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
528
+ optionText: { flex: 1, fontSize: 15 },
529
+ hint: { fontSize: 12.5, maxWidth: 110 },
530
+ });