rnwind 0.0.1 → 0.0.2

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 (330) hide show
  1. package/lib/cjs/core/parser/animation.cjs +427 -0
  2. package/lib/cjs/core/parser/animation.cjs.map +1 -0
  3. package/lib/cjs/core/parser/animation.d.ts +126 -0
  4. package/lib/cjs/core/parser/border-dispatcher.cjs +180 -0
  5. package/lib/cjs/core/parser/border-dispatcher.cjs.map +1 -0
  6. package/lib/cjs/core/parser/border-dispatcher.d.ts +15 -0
  7. package/lib/cjs/core/parser/case-convert.cjs +15 -0
  8. package/lib/cjs/core/parser/case-convert.cjs.map +1 -0
  9. package/lib/cjs/core/parser/case-convert.d.ts +6 -0
  10. package/lib/cjs/core/parser/color-properties-dispatcher.cjs +84 -0
  11. package/lib/cjs/core/parser/color-properties-dispatcher.cjs.map +1 -0
  12. package/lib/cjs/core/parser/color-properties-dispatcher.d.ts +19 -0
  13. package/lib/cjs/core/parser/color.cjs +164 -0
  14. package/lib/cjs/core/parser/color.cjs.map +1 -0
  15. package/lib/cjs/core/parser/color.d.ts +12 -0
  16. package/lib/cjs/core/parser/constants.cjs +21 -0
  17. package/lib/cjs/core/parser/constants.cjs.map +1 -0
  18. package/lib/cjs/core/parser/constants.d.ts +8 -0
  19. package/lib/cjs/core/parser/declaration.cjs +347 -0
  20. package/lib/cjs/core/parser/declaration.cjs.map +1 -0
  21. package/lib/cjs/core/parser/declaration.d.ts +15 -0
  22. package/lib/cjs/core/parser/gradient.cjs +132 -0
  23. package/lib/cjs/core/parser/gradient.cjs.map +1 -0
  24. package/lib/cjs/core/parser/gradient.d.ts +59 -0
  25. package/lib/cjs/core/parser/haptics.cjs +73 -0
  26. package/lib/cjs/core/parser/haptics.cjs.map +1 -0
  27. package/lib/cjs/core/parser/haptics.d.ts +47 -0
  28. package/lib/cjs/core/parser/index.d.ts +8 -0
  29. package/lib/cjs/core/parser/keyframes.cjs +95 -0
  30. package/lib/cjs/core/parser/keyframes.cjs.map +1 -0
  31. package/lib/cjs/core/parser/keyframes.d.ts +26 -0
  32. package/lib/cjs/core/parser/layout-dispatcher.cjs +100 -0
  33. package/lib/cjs/core/parser/layout-dispatcher.cjs.map +1 -0
  34. package/lib/cjs/core/parser/layout-dispatcher.d.ts +14 -0
  35. package/lib/cjs/core/parser/length.cjs +96 -0
  36. package/lib/cjs/core/parser/length.cjs.map +1 -0
  37. package/lib/cjs/core/parser/length.d.ts +48 -0
  38. package/lib/cjs/core/parser/motion-dispatcher.cjs +77 -0
  39. package/lib/cjs/core/parser/motion-dispatcher.cjs.map +1 -0
  40. package/lib/cjs/core/parser/motion-dispatcher.d.ts +11 -0
  41. package/lib/cjs/core/parser/property.cjs +22 -0
  42. package/lib/cjs/core/parser/property.cjs.map +1 -0
  43. package/lib/cjs/core/parser/property.d.ts +8 -0
  44. package/lib/cjs/core/parser/safe-area.cjs +404 -0
  45. package/lib/cjs/core/parser/safe-area.cjs.map +1 -0
  46. package/lib/cjs/core/parser/safe-area.d.ts +39 -0
  47. package/lib/cjs/core/parser/selector.cjs +22 -0
  48. package/lib/cjs/core/parser/selector.cjs.map +1 -0
  49. package/lib/cjs/core/parser/selector.d.ts +11 -0
  50. package/lib/cjs/core/parser/shorthand.cjs +156 -0
  51. package/lib/cjs/core/parser/shorthand.cjs.map +1 -0
  52. package/lib/cjs/core/parser/shorthand.d.ts +61 -0
  53. package/lib/cjs/core/parser/text-truncate.cjs +78 -0
  54. package/lib/cjs/core/parser/text-truncate.cjs.map +1 -0
  55. package/lib/cjs/core/parser/text-truncate.d.ts +44 -0
  56. package/lib/cjs/core/parser/theme-vars.cjs +414 -0
  57. package/lib/cjs/core/parser/theme-vars.cjs.map +1 -0
  58. package/lib/cjs/core/parser/theme-vars.d.ts +61 -0
  59. package/lib/cjs/core/parser/tokens.cjs +304 -0
  60. package/lib/cjs/core/parser/tokens.cjs.map +1 -0
  61. package/lib/cjs/core/parser/tokens.d.ts +45 -0
  62. package/lib/cjs/core/parser/transform.cjs +198 -0
  63. package/lib/cjs/core/parser/transform.cjs.map +1 -0
  64. package/lib/cjs/core/parser/transform.d.ts +36 -0
  65. package/lib/cjs/core/parser/tw-parser.cjs +1567 -0
  66. package/lib/cjs/core/parser/tw-parser.cjs.map +1 -0
  67. package/lib/cjs/core/parser/tw-parser.d.ts +194 -0
  68. package/lib/cjs/core/parser/types.d.ts +37 -0
  69. package/lib/cjs/core/parser/typography-dispatcher.cjs +93 -0
  70. package/lib/cjs/core/parser/typography-dispatcher.cjs.map +1 -0
  71. package/lib/cjs/core/parser/typography-dispatcher.d.ts +11 -0
  72. package/lib/cjs/core/parser/typography.cjs +97 -0
  73. package/lib/cjs/core/parser/typography.cjs.map +1 -0
  74. package/lib/cjs/core/parser/typography.d.ts +43 -0
  75. package/lib/cjs/core/style-builder/build-style.cjs +397 -0
  76. package/lib/cjs/core/style-builder/build-style.cjs.map +1 -0
  77. package/lib/cjs/core/style-builder/build-style.d.ts +54 -0
  78. package/lib/cjs/core/style-builder/index.d.ts +3 -0
  79. package/lib/cjs/core/style-builder/union-builder.cjs +326 -0
  80. package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -0
  81. package/lib/cjs/core/style-builder/union-builder.d.ts +128 -0
  82. package/lib/cjs/core/types.d.ts +14 -0
  83. package/lib/cjs/metro/dts.cjs +127 -0
  84. package/lib/cjs/metro/dts.cjs.map +1 -0
  85. package/lib/cjs/metro/dts.d.ts +16 -0
  86. package/lib/cjs/metro/index.cjs +19 -0
  87. package/lib/cjs/metro/index.cjs.map +1 -0
  88. package/lib/cjs/metro/index.d.ts +9 -0
  89. package/lib/cjs/metro/resolver.cjs +47 -0
  90. package/lib/cjs/metro/resolver.cjs.map +1 -0
  91. package/lib/cjs/metro/resolver.d.ts +22 -0
  92. package/lib/cjs/metro/state.cjs +251 -0
  93. package/lib/cjs/metro/state.cjs.map +1 -0
  94. package/lib/cjs/metro/state.d.ts +72 -0
  95. package/lib/cjs/metro/transform-ast.cjs +1255 -0
  96. package/lib/cjs/metro/transform-ast.cjs.map +1 -0
  97. package/lib/cjs/metro/transform-ast.d.ts +73 -0
  98. package/lib/cjs/metro/transformer.cjs +345 -0
  99. package/lib/cjs/metro/transformer.cjs.map +1 -0
  100. package/lib/cjs/metro/transformer.d.ts +47 -0
  101. package/lib/cjs/metro/warn-unknown-classes.cjs +86 -0
  102. package/lib/cjs/metro/warn-unknown-classes.cjs.map +1 -0
  103. package/lib/cjs/metro/warn-unknown-classes.d.ts +21 -0
  104. package/lib/cjs/metro/with-config.cjs +196 -0
  105. package/lib/cjs/metro/with-config.cjs.map +1 -0
  106. package/lib/cjs/metro/with-config.d.ts +57 -0
  107. package/lib/cjs/runtime/chain-handlers.cjs +37 -0
  108. package/lib/cjs/runtime/chain-handlers.cjs.map +1 -0
  109. package/lib/cjs/runtime/chain-handlers.d.ts +33 -0
  110. package/lib/cjs/runtime/components/rnwind-provider.cjs +98 -0
  111. package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -0
  112. package/lib/cjs/runtime/components/rnwind-provider.d.ts +84 -0
  113. package/lib/cjs/runtime/gradient-types.d.ts +58 -0
  114. package/lib/cjs/runtime/haptics.cjs +113 -0
  115. package/lib/cjs/runtime/haptics.cjs.map +1 -0
  116. package/lib/cjs/runtime/haptics.d.ts +48 -0
  117. package/lib/cjs/runtime/hooks/use-css.cjs +21 -0
  118. package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -0
  119. package/lib/cjs/runtime/hooks/use-css.d.ts +11 -0
  120. package/lib/cjs/runtime/hooks/use-interact.cjs +46 -0
  121. package/lib/cjs/runtime/hooks/use-interact.cjs.map +1 -0
  122. package/lib/cjs/runtime/hooks/use-interact.d.ts +42 -0
  123. package/lib/cjs/runtime/hooks/use-scheme.cjs +68 -0
  124. package/lib/cjs/runtime/hooks/use-scheme.cjs.map +1 -0
  125. package/lib/cjs/runtime/hooks/use-scheme.d.ts +34 -0
  126. package/lib/cjs/runtime/index.cjs +45 -0
  127. package/lib/cjs/runtime/index.cjs.map +1 -0
  128. package/lib/cjs/runtime/index.d.ts +27 -0
  129. package/lib/cjs/runtime/interactive-box.cjs +35 -0
  130. package/lib/cjs/runtime/interactive-box.cjs.map +1 -0
  131. package/lib/cjs/runtime/interactive-box.d.ts +40 -0
  132. package/lib/cjs/runtime/lookup-css.cjs +542 -0
  133. package/lib/cjs/runtime/lookup-css.cjs.map +1 -0
  134. package/lib/cjs/runtime/lookup-css.d.ts +164 -0
  135. package/lib/cjs/runtime/types.d.ts +29 -0
  136. package/lib/cjs/testing/index.cjs +367 -0
  137. package/lib/cjs/testing/index.cjs.map +1 -0
  138. package/lib/cjs/testing/index.d.ts +145 -0
  139. package/lib/esm/core/parser/animation.d.ts +126 -0
  140. package/lib/esm/core/parser/animation.mjs +408 -0
  141. package/lib/esm/core/parser/animation.mjs.map +1 -0
  142. package/lib/esm/core/parser/border-dispatcher.d.ts +15 -0
  143. package/lib/esm/core/parser/border-dispatcher.mjs +178 -0
  144. package/lib/esm/core/parser/border-dispatcher.mjs.map +1 -0
  145. package/lib/esm/core/parser/case-convert.d.ts +6 -0
  146. package/lib/esm/core/parser/case-convert.mjs +13 -0
  147. package/lib/esm/core/parser/case-convert.mjs.map +1 -0
  148. package/lib/esm/core/parser/color-properties-dispatcher.d.ts +19 -0
  149. package/lib/esm/core/parser/color-properties-dispatcher.mjs +82 -0
  150. package/lib/esm/core/parser/color-properties-dispatcher.mjs.map +1 -0
  151. package/lib/esm/core/parser/color.d.ts +12 -0
  152. package/lib/esm/core/parser/color.mjs +162 -0
  153. package/lib/esm/core/parser/color.mjs.map +1 -0
  154. package/lib/esm/core/parser/constants.d.ts +8 -0
  155. package/lib/esm/core/parser/constants.mjs +13 -0
  156. package/lib/esm/core/parser/constants.mjs.map +1 -0
  157. package/lib/esm/core/parser/declaration.d.ts +15 -0
  158. package/lib/esm/core/parser/declaration.mjs +345 -0
  159. package/lib/esm/core/parser/declaration.mjs.map +1 -0
  160. package/lib/esm/core/parser/gradient.d.ts +59 -0
  161. package/lib/esm/core/parser/gradient.mjs +130 -0
  162. package/lib/esm/core/parser/gradient.mjs.map +1 -0
  163. package/lib/esm/core/parser/haptics.d.ts +47 -0
  164. package/lib/esm/core/parser/haptics.mjs +71 -0
  165. package/lib/esm/core/parser/haptics.mjs.map +1 -0
  166. package/lib/esm/core/parser/index.d.ts +8 -0
  167. package/lib/esm/core/parser/keyframes.d.ts +26 -0
  168. package/lib/esm/core/parser/keyframes.mjs +91 -0
  169. package/lib/esm/core/parser/keyframes.mjs.map +1 -0
  170. package/lib/esm/core/parser/layout-dispatcher.d.ts +14 -0
  171. package/lib/esm/core/parser/layout-dispatcher.mjs +98 -0
  172. package/lib/esm/core/parser/layout-dispatcher.mjs.map +1 -0
  173. package/lib/esm/core/parser/length.d.ts +48 -0
  174. package/lib/esm/core/parser/length.mjs +90 -0
  175. package/lib/esm/core/parser/length.mjs.map +1 -0
  176. package/lib/esm/core/parser/motion-dispatcher.d.ts +11 -0
  177. package/lib/esm/core/parser/motion-dispatcher.mjs +75 -0
  178. package/lib/esm/core/parser/motion-dispatcher.mjs.map +1 -0
  179. package/lib/esm/core/parser/property.d.ts +8 -0
  180. package/lib/esm/core/parser/property.mjs +20 -0
  181. package/lib/esm/core/parser/property.mjs.map +1 -0
  182. package/lib/esm/core/parser/safe-area.d.ts +39 -0
  183. package/lib/esm/core/parser/safe-area.mjs +402 -0
  184. package/lib/esm/core/parser/safe-area.mjs.map +1 -0
  185. package/lib/esm/core/parser/selector.d.ts +11 -0
  186. package/lib/esm/core/parser/selector.mjs +20 -0
  187. package/lib/esm/core/parser/selector.mjs.map +1 -0
  188. package/lib/esm/core/parser/shorthand.d.ts +61 -0
  189. package/lib/esm/core/parser/shorthand.mjs +148 -0
  190. package/lib/esm/core/parser/shorthand.mjs.map +1 -0
  191. package/lib/esm/core/parser/text-truncate.d.ts +44 -0
  192. package/lib/esm/core/parser/text-truncate.mjs +75 -0
  193. package/lib/esm/core/parser/text-truncate.mjs.map +1 -0
  194. package/lib/esm/core/parser/theme-vars.d.ts +61 -0
  195. package/lib/esm/core/parser/theme-vars.mjs +409 -0
  196. package/lib/esm/core/parser/theme-vars.mjs.map +1 -0
  197. package/lib/esm/core/parser/tokens.d.ts +45 -0
  198. package/lib/esm/core/parser/tokens.mjs +298 -0
  199. package/lib/esm/core/parser/tokens.mjs.map +1 -0
  200. package/lib/esm/core/parser/transform.d.ts +36 -0
  201. package/lib/esm/core/parser/transform.mjs +193 -0
  202. package/lib/esm/core/parser/transform.mjs.map +1 -0
  203. package/lib/esm/core/parser/tw-parser.d.ts +194 -0
  204. package/lib/esm/core/parser/tw-parser.mjs +1565 -0
  205. package/lib/esm/core/parser/tw-parser.mjs.map +1 -0
  206. package/lib/esm/core/parser/types.d.ts +37 -0
  207. package/lib/esm/core/parser/typography-dispatcher.d.ts +11 -0
  208. package/lib/esm/core/parser/typography-dispatcher.mjs +91 -0
  209. package/lib/esm/core/parser/typography-dispatcher.mjs.map +1 -0
  210. package/lib/esm/core/parser/typography.d.ts +43 -0
  211. package/lib/esm/core/parser/typography.mjs +91 -0
  212. package/lib/esm/core/parser/typography.mjs.map +1 -0
  213. package/lib/esm/core/style-builder/build-style.d.ts +54 -0
  214. package/lib/esm/core/style-builder/build-style.mjs +395 -0
  215. package/lib/esm/core/style-builder/build-style.mjs.map +1 -0
  216. package/lib/esm/core/style-builder/index.d.ts +3 -0
  217. package/lib/esm/core/style-builder/union-builder.d.ts +128 -0
  218. package/lib/esm/core/style-builder/union-builder.mjs +324 -0
  219. package/lib/esm/core/style-builder/union-builder.mjs.map +1 -0
  220. package/lib/esm/core/types.d.ts +14 -0
  221. package/lib/esm/metro/dts.d.ts +16 -0
  222. package/lib/esm/metro/dts.mjs +125 -0
  223. package/lib/esm/metro/dts.mjs.map +1 -0
  224. package/lib/esm/metro/index.d.ts +9 -0
  225. package/lib/esm/metro/index.mjs +6 -0
  226. package/lib/esm/metro/index.mjs.map +1 -0
  227. package/lib/esm/metro/resolver.d.ts +22 -0
  228. package/lib/esm/metro/resolver.mjs +43 -0
  229. package/lib/esm/metro/resolver.mjs.map +1 -0
  230. package/lib/esm/metro/state.d.ts +72 -0
  231. package/lib/esm/metro/state.mjs +243 -0
  232. package/lib/esm/metro/state.mjs.map +1 -0
  233. package/lib/esm/metro/transform-ast.d.ts +73 -0
  234. package/lib/esm/metro/transform-ast.mjs +1234 -0
  235. package/lib/esm/metro/transform-ast.mjs.map +1 -0
  236. package/lib/esm/metro/transformer.d.ts +47 -0
  237. package/lib/esm/metro/transformer.mjs +322 -0
  238. package/lib/esm/metro/transformer.mjs.map +1 -0
  239. package/lib/esm/metro/warn-unknown-classes.d.ts +21 -0
  240. package/lib/esm/metro/warn-unknown-classes.mjs +84 -0
  241. package/lib/esm/metro/warn-unknown-classes.mjs.map +1 -0
  242. package/lib/esm/metro/with-config.d.ts +57 -0
  243. package/lib/esm/metro/with-config.mjs +194 -0
  244. package/lib/esm/metro/with-config.mjs.map +1 -0
  245. package/lib/esm/runtime/chain-handlers.d.ts +33 -0
  246. package/lib/esm/runtime/chain-handlers.mjs +34 -0
  247. package/lib/esm/runtime/chain-handlers.mjs.map +1 -0
  248. package/lib/esm/runtime/components/rnwind-provider.d.ts +84 -0
  249. package/lib/esm/runtime/components/rnwind-provider.mjs +94 -0
  250. package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -0
  251. package/lib/esm/runtime/gradient-types.d.ts +58 -0
  252. package/lib/esm/runtime/haptics.d.ts +48 -0
  253. package/lib/esm/runtime/haptics.mjs +110 -0
  254. package/lib/esm/runtime/haptics.mjs.map +1 -0
  255. package/lib/esm/runtime/hooks/use-css.d.ts +11 -0
  256. package/lib/esm/runtime/hooks/use-css.mjs +19 -0
  257. package/lib/esm/runtime/hooks/use-css.mjs.map +1 -0
  258. package/lib/esm/runtime/hooks/use-interact.d.ts +42 -0
  259. package/lib/esm/runtime/hooks/use-interact.mjs +44 -0
  260. package/lib/esm/runtime/hooks/use-interact.mjs.map +1 -0
  261. package/lib/esm/runtime/hooks/use-scheme.d.ts +34 -0
  262. package/lib/esm/runtime/hooks/use-scheme.mjs +63 -0
  263. package/lib/esm/runtime/hooks/use-scheme.mjs.map +1 -0
  264. package/lib/esm/runtime/index.d.ts +27 -0
  265. package/lib/esm/runtime/index.mjs +18 -0
  266. package/lib/esm/runtime/index.mjs.map +1 -0
  267. package/lib/esm/runtime/interactive-box.d.ts +40 -0
  268. package/lib/esm/runtime/interactive-box.mjs +33 -0
  269. package/lib/esm/runtime/interactive-box.mjs.map +1 -0
  270. package/lib/esm/runtime/lookup-css.d.ts +164 -0
  271. package/lib/esm/runtime/lookup-css.mjs +531 -0
  272. package/lib/esm/runtime/lookup-css.mjs.map +1 -0
  273. package/lib/esm/runtime/types.d.ts +29 -0
  274. package/lib/esm/testing/index.d.ts +145 -0
  275. package/lib/esm/testing/index.mjs +344 -0
  276. package/lib/esm/testing/index.mjs.map +1 -0
  277. package/package.json +79 -13
  278. package/preset.css +1171 -0
  279. package/src/core/parser/animation.ts +404 -0
  280. package/src/core/parser/border-dispatcher.ts +176 -0
  281. package/src/core/parser/case-convert.ts +10 -0
  282. package/src/core/parser/color-properties-dispatcher.ts +78 -0
  283. package/src/core/parser/color.ts +157 -0
  284. package/src/core/parser/constants.ts +11 -0
  285. package/src/core/parser/declaration.ts +340 -0
  286. package/src/core/parser/gradient.ts +148 -0
  287. package/src/core/parser/haptics.ts +88 -0
  288. package/src/core/parser/index.ts +8 -0
  289. package/src/core/parser/keyframes.ts +84 -0
  290. package/src/core/parser/layout-dispatcher.ts +92 -0
  291. package/src/core/parser/length.ts +100 -0
  292. package/src/core/parser/motion-dispatcher.ts +89 -0
  293. package/src/core/parser/property.ts +15 -0
  294. package/src/core/parser/safe-area.ts +404 -0
  295. package/src/core/parser/selector.ts +17 -0
  296. package/src/core/parser/shorthand.ts +152 -0
  297. package/src/core/parser/text-truncate.ts +79 -0
  298. package/src/core/parser/theme-vars.ts +412 -0
  299. package/src/core/parser/tokens.ts +286 -0
  300. package/src/core/parser/transform.ts +195 -0
  301. package/src/core/parser/tw-parser.ts +1709 -0
  302. package/src/core/parser/types.ts +45 -0
  303. package/src/core/parser/typography-dispatcher.ts +83 -0
  304. package/src/core/parser/typography.ts +83 -0
  305. package/src/core/style-builder/build-style.ts +442 -0
  306. package/src/core/style-builder/index.ts +3 -0
  307. package/src/core/style-builder/union-builder.ts +328 -0
  308. package/src/core/types.ts +15 -0
  309. package/src/metro/dts.ts +128 -0
  310. package/src/metro/index.ts +9 -0
  311. package/src/metro/resolver.ts +42 -0
  312. package/src/metro/state.ts +257 -0
  313. package/src/metro/transform-ast.ts +1498 -0
  314. package/src/metro/transformer.ts +347 -0
  315. package/src/metro/warn-unknown-classes.ts +79 -0
  316. package/src/metro/with-config.ts +229 -0
  317. package/src/runtime/chain-handlers.ts +47 -0
  318. package/src/runtime/components/rnwind-provider.tsx +144 -0
  319. package/src/runtime/gradient-types.ts +60 -0
  320. package/src/runtime/haptics.ts +120 -0
  321. package/src/runtime/hooks/use-css.ts +16 -0
  322. package/src/runtime/hooks/use-interact.ts +65 -0
  323. package/src/runtime/hooks/use-scheme.ts +63 -0
  324. package/src/runtime/index.ts +54 -0
  325. package/src/runtime/interactive-box.tsx +57 -0
  326. package/src/runtime/lookup-css.ts +628 -0
  327. package/src/runtime/types.ts +32 -0
  328. package/src/testing/index.ts +507 -0
  329. package/src/types/tailwindcss-node.d.ts +33 -0
  330. package/src/index.ts +0 -1
