@expokit/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,205 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ import { motion } from '@/lib/kit';
5
+
6
+ export type FlipDirection = 'horizontal' | 'vertical';
7
+ export type FlipTrigger = 'press' | 'none';
8
+
9
+ export interface FlipCardProps {
10
+ /** Shown first. Sets the card's size — the back fills the same box. */
11
+ front: React.ReactNode;
12
+ back: React.ReactNode;
13
+ /** Controlled side: `true` shows the back. */
14
+ flipped?: boolean;
15
+ /** Uncontrolled initial side (default false). */
16
+ defaultFlipped?: boolean;
17
+ onFlip?: (flipped: boolean) => void;
18
+ /** Spin around the vertical axis (left/right) or the horizontal one (top/bottom). */
19
+ direction?: FlipDirection;
20
+ /** `press` flips on tap; `none` leaves it to `flipped` (e.g. a button elsewhere). */
21
+ trigger?: FlipTrigger;
22
+ /** Depth of the 3D effect — smaller is more dramatic (default 1000). */
23
+ perspective?: number;
24
+ /** Corner radius of both faces (default 20). */
25
+ radius?: number;
26
+ /** Soft shadow that narrows as the card turns edge-on (default true). */
27
+ shadow?: boolean;
28
+ /**
29
+ * Drive the turn from outside (0 = front, 1 = back) — from `useFlipProgress`, when something else
30
+ * must follow the flip, like a TiltCard's shadow. `flipped` still sets the side for accessibility.
31
+ */
32
+ progress?: Animated.Value;
33
+ /** Accessibility label; the hint says which side is showing. */
34
+ accessibilityLabel?: string;
35
+ /** Screen-reader hints (defaults in English). */
36
+ labels?: { toBack?: string; toFront?: string };
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+
40
+ // How wide the shadow under a flipping card is through the turn: roughly cos θ, always a little
41
+ // narrower than the card so a flat shadow plate never shows past it, and none at all edge-on.
42
+ const SQUEEZE = {
43
+ inputRange: [0, 0.25, 0.45, 0.5, 0.55, 0.75, 1],
44
+ outputRange: [1, 0.6, 0.12, 0, 0.12, 0.6, 1],
45
+ };
46
+
47
+ /**
48
+ * The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
49
+ * `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
50
+ * shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
51
+ */
52
+ export function useFlipProgress(flipped: boolean) {
53
+ const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
54
+ useEffect(() => {
55
+ Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
56
+ }, [flipped, progress]);
57
+ const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
58
+ return { progress, shadowScaleX };
59
+ }
60
+
61
+ /**
62
+ * A card with two faces that flips in 3D on tap — it lifts toward you half-way through and its
63
+ * shadow narrows as it turns edge-on, like a real card turning over. Controlled or uncontrolled.
64
+ */
65
+ export function FlipCard({
66
+ front,
67
+ back,
68
+ flipped: flippedProp,
69
+ defaultFlipped = false,
70
+ onFlip,
71
+ direction = 'horizontal',
72
+ trigger = 'press',
73
+ perspective = 1000,
74
+ radius = 20,
75
+ shadow = true,
76
+ progress,
77
+ accessibilityLabel,
78
+ labels,
79
+ style,
80
+ }: FlipCardProps) {
81
+ const controlled = flippedProp !== undefined;
82
+ const [own, setOwn] = useState(defaultFlipped);
83
+ const flipped = controlled ? flippedProp : own;
84
+
85
+ const turn = useFlipProgress(flipped).progress;
86
+ const p = progress ?? turn;
87
+
88
+ const toggle = () => {
89
+ const next = !flipped;
90
+ if (!controlled) setOwn(next);
91
+ onFlip?.(next);
92
+ };
93
+
94
+ const horizontal = direction === 'horizontal';
95
+ const axis = horizontal ? 'rotateY' : 'rotateX';
96
+ const frontTurn = p.interpolate({
97
+ inputRange: [0, 1],
98
+ outputRange: ['0deg', '180deg'],
99
+ });
100
+ const backTurn = p.interpolate({
101
+ inputRange: [0, 1],
102
+ outputRange: ['180deg', '360deg'],
103
+ });
104
+ // Lift toward the viewer half-way through the turn.
105
+ const lift = p.interpolate({
106
+ inputRange: [0, 0.5, 1],
107
+ outputRange: [1, 1.05, 1],
108
+ });
109
+ // Swap faces exactly at 90° — backfaceVisibility alone flickers on some Android versions.
110
+ const frontOpacity = p.interpolate({
111
+ inputRange: [0, 0.499, 0.5, 1],
112
+ outputRange: [1, 1, 0, 0],
113
+ });
114
+ const backOpacity = p.interpolate({
115
+ inputRange: [0, 0.499, 0.5, 1],
116
+ outputRange: [0, 0, 1, 1],
117
+ });
118
+
119
+ const face = (turn: Animated.AnimatedInterpolation<string>, opacity: Animated.AnimatedInterpolation<number>) =>
120
+ ({
121
+ borderRadius: radius,
122
+ opacity,
123
+ transform: [{ perspective }, { [axis]: turn } as never, { scale: lift }],
124
+ }) as Animated.WithAnimatedObject<ViewStyle>;
125
+
126
+ // The shadow squeezes along the turning axis as the card goes edge-on (see SQUEEZE) and fades a little.
127
+ const squeeze = p.interpolate(SQUEEZE);
128
+
129
+ const body = (
130
+ <View>
131
+ {shadow ? (
132
+ <Animated.View
133
+ pointerEvents="none"
134
+ style={[
135
+ styles.plate,
136
+ {
137
+ borderRadius: radius,
138
+ opacity: p.interpolate({
139
+ inputRange: [0, 0.5, 1],
140
+ outputRange: [1, 0.4, 1],
141
+ }),
142
+ transform: [{ scale: 0.9 }, horizontal ? { scaleX: squeeze } : { scaleY: squeeze }],
143
+ },
144
+ ]}
145
+ />
146
+ ) : null}
147
+ {/* Faces live in their own (non-collapsing) layer: iOS intersects sibling layers in 3D, so a
148
+ turning face next to the flat shadow plate would be sliced by it. */}
149
+ {/* On Android the wrapper carries the elevation that keeps the faces above the plate; the faces
150
+ get none, since a face turned in 3D would cast a warped shadow wedge below the card. */}
151
+ <View collapsable={false} style={{ elevation: shadow ? 23 : 0 }}>
152
+ {/* The hidden face stays mounted (for the turn) but is hidden from screen readers. */}
153
+ <Animated.View
154
+ style={[styles.face, face(frontTurn, frontOpacity)]}
155
+ pointerEvents={flipped ? 'none' : 'auto'}
156
+ accessibilityElementsHidden={flipped}
157
+ importantForAccessibility={flipped ? 'no-hide-descendants' : 'auto'}
158
+ >
159
+ {front}
160
+ </Animated.View>
161
+ <Animated.View
162
+ style={[styles.face, styles.back, face(backTurn, backOpacity)]}
163
+ pointerEvents={flipped ? 'auto' : 'none'}
164
+ accessibilityElementsHidden={!flipped}
165
+ importantForAccessibility={flipped ? 'auto' : 'no-hide-descendants'}
166
+ >
167
+ {back}
168
+ </Animated.View>
169
+ </View>
170
+ </View>
171
+ );
172
+
173
+ if (trigger === 'none') return <View style={style}>{body}</View>;
174
+ return (
175
+ <Pressable
176
+ onPress={toggle}
177
+ accessibilityRole="button"
178
+ accessibilityLabel={accessibilityLabel}
179
+ accessibilityHint={flipped ? (labels?.toFront ?? 'Showing the back. Tap to flip to the front.') : (labels?.toBack ?? 'Tap to flip to the back.')}
180
+ accessibilityState={{ expanded: flipped }}
181
+ style={style}
182
+ >
183
+ {body}
184
+ </Pressable>
185
+ );
186
+ }
187
+
188
+ const styles = StyleSheet.create({
189
+ face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
190
+ back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
191
+ plate: {
192
+ position: 'absolute',
193
+ top: 0,
194
+ left: 0,
195
+ right: 0,
196
+ bottom: 0,
197
+ backgroundColor: '#000',
198
+ shadowColor: '#000',
199
+ shadowOpacity: 0.4,
200
+ shadowRadius: 20,
201
+ shadowOffset: { width: 0, height: 14 },
202
+ // High enough that Android's shadow spreads past the inset plate (iOS uses the shadow* props).
203
+ elevation: 22,
204
+ },
205
+ });
@@ -0,0 +1,385 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ Image,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type ImageSourcePropType,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import Svg, { Circle, ClipPath, Defs, G, Line, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
13
+
14
+ import { FlipCard, useFlipProgress } from '@/components/ui/flip-card';
15
+ import { TiltCard } from '@/components/ui/tilt-card';
16
+
17
+ export type PaymentCardBrand = 'visa' | 'mastercard' | 'amex' | 'rupay' | 'none';
18
+
19
+ export interface PaymentCardProps {
20
+ /** Card number — spaces are fine. Shown masked to the last 4 unless `masked` is false. */
21
+ number: string;
22
+ /** Cardholder name. */
23
+ name: string;
24
+ /** Expiry, e.g. "09/29". */
25
+ expiry: string;
26
+ /** Shown on the back. */
27
+ cvv?: string;
28
+ brand?: PaymentCardBrand;
29
+ /** Issuer name, top-left. */
30
+ bank?: string;
31
+ /** Small label top-right, e.g. "Debit", "World Elite". */
32
+ label?: string;
33
+ /** Card art — a photo or texture (`require('./card.jpg')` or `{ uri }`). Default: brushed graphite. */
34
+ image?: ImageSourcePropType;
35
+ /** `light` text for dark art (default), `dark` text for pale art. */
36
+ tone?: 'light' | 'dark';
37
+ /** EMV chip finish. */
38
+ chip?: 'gold' | 'silver';
39
+ /** Show only the last 4 digits (default true). */
40
+ masked?: boolean;
41
+ /** Controlled side: true shows the back. */
42
+ flipped?: boolean;
43
+ defaultFlipped?: boolean;
44
+ onFlip?: (flipped: boolean) => void;
45
+ /** Tilt toward the finger (default true). */
46
+ tilt?: boolean;
47
+ /**
48
+ * Tap flips the card (default true). `false` makes it a still card — no tilt, no taps — for use
49
+ * inside something tappable (e.g. a CardStack); a controlled `flipped` still turns it.
50
+ */
51
+ flippable?: boolean;
52
+ /** Frozen cards are frosted over with a label and ignore taps. */
53
+ frozen?: boolean;
54
+ /** Corner radius (default 16 — a real card's corner at phone size). */
55
+ radius?: number;
56
+ /** Every built-in string, for translation ({bank} and {last4} are filled in). */
57
+ labels?: PaymentCardLabels;
58
+ style?: StyleProp<ViewStyle>;
59
+ }
60
+
61
+ export interface PaymentCardLabels {
62
+ cardholder?: string;
63
+ validThru?: string;
64
+ frozen?: string;
65
+ /** Fine print on the back (default "This card is the property of {bank} …"). */
66
+ legal?: string;
67
+ /** Default "Card ending {last4} · tap to turn back". */
68
+ back?: string;
69
+ /** Screen-reader label (default "{bank} card ending {last4}"). */
70
+ accessibility?: string;
71
+ }
72
+
73
+ /**
74
+ * "4821 7590 1234 0317" → groups of 4 (Amex 4-6-5, 14-digit Diners 4-6-4, 19-digit cards get a
75
+ * fifth group). Masking hides every digit but the last four, whatever the length. Keeps • placeholders.
76
+ */
77
+ function formatNumber(number: string, brand: PaymentCardBrand, masked: boolean) {
78
+ const raw = number.replace(/[^\d•]/g, '');
79
+ const shown = masked ? [...raw].map((c, i) => (i < raw.length - 4 ? '•' : c)).join('') : raw;
80
+ const groups = brand === 'amex' ? [4, 6, 5] : raw.length === 14 ? [4, 6, 4] : [4, 4, 4, 4];
81
+ const out: string[] = [];
82
+ let i = 0;
83
+ for (const g of groups) {
84
+ const part = shown.slice(i, i + g);
85
+ if (!part) break;
86
+ out.push(part);
87
+ i += g;
88
+ }
89
+ if (i < shown.length) out.push(shown.slice(i));
90
+ return out.join(' ');
91
+ }
92
+
93
+ function BrandMark({ brand, ink }: { brand: PaymentCardBrand; ink: string }) {
94
+ if (brand === 'none') return null;
95
+ if (brand === 'mastercard') {
96
+ // Red and yellow circles with the true orange overlap.
97
+ return (
98
+ <Svg width={46} height={28} viewBox="0 0 46 28">
99
+ <Defs>
100
+ <ClipPath id="mc-left">
101
+ <Circle cx={15} cy={14} r={13} />
102
+ </ClipPath>
103
+ </Defs>
104
+ <Circle cx={15} cy={14} r={13} fill="#EB001B" />
105
+ <Circle cx={31} cy={14} r={13} fill="#F79E1B" />
106
+ <G clipPath="url(#mc-left)">
107
+ <Circle cx={31} cy={14} r={13} fill="#FF5F00" />
108
+ </G>
109
+ </Svg>
110
+ );
111
+ }
112
+ if (brand === 'visa') {
113
+ return <Text style={[styles.visa, { color: ink }]}>VISA</Text>;
114
+ }
115
+ if (brand === 'amex') {
116
+ return (
117
+ <View style={[styles.amex, { borderColor: ink }]}>
118
+ <Text style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>AMERICAN</Text>
119
+ <Text style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>EXPRESS</Text>
120
+ </View>
121
+ );
122
+ }
123
+ return (
124
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 3 }}>
125
+ <Text style={{ color: ink, fontSize: 18, fontWeight: '800', fontStyle: 'italic', letterSpacing: -0.3 }}>RuPay</Text>
126
+ <Svg width={14} height={16} viewBox="0 0 14 16">
127
+ <Path d="M2 1 L10 8 L2 15 Z" fill="#F47920" />
128
+ <Path d="M6 1 L14 8 L6 15 Z" fill="#0B8E36" />
129
+ </Svg>
130
+ </View>
131
+ );
132
+ }
133
+
134
+ /** EMV chip with contact pads, in gold or silver. */
135
+ function Chip({ finish }: { finish: 'gold' | 'silver' }) {
136
+ const [a, b, c] = finish === 'gold' ? ['#f3dc9d', '#d1ab5c', '#9c7a34'] : ['#f1f2f4', '#c4c7cd', '#8d9199'];
137
+ return (
138
+ <Svg width={42} height={32} viewBox="0 0 42 32">
139
+ <Defs>
140
+ <LinearGradient id={`chip-${finish}`} x1="0" y1="0" x2="1" y2="1">
141
+ <Stop offset="0" stopColor={a} />
142
+ <Stop offset="0.55" stopColor={b} />
143
+ <Stop offset="1" stopColor={c} />
144
+ </LinearGradient>
145
+ </Defs>
146
+ <Rect x={0.5} y={0.5} width={41} height={31} rx={6} fill={`url(#chip-${finish})`} stroke={c} strokeOpacity={0.6} />
147
+ <Path d="M0 11 H14 Q17 11 17 14 V18 Q17 21 14 21 H0 M42 11 H28 Q25 11 25 14 V18 Q25 21 28 21 H42 M14 0 V11 M28 0 V11 M14 32 V21 M28 32 V21" stroke={c} strokeOpacity={0.7} strokeWidth={1} fill="none" />
148
+ </Svg>
149
+ );
150
+ }
151
+
152
+ function Contactless({ ink }: { ink: string }) {
153
+ return (
154
+ <Svg width={20} height={24} viewBox="0 0 20 24">
155
+ {[5, 9, 13].map((r, k) => (
156
+ <Path key={r} d={`M${3 + k * 4} ${12 - r * 0.72} A ${r} ${r} 0 0 1 ${3 + k * 4} ${12 + r * 0.72}`} stroke={ink} strokeOpacity={0.85} strokeWidth={1.8} fill="none" strokeLinecap="round" />
157
+ ))}
158
+ </Svg>
159
+ );
160
+ }
161
+
162
+ /** Brushed graphite, used when no `image` is given. */
163
+ function Graphite() {
164
+ return (
165
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill} preserveAspectRatio="none" viewBox="0 0 100 63">
166
+ <Defs>
167
+ <LinearGradient id="pc-graphite" x1="0" y1="0" x2="1" y2="1">
168
+ <Stop offset="0" stopColor="#3a3a40" />
169
+ <Stop offset="0.45" stopColor="#232327" />
170
+ <Stop offset="1" stopColor="#141416" />
171
+ </LinearGradient>
172
+ </Defs>
173
+ <Rect width="100" height="63" fill="url(#pc-graphite)" />
174
+ {Array.from({ length: 32 }, (_, i) => (
175
+ <Line key={i} x1="0" y1={i * 2} x2="100" y2={i * 2 + 0.6} stroke="#ffffff" strokeOpacity={i % 3 ? 0.012 : 0.025} strokeWidth={0.4} />
176
+ ))}
177
+ </Svg>
178
+ );
179
+ }
180
+
181
+ /** A soft darkening (mostly toward the bottom) so the text stays crisp on busy or pale art. */
182
+ function Shade({ strength }: { strength: number }) {
183
+ return (
184
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill} preserveAspectRatio="none" viewBox="0 0 1 1">
185
+ <Defs>
186
+ <LinearGradient id={`pc-shade-${Math.round(strength * 100)}`} x1="0" y1="0" x2="0" y2="1">
187
+ {/* A whisper at the top keeps the issuer name readable on pale art; most of it is at the bottom. */}
188
+ <Stop offset="0" stopColor="#000" stopOpacity={strength * 0.4} />
189
+ <Stop offset="0.4" stopColor="#000" stopOpacity={0} />
190
+ <Stop offset="1" stopColor="#000" stopOpacity={strength} />
191
+ </LinearGradient>
192
+ </Defs>
193
+ <Rect width="1" height="1" fill={`url(#pc-shade-${Math.round(strength * 100)})`} />
194
+ </Svg>
195
+ );
196
+ }
197
+
198
+ /**
199
+ * A realistic bank card: card art (any photo), EMV chip, contactless mark, embossed number and
200
+ * the network's mark. It tilts toward your finger and flips on tap to the back — magnetic stripe,
201
+ * signature panel with the CVV and a hologram. Frozen state for card controls.
202
+ */
203
+ export function PaymentCard({
204
+ number,
205
+ name,
206
+ expiry,
207
+ cvv,
208
+ brand = 'visa',
209
+ bank,
210
+ label,
211
+ image,
212
+ tone = 'light',
213
+ chip = 'gold',
214
+ masked = true,
215
+ flipped: flippedProp,
216
+ defaultFlipped = false,
217
+ onFlip,
218
+ tilt = true,
219
+ flippable = true,
220
+ frozen = false,
221
+ radius = 16,
222
+ labels,
223
+ style,
224
+ }: PaymentCardProps) {
225
+ const controlled = flippedProp !== undefined;
226
+ const [own, setOwn] = useState(defaultFlipped);
227
+ // A frozen card always shows its front (where the frost and the label are).
228
+ const flipped = (controlled ? flippedProp : own) && !frozen;
229
+ // One flip drives both the card and the tilt shadow, so the shadow narrows as the card turns
230
+ // edge-on — otherwise the flat shadow plate behind it shows as a dark box mid-flip.
231
+ const flip = useFlipProgress(flipped);
232
+
233
+ const toggle = () => {
234
+ if (frozen) return;
235
+ if (!controlled) setOwn(!flipped);
236
+ onFlip?.(!flipped);
237
+ };
238
+
239
+ const ink = tone === 'light' ? '#ffffff' : '#141414';
240
+ const soft = tone === 'light' ? 'rgba(255,255,255,0.72)' : 'rgba(20,20,20,0.62)';
241
+ const emboss = tone === 'light' ? { textShadowColor: 'rgba(0,0,0,0.35)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 1.5 } : null;
242
+ const last4 = number.replace(/\D/g, '').slice(-4);
243
+ const fill = (t: string) => t.replace('{bank}', bank ?? 'the issuer').replace('{last4}', last4);
244
+ const text = {
245
+ cardholder: 'CARDHOLDER',
246
+ validThru: 'VALID THRU',
247
+ frozen: 'Frozen',
248
+ legal: 'This card is the property of {bank} and must be returned on request. Use is subject to the cardholder agreement.',
249
+ back: 'Card ending {last4} · tap to turn back',
250
+ accessibility: bank ? '{bank} card ending {last4}' : 'Card ending {last4}',
251
+ ...labels,
252
+ };
253
+
254
+ const art = (darken: number) => (
255
+ <>
256
+ {image ? <Image alt="" source={image} style={StyleSheet.absoluteFill} resizeMode="cover" accessibilityIgnoresInvertColors /> : <Graphite />}
257
+ {darken > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: `rgba(0,0,0,${darken})` }]} /> : null}
258
+ </>
259
+ );
260
+ const edge = <View pointerEvents="none" style={[StyleSheet.absoluteFill, { borderRadius: radius, borderWidth: 1, borderColor: 'rgba(255,255,255,0.16)' }]} />;
261
+
262
+ const front = (
263
+ <View style={[styles.face, { borderRadius: radius }]}>
264
+ {art(0)}
265
+ {tone === 'light' ? <Shade strength={image ? 0.38 : 0.2} /> : null}
266
+ <View style={styles.inner}>
267
+ <View style={styles.row}>
268
+ <Text style={[{ color: ink, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, emboss]}>{bank ?? ''}</Text>
269
+ {label ? <Text style={[{ color: soft, fontSize: 11, fontWeight: '600', letterSpacing: 1 }, emboss]}>{label.toUpperCase()}</Text> : null}
270
+ </View>
271
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
272
+ <Chip finish={chip} />
273
+ <Contactless ink={ink} />
274
+ </View>
275
+ <View style={{ gap: 10 }}>
276
+ <Text style={[{ color: ink, fontSize: 19, fontWeight: '500', letterSpacing: 2, fontVariant: ['tabular-nums'] }, emboss]}>
277
+ {formatNumber(number, brand, masked)}
278
+ </Text>
279
+ <View style={[styles.row, { alignItems: 'flex-end' }]}>
280
+ <View style={{ flexDirection: 'row', gap: 20, flexShrink: 1 }}>
281
+ <View style={{ flexShrink: 1 }}>
282
+ <Text style={[styles.tiny, { color: soft }]}>{text.cardholder}</Text>
283
+ <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', letterSpacing: 0.8 }, emboss]} numberOfLines={1}>
284
+ {name.toUpperCase()}
285
+ </Text>
286
+ </View>
287
+ <View>
288
+ <Text style={[styles.tiny, { color: soft }]}>{text.validThru}</Text>
289
+ <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', fontVariant: ['tabular-nums'] }, emboss]}>{expiry}</Text>
290
+ </View>
291
+ </View>
292
+ <BrandMark brand={brand} ink={ink} />
293
+ </View>
294
+ </View>
295
+ </View>
296
+ {frozen ? (
297
+ <View style={[StyleSheet.absoluteFill, styles.center, { backgroundColor: 'rgba(214,232,255,0.28)' }]}>
298
+ <View style={styles.frozenPill}>
299
+ <Text style={{ color: '#ffffff', fontSize: 13, fontWeight: '700', letterSpacing: 0.4 }}>❄︎ {text.frozen}</Text>
300
+ </View>
301
+ </View>
302
+ ) : null}
303
+ {edge}
304
+ </View>
305
+ );
306
+
307
+ const back = (
308
+ <View style={[styles.face, { borderRadius: radius }]}>
309
+ {art(0.42)}
310
+ <View style={styles.stripe} />
311
+ <View style={styles.backRow}>
312
+ <View style={styles.signature}>
313
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill} preserveAspectRatio="none" viewBox="0 0 100 10">
314
+ {Array.from({ length: 26 }, (_, i) => (
315
+ <Line key={i} x1={i * 4} y1={10} x2={i * 4 + 6} y2={0} stroke="#9aa3b2" strokeOpacity={0.35} strokeWidth={0.6} />
316
+ ))}
317
+ </Svg>
318
+ </View>
319
+ <View style={styles.cvv}>
320
+ <Text style={{ color: '#111', fontSize: 15, fontWeight: '700', fontStyle: 'italic', letterSpacing: 1.5, fontVariant: ['tabular-nums'] }}>{cvv ?? '•••'}</Text>
321
+ </View>
322
+ </View>
323
+ <View style={styles.backFoot}>
324
+ <View style={{ flex: 1, gap: 3 }}>
325
+ <Text style={{ color: 'rgba(255,255,255,0.78)', fontSize: 9, lineHeight: 12 }}>
326
+ {fill(text.legal)}
327
+ </Text>
328
+ <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 10, fontWeight: '600' }}>{fill(text.back)}</Text>
329
+ </View>
330
+ {/* Security hologram. */}
331
+ <Svg width={40} height={30} viewBox="0 0 40 30">
332
+ <Defs>
333
+ <LinearGradient id="pc-holo" x1="0" y1="0" x2="1" y2="1">
334
+ <Stop offset="0" stopColor="#c9f2ff" />
335
+ <Stop offset="0.3" stopColor="#f5c6ff" />
336
+ <Stop offset="0.6" stopColor="#fff1b8" />
337
+ <Stop offset="1" stopColor="#b8ffe1" />
338
+ </LinearGradient>
339
+ </Defs>
340
+ <Rect x={0} y={0} width={40} height={30} rx={4} fill="url(#pc-holo)" opacity={0.92} />
341
+ <Circle cx={20} cy={15} r={8} fill="none" stroke="#ffffff" strokeOpacity={0.7} strokeWidth={1.2} />
342
+ </Svg>
343
+ </View>
344
+ {edge}
345
+ </View>
346
+ );
347
+
348
+ const card = <FlipCard front={front} back={back} flipped={flipped} progress={flip.progress} trigger="none" perspective={1600} radius={radius} shadow={false} />;
349
+ const a11y = [fill(text.accessibility), frozen ? text.frozen : null].filter(Boolean).join(', ');
350
+
351
+ if (!flippable) {
352
+ // A still card: no tilt and no taps of its own, so a parent (e.g. a CardStack) gets the touch.
353
+ return (
354
+ <View accessible accessibilityLabel={a11y} style={[{ borderRadius: radius }, style]}>
355
+ {card}
356
+ </View>
357
+ );
358
+ }
359
+ return tilt ? (
360
+ <TiltCard onPress={toggle} glare="soft" maxTilt={10} radius={radius} shadowScaleX={flip.shadowScaleX} accessibilityLabel={a11y} style={style}>
361
+ {card}
362
+ </TiltCard>
363
+ ) : (
364
+ <Pressable onPress={toggle} accessibilityRole="button" accessibilityState={{ disabled: frozen }} accessibilityLabel={a11y} style={[styles.flat, { borderRadius: radius }, style]}>
365
+ {card}
366
+ </Pressable>
367
+ );
368
+ }
369
+
370
+ const styles = StyleSheet.create({
371
+ face: { aspectRatio: 1.586, overflow: 'hidden', backgroundColor: '#1c1c20' },
372
+ inner: { flex: 1, padding: 20, justifyContent: 'space-between' },
373
+ row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
374
+ tiny: { fontSize: 8, fontWeight: '700', letterSpacing: 1.2, marginBottom: 2 },
375
+ visa: { fontSize: 24, fontWeight: '900', fontStyle: 'italic', letterSpacing: -0.5 },
376
+ amex: { borderWidth: 1.5, borderRadius: 3, paddingHorizontal: 5, paddingVertical: 3 },
377
+ center: { alignItems: 'center', justifyContent: 'center' },
378
+ frozenPill: { paddingHorizontal: 14, paddingVertical: 7, borderRadius: 999, backgroundColor: 'rgba(10,20,35,0.45)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.35)' },
379
+ stripe: { position: 'absolute', left: 0, right: 0, top: '13%', height: '19%', backgroundColor: '#0b0b0c' },
380
+ backRow: { position: 'absolute', left: 18, right: 18, top: '42%', height: '16%', flexDirection: 'row', gap: 8 },
381
+ signature: { flex: 1, borderRadius: 3, backgroundColor: '#eef0f4', overflow: 'hidden' },
382
+ cvv: { width: 58, borderRadius: 3, backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center' },
383
+ backFoot: { position: 'absolute', left: 18, right: 18, bottom: 16, flexDirection: 'row', alignItems: 'flex-end', gap: 12 },
384
+ flat: { shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 18, shadowOffset: { width: 0, height: 10 }, elevation: 8 },
385
+ });