@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,55 @@
1
+ import { createScope } from '../../core/scope';
2
+ import { inert, warnOnce, type Instance } from '../../core/types';
3
+
4
+ /* Size a one-line element so it spans its parent's content box (giant "Portfolio" / footer words). */
5
+
6
+ export interface FitTextOptions {
7
+ /** Fraction of the available width to use; leaves room for side bearings. */
8
+ safety?: number;
9
+ }
10
+
11
+ export interface FitTextInstance extends Instance {
12
+ refit(): void;
13
+ }
14
+
15
+ export function createFitText(el: HTMLElement | null, options: FitTextOptions = {}): FitTextInstance {
16
+ const parent = el?.parentElement;
17
+ if (!el || !parent) {
18
+ warnOnce('fit-text: element needs a parent');
19
+ return inert({ refit() {} });
20
+ }
21
+ const safety = Math.min(1, Math.max(0.5, options.safety ?? 0.995));
22
+ const scope = createScope();
23
+ const previousSize = el.style.fontSize;
24
+
25
+ const refit = () => {
26
+ const style = getComputedStyle(parent);
27
+ const available = parent.clientWidth - parseFloat(style.paddingLeft || '0') - parseFloat(style.paddingRight || '0');
28
+ el.style.fontSize = '100px';
29
+ const width = el.getBoundingClientRect().width;
30
+ if (width > 0 && available > 0) el.style.fontSize = `${(100 * available * safety) / width}px`;
31
+ else el.style.fontSize = previousSize;
32
+ };
33
+
34
+ // Refit on width changes only; the height change caused by fitting must not loop.
35
+ let lastWidth = -1;
36
+ scope
37
+ .observe(
38
+ new ResizeObserver(([entry]) => {
39
+ const width = entry?.contentRect.width ?? 0;
40
+ if (width === lastWidth) return;
41
+ lastWidth = width;
42
+ refit();
43
+ }),
44
+ )
45
+ .observe(parent);
46
+ refit();
47
+ el.dataset.mkReady = '';
48
+ scope.add(() => {
49
+ el.style.fontSize = previousSize;
50
+ if (!el.getAttribute('style')) el.removeAttribute('style');
51
+ delete el.dataset.mkReady;
52
+ });
53
+
54
+ return { refit, destroy: () => scope.dispose() };
55
+ }
@@ -0,0 +1,6 @@
1
+ /* fit-text — one line, measured as an inline box */
2
+ [data-mk~="fit-text"],
3
+ .mk-fit {
4
+ display: inline-block;
5
+ white-space: nowrap;
6
+ }
@@ -0,0 +1,46 @@
1
+ # flow
2
+
3
+ Los hijos del contenedor caen desde 20 px arriba, desenfocados 16 px, en 0.5 s y con 50 ms entre ellos.
4
+
5
+ Origen: Stepbro — flowIn (apps Tasks y Money).
6
+
7
+ ## Vanilla / Astro
8
+
9
+ ```html
10
+ <div data-mk="flow">
11
+ <p>Primer párrafo</p>
12
+ <p>Segundo párrafo</p>
13
+ </div>
14
+ ```
15
+
16
+ Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createFlow(el, opciones)`.
17
+
18
+ ## React
19
+
20
+ ```tsx
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ useFlow(ref);
23
+ // o, sin contenedor: flowIn(elements, { stagger: .04 })
24
+ ```
25
+
26
+ ## Opciones
27
+
28
+ | Opción | Por defecto | Qué hace |
29
+ |---|---|---|
30
+ | `y` | `20` | px de caída |
31
+ | `blur` | `16` | px de desenfoque inicial |
32
+ | `duration` | `.5` | Segundos |
33
+ | `stagger` | `.05` | Segundos entre hijos |
34
+ | `delay` | `0` | Segundos |
35
+ | `trigger` | `'scroll'` | 'scroll', 'load' o 'manual' |
36
+ | `start` | `'top 88%'` | Inicio del ScrollTrigger |
37
+
38
+ ## Controles
39
+
40
+ `play()`, `destroy()`; utilidad `flowIn(targets, options)`
41
+
42
+ ## CSS
43
+
44
+ Curva `mk-flow` (`M0,0 C0.25,0.1 0.25,1 1,1`).
45
+
46
+ Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
@@ -0,0 +1,55 @@
1
+ import { gsap, ScrollTrigger, registerMotion } from '../../core/gsap';
2
+ import { flowIn, type FlowInOptions } from '../../motion/flow';
3
+ import { entrances } from '../../core/reduced';
4
+ import { inert, warnOnce, type Instance } from '../../core/types';
5
+
6
+ /* Stepbro flowIn on a container: its children drop in from 20 px above, blurred 16 px, 0.5 s, 50 ms apart. */
7
+
8
+ export interface FlowOptions extends FlowInOptions {
9
+ trigger?: 'scroll' | 'load' | 'manual';
10
+ start?: string;
11
+ }
12
+
13
+ export interface FlowInstance extends Instance {
14
+ play(): void;
15
+ }
16
+
17
+ export { flowIn };
18
+
19
+ /** Flow the container's children in, on scroll by default. */
20
+ export function createFlow(container: HTMLElement | null, options: FlowOptions = {}): FlowInstance {
21
+ if (!container) {
22
+ warnOnce('flow: missing container');
23
+ return inert({ play() {} });
24
+ }
25
+ registerMotion();
26
+ const children = [...container.children] as HTMLElement[];
27
+ const animate = entrances() && children.length > 0;
28
+ const ctx = gsap.context(() => {});
29
+ let played = !animate;
30
+
31
+ const play = () => {
32
+ if (played) return;
33
+ played = true;
34
+ ctx.add(() => flowIn(children, options));
35
+ };
36
+
37
+ if (animate) {
38
+ ctx.add(() => {
39
+ gsap.set(children, { autoAlpha: 0 });
40
+ if (options.trigger === 'load') play();
41
+ else if (options.trigger !== 'manual') {
42
+ ScrollTrigger.create({ trigger: container, start: options.start ?? 'top 88%', end: 'max', once: true, onEnter: play });
43
+ }
44
+ });
45
+ }
46
+ container.dataset.mkReady = '';
47
+
48
+ return {
49
+ play,
50
+ destroy() {
51
+ ctx.revert();
52
+ delete container.dataset.mkReady;
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,29 @@
1
+ # frame-zoom
2
+
3
+ Un marco que se cierra como una ventana mientras cruza la pantalla: entra completo por abajo, a media pantalla va al 90 % y al irse por arriba llega al 80 %, con la imagen de dentro creciendo al revés (110 % → 130 %; en el móvil 95–100 % y 105–110 %). Sigue al scroll de la página sin retraso. `open(true)` lo devuelve completo (1 s `expo.out`), como el vídeo de Siena al abrirse.
4
+
5
+ Origen: Siena Film Foundation, máscara del vídeo de película (marco `e·20 + 80 %`, imagen `130 − e·20 %`, `e = 1 − progreso` del rastreador de Siena, que va de `top bottom` a `bottom top`).
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <div data-mk="frame-zoom" class="frame" style="border-radius: 24px">
11
+ <video src="trailer.mp4" muted autoplay loop playsinline></video>
12
+ </div>
13
+ ```
14
+
15
+ ## React
16
+
17
+ ```tsx
18
+ const ref = useRef<HTMLDivElement>(null);
19
+ const zoom = useFrameZoom(ref);
20
+ <button onClick={() => zoom.current?.open(true)}>Ver</button>
21
+ ```
22
+
23
+ ## API
24
+
25
+ `createFrameZoom(root, { start, end, lerp, scroll })` → `{ seek(p), open(on), state, destroy }`.
26
+
27
+ ## Accesibilidad
28
+
29
+ Con movimiento reducido el marco queda completo y quieto. No es una entrada (no esconde nada), así que la red de seguridad de `MK_HEAD_SCRIPT` no lo apaga.
@@ -0,0 +1,91 @@
1
+ import { gsap, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { trackScroll, type ScrollTrack } from '../../core/scroll-track';
4
+ import { TIMING } from '../../core/timing';
5
+ import { inert, warnOnce, type Instance } from '../../core/types';
6
+
7
+ /*
8
+ * Siena's video mask: the frame and the picture inside scale in opposite
9
+ * directions. With e = (1 − progress)·(1 − open) + open, the frame is
10
+ * (80 + 20e) % and the picture (130 − 20e) % (phones: 95 + 5e and 105 − 5e):
11
+ * full as it enters at the bottom, it closes like a window while it crosses the
12
+ * screen (Siena's tracker: top bottom → bottom top). open(true) brings it back
13
+ * to full (1 s expo.out).
14
+ */
15
+
16
+ export interface FrameZoomOptions {
17
+ start?: string;
18
+ end?: string;
19
+ lerp?: number;
20
+ scroll?: boolean;
21
+ }
22
+
23
+ export interface FrameZoomInstance extends Instance {
24
+ seek(progress: number): void;
25
+ /** Full frame (true) or back to the scroll (false). */
26
+ open(on: boolean): void;
27
+ readonly state: { progress: number; open: number };
28
+ }
29
+
30
+ export function createFrameZoom(root: HTMLElement | null, options: FrameZoomOptions = {}): FrameZoomInstance {
31
+ const inner = (root?.firstElementChild as HTMLElement | null) ?? null;
32
+ if (!root || !inner) {
33
+ warnOnce('frame-zoom: needs a frame with a picture inside');
34
+ return inert({ seek() {}, open() {}, state: { progress: 0, open: 0 } });
35
+ }
36
+ const t = TIMING.frameZoom;
37
+ const { start = 'top bottom', end = 'bottom top', lerp = t.lerp, scroll = true } = options;
38
+ registerMotion();
39
+ const phone = typeof matchMedia === 'function' && matchMedia('(max-width: 767px)').matches;
40
+ const [outerBase, outerRange] = phone ? t.phoneOuter : t.outer;
41
+ const [innerBase, innerRange] = phone ? t.phoneInner : t.inner;
42
+ // Not an entrance: it hides nothing, so only reduced motion stills it.
43
+ const moving = !reduced();
44
+ const state = { progress: 0, open: 0 };
45
+ const originals = [root.style.transform, inner.style.transform, root.style.overflow];
46
+ root.style.overflow = 'clip';
47
+
48
+ const render = () => {
49
+ const e = moving ? (1 - state.progress) * (1 - state.open) + state.open : 1;
50
+ root.style.transform = `scale(${Number(((outerBase + outerRange * e) / 100).toFixed(4))})`;
51
+ inner.style.transform = `scale(${Number(((innerBase - innerRange * e) / 100).toFixed(4))})`;
52
+ };
53
+ let track: ScrollTrack | null = null;
54
+ if (moving && scroll)
55
+ track = trackScroll(root, {
56
+ start,
57
+ end,
58
+ lerp,
59
+ onUpdate: (value) => {
60
+ state.progress = value;
61
+ render();
62
+ },
63
+ });
64
+ else render();
65
+ root.dataset.mkReady = '';
66
+
67
+ return {
68
+ state,
69
+ seek(progress: number) {
70
+ if (track) track.set(progress);
71
+ else {
72
+ state.progress = Math.min(1, Math.max(0, progress));
73
+ render();
74
+ }
75
+ },
76
+ open(on: boolean) {
77
+ gsap.killTweensOf(state);
78
+ if (!moving) return;
79
+ gsap.to(state, { open: on ? 1 : 0, duration: t.open, ease: 'expo.out', onUpdate: render });
80
+ },
81
+ destroy() {
82
+ track?.destroy();
83
+ gsap.killTweensOf(state);
84
+ root.style.transform = originals[0]!;
85
+ inner.style.transform = originals[1]!;
86
+ root.style.overflow = originals[2]!;
87
+ for (const el of [root, inner]) if (el.getAttribute('style') === '') el.removeAttribute('style');
88
+ delete root.dataset.mkReady;
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,23 @@
1
+ # infinite-reel
2
+
3
+ Reel infinito horizontal o vertical con rueda, arrastre e inercia (Siena).
4
+
5
+ ```html
6
+ <div class="mk-infinite-reel" data-mk="infinite-reel" aria-label="Películas">
7
+ <div data-mk-reel-track><article>Uno</article><article>Dos</article></div>
8
+ </div>
9
+ ```
10
+
11
+ ```ts
12
+ import { createInfiniteReel } from '@yewdiel21/motion-kit';
13
+ import { useInfiniteReel } from '@yewdiel21/motion-kit/react';
14
+ import '@yewdiel21/motion-kit/css';
15
+ const instance = createInfiniteReel(root);
16
+ instance.destroy();
17
+ ```
18
+
19
+ Opciones: `axis: 'x' | 'y'` (x), `speed` px/frame (0), `friction` (.95). `moveBy(px)`, `pause()`, `resume()`, `position`. Rueda, arrastre, flechas y Espacio cuando tiene foco. Copias con aria-hidden/inert y sin IDs. Movimiento reducido: scroll nativo sin copias, ticker ni inercia. Origen: Siena `siena.reel.horizontal/vertical`; adaptación DOM sin snap ni shaders del reel original.
20
+
21
+ Vanilla declarativo: `autoInit(root)`. React: `useInfiniteReel(ref, options, deps)` devuelve una ref a la instancia; se destruye al desmontar. Con movimiento reducido no se ejecuta movimiento decorativo. `destroy()` restaura atributos y estilos y quita listeners, observers y nodos generados.
22
+
23
+ Desde 0.8.0. Parámetros investigados en `REA - Dousan OFFFICE Siena/`; implementación propia, sin código ni assets de esos sitios.
@@ -0,0 +1,140 @@
1
+ import { whileVisible } from '../../core/loop';
2
+ import { wrap } from '../../core/math';
3
+ import { reduced } from '../../core/reduced';
4
+ import { createScope } from '../../core/scope';
5
+ import { TIMING } from '../../core/timing';
6
+ import { inert, type Instance } from '../../core/types';
7
+
8
+ export interface InfiniteReelOptions { speed?: number; friction?: number; axis?: 'x' | 'y'; }
9
+ export interface InfiniteReelInstance extends Instance {
10
+ moveBy(pixels: number): void;
11
+ pause(): void;
12
+ resume(): void;
13
+ readonly position: number;
14
+ }
15
+
16
+ export function createInfiniteReel(root: HTMLElement | null, options: InfiniteReelOptions = {}): InfiniteReelInstance {
17
+ const track = root?.querySelector<HTMLElement>('[data-mk-reel-track]');
18
+ if (!root || !track) return inert({ moveBy() {}, pause() {}, resume() {}, position: 0 });
19
+ const scope = createScope();
20
+ const still = reduced();
21
+ const vertical = options.axis === 'y';
22
+ scope.attr(root, 'data-mk-axis', vertical ? 'y' : 'x');
23
+ scope.attr(root, 'tabindex', root.getAttribute('tabindex') ?? '0');
24
+ scope.attr(track, 'style', track.getAttribute('style'));
25
+ const copies: HTMLElement[] = [];
26
+ let length = 0;
27
+ let position = 0;
28
+ let velocity = 0;
29
+ let paused = false;
30
+ let focused = false;
31
+ let drag: { point: number; id: number; moved: number } | null = null;
32
+ let suppressClick = false;
33
+ const coordinate = (event: PointerEvent) => vertical ? event.clientY : event.clientX;
34
+ const draw = () => {
35
+ if (still) {
36
+ if (vertical) root.scrollTop = position;
37
+ else root.scrollLeft = position;
38
+ return;
39
+ }
40
+ [track, ...copies].forEach((group, index) => {
41
+ const offset = index * length - position;
42
+ group.style.transform = vertical ? `translateY(${offset}px)` : `translateX(${offset}px)`;
43
+ });
44
+ };
45
+ const measure = () => {
46
+ copies.splice(0).forEach((copy) => copy.remove());
47
+ const rect = track.getBoundingClientRect();
48
+ length = vertical ? track.scrollHeight || rect.height : track.scrollWidth || rect.width;
49
+ if (!still && length > 0) {
50
+ const viewport = root.getBoundingClientRect();
51
+ const count = Math.min(30, Math.ceil((vertical ? viewport.height : viewport.width) / length) + 1);
52
+ for (let index = 0; index < count; index++) {
53
+ const copy = track.cloneNode(true) as HTMLElement;
54
+ copy.removeAttribute('id');
55
+ copy.removeAttribute('data-mk-reel-track');
56
+ copy.removeAttribute('data-mk');
57
+ copy.setAttribute('data-mk-reel-copy', '');
58
+ copy.setAttribute('aria-hidden', 'true');
59
+ copy.setAttribute('inert', '');
60
+ copy.querySelectorAll('[id]').forEach((child) => child.removeAttribute('id'));
61
+ copy.querySelectorAll('[data-mk]').forEach((child) => child.removeAttribute('data-mk'));
62
+ root.append(copy);
63
+ copies.push(copy);
64
+ }
65
+ }
66
+ position = length > 0 ? wrap(position, length) : 0;
67
+ draw();
68
+ };
69
+ const moveBy = (pixels: number) => {
70
+ if (scope.disposed || !Number.isFinite(pixels) || length <= 0) return;
71
+ position = wrap(position + pixels, length);
72
+ draw();
73
+ };
74
+ scope.on(root, 'wheel', (event) => {
75
+ if (still || !length) return;
76
+ event.preventDefault();
77
+ const units = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? (vertical ? root.clientHeight : root.clientWidth) : 1;
78
+ velocity += (vertical ? event.deltaY : event.deltaX || event.deltaY) * units * TIMING.infiniteReel.wheel;
79
+ }, { passive: false });
80
+ scope.on(root, 'pointerdown', (event) => {
81
+ if (event.button !== 0 || drag || still) return;
82
+ velocity = 0;
83
+ suppressClick = false;
84
+ drag = { point: coordinate(event), id: event.pointerId, moved: 0 };
85
+ });
86
+ scope.on(window, 'pointermove', (event) => {
87
+ if (!drag || event.pointerId !== drag.id) return;
88
+ const delta = drag.point - coordinate(event);
89
+ drag.moved += Math.abs(delta);
90
+ drag.point = coordinate(event);
91
+ if (drag.moved > TIMING.infiniteReel.dragThreshold) {
92
+ suppressClick = true;
93
+ root.setPointerCapture?.(event.pointerId);
94
+ moveBy(delta);
95
+ velocity = delta;
96
+ }
97
+ });
98
+ const release = (event: PointerEvent) => {
99
+ if (!drag || drag.id !== event.pointerId) return;
100
+ drag = null;
101
+ if (root.hasPointerCapture?.(event.pointerId)) root.releasePointerCapture(event.pointerId);
102
+ };
103
+ scope.on(window, 'pointerup', release);
104
+ scope.on(window, 'pointercancel', (event) => { velocity = 0; release(event); });
105
+ scope.on(root, 'lostpointercapture', release);
106
+ scope.on(root, 'click', (event) => {
107
+ if (suppressClick) { event.preventDefault(); event.stopPropagation(); suppressClick = false; }
108
+ }, true);
109
+ scope.on(root, 'focusin', () => { focused = true; });
110
+ scope.on(root, 'focusout', (event) => { focused = root.contains(event.relatedTarget as Node | null); });
111
+ scope.on(root, 'keydown', (event) => {
112
+ if (event.target !== root) return;
113
+ const next = vertical ? 'ArrowDown' : 'ArrowRight';
114
+ const previous = vertical ? 'ArrowUp' : 'ArrowLeft';
115
+ if (![next, previous, ' '].includes(event.key)) return;
116
+ event.preventDefault();
117
+ moveBy((event.key === previous ? -1 : 1) * length / Math.max(1, track.children.length));
118
+ });
119
+ if (typeof ResizeObserver === 'function') {
120
+ const observer = scope.observe(new ResizeObserver(measure));
121
+ observer.observe(root);
122
+ observer.observe(track);
123
+ }
124
+ scope.on(window, 'resize', measure);
125
+ scope.on(track, 'load', measure, true);
126
+ const scroll = { left: root.scrollLeft, top: root.scrollTop };
127
+ scope.add(() => {
128
+ copies.forEach((copy) => copy.remove());
129
+ if (drag && root.hasPointerCapture?.(drag.id)) root.releasePointerCapture(drag.id);
130
+ root.scrollLeft = scroll.left;
131
+ root.scrollTop = scroll.top;
132
+ });
133
+ measure();
134
+ if (!still) scope.add(whileVisible(root, (dt) => {
135
+ if (paused || focused || drag || document.hidden) return;
136
+ moveBy(((options.speed ?? 0) + velocity) * dt);
137
+ velocity *= Math.pow(Math.max(0, Math.min(0.999, options.friction ?? TIMING.infiniteReel.friction)), dt);
138
+ }));
139
+ return { moveBy, pause() { paused = true; }, resume() { paused = false; }, get position() { return position; }, destroy: () => scope.dispose() };
140
+ }
@@ -0,0 +1,8 @@
1
+ .mk-infinite-reel { position: relative; width: 100%; min-width: 0; overflow: hidden; touch-action: pan-y; cursor: grab; }
2
+ .mk-infinite-reel:active { cursor: grabbing; }
3
+ .mk-infinite-reel [data-mk-reel-track], .mk-infinite-reel [data-mk-reel-copy] { display: flex; width: max-content; gap: 16px; padding-right: 16px; will-change: transform; }
4
+ .mk-infinite-reel [data-mk-reel-copy] { position: absolute; top: 0; left: 0; pointer-events: none; }
5
+ .mk-infinite-reel [data-mk-reel-track] > *, .mk-infinite-reel [data-mk-reel-copy] > * { flex: none; }
6
+ .mk-infinite-reel[data-mk-axis='y'] { height: 400px; touch-action: pan-x; }
7
+ .mk-infinite-reel[data-mk-axis='y'] [data-mk-reel-track], .mk-infinite-reel[data-mk-axis='y'] [data-mk-reel-copy] { flex-direction: column; width: 100%; padding-right: 0; padding-bottom: 16px; }
8
+ @media (prefers-reduced-motion: reduce) { .mk-infinite-reel { overflow: auto; cursor: auto; } }
@@ -0,0 +1,30 @@
1
+ # letters
2
+
3
+ Letras que juegan:
4
+
5
+ - **`scale`**: al pasar el ratón por la palabra cada letra crece (`scale 1.3`, sube 5 px) con un resorte con rebote (Motion 300/10), 30 ms después de la anterior; vuelve igual al salir.
6
+ - **`blink`**: cada letra pasa a un color pastel y vuelve (0.6 s, `ease-in-out`) con retardos desordenados (.1, .6, .2, 1, .4, 1.2, .15, .8, .3 s), dos veces. Arranca 0.6 s después de crearse y al pasar el ratón.
7
+
8
+ Origen: Dousan Miao, el titular de la portada («impactful»: `hero-letters-blink`, `hero-scale-letters`).
9
+
10
+ ## HTML / Astro
11
+
12
+ ```html
13
+ <h1>I design <span data-mk="letters" data-mk-effect="blink">impactful</span> products</h1>
14
+ <span data-mk="letters" data-mk-effect="scale">Dousan</span>
15
+ ```
16
+
17
+ ## React
18
+
19
+ ```tsx
20
+ const ref = useRef<HTMLSpanElement>(null);
21
+ useLetters(ref, { effect: 'blink', colors: ['#FFD6FF', '#C8B6FF'] });
22
+ ```
23
+
24
+ ## API
25
+
26
+ `createLetters(el, { effect, colors, autoplay })` → `{ play(), destroy }`. `colors` por defecto: los pastel de Dousan (`#FFD6FF #E7C6FF #C8B6FF #B8C0FF #BBD0FF`).
27
+
28
+ ## Accesibilidad
29
+
30
+ La palabra conserva su texto en `aria-label`. Con movimiento reducido no se parte ni se mueve.
@@ -0,0 +1,113 @@
1
+ import { gsap, EASE, SplitText, registerMotion } from '../../core/gsap';
2
+ import { motionSpring } from '../../core/physics-spring';
3
+ import { entrances } from '../../core/reduced';
4
+ import { clearStyles } from '../../core/styles';
5
+ import { TIMING } from '../../core/timing';
6
+ import { inert, warnOnce, type Instance } from '../../core/types';
7
+
8
+ /*
9
+ * Dousan's hero words. 'scale': on hover every letter springs up (scale 1.3,
10
+ * y −5 px, Motion 300/10), 30 ms after the previous one, and back on leave.
11
+ * 'blink': every letter flashes a pastel and returns (0.6 s, ease-in-out) with
12
+ * Dousan's scattered delays, twice; it plays 0.6 s after start and on hover.
13
+ */
14
+
15
+ export interface LettersOptions {
16
+ effect?: 'scale' | 'blink';
17
+ /** Blink colours (default Dousan's pastels). */
18
+ colors?: string[];
19
+ /** Blink once 0.6 s after start (default true). */
20
+ autoplay?: boolean;
21
+ }
22
+
23
+ export interface LettersInstance extends Instance {
24
+ /** Blink now (the scale effect follows the pointer). */
25
+ play(): void;
26
+ }
27
+
28
+ const PASTELS = ['#FFD6FF', '#E7C6FF', '#C8B6FF', '#B8C0FF', '#BBD0FF'];
29
+ const ORDER = [0, 2, 1, 4, 3, 0, 2, 1, 4];
30
+
31
+ export function createLetters(el: HTMLElement | null, options: LettersOptions = {}): LettersInstance {
32
+ if (!el) {
33
+ warnOnce('letters: missing element');
34
+ return inert({ play() {} });
35
+ }
36
+ const t = TIMING.letters;
37
+ const { effect = 'scale', colors = PASTELS, autoplay = true } = options;
38
+ if (!entrances()) {
39
+ el.dataset.mkReady = '';
40
+ return {
41
+ play() {},
42
+ destroy() {
43
+ delete el.dataset.mkReady;
44
+ },
45
+ };
46
+ }
47
+ registerMotion();
48
+ const split = SplitText.create(el, { type: 'words,chars', wordsClass: 'mk-letters-word', charsClass: 'mk-letter', aria: 'auto' });
49
+ const chars = split.chars as HTMLElement[];
50
+ const cleanups: Array<() => void> = [];
51
+ const spring = motionSpring(...TIMING.springs.letters);
52
+
53
+ const lift = (up: boolean) => {
54
+ chars.forEach((char, index) =>
55
+ gsap.to(char, {
56
+ scale: up ? t.scale : 1,
57
+ y: up ? t.y : 0,
58
+ duration: spring.duration,
59
+ ease: spring.ease,
60
+ delay: index * t.step,
61
+ overwrite: true,
62
+ }),
63
+ );
64
+ };
65
+
66
+ const blink = () => {
67
+ chars.forEach((char, index) => {
68
+ gsap.killTweensOf(char, 'color');
69
+ clearStyles([char], ['color']);
70
+ const own = getComputedStyle(char).color;
71
+ const pastel = colors[ORDER[index % ORDER.length]! % colors.length]!;
72
+ gsap.to(char, {
73
+ keyframes: [
74
+ { color: pastel, duration: t.blink / 2, ease: EASE.inOut },
75
+ { color: own, duration: t.blink / 2, ease: EASE.inOut },
76
+ ],
77
+ delay: t.blinkDelays[index % t.blinkDelays.length],
78
+ repeat: 1,
79
+ repeatDelay: t.blinkRepeatDelay,
80
+ onComplete: () => clearStyles([char], ['color']),
81
+ });
82
+ });
83
+ };
84
+
85
+ if (effect === 'scale') {
86
+ const enter = () => lift(true);
87
+ const leave = () => lift(false);
88
+ el.addEventListener('pointerenter', enter);
89
+ el.addEventListener('pointerleave', leave);
90
+ cleanups.push(() => {
91
+ el.removeEventListener('pointerenter', enter);
92
+ el.removeEventListener('pointerleave', leave);
93
+ });
94
+ } else {
95
+ el.addEventListener('pointerenter', blink);
96
+ cleanups.push(() => el.removeEventListener('pointerenter', blink));
97
+ if (autoplay) {
98
+ const call = gsap.delayedCall(t.blinkStart, blink);
99
+ cleanups.push(() => call.kill());
100
+ }
101
+ }
102
+ el.dataset.mkReady = '';
103
+
104
+ return {
105
+ play: effect === 'blink' ? blink : () => lift(true),
106
+ destroy() {
107
+ for (const cleanup of cleanups.splice(0)) cleanup();
108
+ gsap.killTweensOf(chars);
109
+ split.revert();
110
+ delete el.dataset.mkReady;
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,32 @@
1
+ # marquee
2
+
3
+ Una banda infinita: el primer hijo (el grupo) se copia hasta llenar la banda más una vez y cada copia se desplaza su propio tamaño en bucle lineal, sin cortes. Velocidad: 2 s por elemento del grupo (cuenta hasta 14).
4
+
5
+ Origen: Siena Film Foundation, marquesina vertical de la página de película. Siena la usa en vertical; aquí el eje por defecto es horizontal (`axis: 'y'` para la de Siena).
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <div data-mk="marquee" class="logos">
11
+ <ul><li>Docaviv</li><li>Sundance</li><li>IDFA</li></ul>
12
+ </div>
13
+ <div data-mk="marquee" data-mk-axis="y" data-mk-reverse style="height: 300px">…</div>
14
+ ```
15
+
16
+ Separa los elementos con `--mk-marquee-gap` o con margen dentro del grupo, no con `gap` en el contenedor.
17
+
18
+ ## React
19
+
20
+ ```tsx
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ const marquee = useMarquee(ref, { axis: 'x', speed: 2 });
23
+ // marquee.current?.refresh() si cambia el contenido
24
+ ```
25
+
26
+ ## API
27
+
28
+ `createMarquee(root, { axis, speed, reverse })` → `{ refresh(), destroy }`. Variables: `--mk-marquee-duration` (la escribe el motor), `--mk-marquee-gap`.
29
+
30
+ ## Accesibilidad
31
+
32
+ Las copias llevan `aria-hidden` e `inert`: lectores y teclado encuentran el grupo una sola vez. Con movimiento reducido no hay copias ni movimiento.