@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,389 @@
1
+ import { gsap, EASE, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { TIMING } from '../core/timing';
4
+
5
+ /*
6
+ * Stepbro's floating modal (ported from Kayro): on desktop (≥ 680 px) a handle
7
+ * lifts a surface into a window that moves on a spring (1100/38, mass by area),
8
+ * glides when flung (≥ 350 px/s, decays in 0.18 s), stays inside the viewport
9
+ * (8 px margin), resizes from its corners (min 320 × 240) and remembers where it
10
+ * was left (localStorage `mk-float.<key>`). A press on the handle without
11
+ * moving toggles floating.
12
+ */
13
+
14
+ const MIN_WIDTH = 320;
15
+ const MIN_HEIGHT = 240;
16
+ const MARGIN = 8;
17
+ const DESKTOP = 680;
18
+ const LIFT = 4;
19
+ const SPRING = { stiffness: 1100, damping: 38 };
20
+ const MASS_AREA = 24_000;
21
+ const MAX_MASS = 1.5;
22
+ const REST_SPEED = 24;
23
+ const FLING_SPEED = 350;
24
+ const POINTER_SMOOTHING = 0.05;
25
+ const SHRINK_FULL = 0.85;
26
+ const FLOAT_SHADOW = 'rgba(0, 0, 0, 0.24) 0px 0px 24px -6px';
27
+
28
+ export const floatingAvailable = () => typeof innerWidth === 'number' && innerWidth >= DESKTOP;
29
+
30
+ export interface Rect {
31
+ top: number;
32
+ left: number;
33
+ width: number;
34
+ height: number;
35
+ }
36
+
37
+ /** A window kept inside the viewport, at least 320 × 240. */
38
+ export function clampRect(rect: Rect, viewport = { width: innerWidth, height: innerHeight }): Rect {
39
+ const maxWidth = Math.max(MIN_WIDTH, viewport.width - MARGIN * 2);
40
+ const maxHeight = Math.max(MIN_HEIGHT, viewport.height - MARGIN * 2);
41
+ const width = Math.min(maxWidth, Math.max(MIN_WIDTH, rect.width));
42
+ const height = Math.min(maxHeight, Math.max(MIN_HEIGHT, rect.height));
43
+ return {
44
+ top: Math.min(Math.max(MARGIN, viewport.height - height - MARGIN), Math.max(MARGIN, rect.top)),
45
+ left: Math.min(Math.max(MARGIN, viewport.width - width - MARGIN), Math.max(MARGIN, rect.left)),
46
+ width,
47
+ height,
48
+ };
49
+ }
50
+
51
+ const rectOf = (element: HTMLElement): Rect => {
52
+ const box = element.getBoundingClientRect();
53
+ return { top: box.top, left: box.left, width: box.width, height: box.height };
54
+ };
55
+
56
+ const place = (element: HTMLElement, rect: Rect) =>
57
+ Object.assign(element.style, {
58
+ position: 'fixed',
59
+ inset: 'auto',
60
+ margin: '0',
61
+ top: `${rect.top}px`,
62
+ left: `${rect.left}px`,
63
+ width: `${rect.width}px`,
64
+ height: `${rect.height}px`,
65
+ maxWidth: 'none',
66
+ maxHeight: 'none',
67
+ });
68
+
69
+ const storageKey = (key: string) => `mk-float.${key}`;
70
+
71
+ /** Where a floating window was left, if this device remembers it. */
72
+ export function savedFloat(key: string): Rect | null {
73
+ try {
74
+ const value = JSON.parse(localStorage.getItem(storageKey(key)) ?? 'null') as Rect | null;
75
+ return value && (['top', 'left', 'width', 'height'] as const).every((name) => Number.isFinite(value[name])) ? clampRect(value) : null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function saveFloat(key: string, rect: Rect) {
82
+ try {
83
+ localStorage.setItem(
84
+ storageKey(key),
85
+ JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left), width: Math.round(rect.width), height: Math.round(rect.height) }),
86
+ );
87
+ } catch {
88
+ // A convenience only.
89
+ }
90
+ }
91
+
92
+ export function forgetFloat(key: string) {
93
+ try {
94
+ localStorage.removeItem(storageKey(key));
95
+ } catch {
96
+ // A convenience only.
97
+ }
98
+ }
99
+
100
+ /** Lift a surface into a window at `rect` (its shadow eases in, 0.45 s). */
101
+ export function liftWindow(surface: HTMLElement, rect: Rect) {
102
+ registerMotion();
103
+ place(surface, clampRect(rect));
104
+ surface.classList.add('is-floating');
105
+ if (!reduced()) {
106
+ gsap.fromTo(
107
+ surface,
108
+ { boxShadow: 'rgba(0, 0, 0, 0) 0px 0px 0px 0px' },
109
+ { boxShadow: FLOAT_SHADOW, duration: TIMING.float.shadow, ease: EASE.signature, clearProps: 'boxShadow' },
110
+ );
111
+ }
112
+ }
113
+
114
+ const PLACED = [
115
+ 'position',
116
+ 'inset',
117
+ 'right',
118
+ 'bottom',
119
+ 'margin',
120
+ 'margin-top',
121
+ 'margin-right',
122
+ 'margin-bottom',
123
+ 'margin-left',
124
+ 'top',
125
+ 'left',
126
+ 'width',
127
+ 'height',
128
+ 'max-width',
129
+ 'max-height',
130
+ ];
131
+
132
+ /** Remove what liftWindow wrote (no animation). */
133
+ export function unplaceWindow(surface: HTMLElement) {
134
+ surface.classList.remove('is-floating');
135
+ for (const name of PLACED) surface.style.removeProperty(name);
136
+ }
137
+
138
+ /** Put a floating surface back in its place, travelling from where it floated. */
139
+ export function dockWindow(surface: HTMLElement, from: Rect) {
140
+ registerMotion();
141
+ unplaceWindow(surface);
142
+ if (reduced()) return;
143
+ const to = rectOf(surface);
144
+ gsap.from(surface, { x: from.left - to.left, y: from.top - to.top, duration: TIMING.float.shadow, ease: EASE.signature, clearProps: 'transform' });
145
+ }
146
+
147
+ export interface FloatingOptions {
148
+ key: string;
149
+ handle: HTMLElement;
150
+ corners?: HTMLElement[];
151
+ isFloating: () => boolean;
152
+ float: () => void;
153
+ dock: () => void;
154
+ }
155
+
156
+ /** Move a floating surface by its handle and resize it from its corners. Returns the detach function. */
157
+ export function attachFloating(surface: HTMLElement, { key, handle, corners = [], isFloating, float, dock }: FloatingOptions) {
158
+ const cleanups: Array<() => void> = [];
159
+ const listen = (element: HTMLElement, listener: (event: PointerEvent) => void) => {
160
+ element.addEventListener('pointerdown', listener);
161
+ cleanups.push(() => element.removeEventListener('pointerdown', listener));
162
+ };
163
+ let active: number | null = null;
164
+ let release: (() => void) | null = null;
165
+ /**
166
+ * Follow one pointer until it is released. Moves and the release are heard on
167
+ * the window, not only through pointer capture: the window moves under the
168
+ * pointer, and where capture does not hold the gesture must still end.
169
+ */
170
+ const capture = (element: HTMLElement, down: PointerEvent, { onMove, onUp }: { onMove: (event: PointerEvent) => void; onUp: () => void }) => {
171
+ release?.();
172
+ active = down.pointerId;
173
+ try {
174
+ element.setPointerCapture?.(down.pointerId);
175
+ } catch {
176
+ // The window listeners below still follow the pointer.
177
+ }
178
+ const move = (event: PointerEvent) => {
179
+ if (event.pointerId === active) onMove(event);
180
+ };
181
+ const up = (event: PointerEvent) => {
182
+ if (event.pointerId !== active) return;
183
+ stop();
184
+ onUp();
185
+ };
186
+ const stop = () => {
187
+ active = null;
188
+ release = null;
189
+ window.removeEventListener('pointermove', move);
190
+ window.removeEventListener('pointerup', up);
191
+ window.removeEventListener('pointercancel', up);
192
+ };
193
+ release = stop;
194
+ window.addEventListener('pointermove', move);
195
+ window.addEventListener('pointerup', up);
196
+ window.addEventListener('pointercancel', up);
197
+ };
198
+ cleanups.push(() => release?.());
199
+
200
+ type Body = {
201
+ x: number;
202
+ y: number;
203
+ vx: number;
204
+ vy: number;
205
+ w: number;
206
+ h: number;
207
+ targetX: number;
208
+ targetY: number;
209
+ targetW: number;
210
+ targetH: number;
211
+ mass: number;
212
+ time: number;
213
+ gliding: boolean;
214
+ fromX: number;
215
+ fromY: number;
216
+ pointerVx: number;
217
+ pointerVy: number;
218
+ pointerX: number;
219
+ pointerY: number;
220
+ pointerTime: number;
221
+ };
222
+ let body: Body | null = null;
223
+ const settle = () => {
224
+ if (!body) return;
225
+ gsap.ticker.remove(tick);
226
+ body = null;
227
+ };
228
+ function tick() {
229
+ if (!body) return;
230
+ const now = performance.now();
231
+ const elapsed = Math.min((now - body.time) / 1000, 1 / 30);
232
+ if (elapsed <= 0) return;
233
+ body.time = now;
234
+ if (body.gliding) {
235
+ const decay = Math.exp(-elapsed / TIMING.float.glideDecay);
236
+ body.vx *= decay;
237
+ body.vy *= decay;
238
+ } else {
239
+ body.vx += (((body.targetX - body.x) * SPRING.stiffness - body.vx * SPRING.damping) * elapsed) / body.mass;
240
+ body.vy += (((body.targetY - body.y) * SPRING.stiffness - body.vy * SPRING.damping) * elapsed) / body.mass;
241
+ }
242
+ body.x += body.vx * elapsed;
243
+ body.y += body.vy * elapsed;
244
+ const smoothing = 1 - Math.exp(-elapsed / TIMING.float.sizeSmoothing);
245
+ body.w += (body.targetW - body.w) * smoothing;
246
+ body.h += (body.targetH - body.h) * smoothing;
247
+ if (Math.abs(body.targetW - body.w) < 0.5) body.w = body.targetW;
248
+ if (Math.abs(body.targetH - body.h) < 0.5) body.h = body.targetH;
249
+ const rect = clampRect({ top: body.y, left: body.x, width: body.w, height: body.h });
250
+ if (rect.left !== body.x) {
251
+ body.x = rect.left;
252
+ body.vx = 0;
253
+ }
254
+ if (rect.top !== body.y) {
255
+ body.y = rect.top;
256
+ body.vy = 0;
257
+ }
258
+ place(surface, rect);
259
+ const resting = Math.hypot(body.vx, body.vy) < REST_SPEED && body.w === body.targetW && body.h === body.targetH;
260
+ if (body.gliding && resting) {
261
+ saveFloat(key, rect);
262
+ settle();
263
+ }
264
+ }
265
+
266
+ listen(handle, (down) => {
267
+ if (down.button !== 0 || active !== null || !floatingAvailable()) return;
268
+ down.preventDefault();
269
+ settle();
270
+ let moved = false;
271
+ capture(handle, down, {
272
+ onMove(event) {
273
+ const dx = event.clientX - down.clientX;
274
+ const dy = event.clientY - down.clientY;
275
+ if (!moved) {
276
+ if (Math.abs(dx) < LIFT && Math.abs(dy) < LIFT) return;
277
+ moved = true;
278
+ if (!isFloating()) float();
279
+ const rect = rectOf(surface);
280
+ const fullWidth = innerWidth - MARGIN * 2;
281
+ const fullHeight = innerHeight - MARGIN * 2;
282
+ // A window as large as the screen shrinks so it can be moved.
283
+ const width = rect.width >= fullWidth - 2 ? Math.max(MIN_WIDTH, Math.round(fullWidth * SHRINK_FULL)) : rect.width;
284
+ const height = rect.height >= fullHeight - 2 ? Math.max(MIN_HEIGHT, Math.round(fullHeight * SHRINK_FULL)) : rect.height;
285
+ const grip = rect.width > 0 ? (down.clientX - rect.left) / rect.width : 0;
286
+ const left = rect.left + (rect.width - width) * grip;
287
+ const now = performance.now();
288
+ body = {
289
+ x: left,
290
+ y: rect.top,
291
+ vx: 0,
292
+ vy: 0,
293
+ w: rect.width,
294
+ h: rect.height,
295
+ targetX: left,
296
+ targetY: rect.top,
297
+ targetW: width,
298
+ targetH: height,
299
+ mass: Math.min(MAX_MASS, Math.max(1, Math.sqrt((rect.width * rect.height) / MASS_AREA))),
300
+ time: now,
301
+ gliding: false,
302
+ fromX: left,
303
+ fromY: rect.top,
304
+ pointerVx: 0,
305
+ pointerVy: 0,
306
+ pointerX: event.clientX,
307
+ pointerY: event.clientY,
308
+ pointerTime: now,
309
+ };
310
+ gsap.ticker.add(tick);
311
+ }
312
+ if (!body) return;
313
+ body.targetX = body.fromX + dx;
314
+ body.targetY = body.fromY + dy;
315
+ const now = performance.now();
316
+ const seconds = Math.max(1, now - body.pointerTime) / 1000;
317
+ const smoothing = 1 - Math.exp(-seconds / POINTER_SMOOTHING);
318
+ body.pointerVx += ((event.clientX - body.pointerX) / seconds - body.pointerVx) * smoothing;
319
+ body.pointerVy += ((event.clientY - body.pointerY) / seconds - body.pointerVy) * smoothing;
320
+ body.pointerX = event.clientX;
321
+ body.pointerY = event.clientY;
322
+ body.pointerTime = now;
323
+ },
324
+ onUp() {
325
+ if (!moved) {
326
+ if (isFloating()) dock();
327
+ else float();
328
+ return;
329
+ }
330
+ if (!body) return;
331
+ // A fling keeps gliding; a gentle release stays where it is.
332
+ const speed = performance.now() - body.pointerTime > 80 ? 0 : Math.hypot(body.pointerVx, body.pointerVy);
333
+ if (speed >= FLING_SPEED) {
334
+ body.vx = body.pointerVx;
335
+ body.vy = body.pointerVy;
336
+ } else {
337
+ body.vx = 0;
338
+ body.vy = 0;
339
+ body.targetX = body.x;
340
+ body.targetY = body.y;
341
+ }
342
+ body.gliding = true;
343
+ },
344
+ });
345
+ });
346
+
347
+ for (const corner of corners) {
348
+ listen(corner, (down) => {
349
+ if (down.button !== 0 || active !== null || !floatingAvailable()) return;
350
+ down.preventDefault();
351
+ settle();
352
+ const side = corner.dataset.mkFloatCorner ?? 'br';
353
+ const fromLeft = side.endsWith('l');
354
+ const fromTop = side.startsWith('t');
355
+ let start: Rect | null = null;
356
+ capture(corner, down, {
357
+ onMove(event) {
358
+ const dx = event.clientX - down.clientX;
359
+ const dy = event.clientY - down.clientY;
360
+ if (!start) {
361
+ if (Math.abs(dx) < LIFT && Math.abs(dy) < LIFT) return;
362
+ if (!isFloating()) float();
363
+ start = rectOf(surface);
364
+ }
365
+ const width = Math.min(innerWidth - MARGIN * 2, Math.max(MIN_WIDTH, fromLeft ? start.width - dx : start.width + dx));
366
+ const height = Math.min(innerHeight - MARGIN * 2, Math.max(MIN_HEIGHT, fromTop ? start.height - dy : start.height + dy));
367
+ place(
368
+ surface,
369
+ clampRect({
370
+ top: fromTop ? start.top + start.height - height : start.top,
371
+ left: fromLeft ? start.left + start.width - width : start.left,
372
+ width,
373
+ height,
374
+ }),
375
+ );
376
+ },
377
+ onUp() {
378
+ if (start) saveFloat(key, rectOf(surface));
379
+ },
380
+ });
381
+ });
382
+ }
383
+
384
+ return () => {
385
+ if (body?.gliding) saveFloat(key, rectOf(surface));
386
+ settle();
387
+ for (const cleanup of cleanups) cleanup();
388
+ };
389
+ }
@@ -0,0 +1,115 @@
1
+ import { gsap, EASE, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { TIMING } from '../core/timing';
4
+ import { nextFrame } from '../core/frame';
5
+ import { clearStyles } from '../core/styles';
6
+
7
+ /*
8
+ * Stepbro flowIn / flowOut: items drop in from 20 px above out of a 16 px blur,
9
+ * 0.5 s, 50 ms apart (`flowSoft`), and leave 20 px down into the same blur.
10
+ * Style 2 grows them from 90 % instead of moving them.
11
+ */
12
+
13
+ export interface FlowInOptions {
14
+ /** Distance travelled (style 1), px. */
15
+ y?: number;
16
+ blur?: number;
17
+ duration?: number;
18
+ stagger?: number;
19
+ delay?: number;
20
+ /** 1: drop from above (default). 2: grow from `startScale`. */
21
+ style?: 1 | 2;
22
+ startScale?: number;
23
+ /** Last item first. */
24
+ reverse?: boolean;
25
+ /** With a single container as target, animate its matches instead. */
26
+ target?: string;
27
+ ease?: string;
28
+ /**
29
+ * Hide with visibility as well as opacity (default). Off for controls that must
30
+ * take focus while they enter (menu options): a visibility:hidden element cannot.
31
+ */
32
+ autoAlpha?: boolean;
33
+ }
34
+
35
+ export interface FlowOutOptions {
36
+ y?: number;
37
+ blur?: number;
38
+ duration?: number;
39
+ stagger?: number;
40
+ delay?: number;
41
+ reverse?: boolean;
42
+ target?: string;
43
+ }
44
+
45
+ const CLEAR = 'transform,filter,opacity,visibility';
46
+
47
+ function list(targets: gsap.TweenTarget, target?: string, reverse?: boolean): HTMLElement[] {
48
+ let items: HTMLElement[];
49
+ if (target && targets instanceof Element) items = [...targets.querySelectorAll<HTMLElement>(target)];
50
+ else items = gsap.utils.toArray<HTMLElement>(targets);
51
+ return reverse ? items.reverse() : items;
52
+ }
53
+
54
+ /** Flow items in. Returns the tween, or null when there is nothing to animate or motion is reduced. */
55
+ export function flowIn(targets: gsap.TweenTarget, options: FlowInOptions = {}): gsap.core.Tween | null {
56
+ registerMotion();
57
+ const {
58
+ y = 20,
59
+ blur = 16,
60
+ duration = TIMING.flow,
61
+ stagger = TIMING.flowStagger,
62
+ delay = 0,
63
+ style = 1,
64
+ startScale = 0.9,
65
+ reverse = false,
66
+ target,
67
+ ease = EASE.flow,
68
+ autoAlpha = true,
69
+ } = options;
70
+ const items = list(targets, target, reverse);
71
+ if (!items.length) return null;
72
+ if (reduced()) {
73
+ // Not gsap.set(clearProps): it would move hidden items around to measure them.
74
+ clearStyles(items, ['transform', 'translate', 'rotate', 'scale', 'filter', 'opacity', 'visibility']);
75
+ return null;
76
+ }
77
+ const from = style === 2 ? { scale: startScale } : { y: -y };
78
+ const to = style === 2 ? { scale: 1 } : { y: 0 };
79
+ const alpha = autoAlpha ? 'autoAlpha' : 'opacity';
80
+ return gsap.fromTo(
81
+ items,
82
+ { ...from, [alpha]: 0, filter: `blur(${blur}px)` },
83
+ { ...to, [alpha]: 1, filter: 'blur(0px)', duration, stagger, delay, ease, clearProps: CLEAR },
84
+ );
85
+ }
86
+
87
+ /** Flow items out (down into a blur). They stay hidden afterwards; flowIn brings them back. */
88
+ export function flowOut(targets: gsap.TweenTarget, options: FlowOutOptions = {}): gsap.core.Tween | null {
89
+ registerMotion();
90
+ const { y = 20, blur = 16, duration = TIMING.flow, stagger = TIMING.flowStagger, delay = 0, reverse = false, target } = options;
91
+ const items = list(targets, target, reverse);
92
+ if (!items.length) return null;
93
+ if (reduced()) {
94
+ gsap.set(items, { autoAlpha: 0 });
95
+ return null;
96
+ }
97
+ return gsap.to(items, { y, autoAlpha: 0, filter: `blur(${blur}px)`, duration, stagger, delay, ease: EASE.flow });
98
+ }
99
+
100
+ /**
101
+ * Stepbro's step change: the current children flow out, `mutate` swaps the
102
+ * content (render the next step there), and after a frame the new children flow in.
103
+ */
104
+ export async function stepChange(
105
+ container: HTMLElement,
106
+ mutate: () => void | Promise<void>,
107
+ { duration = TIMING.stepChange }: { duration?: number } = {},
108
+ ): Promise<void> {
109
+ const before = [...container.children] as HTMLElement[];
110
+ const out = flowOut(before, { duration });
111
+ if (out) await out;
112
+ await mutate();
113
+ await nextFrame();
114
+ flowIn([...container.children] as HTMLElement[], { duration });
115
+ }
@@ -0,0 +1,44 @@
1
+ import { gsap, EASE, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { TIMING } from '../core/timing';
4
+
5
+ /*
6
+ * Stepbro's PrettyTabs: the indicator fits the selected option's box in 0.45 s
7
+ * on the signature curve (Flip.fit in the original; here the same box is
8
+ * tweened directly). The indicator is an absolutely positioned child at 0,0 of
9
+ * the group. A scrolling group centres the selected option.
10
+ */
11
+
12
+ export interface IndicatorOptions {
13
+ immediate?: boolean;
14
+ duration?: number;
15
+ /** Scroll container to centre the target in (when it overflows). */
16
+ scroller?: HTMLElement | null;
17
+ orientation?: 'horizontal' | 'vertical';
18
+ }
19
+
20
+ export function slideIndicator(indicator: HTMLElement, target: HTMLElement | null, options: IndicatorOptions = {}) {
21
+ registerMotion();
22
+ const { immediate = false, duration = TIMING.indicator, scroller, orientation = 'horizontal' } = options;
23
+ gsap.killTweensOf(indicator);
24
+ if (!target) {
25
+ gsap.set(indicator, { opacity: 0 });
26
+ return null;
27
+ }
28
+ const box = { x: target.offsetLeft, y: target.offsetTop, width: target.offsetWidth, height: target.offsetHeight, opacity: 1 };
29
+ if (scroller) {
30
+ const horizontal = orientation === 'horizontal';
31
+ const overflow = horizontal ? scroller.scrollWidth > scroller.clientWidth : scroller.scrollHeight > scroller.clientHeight;
32
+ if (overflow && typeof scroller.scrollTo === 'function') {
33
+ const offset = horizontal
34
+ ? box.x - (scroller.clientWidth - box.width) / 2
35
+ : box.y - (scroller.clientHeight - box.height) / 2;
36
+ scroller.scrollTo({ [horizontal ? 'left' : 'top']: offset, behavior: immediate || reduced() ? 'auto' : 'smooth' });
37
+ }
38
+ }
39
+ if (immediate || reduced()) {
40
+ gsap.set(indicator, box);
41
+ return null;
42
+ }
43
+ return gsap.to(indicator, { ...box, duration, ease: EASE.signature });
44
+ }
@@ -0,0 +1,39 @@
1
+ import { gsap, Flip, EASE, registerMotion } from '../core/gsap';
2
+ import { reduced } from '../core/reduced';
3
+ import { TIMING } from '../core/timing';
4
+
5
+ /*
6
+ * Stepbro's layout reflow: items that changed place travel there on a spring
7
+ * (0.62 s, ratio .52) and new ones grow in (scale .94, in 60 % of the time).
8
+ * Capture before the DOM change, reflow after it; items pair by `data-flip-id`.
9
+ * Don't make the items `position: absolute` — the list would collapse.
10
+ */
11
+
12
+ export interface CapturedLayout {
13
+ state: ReturnType<typeof Flip.getState>;
14
+ ids: Set<string | undefined>;
15
+ }
16
+
17
+ export function captureLayout(root: Element, selector: string): CapturedLayout {
18
+ registerMotion();
19
+ const items = [...root.querySelectorAll<HTMLElement>(selector)];
20
+ return { state: Flip.getState(items), ids: new Set(items.map((item) => item.dataset.flipId)) };
21
+ }
22
+
23
+ export function reflowLayout(root: Element, selector: string, captured: CapturedLayout, { duration = TIMING.layoutTravel }: { duration?: number } = {}) {
24
+ registerMotion();
25
+ if (reduced()) return null;
26
+ const targets = [...root.querySelectorAll<HTMLElement>(selector)];
27
+ return Flip.from(captured.state, {
28
+ targets,
29
+ duration,
30
+ ease: EASE.layoutReflow,
31
+ zIndex: 3,
32
+ onEnter: (elements) =>
33
+ gsap.fromTo(
34
+ elements,
35
+ { opacity: 0, scale: 0.94 },
36
+ { opacity: 1, scale: 1, duration: duration * TIMING.layoutEnterFactor, ease: EASE.layoutReflow, clearProps: 'opacity,transform' },
37
+ ),
38
+ });
39
+ }