@yewdiel21/motion-kit 0.0.0-stage → 0.9.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 (391) hide show
  1. package/NOTICE +38 -0
  2. package/README.md +80 -2
  3. package/bin/motion-kit.mjs +242 -0
  4. package/css.d.ts +2 -0
  5. package/dist/auto/index.d.ts +63 -0
  6. package/dist/auto/index.js +207 -0
  7. package/dist/auto/index.js.map +1 -0
  8. package/dist/chunk-2BQGFOA7.js +177 -0
  9. package/dist/chunk-2BQGFOA7.js.map +1 -0
  10. package/dist/chunk-34XYOXHG.js +2374 -0
  11. package/dist/chunk-34XYOXHG.js.map +1 -0
  12. package/dist/chunk-6OJAP6ZL.js +832 -0
  13. package/dist/chunk-6OJAP6ZL.js.map +1 -0
  14. package/dist/chunk-ABNRDDO7.js +28 -0
  15. package/dist/chunk-ABNRDDO7.js.map +1 -0
  16. package/dist/chunk-FS2HJVVY.js +3320 -0
  17. package/dist/chunk-FS2HJVVY.js.map +1 -0
  18. package/dist/chunk-J27KH2A2.js +157 -0
  19. package/dist/chunk-J27KH2A2.js.map +1 -0
  20. package/dist/chunk-KLY5BRRI.js +18 -0
  21. package/dist/chunk-KLY5BRRI.js.map +1 -0
  22. package/dist/chunk-Q4LVFNLN.js +1617 -0
  23. package/dist/chunk-Q4LVFNLN.js.map +1 -0
  24. package/dist/chunk-RFDIZYFX.js +304 -0
  25. package/dist/chunk-RFDIZYFX.js.map +1 -0
  26. package/dist/chunk-TGV27ZWM.js +17 -0
  27. package/dist/chunk-TGV27ZWM.js.map +1 -0
  28. package/dist/confetti-BSX1CLLV.d.ts +41 -0
  29. package/dist/css/components/chat-background.css +87 -0
  30. package/dist/css/components/chat.css +237 -0
  31. package/dist/css/components/cursor.css +178 -0
  32. package/dist/css/components/dialog.css +259 -0
  33. package/dist/css/components/island.css +301 -0
  34. package/dist/css/components/menu.css +105 -0
  35. package/dist/css/components/nav.css +1054 -0
  36. package/dist/css/components/steps.css +50 -0
  37. package/dist/css/components/theme-picker.css +115 -0
  38. package/dist/css/components/toast.css +115 -0
  39. package/dist/css/controls.css +950 -0
  40. package/dist/css/effects.css +587 -0
  41. package/dist/css/fonts.css +3 -0
  42. package/dist/css/themes/black-green.css +126 -0
  43. package/dist/css/themes/black.css +126 -0
  44. package/dist/css/themes/blue.css +126 -0
  45. package/dist/css/themes/brown.css +126 -0
  46. package/dist/css/themes/cold-blue.css +126 -0
  47. package/dist/css/themes/funny.css +126 -0
  48. package/dist/css/themes/gray.css +126 -0
  49. package/dist/css/themes/green.css +126 -0
  50. package/dist/css/themes/modern-1.css +126 -0
  51. package/dist/css/themes/neogreen.css +126 -0
  52. package/dist/css/themes/new.css +126 -0
  53. package/dist/css/themes/new2.css +126 -0
  54. package/dist/css/themes/oled.css +126 -0
  55. package/dist/css/themes/orange.css +126 -0
  56. package/dist/css/themes/pastel-pink.css +126 -0
  57. package/dist/css/themes/pink.css +126 -0
  58. package/dist/css/themes/purple.css +126 -0
  59. package/dist/css/themes/red.css +126 -0
  60. package/dist/css/themes/super-blue.css +126 -0
  61. package/dist/css/themes.css +2431 -0
  62. package/dist/css/tokens.css +452 -0
  63. package/dist/elements/index.d.ts +183 -0
  64. package/dist/elements/index.js +472 -0
  65. package/dist/elements/index.js.map +1 -0
  66. package/dist/engine-CUr9QijM.d.ts +46 -0
  67. package/dist/gl/index.d.ts +61 -0
  68. package/dist/gl/index.js +18 -0
  69. package/dist/gl/index.js.map +1 -0
  70. package/dist/index.css +5296 -0
  71. package/dist/index.d.ts +440 -0
  72. package/dist/index.js +390 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/react/index.d.ts +476 -0
  75. package/dist/react/index.js +969 -0
  76. package/dist/react/index.js.map +1 -0
  77. package/dist/reduced-BM4Ls0g5.d.ts +566 -0
  78. package/dist/reveal-WFUWDrei.d.ts +416 -0
  79. package/dist/store-Bqy_obnD.d.ts +195 -0
  80. package/dist/theme/generate.d.ts +18 -0
  81. package/dist/theme/generate.js +64 -0
  82. package/dist/theme/generate.js.map +1 -0
  83. package/dist/theme/index.d.ts +17 -0
  84. package/dist/theme/index.js +32 -0
  85. package/dist/theme/index.js.map +1 -0
  86. package/dist/theme-BFMoCzi6.d.ts +158 -0
  87. package/dist/types-BkS_raL8.d.ts +7 -0
  88. package/package.json +114 -3
  89. package/registry.json +469 -0
  90. package/skills/motion-kit/SKILL.md +189 -0
  91. package/skills/motion-kit/reference/components.md +86 -0
  92. package/skills/motion-kit/reference/design.md +92 -0
  93. package/skills/motion-kit/reference/recipes.md +172 -0
  94. package/src/auto/auto-init.ts +151 -0
  95. package/src/auto/index.ts +3 -0
  96. package/src/auto/parse-options.ts +68 -0
  97. package/src/components/chat/README.md +90 -0
  98. package/src/components/chat/chat.css +236 -0
  99. package/src/components/chat/element.ts +98 -0
  100. package/src/components/chat/engine.ts +295 -0
  101. package/src/components/chat/react.tsx +196 -0
  102. package/src/components/chat-background/README.md +50 -0
  103. package/src/components/chat-background/audio.ts +56 -0
  104. package/src/components/chat-background/chat-background.css +86 -0
  105. package/src/components/chat-background/element.ts +31 -0
  106. package/src/components/chat-background/engine.ts +108 -0
  107. package/src/components/chat-background/react.tsx +36 -0
  108. package/src/components/cursor/README.md +53 -0
  109. package/src/components/cursor/cursor.css +177 -0
  110. package/src/components/cursor/element.ts +23 -0
  111. package/src/components/cursor/engine.ts +254 -0
  112. package/src/components/cursor/react.tsx +26 -0
  113. package/src/components/dialog/README.md +106 -0
  114. package/src/components/dialog/dialog.css +258 -0
  115. package/src/components/dialog/element.ts +118 -0
  116. package/src/components/dialog/engine.ts +432 -0
  117. package/src/components/dialog/react.tsx +200 -0
  118. package/src/components/icon.tsx +10 -0
  119. package/src/components/icons.ts +24 -0
  120. package/src/components/island/README.md +91 -0
  121. package/src/components/island/element.ts +45 -0
  122. package/src/components/island/engine.ts +200 -0
  123. package/src/components/island/island.css +300 -0
  124. package/src/components/island/react.tsx +115 -0
  125. package/src/components/menu/README.md +67 -0
  126. package/src/components/menu/element.ts +55 -0
  127. package/src/components/menu/engine.ts +142 -0
  128. package/src/components/menu/menu.css +104 -0
  129. package/src/components/menu/react.tsx +82 -0
  130. package/src/components/nav/README.md +85 -0
  131. package/src/components/nav/element.ts +39 -0
  132. package/src/components/nav/engine.ts +164 -0
  133. package/src/components/nav/nav.css +1053 -0
  134. package/src/components/nav/react.tsx +90 -0
  135. package/src/components/steps/README.md +70 -0
  136. package/src/components/steps/element.ts +43 -0
  137. package/src/components/steps/engine.ts +185 -0
  138. package/src/components/steps/react.tsx +91 -0
  139. package/src/components/steps/steps.css +49 -0
  140. package/src/components/theme-picker/README.md +57 -0
  141. package/src/components/theme-picker/element.ts +20 -0
  142. package/src/components/theme-picker/engine.ts +91 -0
  143. package/src/components/theme-picker/react.tsx +31 -0
  144. package/src/components/theme-picker/theme-picker.css +114 -0
  145. package/src/components/toast/README.md +60 -0
  146. package/src/components/toast/element.ts +25 -0
  147. package/src/components/toast/engine.ts +483 -0
  148. package/src/components/toast/react.tsx +22 -0
  149. package/src/components/toast/store.ts +122 -0
  150. package/src/components/toast/toast.css +114 -0
  151. package/src/core/channels.ts +165 -0
  152. package/src/core/curves.ts +35 -0
  153. package/src/core/frame.ts +12 -0
  154. package/src/core/gsap.ts +65 -0
  155. package/src/core/loop.ts +44 -0
  156. package/src/core/math.ts +9 -0
  157. package/src/core/physics-spring.ts +59 -0
  158. package/src/core/pointer.ts +63 -0
  159. package/src/core/reduced.ts +21 -0
  160. package/src/core/scope.ts +70 -0
  161. package/src/core/scroll-track.ts +106 -0
  162. package/src/core/split.ts +68 -0
  163. package/src/core/spring.ts +22 -0
  164. package/src/core/styles.ts +31 -0
  165. package/src/core/timing.ts +204 -0
  166. package/src/core/types.ts +20 -0
  167. package/src/core/visible.ts +19 -0
  168. package/src/css/base.css +142 -0
  169. package/src/css/controls/README.md +122 -0
  170. package/src/css/controls/burger.css +67 -0
  171. package/src/css/controls/button.css +226 -0
  172. package/src/css/controls/checkbox.css +53 -0
  173. package/src/css/controls/chip.css +85 -0
  174. package/src/css/controls/field.css +124 -0
  175. package/src/css/controls/glass.css +220 -0
  176. package/src/css/controls/icon.css +26 -0
  177. package/src/css/controls/segmented.css +60 -0
  178. package/src/css/controls/switch.css +72 -0
  179. package/src/css/default-theme.css +123 -0
  180. package/src/css/effects/README.md +134 -0
  181. package/src/css/effects/anim.css +224 -0
  182. package/src/css/effects/fade.css +15 -0
  183. package/src/css/effects/float.css +64 -0
  184. package/src/css/effects/frost.css +62 -0
  185. package/src/css/effects/grain.css +58 -0
  186. package/src/css/effects/loading.css +60 -0
  187. package/src/css/effects/success.css +22 -0
  188. package/src/css/effects/text.css +67 -0
  189. package/src/css/fonts.css +3 -0
  190. package/src/css/states.css +51 -0
  191. package/src/css/themes/black-green.css +126 -0
  192. package/src/css/themes/black.css +126 -0
  193. package/src/css/themes/blue.css +126 -0
  194. package/src/css/themes/brown.css +126 -0
  195. package/src/css/themes/cold-blue.css +126 -0
  196. package/src/css/themes/funny.css +126 -0
  197. package/src/css/themes/gray.css +126 -0
  198. package/src/css/themes/green.css +126 -0
  199. package/src/css/themes/modern-1.css +126 -0
  200. package/src/css/themes/neogreen.css +126 -0
  201. package/src/css/themes/new.css +126 -0
  202. package/src/css/themes/new2.css +126 -0
  203. package/src/css/themes/oled.css +126 -0
  204. package/src/css/themes/orange.css +126 -0
  205. package/src/css/themes/pastel-pink.css +126 -0
  206. package/src/css/themes/pink.css +126 -0
  207. package/src/css/themes/purple.css +126 -0
  208. package/src/css/themes/red.css +126 -0
  209. package/src/css/themes/super-blue.css +126 -0
  210. package/src/css/tokens.css +182 -0
  211. package/src/elements/base.ts +76 -0
  212. package/src/elements/index.ts +48 -0
  213. package/src/gl/index.ts +15 -0
  214. package/src/gl/liquid-blend/README.md +23 -0
  215. package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
  216. package/src/gl/liquid-blend/shader.ts +9 -0
  217. package/src/gl/matcap/README.md +23 -0
  218. package/src/gl/matcap/create-matcap.ts +7 -0
  219. package/src/gl/matcap/shader.ts +16 -0
  220. package/src/gl/particle-hero/README.md +41 -0
  221. package/src/gl/particle-hero/create-particle-hero.ts +544 -0
  222. package/src/gl/particle-hero/particle-hero.css +19 -0
  223. package/src/gl/particle-hero/shaders.ts +84 -0
  224. package/src/gl/shader-blur/README.md +23 -0
  225. package/src/gl/shader-blur/create-shader-blur.ts +7 -0
  226. package/src/gl/shader-blur/shader.ts +11 -0
  227. package/src/gl/shader-grain/README.md +23 -0
  228. package/src/gl/shader-grain/create-shader-grain.ts +7 -0
  229. package/src/gl/shader-grain/shader.ts +6 -0
  230. package/src/gl/shared/create-shader-effect.ts +186 -0
  231. package/src/gl/shared/shader.css +3 -0
  232. package/src/gl/shared/shaders.ts +30 -0
  233. package/src/index.ts +177 -0
  234. package/src/motion/anchor.ts +45 -0
  235. package/src/motion/chat.ts +97 -0
  236. package/src/motion/confetti.ts +130 -0
  237. package/src/motion/count.ts +95 -0
  238. package/src/motion/details.ts +70 -0
  239. package/src/motion/drag-dismiss.ts +231 -0
  240. package/src/motion/effects.ts +81 -0
  241. package/src/motion/entrances.ts +27 -0
  242. package/src/motion/floating.ts +389 -0
  243. package/src/motion/flow.ts +115 -0
  244. package/src/motion/indicator.ts +44 -0
  245. package/src/motion/layout.ts +39 -0
  246. package/src/motion/liquid.ts +227 -0
  247. package/src/motion/morph.ts +540 -0
  248. package/src/motion/reveal.ts +112 -0
  249. package/src/motion/sheet.ts +65 -0
  250. package/src/motion/surface.ts +108 -0
  251. package/src/react/animated-number.tsx +30 -0
  252. package/src/react/flip-group.tsx +44 -0
  253. package/src/react/frost.tsx +33 -0
  254. package/src/react/index.ts +80 -0
  255. package/src/react/slot-text.tsx +44 -0
  256. package/src/react/theme-provider.tsx +42 -0
  257. package/src/react/use-card-dock.ts +9 -0
  258. package/src/react/use-cascade.ts +9 -0
  259. package/src/react/use-contrast.ts +9 -0
  260. package/src/react/use-counter.ts +8 -0
  261. package/src/react/use-curved-carousel.ts +10 -0
  262. package/src/react/use-depth-grid.ts +9 -0
  263. package/src/react/use-details.ts +7 -0
  264. package/src/react/use-dock-pill.ts +10 -0
  265. package/src/react/use-drag-ruler.ts +10 -0
  266. package/src/react/use-fill-text.ts +9 -0
  267. package/src/react/use-fit-text.ts +8 -0
  268. package/src/react/use-flow.ts +8 -0
  269. package/src/react/use-frame-zoom.ts +9 -0
  270. package/src/react/use-infinite-reel.ts +8 -0
  271. package/src/react/use-letters.ts +9 -0
  272. package/src/react/use-liquid-blend.ts +8 -0
  273. package/src/react/use-marquee.ts +9 -0
  274. package/src/react/use-mask-text.ts +9 -0
  275. package/src/react/use-matcap.ts +8 -0
  276. package/src/react/use-parallax.ts +9 -0
  277. package/src/react/use-particle-hero.ts +9 -0
  278. package/src/react/use-pile.ts +8 -0
  279. package/src/react/use-pop.ts +8 -0
  280. package/src/react/use-radial-menu.ts +10 -0
  281. package/src/react/use-recipe.ts +38 -0
  282. package/src/react/use-reflow.ts +9 -0
  283. package/src/react/use-repel-bento.ts +8 -0
  284. package/src/react/use-reveal.ts +8 -0
  285. package/src/react/use-rotator.ts +10 -0
  286. package/src/react/use-shader-blur.ts +8 -0
  287. package/src/react/use-shader-grain.ts +8 -0
  288. package/src/react/use-slot-text.ts +17 -0
  289. package/src/react/use-snap-strip.ts +9 -0
  290. package/src/react/use-sortable.ts +14 -0
  291. package/src/react/use-spring-cards.ts +8 -0
  292. package/src/react/use-stretch-footer.ts +20 -0
  293. package/src/react/use-tabs.ts +10 -0
  294. package/src/react/use-text-roll.ts +9 -0
  295. package/src/react/use-ticket-menu.ts +15 -0
  296. package/src/react/use-words.ts +8 -0
  297. package/src/recipes/card-dock/README.md +36 -0
  298. package/src/recipes/card-dock/card-dock.css +178 -0
  299. package/src/recipes/card-dock/create-card-dock.ts +97 -0
  300. package/src/recipes/cascade/README.md +33 -0
  301. package/src/recipes/cascade/create-cascade.ts +111 -0
  302. package/src/recipes/contrast/README.md +29 -0
  303. package/src/recipes/contrast/create-contrast.ts +68 -0
  304. package/src/recipes/counter/README.md +42 -0
  305. package/src/recipes/counter/create-counter.ts +84 -0
  306. package/src/recipes/curved-carousel/README.md +50 -0
  307. package/src/recipes/curved-carousel/carousel-math.ts +20 -0
  308. package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
  309. package/src/recipes/curved-carousel/curved-carousel.css +50 -0
  310. package/src/recipes/depth-grid/README.md +30 -0
  311. package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
  312. package/src/recipes/details/README.md +31 -0
  313. package/src/recipes/details/create-details.ts +27 -0
  314. package/src/recipes/dock-pill/README.md +23 -0
  315. package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
  316. package/src/recipes/dock-pill/dock-pill.css +5 -0
  317. package/src/recipes/drag-ruler/README.md +21 -0
  318. package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
  319. package/src/recipes/drag-ruler/drag-ruler.css +16 -0
  320. package/src/recipes/fill-text/README.md +27 -0
  321. package/src/recipes/fill-text/create-fill-text.ts +93 -0
  322. package/src/recipes/fit-text/README.md +37 -0
  323. package/src/recipes/fit-text/create-fit-text.ts +55 -0
  324. package/src/recipes/fit-text/fit-text.css +6 -0
  325. package/src/recipes/flow/README.md +46 -0
  326. package/src/recipes/flow/create-flow.ts +55 -0
  327. package/src/recipes/frame-zoom/README.md +29 -0
  328. package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
  329. package/src/recipes/infinite-reel/README.md +23 -0
  330. package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
  331. package/src/recipes/infinite-reel/infinite-reel.css +8 -0
  332. package/src/recipes/letters/README.md +30 -0
  333. package/src/recipes/letters/create-letters.ts +113 -0
  334. package/src/recipes/marquee/README.md +32 -0
  335. package/src/recipes/marquee/create-marquee.ts +74 -0
  336. package/src/recipes/marquee/marquee.css +41 -0
  337. package/src/recipes/mask-text/README.md +39 -0
  338. package/src/recipes/mask-text/create-mask-text.ts +134 -0
  339. package/src/recipes/parallax/README.md +31 -0
  340. package/src/recipes/parallax/create-parallax.ts +74 -0
  341. package/src/recipes/pile/README.md +48 -0
  342. package/src/recipes/pile/create-pile.ts +50 -0
  343. package/src/recipes/pile/pile-math.ts +27 -0
  344. package/src/recipes/pile/pile.css +63 -0
  345. package/src/recipes/pill/README.md +30 -0
  346. package/src/recipes/pill/pill.css +48 -0
  347. package/src/recipes/pop/README.md +36 -0
  348. package/src/recipes/pop/create-pop.ts +52 -0
  349. package/src/recipes/radial-menu/README.md +44 -0
  350. package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
  351. package/src/recipes/radial-menu/radial-menu.css +45 -0
  352. package/src/recipes/reflow/README.md +47 -0
  353. package/src/recipes/reflow/create-reflow.ts +49 -0
  354. package/src/recipes/repel-bento/README.md +21 -0
  355. package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
  356. package/src/recipes/repel-bento/repel-bento.css +3 -0
  357. package/src/recipes/reveal/README.md +36 -0
  358. package/src/recipes/reveal/create-reveal.ts +48 -0
  359. package/src/recipes/rotator/README.md +21 -0
  360. package/src/recipes/rotator/create-rotator.ts +66 -0
  361. package/src/recipes/rotator/rotator.css +3 -0
  362. package/src/recipes/slot-text/README.md +44 -0
  363. package/src/recipes/slot-text/create-slot-text.ts +244 -0
  364. package/src/recipes/slot-text/slot-text.css +42 -0
  365. package/src/recipes/snap-strip/README.md +31 -0
  366. package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
  367. package/src/recipes/snap-strip/snap-strip.css +23 -0
  368. package/src/recipes/sortable/README.md +88 -0
  369. package/src/recipes/sortable/create-sortable.ts +863 -0
  370. package/src/recipes/sortable/sortable.css +40 -0
  371. package/src/recipes/spring-cards/README.md +48 -0
  372. package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
  373. package/src/recipes/spring-cards/spring-cards.css +23 -0
  374. package/src/recipes/stretch-footer/README.md +44 -0
  375. package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
  376. package/src/recipes/stretch-footer/stretch-footer.css +28 -0
  377. package/src/recipes/tabs/README.md +39 -0
  378. package/src/recipes/tabs/create-tabs.ts +91 -0
  379. package/src/recipes/tabs/tabs.css +19 -0
  380. package/src/recipes/text-roll/README.md +35 -0
  381. package/src/recipes/text-roll/create-text-roll.ts +137 -0
  382. package/src/recipes/text-roll/text-roll.css +21 -0
  383. package/src/recipes/ticket-menu/README.md +24 -0
  384. package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
  385. package/src/recipes/words/README.md +43 -0
  386. package/src/recipes/words/create-words.ts +114 -0
  387. package/src/theme/css-format.ts +32 -0
  388. package/src/theme/generate.ts +72 -0
  389. package/src/theme/index.ts +15 -0
  390. package/src/theme/presets.ts +79 -0
  391. package/src/theme/theme.ts +119 -0
