@yewdiel21/motion-kit 0.9.0 → 0.10.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 (399) hide show
  1. package/NOTICE +3 -35
  2. package/README.md +93 -81
  3. package/dist/auto/index.d.ts +22 -3
  4. package/dist/auto/index.js +43 -5
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-KLY5BRRI.js → chunk-34EZ7DF4.js} +1 -1
  7. package/dist/chunk-34EZ7DF4.js.map +1 -0
  8. package/dist/{chunk-RFDIZYFX.js → chunk-DOOMULVV.js} +20 -20
  9. package/dist/chunk-DOOMULVV.js.map +1 -0
  10. package/dist/{chunk-ABNRDDO7.js → chunk-I3DXF2KQ.js} +2 -2
  11. package/dist/{chunk-Q4LVFNLN.js → chunk-IKQ5FODN.js} +4 -4
  12. package/dist/chunk-IKQ5FODN.js.map +1 -0
  13. package/dist/{chunk-2BQGFOA7.js → chunk-J6OCFNGU.js} +2 -2
  14. package/dist/chunk-J6OCFNGU.js.map +1 -0
  15. package/dist/{chunk-6OJAP6ZL.js → chunk-QFBOHPWZ.js} +3 -3
  16. package/dist/chunk-QFBOHPWZ.js.map +1 -0
  17. package/dist/{chunk-J27KH2A2.js → chunk-RVSKEBIC.js} +1 -1
  18. package/dist/chunk-RVSKEBIC.js.map +1 -0
  19. package/dist/{chunk-34XYOXHG.js → chunk-TFNRIZHU.js} +1615 -12
  20. package/dist/chunk-TFNRIZHU.js.map +1 -0
  21. package/dist/{chunk-FS2HJVVY.js → chunk-XAKC2QY7.js} +1204 -5
  22. package/dist/chunk-XAKC2QY7.js.map +1 -0
  23. package/dist/css/components/calendar.css +159 -0
  24. package/dist/css/components/chat-background.css +2 -2
  25. package/dist/css/components/chat.css +1 -1
  26. package/dist/css/components/cursor.css +3 -3
  27. package/dist/css/components/dialog.css +3 -3
  28. package/dist/css/components/hover-card.css +14 -0
  29. package/dist/css/components/input-otp.css +10 -0
  30. package/dist/css/components/island.css +1 -1
  31. package/dist/css/components/menu.css +1 -1
  32. package/dist/css/components/nav.css +1 -5
  33. package/dist/css/components/pagination.css +11 -0
  34. package/dist/css/components/quantity.css +16 -0
  35. package/dist/css/components/slot-picker.css +126 -0
  36. package/dist/css/components/steps.css +2 -2
  37. package/dist/css/components/studio-dock.css +35 -0
  38. package/dist/css/components/theme-picker.css +1 -1
  39. package/dist/css/components/toast.css +1 -1
  40. package/dist/css/components/trailer.css +37 -0
  41. package/dist/css/controls.css +78 -13
  42. package/dist/css/effects.css +8 -8
  43. package/dist/css/fonts.css +1 -1
  44. package/dist/css/themes/black-green.css +1 -1
  45. package/dist/css/themes/black.css +1 -1
  46. package/dist/css/themes/blue.css +1 -1
  47. package/dist/css/themes/brown.css +1 -1
  48. package/dist/css/themes/cold-blue.css +1 -1
  49. package/dist/css/themes/funny.css +1 -1
  50. package/dist/css/themes/gray.css +1 -1
  51. package/dist/css/themes/green.css +1 -1
  52. package/dist/css/themes/modern-1.css +1 -1
  53. package/dist/css/themes/neogreen.css +1 -1
  54. package/dist/css/themes/new.css +1 -1
  55. package/dist/css/themes/new2.css +1 -1
  56. package/dist/css/themes/oled.css +1 -1
  57. package/dist/css/themes/orange.css +1 -1
  58. package/dist/css/themes/pastel-pink.css +1 -1
  59. package/dist/css/themes/pink.css +1 -1
  60. package/dist/css/themes/purple.css +1 -1
  61. package/dist/css/themes/red.css +1 -1
  62. package/dist/css/themes/super-blue.css +1 -1
  63. package/dist/css/themes.css +19 -19
  64. package/dist/css/tokens.css +9 -9
  65. package/dist/elements/index.d.ts +75 -7
  66. package/dist/elements/index.js +172 -6
  67. package/dist/elements/index.js.map +1 -1
  68. package/dist/{engine-CUr9QijM.d.ts → engine-GDGwQnJE.d.ts} +2 -2
  69. package/dist/gl/index.js +3 -3
  70. package/dist/index.css +1076 -63
  71. package/dist/index.d.ts +17 -17
  72. package/dist/index.js +49 -7
  73. package/dist/index.js.map +1 -1
  74. package/dist/react/index.d.ts +139 -40
  75. package/dist/react/index.js +355 -8
  76. package/dist/react/index.js.map +1 -1
  77. package/dist/{reduced-BM4Ls0g5.d.ts → reduced-BmBzKH94.d.ts} +215 -18
  78. package/dist/{reveal-WFUWDrei.d.ts → reveal-jsX-uwUz.d.ts} +233 -24
  79. package/dist/{store-Bqy_obnD.d.ts → store-DYb3tTsZ.d.ts} +5 -5
  80. package/dist/theme/generate.d.ts +1 -1
  81. package/dist/theme/generate.js +2 -2
  82. package/dist/theme/generate.js.map +1 -1
  83. package/dist/theme/index.d.ts +2 -2
  84. package/dist/theme/index.js +2 -2
  85. package/dist/{theme-BFMoCzi6.d.ts → theme-Bjf9qIIy.d.ts} +1 -1
  86. package/package.json +2 -2
  87. package/registry.json +230 -156
  88. package/skills/motion-kit/SKILL.md +209 -189
  89. package/skills/motion-kit/reference/components.md +5 -5
  90. package/skills/motion-kit/reference/design.md +14 -14
  91. package/skills/motion-kit/reference/recipes.md +48 -49
  92. package/src/auto/auto-init.ts +38 -0
  93. package/src/components/calendar/README.md +82 -0
  94. package/src/components/calendar/calendar.css +158 -0
  95. package/src/components/calendar/element.ts +27 -0
  96. package/src/components/calendar/engine.ts +412 -0
  97. package/src/components/calendar/react.tsx +69 -0
  98. package/src/components/chat/README.md +1 -3
  99. package/src/components/chat/chat.css +1 -1
  100. package/src/components/chat/engine.ts +1 -1
  101. package/src/components/chat/react.tsx +1 -1
  102. package/src/components/chat-background/README.md +1 -3
  103. package/src/components/chat-background/audio.ts +1 -1
  104. package/src/components/chat-background/chat-background.css +2 -2
  105. package/src/components/chat-background/engine.ts +1 -1
  106. package/src/components/chat-background/react.tsx +1 -1
  107. package/src/components/cursor/README.md +3 -5
  108. package/src/components/cursor/cursor.css +3 -3
  109. package/src/components/cursor/element.ts +2 -2
  110. package/src/components/cursor/engine.ts +5 -5
  111. package/src/components/cursor/react.tsx +1 -1
  112. package/src/components/dialog/README.md +4 -8
  113. package/src/components/dialog/dialog.css +3 -3
  114. package/src/components/dialog/engine.ts +2 -2
  115. package/src/components/dialog/react.tsx +1 -1
  116. package/src/components/hover-card/README.md +16 -0
  117. package/src/components/hover-card/element.ts +8 -0
  118. package/src/components/hover-card/engine.ts +69 -0
  119. package/src/components/hover-card/hover-card.css +13 -0
  120. package/src/components/hover-card/react.tsx +19 -0
  121. package/src/components/input-otp/README.md +18 -0
  122. package/src/components/input-otp/element.ts +14 -0
  123. package/src/components/input-otp/engine.ts +106 -0
  124. package/src/components/input-otp/input-otp.css +9 -0
  125. package/src/components/input-otp/react.tsx +21 -0
  126. package/src/components/island/README.md +7 -7
  127. package/src/components/island/engine.ts +3 -3
  128. package/src/components/island/island.css +1 -1
  129. package/src/components/island/react.tsx +3 -3
  130. package/src/components/menu/README.md +2 -4
  131. package/src/components/menu/engine.ts +1 -1
  132. package/src/components/menu/menu.css +1 -1
  133. package/src/components/menu/react.tsx +1 -1
  134. package/src/components/nav/README.md +12 -14
  135. package/src/components/nav/engine.ts +5 -5
  136. package/src/components/nav/nav.css +1 -5
  137. package/src/components/nav/react.tsx +3 -3
  138. package/src/components/pagination/README.md +17 -0
  139. package/src/components/pagination/element.ts +14 -0
  140. package/src/components/pagination/engine.ts +102 -0
  141. package/src/components/pagination/pagination.css +10 -0
  142. package/src/components/pagination/react.tsx +22 -0
  143. package/src/components/quantity/README.md +20 -0
  144. package/src/components/quantity/element.ts +14 -0
  145. package/src/components/quantity/engine.ts +85 -0
  146. package/src/components/quantity/quantity.css +15 -0
  147. package/src/components/quantity/react.tsx +21 -0
  148. package/src/components/slot-picker/README.md +68 -0
  149. package/src/components/slot-picker/element.ts +27 -0
  150. package/src/components/slot-picker/engine.ts +412 -0
  151. package/src/components/slot-picker/react.tsx +76 -0
  152. package/src/components/slot-picker/slot-picker.css +125 -0
  153. package/src/components/steps/README.md +1 -3
  154. package/src/components/steps/engine.ts +3 -3
  155. package/src/components/steps/react.tsx +2 -2
  156. package/src/components/steps/steps.css +2 -2
  157. package/src/components/studio-dock/README.md +44 -0
  158. package/src/components/studio-dock/element.ts +11 -0
  159. package/src/components/studio-dock/engine.ts +212 -0
  160. package/src/components/studio-dock/react.tsx +23 -0
  161. package/src/components/studio-dock/studio-dock.css +34 -0
  162. package/src/components/theme-picker/README.md +3 -5
  163. package/src/components/theme-picker/engine.ts +2 -2
  164. package/src/components/theme-picker/react.tsx +1 -1
  165. package/src/components/theme-picker/theme-picker.css +1 -1
  166. package/src/components/toast/README.md +1 -3
  167. package/src/components/toast/engine.ts +2 -2
  168. package/src/components/toast/react.tsx +1 -1
  169. package/src/components/toast/store.ts +1 -2
  170. package/src/components/toast/toast.css +1 -1
  171. package/src/components/trailer/README.md +28 -0
  172. package/src/components/trailer/element.ts +13 -0
  173. package/src/components/trailer/engine.ts +189 -0
  174. package/src/components/trailer/react.tsx +16 -0
  175. package/src/components/trailer/trailer.css +36 -0
  176. package/src/core/channels.ts +3 -3
  177. package/src/core/curves.ts +3 -3
  178. package/src/core/dates.ts +98 -0
  179. package/src/core/gsap.ts +2 -2
  180. package/src/core/loop.ts +1 -1
  181. package/src/core/math.ts +1 -1
  182. package/src/core/physics-spring.ts +2 -2
  183. package/src/core/scroll-track.ts +2 -4
  184. package/src/core/spring.ts +1 -1
  185. package/src/core/timing.ts +21 -22
  186. package/src/css/controls/README.md +24 -20
  187. package/src/css/controls/burger.css +1 -1
  188. package/src/css/controls/button.css +4 -4
  189. package/src/css/controls/checkbox.css +1 -1
  190. package/src/css/controls/chip.css +1 -1
  191. package/src/css/controls/field.css +2 -2
  192. package/src/css/controls/glass.css +4 -4
  193. package/src/css/controls/status.css +63 -0
  194. package/src/css/default-theme.css +1 -1
  195. package/src/css/effects/README.md +19 -21
  196. package/src/css/effects/anim.css +1 -1
  197. package/src/css/effects/fade.css +1 -1
  198. package/src/css/effects/float.css +1 -1
  199. package/src/css/effects/frost.css +1 -1
  200. package/src/css/effects/grain.css +1 -1
  201. package/src/css/effects/loading.css +1 -1
  202. package/src/css/effects/success.css +1 -1
  203. package/src/css/effects/text.css +1 -1
  204. package/src/css/fonts.css +1 -1
  205. package/src/css/states.css +1 -1
  206. package/src/css/themes/black-green.css +1 -1
  207. package/src/css/themes/black.css +1 -1
  208. package/src/css/themes/blue.css +1 -1
  209. package/src/css/themes/brown.css +1 -1
  210. package/src/css/themes/cold-blue.css +1 -1
  211. package/src/css/themes/funny.css +1 -1
  212. package/src/css/themes/gray.css +1 -1
  213. package/src/css/themes/green.css +1 -1
  214. package/src/css/themes/modern-1.css +1 -1
  215. package/src/css/themes/neogreen.css +1 -1
  216. package/src/css/themes/new.css +1 -1
  217. package/src/css/themes/new2.css +1 -1
  218. package/src/css/themes/oled.css +1 -1
  219. package/src/css/themes/orange.css +1 -1
  220. package/src/css/themes/pastel-pink.css +1 -1
  221. package/src/css/themes/pink.css +1 -1
  222. package/src/css/themes/purple.css +1 -1
  223. package/src/css/themes/red.css +1 -1
  224. package/src/css/themes/super-blue.css +1 -1
  225. package/src/css/tokens.css +8 -8
  226. package/src/elements/index.ts +24 -0
  227. package/src/gl/liquid-blend/README.md +3 -3
  228. package/src/gl/matcap/README.md +3 -3
  229. package/src/gl/particle-hero/README.md +1 -1
  230. package/src/gl/particle-hero/create-particle-hero.ts +3 -5
  231. package/src/gl/particle-hero/shaders.ts +1 -1
  232. package/src/gl/shader-blur/README.md +3 -3
  233. package/src/gl/shader-grain/README.md +3 -3
  234. package/src/index.ts +25 -0
  235. package/src/motion/chat.ts +2 -2
  236. package/src/motion/confetti.ts +1 -1
  237. package/src/motion/count.ts +1 -1
  238. package/src/motion/details.ts +1 -1
  239. package/src/motion/drag-dismiss.ts +1 -1
  240. package/src/motion/effects.ts +1 -1
  241. package/src/motion/entrances.ts +1 -1
  242. package/src/motion/floating.ts +1 -1
  243. package/src/motion/flow.ts +2 -2
  244. package/src/motion/indicator.ts +1 -1
  245. package/src/motion/layout.ts +1 -1
  246. package/src/motion/liquid.ts +1 -1
  247. package/src/motion/morph.ts +2 -2
  248. package/src/motion/reveal.ts +1 -1
  249. package/src/motion/sheet.ts +1 -1
  250. package/src/motion/surface.ts +1 -1
  251. package/src/react/animated-number.tsx +2 -2
  252. package/src/react/flip-group.tsx +1 -1
  253. package/src/react/frost.tsx +1 -1
  254. package/src/react/index.ts +21 -0
  255. package/src/react/slot-text.tsx +1 -1
  256. package/src/react/use-action-button.ts +6 -0
  257. package/src/react/use-card-dock.ts +1 -1
  258. package/src/react/use-cascade.ts +1 -1
  259. package/src/react/use-contrast.ts +1 -1
  260. package/src/react/use-depth-grid.ts +1 -1
  261. package/src/react/use-editorial-reveal.ts +7 -0
  262. package/src/react/use-fill-text.ts +1 -1
  263. package/src/react/use-film-hero.ts +6 -0
  264. package/src/react/use-font-cycle.ts +7 -0
  265. package/src/react/use-frame-zoom.ts +1 -1
  266. package/src/react/use-image-zoom.ts +5 -0
  267. package/src/react/use-joined-cta.ts +5 -0
  268. package/src/react/use-letters.ts +1 -1
  269. package/src/react/use-local-clock.ts +5 -0
  270. package/src/react/use-marquee.ts +1 -1
  271. package/src/react/use-mask-text.ts +1 -1
  272. package/src/react/use-parallax.ts +1 -1
  273. package/src/react/use-particle-hero.ts +1 -1
  274. package/src/react/use-scroll-dock.ts +5 -0
  275. package/src/react/use-snap-strip.ts +1 -1
  276. package/src/react/use-sticker.ts +5 -0
  277. package/src/react/use-studio-dock.ts +8 -0
  278. package/src/react/use-text-roll.ts +1 -1
  279. package/src/react/use-thumbnail-nav.ts +7 -0
  280. package/src/react/use-trailer.ts +6 -0
  281. package/src/react/use-weight-hover.ts +5 -0
  282. package/src/recipes/action-button/README.md +31 -0
  283. package/src/recipes/action-button/action-button.css +113 -0
  284. package/src/recipes/action-button/create-action-button.ts +56 -0
  285. package/src/recipes/card-dock/README.md +1 -1
  286. package/src/recipes/card-dock/card-dock.css +1 -1
  287. package/src/recipes/card-dock/create-card-dock.ts +2 -2
  288. package/src/recipes/cascade/README.md +1 -3
  289. package/src/recipes/cascade/create-cascade.ts +3 -3
  290. package/src/recipes/contrast/README.md +0 -2
  291. package/src/recipes/contrast/create-contrast.ts +1 -1
  292. package/src/recipes/counter/README.md +0 -2
  293. package/src/recipes/counter/create-counter.ts +1 -1
  294. package/src/recipes/curved-carousel/README.md +0 -2
  295. package/src/recipes/curved-carousel/create-curved-carousel.ts +2 -2
  296. package/src/recipes/curved-carousel/curved-carousel.css +1 -1
  297. package/src/recipes/depth-grid/README.md +0 -2
  298. package/src/recipes/depth-grid/create-depth-grid.ts +3 -4
  299. package/src/recipes/details/README.md +0 -2
  300. package/src/recipes/details/create-details.ts +1 -1
  301. package/src/recipes/dock-pill/README.md +3 -3
  302. package/src/recipes/drag-ruler/README.md +3 -3
  303. package/src/recipes/editorial-reveal/README.md +35 -0
  304. package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
  305. package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
  306. package/src/recipes/fill-text/README.md +1 -1
  307. package/src/recipes/fill-text/create-fill-text.ts +1 -1
  308. package/src/recipes/film-hero/README.md +23 -0
  309. package/src/recipes/film-hero/create-film-hero.ts +65 -0
  310. package/src/recipes/film-hero/film-hero.css +29 -0
  311. package/src/recipes/fit-text/README.md +0 -2
  312. package/src/recipes/flow/README.md +0 -2
  313. package/src/recipes/flow/create-flow.ts +1 -1
  314. package/src/recipes/font-cycle/README.md +11 -0
  315. package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
  316. package/src/recipes/font-cycle/font-cycle.css +36 -0
  317. package/src/recipes/frame-zoom/README.md +1 -3
  318. package/src/recipes/frame-zoom/create-frame-zoom.ts +2 -2
  319. package/src/recipes/image-zoom/README.md +16 -0
  320. package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
  321. package/src/recipes/image-zoom/image-zoom.css +7 -0
  322. package/src/recipes/infinite-reel/README.md +3 -3
  323. package/src/recipes/joined-cta/README.md +13 -0
  324. package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
  325. package/src/recipes/joined-cta/joined-cta.css +17 -0
  326. package/src/recipes/letters/README.md +2 -4
  327. package/src/recipes/letters/create-letters.ts +3 -3
  328. package/src/recipes/local-clock/README.md +31 -0
  329. package/src/recipes/local-clock/create-local-clock.ts +61 -0
  330. package/src/recipes/marquee/README.md +0 -2
  331. package/src/recipes/marquee/create-marquee.ts +3 -3
  332. package/src/recipes/marquee/marquee.css +1 -1
  333. package/src/recipes/mask-text/README.md +3 -5
  334. package/src/recipes/mask-text/create-mask-text.ts +4 -4
  335. package/src/recipes/parallax/README.md +1 -3
  336. package/src/recipes/parallax/create-parallax.ts +1 -1
  337. package/src/recipes/pile/README.md +0 -2
  338. package/src/recipes/pile/create-pile.ts +1 -1
  339. package/src/recipes/pile/pile-math.ts +1 -1
  340. package/src/recipes/pile/pile.css +1 -1
  341. package/src/recipes/pill/README.md +0 -2
  342. package/src/recipes/pill/pill.css +2 -2
  343. package/src/recipes/pop/README.md +0 -2
  344. package/src/recipes/pop/create-pop.ts +1 -1
  345. package/src/recipes/radial-menu/README.md +0 -2
  346. package/src/recipes/radial-menu/create-radial-menu.ts +1 -1
  347. package/src/recipes/radial-menu/radial-menu.css +1 -1
  348. package/src/recipes/reflow/README.md +3 -5
  349. package/src/recipes/reflow/create-reflow.ts +1 -1
  350. package/src/recipes/repel-bento/README.md +3 -3
  351. package/src/recipes/reveal/README.md +0 -2
  352. package/src/recipes/reveal/create-reveal.ts +1 -1
  353. package/src/recipes/rotator/README.md +3 -3
  354. package/src/recipes/scroll-dock/README.md +46 -0
  355. package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
  356. package/src/recipes/scroll-dock/scroll-dock.css +70 -0
  357. package/src/recipes/slot-text/README.md +0 -2
  358. package/src/recipes/slot-text/create-slot-text.ts +1 -1
  359. package/src/recipes/snap-strip/README.md +1 -1
  360. package/src/recipes/snap-strip/create-snap-strip.ts +5 -6
  361. package/src/recipes/snap-strip/snap-strip.css +1 -1
  362. package/src/recipes/sortable/README.md +3 -5
  363. package/src/recipes/sortable/create-sortable.ts +4 -4
  364. package/src/recipes/sortable/sortable.css +1 -1
  365. package/src/recipes/spring-cards/README.md +0 -2
  366. package/src/recipes/spring-cards/create-spring-cards.ts +1 -1
  367. package/src/recipes/spring-cards/spring-cards.css +1 -1
  368. package/src/recipes/sticker/README.md +59 -0
  369. package/src/recipes/sticker/create-sticker.ts +490 -0
  370. package/src/recipes/sticker/sticker.css +112 -0
  371. package/src/recipes/stretch-footer/README.md +0 -2
  372. package/src/recipes/stretch-footer/create-stretch-footer.ts +1 -1
  373. package/src/recipes/stretch-footer/stretch-footer.css +1 -1
  374. package/src/recipes/tabs/README.md +0 -2
  375. package/src/recipes/tabs/create-tabs.ts +1 -1
  376. package/src/recipes/text-roll/README.md +1 -3
  377. package/src/recipes/text-roll/create-text-roll.ts +3 -3
  378. package/src/recipes/text-roll/text-roll.css +1 -1
  379. package/src/recipes/thumbnail-nav/README.md +16 -0
  380. package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
  381. package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
  382. package/src/recipes/ticket-menu/README.md +3 -3
  383. package/src/recipes/weight-hover/README.md +37 -0
  384. package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
  385. package/src/recipes/weight-hover/weight-hover.css +7 -0
  386. package/src/recipes/words/README.md +0 -2
  387. package/src/recipes/words/create-words.ts +1 -1
  388. package/src/theme/generate.ts +1 -1
  389. package/src/theme/presets.ts +2 -2
  390. package/src/theme/theme.ts +1 -2
  391. package/dist/chunk-2BQGFOA7.js.map +0 -1
  392. package/dist/chunk-34XYOXHG.js.map +0 -1
  393. package/dist/chunk-6OJAP6ZL.js.map +0 -1
  394. package/dist/chunk-FS2HJVVY.js.map +0 -1
  395. package/dist/chunk-J27KH2A2.js.map +0 -1
  396. package/dist/chunk-KLY5BRRI.js.map +0 -1
  397. package/dist/chunk-Q4LVFNLN.js.map +0 -1
  398. package/dist/chunk-RFDIZYFX.js.map +0 -1
  399. /package/dist/{chunk-ABNRDDO7.js.map → chunk-I3DXF2KQ.js.map} +0 -0
