@retray-dev/ui-kit 7.0.1 → 9.0.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 (234) hide show
  1. package/COMPONENTS.md +554 -11
  2. package/EXAMPLES.md +2 -2
  3. package/README.md +14 -8
  4. package/dist/Accordion.js +57 -5
  5. package/dist/Accordion.mjs +4 -3
  6. package/dist/AlertBanner.js +4 -1
  7. package/dist/AlertBanner.mjs +3 -2
  8. package/dist/AppHeader.d.mts +40 -0
  9. package/dist/AppHeader.d.ts +40 -0
  10. package/dist/AppHeader.js +515 -0
  11. package/dist/AppHeader.mjs +10 -0
  12. package/dist/Avatar.js +39 -29
  13. package/dist/Avatar.mjs +2 -1
  14. package/dist/Badge.js +11 -1
  15. package/dist/Badge.mjs +2 -1
  16. package/dist/Button.d.mts +8 -3
  17. package/dist/Button.d.ts +8 -3
  18. package/dist/Button.js +126 -108
  19. package/dist/Button.mjs +6 -5
  20. package/dist/ButtonGroup.mjs +1 -0
  21. package/dist/Card.js +90 -70
  22. package/dist/Card.mjs +5 -4
  23. package/dist/CategoryStrip.js +79 -22
  24. package/dist/CategoryStrip.mjs +6 -6
  25. package/dist/Checkbox.js +118 -86
  26. package/dist/Checkbox.mjs +5 -5
  27. package/dist/Chip.js +113 -80
  28. package/dist/Chip.mjs +5 -5
  29. package/dist/ConfirmDialog.js +140 -110
  30. package/dist/ConfirmDialog.mjs +7 -6
  31. package/dist/CurrencyDisplay.mjs +1 -0
  32. package/dist/CurrencyInput.d.mts +1 -1
  33. package/dist/CurrencyInput.d.ts +1 -1
  34. package/dist/CurrencyInput.js +9 -5
  35. package/dist/CurrencyInput.mjs +5 -4
  36. package/dist/DetailRow.mjs +1 -0
  37. package/dist/EmptyState.js +131 -111
  38. package/dist/EmptyState.mjs +7 -6
  39. package/dist/ErrorBoundary.d.mts +42 -0
  40. package/dist/ErrorBoundary.d.ts +42 -0
  41. package/dist/ErrorBoundary.js +351 -0
  42. package/dist/ErrorBoundary.mjs +7 -0
  43. package/dist/Form.mjs +1 -0
  44. package/dist/HolographicCard.d.mts +55 -0
  45. package/dist/HolographicCard.d.ts +55 -0
  46. package/dist/HolographicCard.js +316 -0
  47. package/dist/HolographicCard.mjs +191 -0
  48. package/dist/IconButton.d.mts +8 -3
  49. package/dist/IconButton.d.ts +8 -3
  50. package/dist/IconButton.js +115 -98
  51. package/dist/IconButton.mjs +5 -4
  52. package/dist/ImageViewer.d.mts +23 -0
  53. package/dist/ImageViewer.d.ts +23 -0
  54. package/dist/ImageViewer.js +582 -0
  55. package/dist/ImageViewer.mjs +8 -0
  56. package/dist/Input.mjs +4 -3
  57. package/dist/LabelValue.mjs +1 -0
  58. package/dist/ListGroup.mjs +1 -0
  59. package/dist/ListItem.js +131 -117
  60. package/dist/ListItem.mjs +6 -5
  61. package/dist/MediaCard.js +54 -6
  62. package/dist/MediaCard.mjs +6 -5
  63. package/dist/MenuGroup.mjs +1 -0
  64. package/dist/MenuItem.js +91 -79
  65. package/dist/MenuItem.mjs +6 -5
  66. package/dist/MonthPicker.d.mts +10 -2
  67. package/dist/MonthPicker.d.ts +10 -2
  68. package/dist/MonthPicker.js +80 -17
  69. package/dist/MonthPicker.mjs +3 -2
  70. package/dist/PagerDots.d.mts +35 -0
  71. package/dist/PagerDots.d.ts +35 -0
  72. package/dist/PagerDots.js +392 -0
  73. package/dist/PagerDots.mjs +7 -0
  74. package/dist/Pressable.d.mts +5 -5
  75. package/dist/Pressable.d.ts +5 -5
  76. package/dist/Pressable.js +97 -86
  77. package/dist/Pressable.mjs +5 -4
  78. package/dist/PricingCard.d.mts +50 -0
  79. package/dist/PricingCard.d.ts +50 -0
  80. package/dist/PricingCard.js +636 -0
  81. package/dist/PricingCard.mjs +11 -0
  82. package/dist/Progress.mjs +3 -2
  83. package/dist/RadioGroup.js +81 -30
  84. package/dist/RadioGroup.mjs +5 -5
  85. package/dist/RetrayProvider.d.mts +2 -0
  86. package/dist/RetrayProvider.d.ts +2 -0
  87. package/dist/RetrayProvider.js +214 -0
  88. package/dist/RetrayProvider.mjs +5 -0
  89. package/dist/Select.js +51 -4
  90. package/dist/Select.mjs +5 -4
  91. package/dist/SelectableGrid.d.mts +44 -0
  92. package/dist/SelectableGrid.d.ts +44 -0
  93. package/dist/SelectableGrid.js +448 -0
  94. package/dist/SelectableGrid.mjs +9 -0
  95. package/dist/Separator.mjs +1 -0
  96. package/dist/Sheet.d.mts +13 -1
  97. package/dist/Sheet.d.ts +13 -1
  98. package/dist/Sheet.js +115 -5
  99. package/dist/Sheet.mjs +4 -2
  100. package/dist/Skeleton.d.mts +50 -0
  101. package/dist/Skeleton.d.ts +50 -0
  102. package/dist/Skeleton.js +61 -0
  103. package/dist/Skeleton.mjs +4 -2
  104. package/dist/Slider.js +51 -4
  105. package/dist/Slider.mjs +3 -2
  106. package/dist/Spinner.js +28 -7
  107. package/dist/Spinner.mjs +2 -1
  108. package/dist/Switch.js +98 -48
  109. package/dist/Switch.mjs +4 -3
  110. package/dist/TabBar.d.mts +42 -0
  111. package/dist/TabBar.d.ts +42 -0
  112. package/dist/TabBar.js +361 -0
  113. package/dist/TabBar.mjs +6 -0
  114. package/dist/Tabs.js +92 -62
  115. package/dist/Tabs.mjs +5 -4
  116. package/dist/Text.js +16 -0
  117. package/dist/Text.mjs +2 -1
  118. package/dist/Textarea.mjs +4 -3
  119. package/dist/Toast.d.mts +7 -7
  120. package/dist/Toast.d.ts +7 -7
  121. package/dist/Toast.mjs +1 -0
  122. package/dist/Toggle.d.mts +6 -3
  123. package/dist/Toggle.d.ts +6 -3
  124. package/dist/Toggle.js +135 -120
  125. package/dist/Toggle.mjs +5 -5
  126. package/dist/VirtualList.mjs +1 -0
  127. package/dist/{chunk-7H2OR44A.mjs → chunk-26BCI223.mjs} +1 -1
  128. package/dist/{chunk-CRYBX2CM.mjs → chunk-2TFTAWVJ.mjs} +44 -59
  129. package/dist/chunk-3DKJ2GIC.mjs +30 -0
  130. package/dist/{chunk-KWCPOM6W.mjs → chunk-3U4SSNWP.mjs} +32 -48
  131. package/dist/chunk-4I7D47FH.mjs +139 -0
  132. package/dist/chunk-4K625MVM.mjs +142 -0
  133. package/dist/{chunk-MN7OG7IY.mjs → chunk-6OAZJ577.mjs} +6 -4
  134. package/dist/{chunk-L7E7TVEZ.mjs → chunk-756RAKE4.mjs} +2 -2
  135. package/dist/{chunk-HSPSMN6U.mjs → chunk-7QHVVCB3.mjs} +2 -2
  136. package/dist/{chunk-URLL5JBR.mjs → chunk-A3A6KNQN.mjs} +3 -3
  137. package/dist/chunk-AJ7ZDNBT.mjs +120 -0
  138. package/dist/{chunk-FTLJOUOQ.mjs → chunk-AV4EMIRH.mjs} +25 -28
  139. package/dist/chunk-AZJF2BLK.mjs +115 -0
  140. package/dist/chunk-BNP626TY.mjs +159 -0
  141. package/dist/{chunk-5IKW3VNC.mjs → chunk-DVK4G2GT.mjs} +17 -1
  142. package/dist/{chunk-6LQYY7HC.mjs → chunk-EH745HE5.mjs} +2 -2
  143. package/dist/chunk-EJ7ZPXOH.mjs +163 -0
  144. package/dist/{chunk-RKLHUDZS.mjs → chunk-GD6KXMG5.mjs} +29 -15
  145. package/dist/{chunk-RR2VQLKE.mjs → chunk-GQYFLP3D.mjs} +14 -17
  146. package/dist/{chunk-Y6MXOREN.mjs → chunk-ID72TK46.mjs} +8 -17
  147. package/dist/{chunk-NQGVLMWG.mjs → chunk-JMOZEC77.mjs} +1 -1
  148. package/dist/{chunk-GCWOGZYL.mjs → chunk-JT7HKXRB.mjs} +39 -29
  149. package/dist/{chunk-LWG526VX.mjs → chunk-KIHCWCWL.mjs} +47 -62
  150. package/dist/chunk-LXJIIOYQ.mjs +104 -0
  151. package/dist/{chunk-SBZYEV4S.mjs → chunk-M6ZXVBTK.mjs} +5 -2
  152. package/dist/{chunk-XDMN67KV.mjs → chunk-MAC465BB.mjs} +10 -8
  153. package/dist/chunk-MBMXYJJV.mjs +36 -0
  154. package/dist/chunk-MLF3EZFW.mjs +119 -0
  155. package/dist/chunk-NA7PARID.mjs +147 -0
  156. package/dist/{chunk-QXGYKWI7.mjs → chunk-O3HA6TYM.mjs} +9 -4
  157. package/dist/{chunk-63357L2X.mjs → chunk-OB4JUQ3O.mjs} +1 -1
  158. package/dist/{chunk-AU2VDY4P.mjs → chunk-PFZTM6D5.mjs} +52 -4
  159. package/dist/chunk-QKH5ZOD5.mjs +97 -0
  160. package/dist/{chunk-KZJRQOIU.mjs → chunk-TERDKCLE.mjs} +11 -1
  161. package/dist/{chunk-U4N7WF4Z.mjs → chunk-UREA2GYY.mjs} +28 -23
  162. package/dist/{chunk-TAJ2PQ2O.mjs → chunk-VGTDN7SW.mjs} +7 -6
  163. package/dist/{chunk-URDE3EUU.mjs → chunk-VQ57HWPL.mjs} +27 -15
  164. package/dist/chunk-WBOOUHSS.mjs +62 -0
  165. package/dist/{chunk-GNGLDL6Z.mjs → chunk-WJLKJMKR.mjs} +18 -0
  166. package/dist/{chunk-YZJAFS4P.mjs → chunk-X4G6APW6.mjs} +22 -19
  167. package/dist/chunk-Y6FXYEAI.mjs +8 -0
  168. package/dist/chunk-YFZ3ELX5.mjs +16 -0
  169. package/dist/{chunk-QCNARS3X.mjs → chunk-YNROWHQJ.mjs} +1 -1
  170. package/dist/chunk-Z4BVUWW6.mjs +196 -0
  171. package/dist/{chunk-GPOUINK5.mjs → chunk-ZJKGQMYH.mjs} +10 -27
  172. package/dist/index-wt-orHUi.d.mts +85 -0
  173. package/dist/index-wt-orHUi.d.ts +85 -0
  174. package/dist/index.d.mts +59 -51
  175. package/dist/index.d.ts +59 -51
  176. package/dist/index.js +1940 -744
  177. package/dist/index.mjs +49 -39
  178. package/package.json +35 -5
  179. package/src/components/Accordion/Accordion.tsx +12 -1
  180. package/src/components/AlertBanner/AlertBanner.tsx +5 -0
  181. package/src/components/AppHeader/AppHeader.tsx +172 -0
  182. package/src/components/AppHeader/index.ts +1 -0
  183. package/src/components/Avatar/Avatar.tsx +10 -2
  184. package/src/components/Badge/Badge.tsx +8 -1
  185. package/src/components/Button/Button.tsx +20 -27
  186. package/src/components/Card/Card.tsx +12 -23
  187. package/src/components/CategoryStrip/CategoryStrip.tsx +17 -21
  188. package/src/components/Checkbox/Checkbox.tsx +26 -40
  189. package/src/components/Chip/Chip.tsx +24 -33
  190. package/src/components/CurrencyInput/CurrencyInput.tsx +10 -8
  191. package/src/components/EmptyState/EmptyState.tsx +2 -1
  192. package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
  193. package/src/components/ErrorBoundary/index.ts +1 -0
  194. package/src/components/HolographicCard/HolographicCard.tsx +315 -0
  195. package/src/components/HolographicCard/index.ts +1 -0
  196. package/src/components/IconButton/IconButton.tsx +19 -27
  197. package/src/components/ImageViewer/ImageViewer.tsx +290 -0
  198. package/src/components/ImageViewer/index.ts +1 -0
  199. package/src/components/ListItem/ListItem.tsx +70 -67
  200. package/src/components/MediaCard/MediaCard.tsx +8 -2
  201. package/src/components/MenuItem/MenuItem.tsx +10 -25
  202. package/src/components/MonthPicker/MonthPicker.tsx +39 -13
  203. package/src/components/MonthPicker/index.ts +1 -1
  204. package/src/components/PagerDots/PagerDots.tsx +200 -0
  205. package/src/components/PagerDots/index.ts +1 -0
  206. package/src/components/Pressable/Pressable.tsx +19 -35
  207. package/src/components/PricingCard/PricingCard.tsx +220 -0
  208. package/src/components/PricingCard/index.ts +1 -0
  209. package/src/components/RadioGroup/RadioGroup.tsx +14 -27
  210. package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
  211. package/src/components/RetrayProvider/index.ts +1 -0
  212. package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
  213. package/src/components/SelectableGrid/index.ts +1 -0
  214. package/src/components/Sheet/Sheet.tsx +65 -1
  215. package/src/components/Skeleton/Skeleton.tsx +142 -1
  216. package/src/components/Spinner/Spinner.tsx +17 -2
  217. package/src/components/Switch/Switch.tsx +30 -58
  218. package/src/components/TabBar/TabBar.tsx +169 -0
  219. package/src/components/TabBar/index.ts +1 -0
  220. package/src/components/Tabs/Tabs.tsx +23 -26
  221. package/src/components/Text/Text.tsx +2 -0
  222. package/src/components/Toggle/Toggle.tsx +35 -51
  223. package/src/fonts.ts +4 -1
  224. package/src/index.ts +23 -2
  225. package/src/utils/animations.ts +29 -1
  226. package/src/utils/fontGuard.ts +34 -0
  227. package/src/utils/haptics.ts +211 -9
  228. package/src/utils/pressable.ts +66 -0
  229. package/dist/chunk-76PFOSM2.mjs +0 -41
  230. package/dist/chunk-DITNP6PL.mjs +0 -106
  231. package/dist/chunk-JBLL7U3U.mjs +0 -64
  232. package/dist/chunk-LG4DO3DK.mjs +0 -174
  233. package/dist/chunk-RMMK64W5.mjs +0 -54
  234. package/dist/chunk-RTC3CFXF.mjs +0 -29