@@ -0,0 +1,540 @@
1
+ import { gsap, EASE, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { TIMING } from '../core/timing';
4
+ import { MORPH_STYLES, clearStyles } from '../core/styles';
5
+ import { liquidClose, liquidOpen } from './liquid';
6
+ import { backdropVeil, ghostOf, transparentColor } from './surface';
7
+
8
+ /*
9
+ * Container transform after Stepbro's ModalHost (ported from Kayro): a surface
10
+ * (dialog, menu, option list) grows out of the control that opened it and folds
11
+ * back into it. The surface itself is never scaled: it starts clipped to the
12
+ * opener's size and corners, moved onto the opener, and widens to its own box
13
+ * while the opener's colour fades into its own. A dialog's content travels as
14
+ * one group, scaled from the opener's box to its own while it sharpens and
15
+ * fades in. `data-mk-shared="id"` pieces (a title) fly between opener and surface.
16
+ */
17
+
18
+ const FACE = 'mk-morph-face';
19
+ const HIDDEN_ORIGIN = 'data-mk-morph-origin';
20
+ const SHARED = 'data-mk-shared';
21
+ const SHARED_HIDDEN = 'data-mk-shared-hidden';
22
+
23
+ const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), Math.max(min, max));
24
+
25
+ /** A computed radius in pixels (percentages resolve against `size`). */
26
+ const radiusOf = (value: string, size: number) => {
27
+ const parsed = Number.parseFloat(value) || 0;
28
+ return value.trim().endsWith('%') ? (parsed / 100) * size : parsed;
29
+ };
30
+
31
+ interface Shape {
32
+ /** Offset that puts the clipped area on the opener. */
33
+ x: number;
34
+ y: number;
35
+ /** Clip at the opener's size, and the full surface. */
36
+ from: string;
37
+ to: string;
38
+ /** The clipped area in the surface's own coordinates. */
39
+ left: number;
40
+ top: number;
41
+ width: number;
42
+ height: number;
43
+ }
44
+
45
+ /**
46
+ * Where the surface starts: the area of the opener's size nearest to it inside
47
+ * the surface, and the offset that moves that area onto the opener. Null when
48
+ * the opener is gone, tiny or off screen.
49
+ */
50
+ function shapeOf(surface: HTMLElement, origin: HTMLElement | null): Shape | null {
51
+ if (!origin?.isConnected) return null;
52
+ const start = origin.getBoundingClientRect();
53
+ const box = surface.getBoundingClientRect();
54
+ if (start.width < 4 || start.height < 4 || !box.width || !box.height) return null;
55
+ if (start.bottom < 0 || start.top > innerHeight || start.right < 0 || start.left > innerWidth) return null;
56
+ const width = Math.min(start.width, box.width);
57
+ const height = Math.min(start.height, box.height);
58
+ const left = clamp(start.left - box.left, 0, box.width - width);
59
+ const top = clamp(start.top - box.top, 0, box.height - height);
60
+ const corner = Math.min(radiusOf(getComputedStyle(origin).borderTopLeftRadius, start.height), width / 2, height / 2);
61
+ const radius = radiusOf(getComputedStyle(surface).borderTopLeftRadius, box.height);
62
+ return {
63
+ x: start.left + (start.width - width) / 2 - (box.left + left),
64
+ y: start.top + (start.height - height) / 2 - (box.top + top),
65
+ from: `inset(${top}px ${box.width - left - width}px ${box.height - top - height}px ${left}px round ${corner}px)`,
66
+ to: `inset(0px 0px 0px 0px round ${radius}px)`,
67
+ left,
68
+ top,
69
+ width,
70
+ height,
71
+ };
72
+ }
73
+
74
+ /** Properties that make a copy look like the opener wherever it is placed. */
75
+ const LOOK = [
76
+ 'display',
77
+ 'alignItems',
78
+ 'justifyContent',
79
+ 'gap',
80
+ 'padding',
81
+ 'borderRadius',
82
+ 'borderWidth',
83
+ 'borderStyle',
84
+ 'borderColor',
85
+ 'backgroundColor',
86
+ 'color',
87
+ 'fontFamily',
88
+ 'fontSize',
89
+ 'fontWeight',
90
+ 'lineHeight',
91
+ 'letterSpacing',
92
+ 'whiteSpace',
93
+ ] as const;
94
+
95
+ /**
96
+ * A layer over the surface that looks like the opener: its colour fills the
97
+ * surface and, for lists, a copy of its label sits where it was. Fading it out
98
+ * turns the opener into the surface.
99
+ */
100
+ function faceOf(surface: HTMLElement, origin: HTMLElement, shape: Shape, withCopy: boolean) {
101
+ const look = getComputedStyle(origin);
102
+ const face = document.createElement('div');
103
+ face.className = FACE;
104
+ face.setAttribute('aria-hidden', 'true');
105
+ face.inert = true;
106
+ if (!transparentColor(look.backgroundColor)) face.style.background = look.backgroundColor;
107
+ surface.append(face);
108
+ if (!withCopy) return { face, copy: null as HTMLElement | null };
109
+ const copy = document.createElement('span');
110
+ copy.innerHTML = origin.innerHTML;
111
+ for (const element of copy.querySelectorAll('[id], [name], [for]')) for (const name of ['id', 'name', 'for']) element.removeAttribute(name);
112
+ for (const property of LOOK) copy.style[property] = look[property];
113
+ Object.assign(copy.style, {
114
+ position: 'absolute',
115
+ left: `${shape.left}px`,
116
+ top: `${shape.top}px`,
117
+ width: `${shape.width}px`,
118
+ height: `${shape.height}px`,
119
+ boxSizing: 'border-box',
120
+ margin: '0',
121
+ overflow: 'hidden',
122
+ });
123
+ face.append(copy);
124
+ return { face, copy };
125
+ }
126
+
127
+ /** The morph each surface is playing, so a new one stops it first. */
128
+ const running = new WeakMap<HTMLElement, gsap.core.Animation>();
129
+
130
+ /** Whether a surface is opening or closing right now. */
131
+ export const isMorphing = (surface: HTMLElement) => running.has(surface);
132
+
133
+ function stop(surface: HTMLElement) {
134
+ running.get(surface)?.kill();
135
+ running.delete(surface);
136
+ gsap.killTweensOf(surface);
137
+ for (const face of surface.querySelectorAll(`:scope > .${FACE}`)) face.remove();
138
+ clearStyles([surface], MORPH_STYLES);
139
+ }
140
+
141
+ /** Stop a surface's morph now and put the surface and its content back to rest. */
142
+ export function stopMorph(surface: HTMLElement) {
143
+ stop(surface);
144
+ const parts = [...surface.querySelectorAll<HTMLElement>(':scope > *')].filter((element) => !element.classList.contains('mk-liquid'));
145
+ gsap.killTweensOf(parts);
146
+ clearStyles(parts, MORPH_STYLES);
147
+ }
148
+
149
+ /** Small controls disappear into the surface they open, like a shared element. */
150
+ const compact = (element: HTMLElement) => {
151
+ const box = element.getBoundingClientRect();
152
+ return box.width <= 360 && box.height <= 72;
153
+ };
154
+
155
+ /** Show a hidden opener again; with `fade` it comes back while its surface closes (0.3 s after 0.2 s). */
156
+ export function showOrigin(origin: HTMLElement | null | undefined, { fade = false }: { fade?: boolean } = {}) {
157
+ if (!origin?.hasAttribute(HIDDEN_ORIGIN)) return;
158
+ if (!fade || reduced()) {
159
+ origin.removeAttribute(HIDDEN_ORIGIN);
160
+ return;
161
+ }
162
+ const previous = origin.style.transition;
163
+ const { originShow, originShowDelay } = TIMING.dialog;
164
+ origin.style.transition = `opacity ${originShow}s cubic-bezier(0.215, 0.61, 0.355, 1) ${originShowDelay}s`;
165
+ origin.removeAttribute(HIDDEN_ORIGIN);
166
+ setTimeout(() => {
167
+ origin.style.transition = previous;
168
+ }, (originShow + originShowDelay) * 1000 + 50);
169
+ }
170
+
171
+ /** Scale and offset that fit the content, as one group, into the opener's box inside the surface. */
172
+ function groupOf(surface: HTMLElement, parts: HTMLElement[], shape: Shape) {
173
+ const box = surface.getBoundingClientRect();
174
+ for (const part of parts) {
175
+ const own = part.getBoundingClientRect();
176
+ gsap.set(part, { transformOrigin: `${box.left - own.left}px ${box.top - own.top}px` });
177
+ }
178
+ return {
179
+ x: shape.left,
180
+ y: shape.top,
181
+ scaleX: box.width ? shape.width / box.width : 1,
182
+ scaleY: box.height ? shape.height / box.height : 1,
183
+ };
184
+ }
185
+
186
+ const escape = (value: string) => (typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(value) : value.replace(/"/g, '\\"'));
187
+
188
+ function sharedPairs(origin: HTMLElement, surface: HTMLElement) {
189
+ return [origin, ...origin.querySelectorAll<HTMLElement>(`[${SHARED}]`)]
190
+ .filter((source) => source.hasAttribute(SHARED))
191
+ .map((source) => ({
192
+ source,
193
+ target: surface.querySelector<HTMLElement>(`[${SHARED}="${escape(source.getAttribute(SHARED) ?? '')}"]`),
194
+ }))
195
+ .filter((pair): pair is { source: HTMLElement; target: HTMLElement } => pair.target !== null);
196
+ }
197
+
198
+ /** Where an element's text sits: its box without padding and border. */
199
+ function textBox(element: HTMLElement) {
200
+ const box = element.getBoundingClientRect();
201
+ const style = getComputedStyle(element);
202
+ const px = (value: string) => parseFloat(value) || 0;
203
+ const left = box.left + px(style.borderLeftWidth) + px(style.paddingLeft);
204
+ const top = box.top + px(style.borderTopWidth) + px(style.paddingTop);
205
+ const bottom = box.bottom - px(style.borderBottomWidth) - px(style.paddingBottom);
206
+ return { left, top, height: Math.max(0, bottom - top), style };
207
+ }
208
+
209
+ const textOf = (element: HTMLElement) =>
210
+ element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
211
+ ? element.value || element.placeholder
212
+ : (element.firstChild?.textContent ?? element.textContent ?? '');
213
+
214
+ /** Fly a copy of `from`'s text onto `to` (place, size, colour) in the top layer. Resolves when it lands. */
215
+ function flyText(from: HTMLElement, to: HTMLElement, text: string, duration: number) {
216
+ return new Promise<void>((resolve) => {
217
+ const start = textBox(from);
218
+ const end = textBox(to);
219
+ const fromSize = parseFloat(start.style.fontSize) || 16;
220
+ const toSize = parseFloat(end.style.fontSize) || fromSize;
221
+ const scale = toSize / fromSize;
222
+ const ghost = document.createElement('div');
223
+ ghost.setAttribute('aria-hidden', 'true');
224
+ ghost.setAttribute('popover', 'manual');
225
+ ghost.textContent = text;
226
+ Object.assign(ghost.style, {
227
+ position: 'fixed',
228
+ inset: 'auto',
229
+ left: `${start.left}px`,
230
+ top: `${start.top}px`,
231
+ margin: '0',
232
+ padding: '0',
233
+ border: '0',
234
+ background: 'transparent',
235
+ overflow: 'visible',
236
+ whiteSpace: 'nowrap',
237
+ pointerEvents: 'none',
238
+ fontFamily: start.style.fontFamily,
239
+ fontSize: start.style.fontSize,
240
+ fontWeight: start.style.fontWeight,
241
+ lineHeight: `${start.height || fromSize * 1.2}px`,
242
+ letterSpacing: start.style.letterSpacing,
243
+ color: start.style.color,
244
+ transformOrigin: '0 0',
245
+ });
246
+ document.body.append(ghost);
247
+ if (typeof ghost.showPopover === 'function') ghost.showPopover();
248
+ const height = (start.height || fromSize * 1.2) * scale;
249
+ gsap.to(ghost, {
250
+ x: end.left - start.left,
251
+ y: end.top + end.height / 2 - height / 2 - start.top,
252
+ scale,
253
+ color: end.style.color,
254
+ duration,
255
+ ease: EASE.signature,
256
+ onComplete: () => {
257
+ ghost.remove();
258
+ resolve();
259
+ },
260
+ });
261
+ });
262
+ }
263
+
264
+ /** Show the opener's shared pieces again (a surface closed without its animation). */
265
+ export function releaseShared(origin: HTMLElement | null | undefined) {
266
+ if (!origin) return;
267
+ for (const element of [origin, ...origin.querySelectorAll(`[${SHARED_HIDDEN}]`)]) element.removeAttribute(SHARED_HIDDEN);
268
+ }
269
+
270
+ /** The opener's pieces travel into the surface; they stay hidden at home until it closes. */
271
+ export function shareInto(origin: HTMLElement, surface: HTMLElement, duration: number = TIMING.dialog.open) {
272
+ for (const { source, target } of sharedPairs(origin, surface)) {
273
+ const text = textOf(source);
274
+ source.setAttribute(SHARED_HIDDEN, '');
275
+ gsap.set(target, { opacity: 0 });
276
+ void flyText(source, target, text, duration).then(() => gsap.set(target, { clearProps: 'opacity' }));
277
+ }
278
+ }
279
+
280
+ /** The surface's pieces travel back home and show there again. */
281
+ export function shareBack(surface: HTMLElement, origin: HTMLElement, duration: number = TIMING.dialog.close) {
282
+ for (const { source, target } of sharedPairs(origin, surface)) {
283
+ gsap.set(target, { opacity: 0 });
284
+ void flyText(target, source, textOf(source), duration).then(() => source.removeAttribute(SHARED_HIDDEN));
285
+ }
286
+ }
287
+
288
+ export interface MorphOpenOptions {
289
+ duration?: number;
290
+ /** Keep a compact opener hidden while the surface is open; call showOrigin when it closes. */
291
+ hideOrigin?: boolean;
292
+ /** What enters inside the surface (selector); by default its children. */
293
+ content?: string;
294
+ /** Delay between content parts: an option list cascades instead of growing as one. */
295
+ stagger?: number;
296
+ liquid?: boolean;
297
+ }
298
+
299
+ const partsOf = (surface: HTMLElement, content: string | undefined) =>
300
+ [...surface.querySelectorAll<HTMLElement>(content ?? ':scope > *')].filter(
301
+ (element) => !element.classList.contains(FACE) && !element.classList.contains('mk-liquid'),
302
+ );
303
+
304
+ /**
305
+ * Open `surface` out of `origin`. Returns the timeline (null when reduced).
306
+ * Shape travels from the opener (0.7 s, signature) and takes the surface's
307
+ * colour over the first 0.3 s; the content grows from the opener's box out of
308
+ * an 8 px blur on the same curve. Without an opener on screen: scale .92 → 1, 0.35 s.
309
+ */
310
+ export function morphOpen(surface: HTMLElement, origin: HTMLElement | null, options: MorphOpenOptions = {}) {
311
+ registerMotion();
312
+ const { duration = TIMING.dialog.open, hideOrigin = false, content, stagger = 0, liquid = false } = options;
313
+ stop(surface);
314
+ if (reduced()) return null;
315
+ const parts = partsOf(surface, content);
316
+ if (liquid && !stagger && origin && shapeOf(surface, origin)) {
317
+ if (hideOrigin && compact(origin)) origin.setAttribute(HIDDEN_ORIGIN, '');
318
+ const settle = liquidOpen(surface, origin, parts);
319
+ const window = gsap.timeline({
320
+ onComplete: () => {
321
+ settle();
322
+ running.delete(surface);
323
+ },
324
+ onInterrupt: settle,
325
+ });
326
+ window.to({}, { duration: TIMING.liquid.safetyMs / 1000 });
327
+ running.set(surface, window);
328
+ return window;
329
+ }
330
+ const enter = stagger
331
+ ? { opacity: 1, y: 0, duration: TIMING.dialog.list, ease: 'power2.out', stagger, clearProps: 'opacity,transform' }
332
+ : { opacity: 1, filter: 'blur(0px)', duration, ease: EASE.signature, clearProps: 'opacity,filter' };
333
+ const enterFrom = stagger ? { opacity: 0, y: -6 } : { opacity: 0, filter: 'blur(8px)' };
334
+ const shape = shapeOf(surface, origin);
335
+ if (!shape || !origin) {
336
+ const rise = gsap
337
+ .timeline({
338
+ onComplete: () => {
339
+ running.delete(surface);
340
+ },
341
+ })
342
+ .fromTo(
343
+ surface,
344
+ { opacity: 0, scale: 0.92 },
345
+ { opacity: 1, scale: 1, duration: TIMING.dialog.fallback, ease: 'power2.out', clearProps: 'opacity,transform' },
346
+ );
347
+ if (parts.length) rise.fromTo(parts, enterFrom, { ...enter, duration: Math.min(enter.duration, TIMING.dialog.fallback) }, 0);
348
+ running.set(surface, rise);
349
+ return rise;
350
+ }
351
+ const list = Boolean(stagger);
352
+ if (!list) shareInto(origin, surface, duration);
353
+ const { face, copy } = faceOf(surface, origin, shape, list);
354
+ const group = list ? null : groupOf(surface, parts, shape);
355
+ if (hideOrigin && compact(origin)) origin.setAttribute(HIDDEN_ORIGIN, '');
356
+ const finish = () => {
357
+ face.remove();
358
+ running.delete(surface);
359
+ gsap.set(surface, { clearProps: 'transform,clipPath,willChange' });
360
+ if (parts.length) gsap.set(parts, { clearProps: 'opacity,transform,transformOrigin,filter' });
361
+ };
362
+ const timeline = gsap
363
+ .timeline({ onComplete: finish, onInterrupt: finish })
364
+ .fromTo(
365
+ surface,
366
+ { x: shape.x, y: shape.y, clipPath: shape.from, willChange: 'transform,clip-path' },
367
+ { x: 0, y: 0, clipPath: shape.to, duration, ease: EASE.signature },
368
+ 0,
369
+ )
370
+ // The opener's colour hands over early, so a light button never flashes over a dark form.
371
+ .to(face, { opacity: 0, duration: TIMING.dialog.surfaceColor, ease: EASE.signature }, 0);
372
+ if (copy) timeline.to(copy, { opacity: 0, duration: 0.25, ease: 'power2.out' }, 0.05);
373
+ if (parts.length) {
374
+ if (group) {
375
+ timeline.fromTo(
376
+ parts,
377
+ { ...group, opacity: 0, filter: 'blur(8px)' },
378
+ { x: 0, y: 0, scaleX: 1, scaleY: 1, opacity: 1, filter: 'blur(0px)', duration, ease: EASE.signature },
379
+ 0,
380
+ );
381
+ } else timeline.fromTo(parts, enterFrom, enter, duration * 0.2);
382
+ }
383
+ running.set(surface, timeline);
384
+ return timeline;
385
+ }
386
+
387
+ export interface MorphCloseOptions {
388
+ duration?: number;
389
+ content?: string;
390
+ liquid?: boolean;
391
+ }
392
+
393
+ /**
394
+ * Fold `surface` back into `origin`, then call `onComplete` (which hides it).
395
+ * Without an opener on screen it closes in place: 90 % scale, 32 px blur.
396
+ */
397
+ export function morphClose(surface: HTMLElement, origin: HTMLElement | null, onComplete: () => void, options: MorphCloseOptions = {}) {
398
+ registerMotion();
399
+ const { duration = TIMING.dialog.close, content, liquid = false } = options;
400
+ stop(surface);
401
+ if (reduced()) {
402
+ onComplete();
403
+ return null;
404
+ }
405
+ const parts = partsOf(surface, content);
406
+ const shape = shapeOf(surface, origin);
407
+ if (!shape || !origin) {
408
+ const fade = gsap
409
+ .timeline({
410
+ onComplete: () => {
411
+ running.delete(surface);
412
+ onComplete();
413
+ gsap.set(surface, { clearProps: 'opacity,transform' });
414
+ if (parts.length) gsap.set(parts, { clearProps: 'filter' });
415
+ },
416
+ })
417
+ .to(surface, { scale: 0.9, duration, ease: EASE.signature }, 0)
418
+ .to(surface, { opacity: 0, duration, ease: EASE.fadeAway }, 0);
419
+ if (parts.length) fade.to(parts, { filter: 'blur(32px)', duration, ease: EASE.blurAway }, 0);
420
+ running.set(surface, fade);
421
+ return fade;
422
+ }
423
+ const list = content !== undefined;
424
+ if (liquid && !list) {
425
+ liquidClose(surface, origin, parts, onComplete);
426
+ return null;
427
+ }
428
+ const { face, copy } = faceOf(surface, origin, shape, list);
429
+ gsap.set(copy ? [face, copy] : face, { opacity: 0 });
430
+ if (!list) {
431
+ // The shape folds back into the opener and takes its colour (0.5 s); the
432
+ // content heads for the opener on a 2 s path while it fades and blurs away
433
+ // within the 0.5 s, and the surface fades out (power3.in).
434
+ shareBack(surface, origin, duration);
435
+ const group = groupOf(surface, parts, shape);
436
+ const signature = gsap.parseEase(EASE.signature);
437
+ const toward = (progress: number) => signature(progress * (duration / TIMING.dialog.contentReturn));
438
+ const timeline = gsap
439
+ .timeline({
440
+ onComplete: () => {
441
+ running.delete(surface);
442
+ onComplete();
443
+ face.remove();
444
+ gsap.set(surface, { clearProps: 'transform,clipPath,opacity,willChange' });
445
+ if (parts.length) gsap.set(parts, { clearProps: 'opacity,transform,transformOrigin,filter,borderRadius' });
446
+ },
447
+ onInterrupt: () => {
448
+ face.remove();
449
+ if (parts.length) gsap.set(parts, { clearProps: 'opacity,transform,transformOrigin,filter,borderRadius' });
450
+ },
451
+ })
452
+ .fromTo(
453
+ surface,
454
+ { clipPath: shape.to, willChange: 'transform,clip-path' },
455
+ { x: shape.x, y: shape.y, clipPath: shape.from, duration, ease: EASE.signature },
456
+ 0,
457
+ )
458
+ .to(face, { opacity: 1, duration, ease: EASE.signature }, 0)
459
+ .to(surface, { opacity: 0, duration, ease: 'power3.in' }, 0);
460
+ if (parts.length) {
461
+ timeline
462
+ .to(parts, { ...group, duration, ease: toward }, 0)
463
+ // Stepbro rounds the content toward a 400 px radius while it fades and blurs.
464
+ .to(parts, { opacity: 0, borderRadius: '400px', duration, ease: EASE.fadeAway }, 0)
465
+ .to(parts, { filter: 'blur(32px)', duration, ease: EASE.blurAway }, 0);
466
+ }
467
+ running.set(surface, timeline);
468
+ return timeline;
469
+ }
470
+ // A list: its content dissolves while the shape travels back and takes the label back.
471
+ const timeline = gsap
472
+ .timeline({
473
+ onComplete: () => {
474
+ running.delete(surface);
475
+ onComplete();
476
+ face.remove();
477
+ gsap.set(surface, { clearProps: 'transform,clipPath,willChange' });
478
+ if (parts.length) gsap.set(parts, { clearProps: 'opacity,transform,filter' });
479
+ },
480
+ onInterrupt: () => {
481
+ face.remove();
482
+ if (parts.length) gsap.set(parts, { clearProps: 'opacity,transform,filter' });
483
+ },
484
+ })
485
+ .fromTo(
486
+ surface,
487
+ { clipPath: shape.to, willChange: 'transform,clip-path' },
488
+ { x: shape.x, y: shape.y, clipPath: shape.from, duration, ease: EASE.signature },
489
+ 0,
490
+ )
491
+ .to(face, { opacity: 1, duration: duration * 0.5, ease: 'power1.in' }, duration * 0.3);
492
+ if (parts.length) timeline.to(parts, { opacity: 0, filter: 'blur(32px)', duration: duration * 0.7, ease: 'power2.in' }, 0);
493
+ if (copy) timeline.to(copy, { opacity: 1, duration: duration * 0.3, ease: 'power1.in' }, duration * 0.65);
494
+ running.set(surface, timeline);
495
+ return timeline;
496
+ }
497
+
498
+ /**
499
+ * Close a modal at once (focus returns, the page takes input) while an inert
500
+ * copy folds back into the opener over a fading veil. `inPlace` (after a drag)
501
+ * closes it where it was dropped instead of folding into the opener.
502
+ */
503
+ export function morphCloseWithGhost(
504
+ surface: HTMLElement,
505
+ origin: HTMLElement | null,
506
+ { inPlace = false, liquid = false }: { inPlace?: boolean; liquid?: boolean } = {},
507
+ ) {
508
+ registerMotion();
509
+ const dragged = inPlace
510
+ ? {
511
+ x: Number(gsap.getProperty(surface, 'x')) || 0,
512
+ y: Number(gsap.getProperty(surface, 'y')) || 0,
513
+ scale: Number(gsap.getProperty(surface, 'scaleX')) || 1,
514
+ }
515
+ : null;
516
+ if (dragged) gsap.set(surface, { clearProps: 'transform' });
517
+ const ghost = reduced() ? null : ghostOf(surface);
518
+ if (!ghost) {
519
+ showOrigin(origin);
520
+ releaseShared(origin);
521
+ return;
522
+ }
523
+ // The veil copies the modal's backdrop (tint and blur) and fades with the copy.
524
+ const veil = backdropVeil(surface);
525
+ document.body.append(veil, ghost);
526
+ showOrigin(origin, { fade: true });
527
+ gsap.to(veil, { opacity: 0, duration: TIMING.dialog.close, ease: 'power3.in' });
528
+ if (dragged) gsap.set(ghost, { ...dragged, transformOrigin: getComputedStyle(surface).transformOrigin });
529
+ morphClose(
530
+ ghost,
531
+ dragged ? null : origin,
532
+ () => {
533
+ showOrigin(origin);
534
+ releaseShared(origin);
535
+ ghost.remove();
536
+ veil.remove();
537
+ },
538
+ { liquid },
539
+ );
540
+ }
@@ -0,0 +1,112 @@
1
+ import { gsap, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { pointer } from '../core/pointer';
4
+ import { TIMING } from '../core/timing';
5
+
6
+ /*
7
+ * Stepbro's reveal (PrettyCalendarMenu, Nav, SupportModal): a veil in the
8
+ * surface's own colour covers it and a round opening grows from the origin,
9
+ * uncovering the content like a wave; then the veil fades. Overshoot 2 lets the
10
+ * opening pass the far corner halfway through, so the end is soft.
11
+ */
12
+
13
+ export type RevealSpeed = keyof typeof TIMING.reveal;
14
+
15
+ export interface RevealOptions {
16
+ /** Element (its centre) or viewport point. Default: the last pointer position, else the centre. */
17
+ origin?: Element | { x: number; y: number } | null;
18
+ speed?: RevealSpeed;
19
+ delay?: number;
20
+ overshoot?: number;
21
+ /** Veil colour. Default: the first opaque background up the tree, else var(--mk-bg). */
22
+ fill?: string;
23
+ }
24
+
25
+ export interface RevealHandle {
26
+ timeline: gsap.core.Timeline;
27
+ finished: Promise<void>;
28
+ /** Stop now and remove the veil. */
29
+ cancel(): void;
30
+ }
31
+
32
+ const TRANSPARENT = /^(transparent|rgba\(0, 0, 0, 0\))$/;
33
+
34
+ function backgroundOf(el: Element): string {
35
+ for (let node: Element | null = el; node; node = node.parentElement) {
36
+ const color = getComputedStyle(node).backgroundColor;
37
+ if (color && !TRANSPARENT.test(color)) return color;
38
+ }
39
+ return 'var(--mk-bg, #fff)';
40
+ }
41
+
42
+ export function revealFrom(target: HTMLElement, options: RevealOptions = {}): RevealHandle | null {
43
+ if (reduced() || !target.isConnected) return null;
44
+ registerMotion();
45
+ const { speed = 'normal', delay = 0, overshoot = 2 } = options;
46
+ const style = getComputedStyle(target);
47
+ const fill = options.fill ?? backgroundOf(target);
48
+ const position = target.style.position;
49
+ if (style.position === 'static') target.style.position = 'relative';
50
+
51
+ const box = target.getBoundingClientRect();
52
+ const width = target.clientWidth;
53
+ const height = target.clientHeight;
54
+ let point: { x: number; y: number } | null = null;
55
+ const origin = options.origin;
56
+ if (origin instanceof Element) {
57
+ if (origin.isConnected) {
58
+ const rect = origin.getBoundingClientRect();
59
+ point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
60
+ }
61
+ } else if (origin) point = origin;
62
+ else if (origin === undefined && pointer.active) point = { x: pointer.x, y: pointer.y };
63
+ const x = point ? point.x - box.left - target.clientLeft : width / 2;
64
+ const y = point ? point.y - box.top - target.clientTop : height / 2;
65
+ const radius =
66
+ Math.max(Math.hypot(x, y), Math.hypot(width - x, y), Math.hypot(x, height - y), Math.hypot(width - x, height - y)) * overshoot;
67
+
68
+ const veil = document.createElement('div');
69
+ veil.setAttribute('aria-hidden', 'true');
70
+ veil.dataset.mkVeil = '';
71
+ Object.assign(veil.style, {
72
+ position: 'absolute',
73
+ top: `${target.scrollTop}px`,
74
+ left: `${target.scrollLeft}px`,
75
+ width: `${width}px`,
76
+ height: `${height}px`,
77
+ borderRadius: style.borderRadius,
78
+ pointerEvents: 'none',
79
+ zIndex: '1000',
80
+ });
81
+ const progress = { value: 0 };
82
+ const paint = () => {
83
+ veil.style.background = `radial-gradient(${radius}px at ${x}px ${y}px, transparent 0%, ${fill} ${progress.value}%)`;
84
+ };
85
+ paint();
86
+ target.append(veil);
87
+
88
+ let done = false;
89
+ let resolve!: () => void;
90
+ const finished = new Promise<void>((r) => (resolve = r));
91
+ const cleanup = () => {
92
+ if (done) return;
93
+ done = true;
94
+ veil.remove();
95
+ target.style.position = position;
96
+ resolve();
97
+ };
98
+ const [duration, fade] = TIMING.reveal[speed];
99
+ const timeline = gsap
100
+ .timeline({ delay, onComplete: cleanup, onInterrupt: cleanup })
101
+ .to(progress, { value: 100, duration, ease: 'power2.out', onUpdate: paint }, 0)
102
+ .to(veil, { opacity: 0, duration: fade, ease: 'power1.out' }, Math.max(0, duration * Math.min(1, 1 / overshoot) - 0.1));
103
+
104
+ return {
105
+ timeline,
106
+ finished,
107
+ cancel() {
108
+ timeline.kill();
109
+ cleanup();
110
+ },
111
+ };
112
+ }