@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,543 @@
1
+ import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
2
+ import { Animated, Image, PixelRatio, StyleSheet, Text, View, type ImageSourcePropType, type StyleProp, type ViewStyle } from 'react-native';
3
+ import Svg, { Circle, ClipPath, Defs, G, Line, LinearGradient, Path, Pattern, Rect, Stop } from 'react-native-svg';
4
+
5
+ import { alpha, motion, readableOn } from '@/lib/kit';
6
+
7
+ export type TicketSide = 'top' | 'right' | 'bottom' | 'left';
8
+ /** `perforated` = small round bites (a stamp), `scalloped` = round bumps, `zigzag` = torn-receipt teeth. */
9
+ export type TicketEdge = 'straight' | 'perforated' | 'scalloped' | 'zigzag';
10
+ export type TicketPattern = 'none' | 'dots' | 'stripes' | 'grid' | 'waves' | 'sunburst' | 'confetti';
11
+ /** One radius for every corner, or [topLeft, topRight, bottomRight, bottomLeft]. Negative cuts the corner inward. */
12
+ export type TicketCorners = number | readonly [number, number, number, number];
13
+
14
+ export interface TicketNotch {
15
+ side: TicketSide;
16
+ /** Centre along that side, in points from its top / left end. */
17
+ at: number;
18
+ radius: number;
19
+ }
20
+
21
+ export interface TicketRect {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+
28
+ export interface TicketGeometry {
29
+ width: number;
30
+ height: number;
31
+ corners?: TicketCorners;
32
+ /** Round bites cut into a side — e.g. both ends of a tear line. */
33
+ notches?: readonly TicketNotch[];
34
+ /** Pattern per side (default straight). */
35
+ edges?: Partial<Record<TicketSide, TicketEdge>>;
36
+ /** Bite radius, scallop depth or tooth height of the edge patterns (default 5). */
37
+ edgeSize?: number;
38
+ }
39
+
40
+ const SIDES: readonly TicketSide[] = ['top', 'right', 'bottom', 'left'];
41
+ const PREV: Record<TicketSide, TicketSide> = { top: 'left', right: 'top', bottom: 'right', left: 'bottom' };
42
+ const NEXT: Record<TicketSide, TicketSide> = { top: 'right', right: 'bottom', bottom: 'left', left: 'top' };
43
+ const TURN: Record<TicketSide, number> = { top: 0, right: 90, bottom: 180, left: 270 };
44
+
45
+ const r2 = (n: number) => Math.round(n * 100) / 100;
46
+
47
+ function cornerList(c: TicketCorners | undefined): [number, number, number, number] {
48
+ if (c === undefined) return [0, 0, 0, 0];
49
+ return typeof c === 'number' ? [c, c, c, c] : [c[0], c[1], c[2], c[3]];
50
+ }
51
+
52
+ /** How far a side's pattern reaches into the shape. */
53
+ const depthOf = (edge: TicketEdge | undefined, size: number) => (edge && edge !== 'straight' ? size : 0);
54
+
55
+ /**
56
+ * SVG path data for a ticket outline: rounded or cut-in corners, notches and edge patterns,
57
+ * drawn clockwise from the top-left corner.
58
+ */
59
+ export function ticketPath({ width: w, height: h, corners, notches = [], edges = {}, edgeSize = 5 }: TicketGeometry): string {
60
+ if (!(w > 0 && h > 0)) return '';
61
+ const [tl, tr, br, bl] = cornerList(corners);
62
+ const s = edgeSize;
63
+ // Scallops and zigzags stand on a baseline set in from the edge; their tips touch the edge.
64
+ const base = (side: TicketSide) => (edges[side] === 'scalloped' || edges[side] === 'zigzag' ? s : 0);
65
+ const length: Record<TicketSide, number> = { top: w, right: h, bottom: w, left: h };
66
+ const startCorner: Record<TicketSide, number> = { top: tl, right: tr, bottom: br, left: bl };
67
+ const endCorner: Record<TicketSide, number> = { top: tr, right: br, bottom: bl, left: tl };
68
+ // Each side in its own frame: u runs along it (clockwise), v points into the shape. Every frame
69
+ // is a rotation of the top one, so arc sweep flags mean the same thing on all four sides.
70
+ const at = (side: TicketSide, u: number, v: number) => {
71
+ const [x, y] = side === 'top' ? [u, v] : side === 'right' ? [w - v, u] : side === 'bottom' ? [w - u, h - v] : [v, h - u];
72
+ return `${r2(x)} ${r2(y)}`;
73
+ };
74
+
75
+ let d = '';
76
+ for (const side of SIDES) {
77
+ const b = base(side);
78
+ const u0 = base(PREV[side]) + Math.abs(startCorner[side]);
79
+ const u1 = length[side] - base(NEXT[side]) - Math.abs(endCorner[side]);
80
+ if (side === 'top') d = `M ${at(side, u0, b)}`;
81
+ const L = (u: number, v = b) => ` L ${at(side, u, v)}`;
82
+ const A = (rx: number, ry: number, sweep: 0 | 1, u: number) => ` A ${r2(rx)} ${r2(ry)} ${TURN[side]} 0 ${sweep} ${at(side, u, b)}`;
83
+
84
+ const cuts = notches
85
+ .filter((n) => n.side === side)
86
+ .map((n) => ({ u: side === 'top' || side === 'right' ? n.at : length[side] - n.at, r: n.radius }))
87
+ .filter((c) => c.r > 0 && c.u - c.r >= u0 - 0.01 && c.u + c.r <= u1 + 0.01)
88
+ .sort((a, z) => a.u - z.u);
89
+
90
+ // Fills [a, z] with the side's pattern: the pen starts at (a, b) and ends at (z, b).
91
+ const run = (a: number, z: number) => {
92
+ const edge = edges[side] ?? 'straight';
93
+ const span = z - a;
94
+ if (edge === 'perforated' && span > 3 * s) {
95
+ const gap = s * 1.1;
96
+ const n = Math.floor((span - gap) / (2 * s + gap));
97
+ let u = a + (span - (n * 2 * s + (n - 1) * gap)) / 2;
98
+ for (let i = 0; i < n; i++, u += 2 * s + gap) d += L(u) + A(s, s, 0, u + 2 * s);
99
+ } else if (edge === 'scalloped' && span > s) {
100
+ const n = Math.max(1, Math.round(span / (2.4 * s)));
101
+ for (let i = 1; i <= n; i++) d += A(span / n / 2, s, 1, a + (i * span) / n);
102
+ return;
103
+ } else if (edge === 'zigzag' && span > s) {
104
+ const n = Math.max(1, Math.round(span / (2 * s)));
105
+ for (let i = 0; i < n; i++) d += L(a + ((i + 0.5) * span) / n, b - s) + L(a + ((i + 1) * span) / n);
106
+ return;
107
+ }
108
+ d += L(z);
109
+ };
110
+
111
+ let pen = u0;
112
+ for (const c of cuts) {
113
+ run(pen, c.u - c.r);
114
+ d += A(c.r, c.r, 0, c.u + c.r);
115
+ pen = c.u + c.r;
116
+ }
117
+ run(pen, u1);
118
+
119
+ // Into the next side: a convex corner turns clockwise, a cut-in corner the other way.
120
+ const c = endCorner[side];
121
+ const next = at(NEXT[side], b + Math.abs(c), base(NEXT[side]));
122
+ d += c === 0 ? ` L ${next}` : ` A ${r2(Math.abs(c))} ${r2(Math.abs(c))} 0 0 ${c > 0 ? 1 : 0} ${next}`;
123
+ }
124
+ return `${d} Z`;
125
+ }
126
+
127
+ /**
128
+ * Rectangles that cover the shape but none of its holes (notches, cut-in corners, edge patterns)
129
+ * — e.g. where a TiltCard's reflection may fall (`glareClip`). `offset` shifts them into a parent's
130
+ * coordinates.
131
+ */
132
+ export function ticketSafeRects(g: TicketGeometry, offset: { x: number; y: number } = { x: 0, y: 0 }): TicketRect[] {
133
+ const { width: w, height: h, notches = [], edges = {}, edgeSize = 5 } = g;
134
+ if (!(w > 0 && h > 0)) return [];
135
+ const [tl, tr, br, bl] = cornerList(g.corners);
136
+ const x0 = depthOf(edges.left, edgeSize);
137
+ const y0 = depthOf(edges.top, edgeSize);
138
+ const x1 = w - depthOf(edges.right, edgeSize);
139
+ const y1 = h - depthOf(edges.bottom, edgeSize);
140
+ // Each round hole is covered by a few stepped rects that hug its curve, so almost nothing
141
+ // around it is left without light.
142
+ const holes: [number, number, number, number][] = [];
143
+ const STEPS = [0, 0.45, 0.75, 1];
144
+ /** A quarter disc of radius r at corner (cx, cy), opening toward (dx, dy) = ±1. */
145
+ const quarter = (cx: number, cy: number, r: number, dx: number, dy: number) => {
146
+ for (let i = 0; i < STEPS.length - 1; i++) {
147
+ const reach = r * Math.sqrt(1 - STEPS[i] * STEPS[i]);
148
+ const [a, b] = [cy + dy * r * STEPS[i], cy + dy * r * STEPS[i + 1]];
149
+ holes.push([Math.min(cx, cx + dx * reach), Math.min(a, b), Math.max(cx, cx + dx * reach), Math.max(a, b)]);
150
+ }
151
+ };
152
+ /** The same with x and y swapped (steps run along x). */
153
+ const quarterX = (cx: number, cy: number, r: number, dx: number, dy: number) => {
154
+ for (let i = 0; i < STEPS.length - 1; i++) {
155
+ const reach = r * Math.sqrt(1 - STEPS[i] * STEPS[i]);
156
+ const [a, b] = [cx + dx * r * STEPS[i], cx + dx * r * STEPS[i + 1]];
157
+ holes.push([Math.min(a, b), Math.min(cy, cy + dy * reach), Math.max(a, b), Math.max(cy, cy + dy * reach)]);
158
+ }
159
+ };
160
+ for (const n of notches) {
161
+ const r = n.radius;
162
+ // Two quarter discs make the notch; the strip under a patterned edge goes too.
163
+ if (n.side === 'left') {
164
+ quarter(x0, n.at, r, 1, -1);
165
+ quarter(x0, n.at, r, 1, 1);
166
+ holes.push([0, n.at - r, x0, n.at + r]);
167
+ } else if (n.side === 'right') {
168
+ quarter(x1, n.at, r, -1, -1);
169
+ quarter(x1, n.at, r, -1, 1);
170
+ holes.push([x1, n.at - r, w, n.at + r]);
171
+ } else if (n.side === 'top') {
172
+ quarterX(n.at, y0, r, -1, 1);
173
+ quarterX(n.at, y0, r, 1, 1);
174
+ holes.push([n.at - r, 0, n.at + r, y0]);
175
+ } else {
176
+ quarterX(n.at, y1, r, -1, -1);
177
+ quarterX(n.at, y1, r, 1, -1);
178
+ holes.push([n.at - r, y1, n.at + r, h]);
179
+ }
180
+ }
181
+ if (tl < 0) quarter(0, 0, -tl, 1, 1);
182
+ if (tr < 0) quarter(w, 0, -tr, -1, 1);
183
+ if (br < 0) quarter(w, h, -br, -1, -1);
184
+ if (bl < 0) quarter(0, h, -bl, 1, -1);
185
+ // Every edge on the device pixel grid (holes grow outward): neighbouring windows then meet
186
+ // exactly, so a blended reflection shows no hairline seams where they join.
187
+ const px = PixelRatio.get();
188
+ const down = (v: number) => Math.floor(v * px) / px;
189
+ const up = (v: number) => Math.ceil(v * px) / px;
190
+ const near = (v: number) => Math.round(v * px) / px;
191
+ for (const hole of holes) [hole[0], hole[1], hole[2], hole[3]] = [down(hole[0]), down(hole[1]), up(hole[2]), up(hole[3])];
192
+
193
+ const cuts = (lo: number, hi: number, more: number[]) => [...new Set([lo, hi, ...more])].filter((v) => v >= lo && v <= hi).sort((a, b) => a - b);
194
+ const xs = cuts(near(x0), near(x1), holes.flatMap(([a, , c]) => [a, c]));
195
+ const ys = cuts(near(y0), near(y1), holes.flatMap(([, b, , d]) => [b, d]));
196
+ const rows: { y: number; h: number; spans: [number, number][] }[] = [];
197
+ // Every cut is on the pixel grid, so anything thinner than half a pixel is a rounding twin; a
198
+ // one-pixel row (where two stepped holes overlap) must stay, or the reflection shows a gap there.
199
+ const thin = 0.5 / px;
200
+ for (let j = 0; j < ys.length - 1; j++) {
201
+ const [ya, yb] = [ys[j], ys[j + 1]];
202
+ if (yb - ya < thin) continue;
203
+ const spans: [number, number][] = [];
204
+ for (let i = 0; i < xs.length - 1; i++) {
205
+ const [xa, xb] = [xs[i], xs[i + 1]];
206
+ if (xb - xa < thin || holes.some(([hx0, hy0, hx1, hy1]) => xa < hx1 && xb > hx0 && ya < hy1 && yb > hy0)) continue;
207
+ const last = spans[spans.length - 1];
208
+ if (last && Math.abs(last[1] - xa) < 0.01) last[1] = xb;
209
+ else spans.push([xa, xb]);
210
+ }
211
+ const prev = rows[rows.length - 1];
212
+ const same = prev && prev.spans.length === spans.length && prev.spans.every((s, i) => s[0] === spans[i][0] && s[1] === spans[i][1]);
213
+ if (prev && same && Math.abs(prev.y + prev.h - ya) < 0.01) prev.h += yb - ya;
214
+ else rows.push({ y: ya, h: yb - ya, spans });
215
+ }
216
+ return rows.flatMap((r) => r.spans.map(([a, b]) => ({ x: a + offset.x, y: r.y + offset.y, width: b - a, height: r.h })));
217
+ }
218
+
219
+ /** A small, stable random sequence from a string. */
220
+ function random(seed: string) {
221
+ let h = 2166136261;
222
+ for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 16777619);
223
+ return () => {
224
+ h ^= h << 13;
225
+ h ^= h >>> 17;
226
+ h ^= h << 5;
227
+ return ((h >>> 0) % 10000) / 10000;
228
+ };
229
+ }
230
+
231
+ const CONFETTI = ['#ffd23f', '#ff5fa2', '#3ddcff', '#7dff7a', '#ffffff'];
232
+
233
+ /** A rubber stamp ("USED", "CHECKED IN") that thumps down whenever its text changes. */
234
+ export function TicketStamp({ text, color, size = 17, style }: { text: string; color: string; size?: number; style?: StyleProp<ViewStyle> }) {
235
+ const thump = useRef(new Animated.Value(0)).current;
236
+ useEffect(() => {
237
+ thump.setValue(0);
238
+ Animated.spring(thump, { toValue: 1, ...motion.bouncy, useNativeDriver: true }).start();
239
+ }, [text, thump]);
240
+ return (
241
+ <Animated.View
242
+ pointerEvents="none"
243
+ style={[
244
+ {
245
+ opacity: thump.interpolate({ inputRange: [0, 0.4, 1], outputRange: [0, 1, 0.92] }),
246
+ transform: [{ rotate: '-13deg' }, { scale: thump.interpolate({ inputRange: [0, 1], outputRange: [1.9, 1] }) }],
247
+ },
248
+ style,
249
+ ]}
250
+ >
251
+ <View style={{ borderWidth: 2.5, borderRadius: 9, padding: 3, borderColor: color }}>
252
+ <View style={{ borderWidth: 1, borderRadius: 6, paddingHorizontal: size * 0.7, paddingVertical: size * 0.22, borderColor: alpha(color, 0.55) }}>
253
+ <Text style={{ color, fontSize: size, fontWeight: '900', letterSpacing: size * 0.13 }}>{text.toUpperCase()}</Text>
254
+ </View>
255
+ </View>
256
+ </Animated.View>
257
+ );
258
+ }
259
+
260
+ /** A QR-style matrix (finder squares, timing lines, data) drawn as one path — a look-alike, not a real code. */
261
+ export function QrLike({ code, size, color = '#111111' }: { code: string; size: number; color?: string }) {
262
+ const d = useMemo(() => {
263
+ const N = 25;
264
+ const next = random(code + '|qr');
265
+ const finder = (x: number, y: number) => {
266
+ for (const [fx, fy] of [[0, 0], [N - 7, 0], [0, N - 7]]) {
267
+ const [u, v] = [x - fx, y - fy];
268
+ if (u >= -1 && u <= 7 && v >= -1 && v <= 7) {
269
+ if (u < 0 || v < 0 || u > 6 || v > 6) return 0; // the white ring around a finder
270
+ return u === 0 || v === 0 || u === 6 || v === 6 || (u >= 2 && u <= 4 && v >= 2 && v <= 4) ? 1 : 0;
271
+ }
272
+ }
273
+ return -1;
274
+ };
275
+ let out = '';
276
+ for (let y = 0; y < N; y++)
277
+ for (let x = 0; x < N; x++) {
278
+ const f = finder(x, y);
279
+ const on = f >= 0 ? f === 1 : x === 6 || y === 6 ? (x + y) % 2 === 0 : next() < 0.47;
280
+ if (on) out += `M${x} ${y}h1v1h-1z`;
281
+ }
282
+ return out;
283
+ }, [code]);
284
+ return (
285
+ <Svg width={size} height={size} viewBox="0 0 25 25">
286
+ <Path d={d} fill={color} />
287
+ </Svg>
288
+ );
289
+ }
290
+
291
+ /** A Code 128-style barcode drawn as one path — a look-alike. `vertical` turns it on its side. */
292
+ export function BarcodeLike({ code, width, height, color = '#111111', vertical = false }: { code: string; width: number; height: number; color?: string; vertical?: boolean }) {
293
+ const { d, units } = useMemo(() => {
294
+ const next = random(code + '|bars');
295
+ const bars: [number, number][] = [];
296
+ let u = 0;
297
+ const push = (bar: number, gap: number) => {
298
+ bars.push([u, bar]);
299
+ u += bar + gap;
300
+ };
301
+ push(2, 1);
302
+ push(1, 1);
303
+ for (let i = 0; i < 34; i++) push(1 + Math.floor(next() * 4), 1 + Math.floor(next() * 3));
304
+ push(2, 1);
305
+ push(1, 1);
306
+ push(2, 0);
307
+ return { d: bars.map(([at, w]) => (vertical ? `M0 ${at}h1v${w}h-1z` : `M${at} 0h${w}v1h-${w}z`)).join(''), units: u };
308
+ }, [code, vertical]);
309
+ return (
310
+ <Svg width={width} height={height} viewBox={vertical ? `0 0 1 ${units}` : `0 0 ${units} 1`} preserveAspectRatio="none">
311
+ <Path d={d} fill={color} />
312
+ </Svg>
313
+ );
314
+ }
315
+
316
+ export interface TicketShapeProps extends Omit<TicketGeometry, 'width' | 'height'> {
317
+ children?: React.ReactNode;
318
+ /** A colour, or 2+ colours for a gradient (default white). */
319
+ fill?: string | readonly string[];
320
+ /** Gradient direction in degrees: 0 runs left → right, 90 top → bottom (default 125). */
321
+ angle?: number;
322
+ /** Photo painted inside the outline (cover), over the fill (`require()` or `{ uri }`). */
323
+ image?: ImageSourcePropType;
324
+ imageOpacity?: number;
325
+ /** Darkens the paint toward the bottom (0 – 1) so white text reads on a photo — inside the outline only. */
326
+ shade?: number;
327
+ /** Texture over the fill. */
328
+ pattern?: TicketPattern;
329
+ /** Pattern ink (default: white or black at low opacity, whichever suits the fill; confetti is multicolour). */
330
+ patternColor?: string;
331
+ /** Keeps the confetti scatter the same on every render (e.g. the coupon code). */
332
+ seed?: string;
333
+ /** Outline colour. */
334
+ stroke?: string;
335
+ strokeWidth?: number;
336
+ /** Dash the outline — a "cut here" coupon. */
337
+ dashed?: boolean;
338
+ /** Dashed tear line along this side. */
339
+ tear?: TicketSide;
340
+ tearColor?: string;
341
+ /**
342
+ * Paint as one piece of a bigger ticket: that ticket's rect in this shape's points. Gradients,
343
+ * patterns and photos then line up across the pieces.
344
+ */
345
+ frame?: TicketRect;
346
+ /** Reports the measured size after layout. */
347
+ onSize?: (size: { width: number; height: number }) => void;
348
+ /** Padding, width, margins… — the shape takes its size from this box. */
349
+ style?: StyleProp<ViewStyle>;
350
+ }
351
+
352
+ /**
353
+ * A View painted as a ticket: notches, cut-in corners and patterned edges are real holes (the
354
+ * screen behind shows through), and the fill can be a colour, a gradient, a texture or a photo.
355
+ * Children sit on top; pad them away from patterned edges.
356
+ */
357
+ export function TicketShape({
358
+ children,
359
+ corners,
360
+ notches,
361
+ edges,
362
+ edgeSize = 5,
363
+ fill = '#ffffff',
364
+ angle = 125,
365
+ image,
366
+ imageOpacity = 1,
367
+ shade = 0,
368
+ pattern = 'none',
369
+ patternColor,
370
+ seed = 'ticket',
371
+ stroke,
372
+ strokeWidth = 1.5,
373
+ dashed = false,
374
+ tear,
375
+ tearColor,
376
+ frame: frameProp,
377
+ onSize,
378
+ style,
379
+ }: TicketShapeProps) {
380
+ const [size, setSize] = useState({ w: 0, h: 0 });
381
+ const id = 'ts' + useId().replace(/[^A-Za-z0-9_-]/g, '');
382
+ const { w, h } = size;
383
+ const colors = typeof fill === 'string' ? [fill] : fill.length ? fill : ['#ffffff'];
384
+ const ink = readableOn(colors[Math.floor((colors.length - 1) / 2)]);
385
+ const d = ticketPath({ width: w, height: h, corners, notches, edges, edgeSize });
386
+ const f = frameProp ?? { x: 0, y: 0, width: w, height: h };
387
+
388
+ let paint: React.ReactNode = null;
389
+ if (w && h) {
390
+ // CSS-style gradient line: the first and last colours land exactly on opposite corners.
391
+ const a = (angle * Math.PI) / 180;
392
+ const half = (Math.abs(f.width * Math.cos(a)) + Math.abs(f.height * Math.sin(a))) / 2;
393
+ const [cx, cy] = [f.x + f.width / 2, f.y + f.height / 2];
394
+ const pc = patternColor ?? alpha(ink, 0.13);
395
+ const clip = `url(#${id}-clip)`;
396
+ const corner = cornerList(corners);
397
+ const [s0, s1] = tear === 'top' || tear === 'bottom' ? (tear === 'top' ? [corner[0], corner[1]] : [corner[3], corner[2]]) : tear === 'left' ? [corner[0], corner[3]] : [corner[1], corner[2]];
398
+ const inset = [Math.abs(s0) + 7, Math.abs(s1) + 7];
399
+ const tearLine =
400
+ tear === 'top' || tear === 'bottom'
401
+ ? { x1: inset[0], x2: w - inset[1], y1: tear === 'top' ? 0.75 : h - 0.75, y2: tear === 'top' ? 0.75 : h - 0.75 }
402
+ : { y1: inset[0], y2: h - inset[1], x1: tear === 'left' ? 0.75 : w - 0.75, x2: tear === 'left' ? 0.75 : w - 0.75 };
403
+
404
+ let texture: React.ReactNode = null;
405
+ if (pattern === 'sunburst') {
406
+ const R = Math.hypot(f.width, f.height);
407
+ const n = 18;
408
+ let rays = '';
409
+ for (let i = 0; i < n; i++) {
410
+ const t0 = (i * 2 * Math.PI) / n;
411
+ const t1 = t0 + Math.PI / n;
412
+ rays += `M ${r2(cx)} ${r2(cy)} L ${r2(cx + R * Math.cos(t0))} ${r2(cy + R * Math.sin(t0))} L ${r2(cx + R * Math.cos(t1))} ${r2(cy + R * Math.sin(t1))} Z `;
413
+ }
414
+ texture = <Path d={rays} fill={patternColor ?? alpha(ink, 0.08)} />;
415
+ } else if (pattern === 'confetti') {
416
+ const rnd = random(seed);
417
+ const count = Math.max(10, Math.min(46, Math.round((f.width * f.height) / 1500)));
418
+ texture = Array.from({ length: count }, (_, i) => {
419
+ const x = f.x + rnd() * f.width;
420
+ const y = f.y + rnd() * f.height;
421
+ const turn = Math.round(rnd() * 180);
422
+ const color = patternColor ?? CONFETTI[Math.floor(rnd() * CONFETTI.length)];
423
+ const o = 0.35 + rnd() * 0.4;
424
+ return rnd() < 0.55 ? (
425
+ <Rect key={i} x={x - 1.6} y={y - 4.5} width={3.2} height={9} rx={1} fill={color} opacity={o} transform={`rotate(${turn} ${r2(x)} ${r2(y)})`} />
426
+ ) : (
427
+ <Circle key={i} cx={x} cy={y} r={2.1} fill={color} opacity={o} />
428
+ );
429
+ });
430
+ } else if (pattern !== 'none') {
431
+ texture = <Rect width={w} height={h} fill={`url(#${id}-pattern)`} />;
432
+ }
433
+
434
+ const fillDefs =
435
+ colors.length > 1 ? (
436
+ <LinearGradient
437
+ id={`${id}-fill`}
438
+ gradientUnits="userSpaceOnUse"
439
+ x1={cx - Math.cos(a) * half}
440
+ y1={cy - Math.sin(a) * half}
441
+ x2={cx + Math.cos(a) * half}
442
+ y2={cy + Math.sin(a) * half}
443
+ >
444
+ {colors.map((c, i) => (
445
+ <Stop key={i} offset={i / (colors.length - 1)} stopColor={c} />
446
+ ))}
447
+ </LinearGradient>
448
+ ) : null;
449
+ const fillPath = <Path d={d} fill={colors.length > 1 ? `url(#${id}-fill)` : colors[0]} />;
450
+ const overlayDefs = (
451
+ <>
452
+ <ClipPath id={`${id}-clip`}>
453
+ <Path d={d} />
454
+ </ClipPath>
455
+ {shade > 0 ? (
456
+ <LinearGradient id={`${id}-shade`} x1="0" y1="0" x2="0" y2="1">
457
+ <Stop offset="0" stopColor="#000000" stopOpacity={shade * 0.45} />
458
+ <Stop offset="0.42" stopColor="#000000" stopOpacity={shade * 0.12} />
459
+ <Stop offset="1" stopColor="#000000" stopOpacity={shade} />
460
+ </LinearGradient>
461
+ ) : null}
462
+ {pattern === 'dots' ? (
463
+ <Pattern id={`${id}-pattern`} patternUnits="userSpaceOnUse" x={f.x} y={f.y} width={14} height={14}>
464
+ <Circle cx={7} cy={7} r={1.7} fill={pc} />
465
+ </Pattern>
466
+ ) : pattern === 'stripes' ? (
467
+ <Pattern id={`${id}-pattern`} patternUnits="userSpaceOnUse" x={f.x} y={f.y} width={12} height={12} patternTransform="rotate(-35)">
468
+ <Rect width={5} height={12} fill={pc} />
469
+ </Pattern>
470
+ ) : pattern === 'grid' ? (
471
+ <Pattern id={`${id}-pattern`} patternUnits="userSpaceOnUse" x={f.x} y={f.y} width={16} height={16}>
472
+ <Path d="M 16 0 L 0 0 0 16" fill="none" stroke={pc} strokeWidth={1} />
473
+ </Pattern>
474
+ ) : pattern === 'waves' ? (
475
+ <Pattern id={`${id}-pattern`} patternUnits="userSpaceOnUse" x={f.x} y={f.y} width={24} height={12}>
476
+ <Path d="M 0 6 Q 6 0 12 6 T 24 6" fill="none" stroke={pc} strokeWidth={1.4} />
477
+ </Pattern>
478
+ ) : null}
479
+ </>
480
+ );
481
+ const overlays = (
482
+ <>
483
+ {shade > 0 ? <Rect width={w} height={h} fill={`url(#${id}-shade)`} clipPath={clip} /> : null}
484
+ {texture ? <G clipPath={clip}>{texture}</G> : null}
485
+ {stroke ? <Path d={d} fill="none" stroke={stroke} strokeWidth={strokeWidth} strokeDasharray={dashed ? [7, 5] : undefined} strokeLinecap="round" /> : null}
486
+ {tear ? <Line {...tearLine} stroke={tearColor ?? alpha(ink, 0.28)} strokeWidth={1.6} strokeDasharray={[6, 5]} strokeLinecap="round" /> : null}
487
+ </>
488
+ );
489
+ // A photo is an ordinary <Image> (it loads and caches like any other image on every platform),
490
+ // shown through windows that cover the outline but none of its holes; the fill sits under it and
491
+ // the shade, texture and lines go on top in a second layer.
492
+ const windows = image ? ticketSafeRects({ width: w, height: h, corners, notches, edges, edgeSize }) : null;
493
+ // Both layers are sized "100%": react-native-svg truncates a numeric width (317.7 → 317), which
494
+ // would leave a sliver of the card unpainted along the right and bottom edges.
495
+ paint = (
496
+ <View pointerEvents="none" style={StyleSheet.absoluteFill} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
497
+ <Svg width="100%" height="100%">
498
+ <Defs>
499
+ {fillDefs}
500
+ {windows ? null : overlayDefs}
501
+ </Defs>
502
+ {fillPath}
503
+ {windows ? null : overlays}
504
+ </Svg>
505
+ {windows ? (
506
+ <>
507
+ {windows.map((r, i) => (
508
+ <View key={i} style={{ position: 'absolute', left: r.x, top: r.y, width: r.width, height: r.height, overflow: 'hidden' }}>
509
+ <Image
510
+ alt=""
511
+ source={image!}
512
+ resizeMode="cover"
513
+ accessibilityIgnoresInvertColors
514
+ style={{ position: 'absolute', left: f.x - r.x, top: f.y - r.y, width: f.width, height: f.height, opacity: imageOpacity }}
515
+ />
516
+ </View>
517
+ ))}
518
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
519
+ <Defs>{overlayDefs}</Defs>
520
+ {overlays}
521
+ </Svg>
522
+ </>
523
+ ) : null}
524
+ </View>
525
+ );
526
+ }
527
+
528
+ return (
529
+ <View
530
+ style={style}
531
+ onLayout={(e) => {
532
+ const { width, height } = e.nativeEvent.layout;
533
+ if (width !== w || height !== h) {
534
+ setSize({ w: width, h: height });
535
+ onSize?.({ width, height });
536
+ }
537
+ }}
538
+ >
539
+ {paint}
540
+ {children}
541
+ </View>
542
+ );
543
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "ticket-shape",
3
+ "type": "lib",
4
+ "title": "Ticket shape",
5
+ "description": "Ticket and coupon outlines with real cut-outs — tear-line notches and perforated, scalloped or zigzag edges — painted with a colour, a gradient, a pattern or a photo, plus look-alike QR codes and barcodes. Shared by Ticket, Coupon and Scratch Card.",
6
+ "tier": "free",
7
+ "dependencies": [
8
+ "react-native-svg"
9
+ ],
10
+ "registryDependencies": [
11
+ "kit"
12
+ ],
13
+ "sharedFiles": [
14
+ "lib/kit.tsx"
15
+ ],
16
+ "filesDir": "ticket-shape/files"
17
+ }