@@ -0,0 +1,315 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import { StyleSheet, ViewStyle, TouchableOpacity, Platform } from 'react-native'
3
+ import {
4
+ Canvas,
5
+ Group,
6
+ Image as SkiaImage,
7
+ useImage,
8
+ RoundedRect,
9
+ LinearGradient,
10
+ vec,
11
+ } from '@shopify/react-native-skia'
12
+ import Animated, {
13
+ useSharedValue,
14
+ useDerivedValue,
15
+ useAnimatedStyle,
16
+ withTiming,
17
+ } from 'react-native-reanimated'
18
+ import { usePressScale } from '../../utils/usePressScale'
19
+ import { PRESS_SCALE } from '../../utils/animations'
20
+ import { impactLight } from '../../utils/haptics'
21
+ import { RADIUS } from '../../tokens'
22
+
23
+ // ─── Foil Color Presets ───────────────────────────────────────────────────────
24
+
25
+ /** Available foil color preset names */
26
+ export type FoilPreset =
27
+ | 'rainbow'
28
+ | 'gold'
29
+ | 'silver'
30
+ | 'cosmic'
31
+ | 'emerald'
32
+ | 'rose'
33
+ | 'ocean'
34
+ | 'fire'
35
+ | 'aurora'
36
+ | 'neon'
37
+
38
+ /** Foil color presets — each is an array of RGBA colors for the gradient */
39
+ export const FOIL_PRESETS: Record<FoilPreset, string[]> = {
40
+ // Classic holographic rainbow
41
+ rainbow: [
42
+ 'rgba(255, 0, 128, 0.45)',
43
+ 'rgba(255, 200, 0, 0.40)',
44
+ 'rgba(0, 255, 170, 0.40)',
45
+ 'rgba(0, 170, 255, 0.45)',
46
+ 'rgba(180, 0, 255, 0.45)',
47
+ ],
48
+ // Premium gold foil
49
+ gold: [
50
+ 'rgba(255, 215, 0, 0.50)',
51
+ 'rgba(255, 180, 0, 0.45)',
52
+ 'rgba(218, 165, 32, 0.40)',
53
+ 'rgba(255, 223, 128, 0.50)',
54
+ 'rgba(184, 134, 11, 0.45)',
55
+ ],
56
+ // Chrome silver foil
57
+ silver: [
58
+ 'rgba(192, 192, 192, 0.50)',
59
+ 'rgba(220, 220, 220, 0.45)',
60
+ 'rgba(169, 169, 169, 0.40)',
61
+ 'rgba(240, 240, 240, 0.50)',
62
+ 'rgba(128, 128, 128, 0.45)',
63
+ ],
64
+ // Deep space cosmic
65
+ cosmic: [
66
+ 'rgba(75, 0, 130, 0.50)',
67
+ 'rgba(138, 43, 226, 0.45)',
68
+ 'rgba(255, 20, 147, 0.40)',
69
+ 'rgba(0, 191, 255, 0.45)',
70
+ 'rgba(148, 0, 211, 0.50)',
71
+ ],
72
+ // Emerald green luxury
73
+ emerald: [
74
+ 'rgba(0, 201, 87, 0.50)',
75
+ 'rgba(46, 139, 87, 0.45)',
76
+ 'rgba(0, 255, 127, 0.40)',
77
+ 'rgba(60, 179, 113, 0.45)',
78
+ 'rgba(0, 128, 0, 0.50)',
79
+ ],
80
+ // Rose gold / pink
81
+ rose: [
82
+ 'rgba(255, 105, 180, 0.50)',
83
+ 'rgba(255, 182, 193, 0.45)',
84
+ 'rgba(219, 112, 147, 0.40)',
85
+ 'rgba(255, 20, 147, 0.45)',
86
+ 'rgba(199, 21, 133, 0.50)',
87
+ ],
88
+ // Deep ocean blue
89
+ ocean: [
90
+ 'rgba(0, 119, 190, 0.50)',
91
+ 'rgba(0, 180, 216, 0.45)',
92
+ 'rgba(72, 202, 228, 0.40)',
93
+ 'rgba(144, 224, 239, 0.45)',
94
+ 'rgba(0, 150, 199, 0.50)',
95
+ ],
96
+ // Hot fire gradient
97
+ fire: [
98
+ 'rgba(255, 69, 0, 0.50)',
99
+ 'rgba(255, 140, 0, 0.45)',
100
+ 'rgba(255, 215, 0, 0.40)',
101
+ 'rgba(255, 99, 71, 0.45)',
102
+ 'rgba(220, 20, 60, 0.50)',
103
+ ],
104
+ // Aurora borealis
105
+ aurora: [
106
+ 'rgba(0, 255, 127, 0.45)',
107
+ 'rgba(127, 255, 212, 0.40)',
108
+ 'rgba(0, 206, 209, 0.45)',
109
+ 'rgba(138, 43, 226, 0.40)',
110
+ 'rgba(255, 20, 147, 0.45)',
111
+ ],
112
+ // Neon cyberpunk
113
+ neon: [
114
+ 'rgba(255, 0, 255, 0.55)',
115
+ 'rgba(0, 255, 255, 0.50)',
116
+ 'rgba(255, 255, 0, 0.45)',
117
+ 'rgba(0, 255, 0, 0.50)',
118
+ 'rgba(255, 0, 128, 0.55)',
119
+ ],
120
+ }
121
+
122
+ // Default preset
123
+ const DEFAULT_FOIL_COLORS = FOIL_PRESETS.rainbow
124
+
125
+ // ─── Tilt Configuration ───────────────────────────────────────────────────────
126
+
127
+ /** Default max tilt in degrees */
128
+ const DEFAULT_TILT_DEG = 10
129
+ /** Default normalization factor (~30° of device rotation = full tilt) */
130
+ const DEFAULT_NORM_FACTOR = Math.PI / 6
131
+
132
+ export interface HolographicCardProps {
133
+ /** Card art — `require()` asset or `{ uri }`. Omitted = gradient-only foil surface. */
134
+ source?: Parameters<typeof useImage>[0]
135
+ /** Card width (dp). Defaults to 300. */
136
+ width?: number
137
+ /** Card height (dp). Defaults to `width * 1.4` (trading-card ratio). */
138
+ height?: number
139
+ /** Corner radius (dp). Defaults to `RADIUS.md`. */
140
+ borderRadius?: number
141
+ /** React to device motion (gyroscope) for parallax tilt + sheen. Defaults to true. */
142
+ enableTilt?: boolean
143
+ /** Strength of the foil sheen, 0–1. Defaults to 1. */
144
+ intensity?: number
145
+ /** Called when the card is pressed. */
146
+ onPress?: () => void
147
+ style?: ViewStyle
148
+
149
+ // ─── New Customization Props ────────────────────────────────────────────────
150
+
151
+ /** Foil color preset. Defaults to 'rainbow'. Ignored if `foilColors` is provided. */
152
+ foilPreset?: FoilPreset
153
+ /** Custom foil gradient colors (array of RGBA strings). Overrides `foilPreset`. */
154
+ foilColors?: string[]
155
+ /** Maximum tilt angle in degrees (0–45). Defaults to 10. */
156
+ maxTiltDegrees?: number
157
+ /** Sensitivity of tilt to device motion (0.1–2). Higher = more responsive. Defaults to 1. */
158
+ tiltSensitivity?: number
159
+ /** How far the sheen moves relative to tilt (0–1). Defaults to 0.6. */
160
+ sheenSpread?: number
161
+ /** Animation duration for tilt smoothing in ms. Defaults to 80. */
162
+ tiltAnimationDuration?: number
163
+ /** Perspective depth for 3D effect (200–2000). Defaults to 800. */
164
+ perspective?: number
165
+ /** Blend mode for the foil overlay. Defaults to 'plus'. */
166
+ blendMode?: 'plus' | 'screen' | 'overlay' | 'softLight' | 'hardLight'
167
+ }
168
+
169
+ /**
170
+ * Holographic / foil trading-card surface (Pokémon-TCG style). Renders the art
171
+ * on a Skia canvas with an animated rainbow sheen, and tilts in 3D toward device
172
+ * motion. The sheen position tracks the tilt so the foil "catches the light".
173
+ *
174
+ * Deep-import only — keeps Skia out of the main bundle:
175
+ * `import { HolographicCard } from '@retray-dev/ui-kit/HolographicCard'`
176
+ *
177
+ * Requires the optional peers `@shopify/react-native-skia` and (for tilt)
178
+ * `expo-sensors`. Without `expo-sensors` the card renders with a static sheen.
179
+ */
180
+ export function HolographicCard({
181
+ source,
182
+ width = 300,
183
+ height,
184
+ borderRadius = RADIUS.md,
185
+ enableTilt = true,
186
+ intensity = 1,
187
+ onPress,
188
+ style,
189
+ // New customization props
190
+ foilPreset = 'rainbow',
191
+ foilColors,
192
+ maxTiltDegrees = DEFAULT_TILT_DEG,
193
+ tiltSensitivity = 1,
194
+ sheenSpread = 0.6,
195
+ tiltAnimationDuration = 80,
196
+ perspective = 800,
197
+ blendMode = 'plus',
198
+ }: HolographicCardProps) {
199
+ const h = height ?? width * 1.4
200
+ // Called unconditionally (rules of hooks); useImage returns null for a nullish source.
201
+ const image = useImage((source ?? null) as Parameters<typeof useImage>[0])
202
+
203
+ // Clamp and compute values
204
+ const clampedTilt = Math.max(0, Math.min(45, maxTiltDegrees))
205
+ const clampedSensitivity = Math.max(0.1, Math.min(2, tiltSensitivity))
206
+ const clampedSpread = Math.max(0, Math.min(1, sheenSpread))
207
+ const clampedPerspective = Math.max(200, Math.min(2000, perspective))
208
+
209
+ // Resolve foil colors: custom > preset > default
210
+ const resolvedFoilColors = useMemo(() => {
211
+ if (foilColors && foilColors.length >= 2) return foilColors
212
+ return FOIL_PRESETS[foilPreset] ?? DEFAULT_FOIL_COLORS
213
+ }, [foilColors, foilPreset])
214
+
215
+ // Normalization factor adjusted by sensitivity
216
+ const normFactor = DEFAULT_NORM_FACTOR / clampedSensitivity
217
+
218
+ // Tilt in normalized [-1, 1] on each axis.
219
+ const tiltX = useSharedValue(0)
220
+ const tiltY = useSharedValue(0)
221
+
222
+ useEffect(() => {
223
+ if (!enableTilt || Platform.OS === 'web') return
224
+ let remove: (() => void) | undefined
225
+ let cancelled = false
226
+ // Dynamic import: expo-sensors is an optional peer. Absence = static sheen.
227
+ import('expo-sensors')
228
+ .then(({ DeviceMotion }) => {
229
+ if (cancelled) return
230
+ DeviceMotion.setUpdateInterval(16)
231
+ const sub = DeviceMotion.addListener(({ rotation }) => {
232
+ if (!rotation) return
233
+ const nx = Math.max(-1, Math.min(rotation.gamma / normFactor, 1))
234
+ const ny = Math.max(-1, Math.min(rotation.beta / normFactor, 1))
235
+ tiltX.value = withTiming(nx, { duration: tiltAnimationDuration })
236
+ tiltY.value = withTiming(ny, { duration: tiltAnimationDuration })
237
+ })
238
+ remove = () => sub.remove()
239
+ })
240
+ .catch(() => {
241
+ // expo-sensors not installed — leave tilt at rest.
242
+ })
243
+ return () => {
244
+ cancelled = true
245
+ remove?.()
246
+ }
247
+ }, [enableTilt, tiltX, tiltY, normFactor, tiltAnimationDuration])
248
+
249
+ // 3D parallax — perspective tilt of the whole card toward the device motion.
250
+ const { animatedStyle: pressStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
251
+ pressScale: PRESS_SCALE.card,
252
+ disabled: !onPress,
253
+ })
254
+
255
+ const tiltStyle = useAnimatedStyle(() => ({
256
+ transform: [
257
+ { perspective: clampedPerspective },
258
+ { rotateX: `${-tiltY.value * clampedTilt}deg` },
259
+ { rotateY: `${tiltX.value * clampedTilt}deg` },
260
+ ],
261
+ }))
262
+
263
+ // Sheen sweeps across the card as it tilts.
264
+ const start = useDerivedValue(() => vec(width * (0.5 - tiltX.value * clampedSpread), h * (0.5 - tiltY.value * clampedSpread)))
265
+ const end = useDerivedValue(() => vec(width * (0.5 + tiltX.value * clampedSpread), h * (0.5 + tiltY.value * clampedSpread)))
266
+
267
+ const rrct = { x: 0, y: 0, width, height: h }
268
+
269
+ const canvas = (
270
+ <Canvas style={{ width, height: h }}>
271
+ {/* Art clipped to the rounded card. */}
272
+ {image ? (
273
+ <Group clip={{ rect: rrct, rx: borderRadius, ry: borderRadius }}>
274
+ <SkiaImage image={image} x={0} y={0} width={width} height={h} fit="cover" />
275
+ </Group>
276
+ ) : null}
277
+ {/* Foil sheen — additive rainbow gradient that tracks the tilt. */}
278
+ <RoundedRect x={0} y={0} width={width} height={h} r={borderRadius} opacity={intensity} blendMode={blendMode}>
279
+ <LinearGradient start={start} end={end} colors={resolvedFoilColors} />
280
+ </RoundedRect>
281
+ </Canvas>
282
+ )
283
+
284
+ const card = (
285
+ <Animated.View style={[{ width, height: h }, tiltStyle, style]}>{canvas}</Animated.View>
286
+ )
287
+
288
+ if (!onPress) return card
289
+
290
+ return (
291
+ <Animated.View style={pressStyle}>
292
+ <TouchableOpacity
293
+ onPress={() => {
294
+ impactLight()
295
+ onPress()
296
+ }}
297
+ onPressIn={onPressIn}
298
+ onPressOut={onPressOut}
299
+ activeOpacity={1}
300
+ touchSoundDisabled={true}
301
+ accessibilityRole="imagebutton"
302
+ {...hoverHandlers}
303
+ style={styles.touch}
304
+ >
305
+ {card}
306
+ </TouchableOpacity>
307
+ </Animated.View>
308
+ )
309
+ }
310
+
311
+ const styles = StyleSheet.create({
312
+ touch: {
313
+ alignSelf: 'flex-start',
314
+ },
315
+ })
@@ -0,0 +1 @@
1
+ export * from './HolographicCard'
@@ -1,20 +1,16 @@
1
1
  import React from 'react'
