motionary 0.0.0-stage → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/CHANGELOG.md +803 -0
  2. package/LICENSE +21 -0
  3. package/README.md +218 -2
  4. package/README_ja.md +219 -0
  5. package/README_zh.md +219 -0
  6. package/bin/usa-codemod-5.mjs +79 -0
  7. package/bin/usa-codemod-6.mjs +146 -0
  8. package/dist/chunks/base-BaQV-2ha.cjs +436 -0
  9. package/dist/chunks/base-BaQV-2ha.cjs.map +1 -0
  10. package/dist/chunks/base-C_3cAoRz.js +404 -0
  11. package/dist/chunks/base-C_3cAoRz.js.map +1 -0
  12. package/dist/chunks/bind-B_CTL6Qn.js +22 -0
  13. package/dist/chunks/bind-B_CTL6Qn.js.map +1 -0
  14. package/dist/chunks/bind-Ui43-n2d.cjs +25 -0
  15. package/dist/chunks/bind-Ui43-n2d.cjs.map +1 -0
  16. package/dist/chunks/core-B5T0dhFH.cjs +920 -0
  17. package/dist/chunks/core-B5T0dhFH.cjs.map +1 -0
  18. package/dist/chunks/core-BGAyaY6L.cjs +265 -0
  19. package/dist/chunks/core-BGAyaY6L.cjs.map +1 -0
  20. package/dist/chunks/core-D7cVumUu.js +909 -0
  21. package/dist/chunks/core-D7cVumUu.js.map +1 -0
  22. package/dist/chunks/core-VEBw36QK.js +260 -0
  23. package/dist/chunks/core-VEBw36QK.js.map +1 -0
  24. package/dist/chunks/core-ps8uvYuN.js +181 -0
  25. package/dist/chunks/core-ps8uvYuN.js.map +1 -0
  26. package/dist/chunks/core-zq17EeCI.cjs +185 -0
  27. package/dist/chunks/core-zq17EeCI.cjs.map +1 -0
  28. package/dist/chunks/fx-Bs6IrV4Q.js +109 -0
  29. package/dist/chunks/fx-Bs6IrV4Q.js.map +1 -0
  30. package/dist/chunks/fx-lBGVtQO1.cjs +114 -0
  31. package/dist/chunks/fx-lBGVtQO1.cjs.map +1 -0
  32. package/dist/chunks/generative-D2YyhaeO.js +336 -0
  33. package/dist/chunks/generative-D2YyhaeO.js.map +1 -0
  34. package/dist/chunks/generative-DzIZq-_g.cjs +348 -0
  35. package/dist/chunks/generative-DzIZq-_g.cjs.map +1 -0
  36. package/dist/chunks/gpu-Bu7qwugO.js +380 -0
  37. package/dist/chunks/gpu-Bu7qwugO.js.map +1 -0
  38. package/dist/chunks/gpu-CenK2l6b.cjs +387 -0
  39. package/dist/chunks/gpu-CenK2l6b.cjs.map +1 -0
  40. package/dist/chunks/index-tags-DucKMQr_.js +26 -0
  41. package/dist/chunks/index-tags-DucKMQr_.js.map +1 -0
  42. package/dist/chunks/index-tags-wv0R_vkO.cjs +28 -0
  43. package/dist/chunks/index-tags-wv0R_vkO.cjs.map +1 -0
  44. package/dist/chunks/presets-BYBVJVeP.js +226 -0
  45. package/dist/chunks/presets-BYBVJVeP.js.map +1 -0
  46. package/dist/chunks/presets-CUHys3sK.cjs +233 -0
  47. package/dist/chunks/presets-CUHys3sK.cjs.map +1 -0
  48. package/dist/chunks/registry-CKNLQpwd.js +121 -0
  49. package/dist/chunks/registry-CKNLQpwd.js.map +1 -0
  50. package/dist/chunks/registry-DehBVRDV.cjs +131 -0
  51. package/dist/chunks/registry-DehBVRDV.cjs.map +1 -0
  52. package/dist/chunks/spring-CbvHfVtP.js +219 -0
  53. package/dist/chunks/spring-CbvHfVtP.js.map +1 -0
  54. package/dist/chunks/spring-Dgx187Vh.cjs +232 -0
  55. package/dist/chunks/spring-Dgx187Vh.cjs.map +1 -0
  56. package/dist/chunks/stagger-CCFyhzSw.cjs +86 -0
  57. package/dist/chunks/stagger-CCFyhzSw.cjs.map +1 -0
  58. package/dist/chunks/stagger-DtMKo2SK.js +84 -0
  59. package/dist/chunks/stagger-DtMKo2SK.js.map +1 -0
  60. package/dist/chunks/variants-BhjyddG8.cjs +39 -0
  61. package/dist/chunks/variants-BhjyddG8.cjs.map +1 -0
  62. package/dist/chunks/variants-DRlKVHvu.js +35 -0
  63. package/dist/chunks/variants-DRlKVHvu.js.map +1 -0
  64. package/dist/components/a11y.cjs +276 -0
  65. package/dist/components/a11y.cjs.map +1 -0
  66. package/dist/components/a11y.d.cts +131 -0
  67. package/dist/components/a11y.d.ts +131 -0
  68. package/dist/components/a11y.js +259 -0
  69. package/dist/components/a11y.js.map +1 -0
  70. package/dist/components/angular.cjs +76 -0
  71. package/dist/components/angular.cjs.map +1 -0
  72. package/dist/components/angular.d.cts +1734 -0
  73. package/dist/components/angular.d.ts +1734 -0
  74. package/dist/components/angular.js +70 -0
  75. package/dist/components/angular.js.map +1 -0
  76. package/dist/components/background.cjs +659 -0
  77. package/dist/components/background.cjs.map +1 -0
  78. package/dist/components/background.css +9 -0
  79. package/dist/components/background.d.cts +166 -0
  80. package/dist/components/background.d.ts +166 -0
  81. package/dist/components/background.js +647 -0
  82. package/dist/components/background.js.map +1 -0
  83. package/dist/components/bridge.cjs +154 -0
  84. package/dist/components/bridge.cjs.map +1 -0
  85. package/dist/components/bridge.d.cts +63 -0
  86. package/dist/components/bridge.d.ts +63 -0
  87. package/dist/components/bridge.js +147 -0
  88. package/dist/components/bridge.js.map +1 -0
  89. package/dist/components/cards.cjs +561 -0
  90. package/dist/components/cards.cjs.map +1 -0
  91. package/dist/components/cards.css +6 -0
  92. package/dist/components/cards.d.cts +115 -0
  93. package/dist/components/cards.d.ts +115 -0
  94. package/dist/components/cards.js +554 -0
  95. package/dist/components/cards.js.map +1 -0
  96. package/dist/components/click.cjs +877 -0
  97. package/dist/components/click.cjs.map +1 -0
  98. package/dist/components/click.css +9 -0
  99. package/dist/components/click.d.cts +218 -0
  100. package/dist/components/click.d.ts +218 -0
  101. package/dist/components/click.js +863 -0
  102. package/dist/components/click.js.map +1 -0
  103. package/dist/components/depth.cjs +250 -0
  104. package/dist/components/depth.cjs.map +1 -0
  105. package/dist/components/depth.css +3 -0
  106. package/dist/components/depth.d.cts +86 -0
  107. package/dist/components/depth.d.ts +86 -0
  108. package/dist/components/depth.js +242 -0
  109. package/dist/components/depth.js.map +1 -0
  110. package/dist/components/effects.cjs +2258 -0
  111. package/dist/components/effects.cjs.map +1 -0
  112. package/dist/components/effects.d.cts +545 -0
  113. package/dist/components/effects.d.ts +545 -0
  114. package/dist/components/effects.js +2200 -0
  115. package/dist/components/effects.js.map +1 -0
  116. package/dist/components/feedback.cjs +442 -0
  117. package/dist/components/feedback.cjs.map +1 -0
  118. package/dist/components/feedback.css +7 -0
  119. package/dist/components/feedback.d.cts +144 -0
  120. package/dist/components/feedback.d.ts +144 -0
  121. package/dist/components/feedback.js +433 -0
  122. package/dist/components/feedback.js.map +1 -0
  123. package/dist/components/fx-gpu.cjs +12 -0
  124. package/dist/components/fx-gpu.cjs.map +1 -0
  125. package/dist/components/fx-gpu.d.cts +63 -0
  126. package/dist/components/fx-gpu.d.ts +63 -0
  127. package/dist/components/fx-gpu.js +5 -0
  128. package/dist/components/fx-gpu.js.map +1 -0
  129. package/dist/components/fx.cjs +169 -0
  130. package/dist/components/fx.cjs.map +1 -0
  131. package/dist/components/fx.css +3 -0
  132. package/dist/components/fx.d.cts +114 -0
  133. package/dist/components/fx.d.ts +114 -0
  134. package/dist/components/fx.js +156 -0
  135. package/dist/components/fx.js.map +1 -0
  136. package/dist/components/fx2.cjs +25 -0
  137. package/dist/components/fx2.cjs.map +1 -0
  138. package/dist/components/fx2.d.cts +136 -0
  139. package/dist/components/fx2.d.ts +136 -0
  140. package/dist/components/fx2.js +17 -0
  141. package/dist/components/fx2.js.map +1 -0
  142. package/dist/components/gesture.cjs +209 -0
  143. package/dist/components/gesture.cjs.map +1 -0
  144. package/dist/components/gesture.css +3 -0
  145. package/dist/components/gesture.d.cts +134 -0
  146. package/dist/components/gesture.d.ts +134 -0
  147. package/dist/components/gesture.js +203 -0
  148. package/dist/components/gesture.js.map +1 -0
  149. package/dist/components/interaction.cjs +424 -0
  150. package/dist/components/interaction.cjs.map +1 -0
  151. package/dist/components/interaction.css +8 -0
  152. package/dist/components/interaction.d.cts +115 -0
  153. package/dist/components/interaction.d.ts +115 -0
  154. package/dist/components/interaction.js +416 -0
  155. package/dist/components/interaction.js.map +1 -0
  156. package/dist/components/jsx.cjs +3 -0
  157. package/dist/components/jsx.cjs.map +1 -0
  158. package/dist/components/jsx.d.cts +53 -0
  159. package/dist/components/jsx.d.ts +53 -0
  160. package/dist/components/jsx.js +2 -0
  161. package/dist/components/jsx.js.map +1 -0
  162. package/dist/components/layout.cjs +257 -0
  163. package/dist/components/layout.cjs.map +1 -0
  164. package/dist/components/layout.css +3 -0
  165. package/dist/components/layout.d.cts +99 -0
  166. package/dist/components/layout.d.ts +99 -0
  167. package/dist/components/layout.js +249 -0
  168. package/dist/components/layout.js.map +1 -0
  169. package/dist/components/lazy.cjs +92 -0
  170. package/dist/components/lazy.cjs.map +1 -0
  171. package/dist/components/lazy.d.cts +48 -0
  172. package/dist/components/lazy.d.ts +48 -0
  173. package/dist/components/lazy.js +87 -0
  174. package/dist/components/lazy.js.map +1 -0
  175. package/dist/components/lite.cjs +11095 -0
  176. package/dist/components/lite.cjs.map +1 -0
  177. package/dist/components/lite.d.cts +4 -0
  178. package/dist/components/lite.d.ts +4 -0
  179. package/dist/components/lite.js +10827 -0
  180. package/dist/components/lite.js.map +1 -0
  181. package/dist/components/packs.cjs +192 -0
  182. package/dist/components/packs.cjs.map +1 -0
  183. package/dist/components/packs.css +3 -0
  184. package/dist/components/packs.d.cts +72 -0
  185. package/dist/components/packs.d.ts +72 -0
  186. package/dist/components/packs.js +184 -0
  187. package/dist/components/packs.js.map +1 -0
  188. package/dist/components/page.cjs +824 -0
  189. package/dist/components/page.cjs.map +1 -0
  190. package/dist/components/page.css +11 -0
  191. package/dist/components/page.d.cts +249 -0
  192. package/dist/components/page.d.ts +249 -0
  193. package/dist/components/page.js +799 -0
  194. package/dist/components/page.js.map +1 -0
  195. package/dist/components/perf.cjs +120 -0
  196. package/dist/components/perf.cjs.map +1 -0
  197. package/dist/components/perf.d.cts +98 -0
  198. package/dist/components/perf.d.ts +98 -0
  199. package/dist/components/perf.js +110 -0
  200. package/dist/components/perf.js.map +1 -0
  201. package/dist/components/physics.cjs +448 -0
  202. package/dist/components/physics.cjs.map +1 -0
  203. package/dist/components/physics.css +5 -0
  204. package/dist/components/physics.d.cts +189 -0
  205. package/dist/components/physics.d.ts +189 -0
  206. package/dist/components/physics.js +430 -0
  207. package/dist/components/physics.js.map +1 -0
  208. package/dist/components/react.cjs +96 -0
  209. package/dist/components/react.cjs.map +1 -0
  210. package/dist/components/react.d.cts +18 -0
  211. package/dist/components/react.d.ts +18 -0
  212. package/dist/components/react.js +91 -0
  213. package/dist/components/react.js.map +1 -0
  214. package/dist/components/reveal.cjs +362 -0
  215. package/dist/components/reveal.cjs.map +1 -0
  216. package/dist/components/reveal.css +5 -0
  217. package/dist/components/reveal.d.cts +105 -0
  218. package/dist/components/reveal.d.ts +105 -0
  219. package/dist/components/reveal.js +353 -0
  220. package/dist/components/reveal.js.map +1 -0
  221. package/dist/components/solid.cjs +86 -0
  222. package/dist/components/solid.cjs.map +1 -0
  223. package/dist/components/solid.d.cts +1751 -0
  224. package/dist/components/solid.d.ts +1751 -0
  225. package/dist/components/solid.js +82 -0
  226. package/dist/components/solid.js.map +1 -0
  227. package/dist/components/svelte.cjs +68 -0
  228. package/dist/components/svelte.cjs.map +1 -0
  229. package/dist/components/svelte.d.cts +1733 -0
  230. package/dist/components/svelte.d.ts +1733 -0
  231. package/dist/components/svelte.js +64 -0
  232. package/dist/components/svelte.js.map +1 -0
  233. package/dist/components/svg.cjs +359 -0
  234. package/dist/components/svg.cjs.map +1 -0
  235. package/dist/components/svg.css +3 -0
  236. package/dist/components/svg.d.cts +113 -0
  237. package/dist/components/svg.d.ts +113 -0
  238. package/dist/components/svg.js +347 -0
  239. package/dist/components/svg.js.map +1 -0
  240. package/dist/components/text.cjs +968 -0
  241. package/dist/components/text.cjs.map +1 -0
  242. package/dist/components/text.css +8 -0
  243. package/dist/components/text.d.cts +342 -0
  244. package/dist/components/text.d.ts +342 -0
  245. package/dist/components/text.js +947 -0
  246. package/dist/components/text.js.map +1 -0
  247. package/dist/components/timeline.cjs +109 -0
  248. package/dist/components/timeline.cjs.map +1 -0
  249. package/dist/components/timeline.css +3 -0
  250. package/dist/components/timeline.d.cts +155 -0
  251. package/dist/components/timeline.d.ts +155 -0
  252. package/dist/components/timeline.js +103 -0
  253. package/dist/components/timeline.js.map +1 -0
  254. package/dist/components/tokens.cjs +226 -0
  255. package/dist/components/tokens.cjs.map +1 -0
  256. package/dist/components/tokens.d.cts +74 -0
  257. package/dist/components/tokens.d.ts +74 -0
  258. package/dist/components/tokens.js +211 -0
  259. package/dist/components/tokens.js.map +1 -0
  260. package/dist/components/transitions.cjs +430 -0
  261. package/dist/components/transitions.cjs.map +1 -0
  262. package/dist/components/transitions.css +5 -0
  263. package/dist/components/transitions.d.cts +126 -0
  264. package/dist/components/transitions.d.ts +126 -0
  265. package/dist/components/transitions.js +423 -0
  266. package/dist/components/transitions.js.map +1 -0
  267. package/dist/components/ui.cjs +1050 -0
  268. package/dist/components/ui.cjs.map +1 -0
  269. package/dist/components/ui.css +14 -0
  270. package/dist/components/ui.d.cts +224 -0
  271. package/dist/components/ui.d.ts +224 -0
  272. package/dist/components/ui.js +1034 -0
  273. package/dist/components/ui.js.map +1 -0
  274. package/dist/components/vue.cjs +67 -0
  275. package/dist/components/vue.cjs.map +1 -0
  276. package/dist/components/vue.d.cts +1722 -0
  277. package/dist/components/vue.d.ts +1722 -0
  278. package/dist/components/vue.js +64 -0
  279. package/dist/components/vue.js.map +1 -0
  280. package/dist/components/webgl.cjs +459 -0
  281. package/dist/components/webgl.cjs.map +1 -0
  282. package/dist/components/webgl.css +3 -0
  283. package/dist/components/webgl.d.cts +133 -0
  284. package/dist/components/webgl.d.ts +133 -0
  285. package/dist/components/webgl.js +442 -0
  286. package/dist/components/webgl.js.map +1 -0
  287. package/dist/components/widgets.cjs +591 -0
  288. package/dist/components/widgets.cjs.map +1 -0
  289. package/dist/components/widgets.d.cts +110 -0
  290. package/dist/components/widgets.d.ts +110 -0
  291. package/dist/components/widgets.js +581 -0
  292. package/dist/components/widgets.js.map +1 -0
  293. package/dist/components.cjs +351 -0
  294. package/dist/components.cjs.map +1 -0
  295. package/dist/components.css +75 -0
  296. package/dist/components.d.cts +2997 -0
  297. package/dist/components.d.ts +2997 -0
  298. package/dist/components.js +106 -0
  299. package/dist/components.js.map +1 -0
  300. package/dist/components.umd.js +23 -0
  301. package/dist/components.umd.js.map +1 -0
  302. package/dist/element.cjs +97 -0
  303. package/dist/element.cjs.map +1 -0
  304. package/dist/element.d.cts +215 -0
  305. package/dist/element.d.ts +215 -0
  306. package/dist/element.js +95 -0
  307. package/dist/element.js.map +1 -0
  308. package/dist/element.umd.js +2 -0
  309. package/dist/element.umd.js.map +1 -0
  310. package/dist/index.cjs +149 -0
  311. package/dist/index.cjs.map +1 -0
  312. package/dist/index.d.cts +425 -0
  313. package/dist/index.d.ts +425 -0
  314. package/dist/index.js +131 -0
  315. package/dist/index.js.map +1 -0
  316. package/dist/index.umd.js +15 -0
  317. package/dist/index.umd.js.map +1 -0
  318. package/dist/presets/extended.cjs +243 -0
  319. package/dist/presets/extended.cjs.map +1 -0
  320. package/dist/presets/extended.d.cts +37 -0
  321. package/dist/presets/extended.d.ts +37 -0
  322. package/dist/presets/extended.js +239 -0
  323. package/dist/presets/extended.js.map +1 -0
  324. package/dist/presets-extended.umd.js +2 -0
  325. package/dist/presets-extended.umd.js.map +1 -0
  326. package/dist/react.cjs +68 -0
  327. package/dist/react.cjs.map +1 -0
  328. package/dist/react.d.cts +158 -0
  329. package/dist/react.d.ts +158 -0
  330. package/dist/react.js +65 -0
  331. package/dist/react.js.map +1 -0
  332. package/dist/solid.cjs +93 -0
  333. package/dist/solid.cjs.map +1 -0
  334. package/dist/solid.d.cts +249 -0
  335. package/dist/solid.d.ts +249 -0
  336. package/dist/solid.js +89 -0
  337. package/dist/solid.js.map +1 -0
  338. package/dist/svelte.cjs +66 -0
  339. package/dist/svelte.cjs.map +1 -0
  340. package/dist/svelte.d.cts +243 -0
  341. package/dist/svelte.d.ts +243 -0
  342. package/dist/svelte.js +63 -0
  343. package/dist/svelte.js.map +1 -0
  344. package/dist/vue.cjs +62 -0
  345. package/dist/vue.cjs.map +1 -0
  346. package/dist/vue.d.cts +158 -0
  347. package/dist/vue.d.ts +158 -0
  348. package/dist/vue.js +60 -0
  349. package/dist/vue.js.map +1 -0
  350. package/dist/widgets.umd.js +2 -0
  351. package/dist/widgets.umd.js.map +1 -0
  352. package/docs/API.md +147 -0
  353. package/docs/ROADMAP.md +14 -0
  354. package/docs/accessibility.md +60 -0
  355. package/docs/components.md +564 -0
  356. package/docs/deprecations.md +30 -0
  357. package/docs/frameworks-ssr.md +100 -0
  358. package/docs/hybrid-apps.md +145 -0
  359. package/docs/images/showcase-detail.png +0 -0
  360. package/docs/images/showcase-grid.png +0 -0
  361. package/docs/images/showcase-mobile.png +0 -0
  362. package/docs/migration-from-aos.md +67 -0
  363. package/docs/migration-from-gsap-scrolltrigger.md +80 -0
  364. package/docs/motion-tokens.css +31 -0
  365. package/docs/motion-tokens.md +46 -0
  366. package/docs/motion.tokens.json +132 -0
  367. package/docs/performance.md +24 -0
  368. package/docs/presets.md +310 -0
  369. package/docs/upgrading-3.md +21 -0
  370. package/docs/upgrading-4.md +65 -0
  371. package/docs/upgrading-5.md +31 -0
  372. package/docs/upgrading-6.md +32 -0
  373. package/docs/windows-apps.md +119 -0
  374. package/package.json +562 -4