@@ -0,0 +1,412 @@
1
+ import { CustomEase, gsap, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import {
6
+ addDays, addMonths, capitalize, clampFirstDay, dayKey, monthWeeks, safeFormat, sameDay, sameMonth,
7
+ fromJson, startOfWeek, toDate, weekdayNames, type DateInput,
8
+ } from '../../core/dates';
9
+ import { flowIn } from '../../motion/flow';
10
+
11
+ /*
12
+ * Slot picker (Melon Mind "Agendar cita"): a compact month on one side and the
13
+ * day's times on the other. From the reference: changing day slides the times
14
+ * out −30 px and the new ones in from +30 px, .25 s ease-out (view transition
15
+ * "time-slot", schedulePicker.css); the first list grows in (scale .7, blur 32,
16
+ * .3 s cubic-bezier(.38,.49,0,1.1)); the day heading opens 0 → 56 px in .5 s
17
+ * cubic-bezier(.38,.49,0,1.4) and a picked time lights up in .7 s
18
+ * (animation-highlight-1). Month changes slide the days the same way.
19
+ */
20
+
21
+ export type SlotSource = string[] | Record<string, string[]> | ((date: Date) => string[] | Promise<string[]>);
22
+
23
+ export interface SlotValue { date: Date; time: string; key: string; value: string }
24
+
25
+ export interface SlotPickerLabels {
26
+ placeholder?: string;
27
+ empty?: string;
28
+ loading?: string;
29
+ heading?: (date: Date) => string;
30
+ }
31
+
32
+ export interface SlotPickerOptions {
33
+ /** Month shown first (and the selected day when `value` is not given). */
34
+ date?: DateInput;
35
+ /** Initial value: "2026-10-23T10:30" or { date, time }. */
36
+ value?: string | { date: DateInput; time: string } | null;
37
+ slots?: SlotSource;
38
+ min?: DateInput;
39
+ max?: DateInput;
40
+ /** Weekdays never available (0 = Sunday). */
41
+ closedDays?: number[];
42
+ isDisabled?: (date: Date) => boolean;
43
+ firstDay?: number;
44
+ locale?: string;
45
+ labels?: SlotPickerLabels;
46
+ onChange?: (value: SlotValue) => void;
47
+ onDateChange?: (date: Date) => void;
48
+ }
49
+
50
+ export interface SlotPickerInstance extends Instance {
51
+ selectDate(date: DateInput): void;
52
+ selectTime(time: string | null): void;
53
+ setSlots(slots: SlotSource): void;
54
+ readonly date: Date | null;
55
+ readonly time: string | null;
56
+ readonly value: string | null;
57
+ }
58
+
59
+ const SLIDE = 30;
60
+ const SLIDE_DURATION = 0.25;
61
+ let slotEaseReady = false;
62
+
63
+ export function createSlotPicker(root: HTMLElement | null, options: SlotPickerOptions = {}): SlotPickerInstance {
64
+ const daysHost = root?.querySelector<HTMLElement>('[data-mk-slot-days]');
65
+ const timesHost = root?.querySelector<HTMLElement>('[data-mk-slot-times]');
66
+ if (!root || !daysHost || !timesHost) {
67
+ return inert({ selectDate() {}, selectTime() {}, setSlots() {}, date: null, time: null, value: null });
68
+ }
69
+ registerMotion();
70
+ if (!slotEaseReady && typeof window !== 'undefined') {
71
+ CustomEase.create('mk-slot-in', '.38,.49,0,1.1');
72
+ slotEaseReady = true;
73
+ }
74
+ const scope = createScope();
75
+ const ctx = gsap.context(() => {});
76
+ scope.add(() => ctx.revert());
77
+ const locale = options.locale ?? 'es';
78
+ const firstDay = clampFirstDay(options.firstDay);
79
+ const labels = {
80
+ placeholder: options.labels?.placeholder ?? 'Elige un día para ver los horarios.',
81
+ empty: options.labels?.empty ?? 'No hay horarios este día.',
82
+ loading: options.labels?.loading ?? 'Cargando horarios…',
83
+ heading: options.labels?.heading ?? ((d: Date) => capitalize(safeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long' }).format(d))),
84
+ };
85
+ const min = options.min !== undefined ? toDate(options.min) : null;
86
+ const max = options.max !== undefined ? toDate(options.max) : null;
87
+ const closedList = fromJson<number[]>(options.closedDays);
88
+ const closed = new Set((Array.isArray(closedList) ? closedList : []).filter((n) => Number.isInteger(n)));
89
+ let slots: SlotSource = fromJson<SlotSource>(options.slots) ?? [];
90
+
91
+ const initial = (() => {
92
+ const v = options.value;
93
+ if (typeof v === 'string') {
94
+ const match = /^(\d{4}-\d{2}-\d{2})(?:T(\d{1,2}:\d{2}))?/.exec(v);
95
+ return match ? { date: toDate(match[1]), time: match[2] ?? null } : null;
96
+ }
97
+ return v ? { date: toDate(v.date), time: v.time ?? null } : null;
98
+ })();
99
+ let month = initial?.date ?? toDate(options.date);
100
+ let date: Date | null = initial?.date ?? (options.date !== undefined ? toDate(options.date) : null);
101
+ let time: string | null = initial?.time ?? null;
102
+ let focusDay = date ?? month;
103
+ let times: string[] = [];
104
+ let request = 0;
105
+
106
+ const title = root.querySelector<HTMLElement>('[data-mk-slot-title]');
107
+ const heading = root.querySelector<HTMLElement>('[data-mk-slot-heading]');
108
+ const prev = root.querySelector<HTMLButtonElement>('[data-mk-slot-prev]');
109
+ const next = root.querySelector<HTMLButtonElement>('[data-mk-slot-next]');
110
+ const output = root.querySelector<HTMLInputElement>('[data-mk-slot-value]');
111
+ const originals = [daysHost, timesHost, title, heading].filter(Boolean).map((el) => [el!, [...el!.childNodes]] as const);
112
+ scope.add(() => originals.forEach(([el, nodes]) => el.replaceChildren(...nodes)));
113
+ if (output) {
114
+ const before = output.value;
115
+ scope.add(() => { output.value = before; });
116
+ }
117
+ scope.attr(root, 'data-mk-ready', '');
118
+ scope.attr(daysHost, 'role', 'grid');
119
+ scope.attr(timesHost, 'role', 'radiogroup');
120
+ if (heading) {
121
+ if (!heading.id) scope.attr(heading, 'id', `mk-slot-${Math.random().toString(36).slice(2, 8)}`);
122
+ scope.attr(timesHost, 'aria-labelledby', heading.id);
123
+ }
124
+ if (title) {
125
+ if (!title.id) scope.attr(title, 'id', `mk-slot-month-${Math.random().toString(36).slice(2, 8)}`);
126
+ scope.attr(daysHost, 'aria-labelledby', title.id);
127
+ }
128
+
129
+ const monthTitle = safeFormat(locale, { month: 'long', year: 'numeric' });
130
+ const fullDay = safeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
131
+ const disabled = (d: Date) => (min && d < min) || (max && d > max) || closed.has(d.getDay()) || !!options.isDisabled?.(d);
132
+
133
+ /* ── Days ───────────────────────────────────────────── */
134
+ let daysBody: HTMLElement | null = null;
135
+ const leaving = new Set<HTMLElement>();
136
+ scope.add(() => leaving.forEach((el) => el.remove()));
137
+ const buildDays = () => {
138
+ const wrap = document.createElement('div');
139
+ wrap.className = 'mk-slot-picker__days';
140
+ wrap.setAttribute('role', 'rowgroup');
141
+ const head = document.createElement('div');
142
+ head.className = 'mk-slot-picker__weekdays';
143
+ head.setAttribute('role', 'row');
144
+ for (const name of weekdayNames(locale, firstDay, 'short')) {
145
+ const cell = document.createElement('span');
146
+ cell.setAttribute('role', 'columnheader');
147
+ cell.textContent = name.slice(0, 2);
148
+ head.append(cell);
149
+ }
150
+ wrap.append(head);
151
+ const focusKey = dayKey(focusDay);
152
+ for (const week of monthWeeks(month, firstDay)) {
153
+ const row = document.createElement('div');
154
+ row.className = 'mk-slot-picker__week';
155
+ row.setAttribute('role', 'row');
156
+ for (const d of week) {
157
+ const cell = document.createElement('span');
158
+ cell.setAttribute('role', 'gridcell');
159
+ if (!sameMonth(d, month)) {
160
+ row.append(cell);
161
+ continue;
162
+ }
163
+ const button = document.createElement('button');
164
+ button.type = 'button';
165
+ button.className = 'mk-slot-picker__day';
166
+ button.dataset.mkSlotDay = dayKey(d);
167
+ button.textContent = String(d.getDate());
168
+ button.tabIndex = dayKey(d) === focusKey ? 0 : -1;
169
+ button.setAttribute('aria-label', capitalize(fullDay.format(d)));
170
+ if (disabled(d)) button.disabled = true;
171
+ if (sameDay(d, new Date())) button.setAttribute('aria-current', 'date');
172
+ cell.setAttribute('aria-selected', String(sameDay(d, date)));
173
+ cell.append(button);
174
+ row.append(cell);
175
+ }
176
+ wrap.append(row);
177
+ }
178
+ return wrap;
179
+ };
180
+ const renderDays = (direction: number) => {
181
+ const fresh = buildDays();
182
+ if (title) title.textContent = capitalize(monthTitle.format(month));
183
+ // No earlier month when its last day is before `min`; no later one when its first day is after `max`.
184
+ if (prev) scope.attr(prev, 'disabled', min && new Date(month.getFullYear(), month.getMonth(), 0) < min ? '' : null);
185
+ if (next) scope.attr(next, 'disabled', max && new Date(month.getFullYear(), month.getMonth() + 1, 1) > max ? '' : null);
186
+ if (daysBody && direction && !reduced()) {
187
+ const old = daysBody;
188
+ old.style.top = `${old.offsetTop}px`;
189
+ old.classList.add('mk-slot-picker__leaving');
190
+ old.setAttribute('aria-hidden', 'true');
191
+ leaving.add(old);
192
+ daysHost.append(fresh);
193
+ ctx.add(() => {
194
+ gsap.to(old, { x: -SLIDE * direction, opacity: 0, duration: SLIDE_DURATION, ease: 'power1.out', onComplete: () => { old.remove(); leaving.delete(old); } });
195
+ gsap.fromTo(fresh, { x: SLIDE * direction, opacity: 0 }, { x: 0, opacity: 1, duration: SLIDE_DURATION, ease: 'power1.out', clearProps: 'transform,opacity' });
196
+ });
197
+ } else if (daysBody) daysBody.replaceWith(fresh);
198
+ else daysHost.replaceChildren(fresh);
199
+ daysBody = fresh;
200
+ };
201
+ const dayButton = (d: Date) => daysHost.querySelector<HTMLButtonElement>(`.mk-slot-picker__days:not(.mk-slot-picker__leaving) [data-mk-slot-day="${dayKey(d)}"]`);
202
+ const moveFocus = (to: Date, focus: boolean) => {
203
+ focusDay = to;
204
+ if (!sameMonth(to, month)) {
205
+ const direction = to > month ? 1 : -1;
206
+ month = to;
207
+ renderDays(direction);
208
+ } else daysHost.querySelectorAll<HTMLButtonElement>('[data-mk-slot-day]').forEach((b) => (b.tabIndex = b.dataset.mkSlotDay === dayKey(to) ? 0 : -1));
209
+ if (focus) dayButton(to)?.focus();
210
+ };
211
+
212
+ /* ── Times ──────────────────────────────────────────── */
213
+ let timesBody: HTMLElement | null = null;
214
+ let firstList = true;
215
+ const buildTimes = (state: 'placeholder' | 'loading' | 'ready') => {
216
+ const wrap = document.createElement('div');
217
+ wrap.className = 'mk-slot-picker__list';
218
+ if (state !== 'ready' || !times.length) {
219
+ const note = document.createElement('p');
220
+ note.className = 'mk-slot-picker__note';
221
+ note.textContent = state === 'placeholder' ? labels.placeholder : state === 'loading' ? labels.loading : labels.empty;
222
+ wrap.append(note);
223
+ return wrap;
224
+ }
225
+ const focusTime = time && times.includes(time) ? time : times[0];
226
+ for (const t of times) {
227
+ const button = document.createElement('button');
228
+ button.type = 'button';
229
+ button.className = 'mk-slot-picker__time';
230
+ button.setAttribute('role', 'radio');
231
+ button.dataset.mkSlotTime = t;
232
+ button.textContent = t;
233
+ button.setAttribute('aria-checked', String(t === time));
234
+ button.tabIndex = t === focusTime ? 0 : -1;
235
+ wrap.append(button);
236
+ }
237
+ return wrap;
238
+ };
239
+ const renderTimes = (state: 'placeholder' | 'loading' | 'ready', direction: number) => {
240
+ const fresh = buildTimes(state);
241
+ scope.attr(timesHost, 'aria-busy', state === 'loading' ? 'true' : null);
242
+ const motion = !reduced();
243
+ if (timesBody && direction && motion) {
244
+ const old = timesBody;
245
+ old.style.top = `${old.offsetTop}px`;
246
+ old.classList.add('mk-slot-picker__leaving');
247
+ old.setAttribute('aria-hidden', 'true');
248
+ old.querySelectorAll('button').forEach((b) => (b.tabIndex = -1));
249
+ leaving.add(old);
250
+ timesHost.append(fresh);
251
+ ctx.add(() => {
252
+ gsap.to(old, { x: -SLIDE * direction, opacity: 0, duration: SLIDE_DURATION, ease: 'power1.out', onComplete: () => { old.remove(); leaving.delete(old); } });
253
+ gsap.fromTo(fresh, { x: SLIDE * direction, opacity: 0 }, { x: 0, opacity: 1, duration: SLIDE_DURATION, ease: 'power1.out', clearProps: 'transform,opacity' });
254
+ });
255
+ } else if (timesBody) timesBody.replaceWith(fresh);
256
+ else timesHost.replaceChildren(fresh);
257
+ timesBody = fresh;
258
+ if (state === 'ready' && times.length && motion && firstList) {
259
+ firstList = false;
260
+ ctx.add(() => flowIn(fresh, { target: '.mk-slot-picker__time', style: 2, startScale: 0.7, blur: 32, duration: 0.3, stagger: 0.02, ease: 'mk-slot-in' }));
261
+ }
262
+ };
263
+ const resolve = (d: Date): string[] | Promise<string[]> => {
264
+ const source = slots;
265
+ if (Array.isArray(source)) return source;
266
+ if (typeof source === 'function') return source(d);
267
+ return source?.[dayKey(d)] ?? [];
268
+ };
269
+ const clean = (list: unknown) => (Array.isArray(list) ? [...new Set(list.filter((t): t is string => typeof t === 'string' && /^\d{1,2}:\d{2}$/.test(t)))] : []);
270
+ const load = (direction: number) => {
271
+ if (!date) {
272
+ times = [];
273
+ renderTimes('placeholder', 0);
274
+ return;
275
+ }
276
+ const mine = ++request;
277
+ let result: string[] | Promise<string[]>;
278
+ try {
279
+ result = resolve(date);
280
+ } catch {
281
+ result = [];
282
+ }
283
+ if (result && typeof (result as Promise<string[]>).then === 'function') {
284
+ renderTimes('loading', direction);
285
+ (result as Promise<string[]>).then(
286
+ (list) => { if (mine === request && !scope.disposed) { times = clean(list); renderTimes('ready', 0); } },
287
+ () => { if (mine === request && !scope.disposed) { times = []; renderTimes('ready', 0); } },
288
+ );
289
+ } else {
290
+ times = clean(result);
291
+ if (time && !times.includes(time)) time = null;
292
+ renderTimes('ready', direction);
293
+ }
294
+ };
295
+
296
+ const syncHeading = () => {
297
+ if (!heading) return;
298
+ if (date) {
299
+ heading.textContent = labels.heading(date);
300
+ if (!heading.hasAttribute('data-mk-shown')) scope.attr(heading, 'data-mk-shown', '');
301
+ }
302
+ };
303
+ const emit = () => {
304
+ const key = date ? dayKey(date) : '';
305
+ const value = date && time ? `${key}T${time}` : null;
306
+ if (output) {
307
+ output.value = value ?? '';
308
+ output.dispatchEvent(new Event('input', { bubbles: true }));
309
+ output.dispatchEvent(new Event('change', { bubbles: true }));
310
+ }
311
+ if (date && time && value) {
312
+ const detail: SlotValue = { date, time, key, value };
313
+ options.onChange?.(detail);
314
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail }));
315
+ }
316
+ };
317
+
318
+ const selectDate = (input: DateInput, notify = true) => {
319
+ if (scope.disposed) return;
320
+ const d = toDate(input);
321
+ if (disabled(d)) return;
322
+ const direction = date ? Math.sign(d.getTime() - date.getTime()) : 0;
323
+ if (sameDay(d, date)) return;
324
+ date = d;
325
+ time = null;
326
+ focusDay = d;
327
+ if (!sameMonth(d, month)) {
328
+ const dir = d > month ? 1 : -1;
329
+ month = d;
330
+ renderDays(dir);
331
+ }
332
+ daysHost.querySelectorAll<HTMLElement>('[role="gridcell"]').forEach((cell) => {
333
+ const b = cell.querySelector<HTMLButtonElement>('[data-mk-slot-day]');
334
+ if (b) {
335
+ cell.setAttribute('aria-selected', String(b.dataset.mkSlotDay === dayKey(d)));
336
+ b.tabIndex = b.dataset.mkSlotDay === dayKey(d) ? 0 : -1;
337
+ }
338
+ });
339
+ syncHeading();
340
+ load(direction);
341
+ if (notify) {
342
+ options.onDateChange?.(d);
343
+ emit();
344
+ }
345
+ };
346
+ const selectTime = (t: string | null, notify = true) => {
347
+ if (scope.disposed || !date) return;
348
+ if (t !== null && !times.includes(t)) return;
349
+ time = t;
350
+ timesHost.querySelectorAll<HTMLButtonElement>('[data-mk-slot-time]').forEach((b) => {
351
+ b.setAttribute('aria-checked', String(b.dataset.mkSlotTime === t));
352
+ b.tabIndex = (t ? b.dataset.mkSlotTime === t : b === timesHost.querySelector('[data-mk-slot-time]')) ? 0 : -1;
353
+ });
354
+ if (notify) emit();
355
+ };
356
+
357
+ scope.on(root, 'click', (e) => {
358
+ const target = (e.target as Element).closest<HTMLButtonElement>('button');
359
+ if (!target || target.disabled || !root.contains(target)) return;
360
+ if (target === prev) moveFocus(addMonths(month, -1), false);
361
+ else if (target === next) moveFocus(addMonths(month, 1), false);
362
+ else if (target.dataset.mkSlotDay) selectDate(target.dataset.mkSlotDay);
363
+ else if (target.dataset.mkSlotTime) selectTime(target.dataset.mkSlotTime);
364
+ });
365
+ scope.on(daysHost, 'keydown', (e) => {
366
+ const event = e as KeyboardEvent;
367
+ const key = (event.target as HTMLElement).dataset?.mkSlotDay;
368
+ if (!key) return;
369
+ const d = toDate(key);
370
+ const to = ({
371
+ ArrowLeft: () => addDays(d, -1), ArrowRight: () => addDays(d, 1), ArrowUp: () => addDays(d, -7), ArrowDown: () => addDays(d, 7),
372
+ Home: () => startOfWeek(d, firstDay), End: () => addDays(startOfWeek(d, firstDay), 6),
373
+ PageUp: () => addMonths(d, -1), PageDown: () => addMonths(d, 1),
374
+ } as Record<string, () => Date>)[event.key]?.();
375
+ if (!to) return;
376
+ event.preventDefault();
377
+ moveFocus(to, true);
378
+ });
379
+ scope.on(timesHost, 'keydown', (e) => {
380
+ const event = e as KeyboardEvent;
381
+ const current = (event.target as HTMLElement).dataset?.mkSlotTime;
382
+ if (!current) return;
383
+ const i = times.indexOf(current);
384
+ const to = ({ ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: times.length - 1 } as Record<string, number>)[event.key];
385
+ if (to === undefined) return;
386
+ event.preventDefault();
387
+ const t = times[(to + times.length) % times.length];
388
+ selectTime(t);
389
+ timesHost.querySelector<HTMLButtonElement>(`[data-mk-slot-time="${t}"]`)?.focus();
390
+ });
391
+
392
+ renderDays(0);
393
+ if (date) {
394
+ syncHeading();
395
+ load(0);
396
+ if (time && !times.includes(time)) time = null;
397
+ } else renderTimes('placeholder', 0);
398
+
399
+ return {
400
+ selectDate: (d) => selectDate(d),
401
+ selectTime: (t) => selectTime(t),
402
+ setSlots(source) {
403
+ if (scope.disposed) return;
404
+ slots = fromJson<SlotSource>(source) ?? [];
405
+ load(0);
406
+ },
407
+ get date() { return date; },
408
+ get time() { return time; },
409
+ get value() { return date && time ? `${dayKey(date)}T${time}` : null; },
410
+ destroy: () => scope.dispose(),
411
+ };
412
+ }
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import { useRef, type HTMLAttributes, type ReactNode } from 'react';
4
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
5
+ import { createSlotPicker, type SlotPickerInstance, type SlotPickerOptions } from './engine';
6
+
7
+ export interface SlotPickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, SlotPickerOptions {
8
+ /** Form field name: a hidden input carries "YYYY-MM-DDTHH:mm". */
9
+ name?: string;
10
+ labels?: SlotPickerOptions['labels'] & { previous?: string; next?: string };
11
+ /** Rendered under the times (e.g. a submit button). */
12
+ actions?: ReactNode;
13
+ }
14
+
15
+ export function SlotPicker({
16
+ date, value, slots, min, max, closedDays, isDisabled, firstDay, locale, labels, onChange, onDateChange,
17
+ name, actions, className, ...props
18
+ }: SlotPickerProps) {
19
+ const ref = useRef<HTMLDivElement>(null);
20
+ const instance = useRef<SlotPickerInstance | null>(null);
21
+ const kept = useRef<string | Date | null>(null);
22
+ const change = useLatest(onChange);
23
+ const dateChange = useLatest(onDateChange);
24
+ const disabledFn = useLatest(isDisabled);
25
+ // A function source is read through a ref (inline functions do not reload);
26
+ // lists and maps reload when their content changes.
27
+ const source = useLatest(slots);
28
+ const slotsKey = typeof slots === 'function' ? 'function' : JSON.stringify(slots ?? []);
29
+ const stable = () => {
30
+ const current = source.current;
31
+ return typeof current === 'function' ? (d: Date) => (source.current as (day: Date) => string[] | Promise<string[]>)(d) : current ?? [];
32
+ };
33
+ useIsomorphicLayoutEffect(() => {
34
+ const keep = kept.current;
35
+ const created = createSlotPicker(ref.current, {
36
+ date: date ?? (keep instanceof Date ? keep : undefined),
37
+ value: value !== undefined ? value : typeof keep === 'string' ? keep : undefined,
38
+ slots: stable(), min, max, closedDays, firstDay, locale, labels,
39
+ isDisabled: (d) => disabledFn.current?.(d) ?? false,
40
+ onChange: (v) => change.current?.(v),
41
+ onDateChange: (d) => dateChange.current?.(d),
42
+ });
43
+ instance.current = created;
44
+ return () => {
45
+ kept.current = created.value ?? created.date;
46
+ created.destroy();
47
+ instance.current = null;
48
+ };
49
+ // `slots` updates in place; the rest recreates the picker.
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [date, value, min, max, firstDay, locale, JSON.stringify(closedDays ?? [])]);
52
+ const firstSlots = useRef(true);
53
+ useIsomorphicLayoutEffect(() => {
54
+ if (firstSlots.current) { firstSlots.current = false; return; }
55
+ instance.current?.setSlots(stable());
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [slotsKey]);
58
+ return (
59
+ <div {...props} ref={ref} className={['mk-slot-picker', className].filter(Boolean).join(' ')}>
60
+ <div className="mk-slot-picker__calendar">
61
+ <div className="mk-slot-picker__month">
62
+ <h3 className="mk-slot-picker__title" data-mk-slot-title />
63
+ <button type="button" className="mk-slot-picker__nav" data-mk-slot-prev aria-label={labels?.previous ?? 'Mes anterior'}>←</button>
64
+ <button type="button" className="mk-slot-picker__nav" data-mk-slot-next aria-label={labels?.next ?? 'Mes siguiente'}>→</button>
65
+ </div>
66
+ <div data-mk-slot-days />
67
+ </div>
68
+ <div className="mk-slot-picker__panel">
69
+ <p className="mk-slot-picker__heading" data-mk-slot-heading />
70
+ <div data-mk-slot-times />
71
+ {name && <input type="hidden" name={name} data-mk-slot-value />}
72
+ {actions && <div className="mk-slot-picker__actions">{actions}</div>}
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,125 @@
1
+ /* slot-picker — Melon Mind's appointment picker on the --mk-* tokens; parts are .mk-slot-picker__*. */
2
+ .mk-slot-picker,
3
+ mk-slot-picker {
4
+ --mk-slot-picker-radius: var(--mk-radius-md);
5
+ display: grid;
6
+ grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
7
+ overflow: hidden;
8
+ border-radius: var(--mk-radius-xl);
9
+ background: var(--mk-surface);
10
+ color: var(--mk-text);
11
+ }
12
+ .mk-slot-picker__calendar { padding: var(--mk-space-5); }
13
+ .mk-slot-picker__panel {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: var(--mk-space-3);
17
+ min-height: 100%;
18
+ padding: var(--mk-space-5);
19
+ background: var(--mk-surface-2);
20
+ }
21
+ .mk-slot-picker__month { display: flex; align-items: center; gap: var(--mk-space-2); margin-bottom: var(--mk-space-3); }
22
+ .mk-slot-picker__title { margin: 0 auto 0 0; font-size: 1.35rem; font-weight: 400; }
23
+ .mk-slot-picker__nav {
24
+ display: grid;
25
+ place-items: center;
26
+ width: 42px;
27
+ height: 42px;
28
+ border: 0;
29
+ border-radius: 50%;
30
+ background: var(--mk-surface-2);
31
+ color: var(--mk-text);
32
+ font: inherit;
33
+ cursor: pointer;
34
+ transition: transform 0.3s var(--mk-ease-nav-move), opacity 0.2s;
35
+ }
36
+ .mk-slot-picker__nav:active { transform: scale(0.9); }
37
+ .mk-slot-picker__nav:disabled { opacity: 0.35; cursor: default; }
38
+ [data-mk-slot-days],
39
+ [data-mk-slot-times] { position: relative; }
40
+ .mk-slot-picker__days { display: flex; flex-direction: column; gap: 6px; }
41
+ .mk-slot-picker__weekdays,
42
+ .mk-slot-picker__week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
43
+ .mk-slot-picker__weekdays span {
44
+ padding: 6px 0;
45
+ border-radius: var(--mk-radius-sm);
46
+ background: color-mix(in srgb, var(--mk-surface-2) 70%, transparent);
47
+ text-align: center;
48
+ font-size: 0.9rem;
49
+ }
50
+ .mk-slot-picker__week > [role='gridcell'] { display: grid; }
51
+ .mk-slot-picker__day {
52
+ aspect-ratio: 1.15;
53
+ border: 0;
54
+ border-radius: var(--mk-slot-picker-radius);
55
+ background: var(--mk-surface-3);
56
+ color: var(--mk-text);
57
+ font: inherit;
58
+ font-size: 0.95rem;
59
+ cursor: pointer;
60
+ transition: background 0.25s, color 0.25s, border-radius 0.3s var(--mk-ease-nav-radius), transform 0.3s var(--mk-ease-nav-radius);
61
+ }
62
+ .mk-slot-picker__day:hover:not(:disabled) { background: var(--mk-surface-5); border-radius: var(--mk-radius-full); }
63
+ .mk-slot-picker__day:active:not(:disabled) { transform: scale(0.92); }
64
+ .mk-slot-picker__day:disabled { opacity: 0.35; cursor: default; }
65
+ .mk-slot-picker__day[aria-current='date'] { box-shadow: inset 0 0 0 1.5px var(--mk-outline); }
66
+ [aria-selected='true'] > .mk-slot-picker__day,
67
+ [aria-selected='true'] > .mk-slot-picker__day:hover:not(:disabled) { background: var(--mk-accent); color: var(--mk-on-accent); }
68
+ :is(.mk-slot-picker__day, .mk-slot-picker__time, .mk-slot-picker__nav):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
69
+
70
+ .mk-slot-picker__heading {
71
+ margin: 0;
72
+ padding: 8px;
73
+ border-radius: var(--mk-radius-md);
74
+ background: var(--mk-surface);
75
+ font-weight: 600;
76
+ }
77
+ .mk-slot-picker__heading:not([data-mk-shown]) { display: none; }
78
+ .mk-slot-picker__heading[data-mk-shown] { animation: mk-slot-heading-in 0.5s cubic-bezier(0.38, 0.49, 0, 1.4); }
79
+ @keyframes mk-slot-heading-in {
80
+ 0% { max-height: 0; min-height: 0; padding-block: 0; opacity: 0; }
81
+ 100% { max-height: 56px; min-height: 40px; opacity: 1; }
82
+ }
83
+ .mk-slot-picker__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr)); gap: 8px; align-content: start; }
84
+ .mk-slot-picker__leaving { position: absolute; left: 0; right: 0; pointer-events: none; }
85
+ .mk-slot-picker__note { grid-column: 1 / -1; margin: 2rem 0; color: var(--mk-muted); text-align: center; }
86
+ .mk-slot-picker__time {
87
+ position: relative;
88
+ isolation: isolate;
89
+ height: 44px;
90
+ border: 0;
91
+ border-radius: var(--mk-radius-sm);
92
+ background: var(--mk-surface-4);
93
+ color: var(--mk-text);
94
+ font: inherit;
95
+ font-variant-numeric: tabular-nums;
96
+ cursor: pointer;
97
+ transition: background 0.2s, color 0.2s, transform 0.3s var(--mk-ease-nav-move);
98
+ }
99
+ .mk-slot-picker__time:hover { transform: translateY(-2px); }
100
+ .mk-slot-picker__time[aria-checked='true'] { color: var(--mk-on-accent); }
101
+ .mk-slot-picker__time[aria-checked='true']::before {
102
+ content: '';
103
+ position: absolute;
104
+ inset: 0;
105
+ z-index: -1;
106
+ border-radius: inherit;
107
+ background: var(--mk-accent);
108
+ animation: mk-slot-highlight 0.7s cubic-bezier(0.38, 0.49, 0, 1.1);
109
+ }
110
+ @keyframes mk-slot-highlight {
111
+ 0% { transform: scale(0.8); opacity: 0; filter: blur(16px); box-shadow: 0 0 0 1px var(--mk-surface-2); }
112
+ 100% { transform: scale(1); opacity: 1; filter: blur(0); }
113
+ }
114
+ .mk-slot-picker__actions { margin-top: auto; display: flex; justify-content: flex-end; }
115
+
116
+ @media (max-width: 680px) {
117
+ .mk-slot-picker,
118
+ mk-slot-picker { grid-template-columns: 1fr; }
119
+ .mk-slot-picker__calendar,
120
+ .mk-slot-picker__panel { padding: var(--mk-space-4); }
121
+ }
122
+ @media (prefers-reduced-motion: reduce) {
123
+ .mk-slot-picker *,
124
+ mk-slot-picker * { transition: none !important; animation: none !important; }
125
+ }
@@ -1,8 +1,6 @@
1
1
  # steps
2
2
 
3
- El onboarding de Stepbro: un paso a la vez. Al cambiar de paso, los elementos del actual se van hacia abajo con blur (0.5 s, 50 ms entre ellos, `flowSoft`), se cambia el paso, se espera un frame y los del siguiente caen desde arriba. Un contador rueda hasta el número del paso (0.9 s), el foco pasa al título del paso nuevo y, mientras un paso trabaja (`busy`), el fondo de chat de dentro acelera y crece (energía 1.6, escala 1.8 en 0.8 s; vuelta en 0.6 s).
4
-
5
- Origen: Stepbro `OnboardingView` (`step-change`, `onboarding-busy-background`, kit Money). El fondo va al 20 % de opacidad, como en Stepbro.
3
+ El onboarding: un paso a la vez. Al cambiar de paso, los elementos del actual se van hacia abajo con blur (0.5 s, 50 ms entre ellos, `flowSoft`), se cambia el paso, se espera un frame y los del siguiente caen desde arriba. Un contador rueda hasta el número del paso (0.9 s), el foco pasa al título del paso nuevo y, mientras un paso trabaja (`busy`), el fondo de chat de dentro acelera y crece (energía 1.6, escala 1.8 en 0.8 s; vuelta en 0.6 s).
6
4
 
7
5
  ## HTML / Astro
8
6
 
@@ -10,7 +10,7 @@ import { flowIn, flowOut } from '../../motion/flow';
10
10
  import { chatBackgroundOf } from '../chat-background/engine';
11
11
 
12
12
  /*
13
- * Stepbro's onboarding: one step at a time. Changing step flows the current
13
+ * onboarding: one step at a time. Changing step flows the current
14
14
  * step's items out (down into a 16 px blur, 0.5 s, 50 ms apart), swaps the step,
15
15
  * waits a frame and flows the next one's items in from above. A counter rolls
16
16
  * to the step number (0.9 s), focus moves to the new step's title, and while a
@@ -24,7 +24,7 @@ export interface StepsOptions {
24
24
  /** First step shown (0-based). Default: the one marked data-mk-current, else 0. */
25
25
  index?: number;
26
26
  duration?: number;
27
- /** Flow the first step in on start (default, as Stepbro). */
27
+ /** Flow the first step in on start. */
28
28
  animateFirst?: boolean;
29
29
  /** Items of a step that flow (default [data-mk-flow-item], else the step's children). */
30
30
  target?: string;
@@ -35,7 +35,7 @@ export interface StepsInstance extends Instance {
35
35
  next(): Promise<void>;
36
36
  prev(): Promise<void>;
37
37
  goTo(index: number): Promise<void>;
38
- /** A step is working: drives the chat background inside (Stepbro's onBusy). */
38
+ /** A step is working: drives the chat background inside. */
39
39
  busy(on: boolean): void;
40
40
  readonly index: number;
41
41
  readonly count: number;
@@ -24,7 +24,7 @@ export interface StepsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChan
24
24
  /** Current step (0-based). Changing it flows to that step. */
25
25
  index?: number;
26
26
  onChange?: (index: number, previous: number) => void;
27
- /** A step is working: the chat background inside speeds up (Stepbro's onBusy). */
27
+ /** A step is working: the chat background inside speeds up. */
28
28
  busy?: boolean;
29
29
  duration?: number;
30
30
  children?: ReactNode;
@@ -85,7 +85,7 @@ function Counter(props: HTMLAttributes<HTMLSpanElement>) {
85
85
  }
86
86
 
87
87
  /**
88
- * Stepbro's onboarding: <Steps index={i} busy={working}><ChatBackground /><Steps.Counter />
88
+ * onboarding: <Steps index={i} busy={working}><ChatBackground /><Steps.Counter />
89
89
  * <Steps.Step><h2 data-mk-flow-item>…</h2></Steps.Step>…</Steps>. Steps must be direct children.
90
90
  */
91
91
  export const Steps = Object.assign(Root, { Step, Counter });