2
2
  import {
3
- TouchableOpacity,
4
3
  ActivityIndicator,
5
4
  StyleSheet,
6
5
  View,
7
6
  Text,
8
- TouchableOpacityProps,
9
7
  ViewStyle,
10
8
  } from 'react-native'
11
- import Animated from 'react-native-reanimated'
12
9
  import { impactLight } from '../../utils/haptics'
13
10
  import { useTheme } from '../../theme'
14
11
  import { s, ms } from '../../utils/scaling'
15
12
  import { renderIcon } from '../../utils/icons'
16
- import { usePressScale } from '../../utils/usePressScale'
17
- import { PRESS_SCALE } from '../../utils/animations'
13
+ import { PressableButton } from '../../utils/pressable'
18
14
 
19
15
  // primary: filled primary
20
16
  // secondary: filled surface — icon on neutral bg (Airbnb icon-button-circle)
@@ -24,7 +20,7 @@ import { PRESS_SCALE } from '../../utils/animations'
24
20
  export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'destructive'
25
21
  export type IconButtonSize = 'sm' | 'md' | 'lg'
26
22
 
27
- export interface IconButtonProps extends TouchableOpacityProps {
23
+ export interface IconButtonProps {
28
24
  iconName?: string
29
25
  icon?: React.ReactNode
30
26
  iconColor?: string
@@ -36,10 +32,16 @@ export interface IconButtonProps extends TouchableOpacityProps {
36
32
  * The dot/count appears top-right of the button.
37
33
  */
38
34
  badge?: boolean | number
35
+ disabled?: boolean
36
+ style?: ViewStyle
37
+ onPress?: () => void
38
+ accessibilityLabel?: string
39
+ accessibilityHint?: string
39
40
  }
40
41
 
41
42
  const sizeMap: Record<IconButtonSize, { container: number; icon: number }> = {
42
- sm: { container: s(32), icon: 16 },
43
+ // AUDIT FIX: sm was 32pt — below Apple HIG 44pt minimum touch target.
44
+ sm: { container: s(44), icon: 18 },
43
45
  md: { container: s(44), icon: 20 },
44
46
  lg: { container: s(52), icon: 24 },
45
47
  }
@@ -57,18 +59,13 @@ function IconButtonBase({
57
59
  onPress,
58
60
  accessibilityLabel,
59
61
  accessibilityHint,
60
- ...props
61
62
  }: IconButtonProps) {
62
63
  const { colors } = useTheme()
63
64
  const isDisabled = disabled || loading
64
- const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
65
- pressScale: PRESS_SCALE.button,
66
- disabled: isDisabled,
67
- })
68
65
 
69
- const handlePress: TouchableOpacityProps['onPress'] = (e) => {
66
+ const handlePress = () => {
70
67
  impactLight()
71
- onPress?.(e)
68
+ onPress?.()
72
69
  }
73
70
 
74
71
  const containerVariantStyle: ViewStyle = {
@@ -104,11 +101,8 @@ function IconButtonBase({
104
101
  const showCount = typeof badge === 'number' && badge > 0
105
102
 
106
103
  return (
107
- <Animated.View
108
- style={[styles.wrapper, animatedStyle]}
109
- {...hoverHandlers}
110
- >
111
- <TouchableOpacity
104
+ <View style={styles.wrapper}>
105
+ <PressableButton
112
106
  style={[
113
107
  styles.base,
114
108
  containerVariantStyle,
@@ -116,24 +110,22 @@ function IconButtonBase({
116
110
  isDisabled && styles.disabled,
117
111
  style,
118
112
  ]}
119
- disabled={isDisabled}
120
- activeOpacity={1}
121
- touchSoundDisabled={true}
113
+ enabled={!isDisabled}
122
114
  onPress={handlePress}
123
- onPressIn={onPressIn}
124
- onPressOut={onPressOut}
115
+ rippleColor="transparent"
116
+ touchSoundDisabled
117
+ activateOnHover
125
118
  accessibilityRole="button"
126
119
  accessibilityLabel={accessibilityLabel ?? iconName ?? 'icon button'}
127
120
  accessibilityHint={accessibilityHint}
128
121
  accessibilityState={{ disabled: isDisabled, busy: loading }}
129
- {...props}
130
122
  >
131
123
  {loading ? (
132
124
  <ActivityIndicator size="small" color={spinnerColor} />
133
125
  ) : (
134
126
  resolvedIcon
135
127
  )}
136
- </TouchableOpacity>
128
+ </PressableButton>
137
129
  {showBadge && (
138
130
  <View style={[
139
131
  styles.badge,
@@ -147,7 +139,7 @@ function IconButtonBase({
147
139
  )}
148
140
  </View>
149
141
  )}
150
- </Animated.View>
142
+ </View>
151
143
  )
152
144
  }
153
145