@@ -0,0 +1,2200 @@
1
+ import { registerBuiltinEffects } from './fx.js';
2
+ import { P as PALETTE, b as overlay, a as all, o as origin, s as spawn, r as rand, c as canvasBackground, h as hexRgb, f as fxLayer, G as GENERATIVE_FX } from '../chunks/generative-D2YyhaeO.js';
3
+ export { n as noise2 } from '../chunks/generative-D2YyhaeO.js';
4
+ import { x as defineElement, p as prefersReducedMotion, u as adoptStyles } from '../chunks/base-C_3cAoRz.js';
5
+ import { p as playEffect, b as bindEffect, c as registerEffects } from '../chunks/registry-CKNLQpwd.js';
6
+ import { applyMotionTokens, motionTokensToVars, mergeMotionTokens } from './tokens.js';
7
+ import { T as TIMELINE_PRESETS } from '../chunks/core-VEBw36QK.js';
8
+ import '../chunks/fx-Bs6IrV4Q.js';
9
+
10
+ const CARD_FX = [
11
+ {
12
+ name: 'holo',
13
+ kind: 'card',
14
+ description: 'Holographic foil: a rainbow sheen and sparkle that follow the pointer (persistent; bind with trigger="load").',
15
+ reduced: 'run',
16
+ defaults: { strength: 0.55 },
17
+ run: (el, o, ctx) => {
18
+ const [layer, remove] = overlay(el, `mix-blend-mode:color-dodge;opacity:${o.strength};background:linear-gradient(115deg,transparent 20%,#ff8bd855 35%,#8bf3ff55 45%,#fff58b55 55%,transparent 70%),repeating-linear-gradient(55deg,#ffffff10 0 2px,transparent 2px 6px);background-size:250% 250%,100% 100%;background-position:50% 50%;transition:background-position .2s ease-out`);
19
+ if (ctx.reduced)
20
+ return remove;
21
+ const move = (e) => {
22
+ const r = el.getBoundingClientRect();
23
+ const x = ((e.clientX - r.left) / (r.width || 1)) * 100;
24
+ const y = ((e.clientY - r.top) / (r.height || 1)) * 100;
25
+ layer.style.backgroundPosition = `${x}% ${y}%, 0 0`;
26
+ el.style.transform = `perspective(700px) rotateY(${(x - 50) / 8}deg) rotateX(${(50 - y) / 8}deg)`;
27
+ };
28
+ const leave = () => {
29
+ layer.style.backgroundPosition = '50% 50%, 0 0';
30
+ el.style.transform = '';
31
+ };
32
+ el.addEventListener('pointermove', move);
33
+ el.addEventListener('pointerleave', leave);
34
+ return () => {
35
+ el.removeEventListener('pointermove', move);
36
+ el.removeEventListener('pointerleave', leave);
37
+ leave();
38
+ remove();
39
+ };
40
+ },
41
+ },
42
+ {
43
+ name: 'glare-sweep',
44
+ kind: 'card',
45
+ description: 'A diagonal light streak sweeps across the card once.',
46
+ defaults: { duration: 900, color: 'rgba(255,255,255,.55)' },
47
+ run: (el, o, ctx) => {
48
+ if (ctx.reduced)
49
+ return;
50
+ const [g, remove] = overlay(el, `overflow:hidden;background:linear-gradient(105deg,transparent 35%,${o.color} 50%,transparent 65%);background-size:250% 100%`);
51
+ const a = ctx.animate(g, [{ backgroundPosition: '150% 0' }, { backgroundPosition: '-50% 0' }], { duration: o.duration, easing: 'ease-in-out' });
52
+ if (a)
53
+ a.finished.then(remove, remove);
54
+ else
55
+ remove();
56
+ return a;
57
+ },
58
+ },
59
+ {
60
+ name: 'book-open',
61
+ kind: 'card',
62
+ description: 'The card opens like a book cover around its left edge, peeks and closes.',
63
+ defaults: { angle: 35, duration: 1100 },
64
+ run: (el, o, ctx) => {
65
+ el.style.transformOrigin = 'left center';
66
+ return ctx.animate(el, [{ transform: 'perspective(900px) rotateY(0)' }, { transform: `perspective(900px) rotateY(-${o.angle}deg)`, offset: 0.45 }, { transform: 'perspective(900px) rotateY(0)' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.3, 0.64, 1)' });
67
+ },
68
+ },
69
+ {
70
+ name: 'card-fan',
71
+ kind: 'card',
72
+ description: 'The element’s children fan out like a hand of cards, then gather.',
73
+ defaults: { spread: 14, duration: 900 },
74
+ run: (el, o, ctx) => {
75
+ const kids = Array.from(el.children);
76
+ const mid = (kids.length - 1) / 2;
77
+ return all(kids.map((k, i) => ctx.animate(k, [{ transform: 'none' }, { transform: `translateX(${(i - mid) * o.spread}px) rotate(${(i - mid) * (o.spread / 2)}deg)`, offset: 0.5 }, { transform: 'none' }], { duration: o.duration, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' })));
78
+ },
79
+ },
80
+ {
81
+ name: 'topple',
82
+ kind: 'card',
83
+ description: 'Tips backwards on its bottom edge and springs upright.',
84
+ defaults: { duration: 1000 },
85
+ run: (el, o, ctx) => {
86
+ el.style.transformOrigin = 'bottom center';
87
+ return ctx.animate(el, [{ transform: 'perspective(800px) rotateX(0)' }, { transform: 'perspective(800px) rotateX(55deg)', offset: 0.35 }, { transform: 'perspective(800px) rotateX(-12deg)', offset: 0.6 }, { transform: 'perspective(800px) rotateX(5deg)', offset: 0.8 }, { transform: 'perspective(800px) rotateX(0)' }], { duration: o.duration, easing: 'ease-out' });
88
+ },
89
+ },
90
+ {
91
+ name: 'float-tilt',
92
+ kind: 'loop',
93
+ description: 'Idle floating with a slow 3D sway (loop; stops under reduced motion).',
94
+ defaults: { duration: 4200, distance: 8 },
95
+ run: (el, o, ctx) => {
96
+ const a = ctx.animate(el, [{ transform: 'translateY(0) rotateX(0) rotateY(0)' }, { transform: `translateY(-${o.distance}px) rotateX(4deg) rotateY(-5deg)` }, { transform: 'translateY(0) rotateX(0) rotateY(0)' }], { duration: o.duration, iterations: Infinity, easing: 'ease-in-out' });
97
+ return () => a?.cancel();
98
+ },
99
+ },
100
+ ];
101
+ const CLICK_FX = [
102
+ {
103
+ name: 'shockwave',
104
+ kind: 'click',
105
+ description: 'Two expanding rings from the click point.',
106
+ defaults: { color: '#7c5cff', size: 160, duration: 700 },
107
+ run: (el, o, ctx) => {
108
+ if (ctx.reduced)
109
+ return;
110
+ const { x, y } = origin(el, ctx);
111
+ return all([0, 120].map((delay) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:3px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(0)', opacity: 0.9 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' })));
112
+ },
113
+ },
114
+ {
115
+ name: 'ink-splash',
116
+ kind: 'click',
117
+ description: 'Ink blobs splatter from the click point and fade.',
118
+ defaults: { count: 9, colors: PALETTE, duration: 800 },
119
+ run: (el, o, ctx) => {
120
+ if (ctx.reduced)
121
+ return;
122
+ const { x, y } = origin(el, ctx);
123
+ return all(Array.from({ length: o.count }, (_, i) => {
124
+ const s = rand(6, 22);
125
+ const ang = rand(0, Math.PI * 2);
126
+ const d = rand(20, 70);
127
+ return spawn(x, y, `width:${s}px;height:${s}px;margin:${-s / 2}px 0 0 ${-s / 2}px;background:${o.colors[i % o.colors.length]};border-radius:${rand(40, 50)}% ${rand(50, 60)}% ${rand(40, 60)}% ${rand(45, 55)}%`, ctx, [{ transform: 'translate(0,0) scale(.3)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) scale(1)`, opacity: 0.9, offset: 0.5 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d + 8}px) scale(1.1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' });
128
+ }));
129
+ },
130
+ },
131
+ {
132
+ name: 'star-burst',
133
+ kind: 'click',
134
+ description: 'Spinning stars burst outward.',
135
+ defaults: { count: 10, colors: ['#facc15', '#fb923c', '#f472b6'], duration: 900 },
136
+ run: (el, o, ctx) => {
137
+ if (ctx.reduced)
138
+ return;
139
+ const { x, y } = origin(el, ctx);
140
+ return all(Array.from({ length: o.count }, (_, i) => {
141
+ const ang = (i / o.count) * Math.PI * 2;
142
+ const d = rand(40, 80);
143
+ const s = rand(12, 22);
144
+ return spawn(x, y, `font-size:${s}px;line-height:1;margin:${-s / 2}px 0 0 ${-s / 2}px;color:${o.colors[i % o.colors.length]}`, ctx, [{ transform: 'translate(0,0) rotate(0) scale(.2)', opacity: 1 }, { transform: `translate(${Math.cos(ang) * d}px,${Math.sin(ang) * d}px) rotate(${rand(180, 360)}deg) scale(1)`, opacity: 0 }], { duration: o.duration, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' }, '★');
145
+ }));
146
+ },
147
+ },
148
+ {
149
+ name: 'jelly-press',
150
+ kind: 'click',
151
+ description: 'The pressed element squishes like jelly and wobbles back.',
152
+ defaults: { duration: 650 },
153
+ run: (el, o, ctx) => ctx.reduced ? ctx.animate(el, [{ opacity: 1 }, { opacity: 0.7 }, { opacity: 1 }], { duration: 250 }) : ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: 'scale(1.15,.85)', offset: 0.2 }, { transform: 'scale(.9,1.1)', offset: 0.4 }, { transform: 'scale(1.05,.95)', offset: 0.6 }, { transform: 'scale(.98,1.02)', offset: 0.8 }, { transform: 'scale(1,1)' }], { duration: o.duration, easing: 'ease-out' }),
154
+ },
155
+ {
156
+ name: 'ring-ripple',
157
+ kind: 'click',
158
+ description: 'Concentric outlined rings ripple out like water.',
159
+ defaults: { rings: 3, color: '#22d3ee', size: 120, duration: 1000 },
160
+ run: (el, o, ctx) => {
161
+ if (ctx.reduced)
162
+ return;
163
+ const { x, y } = origin(el, ctx);
164
+ return all(Array.from({ length: o.rings }, (_, i) => spawn(x, y, `width:${o.size}px;height:${o.size}px;margin:${-o.size / 2}px 0 0 ${-o.size / 2}px;border:2px solid ${o.color};border-radius:50%`, ctx, [{ transform: 'scale(.1)', opacity: 0.8 }, { transform: 'scale(1)', opacity: 0 }], { duration: o.duration, delay: i * 160, easing: 'ease-out' })));
165
+ },
166
+ },
167
+ {
168
+ name: 'emoji-rain',
169
+ kind: 'click',
170
+ description: 'Emoji pop up and fall around the click (pass `emoji: "🎉✨💜"`).',
171
+ defaults: { emoji: '🎉✨💜🔥', count: 14, duration: 1300 },
172
+ run: (el, o, ctx) => {
173
+ if (ctx.reduced)
174
+ return;
175
+ const { x, y } = origin(el, ctx);
176
+ const chars = Array.from(String(o.emoji));
177
+ return all(Array.from({ length: o.count }, (_, i) => {
178
+ const dx = rand(-90, 90);
179
+ return spawn(x, y, `font-size:${rand(16, 28)}px;line-height:1`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 0 }, { transform: `translate(${dx * 0.6}px,${rand(-90, -50)}px) scale(1)`, opacity: 1, offset: 0.35 }, { transform: `translate(${dx}px,${rand(40, 90)}px) rotate(${rand(-60, 60)}deg) scale(.9)`, opacity: 0 }], { duration: o.duration, delay: i * 25, easing: 'cubic-bezier(0.33, 0, 0.67, 1)' }, chars[i % chars.length]);
180
+ }));
181
+ },
182
+ },
183
+ ];
184
+
185
+ /**
186
+ * Sample a damped spring from 0 → 1 and return the progress values plus the
187
+ * time (ms) it takes to settle. Pure, deterministic.
188
+ */
189
+ function solveSpring({ stiffness = 180, damping = 12, mass = 1, steps = 40 } = {}) {
190
+ const dt = 1 / 120;
191
+ let x = 0;
192
+ let v = 0;
193
+ const trace = [];
194
+ let t = 0;
195
+ for (; t < 4; t += dt) {
196
+ const a = (-stiffness * (x - 1) - damping * v) / mass;
197
+ v += a * dt;
198
+ x += v * dt;
199
+ trace.push(x);
200
+ if (t > 0.1 && Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.01)
201
+ break;
202
+ }
203
+ const values = Array.from({ length: steps + 1 }, (_, i) => +trace[Math.min(trace.length - 1, Math.round((i / steps) * (trace.length - 1)))].toFixed(4));
204
+ values[0] = 0;
205
+ values[steps] = 1;
206
+ return { values, duration: Math.round(trace.length * dt * 1000) };
207
+ }
208
+ /** Keyframes for a property driven by a spring: `map(progress)` → keyframe. */
209
+ function springKeyframes(map, spring) {
210
+ const { values, duration } = solveSpring(spring);
211
+ return { frames: values.map(map), duration };
212
+ }
213
+ /** Height (0 = floor, 1 = drop height) of a ball dropped with restitution `bounce`, sampled `steps` times. */
214
+ function bounceKeyframes(bounce = 0.5, steps = 48) {
215
+ // segment durations scale with sqrt(height); heights scale with bounce^2 per hop
216
+ const hops = [1];
217
+ let h = 1;
218
+ while (hops.length < 6 && h > 0.01) {
219
+ h *= bounce * bounce;
220
+ hops.push(h);
221
+ }
222
+ const segT = hops.map((hh, i) => (i === 0 ? Math.sqrt(hh) : 2 * Math.sqrt(hh)));
223
+ const total = segT.reduce((a, b) => a + b, 0);
224
+ return Array.from({ length: steps + 1 }, (_, i) => {
225
+ let t = (i / steps) * total;
226
+ for (let k = 0; k < hops.length; k++) {
227
+ if (t <= segT[k] || k === hops.length - 1) {
228
+ const tt = Math.min(t, segT[k]);
229
+ if (k === 0)
230
+ return +(1 - (tt / segT[0]) ** 2).toFixed(4);
231
+ const half = segT[k] / 2;
232
+ return +(hops[k] * (1 - ((tt - half) / half) ** 2)).toFixed(4);
233
+ }
234
+ t -= segT[k];
235
+ }
236
+ return 0;
237
+ });
238
+ }
239
+ const fade = (el, ctx) => ctx.animate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
240
+ const PHYSICS_FX = [
241
+ {
242
+ name: 'bounce-in',
243
+ kind: 'enter',
244
+ description: 'Springs in from small with an overshoot (spring solver; stiffness / damping options).',
245
+ defaults: { stiffness: 220, damping: 11, from: 0.3 },
246
+ run: (el, o, ctx) => {
247
+ if (ctx.reduced)
248
+ return fade(el, ctx);
249
+ const { frames, duration } = springKeyframes((p) => ({ transform: `scale(${(o.from + (1 - o.from) * p).toFixed(4)})`, opacity: Math.min(1, p * 3) }), o);
250
+ return ctx.animate(el, frames, { duration, easing: 'linear' });
251
+ },
252
+ },
253
+ {
254
+ name: 'rubber-band',
255
+ kind: 'attention',
256
+ description: 'Stretches wide, snaps back thin and settles like a rubber band.',
257
+ defaults: { duration: 900, amount: 0.25 },
258
+ run: (el, o, ctx) => {
259
+ if (ctx.reduced)
260
+ return;
261
+ const a = o.amount;
262
+ return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${1 + a},${1 - a})`, offset: 0.3 }, { transform: `scale(${1 - a},${1 + a})`, offset: 0.4 }, { transform: `scale(${1 + a / 2},${1 - a / 2})`, offset: 0.5 }, { transform: `scale(${1 - a / 5},${1 + a / 5})`, offset: 0.65 }, { transform: `scale(${1 + a / 10},${1 - a / 10})`, offset: 0.75 }, { transform: 'scale(1,1)' }], { duration: o.duration });
263
+ },
264
+ },
265
+ {
266
+ name: 'elastic-hover',
267
+ kind: 'hover',
268
+ description: 'Hover lifts with a springy overshoot; leaving springs back (persistent; trigger="load").',
269
+ defaults: { lift: 6, scale: 1.04, stiffness: 260, damping: 10 },
270
+ run: (el, o, ctx) => {
271
+ if (ctx.reduced)
272
+ return;
273
+ const to = (on) => {
274
+ const from = on ? 0 : 1;
275
+ const { frames, duration } = springKeyframes((p) => {
276
+ const k = from + (on ? p : -p);
277
+ return { transform: `translateY(${(-o.lift * k).toFixed(2)}px) scale(${(1 + (o.scale - 1) * k).toFixed(4)})` };
278
+ }, o);
279
+ ctx.animate(el, frames, { duration, fill: 'forwards', easing: 'linear' });
280
+ };
281
+ const enter = () => to(true);
282
+ const leave = () => to(false);
283
+ el.addEventListener('pointerenter', enter);
284
+ el.addEventListener('pointerleave', leave);
285
+ return () => {
286
+ el.removeEventListener('pointerenter', enter);
287
+ el.removeEventListener('pointerleave', leave);
288
+ };
289
+ },
290
+ },
291
+ {
292
+ name: 'drop-bounce',
293
+ kind: 'enter',
294
+ description: 'Falls in under gravity and bounces to rest (`height`, `bounce` restitution 0–0.9).',
295
+ defaults: { height: 120, bounce: 0.5, duration: 1100 },
296
+ run: (el, o, ctx) => {
297
+ if (ctx.reduced)
298
+ return fade(el, ctx);
299
+ const hs = bounceKeyframes(Math.min(0.9, Math.max(0, o.bounce)));
300
+ return ctx.animate(el, hs.map((h, i) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)`, opacity: i === 0 ? 0 : 1 })), { duration: o.duration, easing: 'linear' });
301
+ },
302
+ },
303
+ {
304
+ name: 'gravity-text',
305
+ kind: 'text',
306
+ description: 'Each character drops in under gravity with a bounce, staggered (splits the text into aria-hidden spans; the label stays readable).',
307
+ defaults: { height: 60, bounce: 0.45, duration: 900, stagger: 45 },
308
+ run: (el, o, ctx) => {
309
+ if (ctx.reduced)
310
+ return;
311
+ if (!el.dataset.usaSplit) {
312
+ const text = el.textContent || '';
313
+ el.setAttribute('aria-label', text);
314
+ el.textContent = '';
315
+ for (const ch of Array.from(text)) {
316
+ const s = document.createElement('span');
317
+ s.setAttribute('aria-hidden', 'true');
318
+ s.style.display = 'inline-block';
319
+ s.style.whiteSpace = 'pre';
320
+ s.textContent = ch;
321
+ el.appendChild(s);
322
+ }
323
+ el.dataset.usaSplit = '1';
324
+ }
325
+ const hs = bounceKeyframes(o.bounce, 32);
326
+ return all(Array.from(el.children).map((s, i) => ctx.animate(s, hs.map((h) => ({ transform: `translateY(${(-h * o.height).toFixed(1)}px)` })), { duration: o.duration, delay: i * o.stagger, easing: 'linear', fill: 'backwards' })));
327
+ },
328
+ },
329
+ {
330
+ name: 'spring-follow',
331
+ kind: 'cursor',
332
+ description: 'The element springs toward the pointer while it moves over its parent, and back home on leave (persistent).',
333
+ defaults: { stiffness: 0.12, damping: 0.75, range: 40 },
334
+ run: (el, o, ctx) => {
335
+ if (ctx.reduced)
336
+ return;
337
+ const host = el.parentElement || el;
338
+ let tx = 0;
339
+ let ty = 0;
340
+ let x = 0;
341
+ let y = 0;
342
+ let vx = 0;
343
+ let vy = 0;
344
+ let raf = 0;
345
+ const tick = () => {
346
+ vx = (vx + (tx - x) * o.stiffness) * o.damping;
347
+ vy = (vy + (ty - y) * o.stiffness) * o.damping;
348
+ x += vx;
349
+ y += vy;
350
+ el.style.translate = `${x.toFixed(2)}px ${y.toFixed(2)}px`;
351
+ raf = Math.abs(vx) + Math.abs(vy) + Math.abs(tx - x) + Math.abs(ty - y) > 0.05 ? requestAnimationFrame(tick) : 0;
352
+ };
353
+ const kick = () => {
354
+ if (!raf)
355
+ raf = requestAnimationFrame(tick);
356
+ };
357
+ const move = (e) => {
358
+ const r = host.getBoundingClientRect();
359
+ const nx = (e.clientX - r.left) / (r.width || 1) - 0.5;
360
+ const ny = (e.clientY - r.top) / (r.height || 1) - 0.5;
361
+ tx = nx * 2 * o.range;
362
+ ty = ny * 2 * o.range;
363
+ kick();
364
+ };
365
+ const leave = () => {
366
+ tx = ty = 0;
367
+ kick();
368
+ };
369
+ host.addEventListener('pointermove', move);
370
+ host.addEventListener('pointerleave', leave);
371
+ return () => {
372
+ host.removeEventListener('pointermove', move);
373
+ host.removeEventListener('pointerleave', leave);
374
+ cancelAnimationFrame(raf);
375
+ el.style.translate = '';
376
+ };
377
+ },
378
+ },
379
+ {
380
+ name: 'bell-swing',
381
+ kind: 'attention',
382
+ description: 'Swings from its top like a ringing bell with damped oscillation.',
383
+ defaults: { angle: 22, stiffness: 120, damping: 4 },
384
+ run: (el, o, ctx) => {
385
+ if (ctx.reduced)
386
+ return;
387
+ el.style.transformOrigin = 'top center';
388
+ const { frames, duration } = springKeyframes((p) => ({ transform: `rotate(${(o.angle * (1 - p)).toFixed(2)}deg)` }), { stiffness: o.stiffness, damping: o.damping, steps: 48 });
389
+ frames[0] = { transform: 'rotate(0deg)' };
390
+ return ctx.animate(el, frames, { duration: Math.min(duration, 2400), easing: 'linear' });
391
+ },
392
+ },
393
+ ];
394
+
395
+ /** A full-viewport, aria-hidden, pointer-blocking (while covering) transition layer. */
396
+ function screen(css = '') {
397
+ const s = document.createElement('div');
398
+ s.setAttribute('aria-hidden', 'true');
399
+ s.setAttribute('data-usa-page-fx', '');
400
+ s.style.cssText = `position:fixed;inset:0;z-index:2147483001;pointer-events:auto;${css}`;
401
+ document.body.appendChild(s);
402
+ return s;
403
+ }
404
+ const wait = (a) => (a ? a.finished.catch(() => undefined) : Promise.resolve());
405
+ const call = async (fn) => {
406
+ if (typeof fn === 'function')
407
+ await fn();
408
+ };
409
+ /** Reduced-motion version of every transition: a quick cross-fade through the cover colour. */
410
+ async function crossFade(o, ctx) {
411
+ const s = screen(`background:${o.color};opacity:0`);
412
+ await wait(ctx.animate(s, [{ opacity: 0 }, { opacity: 1 }], { duration: 150, fill: 'forwards' }));
413
+ await call(o.onCovered);
414
+ await wait(ctx.animate(s, [{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' }));
415
+ s.remove();
416
+ }
417
+ /** Build a transition from a cover layer and cover / reveal animations of its parts. */
418
+ function transition(build) {
419
+ return async (el, o, ctx) => {
420
+ if (ctx.reduced)
421
+ return crossFade(o, ctx);
422
+ const s = screen('pointer-events:auto');
423
+ const { parts, cover, delay } = build(s, o, ctx, el);
424
+ const frames = (i) => (typeof cover === 'function' ? cover(i) : cover);
425
+ const half = o.duration / 2;
426
+ await Promise.all(parts.map((p, i) => wait(ctx.animate(p, frames(i), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
427
+ await call(o.onCovered);
428
+ if (o.hold)
429
+ await new Promise((r) => setTimeout(r, o.hold));
430
+ await Promise.all(parts.map((p, i) => wait(ctx.animate(p, [...frames(i)].reverse(), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
431
+ s.remove();
432
+ };
433
+ }
434
+ const part = (s, css) => {
435
+ const p = document.createElement('div');
436
+ p.style.cssText = `position:absolute;${css}`;
437
+ s.appendChild(p);
438
+ return p;
439
+ };
440
+ const TRANSITION_DEFAULTS = { color: '#0f0f1a', duration: 1000, hold: 0, onCovered: undefined };
441
+ const PAGE_FX = [
442
+ {
443
+ name: 'curtain',
444
+ kind: 'page',
445
+ description: 'Two curtain panels close from the sides, call onCovered(), then open.',
446
+ reduced: 'run',
447
+ defaults: TRANSITION_DEFAULTS,
448
+ run: transition((s, o) => ({
449
+ parts: [part(s, `top:0;bottom:0;left:0;width:50.5%;background:${o.color}`), part(s, `top:0;bottom:0;right:0;width:50.5%;background:${o.color}`)],
450
+ cover: (i) => [{ transform: `translateX(${i ? 100 : -100}%)` }, { transform: 'translateX(0)' }],
451
+ })),
452
+ },
453
+ {
454
+ name: 'iris',
455
+ kind: 'page',
456
+ description: 'A circle closes on the click point (or the element’s center), calls onCovered(), then opens.',
457
+ reduced: 'run',
458
+ defaults: TRANSITION_DEFAULTS,
459
+ run: transition((s, o, ctx, el) => {
460
+ const { x, y } = origin(el, ctx);
461
+ const r = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + 2;
462
+ // a huge box-shadow ring around a hole: the hole shrinks to 0 to cover
463
+ const p = part(s, `left:${x}px;top:${y}px;width:0;height:0;border-radius:50%;box-shadow:0 0 0 ${Math.ceil(r)}px ${o.color}`);
464
+ s.style.background = 'transparent';
465
+ return { parts: [p], cover: [{ width: `${r * 2}px`, height: `${r * 2}px`, margin: `${-r}px 0 0 ${-r}px` }, { width: '0px', height: '0px', margin: '0px 0 0 0px' }] };
466
+ }),
467
+ },
468
+ {
469
+ name: 'pixel-dissolve',
470
+ kind: 'page',
471
+ description: 'The screen fills with pixels in random order, calls onCovered(), then dissolves (`cols` × `rows`).',
472
+ reduced: 'run',
473
+ defaults: { ...TRANSITION_DEFAULTS, cols: 16, rows: 10 },
474
+ run: transition((s, o) => {
475
+ const n = o.cols * o.rows;
476
+ const order = Array.from({ length: n }, (_, i) => i).sort(() => Math.random() - 0.5);
477
+ const parts = Array.from({ length: n }, (_, i) => part(s, `left:${((i % o.cols) * 100) / o.cols}%;top:${(Math.floor(i / o.cols) * 100) / o.rows}%;width:${100 / o.cols + 0.2}%;height:${100 / o.rows + 0.2}%;background:${o.color};opacity:0`));
478
+ return { parts, cover: [{ opacity: 0 }, { opacity: 1 }], delay: (i) => (order[i] / n) * (o.duration / 2) * 0.6 };
479
+ }),
480
+ },
481
+ {
482
+ name: 'blinds',
483
+ kind: 'page',
484
+ description: 'Horizontal slats rotate shut like venetian blinds, call onCovered(), then open (`slats`).',
485
+ reduced: 'run',
486
+ defaults: { ...TRANSITION_DEFAULTS, slats: 8 },
487
+ run: transition((s, o) => ({
488
+ parts: Array.from({ length: o.slats }, (_, i) => part(s, `left:0;right:0;top:${(i * 100) / o.slats}%;height:${100 / o.slats + 0.3}%;background:${o.color};transform-origin:top`)),
489
+ cover: [{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }],
490
+ delay: (i) => i * 30,
491
+ })),
492
+ },
493
+ {
494
+ name: 'velocity-skew',
495
+ kind: 'scroll',
496
+ description: 'Skews the element with scroll velocity and eases back when scrolling stops (persistent).',
497
+ defaults: { max: 8, factor: 0.25 },
498
+ run: (el, o, ctx) => {
499
+ if (ctx.reduced)
500
+ return;
501
+ let last = scrollY;
502
+ let skew = 0;
503
+ let raf = 0;
504
+ const tick = () => {
505
+ const y = scrollY;
506
+ const target = Math.max(-o.max, Math.min(o.max, (y - last) * o.factor));
507
+ last = y;
508
+ skew += (target - skew) * 0.2;
509
+ el.style.transform = `skewY(${skew.toFixed(3)}deg)`;
510
+ raf = Math.abs(skew) > 0.01 || Math.abs(target) > 0.01 ? requestAnimationFrame(tick) : 0;
511
+ if (!raf)
512
+ el.style.transform = '';
513
+ };
514
+ const onScroll = () => {
515
+ if (!raf)
516
+ raf = requestAnimationFrame(tick);
517
+ };
518
+ addEventListener('scroll', onScroll, { passive: true });
519
+ return () => {
520
+ removeEventListener('scroll', onScroll);
521
+ cancelAnimationFrame(raf);
522
+ el.style.transform = '';
523
+ };
524
+ },
525
+ },
526
+ {
527
+ name: 'spotlight',
528
+ kind: 'cursor',
529
+ description: 'Dims the page except a soft circle that follows the pointer (persistent; `radius`, `dim`).',
530
+ defaults: { radius: 180, dim: 0.72 },
531
+ run: (_el, o, ctx) => {
532
+ if (ctx.reduced)
533
+ return;
534
+ const s = screen(`pointer-events:none;background:radial-gradient(circle ${o.radius}px at var(--x,50%) var(--y,50%),transparent 0,transparent 60%,rgba(0,0,0,${o.dim}) 100%)`);
535
+ const move = (e) => {
536
+ s.style.setProperty('--x', `${e.clientX}px`);
537
+ s.style.setProperty('--y', `${e.clientY}px`);
538
+ };
539
+ addEventListener('pointermove', move, { passive: true });
540
+ return () => {
541
+ removeEventListener('pointermove', move);
542
+ s.remove();
543
+ };
544
+ },
545
+ },
546
+ {
547
+ name: 'edge-glow',
548
+ kind: 'scroll',
549
+ description: 'A soft glow lights the top / bottom edge of the viewport while scrolling in that direction (persistent).',
550
+ defaults: { color: '#7c5cff', size: 90 },
551
+ run: (_el, o, ctx) => {
552
+ if (ctx.reduced)
553
+ return;
554
+ const s = screen('pointer-events:none;opacity:0;transition:opacity .35s ease-out');
555
+ let last = scrollY;
556
+ let t = 0;
557
+ const onScroll = () => {
558
+ const down = scrollY >= last;
559
+ last = scrollY;
560
+ s.style.background = `linear-gradient(${down ? 'to top' : 'to bottom'},${o.color}55,transparent ${o.size}px)`;
561
+ s.style.opacity = '1';
562
+ clearTimeout(t);
563
+ t = setTimeout(() => (s.style.opacity = '0'), 160);
564
+ };
565
+ addEventListener('scroll', onScroll, { passive: true });
566
+ return () => {
567
+ removeEventListener('scroll', onScroll);
568
+ clearTimeout(t);
569
+ s.remove();
570
+ };
571
+ },
572
+ },
573
+ ];
574
+
575
+ /**
576
+ * 5.4 — `<usa-story template="…">` scroll-storytelling templates.
577
+ *
578
+ * - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the
579
+ * step in view gets `data-active`, the stage gets `data-active-step="<index>"`.
580
+ * - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
581
+ * - `zoom` — the `[data-stage]` zooms toward the viewer (`zoom="6"`) and fades.
582
+ * - `compare` — before / after (`[data-before]`, `[data-after]`) wipe driven by
583
+ * scroll, plus a draggable, keyboard-accessible handle (`role="slider"`).
584
+ * - `counter` — `[data-count="1234"]` numbers count up when they enter view.
585
+ * - `highlight` — paragraphs dim except the one crossing the viewport center.
586
+ *
587
+ * Every template sets `--usa-story-progress` (0–1) on the host and dispatches
588
+ * `usa-story-step` (`detail: { index }`). Reduced motion: no sliding / zooming
589
+ * (the gallery stacks vertically), counters show final values, the rest is
590
+ * class changes only.
591
+ */
592
+ const STORY_TEMPLATES = ['pin', 'gallery', 'zoom', 'compare', 'counter', 'highlight'];
593
+ const CSS = `usa-story{display:block;position:relative}
594
+ usa-story[template=pin] [data-stage],usa-story[template=gallery] [data-sticky],usa-story[template=zoom] [data-stage],usa-story[template=compare] [data-sticky]{position:sticky;top:0}
595
+ usa-story[template=pin] [data-stage]{align-self:start}
596
+ usa-story[template=pin] [data-step]{min-height:80vh;opacity:.35;transition:opacity .3s}
597
+ usa-story[template=pin] [data-step][data-active]{opacity:1}
598
+ usa-story[template=gallery] [data-sticky]{height:100vh;overflow:hidden;display:flex;align-items:center}
599
+ usa-story[template=gallery] [data-track]{display:flex;gap:24px;will-change:transform}
600
+ usa-story[template=gallery][data-static] [data-sticky]{position:static;height:auto;overflow:visible}
601
+ usa-story[template=gallery][data-static] [data-track]{flex-direction:column;transform:none!important}
602
+ usa-story[template=zoom] [data-stage]{height:100vh;overflow:hidden;display:grid;place-items:center}
603
+ usa-story[template=compare] [data-sticky]{height:var(--usa-story-h,70vh);overflow:hidden}
604
+ usa-story[template=compare] [data-before],usa-story[template=compare] [data-after]{position:absolute;inset:0}
605
+ usa-story[template=compare] [data-after]{clip-path:inset(0 0 0 var(--usa-split,50%))}
606
+ usa-story [data-handle]{position:absolute;top:0;bottom:0;left:var(--usa-split,50%);width:4px;margin-left:-2px;background:#fff;box-shadow:0 0 0 1px #0003;cursor:ew-resize;touch-action:none}
607
+ usa-story [data-handle]:focus-visible{outline:3px solid #7c5cff;outline-offset:2px}
608
+ usa-story[template=highlight] p,usa-story[template=highlight] [data-step]{opacity:.3;transition:opacity .25s}
609
+ usa-story[template=highlight] [data-active]{opacity:1}`;
610
+ const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
611
+ /** Progress of `el` through the viewport: 0 when its top hits the viewport top, 1 when its bottom hits the viewport bottom. */
612
+ function storyProgress(el, vh = typeof innerHeight === 'number' ? innerHeight : 800) {
613
+ const r = el.getBoundingClientRect();
614
+ const span = r.height - vh;
615
+ return span > 0 ? clamp(-r.top / span) : clamp((vh - r.top) / (vh + r.height));
616
+ }
617
+ /** Format a counted value like the target (`1,234`, `12.5`, prefix / suffix kept). */
618
+ function formatCount(target, t) {
619
+ const m = target.match(/^(\D*)([\d,]*\.?\d+)(.*)$/);
620
+ if (!m)
621
+ return target;
622
+ const [, pre, num, post] = m;
623
+ const n = parseFloat(num.replace(/,/g, ''));
624
+ const dec = (num.split('.')[1] || '').length;
625
+ let s = (n * t).toFixed(dec);
626
+ if (num.includes(','))
627
+ s = Number(s).toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
628
+ return pre + s + post;
629
+ }
630
+ function defineStory(tag = 'usa-story') {
631
+ return defineElement(tag, (Base) => class UsaStory extends Base {
632
+ constructor() {
633
+ super(...arguments);
634
+ this.progress = 0;
635
+ this.step = -1;
636
+ this.frame = 0;
637
+ }
638
+ static get observedAttributes() {
639
+ return ['template', 'zoom'];
640
+ }
641
+ get template() {
642
+ const t = this.str('template', 'pin');
643
+ return STORY_TEMPLATES.includes(t) ? t : 'pin';
644
+ }
645
+ steps() {
646
+ const own = Array.from(this.querySelectorAll('[data-step]:not([data-stage])'));
647
+ return own.length || this.template !== 'highlight' ? own : Array.from(this.querySelectorAll('p'));
648
+ }
649
+ setStep(i) {
650
+ if (i === this.step)
651
+ return;
652
+ this.step = i;
653
+ this.steps().forEach((s, k) => s.toggleAttribute('data-active', k === i));
654
+ const stage = this.querySelector('[data-stage]');
655
+ if (stage)
656
+ stage.dataset.activeStep = String(i);
657
+ this.dispatchEvent(new CustomEvent('usa-story-step', { detail: { index: i }, bubbles: true }));
658
+ }
659
+ /** Recompute from the current scroll position (called on scroll / resize). */
660
+ update() {
661
+ this.frame = 0;
662
+ const p = (this.progress = storyProgress(this));
663
+ this.style.setProperty('--usa-story-progress', p.toFixed(4));
664
+ const t = this.template;
665
+ const vh = innerHeight || 800;
666
+ if (t === 'pin' || t === 'highlight') {
667
+ const mid = vh / 2;
668
+ let best = -1;
669
+ let bestD = Infinity;
670
+ this.steps().forEach((s, i) => {
671
+ const r = s.getBoundingClientRect();
672
+ const d = r.top <= mid && r.bottom >= mid ? 0 : Math.min(Math.abs(r.top - mid), Math.abs(r.bottom - mid));
673
+ if (d < bestD)
674
+ (bestD = d), (best = i);
675
+ });
676
+ this.setStep(best);
677
+ }
678
+ else if (t === 'gallery' && !this.reduced) {
679
+ const track = this.querySelector('[data-track]');
680
+ const box = this.querySelector('[data-sticky]');
681
+ if (track && box)
682
+ track.style.transform = `translateX(${(-p * Math.max(0, track.scrollWidth - box.clientWidth)).toFixed(1)}px)`;
683
+ }
684
+ else if (t === 'zoom' && !this.reduced) {
685
+ const inner = this.querySelector('[data-stage] > *');
686
+ if (inner) {
687
+ inner.style.transform = `scale(${(1 + p * (this.num('zoom', 6) - 1)).toFixed(3)})`;
688
+ inner.style.opacity = String(clamp(1.4 - p * 1.4).toFixed(3));
689
+ }
690
+ }
691
+ else if (t === 'compare' && !this.hasAttribute('data-dragged')) {
692
+ this.setSplit(p * 100);
693
+ }
694
+ else if (t === 'counter') {
695
+ this.querySelectorAll('[data-count]:not([data-counted])').forEach((el) => {
696
+ const r = el.getBoundingClientRect();
697
+ if (r.top < vh * 0.9 && r.bottom > 0)
698
+ this.count(el);
699
+ });
700
+ }
701
+ }
702
+ setSplit(pct) {
703
+ const v = clamp(pct, 0, 100);
704
+ this.style.setProperty('--usa-split', `${v.toFixed(2)}%`);
705
+ const h = this.querySelector('[data-handle]');
706
+ if (h)
707
+ h.setAttribute('aria-valuenow', String(Math.round(v)));
708
+ }
709
+ count(el) {
710
+ el.dataset.counted = '';
711
+ const target = el.dataset.count || el.textContent || '0';
712
+ el.setAttribute('aria-label', target);
713
+ if (this.reduced) {
714
+ el.textContent = target;
715
+ return;
716
+ }
717
+ const dur = Number(el.dataset.duration) || 1600;
718
+ const t0 = performance.now();
719
+ const tick = (now) => {
720
+ const k = clamp((now - t0) / dur);
721
+ el.textContent = formatCount(target, 1 - (1 - k) ** 3);
722
+ if (k < 1)
723
+ requestAnimationFrame(tick);
724
+ };
725
+ requestAnimationFrame(tick);
726
+ }
727
+ mount() {
728
+ this.toggleAttribute('data-static', this.reduced);
729
+ if (this.template === 'compare')
730
+ this.mountCompare();
731
+ const kick = () => {
732
+ if (!this.frame)
733
+ this.frame = requestAnimationFrame(() => this.update());
734
+ };
735
+ this.listen(window, 'scroll', kick, { passive: true });
736
+ this.listen(window, 'resize', kick);
737
+ this.onCleanup(() => cancelAnimationFrame(this.frame));
738
+ this.update();
739
+ }
740
+ mountCompare() {
741
+ const box = this.querySelector('[data-sticky]') || this;
742
+ let h = this.querySelector('[data-handle]');
743
+ if (!h) {
744
+ h = document.createElement('div');
745
+ h.setAttribute('data-handle', '');
746
+ box.appendChild(h);
747
+ this.onCleanup(() => h.remove());
748
+ }
749
+ h.tabIndex = 0;
750
+ h.setAttribute('role', 'slider');
751
+ h.setAttribute('aria-label', this.str('label', 'Before / after'));
752
+ h.setAttribute('aria-valuemin', '0');
753
+ h.setAttribute('aria-valuemax', '100');
754
+ const at = (e) => {
755
+ const r = box.getBoundingClientRect();
756
+ this.dataset.dragged = '';
757
+ this.setSplit(((e.clientX - r.left) / (r.width || 1)) * 100);
758
+ };
759
+ let drag = false;
760
+ this.listen(h, 'pointerdown', (e) => {
761
+ drag = true;
762
+ h.setPointerCapture?.(e.pointerId);
763
+ });
764
+ this.listen(h, 'pointermove', (e) => drag && at(e));
765
+ this.listen(h, 'pointerup', () => (drag = false));
766
+ this.listen(h, 'keydown', (e) => {
767
+ const cur = parseFloat(h.getAttribute('aria-valuenow') || '50');
768
+ const step = e.shiftKey ? 10 : 2;
769
+ const next = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? cur - step : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? cur + step : e.key === 'Home' ? 0 : e.key === 'End' ? 100 : null;
770
+ if (next == null)
771
+ return;
772
+ e.preventDefault();
773
+ this.dataset.dragged = '';
774
+ this.setSplit(next);
775
+ });
776
+ this.setSplit(50);
777
+ }
778
+ unmount() {
779
+ this.step = -1;
780
+ delete this.dataset.dragged;
781
+ }
782
+ }, { id: 'usa-story', text: CSS });
783
+ }
784
+
785
+ let actx = null;
786
+ let current = null;
787
+ const mediaSources = new WeakMap();
788
+ /** The running analyser, if `enableAudio()` was called. */
789
+ const getAudio = () => current;
790
+ function audioContext() {
791
+ const AC = globalThis.AudioContext || globalThis.webkitAudioContext;
792
+ if (!AC)
793
+ throw new Error('[motionary] Web Audio is not available');
794
+ if (!actx || actx.state === 'closed')
795
+ actx = new AC();
796
+ return actx;
797
+ }
798
+ /**
799
+ * Start analysing `input` and make it the source of every sound-reactive
800
+ * effect. Call from a user gesture. Replaces a previous source.
801
+ */
802
+ async function enableAudio(input = 'mic', opts = {}) {
803
+ current?.stop();
804
+ const ac = audioContext();
805
+ if (ac.state === 'suspended')
806
+ await ac.resume().catch(() => undefined);
807
+ const analyser = ac.createAnalyser();
808
+ analyser.fftSize = opts.fftSize || 512;
809
+ analyser.smoothingTimeConstant = opts.smoothing ?? 0.8;
810
+ let src;
811
+ let release = () => { };
812
+ const media = typeof input === 'string' && input !== 'mic' ? document.querySelector(input) : input;
813
+ if (media === 'mic') {
814
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
815
+ src = ac.createMediaStreamSource(stream);
816
+ src.connect(analyser); // never to the speakers: no feedback
817
+ release = () => stream.getTracks().forEach((t) => t.stop());
818
+ }
819
+ else if (media && typeof media.play === 'function') {
820
+ const m = media;
821
+ let node = mediaSources.get(m);
822
+ if (!node)
823
+ mediaSources.set(m, (node = ac.createMediaElementSource(m)));
824
+ try {
825
+ node.disconnect();
826
+ }
827
+ catch {
828
+ /* not connected */
829
+ }
830
+ src = node;
831
+ src.connect(analyser);
832
+ analyser.connect(ac.destination);
833
+ release = () => {
834
+ try {
835
+ node.disconnect();
836
+ }
837
+ catch {
838
+ /* already */
839
+ }
840
+ node.connect(ac.destination); // keep the media audible
841
+ };
842
+ if (m.paused)
843
+ await m.play()?.catch?.(() => undefined);
844
+ }
845
+ else if (media && typeof media.getTracks === 'function') {
846
+ src = ac.createMediaStreamSource(media);
847
+ src.connect(analyser);
848
+ }
849
+ else
850
+ throw new Error(`[motionary] enableAudio: no audio source for ${String(input)}`);
851
+ const freq = new Uint8Array(analyser.frequencyBinCount);
852
+ const wave = new Uint8Array(analyser.fftSize);
853
+ const self = {
854
+ context: ac,
855
+ analyser,
856
+ sample: () => {
857
+ analyser.getByteFrequencyData(freq);
858
+ analyser.getByteTimeDomainData(wave);
859
+ let sq = 0;
860
+ for (let i = 0; i < wave.length; i++)
861
+ sq += ((wave[i] - 128) / 128) ** 2;
862
+ const n = Math.max(1, Math.round(freq.length * 0.08));
863
+ let b = 0;
864
+ for (let i = 0; i < n; i++)
865
+ b += freq[i];
866
+ return { level: Math.min(1, Math.sqrt(sq / wave.length) * 2), bass: b / n / 255, freq, wave };
867
+ },
868
+ stop: () => {
869
+ if (current === self) {
870
+ current = null;
871
+ if (raf && typeof cancelAnimationFrame === 'function')
872
+ cancelAnimationFrame(raf);
873
+ raf = 0;
874
+ }
875
+ try {
876
+ src.disconnect();
877
+ analyser.disconnect();
878
+ }
879
+ catch {
880
+ /* already */
881
+ }
882
+ release();
883
+ setVars(0, 0);
884
+ },
885
+ };
886
+ current = self;
887
+ kick();
888
+ return self;
889
+ }
890
+ /** Stop the current audio source (if any). */
891
+ function disableAudio() {
892
+ current?.stop();
893
+ }
894
+ /** A pure beat detector: feed it energy (0–1) and a timestamp per frame; it answers "beat?". */
895
+ function createBeatDetector(o = {}) {
896
+ const { threshold = 1.35, cooldown = 250, history = 43, floor = 0.08 } = o;
897
+ const hist = [];
898
+ let last = -Infinity;
899
+ return (energy, now) => {
900
+ const avg = hist.length ? hist.reduce((a, b) => a + b, 0) / hist.length : energy;
901
+ const ready = hist.length >= 8;
902
+ hist.push(energy);
903
+ if (hist.length > history)
904
+ hist.shift();
905
+ if (ready && energy > floor && energy > avg * threshold && now - last >= cooldown) {
906
+ last = now;
907
+ return true;
908
+ }
909
+ return false;
910
+ };
911
+ }
912
+ const listeners = new Set();
913
+ let raf = 0;
914
+ function setVars(level, bass) {
915
+ if (typeof document === 'undefined')
916
+ return;
917
+ const s = document.documentElement.style;
918
+ s.setProperty('--usa-audio-level', level.toFixed(3));
919
+ s.setProperty('--usa-audio-bass', bass.toFixed(3));
920
+ }
921
+ function loop(now) {
922
+ raf = 0;
923
+ if (!current)
924
+ return;
925
+ const { level, bass } = current.sample();
926
+ const reduced = prefersReducedMotion();
927
+ setVars(reduced ? 0 : level, reduced ? 0 : bass);
928
+ for (const l of Array.from(listeners))
929
+ if (l.detect(bass, now))
930
+ l.cb({ energy: bass, time: now });
931
+ kick();
932
+ }
933
+ function kick() {
934
+ if (!raf && current && typeof requestAnimationFrame === 'function')
935
+ raf = requestAnimationFrame(loop);
936
+ }
937
+ /** Call `cb` on every detected beat of the current audio source. Returns an unsubscribe. */
938
+ function onBeat(cb, o = {}) {
939
+ const l = { detect: createBeatDetector(o), cb };
940
+ listeners.add(l);
941
+ kick();
942
+ return () => void listeners.delete(l);
943
+ }
944
+ /** Play the registered effect `name` on `el` at every beat (not under reduced motion). Returns an unbind. */
945
+ function bindBeat(el, name, options = {}) {
946
+ const { threshold, cooldown, history, floor, ...fx } = options;
947
+ return onBeat(() => {
948
+ if (!prefersReducedMotion())
949
+ playEffect(el, name, fx).catch(() => undefined);
950
+ }, { threshold, cooldown, history, floor });
951
+ }
952
+ // --- visual effects ----------------------------------------------------------
953
+ /** The current sample, or a gentle synthetic one while no audio is enabled. */
954
+ function read(t, bins) {
955
+ if (current)
956
+ return { ...current.sample(), idle: false };
957
+ const freq = new Uint8Array(bins);
958
+ const wave = new Uint8Array(bins * 2);
959
+ for (let i = 0; i < bins; i++)
960
+ freq[i] = 70 + 50 * Math.sin(t * 2 + i * 0.35) * Math.cos(t * 0.7 + i * 0.11) * (1 - i / bins);
961
+ for (let i = 0; i < wave.length; i++)
962
+ wave[i] = 128 + 22 * Math.sin(t * 3 + i * 0.12);
963
+ return { level: 0.25 + 0.1 * Math.sin(t * 2), bass: 0.35 + 0.2 * Math.sin(t * 2.4), freq, wave, idle: true };
964
+ }
965
+ const audioFx = (name, description, defaults, spec) => ({
966
+ name,
967
+ kind: 'background',
968
+ description,
969
+ reduced: 'skip',
970
+ defaults: { colors: PALETTE, background: '#0b0d12', speed: 1, quality: 1, ...defaults },
971
+ run: (el, o, ctx) => canvasBackground(el, ctx, spec, o),
972
+ });
973
+ const AUDIO_FX = [
974
+ audioFx('spectrum-bars', 'Frequency bars dance to the audio (mirror them with `mirror: true`).', { bars: 48, gap: 2, mirror: false }, {
975
+ draw: ({ ctx, w, h, t, o }) => {
976
+ ctx.fillStyle = o.background;
977
+ ctx.fillRect(0, 0, w, h);
978
+ const s = read(t, o.bars);
979
+ const per = Math.max(1, Math.floor((s.freq.length * 0.7) / o.bars));
980
+ const bw = w / o.bars;
981
+ for (let i = 0; i < o.bars; i++) {
982
+ let v = 0;
983
+ for (let k = 0; k < per; k++)
984
+ v += s.freq[Math.min(s.freq.length - 1, i * per + k)];
985
+ const bh = (v / per / 255) * h * (o.mirror ? 0.5 : 0.9);
986
+ ctx.fillStyle = o.colors[i % o.colors.length];
987
+ ctx.fillRect(i * bw + o.gap / 2, o.mirror ? h / 2 - bh : h - bh, Math.max(1, bw - o.gap), o.mirror ? bh * 2 : bh);
988
+ }
989
+ },
990
+ }),
991
+ audioFx('pulse-ring', 'Glowing rings pulse with the bass.', { rings: 3, color: '#7c5cff' }, {
992
+ draw: ({ ctx, w, h, t, o }) => {
993
+ ctx.fillStyle = o.background;
994
+ ctx.fillRect(0, 0, w, h);
995
+ const s = read(t, 32);
996
+ const [r, g, b] = hexRgb(o.color);
997
+ const base = Math.min(w, h) * 0.18;
998
+ for (let i = 0; i < o.rings; i++) {
999
+ const rad = base * (1 + i * 0.55) * (1 + s.bass * 0.6);
1000
+ ctx.strokeStyle = `rgba(${r},${g},${b},${(0.9 - i * 0.25) * (0.4 + s.level)})`;
1001
+ ctx.lineWidth = 3 + s.bass * 10 - i;
1002
+ ctx.beginPath();
1003
+ ctx.arc(w / 2, h / 2, rad, 0, Math.PI * 2);
1004
+ ctx.stroke();
1005
+ }
1006
+ },
1007
+ }),
1008
+ audioFx('wave-ring', 'The live waveform wrapped into a circle.', { color: '#22d3ee', amplitude: 0.35 }, {
1009
+ draw: ({ ctx, w, h, t, o }) => {
1010
+ ctx.fillStyle = o.background;
1011
+ ctx.fillRect(0, 0, w, h);
1012
+ const s = read(t, 64);
1013
+ const n = s.wave.length;
1014
+ const base = Math.min(w, h) * 0.28;
1015
+ ctx.strokeStyle = o.color;
1016
+ ctx.lineWidth = 2;
1017
+ ctx.beginPath();
1018
+ for (let i = 0; i <= n; i++) {
1019
+ const a = (i / n) * Math.PI * 2;
1020
+ const rad = base * (1 + ((s.wave[i % n] - 128) / 128) * o.amplitude * 2);
1021
+ const x = w / 2 + Math.cos(a) * rad;
1022
+ const y = h / 2 + Math.sin(a) * rad;
1023
+ if (i)
1024
+ ctx.lineTo(x, y);
1025
+ else
1026
+ ctx.moveTo(x, y);
1027
+ }
1028
+ ctx.closePath();
1029
+ ctx.stroke();
1030
+ },
1031
+ }),
1032
+ ];
1033
+ /**
1034
+ * `<usa-audio source="#track | mic" label="…">` — a toggle button (yours, as
1035
+ * `[data-audio-toggle]`, or one it renders) that enables the audio source on
1036
+ * click; children with `data-usa-beat="effect"` play that effect on every
1037
+ * beat (`data-usa-beat-options` JSON; `threshold` / `cooldown` attributes).
1038
+ * Emits `usa-beat` and `usa-audio-error`.
1039
+ */
1040
+ function defineAudio(tag = 'usa-audio') {
1041
+ return defineElement(tag, (Base) => class UsaAudio extends Base {
1042
+ constructor() {
1043
+ super(...arguments);
1044
+ this.active = false;
1045
+ this.audio = null;
1046
+ this.off = null;
1047
+ }
1048
+ static get observedAttributes() {
1049
+ return ['source'];
1050
+ }
1051
+ button() {
1052
+ let b = this.querySelector('[data-audio-toggle]');
1053
+ if (!b) {
1054
+ b = document.createElement('button');
1055
+ b.type = 'button';
1056
+ b.setAttribute('data-audio-toggle', '');
1057
+ b.textContent = this.str('label', '🔊 Enable sound-reactive effects');
1058
+ this.prepend(b);
1059
+ this.onCleanup(() => b.remove());
1060
+ }
1061
+ return b;
1062
+ }
1063
+ async toggle() {
1064
+ const b = this.button();
1065
+ if (this.active)
1066
+ return this.stop();
1067
+ try {
1068
+ this.audio = await enableAudio(this.str('source', 'mic'));
1069
+ this.active = true;
1070
+ this.removeAttribute('data-audio-error');
1071
+ b.setAttribute('aria-pressed', 'true');
1072
+ this.off = onBeat((d) => {
1073
+ this.dispatchEvent(new CustomEvent('usa-beat', { detail: d, bubbles: true }));
1074
+ if (prefersReducedMotion())
1075
+ return;
1076
+ this.querySelectorAll('[data-usa-beat]').forEach((el) => {
1077
+ let opts = {};
1078
+ try {
1079
+ opts = JSON.parse(el.dataset.usaBeatOptions || '{}') || {};
1080
+ }
1081
+ catch {
1082
+ /* ignore bad JSON */
1083
+ }
1084
+ playEffect(el, el.dataset.usaBeat || 'pulse', opts).catch(() => undefined);
1085
+ });
1086
+ }, { threshold: this.num('threshold', 1.35), cooldown: this.num('cooldown', 250) });
1087
+ }
1088
+ catch (err) {
1089
+ this.setAttribute('data-audio-error', '');
1090
+ this.dispatchEvent(new CustomEvent('usa-audio-error', { detail: { error: err }, bubbles: true }));
1091
+ }
1092
+ }
1093
+ stop() {
1094
+ this.off?.();
1095
+ this.off = null;
1096
+ if (this.audio && current === this.audio)
1097
+ this.audio.stop();
1098
+ this.audio = null;
1099
+ this.active = false;
1100
+ this.querySelector('[data-audio-toggle]')?.setAttribute('aria-pressed', 'false');
1101
+ }
1102
+ mount() {
1103
+ const b = this.button();
1104
+ b.setAttribute('aria-pressed', 'false');
1105
+ this.listen(b, 'click', () => void this.toggle());
1106
+ this.onCleanup(() => this.stop());
1107
+ }
1108
+ }, { id: 'usa-audio', text: 'usa-audio{display:block}usa-audio[data-audio-error] [data-audio-toggle]{outline:2px solid #ff5c8a}' });
1109
+ }
1110
+
1111
+ const touchOk = (e, o) => o.touch || e.pointerType !== 'touch';
1112
+ /** A fixed, viewport-sized canvas in the fx layer, redrawn while `draw` returns true. */
1113
+ function overlayCanvas(draw) {
1114
+ const c = document.createElement('canvas');
1115
+ c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%';
1116
+ fxLayer().appendChild(c);
1117
+ const g = c.getContext('2d');
1118
+ let raf = 0;
1119
+ const frame = (now) => {
1120
+ raf = 0;
1121
+ if (!g)
1122
+ return;
1123
+ const w = innerWidth;
1124
+ const h = innerHeight;
1125
+ const dpr = Math.min(2, devicePixelRatio || 1);
1126
+ if (c.width !== Math.round(w * dpr) || c.height !== Math.round(h * dpr)) {
1127
+ c.width = Math.round(w * dpr);
1128
+ c.height = Math.round(h * dpr);
1129
+ }
1130
+ g.setTransform(dpr, 0, 0, dpr, 0, 0);
1131
+ g.clearRect(0, 0, w, h);
1132
+ if (draw(g, w, h, now))
1133
+ raf = requestAnimationFrame(frame);
1134
+ };
1135
+ return {
1136
+ kick: () => {
1137
+ if (!raf && g)
1138
+ raf = requestAnimationFrame(frame);
1139
+ },
1140
+ stop: () => {
1141
+ cancelAnimationFrame(raf);
1142
+ raf = 0;
1143
+ c.remove();
1144
+ },
1145
+ };
1146
+ }
1147
+ /** Pointer trail on `el`: keeps the last `life` ms of points and redraws them. */
1148
+ function trail(el, o, paint) {
1149
+ const pts = [];
1150
+ const oc = overlayCanvas((g, _w, _h, now) => {
1151
+ while (pts.length && now - pts[0].t > o.life)
1152
+ pts.shift();
1153
+ if (pts.length > 1)
1154
+ paint(g, pts, now);
1155
+ return pts.length > 0;
1156
+ });
1157
+ const move = (e) => {
1158
+ if (!touchOk(e, o))
1159
+ return;
1160
+ pts.push({ x: e.clientX, y: e.clientY, t: performance.now() });
1161
+ if (pts.length > 64)
1162
+ pts.shift();
1163
+ oc.kick();
1164
+ };
1165
+ el.addEventListener('pointermove', move, { passive: true });
1166
+ return () => {
1167
+ el.removeEventListener('pointermove', move);
1168
+ oc.stop();
1169
+ };
1170
+ }
1171
+ const CURSOR_FX = [
1172
+ {
1173
+ name: 'comet-trail',
1174
+ kind: 'cursor',
1175
+ description: 'A glowing comet tail follows the pointer over the element (persistent; `color`, `width`, `life`).',
1176
+ defaults: { color: '#7c5cff', width: 10, life: 320, touch: false },
1177
+ run: (el, o) => trail(el, o, (g, pts, now) => {
1178
+ const [r, gg, b] = hexRgb(o.color);
1179
+ g.lineCap = 'round';
1180
+ for (let i = 1; i < pts.length; i++) {
1181
+ const k = 1 - (now - pts[i].t) / o.life;
1182
+ g.strokeStyle = `rgba(${r},${gg},${b},${(k * 0.9).toFixed(3)})`;
1183
+ g.lineWidth = Math.max(0.5, o.width * k);
1184
+ g.beginPath();
1185
+ g.moveTo(pts[i - 1].x, pts[i - 1].y);
1186
+ g.lineTo(pts[i].x, pts[i].y);
1187
+ g.stroke();
1188
+ }
1189
+ }),
1190
+ },
1191
+ {
1192
+ name: 'ribbon-trail',
1193
+ kind: 'cursor',
1194
+ description: 'A smooth rainbow ribbon flows behind the pointer (persistent; `width`, `life`).',
1195
+ defaults: { width: 18, life: 600, touch: false },
1196
+ run: (el, o) => trail(el, o, (g, pts, now) => {
1197
+ for (let i = 2; i < pts.length; i++) {
1198
+ const k = 1 - (now - pts[i].t) / o.life;
1199
+ const a = pts[i - 2];
1200
+ const m = pts[i - 1];
1201
+ const b = pts[i];
1202
+ g.strokeStyle = `hsla(${(pts[i].t / 6) % 360},90%,62%,${(k * 0.85).toFixed(3)})`;
1203
+ g.lineWidth = Math.max(0.5, o.width * k * Math.sin(Math.PI * Math.min(1, (i / pts.length) * 1.1)));
1204
+ g.lineCap = 'round';
1205
+ g.beginPath();
1206
+ g.moveTo((a.x + m.x) / 2, (a.y + m.y) / 2);
1207
+ g.quadraticCurveTo(m.x, m.y, (m.x + b.x) / 2, (m.y + b.y) / 2);
1208
+ g.stroke();
1209
+ }
1210
+ }),
1211
+ },
1212
+ {
1213
+ name: 'sparkle-trail',
1214
+ kind: 'cursor',
1215
+ description: 'Little stars twinkle off the pointer as it moves (persistent; `colors`, `spacing` px between stars).',
1216
+ defaults: { colors: PALETTE, spacing: 14, size: 14, touch: false },
1217
+ run: (el, o, ctx) => {
1218
+ let lx = -1e4;
1219
+ let ly = -1e4;
1220
+ const move = (e) => {
1221
+ if (!touchOk(e, o) || Math.hypot(e.clientX - lx, e.clientY - ly) < o.spacing)
1222
+ return;
1223
+ lx = e.clientX;
1224
+ ly = e.clientY;
1225
+ const s = o.size * rand(0.6, 1.2);
1226
+ spawn(e.clientX - s / 2, e.clientY - s / 2, `font-size:${s}px;line-height:1;color:${o.colors[Math.floor(rand(0, o.colors.length))]}`, ctx, [{ transform: 'translate(0,0) scale(0) rotate(0deg)', opacity: 1 }, { transform: `translate(${rand(-14, 14)}px,${rand(4, 26)}px) scale(1) rotate(${rand(-90, 90)}deg)`, opacity: 0 }], { duration: rand(500, 800), easing: 'ease-out' }, '✦');
1227
+ };
1228
+ el.addEventListener('pointermove', move, { passive: true });
1229
+ return () => el.removeEventListener('pointermove', move);
1230
+ },
1231
+ },
1232
+ {
1233
+ name: 'magnetic-dots',
1234
+ kind: 'cursor',
1235
+ description: 'A grid of dots behind the content leans toward the pointer like iron filings to a magnet (persistent; `gap`, `radius`, `color`).',
1236
+ defaults: { gap: 22, radius: 120, color: '#7c5cff', background: 'transparent', touch: false },
1237
+ reduced: 'skip',
1238
+ run: (el, o, ctx) => {
1239
+ const p = { x: -1e4, y: -1e4 };
1240
+ const move = (e) => {
1241
+ if (!touchOk(e, o))
1242
+ return;
1243
+ const r = el.getBoundingClientRect();
1244
+ p.x = e.clientX - r.left;
1245
+ p.y = e.clientY - r.top;
1246
+ };
1247
+ const leave = () => ((p.x = -1e4), (p.y = -1e4));
1248
+ el.addEventListener('pointermove', move, { passive: true });
1249
+ el.addEventListener('pointerleave', leave);
1250
+ const off = canvasBackground(el, ctx, {
1251
+ draw: ({ ctx: g, w, h }) => {
1252
+ g.clearRect(0, 0, w, h);
1253
+ if (o.background !== 'transparent') {
1254
+ g.fillStyle = o.background;
1255
+ g.fillRect(0, 0, w, h);
1256
+ }
1257
+ g.fillStyle = o.color;
1258
+ for (let y = o.gap / 2; y < h; y += o.gap)
1259
+ for (let x = o.gap / 2; x < w; x += o.gap) {
1260
+ const dx = p.x - x;
1261
+ const dy = p.y - y;
1262
+ const d = Math.hypot(dx, dy);
1263
+ const k = d < o.radius ? (1 - d / o.radius) ** 2 : 0;
1264
+ const s = 1.5 + k * 3;
1265
+ g.globalAlpha = 0.35 + k * 0.65;
1266
+ g.fillRect(x + dx * k * 0.35 - s / 2, y + dy * k * 0.35 - s / 2, s, s);
1267
+ }
1268
+ g.globalAlpha = 1;
1269
+ },
1270
+ }, o);
1271
+ return () => {
1272
+ el.removeEventListener('pointermove', move);
1273
+ el.removeEventListener('pointerleave', leave);
1274
+ off();
1275
+ };
1276
+ },
1277
+ },
1278
+ {
1279
+ name: 'spotlight-cursor',
1280
+ kind: 'cursor',
1281
+ description: 'A soft light follows the pointer across the element, easing behind it (persistent; `color`, `size`).',
1282
+ defaults: { color: '#ffffff', size: 260, ease: 0.18, touch: false },
1283
+ run: (el, o) => {
1284
+ const [r, g, b] = hexRgb(o.color);
1285
+ const s = document.createElement('span');
1286
+ s.setAttribute('aria-hidden', 'true');
1287
+ s.style.cssText = `position:absolute;inset:0;pointer-events:none;border-radius:inherit;opacity:0;transition:opacity .25s;mix-blend-mode:soft-light;background:radial-gradient(circle ${o.size / 2}px at var(--sx,50%) var(--sy,50%),rgba(${r},${g},${b},.75),transparent)`;
1288
+ const restore = el.style.position;
1289
+ if (getComputedStyle(el).position === 'static')
1290
+ el.style.position = 'relative';
1291
+ el.appendChild(s);
1292
+ let tx = 0;
1293
+ let ty = 0;
1294
+ let x = 0;
1295
+ let y = 0;
1296
+ let raf = 0;
1297
+ const tick = () => {
1298
+ x += (tx - x) * o.ease;
1299
+ y += (ty - y) * o.ease;
1300
+ s.style.setProperty('--sx', `${x.toFixed(1)}px`);
1301
+ s.style.setProperty('--sy', `${y.toFixed(1)}px`);
1302
+ raf = Math.abs(tx - x) + Math.abs(ty - y) > 0.3 ? requestAnimationFrame(tick) : 0;
1303
+ };
1304
+ const move = (e) => {
1305
+ if (!touchOk(e, o))
1306
+ return;
1307
+ const rc = el.getBoundingClientRect();
1308
+ tx = e.clientX - rc.left;
1309
+ ty = e.clientY - rc.top;
1310
+ if (s.style.opacity !== '1')
1311
+ ((x = tx), (y = ty), (s.style.opacity = '1'));
1312
+ if (!raf)
1313
+ raf = requestAnimationFrame(tick);
1314
+ };
1315
+ const leave = () => (s.style.opacity = '0');
1316
+ el.addEventListener('pointermove', move, { passive: true });
1317
+ el.addEventListener('pointerleave', leave);
1318
+ return () => {
1319
+ cancelAnimationFrame(raf);
1320
+ el.removeEventListener('pointermove', move);
1321
+ el.removeEventListener('pointerleave', leave);
1322
+ s.remove();
1323
+ el.style.position = restore;
1324
+ };
1325
+ },
1326
+ },
1327
+ ];
1328
+ // --- gestures → effects ------------------------------------------------------
1329
+ const GESTURES = ['fling', 'twist', 'long-press'];
1330
+ /** Release velocity (px/ms) from recent pointer samples: uses the last `window` ms (pure). */
1331
+ function flingVelocity(pts, window = 90) {
1332
+ if (pts.length < 2)
1333
+ return { vx: 0, vy: 0, speed: 0 };
1334
+ const last = pts[pts.length - 1];
1335
+ let first = pts[0];
1336
+ for (let i = pts.length - 2; i >= 0; i--) {
1337
+ first = pts[i];
1338
+ if (last.t - pts[i].t >= window)
1339
+ break;
1340
+ }
1341
+ const dt = Math.max(1, last.t - first.t);
1342
+ const vx = (last.x - first.x) / dt;
1343
+ const vy = (last.y - first.y) / dt;
1344
+ return { vx, vy, speed: Math.hypot(vx, vy) };
1345
+ }
1346
+ /** Signed smallest difference between two angles in degrees, in (-180, 180] (pure). */
1347
+ function angleDelta(a, b) {
1348
+ let d = (b - a) % 360;
1349
+ if (d > 180)
1350
+ d -= 360;
1351
+ if (d <= -180)
1352
+ d += 360;
1353
+ return d;
1354
+ }
1355
+ /**
1356
+ * Fire `effect` (a registered effect name, or a callback) when `gesture`
1357
+ * happens on `el`. Returns an unbind.
1358
+ */
1359
+ function bindGesture(el, gesture, effect, o = {}) {
1360
+ const { velocity = 0.8, angle = 30, duration = 650, tolerance = 10, effectOptions = {} } = o;
1361
+ const pts = new Map();
1362
+ let path = [];
1363
+ let base = null;
1364
+ let press = null;
1365
+ const fire = (d, e) => {
1366
+ el.dispatchEvent(new CustomEvent('usa-gesture', { detail: d, bubbles: true }));
1367
+ if (typeof effect === 'function')
1368
+ effect(d, e);
1369
+ else
1370
+ playEffect(el, effect, effectOptions, e).catch(() => undefined);
1371
+ };
1372
+ const twistAngle = () => {
1373
+ const [a, b] = [...pts.values()];
1374
+ return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
1375
+ };
1376
+ const charge = (k) => {
1377
+ el.style.setProperty('--usa-charge', k.toFixed(3));
1378
+ el.toggleAttribute('data-charging', k > 0 && k < 1);
1379
+ };
1380
+ const endPress = () => {
1381
+ if (!press)
1382
+ return;
1383
+ cancelAnimationFrame(press.raf);
1384
+ press = null;
1385
+ charge(0);
1386
+ };
1387
+ const down = (e) => {
1388
+ pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
1389
+ if (gesture === 'fling')
1390
+ path = [{ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() }];
1391
+ if (gesture === 'twist' && pts.size === 2)
1392
+ base = twistAngle();
1393
+ if (gesture === 'long-press' && pts.size === 1) {
1394
+ press = { x: e.clientX, y: e.clientY, t0: performance.now(), raf: 0 };
1395
+ const step = (now) => {
1396
+ if (!press)
1397
+ return;
1398
+ const k = Math.min(1, (now - press.t0) / duration);
1399
+ charge(k);
1400
+ if (k >= 1) {
1401
+ endPress();
1402
+ el.style.setProperty('--usa-charge', '1');
1403
+ fire({ gesture, charge: 1 }, e);
1404
+ }
1405
+ else
1406
+ press.raf = requestAnimationFrame(step);
1407
+ };
1408
+ press.raf = requestAnimationFrame(step);
1409
+ }
1410
+ };
1411
+ const move = (e) => {
1412
+ if (!pts.has(e.pointerId ?? 1))
1413
+ return;
1414
+ pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
1415
+ if (gesture === 'fling') {
1416
+ path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
1417
+ if (path.length > 20)
1418
+ path.shift();
1419
+ }
1420
+ else if (gesture === 'twist' && pts.size >= 2 && base !== null) {
1421
+ const now = twistAngle();
1422
+ const d = angleDelta(base, now);
1423
+ if (Math.abs(d) >= angle) {
1424
+ base = now;
1425
+ fire({ gesture, angle: d, direction: d > 0 ? 'cw' : 'ccw' }, e);
1426
+ }
1427
+ }
1428
+ else if (gesture === 'long-press' && press && Math.hypot(e.clientX - press.x, e.clientY - press.y) > tolerance)
1429
+ endPress();
1430
+ };
1431
+ const up = (e) => {
1432
+ pts.delete(e.pointerId ?? 1);
1433
+ if (gesture === 'fling' && path.length) {
1434
+ path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
1435
+ const v = flingVelocity(path);
1436
+ path = [];
1437
+ if (v.speed >= velocity) {
1438
+ const direction = Math.abs(v.vx) > Math.abs(v.vy) ? (v.vx > 0 ? 'right' : 'left') : v.vy > 0 ? 'down' : 'up';
1439
+ fire({ gesture, ...v, direction }, e);
1440
+ }
1441
+ }
1442
+ if (pts.size < 2)
1443
+ base = null;
1444
+ endPress();
1445
+ };
1446
+ el.addEventListener('pointerdown', down);
1447
+ el.addEventListener('pointermove', move);
1448
+ el.addEventListener('pointerup', up);
1449
+ el.addEventListener('pointercancel', up);
1450
+ if (gesture === 'twist' && !el.style.touchAction)
1451
+ el.style.touchAction = 'none';
1452
+ return () => {
1453
+ endPress();
1454
+ el.removeEventListener('pointerdown', down);
1455
+ el.removeEventListener('pointermove', move);
1456
+ el.removeEventListener('pointerup', up);
1457
+ el.removeEventListener('pointercancel', up);
1458
+ };
1459
+ }
1460
+ /**
1461
+ * `<usa-gesture-fx gesture="fling | twist | long-press" effect="tada"
1462
+ * options='{"…"}' velocity angle duration>` — plays `effect` on its first
1463
+ * child (or itself with `self`) when the gesture happens.
1464
+ */
1465
+ function defineGestureFx(tag = 'usa-gesture-fx') {
1466
+ return defineElement(tag, (Base) => class UsaGestureFx extends Base {
1467
+ static get observedAttributes() {
1468
+ return ['gesture', 'effect', 'options'];
1469
+ }
1470
+ get gesture() {
1471
+ const g = this.str('gesture', 'fling');
1472
+ return GESTURES.includes(g) ? g : 'fling';
1473
+ }
1474
+ mount() {
1475
+ let effectOptions = {};
1476
+ try {
1477
+ effectOptions = JSON.parse(this.str('options', '{}')) || {};
1478
+ }
1479
+ catch {
1480
+ /* ignore bad JSON */
1481
+ }
1482
+ const target = this.flag('self') ? this : this.firstElementChild || this;
1483
+ const effect = this.str('effect', 'pulse');
1484
+ this.onCleanup(bindGesture(this, this.gesture, (_d, e) => void playEffect(target, effect, effectOptions, e).catch(() => undefined), {
1485
+ velocity: this.num('velocity', 0.8),
1486
+ angle: this.num('angle', 30),
1487
+ duration: this.num('duration', 650),
1488
+ }));
1489
+ }
1490
+ }, { id: 'usa-gesture-fx', text: 'usa-gesture-fx{display:inline-block;touch-action:none;user-select:none}' });
1491
+ }
1492
+
1493
+ const saved = new WeakMap();
1494
+ /** Toggle `aria-pressed` (or set it) and return the new state. */
1495
+ function togglePressed(el, force) {
1496
+ const on = force ?? el.getAttribute('aria-pressed') !== 'true';
1497
+ el.setAttribute('aria-pressed', String(on));
1498
+ return on;
1499
+ }
1500
+ /** Swap `el`'s label for `ms` (polite live region), then restore it. */
1501
+ function swapLabel(el, text, ms) {
1502
+ if (!saved.has(el))
1503
+ saved.set(el, el.innerHTML);
1504
+ el.setAttribute('aria-live', 'polite');
1505
+ el.textContent = text;
1506
+ return new Promise((r) => setTimeout(() => {
1507
+ el.innerHTML = saved.get(el);
1508
+ saved.delete(el);
1509
+ el.removeAttribute('aria-live');
1510
+ r();
1511
+ }, ms));
1512
+ }
1513
+ /** Add `delta` to the number in `[data-count]` (or `el`), keeping it in `data-count`. Returns the new value. */
1514
+ function bumpCount(el, delta, ctx) {
1515
+ const t = el.querySelector('[data-count]') || el;
1516
+ const n = (Number(t.dataset.count ?? (t.textContent || '').replace(/[^\d.-]/g, '')) || 0) + delta;
1517
+ t.dataset.count = String(n);
1518
+ t.textContent = String(n);
1519
+ ctx?.animate(t, [{ transform: `translateY(${delta > 0 ? 60 : -60}%)`, opacity: 0 }, { transform: 'none', opacity: 1 }], { duration: 260, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1520
+ return n;
1521
+ }
1522
+ const pop = (el, ctx, s = 1.25, d = 380) => ctx.animate(el, [{ transform: 'scale(1)' }, { transform: `scale(${s})`, offset: 0.4 }, { transform: 'scale(1)' }], { duration: d, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1523
+ /** A few glyphs flying out of the pointer / centre. */
1524
+ function burst(el, ctx, glyph, color, n = 6, rise = false) {
1525
+ const { x, y } = origin(el, ctx);
1526
+ return all(Array.from({ length: n }, (_, i) => {
1527
+ const a = rise ? -Math.PI / 2 + rand(-0.6, 0.6) : (i / n) * Math.PI * 2;
1528
+ const d = rand(28, 56);
1529
+ return spawn(x - 7, y - 7, `font-size:14px;line-height:1;color:${color}`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 1 }, { transform: `translate(${Math.cos(a) * d}px,${Math.sin(a) * d}px) scale(1)`, opacity: 0 }], { duration: rand(500, 750), easing: 'cubic-bezier(.2,.8,.3,1)' }, glyph);
1530
+ }));
1531
+ }
1532
+ const click = (name, description, defaults, run) => ({ name, kind: 'click', description, defaults, run });
1533
+ const attn = (name, description, defaults, run) => ({ name, kind: 'attention', description, defaults, run });
1534
+ const MICRO_FX = [
1535
+ click('copy-success', 'Copies `text` (or `data-copy` / the target of `for`) to the clipboard and swaps the label to "Copied ✓".', { text: '', label: 'Copied ✓', ms: 1500 }, (el, o, ctx) => {
1536
+ const src = el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : null;
1537
+ const text = o.text || el.dataset.copy || src?.value || src?.textContent || '';
1538
+ navigator.clipboard?.writeText(text).catch(() => undefined);
1539
+ pop(el, ctx, 1.08);
1540
+ return swapLabel(el, o.label, o.ms);
1541
+ }),
1542
+ click('toggle-morph', 'Toggles `aria-pressed` with a squash-and-stretch morph.', {}, (el, _o, ctx) => {
1543
+ const on = togglePressed(el);
1544
+ return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${on ? 1.15 : 0.85},${on ? 0.85 : 1.15})`, offset: 0.35 }, { transform: 'scale(1,1)' }], { duration: 360, easing: 'ease-out' });
1545
+ }),
1546
+ click('password-reveal', 'Shows / hides the password input (`for` id, or the previous input) with an eye blink.', { show: 'Hide password', hide: 'Show password' }, (el, o, ctx) => {
1547
+ const input = (el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : el.previousElementSibling);
1548
+ if (!input || !('type' in input))
1549
+ return;
1550
+ const show = input.type === 'password';
1551
+ input.type = show ? 'text' : 'password';
1552
+ togglePressed(el, show);
1553
+ el.setAttribute('aria-label', show ? o.show : o.hide);
1554
+ return ctx.animate(el, [{ transform: 'scaleY(1)' }, { transform: 'scaleY(.1)', offset: 0.5 }, { transform: 'scaleY(1)' }], { duration: 240, easing: 'ease-in-out' });
1555
+ }),
1556
+ click('favorite-star', 'Toggles a favourite: the star pops and throws little stars (`color`).', { color: '#facc15' }, (el, o, ctx) => {
1557
+ const on = togglePressed(el);
1558
+ return Promise.all([pop(el, ctx, on ? 1.35 : 0.85)?.finished, on ? burst(el, ctx, '★', o.color) : null]);
1559
+ }),
1560
+ click('like-heart', 'Toggles a like: the heart beats and hearts float up (`color`).', { color: '#ff5c8a' }, (el, o, ctx) => {
1561
+ const on = togglePressed(el);
1562
+ return Promise.all([pop(el, ctx, on ? 1.3 : 0.9)?.finished, on ? burst(el, ctx, '♥', o.color, 5, true) : null]);
1563
+ }),
1564
+ click('bookmark-flip', 'Toggles a bookmark with a 3D flip.', {}, (el, _o, ctx) => {
1565
+ togglePressed(el);
1566
+ return ctx.animate(el, [{ transform: 'perspective(400px) rotateY(0)' }, { transform: 'perspective(400px) rotateY(180deg)' }, { transform: 'perspective(400px) rotateY(360deg)' }], { duration: 520, easing: 'ease-in-out' });
1567
+ }),
1568
+ click('download-progress', 'Fills a progress bar over `duration` ms, then shows "Done ✓" (`aria-busy` while running; fires `usa-done`).', { duration: 1600, label: 'Done ✓', color: '#34d399' }, (el, o, ctx) => {
1569
+ if (el.getAttribute('aria-busy') === 'true')
1570
+ return;
1571
+ el.setAttribute('aria-busy', 'true');
1572
+ const [bar, remove] = overlay(el, `background:${o.color}55;transform-origin:left;transform:scaleX(0)`);
1573
+ const a = ctx.animate(bar, [{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: o.duration, easing: 'linear', fill: 'forwards' });
1574
+ const done = a ? a.finished.catch(() => undefined) : Promise.resolve();
1575
+ return done.then(() => {
1576
+ remove();
1577
+ el.removeAttribute('aria-busy');
1578
+ el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
1579
+ return swapLabel(el, o.label, 1400);
1580
+ });
1581
+ }),
1582
+ click('submit-loading', 'Shows "Sending…" with pulsing dots for `duration` ms, then "Sent ✓" (`aria-busy`, `usa-done`).', { duration: 1200, loading: 'Sending…', label: 'Sent ✓' }, (el, o, ctx) => {
1583
+ if (el.getAttribute('aria-busy') === 'true')
1584
+ return;
1585
+ el.setAttribute('aria-busy', 'true');
1586
+ const t = swapLabel(el, o.loading, o.duration);
1587
+ const a = ctx.animate(el, [{ opacity: 1 }, { opacity: 0.55 }, { opacity: 1 }], { duration: 600, iterations: Math.max(1, Math.round(o.duration / 600)) });
1588
+ return t.then(() => {
1589
+ a?.cancel();
1590
+ el.removeAttribute('aria-busy');
1591
+ el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
1592
+ return swapLabel(el, o.label, 1400);
1593
+ });
1594
+ }),
1595
+ click('send-plane', 'A paper plane ✈ takes off from the button.', { color: '#22d3ee' }, (el, o, ctx) => {
1596
+ const r = el.getBoundingClientRect();
1597
+ pop(el, ctx, 0.92, 200);
1598
+ return spawn(r.left + r.width / 2 - 8, r.top + r.height / 2 - 8, `font-size:16px;color:${o.color}`, ctx, [{ transform: 'translate(0,0) rotate(0)', opacity: 1 }, { transform: 'translate(40px,-10px) rotate(-10deg)', opacity: 1, offset: 0.4 }, { transform: 'translate(160px,-90px) rotate(-25deg)', opacity: 0 }], { duration: 800, easing: 'ease-in' }, '✈')?.finished;
1599
+ }),
1600
+ click('add-to-cart', 'Bumps `[data-count]` by one and floats a "+1" (`text`).', { text: '+1', color: '#34d399' }, (el, o, ctx) => {
1601
+ bumpCount(el, 1, ctx);
1602
+ const { x, y } = origin(el, ctx);
1603
+ return spawn(x - 10, y - 10, `font:700 14px system-ui;color:${o.color}`, ctx, [{ transform: 'translateY(0)', opacity: 1 }, { transform: 'translateY(-40px)', opacity: 0 }], { duration: 700, easing: 'ease-out' }, o.text)?.finished;
1604
+ }),
1605
+ click('counter-bump', 'Adds `step` to `[data-count]` with a rolling number.', { step: 1 }, (el, o, ctx) => void bumpCount(el, Number(o.step) || 1, ctx)),
1606
+ click('upvote', 'Toggles an upvote: the arrow nudges up and `[data-count]` ±1.', {}, (el, _o, ctx) => {
1607
+ const on = togglePressed(el);
1608
+ bumpCount(el, on ? 1 : -1, ctx);
1609
+ return ctx.animate(el, [{ transform: 'translateY(0)' }, { transform: `translateY(${on ? -6 : 4}px)`, offset: 0.4 }, { transform: 'translateY(0)' }], { duration: 320, easing: 'cubic-bezier(.2,1.4,.4,1)' });
1610
+ }),
1611
+ click('clap', 'Counts claps in `[data-count]` with a 👏 burst on every press.', {}, (el, _o, ctx) => {
1612
+ bumpCount(el, 1, ctx);
1613
+ return Promise.all([pop(el, ctx, 1.15, 240)?.finished, burst(el, ctx, '👏', 'inherit', 3, true)]);
1614
+ }),
1615
+ click('emoji-react', 'Pops one `emoji` up from the pointer (reaction button).', { emoji: '🎉' }, (el, o, ctx) => burst(el, ctx, o.emoji, 'inherit', 1, true)),
1616
+ click('refresh-spin', 'Spins the icon one turn (`turns`).', { turns: 1 }, (el, o, ctx) => ctx.animate(el, [{ transform: 'rotate(0)' }, { transform: `rotate(${360 * o.turns}deg)` }], { duration: 600 * o.turns, easing: 'cubic-bezier(.4,0,.2,1)' })),
1617
+ click('trash-shake', 'Shakes, then drops and fades (`remove: true` removes the element afterwards).', { remove: false }, (el, o, ctx) => {
1618
+ const a = ctx.animate(el, [{ transform: 'none', opacity: 1 }, { transform: 'rotate(-6deg)', offset: 0.15 }, { transform: 'rotate(6deg)', offset: 0.3 }, { transform: 'rotate(0)', offset: 0.45, opacity: 1 }, { transform: 'translateY(30px) scale(.8)', opacity: 0 }], { duration: 650, easing: 'ease-in', fill: 'forwards' });
1619
+ const end = () => (o.remove ? el.remove() : a?.cancel());
1620
+ return a ? a.finished.then(end, end) : void end();
1621
+ }),
1622
+ click('check-toggle', 'Toggles a check mark (`aria-checked` for role=checkbox, else `aria-pressed`) that scales in.', {}, (el, _o, ctx) => {
1623
+ const attr = el.getAttribute('role') === 'checkbox' ? 'aria-checked' : 'aria-pressed';
1624
+ const on = el.getAttribute(attr) !== 'true';
1625
+ el.setAttribute(attr, String(on));
1626
+ return ctx.animate(el, on ? [{ transform: 'scale(.6)' }, { transform: 'scale(1.15)', offset: 0.6 }, { transform: 'scale(1)' }] : [{ transform: 'scale(1)' }, { transform: 'scale(.85)' }, { transform: 'scale(1)' }], { duration: 280, easing: 'ease-out' });
1627
+ }),
1628
+ attn('input-shake', 'Shakes an invalid field side to side and sets `aria-invalid` (`color` outline flash).', { color: '#ff5c8a' }, (el, o, ctx) => {
1629
+ el.setAttribute('aria-invalid', 'true');
1630
+ const prev = el.style.outline;
1631
+ el.style.outline = `2px solid ${o.color}`;
1632
+ setTimeout(() => (el.style.outline = prev), 900);
1633
+ return ctx.animate(el, [0, -8, 8, -6, 6, -3, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 420, easing: 'ease-out' });
1634
+ }),
1635
+ attn('error-flash', 'Flashes the element red once (no more than one flash per call).', { color: '#ff5c8a' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}00` }, { boxShadow: `0 0 0 4px ${o.color}`, offset: 0.3 }, { boxShadow: `0 0 0 0 ${o.color}00` }], { duration: 700 })),
1636
+ attn('success-check', 'A green ✓ badge pops over the element and fades.', { color: '#34d399' }, (el, o, ctx) => {
1637
+ const [b, remove] = overlay(el, `display:grid;place-items:center;font:700 22px system-ui;color:#fff;background:${o.color}d0`);
1638
+ b.textContent = '✓';
1639
+ const a = ctx.animate(b, [{ opacity: 0, transform: 'scale(.6)' }, { opacity: 1, transform: 'scale(1)', offset: 0.3 }, { opacity: 1, offset: 0.75 }, { opacity: 0 }], { duration: 1100, easing: 'ease-out' });
1640
+ return a ? a.finished.then(remove, remove) : void setTimeout(remove, 900);
1641
+ }),
1642
+ attn('nudge-hint', 'A small sideways nudge that says "try me" (`distance`).', { distance: 6 }, (el, o, ctx) => ctx.animate(el, [0, o.distance, 0, o.distance / 2, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 700, easing: 'ease-in-out' })),
1643
+ attn('focus-pulse', 'A ring pulses around the element to draw focus to it (`color`).', { color: '#7c5cff' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}aa` }, { boxShadow: `0 0 0 12px ${o.color}00` }], { duration: 900, iterations: 2, easing: 'ease-out' })),
1644
+ attn('notify-badge', 'Bumps the badge count (`[data-count]`, `step`) with a pop — for new notifications.', { step: 1 }, (el, o, ctx) => {
1645
+ bumpCount(el, Number(o.step) || 1, ctx);
1646
+ return pop(el, ctx, 1.3, 320);
1647
+ }),
1648
+ ];
1649
+
1650
+ const THEME_ROLES = ['enter', 'hover', 'click', 'attention', 'background'];
1651
+ const P = (enter, hover, click, attention, background) => ({
1652
+ enter: { effect: enter },
1653
+ hover: { effect: hover },
1654
+ click: { effect: click },
1655
+ attention: { effect: attention },
1656
+ background: { effect: background },
1657
+ });
1658
+ const THEMES = {
1659
+ neon: {
1660
+ name: 'neon',
1661
+ vars: { bg: '#07070c', fg: '#e8e8ff', accent: '#22d3ee', 'accent-2': '#ff2bd6', surface: '#11111c', border: '1px solid #22d3ee', radius: '10px', shadow: '0 0 18px #22d3ee66, inset 0 0 12px #ff2bd622', font: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
1662
+ motion: { duration: { fast: 120, normal: 220 }, easing: { standard: 'cubic-bezier(0.2, 0, 0, 1)' } },
1663
+ presets: P('fade-up', 'neon-flicker', 'shockwave', 'neon-flicker', 'starfield'),
1664
+ },
1665
+ paper: {
1666
+ name: 'paper',
1667
+ vars: { bg: '#f6f1e7', fg: '#2b2721', accent: '#c2410c', 'accent-2': '#0f766e', surface: '#fffdf8', border: '1px solid #e3dccd', radius: '4px', shadow: '0 1px 0 #0000000d, 0 10px 24px -14px #00000040', font: 'Georgia, "Times New Roman", serif' },
1668
+ motion: { duration: { fast: 200, normal: 380, slow: 700 }, easing: { standard: 'cubic-bezier(0, 0, 0, 1)' } },
1669
+ presets: P('paper-fold', 'wiggle', 'ink-splash', 'nudge-hint', 'contours'),
1670
+ },
1671
+ glass: {
1672
+ name: 'glass',
1673
+ vars: { bg: 'linear-gradient(135deg, #1e1b4b, #0e7490)', fg: '#f8fafc', accent: '#a5f3fc', 'accent-2': '#c4b5fd', surface: '#ffffff1f', border: '1px solid #ffffff40', radius: '18px', shadow: '0 8px 32px #0000003d', font: 'system-ui, -apple-system, "Segoe UI", sans-serif' },
1674
+ motion: { duration: { normal: 320 }, easing: { standard: 'cubic-bezier(0.22, 1, 0.36, 1)' } },
1675
+ presets: P('blur', 'glass-shine', 'ripple', 'focus-pulse', 'mesh-gradient'),
1676
+ },
1677
+ retro: {
1678
+ name: 'retro',
1679
+ vars: { bg: '#1d1135', fg: '#ffe9a8', accent: '#ff6b35', 'accent-2': '#2ec4b6', surface: '#2a1b4d', border: '3px solid #ffe9a8', radius: '0', shadow: '4px 4px 0 #ff6b35', font: '"Courier New", ui-monospace, monospace' },
1680
+ motion: { duration: { normal: 300 }, easing: { standard: 'steps(6, end)' } },
1681
+ presets: P('clip-up', 'rubber-band', 'star-burst', 'tada', 'retro-scanlines'),
1682
+ },
1683
+ brutalist: {
1684
+ name: 'brutalist',
1685
+ vars: { bg: '#ffffff', fg: '#000000', accent: '#ff3b00', 'accent-2': '#0047ff', surface: '#fff200', border: '3px solid #000000', radius: '0', shadow: '6px 6px 0 #000000', font: '"Arial Black", Arial, sans-serif' },
1686
+ motion: { duration: { fast: 80, normal: 160 }, easing: { standard: 'linear' } },
1687
+ presets: P('drop-bounce', 'jelly', 'brutal-shift', 'shake', 'voronoi'),
1688
+ },
1689
+ };
1690
+ const THEME_NAMES = Object.keys(THEMES);
1691
+ const BASE_CSS = `[data-usa-theme]{background:var(--usa-theme-bg);color:var(--usa-theme-fg);font-family:var(--usa-theme-font)}
1692
+ [data-usa-theme] .usa-surface{background:var(--usa-theme-surface);border:var(--usa-theme-border);border-radius:var(--usa-theme-radius);box-shadow:var(--usa-theme-shadow)}
1693
+ [data-usa-theme] .usa-accent{color:var(--usa-theme-accent)}
1694
+ [data-usa-theme=glass] .usa-surface{-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}`;
1695
+ const pack = (t) => {
1696
+ const p = typeof t === 'string' ? THEMES[t] : t;
1697
+ if (!p)
1698
+ throw new Error(`[motionary] unknown theme "${String(t)}" — ${THEME_NAMES.join(', ')}`);
1699
+ return p;
1700
+ };
1701
+ /** The CSS custom properties of a theme (design + motion tokens). */
1702
+ function themeVars(t) {
1703
+ const p = pack(t);
1704
+ const out = {};
1705
+ for (const [k, v] of Object.entries(p.vars))
1706
+ out[`--usa-theme-${k}`] = v;
1707
+ return { ...out, ...motionTokensToVars(mergeMotionTokens(p.motion)) };
1708
+ }
1709
+ /** A theme as a CSS rule (`selector` default `[data-usa-theme=<name>]`) — for SSR / static CSS. */
1710
+ function themeCss(t, selector) {
1711
+ const p = pack(t);
1712
+ return `${selector || `[data-usa-theme=${p.name}]`}{${Object.entries(themeVars(p)).map(([k, v]) => `${k}:${v}`).join(';')}}`;
1713
+ }
1714
+ /** Apply a theme to `root` (default `<html>`). Returns an undo. */
1715
+ function applyTheme(t, root) {
1716
+ const p = pack(t);
1717
+ const el = root || document.documentElement;
1718
+ adoptStyles('usa-theme', BASE_CSS);
1719
+ const prevAttr = el.getAttribute('data-usa-theme');
1720
+ el.setAttribute('data-usa-theme', p.name);
1721
+ const vars = Object.entries(p.vars).map(([k, v]) => [`--usa-theme-${k}`, v]);
1722
+ const old = vars.map(([k]) => [k, el.style.getPropertyValue(k)]);
1723
+ for (const [k, v] of vars)
1724
+ el.style.setProperty(k, v);
1725
+ let undoMotion;
1726
+ if (el === document.documentElement)
1727
+ undoMotion = applyMotionTokens(p.motion, el);
1728
+ else {
1729
+ const mv = Object.entries(motionTokensToVars(mergeMotionTokens(p.motion)));
1730
+ const mold = mv.map(([k]) => [k, el.style.getPropertyValue(k)]);
1731
+ for (const [k, v] of mv)
1732
+ el.style.setProperty(k, v);
1733
+ undoMotion = () => mold.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
1734
+ }
1735
+ return () => {
1736
+ undoMotion();
1737
+ old.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
1738
+ if (prevAttr === null)
1739
+ el.removeAttribute('data-usa-theme');
1740
+ else
1741
+ el.setAttribute('data-usa-theme', prevAttr);
1742
+ };
1743
+ }
1744
+ /** The effect preset of a theme for a role. */
1745
+ function themePreset(t, role) {
1746
+ return pack(t).presets[role];
1747
+ }
1748
+ /** Play the preset for `role` of the theme on the closest `[data-usa-theme]` (or `theme`). */
1749
+ function playThemeEffect(el, role, theme) {
1750
+ const name = theme || el.closest('[data-usa-theme]')?.getAttribute('data-usa-theme') || 'neon';
1751
+ const pr = themePreset(name, role);
1752
+ return playEffect(el, pr.effect, pr.options);
1753
+ }
1754
+ const THEME_FX = [
1755
+ {
1756
+ name: 'neon-flicker',
1757
+ kind: 'attention',
1758
+ description: 'A neon tube flickering on — dims (never blacks out) twice, well under 3 flashes per second.',
1759
+ defaults: { color: '#22d3ee' },
1760
+ run: (el, o, ctx) => ctx.animate(el, [{ opacity: 1, filter: 'none' }, { opacity: 0.55, offset: 0.2 }, { opacity: 1, filter: `drop-shadow(0 0 6px ${o.color})`, offset: 0.35 }, { opacity: 0.7, offset: 0.6 }, { opacity: 1, filter: `drop-shadow(0 0 10px ${o.color})` }], { duration: 900, easing: 'linear' }),
1761
+ },
1762
+ {
1763
+ name: 'paper-fold',
1764
+ kind: 'enter',
1765
+ description: 'Unfolds like a sheet of paper hinged at the top.',
1766
+ defaults: { duration: 600 },
1767
+ run: (el, o, ctx) => ctx.animate(el, [{ transform: 'perspective(800px) rotateX(-85deg)', transformOrigin: 'top', opacity: 0 }, { transform: 'perspective(800px) rotateX(12deg)', transformOrigin: 'top', opacity: 1, offset: 0.7 }, { transform: 'perspective(800px) rotateX(0)', transformOrigin: 'top', opacity: 1 }], { duration: o.duration, easing: 'ease-out', fill: 'backwards' }),
1768
+ },
1769
+ {
1770
+ name: 'glass-shine',
1771
+ kind: 'hover',
1772
+ description: 'A bright diagonal shine sweeps across frosted glass.',
1773
+ defaults: { duration: 700 },
1774
+ run: (el, o, ctx) => {
1775
+ const [s, remove] = overlay(el, 'overflow:hidden;background:linear-gradient(105deg,transparent 35%,#ffffff8c 50%,transparent 65%);background-size:250% 100%;background-position:120% 0');
1776
+ const a = ctx.animate(s, [{ backgroundPosition: '120% 0' }, { backgroundPosition: '-20% 0' }], { duration: o.duration, easing: 'ease-in-out' });
1777
+ return a ? a.finished.then(remove, remove) : void remove();
1778
+ },
1779
+ },
1780
+ {
1781
+ name: 'retro-scanlines',
1782
+ kind: 'background',
1783
+ description: 'CRT scanlines with a slow roll (static lines under reduced motion; persistent).',
1784
+ reduced: 'run',
1785
+ defaults: { opacity: 0.18 },
1786
+ run: (el, o, ctx) => {
1787
+ const [s, remove] = overlay(el, `opacity:${o.opacity};background:repeating-linear-gradient(0deg,#000 0 1px,transparent 1px 3px);mix-blend-mode:multiply`);
1788
+ const a = ctx.reduced ? null : ctx.animate(s, [{ backgroundPosition: '0 0' }, { backgroundPosition: '0 30px' }], { duration: 2400, iterations: Infinity, easing: 'linear' });
1789
+ return () => {
1790
+ a?.cancel();
1791
+ remove();
1792
+ };
1793
+ },
1794
+ },
1795
+ {
1796
+ name: 'brutal-shift',
1797
+ kind: 'click',
1798
+ description: 'Presses into its hard drop shadow and springs back (brutalist buttons).',
1799
+ defaults: { offset: 6 },
1800
+ run: (el, o, ctx) => ctx.animate(el, [{ transform: 'translate(0,0)' }, { transform: `translate(${o.offset}px,${o.offset}px)`, boxShadow: '0 0 0 #000', offset: 0.3 }, { transform: 'translate(0,0)' }], { duration: 260, easing: 'ease-out' }),
1801
+ },
1802
+ ];
1803
+ /** `<usa-theme name="neon | paper | glass | retro | brutalist">` — a themed subtree. */
1804
+ function defineTheme(tag = 'usa-theme') {
1805
+ return defineElement(tag, (Base) => class UsaTheme extends Base {
1806
+ static get observedAttributes() {
1807
+ return ['name'];
1808
+ }
1809
+ get theme() {
1810
+ const n = this.str('name', 'neon');
1811
+ return THEMES[n] ? n : 'neon';
1812
+ }
1813
+ mount() {
1814
+ this.onCleanup(applyTheme(this.theme, this));
1815
+ this.querySelectorAll('[data-theme-fx]').forEach((el) => {
1816
+ const role = (el.dataset.themeFx || 'click');
1817
+ if (!THEME_ROLES.includes(role))
1818
+ return;
1819
+ const pr = themePreset(this.theme, role);
1820
+ const trigger = role === 'attention' ? 'click' : role === 'background' ? 'load' : role;
1821
+ try {
1822
+ this.onCleanup(bindEffect(el, pr.effect, { ...(pr.options || {}), trigger }));
1823
+ }
1824
+ catch {
1825
+ /* effect not registered: call registerAllEffects() */
1826
+ }
1827
+ });
1828
+ }
1829
+ }, { id: 'usa-theme-el', text: 'usa-theme{display:block}' });
1830
+ }
1831
+
1832
+ /**
1833
+ * 5.9 — `<usa-player>`: plays JSON animations.
1834
+ *
1835
+ * Format `use-scroll-animate/animation` v1:
1836
+ *
1837
+ * ```json
1838
+ * { "format": "use-scroll-animate/animation", "version": 1, "name": "Hero",
1839
+ * "loop": false,
1840
+ * "tracks": [
1841
+ * { "target": "h1", "start": 0, "duration": 600, "preset": "fade-up" },
1842
+ * { "target": ".cta", "start": 500, "duration": 500, "keyframes": [{ "opacity": 0 }, { "opacity": 1 }], "easing": "ease-out" },
1843
+ * { "target": ".cta", "start": 1100, "effect": "jelly", "options": {} }
1844
+ * ] }
1845
+ * ```
1846
+ *
1847
+ * A track animates `target` (a selector inside the player; `:scope` for the
1848
+ * player itself) with a timeline preset, its own keyframes, or fires any
1849
+ * registered effect at `start`. Playground presets (format
1850
+ * `use-scroll-animate/playground`) are accepted too — their tracks map to the
1851
+ * player's children in order.
1852
+ *
1853
+ * Keyframe tracks are WAAPI animations driven by one clock, so the player can
1854
+ * play, pause, seek, change rate and be scrubbed by scroll
1855
+ * (`trigger="scroll"`). Reduced motion: jumps to the end state, effects skipped.
1856
+ */
1857
+ const ANIMATION_FORMAT = 'use-scroll-animate/animation';
1858
+ function decodePlayground(state) {
1859
+ try {
1860
+ const b64 = state.replace(/-/g, '+').replace(/_/g, '/');
1861
+ const json = decodeURIComponent(escape(atob(b64)));
1862
+ const t = (JSON.parse(json).t || []);
1863
+ return t.map(([preset, start, duration, label], i) => ({ target: `:scope > :nth-child(${i + 1})`, preset, start, duration, label }));
1864
+ }
1865
+ catch {
1866
+ return [];
1867
+ }
1868
+ }
1869
+ /** Validate / normalise an animation (object or JSON text). Throws on anything unusable. */
1870
+ function normalizeAnimation(input) {
1871
+ const d = typeof input === 'string' ? JSON.parse(input) : input;
1872
+ if (!d || typeof d !== 'object')
1873
+ throw new Error('[motionary] animation: expected an object');
1874
+ let tracks;
1875
+ if (d.format === 'use-scroll-animate/playground')
1876
+ tracks = decodePlayground(String(d.state || ''));
1877
+ else if (Array.isArray(d.tracks))
1878
+ tracks = d.tracks;
1879
+ else
1880
+ throw new Error('[motionary] animation: missing "tracks"');
1881
+ if (d.format && d.format !== ANIMATION_FORMAT && d.format !== 'use-scroll-animate/playground')
1882
+ throw new Error(`[motionary] animation: unknown format "${d.format}"`);
1883
+ if (d.version && d.version > 1 && d.format === ANIMATION_FORMAT)
1884
+ throw new Error(`[motionary] animation: version ${d.version} needs a newer motionary`);
1885
+ const out = tracks
1886
+ .filter((t) => t && (t.effect || t.preset || Array.isArray(t.keyframes)))
1887
+ .map((t) => ({ ...t, target: t.target || ':scope', start: Math.max(0, Number(t.start) || 0), duration: t.effect ? 0 : Math.max(1, Number(t.duration) || 600) }));
1888
+ const end = out.reduce((m, t) => Math.max(m, t.start + t.duration), 0);
1889
+ return { tracks: out, duration: Math.max(end, Number(d.duration) || 0), loop: !!d.loop, name: String(d.name || 'animation') };
1890
+ }
1891
+ const pick = (root, sel) => {
1892
+ if (sel === ':scope')
1893
+ return [root];
1894
+ try {
1895
+ return Array.from(root.querySelectorAll(sel));
1896
+ }
1897
+ catch {
1898
+ return [];
1899
+ }
1900
+ };
1901
+ /** Bind an animation to `root` and return its controller (paused at 0 unless `autoplay`). */
1902
+ function createPlayer(root, animation, o = {}) {
1903
+ const a = normalizeAnimation(animation);
1904
+ const loop = o.loop ?? a.loop;
1905
+ const anims = [];
1906
+ const effects = [];
1907
+ for (const t of a.tracks) {
1908
+ const els = pick(root, t.target);
1909
+ if (t.effect)
1910
+ effects.push({ els, t, fired: false });
1911
+ else {
1912
+ const kf = t.keyframes || TIMELINE_PRESETS[t.preset] || TIMELINE_PRESETS.fade;
1913
+ for (const el of els) {
1914
+ if (typeof el.animate !== 'function')
1915
+ continue;
1916
+ const an = el.animate(kf, { duration: t.duration, delay: t.start, easing: t.easing || 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'both' });
1917
+ an.pause();
1918
+ an.currentTime = 0;
1919
+ anims.push(an);
1920
+ }
1921
+ }
1922
+ }
1923
+ let time = 0;
1924
+ let playing = false;
1925
+ let raf = 0;
1926
+ let last = 0;
1927
+ let resolve;
1928
+ let finished = new Promise((r) => (resolve = r));
1929
+ const set = (ms) => {
1930
+ time = Math.min(a.duration, Math.max(0, ms));
1931
+ for (const an of anims)
1932
+ an.currentTime = time;
1933
+ };
1934
+ const fireDue = () => {
1935
+ if (prefersReducedMotion())
1936
+ return;
1937
+ for (const e of effects)
1938
+ if (!e.fired && time >= e.t.start) {
1939
+ e.fired = true;
1940
+ for (const el of e.els)
1941
+ playEffect(el, e.t.effect, e.t.options || {}).catch(() => undefined);
1942
+ }
1943
+ };
1944
+ const done = () => {
1945
+ resolve();
1946
+ o.onFinish?.();
1947
+ finished = new Promise((r) => (resolve = r));
1948
+ };
1949
+ const frame = (now) => {
1950
+ raf = 0;
1951
+ if (!playing)
1952
+ return;
1953
+ set(time + (now - last) * player.rate);
1954
+ last = now;
1955
+ fireDue();
1956
+ if (time >= a.duration) {
1957
+ if (loop) {
1958
+ effects.forEach((e) => (e.fired = false));
1959
+ set(0);
1960
+ }
1961
+ else {
1962
+ playing = false;
1963
+ done();
1964
+ return;
1965
+ }
1966
+ }
1967
+ raf = requestAnimationFrame(frame);
1968
+ };
1969
+ const player = {
1970
+ get duration() {
1971
+ return a.duration;
1972
+ },
1973
+ get currentTime() {
1974
+ return time;
1975
+ },
1976
+ get playing() {
1977
+ return playing;
1978
+ },
1979
+ rate: o.rate ?? 1,
1980
+ get finished() {
1981
+ return finished;
1982
+ },
1983
+ play() {
1984
+ if (prefersReducedMotion()) {
1985
+ set(a.duration);
1986
+ effects.forEach((e) => (e.fired = true));
1987
+ done();
1988
+ return;
1989
+ }
1990
+ if (playing)
1991
+ return;
1992
+ if (time >= a.duration) {
1993
+ effects.forEach((e) => (e.fired = false));
1994
+ set(0);
1995
+ }
1996
+ playing = true;
1997
+ last = performance.now();
1998
+ fireDue();
1999
+ raf = requestAnimationFrame(frame);
2000
+ },
2001
+ pause() {
2002
+ playing = false;
2003
+ cancelAnimationFrame(raf);
2004
+ raf = 0;
2005
+ },
2006
+ seek(ms) {
2007
+ set(ms);
2008
+ effects.forEach((e) => (e.fired = e.t.start < time));
2009
+ },
2010
+ destroy() {
2011
+ player.pause();
2012
+ anims.forEach((an) => an.cancel());
2013
+ },
2014
+ };
2015
+ if (o.autoplay)
2016
+ player.play();
2017
+ return player;
2018
+ }
2019
+ /**
2020
+ * `<usa-player src="hero.json" | <script type="application/json"> child
2021
+ * trigger="load | view | scroll | click | manual" loop rate controls>`.
2022
+ * Emits `usa-player-ready` and `usa-player-finish`; sets `data-error` when the
2023
+ * animation cannot be loaded.
2024
+ */
2025
+ function definePlayer(tag = 'usa-player') {
2026
+ return defineElement(tag, (Base) => class UsaPlayer extends Base {
2027
+ constructor() {
2028
+ super(...arguments);
2029
+ this.player = null;
2030
+ this.json = null;
2031
+ }
2032
+ static get observedAttributes() {
2033
+ return ['src', 'trigger', 'loop', 'rate'];
2034
+ }
2035
+ load(animation) {
2036
+ this.json = animation;
2037
+ this.start();
2038
+ }
2039
+ play() {
2040
+ this.player?.play();
2041
+ }
2042
+ pause() {
2043
+ this.player?.pause();
2044
+ }
2045
+ seek(ms) {
2046
+ this.player?.seek(ms);
2047
+ }
2048
+ start() {
2049
+ this.player?.destroy();
2050
+ this.player = null;
2051
+ if (!this.json)
2052
+ return;
2053
+ try {
2054
+ this.player = createPlayer(this, this.json, {
2055
+ loop: this.hasAttribute('loop') ? true : undefined,
2056
+ rate: this.num('rate', 1),
2057
+ onFinish: () => this.dispatchEvent(new CustomEvent('usa-player-finish', { bubbles: true })),
2058
+ });
2059
+ this.removeAttribute('data-error');
2060
+ }
2061
+ catch (err) {
2062
+ this.setAttribute('data-error', String(err.message || err));
2063
+ return;
2064
+ }
2065
+ const p = this.player;
2066
+ this.dispatchEvent(new CustomEvent('usa-player-ready', { detail: { duration: p.duration }, bubbles: true }));
2067
+ const trig = this.str('trigger', 'view');
2068
+ if (trig === 'load')
2069
+ p.play();
2070
+ else if (trig === 'view')
2071
+ this.inView((v) => v && p.play(), { threshold: 0.25 });
2072
+ else if (trig === 'click')
2073
+ this.listen(this, 'click', () => (p.playing ? p.pause() : p.play()));
2074
+ else if (trig === 'scroll') {
2075
+ let f = 0;
2076
+ const upd = () => {
2077
+ f = 0;
2078
+ p.seek(storyProgress(this) * p.duration);
2079
+ };
2080
+ const kick = () => void (f || (f = requestAnimationFrame(upd)));
2081
+ this.listen(window, 'scroll', kick, { passive: true });
2082
+ this.listen(window, 'resize', kick);
2083
+ this.onCleanup(() => cancelAnimationFrame(f));
2084
+ upd();
2085
+ }
2086
+ if (this.flag('controls'))
2087
+ this.mountControls(p);
2088
+ }
2089
+ mountControls(p) {
2090
+ const b = document.createElement('button');
2091
+ b.type = 'button';
2092
+ b.setAttribute('data-player-toggle', '');
2093
+ b.textContent = '▶︎ / ❚❚';
2094
+ b.setAttribute('aria-label', 'Play / pause animation');
2095
+ this.listen(b, 'click', (e) => {
2096
+ e.stopPropagation();
2097
+ if (p.playing)
2098
+ p.pause();
2099
+ else
2100
+ p.play();
2101
+ b.setAttribute('aria-pressed', String(p.playing));
2102
+ });
2103
+ this.append(b);
2104
+ this.onCleanup(() => b.remove());
2105
+ }
2106
+ changed(name) {
2107
+ if (name === 'src')
2108
+ this.json = null;
2109
+ super.changed(name);
2110
+ }
2111
+ mount() {
2112
+ this.onCleanup(() => {
2113
+ this.player?.destroy();
2114
+ this.player = null;
2115
+ });
2116
+ const inline = this.querySelector('script[type="application/json"]');
2117
+ const src = this.str('src');
2118
+ if (inline && !this.json)
2119
+ this.json = inline.textContent || '';
2120
+ if (src && !this.json) {
2121
+ fetch(src)
2122
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))))
2123
+ .then((t) => this.isConnected && this.load(t))
2124
+ .catch((err) => this.setAttribute('data-error', String(err.message || err)));
2125
+ return;
2126
+ }
2127
+ this.start();
2128
+ }
2129
+ }, { id: 'usa-player', text: 'usa-player{display:block;position:relative}usa-player>script{display:none}usa-player [data-player-toggle]{position:absolute;right:8px;bottom:8px}' });
2130
+ }
2131
+
2132
+ /**
2133
+ * motionary/components/effects — the 5.x effect packs, all
2134
+ * registered through `registerEffect()` (5.0) and playable with
2135
+ * `playEffect()`, `bindEffect()` or `<usa-fx>`. Kept out of
2136
+ * `motionary/components` / `components/lite` so their size budgets
2137
+ * hold; the UMD bundle registers everything.
2138
+ *
2139
+ * ```ts
2140
+ * import { registerAllEffects } from 'motionary/components/effects';
2141
+ * registerAllEffects();
2142
+ * ```
2143
+ */
2144
+ /** The effect packs by version, in release order. */
2145
+ const EFFECT_PACKS = {
2146
+ 'cards-click': [...CARD_FX, ...CLICK_FX],
2147
+ physics: PHYSICS_FX,
2148
+ page: PAGE_FX,
2149
+ generative: GENERATIVE_FX,
2150
+ audio: AUDIO_FX,
2151
+ cursor: CURSOR_FX,
2152
+ micro: MICRO_FX,
2153
+ themes: THEME_FX,
2154
+ };
2155
+ /** 5.1: card & click effects 2.0. */
2156
+ function registerCardClickEffects() {
2157
+ registerEffects(EFFECT_PACKS['cards-click']);
2158
+ }
2159
+ /** 5.2: bounce & physics micro-interactions. */
2160
+ function registerPhysicsEffects() {
2161
+ registerEffects(EFFECT_PACKS.physics);
2162
+ }
2163
+ /** 5.3: page-wide transitions and effects. */
2164
+ function registerPageEffects() {
2165
+ registerEffects(EFFECT_PACKS.page);
2166
+ }
2167
+ /** 5.5: generative Canvas 2D backgrounds. */
2168
+ function registerGenerativeEffects() {
2169
+ registerEffects(EFFECT_PACKS.generative);
2170
+ }
2171
+ /** 5.6: sound-reactive (Web Audio) backgrounds. */
2172
+ function registerAudioEffects() {
2173
+ registerEffects(EFFECT_PACKS.audio);
2174
+ }
2175
+ /** 5.7: cursor trails, magnetic dots, spotlight cursor. */
2176
+ function registerCursorEffects() {
2177
+ registerEffects(EFFECT_PACKS.cursor);
2178
+ }
2179
+ /** 5.8: micro-interactions + theme-pack effects. */
2180
+ function registerMicroEffects() {
2181
+ registerEffects(EFFECT_PACKS.micro);
2182
+ registerEffects(EFFECT_PACKS.themes);
2183
+ }
2184
+ /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
2185
+ function defineEffectElements() {
2186
+ defineStory();
2187
+ defineAudio();
2188
+ defineGestureFx();
2189
+ defineTheme();
2190
+ definePlayer();
2191
+ }
2192
+ /** Register the built-ins and every pack (idempotent). */
2193
+ function registerAllEffects() {
2194
+ registerBuiltinEffects();
2195
+ for (const defs of Object.values(EFFECT_PACKS))
2196
+ registerEffects(defs);
2197
+ }
2198
+
2199
+ export { ANIMATION_FORMAT, AUDIO_FX, CARD_FX, CLICK_FX, CURSOR_FX, EFFECT_PACKS, GENERATIVE_FX, GESTURES, MICRO_FX, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, THEMES, THEME_FX, THEME_NAMES, THEME_ROLES, angleDelta, applyTheme, bindBeat, bindGesture, bounceKeyframes, bumpCount, canvasBackground, createBeatDetector, createPlayer, defineAudio, defineEffectElements, defineGestureFx, definePlayer, defineStory, defineTheme, disableAudio, enableAudio, flingVelocity, formatCount, fxLayer, getAudio, hexRgb, normalizeAnimation, onBeat, playThemeEffect, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerCursorEffects, registerGenerativeEffects, registerMicroEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress, swapLabel, themeCss, themePreset, themeVars, togglePressed };
2200
+ //# sourceMappingURL=effects.js.map