@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,36 @@
1
+ # pop
2
+
3
+ Un elemento entra escalando de .7 a 1 con rebote.
4
+
5
+ Origen: spencergabor.com — imágenes de proyecto.
6
+
7
+ ## Vanilla / Astro
8
+
9
+ ```html
10
+ <img data-mk="pop" src="photo.jpg" alt="Retrato">
11
+ ```
12
+
13
+ Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createPop(el, opciones)`.
14
+
15
+ ## React
16
+
17
+ ```tsx
18
+ const ref = useRef<HTMLImageElement>(null);
19
+ usePop(ref, { scale: .8 });
20
+ ```
21
+
22
+ ## Opciones
23
+
24
+ | Opción | Por defecto | Qué hace |
25
+ |---|---|---|
26
+ | `scale` | `.7` | Escala inicial |
27
+ | `duration` | `1.1` | Segundos |
28
+ | `delay` | `0` | Segundos |
29
+ | `trigger` | `'scroll'` | 'scroll', 'load' o 'manual' |
30
+ | `start` | `'top 85%'` | Inicio del ScrollTrigger |
31
+
32
+ ## Controles
33
+
34
+ `play()`, `destroy()`
35
+
36
+ Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
@@ -0,0 +1,52 @@
1
+ import { gsap, ELASTIC, ScrollTrigger, registerMotion } from '../../core/gsap';
2
+ import { entrances } from '../../core/reduced';
3
+ import { inert, warnOnce, type Instance } from '../../core/types';
4
+
5
+ /* Elastic scale-in for a single element (spencergabor.com project images, scale .7 → 1). */
6
+
7
+ export interface PopOptions {
8
+ scale?: number;
9
+ duration?: number;
10
+ delay?: number;
11
+ trigger?: 'scroll' | 'load' | 'manual';
12
+ start?: string;
13
+ }
14
+
15
+ export interface PopInstance extends Instance {
16
+ play(): void;
17
+ }
18
+
19
+ export function createPop(el: HTMLElement | null, options: PopOptions = {}): PopInstance {
20
+ if (!el) {
21
+ warnOnce('pop: missing element');
22
+ return inert({ play() {} });
23
+ }
24
+ registerMotion();
25
+ const { scale = 0.7, duration = 1.1, delay = 0, trigger = 'scroll', start = 'top 85%' } = options;
26
+ const animate = entrances();
27
+ const ctx = gsap.context(() => {});
28
+ let played = !animate;
29
+
30
+ const play = () => {
31
+ if (played) return;
32
+ played = true;
33
+ ctx.add(() => gsap.to(el, { scale: 1, autoAlpha: 1, duration, delay, ease: ELASTIC }));
34
+ };
35
+
36
+ if (animate) {
37
+ ctx.add(() => {
38
+ gsap.set(el, { scale, autoAlpha: 0 });
39
+ if (trigger === 'load') play();
40
+ else if (trigger !== 'manual') ScrollTrigger.create({ trigger: el, start, end: 'max', once: true, onEnter: play });
41
+ });
42
+ }
43
+ el.dataset.mkReady = '';
44
+
45
+ return {
46
+ play,
47
+ destroy() {
48
+ ctx.revert();
49
+ delete el.dataset.mkReady;
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,44 @@
1
+ # radial-menu
2
+
3
+ Un botón redondo cuyas opciones salen en arco con rebote; cerrado, las opciones quedan `inert`. Cierra con Escape y al pulsar fuera.
4
+
5
+ Origen: spencergabor.com — selector de color.
6
+
7
+ ## Vanilla / Astro
8
+
9
+ ```html
10
+ <div data-mk="radial-menu" class="settings">
11
+ <button data-mk-item aria-label="Modo oscuro">☾</button>
12
+ <button data-mk-item aria-label="Idioma">ES</button>
13
+ <button data-mk-toggle aria-label="Ajustes">◐</button>
14
+ </div>
15
+ ```
16
+
17
+ Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createRadialMenu(el, opciones)`.
18
+
19
+ ## React
20
+
21
+ ```tsx
22
+ const ref = useRef<HTMLDivElement>(null);
23
+ const menu = useRadialMenu(ref, { onToggle: setOpen });
24
+ ```
25
+
26
+ ## Opciones
27
+
28
+ | Opción | Por defecto | Qué hace |
29
+ |---|---|---|
30
+ | `radius` | `'3.6rem'` | Distancia de las opciones |
31
+ | `startAngle` | `1.5708` | Ángulo de la primera opción (rad; π/2 = izquierda) |
32
+ | `spacing` | `.9` | Ángulo entre opciones (rad) |
33
+ | `closeOnOutside` | `true` | Cerrar al pulsar fuera |
34
+ | `onToggle(open)` | — | Callback |
35
+
36
+ ## Controles
37
+
38
+ `open()`, `close()`, `toggle()`, `isOpen`, `destroy()`
39
+
40
+ ## CSS
41
+
42
+ El toggle y las opciones son hijos directos del root (rejilla de una celda). Las posiciones salen de `--mk-i`, `--mk-radius`, `--mk-start` y `--mk-spacing`.
43
+
44
+ Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
@@ -0,0 +1,96 @@
1
+ import { createScope } from '../../core/scope';
2
+ import { inert, warnOnce, type Instance } from '../../core/types';
3
+
4
+ /*
5
+ * spencergabor.com colour picker, generalised: options fan out in an arc from a
6
+ * round toggle (CSS trig on --mk-i), elastic, 50 ms apart. Closed options are
7
+ * `inert`, so they are skipped by Tab and screen readers.
8
+ */
9
+
10
+ export interface RadialMenuOptions {
11
+ /** Distance of the options from the toggle (any CSS length). */
12
+ radius?: string;
13
+ /** Angle of the first option, radians (π/2 → left, 0 → down). */
14
+ startAngle?: number;
15
+ /** Angle between options, radians. */
16
+ spacing?: number;
17
+ closeOnOutside?: boolean;
18
+ onToggle?: (open: boolean) => void;
19
+ }
20
+
21
+ export interface RadialMenuInstance extends Instance {
22
+ open(): void;
23
+ close(): void;
24
+ toggle(): void;
25
+ readonly isOpen: boolean;
26
+ }
27
+
28
+ const noop = { open() {}, close() {}, toggle() {}, isOpen: false };
29
+
30
+ export function createRadialMenu(root: HTMLElement | null, options: RadialMenuOptions = {}): RadialMenuInstance {
31
+ const toggleEl = root?.querySelector<HTMLElement>('[data-mk-toggle]');
32
+ const items = root ? [...root.querySelectorAll<HTMLElement>('[data-mk-item]')] : [];
33
+ if (!root || !toggleEl || !items.length) {
34
+ warnOnce('radial-menu: needs a [data-mk-toggle] and at least one [data-mk-item]');
35
+ return inert(noop);
36
+ }
37
+ const scope = createScope();
38
+
39
+ // Remember what we overwrite so destroy() can put it back.
40
+ const expandedBefore = toggleEl.getAttribute('aria-expanded');
41
+ const inertBefore = items.map((item) => item.hasAttribute('inert'));
42
+ const vars: Array<[string, string | undefined]> = [
43
+ ['--mk-radius', options.radius],
44
+ ['--mk-start', options.startAngle?.toString()],
45
+ ['--mk-spacing', options.spacing?.toString()],
46
+ ];
47
+ vars.forEach(([name, value]) => value !== undefined && root.style.setProperty(name, value));
48
+ items.forEach((item, i) => item.style.setProperty('--mk-i', String(i)));
49
+
50
+ let open = false;
51
+ const set = (next: boolean) => {
52
+ open = next;
53
+ root.toggleAttribute('data-mk-open', open);
54
+ toggleEl.setAttribute('aria-expanded', String(open));
55
+ items.forEach((item) => (item.inert = !open));
56
+ options.onToggle?.(open);
57
+ };
58
+ set(false);
59
+
60
+ scope.on(toggleEl, 'click', () => set(!open));
61
+ scope.on(root, 'keydown', (e: KeyboardEvent) => {
62
+ if (e.key !== 'Escape' || !open) return;
63
+ set(false);
64
+ toggleEl.focus();
65
+ });
66
+ if (options.closeOnOutside ?? true) {
67
+ scope.on(document, 'pointerdown', (e: PointerEvent) => {
68
+ if (open && !root.contains(e.target as Node)) set(false);
69
+ });
70
+ }
71
+ root.dataset.mkReady = '';
72
+
73
+ scope.add(() => {
74
+ root.removeAttribute('data-mk-open');
75
+ delete root.dataset.mkReady;
76
+ if (expandedBefore === null) toggleEl.removeAttribute('aria-expanded');
77
+ else toggleEl.setAttribute('aria-expanded', expandedBefore);
78
+ items.forEach((item, i) => {
79
+ item.inert = inertBefore[i]!;
80
+ item.style.removeProperty('--mk-i');
81
+ if (!item.getAttribute('style')) item.removeAttribute('style');
82
+ });
83
+ vars.forEach(([name]) => root.style.removeProperty(name));
84
+ if (!root.getAttribute('style')) root.removeAttribute('style');
85
+ });
86
+
87
+ return {
88
+ open: () => set(true),
89
+ close: () => set(false),
90
+ toggle: () => set(!open),
91
+ get isOpen() {
92
+ return open;
93
+ },
94
+ destroy: () => scope.dispose(),
95
+ };
96
+ }
@@ -0,0 +1,45 @@
1
+ /* radial-menu — spencergabor.com colour picker, generalised. Scoped to its root:
2
+ [data-mk-item] and [data-mk-open] are also used by other recipes and components. */
3
+
4
+ [data-mk~="radial-menu"],
5
+ .mk-radial {
6
+ display: grid;
7
+ place-items: center;
8
+ --mk-radius: 3.6rem;
9
+ --mk-start: 1.5708;
10
+ --mk-spacing: 0.9;
11
+ }
12
+
13
+ :is([data-mk~="radial-menu"], .mk-radial) > * {
14
+ grid-area: 1 / 1;
15
+ }
16
+
17
+ :is([data-mk~="radial-menu"], .mk-radial) [data-mk-toggle] {
18
+ position: relative;
19
+ z-index: 1;
20
+ transition: scale 0.8s var(--mk-ease-elastic);
21
+ }
22
+
23
+ :is([data-mk~="radial-menu"], .mk-radial) [data-mk-toggle]:active {
24
+ scale: 0.9;
25
+ }
26
+
27
+ :is([data-mk~="radial-menu"], .mk-radial) [data-mk-item] {
28
+ opacity: 0;
29
+ scale: 0.5;
30
+ transition:
31
+ translate 0.15s cubic-bezier(0.55, 0.055, 0.675, 0.19),
32
+ scale 0.15s cubic-bezier(0.55, 0.055, 0.675, 0.19),
33
+ opacity 0.15s;
34
+ }
35
+
36
+ :is([data-mk~="radial-menu"], .mk-radial)[data-mk-open] [data-mk-item] {
37
+ opacity: 1;
38
+ scale: 1;
39
+ translate:
40
+ calc(sin(var(--mk-i, 0) * var(--mk-spacing) + var(--mk-start)) * var(--mk-radius) * -1)
41
+ calc(cos(var(--mk-i, 0) * var(--mk-spacing) + var(--mk-start)) * var(--mk-radius));
42
+ transition-duration: 0.5s;
43
+ transition-timing-function: var(--mk-ease-elastic);
44
+ transition-delay: calc(var(--mk-i, 0) * 0.05s);
45
+ }
@@ -0,0 +1,47 @@
1
+ # reflow
2
+
3
+ Cuando una lista cambia (ordenar, filtrar, añadir), los elementos viajan a su nuevo lugar con un resorte (0.62 s, razón .52) y los nuevos crecen desde 94 %.
4
+
5
+ Origen: Stepbro — task-layout (`layoutReflow`, `FA(.52)` en el kit).
6
+
7
+ ## Vanilla / Astro
8
+
9
+ ```html
10
+ <ul data-mk="reflow">
11
+ <li data-mk-item data-flip-id="t1">…</li>
12
+ <li data-mk-item data-flip-id="t2">…</li>
13
+ </ul>
14
+ ```
15
+
16
+ ```js
17
+ const reflow = createReflow(lista);
18
+ reflow.capture(); // antes de cambiar el DOM
19
+ reordenar(lista); // tu cambio
20
+ reflow.play(); // después
21
+ ```
22
+
23
+ `data-flip-id` estable empareja cada elemento con su posición anterior. No pongas los elementos en `position: absolute`.
24
+
25
+ ## React
26
+
27
+ ```tsx
28
+ const ref = useRef<HTMLUListElement>(null);
29
+ const reflow = useReflow(ref);
30
+ const sort = () => { reflow.current?.capture(); setItems(sorted); };
31
+ useLayoutEffect(() => reflow.current?.play(), [items]);
32
+ ```
33
+
34
+ O `<FlipGroup>`, que hace la captura y la animación por ti.
35
+
36
+ ## Opciones
37
+
38
+ | Opción | Por defecto | Qué hace |
39
+ |---|---|---|
40
+ | `selector` | `'[data-mk-item]'` | Elementos de la lista |
41
+ | `duration` | `.62` | Segundos del viaje |
42
+
43
+ ## Controles
44
+
45
+ `capture()`, `play()`, `destroy()`
46
+
47
+ Con movimiento reducido los elementos cambian de sitio sin viajar.
@@ -0,0 +1,49 @@
1
+ import { gsap, registerMotion } from '../../core/gsap';
2
+ import { inert, warnOnce, type Instance } from '../../core/types';
3
+ import { TIMING } from '../../core/timing';
4
+ import { captureLayout, reflowLayout, type CapturedLayout } from '../../motion/layout';
5
+
6
+ /*
7
+ * Stepbro's layout reflow on a list: call `capture()` before changing the DOM
8
+ * and `play()` after it; moved items travel on a spring and new ones grow in.
9
+ * Give items a stable `data-flip-id` so moves pair up.
10
+ */
11
+
12
+ export interface ReflowOptions {
13
+ selector?: string;
14
+ duration?: number;
15
+ }
16
+
17
+ export interface ReflowInstance extends Instance {
18
+ capture(): void;
19
+ play(): void;
20
+ }
21
+
22
+ export function createReflow(root: HTMLElement | null, options: ReflowOptions = {}): ReflowInstance {
23
+ if (!root) {
24
+ warnOnce('reflow: missing root');
25
+ return inert({ capture() {}, play() {} });
26
+ }
27
+ registerMotion();
28
+ const { selector = '[data-mk-item]', duration = TIMING.layoutTravel } = options;
29
+ const ctx = gsap.context(() => {});
30
+ let captured: CapturedLayout | null = null;
31
+ root.dataset.mkReady = '';
32
+
33
+ return {
34
+ capture() {
35
+ captured = captureLayout(root, selector);
36
+ },
37
+ play() {
38
+ if (!captured) return;
39
+ const state = captured;
40
+ captured = null;
41
+ ctx.add(() => reflowLayout(root, selector, state, { duration }));
42
+ },
43
+ destroy() {
44
+ ctx.revert();
45
+ captured = null;
46
+ delete root.dataset.mkReady;
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,21 @@
1
+ # repel-bento
2
+
3
+ Bento con entrada spring y repulsión de tarjetas vecinas (Dousan).
4
+
5
+ ```html
6
+ <div class="mk-repel-bento" data-mk="repel-bento"><article>Uno</article><article>Dos</article></div>
7
+ ```
8
+
9
+ ```ts
10
+ import { createRepelBento } from '@yewdiel21/motion-kit';
11
+ import { useRepelBento } from '@yewdiel21/motion-kit/react';
12
+ import '@yewdiel21/motion-kit/css';
13
+ const instance = createRepelBento(root);
14
+ instance.destroy();
15
+ ```
16
+
17
+ `distance` máximo 30 px; `breakpoint` 1024. Entrada con spring 60/15 y desfase .006 s; repulsión de vecinos min(5000/distancia,30) con spring 300/25. `play()` repite la entrada; `reset()` vuelve al reposo. Bajo el breakpoint, puntero grueso y movimiento reducido no hay repulsión. Origen: Dousan `desktop-card-enter` y `neighbor-card-repulsion`.
18
+
19
+ Vanilla declarativo: `autoInit(root)`. React: `useRepelBento(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.
20
+
21
+ 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,65 @@
1
+ import { gsap, registerMotion } from '../../core/gsap';
2
+ import { motionSpring } from '../../core/physics-spring';
3
+ import { reduced } from '../../core/reduced';
4
+ import { createScope } from '../../core/scope';
5
+ import { TIMING } from '../../core/timing';
6
+ import { clearStyles } from '../../core/styles';
7
+ import { inert, type Instance } from '../../core/types';
8
+
9
+ export interface RepelBentoOptions { distance?: number; breakpoint?: number; }
10
+ export interface RepelBentoInstance extends Instance { play(): void; reset(): void; }
11
+
12
+ export function createRepelBento(root: HTMLElement | null, options: RepelBentoOptions = {}): RepelBentoInstance {
13
+ if (!root || reduced()) return inert({ play() {}, reset() {} });
14
+ registerMotion();
15
+ const scope = createScope();
16
+ const items = [...root.children] as HTMLElement[];
17
+ const animations = new Map<HTMLElement, gsap.core.Tween>();
18
+ const defaults = TIMING.repelBento;
19
+ const enabled = () => innerWidth >= (options.breakpoint ?? defaults.breakpoint) && !matchMedia('(pointer: coarse)').matches;
20
+ items.forEach((item) => scope.attr(item, 'style', item.getAttribute('style')));
21
+ const animate = (item: HTMLElement, vars: gsap.TweenVars) => {
22
+ animations.get(item)?.kill();
23
+ animations.set(item, gsap.to(item, { ...vars, overwrite: true }));
24
+ };
25
+ const reset = () => {
26
+ if (scope.disposed) return;
27
+ items.forEach((item) => animate(item, { x: 0, y: 0, scale: 1, opacity: 1, ...motionSpring(...TIMING.springs.bentoRepel) }));
28
+ };
29
+ const play = () => {
30
+ if (scope.disposed || !enabled()) return;
31
+ items.forEach((item, index) => {
32
+ const rect = item.getBoundingClientRect();
33
+ gsap.set(item, { x: (rect.left + rect.width / 2 - innerWidth / 2) * defaults.offset, y: (rect.top + rect.height / 2 - innerHeight / 2) * defaults.offset, opacity: 0 });
34
+ animate(item, { x: 0, y: 0, opacity: 1, delay: index * defaults.stagger, ...motionSpring(...TIMING.springs.bentoEnter) });
35
+ });
36
+ };
37
+ items.forEach((hovered) => scope.on(hovered, 'pointerenter', () => {
38
+ if (!enabled()) return;
39
+ const origin = hovered.getBoundingClientRect();
40
+ items.forEach((item) => {
41
+ if (item === hovered) { animate(item, { x: 0, y: 0, scale: 1.02, opacity: 1, ...motionSpring(...TIMING.springs.bentoRepel) }); return; }
42
+ const rect = item.getBoundingClientRect();
43
+ const deltaX = rect.left + rect.width / 2 - origin.left - origin.width / 2;
44
+ const deltaY = rect.top + rect.height / 2 - origin.top - origin.height / 2;
45
+ const distance = Math.hypot(deltaX, deltaY);
46
+ const magnitude = distance > 0 ? Math.min(defaults.strength / distance, options.distance ?? defaults.distance) : 0;
47
+ animate(item, { x: distance ? deltaX / distance * magnitude : 0, y: distance ? deltaY / distance * magnitude : 0, scale: 1, opacity: 1, ...motionSpring(...TIMING.springs.bentoRepel) });
48
+ });
49
+ }));
50
+ scope.on(root, 'pointerleave', reset);
51
+ scope.on(window, 'resize', reset);
52
+ scope.add(() => {
53
+ animations.forEach((animation) => animation.kill());
54
+ clearStyles(items, ['transform', 'translate', 'rotate', 'scale', 'opacity']);
55
+ });
56
+ if (typeof IntersectionObserver === 'function') {
57
+ const observer = scope.observe(new IntersectionObserver(([entry]) => {
58
+ if (!entry?.isIntersecting) return;
59
+ play();
60
+ observer.disconnect();
61
+ }, { threshold: 0.15 }));
62
+ observer.observe(root);
63
+ } else play();
64
+ return { play, reset, destroy: () => scope.dispose() };
65
+ }
@@ -0,0 +1,3 @@
1
+ .mk-repel-bento { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; overflow: clip; overflow-clip-margin: 30px; }
2
+ .mk-repel-bento > * { min-width: 0; border-radius: 32px; }
3
+ @media (max-width: 680px) { .mk-repel-bento { grid-template-columns: 1fr; } }
@@ -0,0 +1,36 @@
1
+ # reveal
2
+
3
+ Un velo del color de la superficie la cubre y se abre como una onda desde un punto; luego se desvanece. El sobrepaso de 2 hace que la apertura pase la esquina más lejana a mitad de camino, y el final queda suave.
4
+
5
+ Origen: Stepbro — reveal (`reveal-wipe` en el kit; usado en la navegación, el calendario y la hora). Velocidades `fast` 0.5/0.25 s, `normal` 0.7/0.35, `slow` 1.05/0.5, `slower` 1.6/0.75 (recorrido/fundido), `power2.out` y fundido `power1.out`.
6
+
7
+ ## Vanilla / Astro
8
+
9
+ ```html
10
+ <div data-mk="reveal" data-mk-trigger="load" data-mk-origin="#abrir-calendario">…</div>
11
+ ```
12
+
13
+ Sin `autoInit`: `createReveal(el, opciones)`. Para un uso suelto: `revealFrom(el, { origin: boton, speed: 'slow' })`.
14
+
15
+ ## React
16
+
17
+ ```tsx
18
+ const ref = useRef<HTMLDivElement>(null);
19
+ const reveal = useReveal(ref);
20
+ // al abrir: reveal.current?.play(botonRef.current!)
21
+ ```
22
+
23
+ ## Opciones
24
+
25
+ | Opción | Por defecto | Qué hace |
26
+ |---|---|---|
27
+ | `speed` | `'normal'` | `'fast'`, `'normal'`, `'slow'`, `'slower'` |
28
+ | `origin` | — | Selector del elemento desde cuyo centro sale la onda. Sin él: último puntero o el centro |
29
+ | `delay` | `0` | Segundos |
30
+ | `trigger` | `'manual'` | `'load'` la reproduce al crearse |
31
+
32
+ ## Controles
33
+
34
+ `play(origen?)`, `destroy()`
35
+
36
+ Con movimiento reducido no se reproduce.
@@ -0,0 +1,48 @@
1
+ import { entrances } from '../../core/reduced';
2
+ import { inert, warnOnce, type Instance } from '../../core/types';
3
+ import { revealFrom, type RevealHandle, type RevealSpeed } from '../../motion/reveal';
4
+
5
+ /*
6
+ * Stepbro's reveal on an element: a veil in its own colour opens like a wave
7
+ * from the origin (an element, else the last pointer position, else the centre).
8
+ */
9
+
10
+ export interface RevealRecipeOptions {
11
+ speed?: RevealSpeed;
12
+ /** CSS selector of the origin element (searched in the document). */
13
+ origin?: string;
14
+ delay?: number;
15
+ trigger?: 'manual' | 'load';
16
+ }
17
+
18
+ export interface RevealInstance extends Instance {
19
+ /** Play the wave, optionally from another origin. */
20
+ play(origin?: Element | { x: number; y: number }): void;
21
+ }
22
+
23
+ export function createReveal(el: HTMLElement | null, options: RevealRecipeOptions = {}): RevealInstance {
24
+ if (!el) {
25
+ warnOnce('reveal: missing element');
26
+ return inert({ play() {} });
27
+ }
28
+ const { speed = 'normal', delay = 0, trigger = 'manual' } = options;
29
+ let handle: RevealHandle | null = null;
30
+
31
+ const play = (origin?: Element | { x: number; y: number }) => {
32
+ handle?.cancel();
33
+ const from = origin ?? (options.origin ? document.querySelector(options.origin) : undefined);
34
+ handle = revealFrom(el, { speed, delay, origin: from });
35
+ };
36
+
37
+ if (trigger === 'load' && entrances()) play();
38
+ el.dataset.mkReady = '';
39
+
40
+ return {
41
+ play,
42
+ destroy() {
43
+ handle?.cancel();
44
+ handle = null;
45
+ delete el.dataset.mkReady;
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,21 @@
1
+ # rotator
2
+
3
+ Rotador de testimonios con autoplay, pausa al enfocar y controles manuales (Dousan).
4
+
5
+ ```html
6
+ <div class="mk-rotator" data-mk="rotator"><blockquote>Uno</blockquote><blockquote>Dos</blockquote></div>
7
+ ```
8
+
9
+ ```ts
10
+ import { createRotator } from '@yewdiel21/motion-kit';
11
+ import { useRotator } from '@yewdiel21/motion-kit/react';
12
+ import '@yewdiel21/motion-kit/css';
13
+ const instance = createRotator(root);
14
+ instance.destroy();
15
+ ```
16
+
17
+ `interval` 8000 ms, `autoplay` true, `index` 0, `onChange(index)`. `select(index)`, `next()`, `previous()`, `index`. Pausa al señalar, enfocar o esconder la pestaña; los controles reinician el intervalo. Solo la pieza activa es visible y accesible. Entrada opacity/y 5 a 0, .25 s. Origen: testimonios de Dousan.
18
+
19
+ Vanilla declarativo: `autoInit(root)`. React: `useRotator(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.
20
+
21
+ 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,66 @@
1
+ import { gsap, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { wrap } from '../../core/math';
4
+ import { createScope } from '../../core/scope';
5
+ import { TIMING } from '../../core/timing';
6
+ import { clearStyles } from '../../core/styles';
7
+ import { inert, type Instance } from '../../core/types';
8
+
9
+ export interface RotatorOptions {
10
+ interval?: number;
11
+ autoplay?: boolean;
12
+ index?: number;
13
+ onChange?: (index: number) => void;
14
+ }
15
+ export interface RotatorInstance extends Instance {
16
+ select(index: number): void;
17
+ next(): void;
18
+ previous(): void;
19
+ readonly index: number;
20
+ }
21
+
22
+ export function createRotator(root: HTMLElement | null, options: RotatorOptions = {}): RotatorInstance {
23
+ const items = root ? [...root.children] as HTMLElement[] : [];
24
+ if (!root || !items.length) return inert({ select() {}, next() {}, previous() {}, index: 0 });
25
+ registerMotion();
26
+ const scope = createScope();
27
+ const still = reduced();
28
+ let index = 0;
29
+ let hovered = false;
30
+ let focused = false;
31
+ let timer: ReturnType<typeof setInterval> | undefined;
32
+ let animation: gsap.core.Tween | undefined;
33
+ for (const item of items) scope.attr(item, 'style', item.getAttribute('style'));
34
+ const select = (next: number, notify = true) => {
35
+ if (scope.disposed || !Number.isFinite(next)) return;
36
+ index = wrap(Math.round(next), items.length);
37
+ animation?.kill();
38
+ for (const [position, item] of items.entries()) {
39
+ scope.attr(item, 'hidden', position === index ? null : '');
40
+ scope.attr(item, 'inert', position === index ? null : '');
41
+ }
42
+ if (!still) animation = gsap.fromTo(items[index]!, { opacity: 0, y: TIMING.rotator.y }, { opacity: 1, y: 0, duration: TIMING.rotator.duration, overwrite: true });
43
+ if (notify) {
44
+ options.onChange?.(index);
45
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { index } }));
46
+ }
47
+ };
48
+ const restart = () => {
49
+ clearInterval(timer);
50
+ if (scope.disposed || still || options.autoplay === false || hovered || focused || document.hidden || items.length < 2) return;
51
+ timer = setInterval(() => select(index + 1), Math.max(100, options.interval ?? TIMING.rotator.interval));
52
+ };
53
+ const move = (next: number) => { select(next); restart(); };
54
+ scope.on(root, 'pointerenter', () => { hovered = true; restart(); });
55
+ scope.on(root, 'pointerleave', () => { hovered = false; restart(); });
56
+ scope.on(root, 'focusin', () => { focused = true; restart(); });
57
+ scope.on(root, 'focusout', (event) => {
58
+ focused = root.contains(event.relatedTarget as Node | null);
59
+ restart();
60
+ });
61
+ scope.on(document, 'visibilitychange', restart);
62
+ scope.add(() => { clearInterval(timer); animation?.kill(); clearStyles(items, ['transform', 'translate', 'rotate', 'scale', 'opacity']); });
63
+ select(options.index ?? 0, false);
64
+ restart();
65
+ return { select: move, next: () => move(index + 1), previous: () => move(index - 1), get index() { return index; }, destroy: () => scope.dispose() };
66
+ }
@@ -0,0 +1,3 @@
1
+ .mk-rotator { min-height: 12rem; }
2
+ .mk-rotator > [hidden] { display: none !important; }
3
+ .mk-rotator > :not([hidden]) { margin: 0; }