@@ -0,0 +1,1498 @@
1
+ import * as t from '@babel/types'
2
+ import type { File } from '@babel/types'
3
+ import traverseImport, { type NodePath } from '@babel/traverse'
4
+ import { createHash } from 'node:crypto'
5
+ import type { GradientAtomInfo, GradientDirection, HapticRequest, HapticTrigger } from '../core/parser'
6
+ import { detectTextTruncate, mayContainTextTruncate } from '../core/parser/text-truncate'
7
+
8
+ const traverse = (traverseImport as unknown as { default?: typeof traverseImport }).default ?? traverseImport
9
+
10
+ /**
11
+ * Name of the internal rnwind context hook the transformer injects at
12
+ * each component top. Must start with `use` + uppercase letter so
13
+ * react-refresh's babel plugin folds it into each component's
14
+ * fast-refresh signature — otherwise stale signatures preserve fiber
15
+ * state across transformer changes and the rendered hook list shifts,
16
+ * which React surfaces as "change in the order of Hooks" runtime errors.
17
+ */
18
+ const USE_RNWIND_INTERNAL = 'useR_'
19
+ /** Internal alias of `useMountHaptic` — emit `_hm` instead of the public name to dodge shadowing. */
20
+ const USE_MOUNT_HAPTIC = '_hm'
21
+ /** Internal alias of `triggerHaptic` — `_ht` so a user-defined `triggerHaptic` won't shadow it. */
22
+ const TRIGGER_HAPTIC = '_ht'
23
+ /** Internal alias of `lookupCss` — `_l` so user can't accidentally shadow at the JSX call sites. */
24
+ const LOOKUP_CSS = '_l'
25
+ /** Name of the runtime atom-registration entry point imported by the union style file. */
26
+ export const REGISTER_ATOMS = 'registerAtoms'
27
+ /** Package specifier rnwind runtime primitives import from. */
28
+ const RUNTIME_MODULE = 'rnwind'
29
+ /** Local binding name for the rnwind context hook result inside components. */
30
+ const CONTEXT_BINDING = '_t'
31
+ /**
32
+ * Name of the per-instance wrapper component rnwind substitutes for any
33
+ * JSX site that uses active: / focus: variants. One `useInteract()` lives
34
+ * INSIDE each InteractiveBox so siblings never share state — a previous
35
+ * design that called `useInteract()` once per enclosing component caused
36
+ * "press one button, all buttons glow" because every site read the same
37
+ * `_i.state` reference.
38
+ */
39
+ const INTERACTIVE_BOX = '_ib'
40
+ /** Leading variant prefixes that trigger interact wiring. */
41
+ const INTERACTIVE_PREFIXES = ['active:', 'focus:']
42
+
43
+ /**
44
+ * Regex matching the atom-name shape of every `*-safe` utility the
45
+ * rnwind preset ships. Used by the transformer to decide whether a
46
+ * file needs the `const _i = useInsets()` binding injected. Matches
47
+ * three families:
48
+ * - `*-safe` exactly (e.g. `pt-safe`, `inset-safe`).
49
+ * - `*-safe-or-<N>` / `*-safe-offset-<N>` (e.g. `mt-safe-or-4`).
50
+ * - `*-screen-safe` (e.g. `h-screen-safe`, `min-h-screen-safe`).
51
+ *
52
+ * Dynamic className expressions always inject `_i` — we can't inspect
53
+ * the string at build time, and the runtime slow path reads `SAFE_ATOMS`
54
+ * to pick up any user-defined safe utility the preset doesn't cover.
55
+ */
56
+ const SAFE_ATOM_PATTERN = /(?:(?:^|-)safe(?:-or-|-offset-|$))|(?:-screen-safe$)/
57
+
58
+ /**
59
+ * RN host components that NEVER emit press / focus events — wrapping
60
+ * them in `<InteractiveBox>` buys nothing and costs `useInteract()`'s
61
+ * hooks per mount. When the JSX tag is in this set, we skip the
62
+ * wrapper even for dynamic `className={…}` expressions. On a list of
63
+ * 1000 `<View className={rowCls}>` rows this drops 6 × 1000 hook
64
+ * initialisations per mount.
65
+ *
66
+ * The set is intentionally conservative — anything not listed keeps
67
+ * the wrapper. Custom components, `Animated.View`, and
68
+ * `Pressable`/`TextInput` all still get interactive variants on
69
+ * dynamic className.
70
+ */
71
+ const NON_INTERACTIVE_HOST_TAGS = new Set([
72
+ 'View',
73
+ 'Text',
74
+ 'ScrollView',
75
+ 'SafeAreaView',
76
+ 'Image',
77
+ 'ImageBackground',
78
+ 'FlatList',
79
+ 'SectionList',
80
+ 'VirtualizedList',
81
+ 'KeyboardAvoidingView',
82
+ 'ActivityIndicator',
83
+ 'RefreshControl',
84
+ 'Fragment',
85
+ ])
86
+
87
+ /** Per-file state returned by the transformer, for the caller to integrate with the atom ledger. */
88
+ export interface TransformAstResult {
89
+ /** `true` when the AST was mutated (any `className=` rewrite landed). */
90
+ touched: boolean
91
+ /** Atom-name arrays the transformer hoisted — one entry per unique atom set. */
92
+ hoistedArrays: ReadonlyMap<string, readonly string[]>
93
+ /** Candidate literal texts collected from every rewritten `className`. */
94
+ literals: readonly string[]
95
+ }
96
+
97
+ /** Inputs to {@link transformAst}. */
98
+ export interface TransformAstOptions {
99
+ /**
100
+ * Module specifiers the transformer side-effect-imports at the top
101
+ * of each rewritten file. Today: the union `style.js` and
102
+ * `keyframes.js` (always two entries — see `STYLE_SPECIFIERS` in
103
+ * resolver.ts). Empty when the file has no atoms to register.
104
+ */
105
+ styleSpecifiers: readonly string[]
106
+ /**
107
+ * Parser-surfaced gradient metadata per atom. The transformer reads
108
+ * this map when rewriting literal `className="..."` sites so it can
109
+ * strip gradient atoms out of the atom array fed to `lookupCss` and
110
+ * emit `colors={...} start={...} end={...}` JSX attributes consumed
111
+ * by `<LinearGradient>` (or any component with the expo prop shape).
112
+ */
113
+ gradientAtoms?: ReadonlyMap<string, GradientAtomInfo>
114
+ /**
115
+ * Parser-surfaced haptic metadata per atom. Keys are the full class
116
+ * name (including any variant prefix — `haptic-light`,
117
+ * `active:haptic-medium`). Values are the structured
118
+ * {@link HapticRequest}. The transformer strips matched atoms from
119
+ * the className, aggregates mount requests per enclosing component,
120
+ * and wires press-in chains directly on the element.
121
+ */
122
+ hapticAtoms?: ReadonlyMap<string, HapticRequest>
123
+ /**
124
+ * Extra prop-name prefixes that turn `<prefix>ClassName="…"` into
125
+ * `<prefix>Style={lookupCss(…)}` with the same plumbing as the plain
126
+ * `className` path. The built-in `'contentContainer'` prefix is always
127
+ * enabled (covers ScrollView / FlatList / SectionList) — entries here
128
+ * are additive, not a replacement. A user-supplied `['myFunny']` yields
129
+ * the effective set `['contentContainer', 'myFunny']`.
130
+ *
131
+ * Prefixed rewrites never go through `<InteractiveBox>`: the targeted
132
+ * sub-surfaces (scroll content containers, column wrappers, etc.)
133
+ * can't fire press / focus events, so we always emit the inline
134
+ * `lookupCss(…)` call regardless of whether the expression is static
135
+ * or dynamic.
136
+ */
137
+ classNamePrefixes?: readonly string[]
138
+ }
139
+
140
+ /**
141
+ * Built-in prefix that's always active — covers the React Native
142
+ * ecosystem's `contentContainerStyle` pattern on ScrollView / FlatList /
143
+ * SectionList. Users who pass `classNamePrefixes` get their list merged
144
+ * on top, never replacing this.
145
+ */
146
+ const DEFAULT_CLASSNAME_PREFIXES: readonly string[] = ['contentContainer']
147
+
148
+ /**
149
+ * Mutate an already-parsed Babel AST in place:
150
+ * - Rewrite every JSX `className="…"` / `className={expr}` attribute to
151
+ * `style={lookupCss(<ref|expr>, _s, <existingStyle>)}`. Static string
152
+ * literals get a module-scope `const _c_<hash> = Object.freeze(['a',
153
+ * 'b'])` hoist so React sees an identity-stable array across renders.
154
+ * - Preserve any adjacent `style={…}` prop — it becomes the third
155
+ * argument so user inline styles keep working (and trump atoms).
156
+ * - Inject `const _s = useScheme()` at the top of the enclosing
157
+ * function component (idempotent — one injection per component).
158
+ * - Prepend `import { lookupCss, useScheme } from 'rnwind'`.
159
+ * - Prepend a side-effect `import 'rnwind/__generated/style'` so the
160
+ * union registry is loaded before any hoist runs.
161
+ * @param ast Babel File AST (usually handed to us by Metro).
162
+ * @param options Extra inputs — side-effect import specifiers + parser metadata.
163
+ * @returns Transform outcome flags + the hoist table.
164
+ */
165
+ export function transformAst(ast: File, options: TransformAstOptions): TransformAstResult {
166
+ const hoister = createHoister()
167
+ const gradientHoister = createGradientHoister()
168
+ const literals: string[] = []
169
+ const prefixSet = buildPrefixSet(options.classNamePrefixes)
170
+ const hapticHoister = createHapticHoister()
171
+ const rewriteCtx: RewriteContext = {
172
+ needsInsets: false,
173
+ gradientAtoms: options.gradientAtoms ?? EMPTY_GRADIENT_ATOMS,
174
+ gradientHoister,
175
+ hapticAtoms: options.hapticAtoms ?? EMPTY_HAPTIC_ATOMS,
176
+ hapticHoister,
177
+ mountByComponent: new Map(),
178
+ needsHapticsHook: false,
179
+ }
180
+ let touched = false
181
+ let usedLookupCss = false
182
+ let usedInteractiveBox = false
183
+
184
+ traverse(ast, {
185
+ JSXAttribute(attributePath: NodePath<t.JSXAttribute>) {
186
+ const { node } = attributePath
187
+ if (!t.isJSXIdentifier(node.name)) return
188
+ const target = classifyAttributeName(node.name.name, prefixSet)
189
+ if (!target) return
190
+ const rewritten = rewriteClassNameAttribute(attributePath, hoister, literals, rewriteCtx, target)
191
+ if (!rewritten) return
192
+ touched = true
193
+ if (rewritten.injectedInteract) usedInteractiveBox = true
194
+ else usedLookupCss = true
195
+ },
196
+ })
197
+
198
+ if (!touched && options.styleSpecifiers.length === 0) return { touched: false, hoistedArrays: hoister.entries, literals }
199
+
200
+ // Inject `useMountHaptic(<hoisted>)` per component that had bare
201
+ // haptic atoms. Done post-traversal so we know every aggregated
202
+ // request up front and can hoist one frozen array per component.
203
+ injectMountHapticCalls(rewriteCtx)
204
+ const usedMountHaptic = rewriteCtx.mountByComponent.size > 0
205
+ prependRuntimeImports(
206
+ ast,
207
+ {
208
+ usedLookupCss,
209
+ usedInteractiveBox,
210
+ usedMountHaptic,
211
+ usedTriggerHaptic: rewriteCtx.needsHapticsHook,
212
+ touched,
213
+ },
214
+ options.styleSpecifiers,
215
+ )
216
+ if (hoister.entries.size > 0) injectHoistedConsts(ast, hoister.entries)
217
+ if (gradientHoister.entries.size > 0) injectGradientConsts(ast, gradientHoister.entries)
218
+ if (hapticHoister.entries.size > 0) injectHapticConsts(ast, hapticHoister.entries)
219
+ return { touched, hoistedArrays: hoister.entries, literals }
220
+ }
221
+
222
+ /** Default empty gradient-atoms map used when callers don't supply one. */
223
+ const EMPTY_GRADIENT_ATOMS: ReadonlyMap<string, GradientAtomInfo> = new Map()
224
+ /** Default empty haptic-atoms map used when callers don't supply one. */
225
+ const EMPTY_HAPTIC_ATOMS: ReadonlyMap<string, HapticRequest> = new Map()
226
+
227
+ /**
228
+ * Target of one rewrite — which JSX prop we replace and what name the
229
+ * replacement carries. `kind: 'className'` is the classic `className →
230
+ * style` path; `kind: 'prefix'` is `<prefix>ClassName → <prefix>Style`
231
+ * and skips the InteractiveBox wrapper (prefixed sub-surfaces can't
232
+ * fire press / focus events).
233
+ */
234
+ type RewriteTarget =
235
+ | { readonly kind: 'className'; readonly styleProp: 'style' }
236
+ | { readonly kind: 'prefix'; readonly styleProp: string }
237
+
238
+ /**
239
+ * Merge the built-in default prefix with the caller-supplied list. The
240
+ * default (`contentContainer`) is always present; user entries are
241
+ * additive. Returned as a Set so the hot-path visitor classifies one
242
+ * attribute in O(1).
243
+ * @param userPrefixes Extra prefixes the caller wants active.
244
+ * @returns Sorted effective prefix set.
245
+ */
246
+ function buildPrefixSet(userPrefixes: readonly string[] | undefined): ReadonlySet<string> {
247
+ const out = new Set<string>(DEFAULT_CLASSNAME_PREFIXES)
248
+ if (userPrefixes) for (const prefix of userPrefixes) out.add(prefix)
249
+ return out
250
+ }
251
+
252
+ /**
253
+ * Decide whether a JSX attribute name is one the transformer should
254
+ * rewrite, and derive the replacement prop name when it is.
255
+ *
256
+ * `className` is the classic path. `<prefix>ClassName` where `prefix`
257
+ * is in the active set becomes `<prefix>Style`. Everything else returns
258
+ * `null` and the visitor moves on.
259
+ * @param name JSXAttribute's identifier text.
260
+ * @param prefixes Effective prefix set for this transform.
261
+ * @returns Rewrite target record, or `null` when the attribute is not ours.
262
+ */
263
+ function classifyAttributeName(name: string, prefixes: ReadonlySet<string>): RewriteTarget | null {
264
+ if (name === 'className') return { kind: 'className', styleProp: 'style' }
265
+ if (!name.endsWith('ClassName')) return null
266
+ const prefix = name.slice(0, -'ClassName'.length)
267
+ if (prefix.length === 0 || !prefixes.has(prefix)) return null
268
+ return { kind: 'prefix', styleProp: `${prefix}Style` }
269
+ }
270
+
271
+ /**
272
+ * Rewrite-wide state threaded through every JSXAttribute visit. Right
273
+ * now it's just the insets-injection flag — flipped to `true` when any
274
+ * atom looks like a safe-area utility so the import writer knows to
275
+ * pull in `useInsets` alongside `useScheme`.
276
+ */
277
+ interface RewriteContext {
278
+ /** Flipped on the first safe-area atom seen in the file. */
279
+ needsInsets: boolean
280
+ /** Parser-surfaced gradient metadata, keyed by atom name. */
281
+ gradientAtoms: ReadonlyMap<string, GradientAtomInfo>
282
+ /** Hoister for gradient spec consts (colours / start / end). */
283
+ gradientHoister: GradientHoister
284
+ /** Parser-surfaced haptic metadata, keyed by full class name. */
285
+ hapticAtoms: ReadonlyMap<string, HapticRequest>
286
+ /** Hoister for HapticRequest objects + mount-request arrays. */
287
+ hapticHoister: HapticHoister
288
+ /**
289
+ * Aggregates mount-haptic requests per enclosing component body.
290
+ * Populated as JSX is visited; consumed after traversal to inject
291
+ * one `useMountHaptic(...)` call per component.
292
+ */
293
+ mountByComponent: Map<t.BlockStatement, HapticRequest[]>
294
+ /** Flipped on the first event (press / focus / hover) haptic that needs `_h`. */
295
+ needsHapticsHook: boolean
296
+ }
297
+
298
+ /** Per-attribute outcome from {@link rewriteClassNameAttribute}. */
299
+ interface RewriteOutcome {
300
+ /** `true` when the element was wired for active/focus interact state. */
301
+ injectedInteract: boolean
302
+ }
303
+
304
+ /**
305
+ * Rewrite one `className` JSXAttribute node.
306
+ *
307
+ * Two paths:
308
+ * - **Non-interactive** (literal with no `active:` / `focus:` tokens):
309
+ * emit `style={lookupCss(<ref|expr>, _s [, userStyle])}` inline on
310
+ * the existing tag. The JSX site keeps its original component.
311
+ * - **Interactive** (literal with an interactive token OR any dynamic
312
+ * expression): replace the JSXElement's tag with `<InteractiveBox>`,
313
+ * move the original tag into a `_rw.as` spec prop, and forward all
314
+ * other attributes untouched. Each InteractiveBox instance calls
315
+ * `useInteract()` internally so sibling elements don't share state.
316
+ *
317
+ * If the element has a sibling `style={…}` attribute it's removed and
318
+ * its expression threads through as the user-style merge source.
319
+ * @param attributePath The JSXAttribute path.
320
+ * @param hoister Per-file hoist table.
321
+ * @param literals Output array — each static literal gets pushed so the
322
+ * caller can feed them into the parser / atom ledger.
323
+ * @param rewriteCtx
324
+ * @param target
325
+ * @returns Outcome flags, or `null` when the attribute was unrewritable.
326
+ */
327
+ function rewriteClassNameAttribute(
328
+ attributePath: NodePath<t.JSXAttribute>,
329
+ hoister: Hoister,
330
+ literals: string[],
331
+ rewriteCtx: RewriteContext,
332
+ target: RewriteTarget,
333
+ ): RewriteOutcome | null {
334
+ const { node } = attributePath
335
+ const { value } = node
336
+ if (!value) return null
337
+ const buildResult = buildFirstArgument(value, hoister, literals, rewriteCtx)
338
+ if (!buildResult) return null
339
+ const { parent } = attributePath
340
+ if (!t.isJSXOpeningElement(parent)) return null
341
+ const userStyleExpr = extractAndDropSiblingStyle(parent, target.styleProp)
342
+ // Single context binding `_t = _r()` — carries scheme, fontScale,
343
+ // insets together so React tracks all three as render deps via one
344
+ // useContext read.
345
+ const ctxBinding = injectContextHook(attributePath)
346
+ applyDerivedJsxAttributes(attributePath, parent, buildResult, target, rewriteCtx)
347
+ // Prefixed rewrites (`<prefix>ClassName`) target a passive sub-surface
348
+ // that can't receive press / focus — skip the InteractiveBox wrapper
349
+ // even for dynamic expressions. Only the plain `className` path is
350
+ // eligible for InteractiveBox routing.
351
+ if (target.kind === 'className' && buildResult.mayBeInteractive && isTagInteractive(parent.name)) {
352
+ rewriteAsInteractiveBox(attributePath, parent, buildResult.expression, ctxBinding, userStyleExpr)
353
+ return { injectedInteract: true }
354
+ }
355
+ const args: t.Expression[] = [buildResult.expression, t.identifier(ctxBinding)]
356
+ // 3rd arg = userStyle (sibling style={…}). 4th arg = interactState
357
+ // (always undefined in the non-interactive branch).
358
+ if (userStyleExpr) args.push(userStyleExpr)
359
+ const call = t.callExpression(t.identifier(LOOKUP_CSS), args)
360
+ attributePath.replaceWith(t.jsxAttribute(t.jsxIdentifier(target.styleProp), t.jsxExpressionContainer(call)))
361
+ return { injectedInteract: false }
362
+ }
363
+
364
+ /**
365
+ * Apply every JSX attribute + side-effect derived from a parsed
366
+ * className literal: gradient props, truncate props, mount-haptic
367
+ * aggregation, and event-haptic handler chaining. Collected in one
368
+ * helper so {@link rewriteClassNameAttribute} stays under the
369
+ * complexity cap.
370
+ * @param attributePath Path of the className attribute being rewritten.
371
+ * @param parent Opening element to mutate.
372
+ * @param result Per-literal derived state.
373
+ * @param target Rewrite target (only `className`-kind gets derived attrs).
374
+ * @param rewriteCtx Rewrite-wide state.
375
+ */
376
+ function applyDerivedJsxAttributes(
377
+ attributePath: NodePath<t.JSXAttribute>,
378
+ parent: t.JSXOpeningElement,
379
+ result: FirstArgumentResult,
380
+ target: RewriteTarget,
381
+ rewriteCtx: RewriteContext,
382
+ ): void {
383
+ if (target.kind !== 'className') return
384
+ if (result.gradientAttrs) appendGradientAttributes(parent, result.gradientAttrs)
385
+ if (result.truncateAttrs) appendGradientAttributes(parent, result.truncateAttrs)
386
+ if (result.mountHaptics) recordMountHaptics(attributePath, result.mountHaptics, rewriteCtx)
387
+ if (result.eventHaptics) injectEventHapticHandlers(attributePath, parent, result.eventHaptics, rewriteCtx)
388
+ }
389
+
390
+ /**
391
+ * Splice gradient JSX attributes (`colors={…}` / `start={…}` /
392
+ * `end={…}`) into a JSXOpeningElement's attribute list, replacing
393
+ * any already-present attribute with the same name. Users who manually
394
+ * set `colors=` on the same element lose; rnwind's class-derived
395
+ * values win — matching how `className`-resolved styles override
396
+ * inline `style={…}`.
397
+ * @param opening JSXOpeningElement to mutate.
398
+ * @param gradientAttrs Freshly built JSX attributes.
399
+ * @param gradientAttributes
400
+ */
401
+ function appendGradientAttributes(opening: t.JSXOpeningElement, gradientAttributes: readonly t.JSXAttribute[]): void {
402
+ const names = new Set<string>()
403
+ for (const attribute of gradientAttributes) if (t.isJSXIdentifier(attribute.name)) names.add(attribute.name.name)
404
+ opening.attributes = opening.attributes.filter((attribute) => {
405
+ if (!t.isJSXAttribute(attribute)) return true
406
+ if (!t.isJSXIdentifier(attribute.name)) return true
407
+ return !names.has(attribute.name.name)
408
+ })
409
+ opening.attributes.push(...gradientAttributes)
410
+ }
411
+
412
+ /**
413
+ * Whether a JSX tag can fire press / focus events. Pure host-tag check
414
+ * against {@link NON_INTERACTIVE_HOST_TAGS}: anything in the set is
415
+ * definitely non-interactive; anything else (custom component,
416
+ * `Animated.View`, etc.) is treated as potentially interactive so the
417
+ * InteractiveBox wrapper is still applied.
418
+ * @param name JSXOpeningElement name node.
419
+ * @returns `true` when the tag might emit press / focus events.
420
+ */
421
+ function isTagInteractive(name: t.JSXOpeningElement['name']): boolean {
422
+ if (t.isJSXIdentifier(name)) return !NON_INTERACTIVE_HOST_TAGS.has(name.name)
423
+ // Member expressions (`Animated.View`, `Foo.Bar`): conservatively
424
+ // treat as interactive since the outer object's semantics are opaque.
425
+ return true
426
+ }
427
+
428
+ /**
429
+ * Replace the JSXElement's tag with `<InteractiveBox>`, packing the
430
+ * original tag, the className ref / expression, the scheme binding, and
431
+ * any user style into a single `_rw` spec prop. All other attributes
432
+ * forward through unchanged.
433
+ *
434
+ * The replacement keeps the element's children — only the opening /
435
+ * closing tag name changes, plus the className attribute is replaced by
436
+ * `_rw` (and a preceding `style` attribute was already spliced out).
437
+ * @param attributePath JSXAttribute path for the className being rewritten.
438
+ * @param opening JSXOpeningElement the attribute lives on.
439
+ * @param classNameExpr The first-arg expression (hoisted ref or dynamic).
440
+ * @param schemeBinding Name of the `_s = useScheme()` binding.
441
+ * @param ctxBinding
442
+ * @param userStyleExpr Optional user style spliced from a sibling `style={…}`.
443
+ * @param insetsBinding `_i = useInsets()` binding name when the rewrite needs insets, else null.
444
+ * @param fontScaleBinding `_fs = useFontScale()` binding name — always present since every rewrite injects it.
445
+ */
446
+ function rewriteAsInteractiveBox(
447
+ attributePath: NodePath<t.JSXAttribute>,
448
+ opening: t.JSXOpeningElement,
449
+ classNameExpr: t.Expression,
450
+ ctxBinding: string,
451
+ userStyleExpr: t.Expression | null,
452
+ ): void {
453
+ const originalTagExpr = jsxNameToExpression(opening.name)
454
+ const rwProperties: t.ObjectProperty[] = [
455
+ t.objectProperty(t.identifier('as'), originalTagExpr),
456
+ t.objectProperty(t.identifier('cn'), classNameExpr),
457
+ t.objectProperty(t.identifier('t'), t.identifier(ctxBinding)),
458
+ ]
459
+ if (userStyleExpr) rwProperties.push(t.objectProperty(t.identifier('us'), userStyleExpr))
460
+ const rwAttribute = t.jsxAttribute(t.jsxIdentifier('_rw'), t.jsxExpressionContainer(t.objectExpression(rwProperties)))
461
+ // Swap the className attribute out for `_rw`, keeping it at the
462
+ // attribute's original position so any surrounding spread attrs stay
463
+ // honouring the user's intended order.
464
+ attributePath.replaceWith(rwAttribute)
465
+ opening.name = t.jsxIdentifier(INTERACTIVE_BOX)
466
+ const jsxElement = findParentJsxElement(attributePath)
467
+ if (jsxElement?.closingElement) jsxElement.closingElement.name = t.jsxIdentifier(INTERACTIVE_BOX)
468
+ }
469
+
470
+ /**
471
+ * Walk from a JSXAttribute path up to its JSXElement ancestor.
472
+ * @param attributePath JSXAttribute path.
473
+ * @returns The enclosing JSXElement, or `null` when the shape is unexpected.
474
+ */
475
+ function findParentJsxElement(attributePath: NodePath<t.JSXAttribute>): t.JSXElement | null {
476
+ const openingPath = attributePath.parentPath
477
+ if (!openingPath) return null
478
+ const elementPath = openingPath.parentPath
479
+ if (!elementPath) return null
480
+ const { node } = elementPath
481
+ return t.isJSXElement(node) ? node : null
482
+ }
483
+
484
+ /**
485
+ * Convert a JSX opening-element name (identifier or member expression)
486
+ * into a regular JS expression we can splice into the `_rw.as` object
487
+ * property. `<Animated.View>` → `Animated.View`, `<Pressable>` →
488
+ * `Pressable`.
489
+ * @param name JSXOpeningElement name node.
490
+ * @returns Equivalent identifier / member-expression node.
491
+ */
492
+ function jsxNameToExpression(name: t.JSXOpeningElement['name']): t.Expression {
493
+ if (t.isJSXIdentifier(name)) return t.identifier(name.name)
494
+ if (t.isJSXMemberExpression(name)) {
495
+ return t.memberExpression(jsxNameToExpression(name.object), t.identifier(name.property.name))
496
+ }
497
+ throw new Error(
498
+ `rnwind: unsupported JSX tag shape "${(name as { type?: string }).type ?? 'unknown'}" for interactive className`,
499
+ )
500
+ }
501
+
502
+ /** Result from {@link buildFirstArgument} — the lookupCss first arg + flags. */
503
+ interface FirstArgumentResult {
504
+ /** Expression to splice in as `lookupCss`'s first argument. */
505
+ expression: t.Expression
506
+ /**
507
+ * Whether this className might engage active/focus variants at runtime.
508
+ * `true` for every dynamic (non-literal) expression — we can't know
509
+ * the eventual string. For literals, `true` only if any token carries
510
+ * a recognised interactive prefix.
511
+ */
512
+ mayBeInteractive: boolean
513
+ /**
514
+ * Whether this particular rewrite needs the safe-area insets
515
+ * argument passed to `lookupCss`. Set to `true` for dynamic
516
+ * expressions (can't inspect tokens at build time) and for literals
517
+ * that include any `*-safe` utility. When `false` the rewrite emits
518
+ * the compact 2-arg call so the runtime fast path stays engaged.
519
+ */
520
+ needsInsets: boolean
521
+ /**
522
+ * Extra JSX attributes the rewrite should inject alongside the
523
+ * `style={...}` prop. Non-null only when the literal carried gradient
524
+ * atoms: `colors={_g_x}`, `start={_gs_x}`, `end={_ge_x}`, optionally
525
+ * `locations={_gl_x}` — stable consts hoisted at module scope.
526
+ */
527
+ gradientAttrs?: readonly t.JSXAttribute[]
528
+ /**
529
+ * Extra JSX attributes derived from text-truncate atoms (`truncate`,
530
+ * `line-clamp-<N>`, `text-ellipsis`, `text-clip`). Emitted as inline
531
+ * literals — `numberOfLines={N}` and/or `ellipsizeMode="tail"` — so
532
+ * `<Text>` (and any `Text`-prop-shaped component) gets the right
533
+ * native truncation without the user hand-wiring props.
534
+ */
535
+ truncateAttrs?: readonly t.JSXAttribute[]
536
+ /**
537
+ * Mount-haptic requests collected from this literal (bare `haptic-*`
538
+ * atoms, no variant prefix). Aggregated per-component by the caller.
539
+ */
540
+ mountHaptics?: readonly HapticRequest[]
541
+ /**
542
+ * Event-haptic entries (`active:haptic-*` / `focus:haptic-*` /
543
+ * `hover:haptic-*`). Caller splices the chained event handlers onto
544
+ * the opening element.
545
+ */
546
+ eventHaptics?: readonly { readonly request: HapticRequest; readonly trigger: Exclude<HapticTrigger, 'mount'> }[]
547
+ }
548
+
549
+ /**
550
+ * Decide what the first arg of the rewritten `lookupCss(...)` call
551
+ * should be:
552
+ * - Static string literal (`"…"` or `{"…"}` or static template): tokenize,
553
+ * push literal text for the ledger, return a hoisted const reference.
554
+ * - Dynamic expression: forward the expression unchanged; runtime
555
+ * tokenizes the string result at render time.
556
+ * @param value Attribute's value node (StringLiteral or JSXExpressionContainer).
557
+ * @param hoister Hoist table.
558
+ * @param literals Output array for static literals.
559
+ * @param rewriteCtx
560
+ * @returns The first-arg expression + interact-eligibility flag, or `null`.
561
+ */
562
+ function buildFirstArgument(
563
+ value: t.JSXAttribute['value'],
564
+ hoister: Hoister,
565
+ literals: string[],
566
+ rewriteCtx: RewriteContext,
567
+ ): FirstArgumentResult | null {
568
+ if (t.isStringLiteral(value)) return literalResult(value.value, hoister, literals, rewriteCtx)
569
+ if (!t.isJSXExpressionContainer(value)) return null
570
+ const { expression } = value
571
+ if (t.isJSXEmptyExpression(expression)) return null
572
+ if (t.isStringLiteral(expression)) return literalResult(expression.value, hoister, literals, rewriteCtx)
573
+ if (t.isTemplateLiteral(expression) && expression.expressions.length === 0 && expression.quasis[0]) {
574
+ const text = expression.quasis[0].value.cooked ?? expression.quasis[0].value.raw
575
+ return literalResult(text, hoister, literals, rewriteCtx)
576
+ }
577
+ // Dynamic expression — can't inspect atoms at build time. Assume safe
578
+ // is possible and pull in `_i` so the runtime can resolve any
579
+ // `*-safe` class a consumer composes at runtime. The runtime fast
580
+ // path is still taken when the dynamic string resolves to a plain
581
+ // non-safe atom list (SAFE_ATOMS index check gates the slow path).
582
+ rewriteCtx.needsInsets = true
583
+ return { expression: expression as t.Expression, mayBeInteractive: true, needsInsets: true }
584
+ }
585
+
586
+ /**
587
+ * Package a literal classname into a hoisted atom-array ref and scan it
588
+ * for interactive prefixes (`active:`, `focus:`) + safe-area patterns.
589
+ * Pre-scanned literals without any interactive tokens skip the (small
590
+ * but measurable) cost of injecting a `useInteract()` hook; literals
591
+ * without any `*-safe` token skip the insets arg, keeping the runtime
592
+ * fast path engaged.
593
+ * @param text Raw classname string.
594
+ * @param hoister Hoist table.
595
+ * @param literals Output array for literal-text sink.
596
+ * @param rewriteCtx Rewrite-wide state; updated when any atom needs insets.
597
+ * @returns The expression + per-rewrite flags.
598
+ */
599
+ function literalResult(text: string, hoister: Hoister, literals: string[], rewriteCtx: RewriteContext): FirstArgumentResult {
600
+ literals.push(text)
601
+ const atoms = tokenize(text)
602
+ const { gradientAttrs, remaining: afterGradient } = extractGradientSpec(atoms, rewriteCtx)
603
+ const { truncateAttrs, remaining: afterTruncate } = extractTextTruncateSpec(afterGradient)
604
+ const { mountHaptics, eventHaptics, remaining } = extractHapticSpec(afterTruncate, rewriteCtx)
605
+ const mayBeInteractive = remaining.some((atom) => INTERACTIVE_PREFIXES.some((prefix) => atom.startsWith(prefix)))
606
+ const needsInsets = remaining.some((atom) => SAFE_ATOM_PATTERN.test(atom))
607
+ if (needsInsets) rewriteCtx.needsInsets = true
608
+ return {
609
+ expression: hoister.refFor(remaining),
610
+ mayBeInteractive,
611
+ needsInsets,
612
+ gradientAttrs: gradientAttrs.length > 0 ? gradientAttrs : undefined,
613
+ truncateAttrs: truncateAttrs.length > 0 ? truncateAttrs : undefined,
614
+ mountHaptics: mountHaptics.length > 0 ? mountHaptics : undefined,
615
+ eventHaptics: eventHaptics.length > 0 ? eventHaptics : undefined,
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Scan the atom list for gradient roles (direction + from/via/to
621
+ * colours), strip those atoms out, and produce the JSX attributes the
622
+ * rewrite will splice onto the opening element:
623
+ * colors={_g_<hash>} start={_gs_<hash>} end={_ge_<hash>}
624
+ * When the atom list doesn't contain a complete gradient (no direction
625
+ * OR no colour stops), the gradient atoms pass through untouched —
626
+ * they'd resolve to `{}` in the runtime anyway. This keeps the
627
+ * transform conservative.
628
+ * @param atoms Tokenised atom list from the literal.
629
+ * @param rewriteCtx Rewrite-wide state (for the hoister).
630
+ * @returns The gradient JSX attrs (possibly empty) and the non-gradient remainder.
631
+ */
632
+ function extractGradientSpec(
633
+ atoms: readonly string[],
634
+ rewriteCtx: RewriteContext,
635
+ ): { gradientAttrs: readonly t.JSXAttribute[]; remaining: readonly string[] } {
636
+ const {gradientAtoms} = rewriteCtx
637
+ if (gradientAtoms.size === 0) return { gradientAttrs: [], remaining: atoms }
638
+
639
+ let direction: GradientDirection | null = null
640
+ let from: string | null = null
641
+ let via: string | null = null
642
+ let to: string | null = null
643
+ const remaining: string[] = []
644
+ for (const atom of atoms) {
645
+ const info = gradientAtoms.get(atom)
646
+ if (!info) {
647
+ remaining.push(atom)
648
+ continue
649
+ }
650
+ switch (info.role) {
651
+ case 'direction': {
652
+ direction = info.dir
653
+ break;
654
+ }
655
+ case 'from': {
656
+ from = info.color
657
+ break;
658
+ }
659
+ case 'via': {
660
+ via = info.color
661
+ break;
662
+ }
663
+ case 'to': { {
664
+ to = info.color
665
+ // No default
666
+ }
667
+ break;
668
+ }
669
+ }
670
+ // Gradient atoms deliberately drop from `remaining` — they're
671
+ // consumed at build time and don't need a runtime style slot.
672
+ }
673
+
674
+ if (direction === null || direction === 'unknown' || (from === null && to === null)) {
675
+ // No recognisable gradient — put atoms back so they at least
676
+ // attempt to resolve through lookupCss.
677
+ return { gradientAttrs: [], remaining: atoms }
678
+ }
679
+ const colors = gradientColors(from, via, to)
680
+ const points = directionToPoints(direction)
681
+ const colorsRef = rewriteCtx.gradientHoister.refForColors(colors)
682
+ const startRef = rewriteCtx.gradientHoister.refForPoint(points.start, 'start')
683
+ const endRef = rewriteCtx.gradientHoister.refForPoint(points.end, 'end')
684
+ const attributes: t.JSXAttribute[] = [
685
+ t.jsxAttribute(t.jsxIdentifier('colors'), t.jsxExpressionContainer(colorsRef)),
686
+ t.jsxAttribute(t.jsxIdentifier('start'), t.jsxExpressionContainer(startRef)),
687
+ t.jsxAttribute(t.jsxIdentifier('end'), t.jsxExpressionContainer(endRef)),
688
+ ]
689
+ return { gradientAttrs: attributes, remaining }
690
+ }
691
+
692
+ /**
693
+ * Scan the atom list for text-truncate utilities (`truncate`,
694
+ * `line-clamp-<N>`, `line-clamp-none`, `text-ellipsis`, `text-clip`),
695
+ * strip them out, and produce the JSX attributes the rewrite will
696
+ * splice onto the opening element: `numberOfLines={N}` and/or
697
+ * `ellipsizeMode="tail"|"clip"`.
698
+ *
699
+ * Merge rule mirrors Tailwind's cascade — later atoms override earlier
700
+ * ones. `numberOfLines: 0` (the `line-clamp-none` reset) suppresses
701
+ * emission entirely; a standalone `text-ellipsis` / `text-clip` with no
702
+ * companion line count also emits nothing because `ellipsizeMode`
703
+ * alone has no effect on RN `<Text>`.
704
+ * @param atoms Tokenised atom list.
705
+ * @returns The truncate JSX attrs (possibly empty) and the non-truncate remainder.
706
+ */
707
+ function extractTextTruncateSpec(
708
+ atoms: readonly string[],
709
+ ): { truncateAttrs: readonly t.JSXAttribute[]; remaining: readonly string[] } {
710
+ if (!mayContainTextTruncate(atoms)) return { truncateAttrs: [], remaining: atoms }
711
+ let numberOfLines: number | undefined
712
+ let ellipsizeMode: 'tail' | 'clip' | undefined
713
+ const remaining: string[] = []
714
+ for (const atom of atoms) {
715
+ const info = detectTextTruncate(atom)
716
+ if (!info) {
717
+ remaining.push(atom)
718
+ continue
719
+ }
720
+ const { numberOfLines: infoLines, ellipsizeMode: infoMode } = info
721
+ if (infoLines !== undefined) numberOfLines = infoLines
722
+ if (infoMode !== undefined) ellipsizeMode = infoMode
723
+ }
724
+ const attributes = buildTruncateAttributes(numberOfLines, ellipsizeMode)
725
+ return { truncateAttrs: attributes, remaining }
726
+ }
727
+
728
+ /**
729
+ * Assemble JSXAttribute nodes for the resolved truncate props. Drops
730
+ * `numberOfLines` when zero (reset) and drops `ellipsizeMode` when not
731
+ * paired with a positive line count — matching RN's behaviour where
732
+ * `ellipsizeMode` needs `numberOfLines` to do anything.
733
+ * @param numberOfLines Resolved clamp count, or undefined.
734
+ * @param ellipsizeMode Resolved ellipsize mode, or undefined.
735
+ * @returns Zero, one, or two JSX attributes.
736
+ */
737
+ function buildTruncateAttributes(
738
+ numberOfLines: number | undefined,
739
+ ellipsizeMode: 'tail' | 'clip' | undefined,
740
+ ): readonly t.JSXAttribute[] {
741
+ const attributes: t.JSXAttribute[] = []
742
+ if (numberOfLines !== undefined && numberOfLines > 0) {
743
+ attributes.push(
744
+ t.jsxAttribute(t.jsxIdentifier('numberOfLines'), t.jsxExpressionContainer(t.numericLiteral(numberOfLines))),
745
+ )
746
+ if (ellipsizeMode !== undefined) {
747
+ attributes.push(t.jsxAttribute(t.jsxIdentifier('ellipsizeMode'), t.stringLiteral(ellipsizeMode)))
748
+ }
749
+ }
750
+ return attributes
751
+ }
752
+
753
+ /**
754
+ * Map of variant-prefix → trigger. Bare atoms (no colon) resolve to
755
+ * `'mount'` through {@link extractHapticSpec}; these entries cover the
756
+ * explicit `active:` / `focus:` / `hover:` cases.
757
+ */
758
+ const HAPTIC_VARIANT_TRIGGER: Record<string, Exclude<HapticTrigger, 'mount'>> = {
759
+ active: 'pressIn',
760
+ focus: 'focus',
761
+ hover: 'hover',
762
+ }
763
+
764
+ /** Map a non-mount haptic trigger to the JSX event prop it chains onto. */
765
+ const HAPTIC_EVENT_PROP: Record<Exclude<HapticTrigger, 'mount'>, string> = {
766
+ pressIn: 'onPressIn',
767
+ pressOut: 'onPressOut',
768
+ focus: 'onFocus',
769
+ hover: 'onMouseEnter',
770
+ }
771
+
772
+ /**
773
+ * Scan atom list for haptic utilities. Bare `haptic-*` → mount trigger;
774
+ * `active:haptic-*` / `focus:haptic-*` / `hover:haptic-*` → the matching
775
+ * event trigger. Matched atoms are stripped from the remainder so the
776
+ * runtime style resolver never tries to look up `--rnwind-haptic`.
777
+ * @param atoms Post-gradient, post-truncate atom list.
778
+ * @param rewriteCtx Rewrite-wide state (for the haptic-atom map).
779
+ * @returns Mount + event haptic entries, plus the non-haptic remainder.
780
+ */
781
+ function extractHapticSpec(
782
+ atoms: readonly string[],
783
+ rewriteCtx: RewriteContext,
784
+ ): {
785
+ mountHaptics: readonly HapticRequest[]
786
+ eventHaptics: readonly { readonly request: HapticRequest; readonly trigger: Exclude<HapticTrigger, 'mount'> }[]
787
+ remaining: readonly string[]
788
+ } {
789
+ const { hapticAtoms } = rewriteCtx
790
+ if (hapticAtoms.size === 0) return { mountHaptics: [], eventHaptics: [], remaining: atoms }
791
+ const mountHaptics: HapticRequest[] = []
792
+ const eventHaptics: { request: HapticRequest; trigger: Exclude<HapticTrigger, 'mount'> }[] = []
793
+ const remaining: string[] = []
794
+ for (const atom of atoms) {
795
+ const resolved = resolveHapticAtom(atom, hapticAtoms)
796
+ if (!resolved) {
797
+ remaining.push(atom)
798
+ continue
799
+ }
800
+ if (resolved.trigger === 'mount') mountHaptics.push(resolved.request)
801
+ else eventHaptics.push({ request: resolved.request, trigger: resolved.trigger })
802
+ }
803
+ return { mountHaptics, eventHaptics, remaining }
804
+ }
805
+
806
+ /**
807
+ * Classify one atom against the parser's haptic map. A colon-free atom
808
+ * maps to `'mount'`; `active:` / `focus:` / `hover:` prefixes map to
809
+ * the matching event trigger. Other prefixes return `null` so they
810
+ * fall through to the regular style path.
811
+ * @param atom Atom name, possibly variant-prefixed.
812
+ * @param hapticAtoms Parser-surfaced haptic metadata.
813
+ * @returns `{request, trigger}` on match, null otherwise.
814
+ */
815
+ function resolveHapticAtom(
816
+ atom: string,
817
+ hapticAtoms: ReadonlyMap<string, HapticRequest>,
818
+ ): { readonly request: HapticRequest; readonly trigger: HapticTrigger } | null {
819
+ // Direct lookup first — Tailwind v4 registers the variant-prefixed
820
+ // class (e.g. `active:haptic-medium`) as its own rule, and the
821
+ // parser's nested-rule walk surfaces the marker under that key.
822
+ const direct = hapticAtoms.get(atom)
823
+ if (direct) {
824
+ const colon = atom.indexOf(':')
825
+ if (colon === -1) return { request: direct, trigger: 'mount' }
826
+ const trigger = HAPTIC_VARIANT_TRIGGER[atom.slice(0, colon)]
827
+ if (trigger) return { request: direct, trigger }
828
+ return null
829
+ }
830
+ // Fallback — try stripping a known variant prefix and looking up
831
+ // the bare class. Handles cases where the parser only registered
832
+ // the base utility (the variant rule may be missing if only the
833
+ // bare class is otherwise used in the theme).
834
+ const colon = atom.indexOf(':')
835
+ if (colon === -1) return null
836
+ const prefix = atom.slice(0, colon)
837
+ const trigger = HAPTIC_VARIANT_TRIGGER[prefix]
838
+ if (!trigger) return null
839
+ const bare = hapticAtoms.get(atom.slice(colon + 1))
840
+ if (!bare) return null
841
+ return { request: bare, trigger }
842
+ }
843
+
844
+ /**
845
+ * Append mount-haptic requests to the aggregate keyed by the JSX site's
846
+ * enclosing component body. Post-traversal the transformer injects one
847
+ * `useMountHaptic(<hoisted>)` call per component.
848
+ * @param attributePath The JSXAttribute path the haptic came from.
849
+ * @param requests Mount requests gathered from this literal.
850
+ * @param rewriteCtx Rewrite-wide state.
851
+ */
852
+ function recordMountHaptics(
853
+ attributePath: NodePath<t.JSXAttribute>,
854
+ requests: readonly HapticRequest[],
855
+ rewriteCtx: RewriteContext,
856
+ ): void {
857
+ const body = findComponentBody(attributePath)
858
+ if (!body) return
859
+ const bucket = rewriteCtx.mountByComponent.get(body.node)
860
+ if (bucket) {
861
+ bucket.push(...requests)
862
+ return
863
+ }
864
+ rewriteCtx.mountByComponent.set(body.node, [...requests])
865
+ }
866
+
867
+ /**
868
+ * Splice one chained event handler per event-haptic entry onto the
869
+ * JSXOpeningElement. Each handler calls `triggerHaptic(_h, <request>,
870
+ * '<trigger>')` and then forwards to any pre-existing user handler.
871
+ * @param attributePath Path of the className attribute being rewritten.
872
+ * @param opening Opening element to mutate.
873
+ * @param entries Event-haptic entries.
874
+ * @param rewriteCtx Rewrite-wide state (for the hoister).
875
+ */
876
+ function injectEventHapticHandlers(
877
+ attributePath: NodePath<t.JSXAttribute>,
878
+ opening: t.JSXOpeningElement,
879
+ entries: readonly { readonly request: HapticRequest; readonly trigger: Exclude<HapticTrigger, 'mount'> }[],
880
+ rewriteCtx: RewriteContext,
881
+ ): void {
882
+ // Make sure `_t = _r()` is in scope — haptic dispatcher reads `_t.onHaptics`.
883
+ injectContextHook(attributePath)
884
+ rewriteCtx.needsHapticsHook = true
885
+ const byTrigger = new Map<Exclude<HapticTrigger, 'mount'>, HapticRequest[]>()
886
+ for (const { request, trigger } of entries) {
887
+ const list = byTrigger.get(trigger)
888
+ if (list) list.push(request)
889
+ else byTrigger.set(trigger, [request])
890
+ }
891
+ for (const [trigger, requests] of byTrigger) {
892
+ const eventProperty = HAPTIC_EVENT_PROP[trigger]
893
+ const existing = extractAndDropSiblingStyle(opening, eventProperty)
894
+ const handler = buildChainedHapticHandler(rewriteCtx, requests, trigger, existing)
895
+ opening.attributes.push(t.jsxAttribute(t.jsxIdentifier(eventProperty), t.jsxExpressionContainer(handler)))
896
+ }
897
+ }
898
+
899
+ /**
900
+ * Build the inline arrow body for one chained handler — dispatch every
901
+ * request in `requests` via `triggerHaptic`, then forward the event to
902
+ * the user-supplied handler (if any) via `existing?.(event)`.
903
+ * @param rewriteCtx Rewrite-wide state.
904
+ * @param requests Requests that share this trigger.
905
+ * @param trigger Lifecycle trigger this handler fires on.
906
+ * @param existing User-supplied event handler expression, or null.
907
+ * @returns ArrowFunctionExpression ready to splice into a JSXAttribute.
908
+ */
909
+ function buildChainedHapticHandler(
910
+ rewriteCtx: RewriteContext,
911
+ requests: readonly HapticRequest[],
912
+ trigger: Exclude<HapticTrigger, 'mount'>,
913
+ existing: t.Expression | null,
914
+ ): t.ArrowFunctionExpression {
915
+ const eventId = t.identifier('_e')
916
+ const body: t.Statement[] = []
917
+ for (const request of requests) {
918
+ const ref = rewriteCtx.hapticHoister.refForRequest(request)
919
+ body.push(
920
+ t.expressionStatement(
921
+ t.callExpression(t.identifier(TRIGGER_HAPTIC), [
922
+ t.memberExpression(t.identifier(CONTEXT_BINDING), t.identifier('onHaptics')),
923
+ ref,
924
+ t.stringLiteral(trigger),
925
+ ]),
926
+ ),
927
+ )
928
+ }
929
+ if (existing) {
930
+ body.push(
931
+ t.expressionStatement(
932
+ t.optionalCallExpression(existing, [eventId], true),
933
+ ),
934
+ )
935
+ }
936
+ return t.arrowFunctionExpression([eventId], t.blockStatement(body))
937
+ }
938
+
939
+ /**
940
+ * Walk the aggregated `mountByComponent` map and inject a single
941
+ * `useMountHaptic(<hoisted requests>)` call per component body.
942
+ * The hoist yields a stable `const _hm_<hash>` referencing a frozen
943
+ * array of request objects.
944
+ * @param rewriteCtx Rewrite-wide state.
945
+ */
946
+ function injectMountHapticCalls(rewriteCtx: RewriteContext): void {
947
+ for (const [body, requests] of rewriteCtx.mountByComponent) {
948
+ const ref = rewriteCtx.hapticHoister.refForRequestList(requests)
949
+ const declaration = t.expressionStatement(
950
+ t.callExpression(t.identifier(USE_MOUNT_HAPTIC), [ref]),
951
+ )
952
+ body.body.unshift(declaration)
953
+ }
954
+ }
955
+
956
+ /** One hoisted haptic entry — either a single request or a frozen list. */
957
+ type HapticEntry =
958
+ | { readonly kind: 'request'; readonly request: HapticRequest }
959
+ | { readonly kind: 'list'; readonly requests: readonly HapticRequest[] }
960
+
961
+ /** Hoister for haptic request objects + mount-request arrays. */
962
+ interface HapticHoister {
963
+ /** Hoist (or fetch) a single request const (`_hr_<hash>`). */
964
+ refForRequest: (request: HapticRequest) => t.Identifier
965
+ /** Hoist (or fetch) a frozen array of requests (`_hm_<hash>`). */
966
+ refForRequestList: (requests: readonly HapticRequest[]) => t.Identifier
967
+ /** All hoisted entries in insertion order. */
968
+ entries: ReadonlyMap<string, HapticEntry>
969
+ }
970
+
971
+ /**
972
+ * Derive a stable cache key for one {@link HapticRequest}. Keys are
973
+ * used both for hoister interning and mount-list digesting.
974
+ * @param request Haptic request.
975
+ * @returns Canonical key text.
976
+ */
977
+ function keyForHapticRequest(request: HapticRequest): string {
978
+ if (request.kind === 'impact') return `impact:${request.style}`
979
+ if (request.kind === 'notification') return `notification:${request.type}`
980
+ return 'selection'
981
+ }
982
+
983
+ /**
984
+ * Build the haptic hoist table. Single requests and mount-request
985
+ * lists each get their own module-scope frozen const so component
986
+ * bodies only reference stable identifiers — no per-render allocation.
987
+ * @returns HapticHoister API.
988
+ */
989
+ function createHapticHoister(): HapticHoister {
990
+ const pool = createInternPool<HapticEntry>()
991
+ const refForRequest = (request: HapticRequest): t.Identifier =>
992
+ pool.intern('_hr', `req:${keyForHapticRequest(request)}`, { kind: 'request', request })
993
+ const refForRequestList = (requests: readonly HapticRequest[]): t.Identifier =>
994
+ pool.intern('_hm', `list:${requests.map((request) => keyForHapticRequest(request)).join('|')}`, {
995
+ kind: 'list',
996
+ requests,
997
+ })
998
+ return { refForRequest, refForRequestList, entries: pool.entries }
999
+ }
1000
+
1001
+ /**
1002
+ * Generic intern-pool: one shared cache for module-scope consts keyed
1003
+ * by an arbitrary string. Returns a stable `t.Identifier` per key and
1004
+ * records `{name → entry}` for the emitter pass.
1005
+ * @returns Intern API.
1006
+ */
1007
+ function createInternPool<Entry>(): {
1008
+ intern: (prefix: string, key: string, entry: Entry) => t.Identifier
1009
+ entries: ReadonlyMap<string, Entry>
1010
+ } {
1011
+ const byKey = new Map<string, t.Identifier>()
1012
+ const entries = new Map<string, Entry>()
1013
+ const intern = (prefix: string, key: string, entry: Entry): t.Identifier => {
1014
+ const existing = byKey.get(key)
1015
+ if (existing) return existing
1016
+ const hash = createHash('sha256').update(key).digest('hex').slice(0, 12)
1017
+ const name = `${prefix}_${hash}`
1018
+ const ident = t.identifier(name)
1019
+ byKey.set(key, ident)
1020
+ entries.set(name, entry)
1021
+ return ident
1022
+ }
1023
+ return { intern, entries }
1024
+ }
1025
+
1026
+ /**
1027
+ * Emit `const _hr_<hash> = Object.freeze({...})` and `const _hm_<hash>
1028
+ * = Object.freeze([{...}, ...])` statements at module scope — one per
1029
+ * hoister entry.
1030
+ * @param ast Babel File AST.
1031
+ * @param entries Hoister entries.
1032
+ */
1033
+ function injectHapticConsts(ast: File, entries: ReadonlyMap<string, HapticEntry>): void {
1034
+ const declarations: t.Statement[] = []
1035
+ for (const [name, entry] of entries) {
1036
+ if (entry.kind === 'request') {
1037
+ declarations.push(
1038
+ t.variableDeclaration('const', [
1039
+ t.variableDeclarator(t.identifier(name), freezeExpression(requestLiteral(entry.request))),
1040
+ ]),
1041
+ )
1042
+ } else {
1043
+ declarations.push(
1044
+ t.variableDeclaration('const', [
1045
+ t.variableDeclarator(
1046
+ t.identifier(name),
1047
+ freezeExpression(t.arrayExpression(entry.requests.map((request) => freezeExpression(requestLiteral(request))))),
1048
+ ),
1049
+ ]),
1050
+ )
1051
+ }
1052
+ }
1053
+ ast.program.body.unshift(...declarations)
1054
+ }
1055
+
1056
+ /**
1057
+ * Build an `Object.freeze(...)` call around the given expression.
1058
+ * @param value Expression to freeze.
1059
+ * @returns CallExpression node.
1060
+ */
1061
+ function freezeExpression(value: t.Expression): t.CallExpression {
1062
+ return t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('freeze')), [value])
1063
+ }
1064
+
1065
+ /**
1066
+ * Build an object-literal representation of a {@link HapticRequest} —
1067
+ * `{kind: 'impact', style: 'Light'}` etc.
1068
+ * @param request Haptic request.
1069
+ * @returns ObjectExpression node.
1070
+ */
1071
+ function requestLiteral(request: HapticRequest): t.ObjectExpression {
1072
+ const properties: t.ObjectProperty[] = [
1073
+ t.objectProperty(t.identifier('kind'), t.stringLiteral(request.kind)),
1074
+ ]
1075
+ if (request.kind === 'impact') properties.push(t.objectProperty(t.identifier('style'), t.stringLiteral(request.style)))
1076
+ else if (request.kind === 'notification') properties.push(t.objectProperty(t.identifier('type'), t.stringLiteral(request.type)))
1077
+ return t.objectExpression(properties)
1078
+ }
1079
+
1080
+ /**
1081
+ * Normalise the `from/via/to` triple into the array
1082
+ * `<LinearGradient colors={…}>` expects: drop `null` entries while
1083
+ * keeping the source order.
1084
+ * @param from Hex colour for `from-*`, or null.
1085
+ * @param via Hex colour for `via-*`, or null.
1086
+ * @param to Hex colour for `to-*`, or null.
1087
+ * @returns Colour array (at least one entry guaranteed by the caller).
1088
+ */
1089
+ function gradientColors(from: string | null, via: string | null, to: string | null): readonly string[] {
1090
+ const out: string[] = []
1091
+ if (from !== null) out.push(from)
1092
+ if (via !== null) out.push(via)
1093
+ if (to !== null) out.push(to)
1094
+ return out
1095
+ }
1096
+
1097
+ /**
1098
+ * Map Tailwind's stock direction tag to the `(start, end)` pair of
1099
+ * unit-square points expo-linear-gradient expects. Pure constants —
1100
+ * the same as NativeWind and the wider RN-gradient community.
1101
+ * @param dir Compact direction tag from the parser.
1102
+ * @returns Start + end point records.
1103
+ */
1104
+ function directionToPoints(dir: GradientDirection): {
1105
+ start: { x: number; y: number }
1106
+ end: { x: number; y: number }
1107
+ } {
1108
+ switch (dir) {
1109
+ case 'to-r': {
1110
+ return { start: { x: 0, y: 0.5 }, end: { x: 1, y: 0.5 } }
1111
+ }
1112
+ case 'to-l': {
1113
+ return { start: { x: 1, y: 0.5 }, end: { x: 0, y: 0.5 } }
1114
+ }
1115
+ case 'to-t': {
1116
+ return { start: { x: 0.5, y: 1 }, end: { x: 0.5, y: 0 } }
1117
+ }
1118
+ case 'to-b': {
1119
+ return { start: { x: 0.5, y: 0 }, end: { x: 0.5, y: 1 } }
1120
+ }
1121
+ case 'to-tr': {
1122
+ return { start: { x: 0, y: 1 }, end: { x: 1, y: 0 } }
1123
+ }
1124
+ case 'to-tl': {
1125
+ return { start: { x: 1, y: 1 }, end: { x: 0, y: 0 } }
1126
+ }
1127
+ case 'to-br': {
1128
+ return { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } }
1129
+ }
1130
+ case 'to-bl': {
1131
+ return { start: { x: 1, y: 0 }, end: { x: 0, y: 1 } }
1132
+ }
1133
+ default: {
1134
+ return { start: { x: 0, y: 0.5 }, end: { x: 1, y: 0.5 } }
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ /**
1140
+ * Look for a sibling style attribute on the same JSXOpeningElement, drop
1141
+ * it, and return its expression for the caller to pass as the `lookupCss`
1142
+ * third arg. The attribute name is parameterised so the prefix path can
1143
+ * pull `contentContainerStyle` (et al.) instead of plain `style`.
1144
+ * @param parent JSXOpeningElement containing the className we're rewriting.
1145
+ * @param styleProp The exact sibling attribute name to look for.
1146
+ * @param styleProperty
1147
+ * @returns Expression from the dropped attribute, or `null`.
1148
+ */
1149
+ function extractAndDropSiblingStyle(parent: t.JSXOpeningElement, styleProperty: string): t.Expression | null {
1150
+ const { attributes } = parent
1151
+ for (let index = 0; index < attributes.length; index += 1) {
1152
+ const attribute = attributes[index]
1153
+ if (!t.isJSXAttribute(attribute)) continue
1154
+ if (!t.isJSXIdentifier(attribute.name) || attribute.name.name !== styleProperty) continue
1155
+ const { value } = attribute
1156
+ if (!value || !t.isJSXExpressionContainer(value)) return null
1157
+ const { expression } = value
1158
+ if (t.isJSXEmptyExpression(expression)) return null
1159
+ attributes.splice(index, 1)
1160
+ return expression as t.Expression
1161
+ }
1162
+ return null
1163
+ }
1164
+
1165
+ const INJECTED = new WeakSet<t.BlockStatement>()
1166
+ /**
1167
+ * Walk up from the rewrite site to the nearest enclosing function
1168
+ * component and inject `const _t = _r()` at the top of its body. This
1169
+ * is the SINGLE rnwind context binding — `_t` carries scheme,
1170
+ * fontScale, insets, etc. Idempotent per component.
1171
+ * @param path Path of any node inside the component's JSX.
1172
+ * @returns The binding name (`_t`).
1173
+ */
1174
+ function injectContextHook(path: NodePath): string {
1175
+ const componentBody = findComponentBody(path)
1176
+ if (!componentBody) return CONTEXT_BINDING
1177
+ if (INJECTED.has(componentBody.node)) return CONTEXT_BINDING
1178
+ INJECTED.add(componentBody.node)
1179
+ const declaration = t.variableDeclaration('const', [
1180
+ t.variableDeclarator(t.identifier(CONTEXT_BINDING), t.callExpression(t.identifier(USE_RNWIND_INTERNAL), [])),
1181
+ ])
1182
+ componentBody.unshiftContainer('body', declaration)
1183
+ return CONTEXT_BINDING
1184
+ }
1185
+
1186
+ /**
1187
+ * Walk up from `path` to the nearest recognised function component.
1188
+ * Accepts:
1189
+ * - `function Capital() {}` declarations.
1190
+ * - `const Capital = () => …` / `const Capital = function () {}` bindings.
1191
+ * - `forwardRef(…)` / `memo(…)` argument callbacks.
1192
+ * - `export default function () {}`.
1193
+ *
1194
+ * Arrow components with expression bodies get promoted to block bodies
1195
+ * so the hook can be `unshift`ed.
1196
+ * @param path Starting path.
1197
+ * @returns BlockStatement path of the component's body, or `null`.
1198
+ */
1199
+ function findComponentBody(path: NodePath): NodePath<t.BlockStatement> | null {
1200
+ let current: NodePath | null = path
1201
+ while (current) {
1202
+ const fn = current.findParent((parent) => parent.isFunction())
1203
+ if (!fn) return null
1204
+ if (isComponentFunction(fn))
1205
+ return ensureBlockBody(fn as NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>)
1206
+ current = fn
1207
+ }
1208
+ return null
1209
+ }
1210
+
1211
+ /**
1212
+ * Classify a function path as a React component per the three accepted
1213
+ * shapes (PascalCase decl, PascalCase var assignment, forwardRef/memo
1214
+ * argument, default export).
1215
+ * @param fn Function-like path.
1216
+ * @returns Whether the path is a React function component.
1217
+ */
1218
+ function isComponentFunction(fn: NodePath): boolean {
1219
+ if (fn.isFunctionDeclaration()) {
1220
+ const { id } = fn.node
1221
+ if (!id) return isExportDefaultValue(fn)
1222
+ return isPascalCase(id.name)
1223
+ }
1224
+ if (fn.isArrowFunctionExpression() || fn.isFunctionExpression()) {
1225
+ return isAssignedToPascalCase(fn) || isHocArgument(fn) || isExportDefaultValue(fn)
1226
+ }
1227
+ return false
1228
+ }
1229
+
1230
+ /**
1231
+ * Whether this function is the value of an `export default`.
1232
+ * @param fn Babel path pointing at the function node.
1233
+ * @returns True when the node is directly the default export value.
1234
+ */
1235
+ function isExportDefaultValue(fn: NodePath): boolean {
1236
+ const { parent } = fn
1237
+ if (t.isExportDefaultDeclaration(parent)) return parent.declaration === fn.node
1238
+ return false
1239
+ }
1240
+
1241
+ /**
1242
+ * Whether this arrow/function-expression is the init of `const Capital = …`.
1243
+ * @param fn Babel path pointing at the function node.
1244
+ * @returns True when the enclosing declarator's id starts with an uppercase letter.
1245
+ */
1246
+ function isAssignedToPascalCase(fn: NodePath): boolean {
1247
+ const { parent } = fn
1248
+ if (!t.isVariableDeclarator(parent)) return false
1249
+ if (!t.isIdentifier(parent.id)) return false
1250
+ return isPascalCase(parent.id.name)
1251
+ }
1252
+
1253
+ /**
1254
+ * Whether this fn is the first argument to `forwardRef(...)` / `memo(...)`.
1255
+ * @param fn Babel path pointing at the function node.
1256
+ * @returns True when wrapped by a recognized React HOC call.
1257
+ */
1258
+ function isHocArgument(fn: NodePath): boolean {
1259
+ const { parent } = fn
1260
+ if (!t.isCallExpression(parent)) return false
1261
+ if (parent.arguments[0] !== fn.node) return false
1262
+ const { callee } = parent
1263
+ if (!t.isIdentifier(callee)) return false
1264
+ return callee.name === 'forwardRef' || callee.name === 'memo'
1265
+ }
1266
+
1267
+ /**
1268
+ * Identifier-starts-with-uppercase — Conventional React component marker.
1269
+ * @param name Identifier text.
1270
+ * @returns True when the first character is `A`–`Z`.
1271
+ */
1272
+ function isPascalCase(name: string): boolean {
1273
+ const first = name.charAt(0)
1274
+ return first >= 'A' && first <= 'Z'
1275
+ }
1276
+
1277
+ /**
1278
+ * Promote an expression-bodied arrow to a block so we can unshift statements in.
1279
+ * @param fn Babel path at the function / arrow whose body should be a block.
1280
+ * @returns The path, mutated in place when the body was an expression.
1281
+ */
1282
+ function ensureBlockBody(
1283
+ fn: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
1284
+ ): NodePath<t.BlockStatement> {
1285
+ const bodyPath = fn.get('body')
1286
+ if (Array.isArray(bodyPath)) throw new Error('rnwind: unexpected multi-body function node')
1287
+ if (bodyPath.isBlockStatement()) return bodyPath as NodePath<t.BlockStatement>
1288
+ const node = bodyPath.node as t.Expression
1289
+ bodyPath.replaceWith(t.blockStatement([t.returnStatement(node)]))
1290
+ return bodyPath as NodePath<t.BlockStatement>
1291
+ }
1292
+
1293
+ type Hoister = {
1294
+ /** Return an identifier referencing the hoisted const for this atom list. */
1295
+ refFor: (atoms: readonly string[]) => t.Identifier
1296
+ /** Read-only view of every (const name → atoms) pair the hoister built. */
1297
+ entries: ReadonlyMap<string, readonly string[]>
1298
+ }
1299
+
1300
+ /**
1301
+ * Build a per-file hoist table. Every unique source-order atom list gets
1302
+ * one module-scope `const _c_<hash> = Object.freeze(['a', 'b'])`. Order
1303
+ * is part of the hash key — `className="a b"` and `className="b a"`
1304
+ * intentionally produce different hoisted consts because RN's style
1305
+ * flatten is order-dependent (later atoms override earlier ones for
1306
+ * conflicting props). Canonicalizing by sort would collapse
1307
+ * `opacity-100 opacity-0` and `opacity-0 opacity-100` to the same atom
1308
+ * list and silently break the user's intended last-wins override.
1309
+ * @returns Hoister API.
1310
+ */
1311
+ function createHoister(): Hoister {
1312
+ const byKey = new Map<string, { name: string; atoms: readonly string[] }>()
1313
+ const entries = new Map<string, readonly string[]>()
1314
+
1315
+ const refFor = (atoms: readonly string[]): t.Identifier => {
1316
+ const ordered = [...atoms]
1317
+ const canonical = ordered.join('\0')
1318
+ const hit = byKey.get(canonical)
1319
+ if (hit) return t.identifier(hit.name)
1320
+ const hash = createHash('sha256').update(canonical).digest('hex').slice(0, 12)
1321
+ const name = `_c_${hash}`
1322
+ byKey.set(canonical, { name, atoms: ordered })
1323
+ entries.set(name, ordered)
1324
+ return t.identifier(name)
1325
+ }
1326
+
1327
+ return { refFor, entries }
1328
+ }
1329
+
1330
+ /** One gradient-hoist entry — either a colour array or a single {x,y} point. */
1331
+ type GradientEntry = { readonly kind: 'colors'; readonly colors: readonly string[] } | { readonly kind: 'point'; readonly point: { x: number; y: number } }
1332
+
1333
+ type GradientHoister = {
1334
+ /** Hoist (or fetch) the colour-array const for a gradient. */
1335
+ refForColors: (colors: readonly string[]) => t.Identifier
1336
+ /** Hoist (or fetch) a start/end point const — role only affects prefix. */
1337
+ refForPoint: (point: { x: number; y: number }, role: 'start' | 'end') => t.Identifier
1338
+ /** All hoisted entries in insertion order. */
1339
+ entries: ReadonlyMap<string, GradientEntry>
1340
+ }
1341
+
1342
+ /**
1343
+ * Build the gradient hoist table. Colour arrays and `(x,y)` point
1344
+ * records each get their own module-scope `const _g_<hash>` so the
1345
+ * JSX site references a stable identity — `<LinearGradient
1346
+ * colors={_g_hash}>`'s prop never changes across renders, which lets
1347
+ * React's prop-diff short-circuit and keeps native-side gradient
1348
+ * rebuilds off the hot path.
1349
+ * @returns GradientHoister API.
1350
+ */
1351
+ function createGradientHoister(): GradientHoister {
1352
+ const pool = createInternPool<GradientEntry>()
1353
+ const refForColors = (colors: readonly string[]): t.Identifier =>
1354
+ pool.intern('_g', `colors:${colors.join('|')}`, { kind: 'colors', colors })
1355
+ const refForPoint = (point: { x: number; y: number }, role: 'start' | 'end'): t.Identifier => {
1356
+ const prefix = role === 'start' ? '_gs' : '_ge'
1357
+ return pool.intern(prefix, `point:${point.x},${point.y}`, { kind: 'point', point })
1358
+ }
1359
+ return { refForColors, refForPoint, entries: pool.entries }
1360
+ }
1361
+
1362
+ /**
1363
+ * Prepend the runtime + style imports to the file's program body.
1364
+ * Runtime primitives `{lookupCss, useScheme}` are
1365
+ * only added when the rewritten code references them. Side-effect
1366
+ * imports go first so the atom registry is populated before any
1367
+ * module-init hoist runs.
1368
+ /** Per-file flags telling the import builder what runtime symbols are in use.
1369
+ */
1370
+ interface RuntimeImportFlags {
1371
+ /** Any className rewrite ran — always pulls in `_r` (rnwind context hook). */
1372
+ touched: boolean
1373
+ /** At least one rewrite emitted an inline `lookupCss(...)` call. */
1374
+ usedLookupCss: boolean
1375
+ /** At least one rewrite swapped the tag for `<InteractiveBox>`. */
1376
+ usedInteractiveBox: boolean
1377
+ /** At least one component accumulated bare `haptic-*` mount requests. */
1378
+ usedMountHaptic: boolean
1379
+ /** At least one event-haptic chain emitted a `triggerHaptic(...)` call. */
1380
+ usedTriggerHaptic: boolean
1381
+ }
1382
+
1383
+ /**
1384
+ * Prepend the runtime + style imports to the file's program body.
1385
+ * Only the specifiers actually used by the rewritten code are added
1386
+ * — a file with only interactive rewrites skips `lookupCss` entirely
1387
+ * (it lives inside InteractiveBox) and vice versa.
1388
+ * @param ast File AST.
1389
+ * @param flags Which runtime symbols the rewritten code references.
1390
+ * @param styleSpecifiers Side-effect import specifiers (style.js + keyframes.js).
1391
+ */
1392
+ function prependRuntimeImports(ast: File, flags: RuntimeImportFlags, styleSpecifiers: readonly string[]): void {
1393
+ const heads: t.Statement[] = []
1394
+ for (const specifier of styleSpecifiers) {
1395
+ heads.push(t.importDeclaration([], t.stringLiteral(specifier)))
1396
+ }
1397
+ if (flags.touched) {
1398
+ heads.push(t.importDeclaration(buildRuntimeSpecifiers(flags), t.stringLiteral(RUNTIME_MODULE)))
1399
+ }
1400
+ if (heads.length > 0) ast.program.body.unshift(...heads)
1401
+ }
1402
+
1403
+ /**
1404
+ * Build the import specifiers for the `rnwind` runtime module — only
1405
+ * symbols the rewritten code actually references. Extracted from
1406
+ * {@link prependRuntimeImports} to keep cognitive complexity low.
1407
+ * @param flags Per-file usage flags.
1408
+ * @returns The import specifiers to splice into the runtime import.
1409
+ */
1410
+ function buildRuntimeSpecifiers(flags: RuntimeImportFlags): t.ImportSpecifier[] {
1411
+ const specifiers: t.ImportSpecifier[] = []
1412
+ const named = (name: string): void => {
1413
+ specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)))
1414
+ }
1415
+ if (flags.usedLookupCss) named(LOOKUP_CSS)
1416
+ named(USE_RNWIND_INTERNAL)
1417
+ if (flags.usedMountHaptic) named(USE_MOUNT_HAPTIC)
1418
+ if (flags.usedTriggerHaptic) named(TRIGGER_HAPTIC)
1419
+ if (flags.usedInteractiveBox) named(INTERACTIVE_BOX)
1420
+ return specifiers
1421
+ }
1422
+
1423
+ /**
1424
+ * Splice hoisted `const _c_<hash> = ['flex-1', 'bg-primary', ...]`
1425
+ * atom-list declarations into the file right after the imports so
1426
+ * every JSX rewrite site sees them in scope.
1427
+ *
1428
+ * The JSX site references the const as `lookupCss(_c0, _s, userStyle,
1429
+ * …)`. The runtime caches its resolved style array per
1430
+ * (hoist, scheme, stateIndex) against a global version counter, so
1431
+ * subsequent renders return the SAME array reference — zero
1432
+ * allocation on the hot path.
1433
+ * @param ast File AST.
1434
+ * @param entries Hoist table (const name → atom names).
1435
+ */
1436
+ function injectHoistedConsts(ast: File, entries: ReadonlyMap<string, readonly string[]>): void {
1437
+ const decls: t.Statement[] = []
1438
+ for (const [name, atoms] of entries) {
1439
+ const array = t.arrayExpression(atoms.map((atom) => t.stringLiteral(atom)))
1440
+ decls.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(name), array)]))
1441
+ }
1442
+ spliceAfterImports(ast, decls)
1443
+ }
1444
+
1445
+ /**
1446
+ * Splice gradient const declarations after the imports. Each entry is
1447
+ * either `colors` (frozen string array) or a `point` ({x, y} object
1448
+ * literal) so `<LinearGradient>` gets a stable ref for every gradient
1449
+ * shape.
1450
+ * @param ast File AST to mutate.
1451
+ * @param entries Gradient-hoister entries.
1452
+ */
1453
+ function injectGradientConsts(ast: File, entries: ReadonlyMap<string, GradientEntry>): void {
1454
+ const decls: t.Statement[] = []
1455
+ for (const [name, entry] of entries) {
1456
+ const init =
1457
+ entry.kind === 'colors'
1458
+ ? t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('freeze')), [
1459
+ t.arrayExpression(entry.colors.map((c) => t.stringLiteral(c))),
1460
+ ])
1461
+ : t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('freeze')), [
1462
+ t.objectExpression([
1463
+ t.objectProperty(t.identifier('x'), t.numericLiteral(entry.point.x)),
1464
+ t.objectProperty(t.identifier('y'), t.numericLiteral(entry.point.y)),
1465
+ ]),
1466
+ ])
1467
+ decls.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(name), init)]))
1468
+ }
1469
+ spliceAfterImports(ast, decls)
1470
+ }
1471
+
1472
+ /**
1473
+ * Insert a block of declarations right after the last import in the
1474
+ * program body. Shared helper for atom-hoist and gradient-hoist.
1475
+ * @param ast File AST.
1476
+ * @param decls Declarations to splice in (already-built statements).
1477
+ */
1478
+ function spliceAfterImports(ast: File, decls: readonly t.Statement[]): void {
1479
+ if (decls.length === 0) return
1480
+ const { body } = ast.program
1481
+ let index = 0
1482
+ while (index < body.length && t.isImportDeclaration(body[index])) index += 1
1483
+ body.splice(index, 0, ...decls)
1484
+ }
1485
+
1486
+ /**
1487
+ * Tokenize a classname literal — split on whitespace, drop empties.
1488
+ * Mirrors what Tailwind + the runtime tokenizer expect.
1489
+ * @param literal Raw classname text.
1490
+ * @returns Atom names in document order.
1491
+ */
1492
+ function tokenize(literal: string): string[] {
1493
+ const out: string[] = []
1494
+ for (const piece of literal.split(/\s+/)) {
1495
+ if (piece.length > 0) out.push(piece)
1496
+ }
1497
+ return out
1498
+ }