love-ui 1.2.12 → 1.2.14

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 (988) hide show
  1. package/dist/index.js +8 -7
  2. package/dist/mcp-server.js +1 -1
  3. package/package.json +3 -2
  4. package/packages/loveui-skills/SKILL.md +99 -0
  5. package/packages/loveui-skills/agents/openai.yaml +11 -0
  6. package/packages/loveui-skills/references/design-directions.md +60 -0
  7. package/packages/loveui-skills/references/mcp-catalog-workflow.md +68 -0
  8. package/packages/loveui-skills/references/page-blueprints.md +76 -0
  9. package/packages/loveui-skills/references/quality-gates.md +51 -0
  10. package/public/r/accordion-controlled.json +1 -1
  11. package/public/r/accordion-demo.json +1 -1
  12. package/public/r/accordion-multiple.json +1 -1
  13. package/public/r/accordion-single.json +1 -1
  14. package/public/r/alert-demo.json +1 -1
  15. package/public/r/alert-dialog-demo.json +1 -1
  16. package/public/r/alert-error.json +1 -1
  17. package/public/r/alert-info.json +1 -1
  18. package/public/r/alert-success.json +1 -1
  19. package/public/r/alert-warning.json +1 -1
  20. package/public/r/alert-with-icon-action.json +1 -1
  21. package/public/r/alert-with-icon.json +1 -1
  22. package/public/r/announcement-tagless.json +1 -1
  23. package/public/r/announcement-themes.json +1 -1
  24. package/public/r/announcement.json +1 -1
  25. package/public/r/autocomplete-async.json +1 -1
  26. package/public/r/autocomplete-autohighlight.json +1 -1
  27. package/public/r/autocomplete-demo.json +1 -1
  28. package/public/r/autocomplete-disabled.json +1 -1
  29. package/public/r/autocomplete-form.json +1 -1
  30. package/public/r/autocomplete-grouped.json +1 -1
  31. package/public/r/autocomplete-inline.json +1 -1
  32. package/public/r/autocomplete-lg.json +1 -1
  33. package/public/r/autocomplete-limit.json +1 -1
  34. package/public/r/autocomplete-sm.json +1 -1
  35. package/public/r/autocomplete-with-clear.json +1 -1
  36. package/public/r/autocomplete-with-label.json +1 -1
  37. package/public/r/autocomplete-with-trigger-clear.json +1 -1
  38. package/public/r/avatar-demo.json +1 -1
  39. package/public/r/avatar-fallback.json +1 -1
  40. package/public/r/avatar-group.json +1 -1
  41. package/public/r/avatar-radius.json +1 -1
  42. package/public/r/avatar-size.json +1 -1
  43. package/public/r/avatar-stack-hover.json +1 -1
  44. package/public/r/avatar-stack.json +1 -1
  45. package/public/r/badge-demo.json +1 -1
  46. package/public/r/badge-destructive.json +1 -1
  47. package/public/r/badge-error.json +1 -1
  48. package/public/r/badge-info.json +1 -1
  49. package/public/r/badge-lg.json +1 -1
  50. package/public/r/badge-outline.json +1 -1
  51. package/public/r/badge-secondary.json +1 -1
  52. package/public/r/badge-sm.json +1 -1
  53. package/public/r/badge-success.json +1 -1
  54. package/public/r/badge-warning.json +1 -1
  55. package/public/r/badge-with-icon.json +1 -1
  56. package/public/r/badge-with-link.json +1 -1
  57. package/public/r/banner-inset.json +1 -1
  58. package/public/r/banner-themes.json +1 -1
  59. package/public/r/banner.json +1 -1
  60. package/public/r/bookmarks.json +206 -0
  61. package/public/r/breadcrumb-custom-separator.json +1 -1
  62. package/public/r/breadcrumb-demo.json +1 -1
  63. package/public/r/button-demo.json +1 -1
  64. package/public/r/button-destructive-outline.json +1 -1
  65. package/public/r/button-destructive.json +1 -1
  66. package/public/r/button-disabled.json +1 -1
  67. package/public/r/button-ghost.json +1 -1
  68. package/public/r/button-lg.json +1 -1
  69. package/public/r/button-link.json +1 -1
  70. package/public/r/button-loading.json +1 -1
  71. package/public/r/button-outline.json +1 -1
  72. package/public/r/button-secondary.json +1 -1
  73. package/public/r/button-sm.json +1 -1
  74. package/public/r/button-with-icon.json +1 -1
  75. package/public/r/button-with-link.json +1 -1
  76. package/public/r/button-xl.json +1 -1
  77. package/public/r/button-xs.json +1 -1
  78. package/public/r/calendar-headless.json +1 -1
  79. package/public/r/calendar-rac.json +16 -0
  80. package/public/r/calendar.json +1 -1
  81. package/public/r/card-demo.json +1 -1
  82. package/public/r/chart.json +15 -0
  83. package/public/r/checkbox-card.json +1 -1
  84. package/public/r/checkbox-demo.json +1 -1
  85. package/public/r/checkbox-disabled.json +1 -1
  86. package/public/r/checkbox-form.json +1 -1
  87. package/public/r/checkbox-group-demo.json +1 -1
  88. package/public/r/checkbox-group-disabled.json +1 -1
  89. package/public/r/checkbox-group-form.json +1 -1
  90. package/public/r/checkbox-group-nested-parent.json +1 -1
  91. package/public/r/checkbox-group-parent.json +1 -1
  92. package/public/r/checkbox-tree.json +12 -0
  93. package/public/r/checkbox-with-description.json +1 -1
  94. package/public/r/choicebox-inline.json +1 -1
  95. package/public/r/choicebox.json +1 -1
  96. package/public/r/codebase.json +1 -1
  97. package/public/r/collaborative-canvas.json +1 -1
  98. package/public/r/collapsible-demo.json +1 -1
  99. package/public/r/combobox-autohighlight.json +1 -1
  100. package/public/r/combobox-controlled.json +1 -1
  101. package/public/r/combobox-create-new.json +1 -1
  102. package/public/r/combobox-demo.json +1 -1
  103. package/public/r/combobox-disabled.json +1 -1
  104. package/public/r/combobox-fixed-width.json +1 -1
  105. package/public/r/combobox-form.json +1 -1
  106. package/public/r/combobox-grouped.json +1 -1
  107. package/public/r/combobox-lg.json +1 -1
  108. package/public/r/combobox-multiple-form.json +1 -1
  109. package/public/r/combobox-multiple.json +1 -1
  110. package/public/r/combobox-sm.json +1 -1
  111. package/public/r/combobox-with-clear.json +1 -1
  112. package/public/r/combobox-with-inner-input.json +1 -1
  113. package/public/r/combobox-with-label.json +1 -1
  114. package/public/r/command.json +18 -0
  115. package/public/r/comp-01.json +22 -0
  116. package/public/r/comp-02.json +23 -0
  117. package/public/r/comp-03.json +23 -0
  118. package/public/r/comp-04.json +23 -0
  119. package/public/r/comp-05.json +22 -0
  120. package/public/r/comp-06.json +23 -0
  121. package/public/r/comp-07.json +22 -0
  122. package/public/r/comp-08.json +23 -0
  123. package/public/r/comp-09.json +22 -0
  124. package/public/r/comp-10.json +22 -0
  125. package/public/r/comp-100.json +23 -0
  126. package/public/r/comp-101.json +23 -0
  127. package/public/r/comp-102.json +23 -0
  128. package/public/r/comp-103.json +23 -0
  129. package/public/r/comp-104.json +23 -0
  130. package/public/r/comp-105.json +23 -0
  131. package/public/r/comp-106.json +22 -0
  132. package/public/r/comp-107.json +22 -0
  133. package/public/r/comp-108.json +23 -0
  134. package/public/r/comp-109.json +22 -0
  135. package/public/r/comp-11.json +22 -0
  136. package/public/r/comp-110.json +22 -0
  137. package/public/r/comp-111.json +21 -0
  138. package/public/r/comp-112.json +21 -0
  139. package/public/r/comp-113.json +22 -0
  140. package/public/r/comp-114.json +23 -0
  141. package/public/r/comp-115.json +22 -0
  142. package/public/r/comp-116.json +22 -0
  143. package/public/r/comp-117.json +23 -0
  144. package/public/r/comp-118.json +23 -0
  145. package/public/r/comp-119.json +27 -0
  146. package/public/r/comp-12.json +22 -0
  147. package/public/r/comp-120.json +27 -0
  148. package/public/r/comp-121.json +27 -0
  149. package/public/r/comp-122.json +27 -0
  150. package/public/r/comp-123.json +22 -0
  151. package/public/r/comp-124.json +22 -0
  152. package/public/r/comp-125.json +31 -0
  153. package/public/r/comp-126.json +31 -0
  154. package/public/r/comp-127.json +21 -0
  155. package/public/r/comp-128.json +21 -0
  156. package/public/r/comp-129.json +24 -0
  157. package/public/r/comp-13.json +22 -0
  158. package/public/r/comp-130.json +23 -0
  159. package/public/r/comp-131.json +23 -0
  160. package/public/r/comp-132.json +23 -0
  161. package/public/r/comp-133.json +23 -0
  162. package/public/r/comp-134.json +23 -0
  163. package/public/r/comp-135.json +24 -0
  164. package/public/r/comp-136.json +23 -0
  165. package/public/r/comp-137.json +23 -0
  166. package/public/r/comp-138.json +25 -0
  167. package/public/r/comp-139.json +23 -0
  168. package/public/r/comp-14.json +22 -0
  169. package/public/r/comp-140.json +23 -0
  170. package/public/r/comp-141.json +23 -0
  171. package/public/r/comp-142.json +25 -0
  172. package/public/r/comp-143.json +23 -0
  173. package/public/r/comp-144.json +24 -0
  174. package/public/r/comp-145.json +24 -0
  175. package/public/r/comp-146.json +24 -0
  176. package/public/r/comp-147.json +24 -0
  177. package/public/r/comp-148.json +25 -0
  178. package/public/r/comp-149.json +24 -0
  179. package/public/r/comp-15.json +22 -0
  180. package/public/r/comp-150.json +21 -0
  181. package/public/r/comp-151.json +23 -0
  182. package/public/r/comp-152.json +23 -0
  183. package/public/r/comp-153.json +23 -0
  184. package/public/r/comp-154.json +23 -0
  185. package/public/r/comp-155.json +23 -0
  186. package/public/r/comp-156.json +25 -0
  187. package/public/r/comp-157.json +27 -0
  188. package/public/r/comp-158.json +24 -0
  189. package/public/r/comp-159.json +24 -0
  190. package/public/r/comp-16.json +22 -0
  191. package/public/r/comp-160.json +24 -0
  192. package/public/r/comp-161.json +24 -0
  193. package/public/r/comp-162.json +24 -0
  194. package/public/r/comp-163.json +28 -0
  195. package/public/r/comp-164.json +24 -0
  196. package/public/r/comp-165.json +24 -0
  197. package/public/r/comp-166.json +25 -0
  198. package/public/r/comp-167.json +24 -0
  199. package/public/r/comp-168.json +24 -0
  200. package/public/r/comp-169.json +23 -0
  201. package/public/r/comp-17.json +25 -0
  202. package/public/r/comp-170.json +25 -0
  203. package/public/r/comp-171.json +28 -0
  204. package/public/r/comp-172.json +23 -0
  205. package/public/r/comp-173.json +23 -0
  206. package/public/r/comp-174.json +23 -0
  207. package/public/r/comp-175.json +23 -0
  208. package/public/r/comp-176.json +23 -0
  209. package/public/r/comp-177.json +23 -0
  210. package/public/r/comp-178.json +23 -0
  211. package/public/r/comp-179.json +24 -0
  212. package/public/r/comp-18.json +25 -0
  213. package/public/r/comp-180.json +23 -0
  214. package/public/r/comp-181.json +25 -0
  215. package/public/r/comp-182.json +24 -0
  216. package/public/r/comp-183.json +25 -0
  217. package/public/r/comp-184.json +25 -0
  218. package/public/r/comp-185.json +24 -0
  219. package/public/r/comp-186.json +25 -0
  220. package/public/r/comp-187.json +25 -0
  221. package/public/r/comp-188.json +25 -0
  222. package/public/r/comp-189.json +23 -0
  223. package/public/r/comp-19.json +23 -0
  224. package/public/r/comp-190.json +23 -0
  225. package/public/r/comp-191.json +24 -0
  226. package/public/r/comp-192.json +23 -0
  227. package/public/r/comp-193.json +23 -0
  228. package/public/r/comp-194.json +24 -0
  229. package/public/r/comp-195.json +23 -0
  230. package/public/r/comp-196.json +24 -0
  231. package/public/r/comp-197.json +23 -0
  232. package/public/r/comp-198.json +23 -0
  233. package/public/r/comp-199.json +23 -0
  234. package/public/r/comp-20.json +23 -0
  235. package/public/r/comp-200.json +25 -0
  236. package/public/r/comp-201.json +22 -0
  237. package/public/r/comp-202.json +22 -0
  238. package/public/r/comp-203.json +23 -0
  239. package/public/r/comp-204.json +23 -0
  240. package/public/r/comp-205.json +23 -0
  241. package/public/r/comp-206.json +24 -0
  242. package/public/r/comp-207.json +23 -0
  243. package/public/r/comp-208.json +23 -0
  244. package/public/r/comp-209.json +23 -0
  245. package/public/r/comp-21.json +23 -0
  246. package/public/r/comp-210.json +24 -0
  247. package/public/r/comp-211.json +24 -0
  248. package/public/r/comp-212.json +23 -0
  249. package/public/r/comp-213.json +23 -0
  250. package/public/r/comp-214.json +23 -0
  251. package/public/r/comp-215.json +24 -0
  252. package/public/r/comp-216.json +22 -0
  253. package/public/r/comp-217.json +22 -0
  254. package/public/r/comp-218.json +25 -0
  255. package/public/r/comp-219.json +23 -0
  256. package/public/r/comp-22.json +23 -0
  257. package/public/r/comp-220.json +24 -0
  258. package/public/r/comp-221.json +23 -0
  259. package/public/r/comp-222.json +26 -0
  260. package/public/r/comp-223.json +26 -0
  261. package/public/r/comp-224.json +23 -0
  262. package/public/r/comp-225.json +24 -0
  263. package/public/r/comp-226.json +26 -0
  264. package/public/r/comp-227.json +26 -0
  265. package/public/r/comp-228.json +26 -0
  266. package/public/r/comp-229.json +30 -0
  267. package/public/r/comp-23.json +24 -0
  268. package/public/r/comp-230.json +30 -0
  269. package/public/r/comp-231.json +32 -0
  270. package/public/r/comp-232.json +31 -0
  271. package/public/r/comp-233.json +30 -0
  272. package/public/r/comp-234.json +28 -0
  273. package/public/r/comp-235.json +28 -0
  274. package/public/r/comp-236.json +24 -0
  275. package/public/r/comp-237.json +26 -0
  276. package/public/r/comp-238.json +26 -0
  277. package/public/r/comp-239.json +26 -0
  278. package/public/r/comp-24.json +23 -0
  279. package/public/r/comp-240.json +23 -0
  280. package/public/r/comp-241.json +24 -0
  281. package/public/r/comp-242.json +23 -0
  282. package/public/r/comp-243.json +23 -0
  283. package/public/r/comp-244.json +23 -0
  284. package/public/r/comp-245.json +23 -0
  285. package/public/r/comp-246.json +23 -0
  286. package/public/r/comp-247.json +23 -0
  287. package/public/r/comp-248.json +23 -0
  288. package/public/r/comp-249.json +24 -0
  289. package/public/r/comp-25.json +24 -0
  290. package/public/r/comp-250.json +25 -0
  291. package/public/r/comp-251.json +25 -0
  292. package/public/r/comp-252.json +25 -0
  293. package/public/r/comp-253.json +25 -0
  294. package/public/r/comp-254.json +35 -0
  295. package/public/r/comp-255.json +30 -0
  296. package/public/r/comp-256.json +25 -0
  297. package/public/r/comp-257.json +25 -0
  298. package/public/r/comp-258.json +32 -0
  299. package/public/r/comp-259.json +26 -0
  300. package/public/r/comp-26.json +24 -0
  301. package/public/r/comp-260.json +26 -0
  302. package/public/r/comp-261.json +24 -0
  303. package/public/r/comp-262.json +31 -0
  304. package/public/r/comp-263.json +25 -0
  305. package/public/r/comp-264.json +33 -0
  306. package/public/r/comp-265.json +32 -0
  307. package/public/r/comp-266.json +24 -0
  308. package/public/r/comp-267.json +19 -0
  309. package/public/r/comp-268.json +19 -0
  310. package/public/r/comp-269.json +19 -0
  311. package/public/r/comp-27.json +24 -0
  312. package/public/r/comp-270.json +19 -0
  313. package/public/r/comp-271.json +19 -0
  314. package/public/r/comp-272.json +19 -0
  315. package/public/r/comp-273.json +19 -0
  316. package/public/r/comp-274.json +19 -0
  317. package/public/r/comp-275.json +19 -0
  318. package/public/r/comp-276.json +19 -0
  319. package/public/r/comp-277.json +21 -0
  320. package/public/r/comp-278.json +21 -0
  321. package/public/r/comp-279.json +23 -0
  322. package/public/r/comp-28.json +24 -0
  323. package/public/r/comp-280.json +23 -0
  324. package/public/r/comp-281.json +23 -0
  325. package/public/r/comp-282.json +23 -0
  326. package/public/r/comp-283.json +23 -0
  327. package/public/r/comp-284.json +23 -0
  328. package/public/r/comp-285.json +23 -0
  329. package/public/r/comp-286.json +23 -0
  330. package/public/r/comp-287.json +24 -0
  331. package/public/r/comp-288.json +23 -0
  332. package/public/r/comp-289.json +24 -0
  333. package/public/r/comp-29.json +24 -0
  334. package/public/r/comp-290.json +24 -0
  335. package/public/r/comp-291.json +24 -0
  336. package/public/r/comp-292.json +24 -0
  337. package/public/r/comp-293.json +26 -0
  338. package/public/r/comp-294.json +24 -0
  339. package/public/r/comp-295.json +23 -0
  340. package/public/r/comp-296.json +23 -0
  341. package/public/r/comp-297.json +30 -0
  342. package/public/r/comp-298.json +26 -0
  343. package/public/r/comp-299.json +28 -0
  344. package/public/r/comp-30.json +23 -0
  345. package/public/r/comp-300.json +29 -0
  346. package/public/r/comp-301.json +24 -0
  347. package/public/r/comp-302.json +18 -0
  348. package/public/r/comp-303.json +18 -0
  349. package/public/r/comp-304.json +18 -0
  350. package/public/r/comp-305.json +21 -0
  351. package/public/r/comp-306.json +21 -0
  352. package/public/r/comp-307.json +21 -0
  353. package/public/r/comp-308.json +21 -0
  354. package/public/r/comp-309.json +21 -0
  355. package/public/r/comp-31.json +21 -0
  356. package/public/r/comp-310.json +23 -0
  357. package/public/r/comp-311.json +20 -0
  358. package/public/r/comp-312.json +21 -0
  359. package/public/r/comp-313.json +26 -0
  360. package/public/r/comp-314.json +26 -0
  361. package/public/r/comp-315.json +24 -0
  362. package/public/r/comp-316.json +25 -0
  363. package/public/r/comp-317.json +24 -0
  364. package/public/r/comp-318.json +24 -0
  365. package/public/r/comp-319.json +24 -0
  366. package/public/r/comp-32.json +21 -0
  367. package/public/r/comp-320.json +27 -0
  368. package/public/r/comp-321.json +28 -0
  369. package/public/r/comp-322.json +27 -0
  370. package/public/r/comp-323.json +30 -0
  371. package/public/r/comp-324.json +28 -0
  372. package/public/r/comp-325.json +29 -0
  373. package/public/r/comp-326.json +30 -0
  374. package/public/r/comp-327.json +30 -0
  375. package/public/r/comp-328.json +37 -0
  376. package/public/r/comp-329.json +38 -0
  377. package/public/r/comp-33.json +18 -0
  378. package/public/r/comp-330.json +27 -0
  379. package/public/r/comp-331.json +42 -0
  380. package/public/r/comp-332.json +25 -0
  381. package/public/r/comp-333.json +29 -0
  382. package/public/r/comp-334.json +22 -0
  383. package/public/r/comp-335.json +22 -0
  384. package/public/r/comp-336.json +22 -0
  385. package/public/r/comp-337.json +22 -0
  386. package/public/r/comp-338.json +22 -0
  387. package/public/r/comp-339.json +22 -0
  388. package/public/r/comp-34.json +27 -0
  389. package/public/r/comp-340.json +22 -0
  390. package/public/r/comp-341.json +22 -0
  391. package/public/r/comp-342.json +22 -0
  392. package/public/r/comp-343.json +22 -0
  393. package/public/r/comp-344.json +22 -0
  394. package/public/r/comp-345.json +22 -0
  395. package/public/r/comp-346.json +22 -0
  396. package/public/r/comp-347.json +22 -0
  397. package/public/r/comp-348.json +22 -0
  398. package/public/r/comp-349.json +22 -0
  399. package/public/r/comp-35.json +27 -0
  400. package/public/r/comp-350.json +22 -0
  401. package/public/r/comp-351.json +22 -0
  402. package/public/r/comp-352.json +24 -0
  403. package/public/r/comp-353.json +24 -0
  404. package/public/r/comp-354.json +23 -0
  405. package/public/r/comp-355.json +23 -0
  406. package/public/r/comp-356.json +23 -0
  407. package/public/r/comp-357.json +23 -0
  408. package/public/r/comp-358.json +23 -0
  409. package/public/r/comp-359.json +24 -0
  410. package/public/r/comp-36.json +26 -0
  411. package/public/r/comp-360.json +25 -0
  412. package/public/r/comp-361.json +23 -0
  413. package/public/r/comp-362.json +24 -0
  414. package/public/r/comp-363.json +27 -0
  415. package/public/r/comp-364.json +26 -0
  416. package/public/r/comp-365.json +24 -0
  417. package/public/r/comp-366.json +23 -0
  418. package/public/r/comp-367.json +23 -0
  419. package/public/r/comp-368.json +23 -0
  420. package/public/r/comp-369.json +23 -0
  421. package/public/r/comp-37.json +23 -0
  422. package/public/r/comp-370.json +23 -0
  423. package/public/r/comp-371.json +24 -0
  424. package/public/r/comp-372.json +24 -0
  425. package/public/r/comp-373.json +25 -0
  426. package/public/r/comp-374.json +26 -0
  427. package/public/r/comp-375.json +25 -0
  428. package/public/r/comp-376.json +26 -0
  429. package/public/r/comp-377.json +27 -0
  430. package/public/r/comp-378.json +24 -0
  431. package/public/r/comp-379.json +24 -0
  432. package/public/r/comp-38.json +23 -0
  433. package/public/r/comp-380.json +24 -0
  434. package/public/r/comp-381.json +26 -0
  435. package/public/r/comp-382.json +25 -0
  436. package/public/r/comp-383.json +26 -0
  437. package/public/r/comp-384.json +24 -0
  438. package/public/r/comp-385.json +24 -0
  439. package/public/r/comp-386.json +24 -0
  440. package/public/r/comp-387.json +28 -0
  441. package/public/r/comp-388.json +26 -0
  442. package/public/r/comp-389.json +24 -0
  443. package/public/r/comp-39.json +23 -0
  444. package/public/r/comp-390.json +23 -0
  445. package/public/r/comp-391.json +23 -0
  446. package/public/r/comp-392.json +23 -0
  447. package/public/r/comp-393.json +23 -0
  448. package/public/r/comp-394.json +24 -0
  449. package/public/r/comp-395.json +24 -0
  450. package/public/r/comp-396.json +24 -0
  451. package/public/r/comp-397.json +25 -0
  452. package/public/r/comp-398.json +26 -0
  453. package/public/r/comp-399.json +26 -0
  454. package/public/r/comp-40.json +23 -0
  455. package/public/r/comp-400.json +20 -0
  456. package/public/r/comp-401.json +19 -0
  457. package/public/r/comp-402.json +19 -0
  458. package/public/r/comp-403.json +19 -0
  459. package/public/r/comp-404.json +19 -0
  460. package/public/r/comp-405.json +19 -0
  461. package/public/r/comp-406.json +19 -0
  462. package/public/r/comp-407.json +19 -0
  463. package/public/r/comp-408.json +19 -0
  464. package/public/r/comp-409.json +22 -0
  465. package/public/r/comp-41.json +29 -0
  466. package/public/r/comp-410.json +22 -0
  467. package/public/r/comp-411.json +22 -0
  468. package/public/r/comp-412.json +19 -0
  469. package/public/r/comp-413.json +22 -0
  470. package/public/r/comp-414.json +22 -0
  471. package/public/r/comp-415.json +22 -0
  472. package/public/r/comp-416.json +23 -0
  473. package/public/r/comp-417.json +22 -0
  474. package/public/r/comp-418.json +23 -0
  475. package/public/r/comp-419.json +23 -0
  476. package/public/r/comp-42.json +30 -0
  477. package/public/r/comp-420.json +23 -0
  478. package/public/r/comp-421.json +23 -0
  479. package/public/r/comp-422.json +23 -0
  480. package/public/r/comp-423.json +24 -0
  481. package/public/r/comp-424.json +22 -0
  482. package/public/r/comp-425.json +23 -0
  483. package/public/r/comp-426.json +23 -0
  484. package/public/r/comp-427.json +23 -0
  485. package/public/r/comp-428.json +23 -0
  486. package/public/r/comp-429.json +23 -0
  487. package/public/r/comp-43.json +32 -0
  488. package/public/r/comp-430.json +23 -0
  489. package/public/r/comp-431.json +23 -0
  490. package/public/r/comp-432.json +23 -0
  491. package/public/r/comp-433.json +24 -0
  492. package/public/r/comp-434.json +24 -0
  493. package/public/r/comp-435.json +24 -0
  494. package/public/r/comp-436.json +24 -0
  495. package/public/r/comp-437.json +25 -0
  496. package/public/r/comp-438.json +23 -0
  497. package/public/r/comp-439.json +24 -0
  498. package/public/r/comp-44.json +25 -0
  499. package/public/r/comp-440.json +25 -0
  500. package/public/r/comp-441.json +25 -0
  501. package/public/r/comp-442.json +24 -0
  502. package/public/r/comp-443.json +24 -0
  503. package/public/r/comp-444.json +24 -0
  504. package/public/r/comp-445.json +24 -0
  505. package/public/r/comp-446.json +24 -0
  506. package/public/r/comp-447.json +25 -0
  507. package/public/r/comp-448.json +23 -0
  508. package/public/r/comp-449.json +23 -0
  509. package/public/r/comp-45.json +25 -0
  510. package/public/r/comp-450.json +23 -0
  511. package/public/r/comp-451.json +23 -0
  512. package/public/r/comp-452.json +23 -0
  513. package/public/r/comp-453.json +25 -0
  514. package/public/r/comp-454.json +22 -0
  515. package/public/r/comp-455.json +22 -0
  516. package/public/r/comp-456.json +22 -0
  517. package/public/r/comp-457.json +21 -0
  518. package/public/r/comp-458.json +22 -0
  519. package/public/r/comp-459.json +26 -0
  520. package/public/r/comp-46.json +26 -0
  521. package/public/r/comp-460.json +26 -0
  522. package/public/r/comp-461.json +27 -0
  523. package/public/r/comp-462.json +27 -0
  524. package/public/r/comp-463.json +30 -0
  525. package/public/r/comp-464.json +29 -0
  526. package/public/r/comp-465.json +29 -0
  527. package/public/r/comp-466.json +21 -0
  528. package/public/r/comp-467.json +23 -0
  529. package/public/r/comp-468.json +21 -0
  530. package/public/r/comp-469.json +21 -0
  531. package/public/r/comp-47.json +32 -0
  532. package/public/r/comp-470.json +21 -0
  533. package/public/r/comp-471.json +21 -0
  534. package/public/r/comp-472.json +23 -0
  535. package/public/r/comp-473.json +24 -0
  536. package/public/r/comp-474.json +22 -0
  537. package/public/r/comp-475.json +22 -0
  538. package/public/r/comp-476.json +21 -0
  539. package/public/r/comp-477.json +31 -0
  540. package/public/r/comp-478.json +36 -0
  541. package/public/r/comp-479.json +28 -0
  542. package/public/r/comp-48.json +32 -0
  543. package/public/r/comp-480.json +30 -0
  544. package/public/r/comp-481.json +34 -0
  545. package/public/r/comp-482.json +33 -0
  546. package/public/r/comp-483.json +37 -0
  547. package/public/r/comp-484.json +41 -0
  548. package/public/r/comp-485.json +43 -0
  549. package/public/r/comp-486.json +23 -0
  550. package/public/r/comp-487.json +26 -0
  551. package/public/r/comp-488.json +27 -0
  552. package/public/r/comp-489.json +28 -0
  553. package/public/r/comp-49.json +32 -0
  554. package/public/r/comp-490.json +23 -0
  555. package/public/r/comp-491.json +24 -0
  556. package/public/r/comp-492.json +25 -0
  557. package/public/r/comp-493.json +23 -0
  558. package/public/r/comp-494.json +23 -0
  559. package/public/r/comp-495.json +24 -0
  560. package/public/r/comp-496.json +23 -0
  561. package/public/r/comp-497.json +24 -0
  562. package/public/r/comp-498.json +24 -0
  563. package/public/r/comp-499.json +24 -0
  564. package/public/r/comp-50.json +32 -0
  565. package/public/r/comp-500.json +25 -0
  566. package/public/r/comp-501.json +25 -0
  567. package/public/r/comp-502.json +26 -0
  568. package/public/r/comp-503.json +27 -0
  569. package/public/r/comp-504.json +28 -0
  570. package/public/r/comp-505.json +28 -0
  571. package/public/r/comp-506.json +26 -0
  572. package/public/r/comp-507.json +27 -0
  573. package/public/r/comp-508.json +25 -0
  574. package/public/r/comp-509.json +25 -0
  575. package/public/r/comp-51.json +23 -0
  576. package/public/r/comp-510.json +24 -0
  577. package/public/r/comp-511.json +27 -0
  578. package/public/r/comp-512.json +27 -0
  579. package/public/r/comp-513.json +21 -0
  580. package/public/r/comp-514.json +21 -0
  581. package/public/r/comp-515.json +21 -0
  582. package/public/r/comp-516.json +22 -0
  583. package/public/r/comp-517.json +22 -0
  584. package/public/r/comp-518.json +21 -0
  585. package/public/r/comp-519.json +21 -0
  586. package/public/r/comp-52.json +23 -0
  587. package/public/r/comp-520.json +22 -0
  588. package/public/r/comp-521.json +22 -0
  589. package/public/r/comp-522.json +21 -0
  590. package/public/r/comp-523.json +21 -0
  591. package/public/r/comp-524.json +21 -0
  592. package/public/r/comp-525.json +21 -0
  593. package/public/r/comp-526.json +22 -0
  594. package/public/r/comp-527.json +23 -0
  595. package/public/r/comp-528.json +22 -0
  596. package/public/r/comp-529.json +22 -0
  597. package/public/r/comp-53.json +24 -0
  598. package/public/r/comp-530.json +22 -0
  599. package/public/r/comp-531.json +22 -0
  600. package/public/r/comp-532.json +22 -0
  601. package/public/r/comp-533.json +22 -0
  602. package/public/r/comp-534.json +22 -0
  603. package/public/r/comp-535.json +22 -0
  604. package/public/r/comp-536.json +22 -0
  605. package/public/r/comp-537.json +22 -0
  606. package/public/r/comp-538.json +22 -0
  607. package/public/r/comp-539.json +22 -0
  608. package/public/r/comp-54.json +26 -0
  609. package/public/r/comp-540.json +21 -0
  610. package/public/r/comp-541.json +21 -0
  611. package/public/r/comp-542.json +124 -0
  612. package/public/r/comp-543.json +29 -0
  613. package/public/r/comp-544.json +26 -0
  614. package/public/r/comp-545.json +29 -0
  615. package/public/r/comp-546.json +29 -0
  616. package/public/r/comp-547.json +29 -0
  617. package/public/r/comp-548.json +29 -0
  618. package/public/r/comp-549.json +29 -0
  619. package/public/r/comp-55.json +27 -0
  620. package/public/r/comp-550.json +29 -0
  621. package/public/r/comp-551.json +30 -0
  622. package/public/r/comp-552.json +29 -0
  623. package/public/r/comp-553.json +29 -0
  624. package/public/r/comp-554.json +36 -0
  625. package/public/r/comp-555.json +23 -0
  626. package/public/r/comp-556.json +23 -0
  627. package/public/r/comp-557.json +23 -0
  628. package/public/r/comp-558.json +23 -0
  629. package/public/r/comp-559.json +23 -0
  630. package/public/r/comp-56.json +26 -0
  631. package/public/r/comp-560.json +23 -0
  632. package/public/r/comp-561.json +25 -0
  633. package/public/r/comp-562.json +23 -0
  634. package/public/r/comp-563.json +23 -0
  635. package/public/r/comp-564.json +24 -0
  636. package/public/r/comp-565.json +20 -0
  637. package/public/r/comp-566.json +20 -0
  638. package/public/r/comp-567.json +20 -0
  639. package/public/r/comp-568.json +20 -0
  640. package/public/r/comp-569.json +20 -0
  641. package/public/r/comp-57.json +26 -0
  642. package/public/r/comp-570.json +20 -0
  643. package/public/r/comp-571.json +23 -0
  644. package/public/r/comp-572.json +23 -0
  645. package/public/r/comp-573.json +20 -0
  646. package/public/r/comp-574.json +22 -0
  647. package/public/r/comp-575.json +20 -0
  648. package/public/r/comp-576.json +21 -0
  649. package/public/r/comp-577.json +23 -0
  650. package/public/r/comp-578.json +28 -0
  651. package/public/r/comp-579.json +28 -0
  652. package/public/r/comp-58.json +25 -0
  653. package/public/r/comp-580.json +29 -0
  654. package/public/r/comp-581.json +45 -0
  655. package/public/r/comp-582.json +42 -0
  656. package/public/r/comp-583.json +41 -0
  657. package/public/r/comp-584.json +41 -0
  658. package/public/r/comp-585.json +41 -0
  659. package/public/r/comp-586.json +34 -0
  660. package/public/r/comp-587.json +29 -0
  661. package/public/r/comp-588.json +33 -0
  662. package/public/r/comp-589.json +37 -0
  663. package/public/r/comp-59.json +22 -0
  664. package/public/r/comp-590.json +40 -0
  665. package/public/r/comp-591.json +29 -0
  666. package/public/r/comp-592.json +41 -0
  667. package/public/r/comp-593.json +36 -0
  668. package/public/r/comp-594.json +33 -0
  669. package/public/r/comp-595.json +22 -0
  670. package/public/r/comp-596.json +26 -0
  671. package/public/r/comp-597.json +22 -0
  672. package/public/r/comp-598.json +22 -0
  673. package/public/r/comp-599.json +22 -0
  674. package/public/r/comp-60.json +23 -0
  675. package/public/r/comp-600.json +17 -0
  676. package/public/r/comp-601.json +20 -0
  677. package/public/r/comp-602.json +17 -0
  678. package/public/r/comp-603.json +20 -0
  679. package/public/r/comp-604.json +17 -0
  680. package/public/r/comp-605.json +17 -0
  681. package/public/r/comp-606.json +20 -0
  682. package/public/r/comp-607.json +20 -0
  683. package/public/r/comp-608.json +20 -0
  684. package/public/r/comp-609.json +20 -0
  685. package/public/r/comp-61.json +23 -0
  686. package/public/r/comp-610.json +23 -0
  687. package/public/r/comp-611.json +23 -0
  688. package/public/r/comp-612.json +17 -0
  689. package/public/r/comp-613.json +20 -0
  690. package/public/r/comp-614.json +17 -0
  691. package/public/r/comp-615.json +26 -0
  692. package/public/r/comp-616.json +26 -0
  693. package/public/r/comp-617.json +26 -0
  694. package/public/r/comp-62.json +23 -0
  695. package/public/r/comp-63.json +22 -0
  696. package/public/r/comp-64.json +23 -0
  697. package/public/r/comp-65.json +22 -0
  698. package/public/r/comp-66.json +22 -0
  699. package/public/r/comp-67.json +23 -0
  700. package/public/r/comp-68.json +24 -0
  701. package/public/r/comp-69.json +24 -0
  702. package/public/r/comp-70.json +24 -0
  703. package/public/r/comp-71.json +22 -0
  704. package/public/r/comp-72.json +21 -0
  705. package/public/r/comp-73.json +18 -0
  706. package/public/r/comp-74.json +27 -0
  707. package/public/r/comp-75.json +22 -0
  708. package/public/r/comp-76.json +23 -0
  709. package/public/r/comp-77.json +22 -0
  710. package/public/r/comp-78.json +21 -0
  711. package/public/r/comp-79.json +22 -0
  712. package/public/r/comp-80.json +21 -0
  713. package/public/r/comp-81.json +21 -0
  714. package/public/r/comp-82.json +22 -0
  715. package/public/r/comp-83.json +21 -0
  716. package/public/r/comp-84.json +21 -0
  717. package/public/r/comp-85.json +22 -0
  718. package/public/r/comp-86.json +22 -0
  719. package/public/r/comp-87.json +21 -0
  720. package/public/r/comp-88.json +22 -0
  721. package/public/r/comp-89.json +21 -0
  722. package/public/r/comp-90.json +22 -0
  723. package/public/r/comp-91.json +22 -0
  724. package/public/r/comp-92.json +22 -0
  725. package/public/r/comp-93.json +22 -0
  726. package/public/r/comp-94.json +24 -0
  727. package/public/r/comp-95.json +26 -0
  728. package/public/r/comp-96.json +21 -0
  729. package/public/r/comp-97.json +21 -0
  730. package/public/r/comp-98.json +22 -0
  731. package/public/r/comp-99.json +23 -0
  732. package/public/r/comparison-event-handlers.json +1 -1
  733. package/public/r/comparison-hover.json +1 -1
  734. package/public/r/comparison.json +1 -1
  735. package/public/r/context-menu.json +16 -0
  736. package/public/r/contribution-graph-custom-footer.json +1 -1
  737. package/public/r/credit-card-apple.json +1 -1
  738. package/public/r/credit-card-back.json +1 -1
  739. package/public/r/credit-card.json +1 -1
  740. package/public/r/cropper.json +15 -0
  741. package/public/r/cursor-color.json +1 -1
  742. package/public/r/cursor-message.json +1 -1
  743. package/public/r/cursor-name-message.json +1 -1
  744. package/public/r/cursor-name.json +1 -1
  745. package/public/r/cursor.json +1 -1
  746. package/public/r/dashboard-1.json +263 -0
  747. package/public/r/dashboard-2.json +264 -0
  748. package/public/r/dashboard-3.json +225 -0
  749. package/public/r/datefield-rac.json +15 -0
  750. package/public/r/deck-controlled.json +1 -1
  751. package/public/r/deck-product-cards.json +1 -1
  752. package/public/r/deck.json +1 -1
  753. package/public/r/dialog-close-confirmation.json +1 -1
  754. package/public/r/dialog-demo.json +1 -1
  755. package/public/r/dialog-from-menu.json +1 -1
  756. package/public/r/dialog-nested.json +1 -1
  757. package/public/r/dialog-stack-controlled.json +1 -1
  758. package/public/r/dialog-stack-navigation.json +1 -1
  759. package/public/r/dialog-stack-six.json +1 -1
  760. package/public/r/dialog-stack.json +1 -1
  761. package/public/r/dropdown-menu.json +15 -0
  762. package/public/r/dropzone-custom-empty-state.json +1 -1
  763. package/public/r/dropzone-image-preview.json +1 -1
  764. package/public/r/editor.json +1 -1
  765. package/public/r/emails.json +243 -0
  766. package/public/r/empty-demo.json +1 -1
  767. package/public/r/example-app-calendar.json +323 -0
  768. package/public/r/field-2.json +19 -0
  769. package/public/r/field-autocomplete.json +1 -1
  770. package/public/r/field-checkbox-group.json +1 -1
  771. package/public/r/field-checkbox.json +1 -1
  772. package/public/r/field-combobox-multiple.json +1 -1
  773. package/public/r/field-combobox.json +1 -1
  774. package/public/r/field-complete-form.json +1 -1
  775. package/public/r/field-demo.json +1 -1
  776. package/public/r/field-disabled.json +1 -1
  777. package/public/r/field-error.json +1 -1
  778. package/public/r/field-number-field.json +1 -1
  779. package/public/r/field-radio.json +1 -1
  780. package/public/r/field-required.json +1 -1
  781. package/public/r/field-select.json +1 -1
  782. package/public/r/field-slider.json +1 -1
  783. package/public/r/field-switch.json +1 -1
  784. package/public/r/field-textarea.json +1 -1
  785. package/public/r/field-validity.json +1 -1
  786. package/public/r/fieldset-demo.json +1 -1
  787. package/public/r/files.json +195 -0
  788. package/public/r/form-demo.json +1 -1
  789. package/public/r/form-zod.json +1 -1
  790. package/public/r/form.json +1 -1
  791. package/public/r/frame-demo.json +1 -1
  792. package/public/r/gantt-lanes.json +1 -1
  793. package/public/r/gantt-no-sidebar.json +1 -1
  794. package/public/r/gantt-read-only.json +1 -1
  795. package/public/r/gantt.json +1 -1
  796. package/public/r/glimpse-custom.json +1 -1
  797. package/public/r/glimpse.json +1 -1
  798. package/public/r/gooey-toast-demo.json +1 -1
  799. package/public/r/gooey-toast-promise.json +1 -1
  800. package/public/r/gooey-toast-states.json +1 -1
  801. package/public/r/gooey-toast-with-button.json +1 -1
  802. package/public/r/group-demo.json +1 -1
  803. package/public/r/group-with-input.json +1 -1
  804. package/public/r/hero.json +1 -1
  805. package/public/r/hover-card.json +15 -0
  806. package/public/r/image-crop-circular.json +1 -1
  807. package/public/r/image-crop-custom.json +1 -1
  808. package/public/r/image-crop.json +1 -1
  809. package/public/r/image-zoom-background.json +1 -1
  810. package/public/r/image-zoom-margin.json +1 -1
  811. package/public/r/image-zoom.json +1 -1
  812. package/public/r/input-demo.json +1 -1
  813. package/public/r/input-disabled.json +1 -1
  814. package/public/r/input-file.json +1 -1
  815. package/public/r/input-group.json +20 -0
  816. package/public/r/input-lg.json +1 -1
  817. package/public/r/input-sm.json +1 -1
  818. package/public/r/input-with-button.json +1 -1
  819. package/public/r/input-with-label.json +1 -1
  820. package/public/r/kanban-simple.json +1 -1
  821. package/public/r/kanban.json +1 -1
  822. package/public/r/kbd.json +12 -0
  823. package/public/r/leads.json +271 -0
  824. package/public/r/list-simple.json +1 -1
  825. package/public/r/list.json +1 -1
  826. package/public/r/maps.json +188 -0
  827. package/public/r/menu-checkbox.json +1 -1
  828. package/public/r/menu-close-on-click.json +1 -1
  829. package/public/r/menu-demo.json +1 -1
  830. package/public/r/menu-group-labels.json +1 -1
  831. package/public/r/menu-hover.json +1 -1
  832. package/public/r/menu-link.json +1 -1
  833. package/public/r/menu-nested.json +1 -1
  834. package/public/r/menu-radio-group.json +1 -1
  835. package/public/r/meter-demo.json +1 -1
  836. package/public/r/meter-with-formatted-value.json +1 -1
  837. package/public/r/meter-with-range.json +1 -1
  838. package/public/r/mini-calendar-controlled.json +1 -1
  839. package/public/r/multiselect.json +15 -0
  840. package/public/r/navigation-menu.json +12 -0
  841. package/public/r/number-field-form.json +1 -1
  842. package/public/r/number-field-with-label.json +1 -1
  843. package/public/r/number-field-with-scrub.json +1 -1
  844. package/public/r/number-field-with-step.json +1 -1
  845. package/public/r/pill-avatar-group.json +1 -1
  846. package/public/r/pill-delta.json +1 -1
  847. package/public/r/pill-icon.json +1 -1
  848. package/public/r/pill-indicator.json +1 -1
  849. package/public/r/pill-status.json +1 -1
  850. package/public/r/pill.json +1 -1
  851. package/public/r/popover-demo.json +1 -1
  852. package/public/r/popover-with-close.json +1 -1
  853. package/public/r/preview-card-demo.json +1 -1
  854. package/public/r/pricing.json +1 -1
  855. package/public/r/progress-with-formatted-value.json +1 -1
  856. package/public/r/progress-with-label-value.json +1 -1
  857. package/public/r/radio-group-card.json +1 -1
  858. package/public/r/radio-group-demo.json +1 -1
  859. package/public/r/radio-group-disabled.json +1 -1
  860. package/public/r/radio-group-form.json +1 -1
  861. package/public/r/radio-group-with-description.json +1 -1
  862. package/public/r/reel-custom.json +1 -1
  863. package/public/r/reel-images.json +1 -1
  864. package/public/r/reel-minimal.json +1 -1
  865. package/public/r/reel.json +1 -1
  866. package/public/r/registry.json +18217 -0
  867. package/public/r/relative-time-controlled.json +1 -1
  868. package/public/r/relative-time-format-date.json +1 -1
  869. package/public/r/relative-time-format-time.json +1 -1
  870. package/public/r/relative-time.json +1 -1
  871. package/public/r/rentals.json +187 -0
  872. package/public/r/roadmap.json +1 -1
  873. package/public/r/sandbox-no-file-explorer.json +1 -1
  874. package/public/r/sandbox.json +1 -1
  875. package/public/r/scroll-area-both.json +1 -1
  876. package/public/r/scroll-area-demo.json +1 -1
  877. package/public/r/scroll-area-horizontal.json +1 -1
  878. package/public/r/select-demo.json +1 -1
  879. package/public/r/select-disabled.json +1 -1
  880. package/public/r/select-form.json +1 -1
  881. package/public/r/select-lg.json +1 -1
  882. package/public/r/select-native.json +12 -0
  883. package/public/r/select-sm.json +1 -1
  884. package/public/r/select-with-groups.json +1 -1
  885. package/public/r/select-without-alignment.json +1 -1
  886. package/public/r/select.json +1 -1
  887. package/public/r/separator-demo.json +1 -1
  888. package/public/r/sheet-demo.json +1 -1
  889. package/public/r/sheet-position.json +1 -1
  890. package/public/r/sidebar-nav.json +22 -0
  891. package/public/r/skeleton-demo.json +1 -1
  892. package/public/r/skeleton.json +1 -1
  893. package/public/r/slider-form.json +1 -1
  894. package/public/r/slider-with-label-value.json +1 -1
  895. package/public/r/sonner.json +16 -0
  896. package/public/r/stats-card.json +22 -0
  897. package/public/r/status-custom.json +1 -1
  898. package/public/r/stepper.json +15 -0
  899. package/public/r/stories-images.json +1 -1
  900. package/public/r/switch-card.json +1 -1
  901. package/public/r/switch-demo.json +1 -1
  902. package/public/r/switch-disabled.json +1 -1
  903. package/public/r/switch-form.json +1 -1
  904. package/public/r/switch-with-description.json +1 -1
  905. package/public/r/table-demo.json +1 -1
  906. package/public/r/table-framed.json +1 -1
  907. package/public/r/table-simple.json +1 -1
  908. package/public/r/table.json +1 -1
  909. package/public/r/tabs-demo.json +1 -1
  910. package/public/r/tabs-underline-vertical.json +1 -1
  911. package/public/r/tabs-underline.json +1 -1
  912. package/public/r/tabs-vertical.json +1 -1
  913. package/public/r/tags-create.json +1 -1
  914. package/public/r/tags-filter.json +1 -1
  915. package/public/r/tags.json +1 -1
  916. package/public/r/template-auth-eight.json +28 -0
  917. package/public/r/template-auth-five.json +28 -0
  918. package/public/r/template-auth-four.json +29 -0
  919. package/public/r/template-auth-nine.json +28 -0
  920. package/public/r/template-auth-one.json +49 -0
  921. package/public/r/template-auth-seven.json +29 -0
  922. package/public/r/template-auth-six.json +28 -0
  923. package/public/r/template-auth-three.json +49 -0
  924. package/public/r/template-auth-two.json +44 -0
  925. package/public/r/template-hero-eight.json +83 -0
  926. package/public/r/template-hero-eleven.json +29 -0
  927. package/public/r/template-hero-five.json +35 -0
  928. package/public/r/template-hero-four.json +35 -0
  929. package/public/r/template-hero-fourteen.json +101 -0
  930. package/public/r/template-hero-nine.json +74 -0
  931. package/public/r/template-hero-one.json +90 -0
  932. package/public/r/template-hero-seven.json +83 -0
  933. package/public/r/template-hero-six.json +83 -0
  934. package/public/r/template-hero-ten.json +77 -0
  935. package/public/r/template-hero-thirteen.json +101 -0
  936. package/public/r/template-hero-three.json +35 -0
  937. package/public/r/template-hero-twelve.json +30 -0
  938. package/public/r/template-hero-two.json +96 -0
  939. package/public/r/template-logo-cloud-five.json +68 -0
  940. package/public/r/template-logo-cloud-four.json +71 -0
  941. package/public/r/template-logo-cloud-one.json +89 -0
  942. package/public/r/template-logo-cloud-three.json +71 -0
  943. package/public/r/template-logo-cloud-two.json +74 -0
  944. package/public/r/textarea-demo.json +1 -1
  945. package/public/r/textarea-disabled.json +1 -1
  946. package/public/r/textarea-form.json +1 -1
  947. package/public/r/textarea-lg.json +1 -1
  948. package/public/r/textarea-sm.json +1 -1
  949. package/public/r/textarea-with-label.json +1 -1
  950. package/public/r/ticker-inline.json +1 -1
  951. package/public/r/timeline.json +15 -0
  952. package/public/r/toast-demo.json +1 -1
  953. package/public/r/toast-heights.json +1 -1
  954. package/public/r/toast-loading.json +1 -1
  955. package/public/r/toast-promise.json +1 -1
  956. package/public/r/toast-with-action.json +1 -1
  957. package/public/r/toast-with-status.json +1 -1
  958. package/public/r/toggle-demo.json +1 -1
  959. package/public/r/toggle-disabled.json +1 -1
  960. package/public/r/toggle-group-demo.json +1 -1
  961. package/public/r/toggle-group-disabled.json +1 -1
  962. package/public/r/toggle-group-lg.json +1 -1
  963. package/public/r/toggle-group-multiple.json +1 -1
  964. package/public/r/toggle-group-outline-with-separator.json +1 -1
  965. package/public/r/toggle-group-outline.json +1 -1
  966. package/public/r/toggle-group-sm.json +1 -1
  967. package/public/r/toggle-group-with-disabled-item.json +1 -1
  968. package/public/r/toggle-icon-group.json +1 -1
  969. package/public/r/toggle-lg.json +1 -1
  970. package/public/r/toggle-outline.json +1 -1
  971. package/public/r/toggle-sm.json +1 -1
  972. package/public/r/toggle-with-icon.json +1 -1
  973. package/public/r/toolbar-demo.json +1 -1
  974. package/public/r/tooltip-demo.json +1 -1
  975. package/public/r/tooltip-grouped.json +1 -1
  976. package/public/r/tree-controlled.json +1 -1
  977. package/public/r/tree-custom-icons.json +1 -1
  978. package/public/r/tree-no-lines.json +1 -1
  979. package/public/r/tree-simple.json +1 -1
  980. package/public/r/tree.json +1 -1
  981. package/public/r/typography.json +1 -1
  982. package/public/r/ui.json +5 -0
  983. package/public/r/use-character-limit.json +12 -0
  984. package/public/r/use-file-upload.json +12 -0
  985. package/public/r/use-pagination.json +12 -0
  986. package/public/r/use-slider-with-input.json +12 -0
  987. package/public/r/use-toast.json +12 -0
  988. package/public/r/utils.json +16 -0
@@ -0,0 +1,124 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "comp-542",
4
+ "type": "registry:component",
5
+ "dependencies": [
6
+ "date-fns",
7
+ "@dnd-kit/core",
8
+ "@dnd-kit/modifiers",
9
+ "@dnd-kit/utilities",
10
+ "@remixicon/react"
11
+ ],
12
+ "registryDependencies": [
13
+ "https://loveui.dev/building-blocks/r/button.json",
14
+ "https://loveui.dev/building-blocks/r/calendar.json",
15
+ "https://loveui.dev/building-blocks/r/checkbox.json",
16
+ "https://loveui.dev/building-blocks/r/dropdown-menu.json",
17
+ "https://loveui.dev/building-blocks/r/dialog.json",
18
+ "https://loveui.dev/building-blocks/r/input.json",
19
+ "https://loveui.dev/building-blocks/r/textarea.json",
20
+ "https://loveui.dev/building-blocks/r/label.json",
21
+ "https://loveui.dev/building-blocks/r/popover.json",
22
+ "https://loveui.dev/building-blocks/r/radio-group.json",
23
+ "https://loveui.dev/building-blocks/r/select.json",
24
+ "https://loveui.dev/building-blocks/r/sonner.json"
25
+ ],
26
+ "files": [
27
+ {
28
+ "path": "registry/default/components/comp-542.tsx",
29
+ "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { addDays, setHours, setMinutes, subDays } from \"date-fns\"\n\nimport {\n EventCalendar,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\n\n// Sample events data with hardcoded times\nconst sampleEvents: CalendarEvent[] = [\n {\n id: \"1\",\n title: \"Annual Planning\",\n description: \"Strategic planning for next year\",\n start: subDays(new Date(), 24), // 24 days before today\n end: subDays(new Date(), 23), // 23 days before today\n allDay: true,\n color: \"sky\",\n location: \"Main Conference Hall\",\n },\n {\n id: \"2\",\n title: \"Project Deadline\",\n description: \"Submit final deliverables\",\n start: setMinutes(setHours(subDays(new Date(), 9), 13), 0), // 1:00 PM, 9 days before\n end: setMinutes(setHours(subDays(new Date(), 9), 15), 30), // 3:30 PM, 9 days before\n color: \"amber\",\n location: \"Office\",\n },\n {\n id: \"3\",\n title: \"Quarterly Budget Review\",\n description: \"Strategic planning for next year\",\n start: subDays(new Date(), 13), // 13 days before today\n end: subDays(new Date(), 13), // 13 days before today\n allDay: true,\n color: \"orange\",\n location: \"Main Conference Hall\",\n },\n {\n id: \"4\",\n title: \"Team Meeting\",\n description: \"Weekly team sync\",\n start: setMinutes(setHours(new Date(), 10), 0), // 10:00 AM today\n end: setMinutes(setHours(new Date(), 11), 0), // 11:00 AM today\n color: \"sky\",\n location: \"Conference Room A\",\n },\n {\n id: \"5\",\n title: \"Lunch with Client\",\n description: \"Discuss new project requirements\",\n start: setMinutes(setHours(addDays(new Date(), 1), 12), 0), // 12:00 PM, 1 day from now\n end: setMinutes(setHours(addDays(new Date(), 1), 13), 15), // 1:15 PM, 1 day from now\n color: \"emerald\",\n location: \"Downtown Cafe\",\n },\n {\n id: \"6\",\n title: \"Product Launch\",\n description: \"New product release\",\n start: addDays(new Date(), 3), // 3 days from now\n end: addDays(new Date(), 6), // 6 days from now\n allDay: true,\n color: \"violet\",\n },\n {\n id: \"7\",\n title: \"Sales Conference\",\n description: \"Discuss about new clients\",\n start: setMinutes(setHours(addDays(new Date(), 4), 14), 30), // 2:30 PM, 4 days from now\n end: setMinutes(setHours(addDays(new Date(), 5), 14), 45), // 2:45 PM, 5 days from now\n color: \"rose\",\n location: \"Downtown Cafe\",\n },\n {\n id: \"8\",\n title: \"Team Meeting\",\n description: \"Weekly team sync\",\n start: setMinutes(setHours(addDays(new Date(), 5), 9), 0), // 9:00 AM, 5 days from now\n end: setMinutes(setHours(addDays(new Date(), 5), 10), 30), // 10:30 AM, 5 days from now\n color: \"orange\",\n location: \"Conference Room A\",\n },\n {\n id: \"9\",\n title: \"Review contracts\",\n description: \"Weekly team sync\",\n start: setMinutes(setHours(addDays(new Date(), 5), 14), 0), // 2:00 PM, 5 days from now\n end: setMinutes(setHours(addDays(new Date(), 5), 15), 30), // 3:30 PM, 5 days from now\n color: \"sky\",\n location: \"Conference Room A\",\n },\n {\n id: \"10\",\n title: \"Team Meeting\",\n description: \"Weekly team sync\",\n start: setMinutes(setHours(addDays(new Date(), 5), 9), 45), // 9:45 AM, 5 days from now\n end: setMinutes(setHours(addDays(new Date(), 5), 11), 0), // 11:00 AM, 5 days from now\n color: \"amber\",\n location: \"Conference Room A\",\n },\n {\n id: \"11\",\n title: \"Marketing Strategy Session\",\n description: \"Quarterly marketing planning\",\n start: setMinutes(setHours(addDays(new Date(), 9), 10), 0), // 10:00 AM, 9 days from now\n end: setMinutes(setHours(addDays(new Date(), 9), 15), 30), // 3:30 PM, 9 days from now\n color: \"emerald\",\n location: \"Marketing Department\",\n },\n {\n id: \"12\",\n title: \"Annual Shareholders Meeting\",\n description: \"Presentation of yearly results\",\n start: addDays(new Date(), 17), // 17 days from now\n end: addDays(new Date(), 17), // 17 days from now\n allDay: true,\n color: \"sky\",\n location: \"Grand Conference Center\",\n },\n {\n id: \"13\",\n title: \"Product Development Workshop\",\n description: \"Brainstorming for new features\",\n start: setMinutes(setHours(addDays(new Date(), 26), 9), 0), // 9:00 AM, 26 days from now\n end: setMinutes(setHours(addDays(new Date(), 27), 17), 0), // 5:00 PM, 27 days from now\n color: \"rose\",\n location: \"Innovation Lab\",\n },\n]\n\nexport default function Component() {\n const [events, setEvents] = useState<CalendarEvent[]>(sampleEvents)\n\n const handleEventAdd = (event: CalendarEvent) => {\n setEvents([...events, event])\n }\n\n const handleEventUpdate = (updatedEvent: CalendarEvent) => {\n setEvents(\n events.map((event) =>\n event.id === updatedEvent.id ? updatedEvent : event\n )\n )\n }\n\n const handleEventDelete = (eventId: string) => {\n setEvents(events.filter((event) => event.id !== eventId))\n }\n\n return (\n <EventCalendar\n events={events}\n onEventAdd={handleEventAdd}\n onEventUpdate={handleEventUpdate}\n onEventDelete={handleEventDelete}\n />\n )\n}\n",
30
+ "type": "registry:component"
31
+ },
32
+ {
33
+ "path": "registry/default/components/event-calendar/agenda-view.tsx",
34
+ "content": "\"use client\"\n\nimport { useMemo } from \"react\"\nimport { RiCalendarEventLine } from \"@remixicon/react\"\nimport { addDays, format, isToday } from \"date-fns\"\n\nimport {\n AgendaDaysToShow,\n CalendarEvent,\n EventItem,\n getAgendaEventsForDay,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\n\ninterface AgendaViewProps {\n currentDate: Date\n events: CalendarEvent[]\n onEventSelect: (event: CalendarEvent) => void\n}\n\nexport function AgendaView({\n currentDate,\n events,\n onEventSelect,\n}: AgendaViewProps) {\n // Show events for the next days based on constant\n const days = useMemo(() => {\n console.log(\"Agenda view updating with date:\", currentDate.toISOString())\n return Array.from({ length: AgendaDaysToShow }, (_, i) =>\n addDays(new Date(currentDate), i)\n )\n }, [currentDate])\n\n const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n e.stopPropagation()\n console.log(\"Agenda view event clicked:\", event)\n onEventSelect(event)\n }\n\n // Check if there are any days with events\n const hasEvents = days.some(\n (day) => getAgendaEventsForDay(events, day).length > 0\n )\n\n return (\n <div className=\"border-t border-border/70 px-4\">\n {!hasEvents ? (\n <div className=\"flex min-h-[70svh] flex-col items-center justify-center py-16 text-center\">\n <RiCalendarEventLine\n size={32}\n className=\"mb-2 text-muted-foreground/50\"\n />\n <h3 className=\"text-lg font-medium\">No events found</h3>\n <p className=\"text-muted-foreground\">\n There are no events scheduled for this time period.\n </p>\n </div>\n ) : (\n days.map((day) => {\n const dayEvents = getAgendaEventsForDay(events, day)\n\n if (dayEvents.length === 0) return null\n\n return (\n <div\n key={day.toString()}\n className=\"relative my-12 border-t border-border/70\"\n >\n <span\n className=\"absolute -top-3 left-0 flex h-6 items-center bg-background pe-4 text-[10px] uppercase data-today:font-medium sm:pe-4 sm:text-xs\"\n data-today={isToday(day) || undefined}\n >\n {format(day, \"d MMM, EEEE\")}\n </span>\n <div className=\"mt-6 space-y-2\">\n {dayEvents.map((event) => (\n <EventItem\n key={event.id}\n event={event}\n view=\"agenda\"\n onClick={(e) => handleEventClick(event, e)}\n />\n ))}\n </div>\n </div>\n )\n })\n )}\n </div>\n )\n}\n",
35
+ "type": "registry:component"
36
+ },
37
+ {
38
+ "path": "registry/default/components/event-calendar/calendar-dnd-context.tsx",
39
+ "content": "\"use client\"\n\nimport {\n createContext,\n useContext,\n useId,\n useRef,\n useState,\n type ReactNode,\n} from \"react\"\nimport {\n DndContext,\n DragOverlay,\n MouseSensor,\n PointerSensor,\n TouchSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragOverEvent,\n type DragStartEvent,\n type UniqueIdentifier,\n} from \"@dnd-kit/core\"\nimport { addMinutes, differenceInMinutes } from \"date-fns\"\n\nimport {\n EventItem,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\n\n// Define the context type\ntype CalendarDndContextType = {\n activeEvent: CalendarEvent | null\n activeId: UniqueIdentifier | null\n activeView: \"month\" | \"week\" | \"day\" | null\n currentTime: Date | null\n eventHeight: number | null\n isMultiDay: boolean\n multiDayWidth: number | null\n dragHandlePosition: {\n x?: number\n y?: number\n data?: {\n isFirstDay?: boolean\n isLastDay?: boolean\n }\n } | null\n}\n\n// Create the context\nconst CalendarDndContext = createContext<CalendarDndContextType>({\n activeEvent: null,\n activeId: null,\n activeView: null,\n currentTime: null,\n eventHeight: null,\n isMultiDay: false,\n multiDayWidth: null,\n dragHandlePosition: null,\n})\n\n// Hook to use the context\nexport const useCalendarDnd = () => useContext(CalendarDndContext)\n\n// Props for the provider\ninterface CalendarDndProviderProps {\n children: ReactNode\n onEventUpdate: (event: CalendarEvent) => void\n}\n\nexport function CalendarDndProvider({\n children,\n onEventUpdate,\n}: CalendarDndProviderProps) {\n const [activeEvent, setActiveEvent] = useState<CalendarEvent | null>(null)\n const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)\n const [activeView, setActiveView] = useState<\"month\" | \"week\" | \"day\" | null>(\n null\n )\n const [currentTime, setCurrentTime] = useState<Date | null>(null)\n const [eventHeight, setEventHeight] = useState<number | null>(null)\n const [isMultiDay, setIsMultiDay] = useState(false)\n const [multiDayWidth, setMultiDayWidth] = useState<number | null>(null)\n const [dragHandlePosition, setDragHandlePosition] = useState<{\n x?: number\n y?: number\n data?: {\n isFirstDay?: boolean\n isLastDay?: boolean\n }\n } | null>(null)\n\n // Store original event dimensions\n const eventDimensions = useRef<{ height: number }>({ height: 0 })\n\n // Configure sensors for better drag detection\n const sensors = useSensors(\n useSensor(MouseSensor, {\n // Require the mouse to move by 5px before activating\n activationConstraint: {\n distance: 5,\n },\n }),\n useSensor(TouchSensor, {\n // Press delay of 250ms, with tolerance of 5px of movement\n activationConstraint: {\n delay: 250,\n tolerance: 5,\n },\n }),\n useSensor(PointerSensor, {\n // Require the pointer to move by 5px before activating\n activationConstraint: {\n distance: 5,\n },\n })\n )\n\n // Generate a stable ID for the DndContext\n const dndContextId = useId()\n\n const handleDragStart = (event: DragStartEvent) => {\n const { active } = event\n\n // Add safety check for data.current\n if (!active.data.current) {\n console.error(\"Missing data in drag start event\", event)\n return\n }\n\n const {\n event: calendarEvent,\n view,\n height,\n isMultiDay: eventIsMultiDay,\n multiDayWidth: eventMultiDayWidth,\n dragHandlePosition: eventDragHandlePosition,\n } = active.data.current as {\n event: CalendarEvent\n view: \"month\" | \"week\" | \"day\"\n height?: number\n isMultiDay?: boolean\n multiDayWidth?: number\n dragHandlePosition?: {\n x?: number\n y?: number\n data?: {\n isFirstDay?: boolean\n isLastDay?: boolean\n }\n }\n }\n\n setActiveEvent(calendarEvent)\n setActiveId(active.id)\n setActiveView(view)\n setCurrentTime(new Date(calendarEvent.start))\n setIsMultiDay(eventIsMultiDay || false)\n setMultiDayWidth(eventMultiDayWidth || null)\n setDragHandlePosition(eventDragHandlePosition || null)\n\n // Store event height if provided\n if (height) {\n eventDimensions.current.height = height\n setEventHeight(height)\n }\n }\n\n const handleDragOver = (event: DragOverEvent) => {\n const { over } = event\n\n if (over && activeEvent && over.data.current) {\n const { date, time } = over.data.current as { date: Date; time?: number }\n\n // Update time for week/day views\n if (time !== undefined && activeView !== \"month\") {\n const newTime = new Date(date)\n\n // Calculate hours and minutes with 15-minute precision\n const hours = Math.floor(time)\n const fractionalHour = time - hours\n\n // Map to nearest 15 minute interval (0, 0.25, 0.5, 0.75)\n let minutes = 0\n if (fractionalHour < 0.125) minutes = 0\n else if (fractionalHour < 0.375) minutes = 15\n else if (fractionalHour < 0.625) minutes = 30\n else minutes = 45\n\n newTime.setHours(hours, minutes, 0, 0)\n\n // Only update if time has changed\n if (\n !currentTime ||\n newTime.getHours() !== currentTime.getHours() ||\n newTime.getMinutes() !== currentTime.getMinutes() ||\n newTime.getDate() !== currentTime.getDate() ||\n newTime.getMonth() !== currentTime.getMonth() ||\n newTime.getFullYear() !== currentTime.getFullYear()\n ) {\n setCurrentTime(newTime)\n }\n } else if (activeView === \"month\") {\n // For month view, just update the date but preserve time\n const newTime = new Date(date)\n if (currentTime) {\n newTime.setHours(\n currentTime.getHours(),\n currentTime.getMinutes(),\n currentTime.getSeconds(),\n currentTime.getMilliseconds()\n )\n }\n\n // Only update if date has changed\n if (\n !currentTime ||\n newTime.getDate() !== currentTime.getDate() ||\n newTime.getMonth() !== currentTime.getMonth() ||\n newTime.getFullYear() !== currentTime.getFullYear()\n ) {\n setCurrentTime(newTime)\n }\n }\n }\n }\n\n const handleDragEnd = (event: DragEndEvent) => {\n const { active, over } = event\n\n // Add robust error checking\n if (!over || !activeEvent || !currentTime) {\n // Reset state and exit early\n setActiveEvent(null)\n setActiveId(null)\n setActiveView(null)\n setCurrentTime(null)\n setEventHeight(null)\n setIsMultiDay(false)\n setMultiDayWidth(null)\n setDragHandlePosition(null)\n return\n }\n\n try {\n // Safely access data with checks\n if (!active.data.current || !over.data.current) {\n throw new Error(\"Missing data in drag event\")\n }\n\n const activeData = active.data.current as {\n event?: CalendarEvent\n view?: string\n }\n const overData = over.data.current as { date?: Date; time?: number }\n\n // Verify we have all required data\n if (!activeData.event || !overData.date) {\n throw new Error(\"Missing required event data\")\n }\n\n const calendarEvent = activeData.event\n const date = overData.date\n const time = overData.time\n\n // Calculate new start time\n const newStart = new Date(date)\n\n // If time is provided (for week/day views), set the hours and minutes\n if (time !== undefined) {\n const hours = Math.floor(time)\n const fractionalHour = time - hours\n\n // Map to nearest 15 minute interval (0, 0.25, 0.5, 0.75)\n let minutes = 0\n if (fractionalHour < 0.125) minutes = 0\n else if (fractionalHour < 0.375) minutes = 15\n else if (fractionalHour < 0.625) minutes = 30\n else minutes = 45\n\n newStart.setHours(hours, minutes, 0, 0)\n } else {\n // For month view, preserve the original time from currentTime\n newStart.setHours(\n currentTime.getHours(),\n currentTime.getMinutes(),\n currentTime.getSeconds(),\n currentTime.getMilliseconds()\n )\n }\n\n // Calculate new end time based on the original duration\n const originalStart = new Date(calendarEvent.start)\n const originalEnd = new Date(calendarEvent.end)\n const durationMinutes = differenceInMinutes(originalEnd, originalStart)\n const newEnd = addMinutes(newStart, durationMinutes)\n\n // Only update if the start time has actually changed\n const hasStartTimeChanged =\n originalStart.getFullYear() !== newStart.getFullYear() ||\n originalStart.getMonth() !== newStart.getMonth() ||\n originalStart.getDate() !== newStart.getDate() ||\n originalStart.getHours() !== newStart.getHours() ||\n originalStart.getMinutes() !== newStart.getMinutes()\n\n if (hasStartTimeChanged) {\n // Update the event only if the time has changed\n onEventUpdate({\n ...calendarEvent,\n start: newStart,\n end: newEnd,\n })\n }\n } catch (error) {\n console.error(\"Error in drag end handler:\", error)\n } finally {\n // Always reset state\n setActiveEvent(null)\n setActiveId(null)\n setActiveView(null)\n setCurrentTime(null)\n setEventHeight(null)\n setIsMultiDay(false)\n setMultiDayWidth(null)\n setDragHandlePosition(null)\n }\n }\n\n return (\n <DndContext\n id={dndContextId}\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragEnd={handleDragEnd}\n >\n <CalendarDndContext.Provider\n value={{\n activeEvent,\n activeId,\n activeView,\n currentTime,\n eventHeight,\n isMultiDay,\n multiDayWidth,\n dragHandlePosition,\n }}\n >\n {children}\n\n <DragOverlay adjustScale={false} dropAnimation={null}>\n {activeEvent && activeView && (\n <div\n style={{\n height: eventHeight ? `${eventHeight}px` : \"auto\",\n width:\n isMultiDay && multiDayWidth ? `${multiDayWidth}%` : \"100%\",\n // Remove the transform that was causing the shift\n }}\n >\n <EventItem\n event={activeEvent}\n view={activeView}\n isDragging={true}\n showTime={activeView !== \"month\"}\n currentTime={currentTime || undefined}\n isFirstDay={dragHandlePosition?.data?.isFirstDay !== false}\n isLastDay={dragHandlePosition?.data?.isLastDay !== false}\n />\n </div>\n )}\n </DragOverlay>\n </CalendarDndContext.Provider>\n </DndContext>\n )\n}\n",
40
+ "type": "registry:component"
41
+ },
42
+ {
43
+ "path": "registry/default/components/event-calendar/constants.ts",
44
+ "content": "// Height of calendar events in pixels - used in month, week and day views\nexport const EventHeight = 24\n\n// Vertical gap between events in pixels - controls spacing in month view\nexport const EventGap = 4\n\n// Height of hour cells in week and day views - controls the scale of time display\nexport const WeekCellsHeight = 64\n\n// Number of days to show in the agenda view\nexport const AgendaDaysToShow = 30\n\n// Start and end hours for the week and day views\nexport const StartHour = 0\nexport const EndHour = 24\n\n// Default start and end times\nexport const DefaultStartHour = 9 // 9 AM\nexport const DefaultEndHour = 10 // 10 AM\n",
45
+ "type": "registry:component"
46
+ },
47
+ {
48
+ "path": "registry/default/components/event-calendar/day-view.tsx",
49
+ "content": "\"use client\"\n\nimport React, { useMemo } from \"react\"\nimport {\n addHours,\n areIntervalsOverlapping,\n differenceInMinutes,\n eachHourOfInterval,\n format,\n getHours,\n getMinutes,\n isSameDay,\n startOfDay,\n} from \"date-fns\"\n\nimport {\n DraggableEvent,\n DroppableCell,\n EventItem,\n isMultiDayEvent,\n useCurrentTimeIndicator,\n WeekCellsHeight,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport {\n EndHour,\n StartHour,\n} from \"@/registry/building-blocks/default/components/event-calendar/constants\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\n\ninterface DayViewProps {\n currentDate: Date\n events: CalendarEvent[]\n onEventSelect: (event: CalendarEvent) => void\n onEventCreate: (startTime: Date) => void\n}\n\ninterface PositionedEvent {\n event: CalendarEvent\n top: number\n height: number\n left: number\n width: number\n zIndex: number\n}\n\nexport function DayView({\n currentDate,\n events,\n onEventSelect,\n onEventCreate,\n}: DayViewProps) {\n const hours = useMemo(() => {\n const dayStart = startOfDay(currentDate)\n return eachHourOfInterval({\n start: addHours(dayStart, StartHour),\n end: addHours(dayStart, EndHour - 1),\n })\n }, [currentDate])\n\n const dayEvents = useMemo(() => {\n return events\n .filter((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return (\n isSameDay(currentDate, eventStart) ||\n isSameDay(currentDate, eventEnd) ||\n (currentDate > eventStart && currentDate < eventEnd)\n )\n })\n .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())\n }, [currentDate, events])\n\n // Filter all-day events\n const allDayEvents = useMemo(() => {\n return dayEvents.filter((event) => {\n // Include explicitly marked all-day events or multi-day events\n return event.allDay || isMultiDayEvent(event)\n })\n }, [dayEvents])\n\n // Get only single-day time-based events\n const timeEvents = useMemo(() => {\n return dayEvents.filter((event) => {\n // Exclude all-day events and multi-day events\n return !event.allDay && !isMultiDayEvent(event)\n })\n }, [dayEvents])\n\n // Process events to calculate positions\n const positionedEvents = useMemo(() => {\n const result: PositionedEvent[] = []\n const dayStart = startOfDay(currentDate)\n\n // Sort events by start time and duration\n const sortedEvents = [...timeEvents].sort((a, b) => {\n const aStart = new Date(a.start)\n const bStart = new Date(b.start)\n const aEnd = new Date(a.end)\n const bEnd = new Date(b.end)\n\n // First sort by start time\n if (aStart < bStart) return -1\n if (aStart > bStart) return 1\n\n // If start times are equal, sort by duration (longer events first)\n const aDuration = differenceInMinutes(aEnd, aStart)\n const bDuration = differenceInMinutes(bEnd, bStart)\n return bDuration - aDuration\n })\n\n // Track columns for overlapping events\n const columns: { event: CalendarEvent; end: Date }[][] = []\n\n sortedEvents.forEach((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n\n // Adjust start and end times if they're outside this day\n const adjustedStart = isSameDay(currentDate, eventStart)\n ? eventStart\n : dayStart\n const adjustedEnd = isSameDay(currentDate, eventEnd)\n ? eventEnd\n : addHours(dayStart, 24)\n\n // Calculate top position and height\n const startHour = getHours(adjustedStart) + getMinutes(adjustedStart) / 60\n const endHour = getHours(adjustedEnd) + getMinutes(adjustedEnd) / 60\n const top = (startHour - StartHour) * WeekCellsHeight\n const height = (endHour - startHour) * WeekCellsHeight\n\n // Find a column for this event\n let columnIndex = 0\n let placed = false\n\n while (!placed) {\n const col = columns[columnIndex] || []\n if (col.length === 0) {\n columns[columnIndex] = col\n placed = true\n } else {\n const overlaps = col.some((c) =>\n areIntervalsOverlapping(\n { start: adjustedStart, end: adjustedEnd },\n { start: new Date(c.event.start), end: new Date(c.event.end) }\n )\n )\n if (!overlaps) {\n placed = true\n } else {\n columnIndex++\n }\n }\n }\n\n // Ensure column is initialized before pushing\n const currentColumn = columns[columnIndex] || []\n columns[columnIndex] = currentColumn\n currentColumn.push({ event, end: adjustedEnd })\n\n // First column takes full width, others are indented by 10% and take 90% width\n const width = columnIndex === 0 ? 1 : 0.9\n const left = columnIndex === 0 ? 0 : columnIndex * 0.1\n\n result.push({\n event,\n top,\n height,\n left,\n width,\n zIndex: 10 + columnIndex, // Higher columns get higher z-index\n })\n })\n\n return result\n }, [currentDate, timeEvents])\n\n const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n e.stopPropagation()\n onEventSelect(event)\n }\n\n const showAllDaySection = allDayEvents.length > 0\n const { currentTimePosition, currentTimeVisible } = useCurrentTimeIndicator(\n currentDate,\n \"day\"\n )\n\n return (\n <div data-slot=\"day-view\" className=\"contents\">\n {showAllDaySection && (\n <div className=\"border-t border-border/70 bg-muted/50\">\n <div className=\"grid grid-cols-[3rem_1fr] sm:grid-cols-[4rem_1fr]\">\n <div className=\"relative\">\n <span className=\"absolute bottom-0 left-0 h-6 w-16 max-w-full pe-2 text-right text-[10px] text-muted-foreground/70 sm:pe-4 sm:text-xs\">\n All day\n </span>\n </div>\n <div className=\"relative border-r border-border/70 p-1 last:border-r-0\">\n {allDayEvents.map((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isFirstDay = isSameDay(currentDate, eventStart)\n const isLastDay = isSameDay(currentDate, eventEnd)\n\n return (\n <EventItem\n key={`spanning-${event.id}`}\n onClick={(e) => handleEventClick(event, e)}\n event={event}\n view=\"month\"\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n >\n {/* Always show the title in day view for better usability */}\n <div>{event.title}</div>\n </EventItem>\n )\n })}\n </div>\n </div>\n </div>\n )}\n\n <div className=\"grid flex-1 grid-cols-[3rem_1fr] overflow-hidden border-t border-border/70 sm:grid-cols-[4rem_1fr]\">\n <div>\n {hours.map((hour, index) => (\n <div\n key={hour.toString()}\n className=\"relative h-[var(--week-cells-height)] border-b border-border/70 last:border-b-0\"\n >\n {index > 0 && (\n <span className=\"absolute -top-3 left-0 flex h-6 w-16 max-w-full items-center justify-end bg-background pe-2 text-[10px] text-muted-foreground/70 sm:pe-4 sm:text-xs\">\n {format(hour, \"h a\")}\n </span>\n )}\n </div>\n ))}\n </div>\n\n <div className=\"relative\">\n {/* Positioned events */}\n {positionedEvents.map((positionedEvent) => (\n <div\n key={positionedEvent.event.id}\n className=\"absolute z-10 px-0.5\"\n style={{\n top: `${positionedEvent.top}px`,\n height: `${positionedEvent.height}px`,\n left: `${positionedEvent.left * 100}%`,\n width: `${positionedEvent.width * 100}%`,\n zIndex: positionedEvent.zIndex,\n }}\n >\n <div className=\"size-full\">\n <DraggableEvent\n event={positionedEvent.event}\n view=\"day\"\n onClick={(e) => handleEventClick(positionedEvent.event, e)}\n showTime\n height={positionedEvent.height}\n />\n </div>\n </div>\n ))}\n\n {/* Current time indicator */}\n {currentTimeVisible && (\n <div\n className=\"pointer-events-none absolute right-0 left-0 z-20\"\n style={{ top: `${currentTimePosition}%` }}\n >\n <div className=\"relative flex items-center\">\n <div className=\"absolute -left-1 h-2 w-2 rounded-full bg-primary\"></div>\n <div className=\"h-[2px] w-full bg-primary\"></div>\n </div>\n </div>\n )}\n\n {/* Time grid */}\n {hours.map((hour) => {\n const hourValue = getHours(hour)\n return (\n <div\n key={hour.toString()}\n className=\"relative h-[var(--week-cells-height)] border-b border-border/70 last:border-b-0\"\n >\n {/* Quarter-hour intervals */}\n {[0, 1, 2, 3].map((quarter) => {\n const quarterHourTime = hourValue + quarter * 0.25\n return (\n <DroppableCell\n key={`${hour.toString()}-${quarter}`}\n id={`day-cell-${currentDate.toISOString()}-${quarterHourTime}`}\n date={currentDate}\n time={quarterHourTime}\n className={cn(\n \"absolute h-[calc(var(--week-cells-height)/4)] w-full\",\n quarter === 0 && \"top-0\",\n quarter === 1 &&\n \"top-[calc(var(--week-cells-height)/4)]\",\n quarter === 2 &&\n \"top-[calc(var(--week-cells-height)/4*2)]\",\n quarter === 3 &&\n \"top-[calc(var(--week-cells-height)/4*3)]\"\n )}\n onClick={() => {\n const startTime = new Date(currentDate)\n startTime.setHours(hourValue)\n startTime.setMinutes(quarter * 15)\n onEventCreate(startTime)\n }}\n />\n )\n })}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n",
50
+ "type": "registry:component"
51
+ },
52
+ {
53
+ "path": "registry/default/components/event-calendar/draggable-event.tsx",
54
+ "content": "\"use client\"\n\nimport { useRef, useState } from \"react\"\nimport { useDraggable } from \"@dnd-kit/core\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { differenceInDays } from \"date-fns\"\n\nimport {\n CalendarEvent,\n EventItem,\n useCalendarDnd,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\n\ninterface DraggableEventProps {\n event: CalendarEvent\n view: \"month\" | \"week\" | \"day\"\n showTime?: boolean\n onClick?: (e: React.MouseEvent) => void\n height?: number\n isMultiDay?: boolean\n multiDayWidth?: number\n isFirstDay?: boolean\n isLastDay?: boolean\n \"aria-hidden\"?: boolean | \"true\" | \"false\"\n}\n\nexport function DraggableEvent({\n event,\n view,\n showTime,\n onClick,\n height,\n isMultiDay,\n multiDayWidth,\n isFirstDay = true,\n isLastDay = true,\n \"aria-hidden\": ariaHidden,\n}: DraggableEventProps) {\n const { activeId } = useCalendarDnd()\n const elementRef = useRef<HTMLDivElement>(null)\n const [dragHandlePosition, setDragHandlePosition] = useState<{\n x: number\n y: number\n } | null>(null)\n\n // Check if this is a multi-day event\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isMultiDayEvent =\n isMultiDay || event.allDay || differenceInDays(eventEnd, eventStart) >= 1\n\n const { attributes, listeners, setNodeRef, transform, isDragging } =\n useDraggable({\n id: `${event.id}-${view}`,\n data: {\n event,\n view,\n height: height || elementRef.current?.offsetHeight || null,\n isMultiDay: isMultiDayEvent,\n multiDayWidth: multiDayWidth,\n dragHandlePosition,\n isFirstDay,\n isLastDay,\n },\n })\n\n // Handle mouse down to track where on the event the user clicked\n const handleMouseDown = (e: React.MouseEvent) => {\n if (elementRef.current) {\n const rect = elementRef.current.getBoundingClientRect()\n setDragHandlePosition({\n x: e.clientX - rect.left,\n y: e.clientY - rect.top,\n })\n }\n }\n\n // Don't render if this event is being dragged\n if (isDragging || activeId === `${event.id}-${view}`) {\n return (\n <div\n ref={setNodeRef}\n className=\"opacity-0\"\n style={{ height: height || \"auto\" }}\n />\n )\n }\n\n const style = transform\n ? {\n transform: CSS.Translate.toString(transform),\n height: height || \"auto\",\n width:\n isMultiDayEvent && multiDayWidth ? `${multiDayWidth}%` : undefined,\n }\n : {\n height: height || \"auto\",\n width:\n isMultiDayEvent && multiDayWidth ? `${multiDayWidth}%` : undefined,\n }\n\n // Handle touch start to track where on the event the user touched\n const handleTouchStart = (e: React.TouchEvent) => {\n if (elementRef.current) {\n const rect = elementRef.current.getBoundingClientRect()\n const touch = e.touches[0]\n if (touch) {\n setDragHandlePosition({\n x: touch.clientX - rect.left,\n y: touch.clientY - rect.top,\n })\n }\n }\n }\n\n return (\n <div\n ref={(node) => {\n setNodeRef(node)\n if (elementRef) elementRef.current = node\n }}\n style={style}\n className=\"touch-none\"\n >\n <EventItem\n event={event}\n view={view}\n showTime={showTime}\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n isDragging={isDragging}\n onClick={onClick}\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n dndListeners={listeners}\n dndAttributes={attributes}\n aria-hidden={ariaHidden}\n />\n </div>\n )\n}\n",
55
+ "type": "registry:component"
56
+ },
57
+ {
58
+ "path": "registry/default/components/event-calendar/droppable-cell.tsx",
59
+ "content": "\"use client\"\n\nimport { useDroppable } from \"@dnd-kit/core\"\n\nimport { useCalendarDnd } from \"@/registry/building-blocks/default/components/event-calendar\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\n\ninterface DroppableCellProps {\n id: string\n date: Date\n time?: number // For week/day views, represents hours (e.g., 9.25 for 9:15)\n children?: React.ReactNode\n className?: string\n onClick?: () => void\n}\n\nexport function DroppableCell({\n id,\n date,\n time,\n children,\n className,\n onClick,\n}: DroppableCellProps) {\n const { activeEvent } = useCalendarDnd()\n\n const { setNodeRef, isOver } = useDroppable({\n id,\n data: {\n date,\n time,\n },\n })\n\n // Format time for display in tooltip (only for debugging)\n const formattedTime =\n time !== undefined\n ? `${Math.floor(time)}:${Math.round((time - Math.floor(time)) * 60)\n .toString()\n .padStart(2, \"0\")}`\n : null\n\n return (\n <div\n ref={setNodeRef}\n onClick={onClick}\n className={cn(\n \"flex h-full flex-col overflow-hidden px-0.5 py-1 data-dragging:bg-accent sm:px-1\",\n className\n )}\n title={formattedTime ? `${formattedTime}` : undefined}\n data-dragging={isOver && activeEvent ? true : undefined}\n >\n {children}\n </div>\n )\n}\n",
60
+ "type": "registry:component"
61
+ },
62
+ {
63
+ "path": "registry/default/components/event-calendar/event-dialog.tsx",
64
+ "content": "\"use client\"\n\nimport { useEffect, useMemo, useState } from \"react\"\nimport { RiCalendarLine, RiDeleteBinLine } from \"@remixicon/react\"\nimport { format, isBefore } from \"date-fns\"\n\nimport type {\n CalendarEvent,\n EventColor,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport {\n DefaultEndHour,\n DefaultStartHour,\n EndHour,\n StartHour,\n} from \"@/registry/building-blocks/default/components/event-calendar/constants\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\nimport { Button } from \"@/registry/building-blocks/default/ui/button\"\nimport { Calendar } from \"@/registry/building-blocks/default/ui/calendar\"\nimport { Checkbox } from \"@/registry/building-blocks/default/ui/checkbox\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/registry/building-blocks/default/ui/dialog\"\nimport { Input } from \"@/registry/building-blocks/default/ui/input\"\nimport { Label } from \"@/registry/building-blocks/default/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/building-blocks/default/ui/popover\"\nimport { RadioGroup, RadioGroupItem } from \"@/registry/building-blocks/default/ui/radio-group\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/building-blocks/default/ui/select\"\nimport { Textarea } from \"@/registry/building-blocks/default/ui/textarea\"\n\ninterface EventDialogProps {\n event: CalendarEvent | null\n isOpen: boolean\n onClose: () => void\n onSave: (event: CalendarEvent) => void\n onDelete: (eventId: string) => void\n}\n\nexport function EventDialog({\n event,\n isOpen,\n onClose,\n onSave,\n onDelete,\n}: EventDialogProps) {\n const [title, setTitle] = useState(\"\")\n const [description, setDescription] = useState(\"\")\n const [startDate, setStartDate] = useState<Date>(new Date())\n const [endDate, setEndDate] = useState<Date>(new Date())\n const [startTime, setStartTime] = useState(`${DefaultStartHour}:00`)\n const [endTime, setEndTime] = useState(`${DefaultEndHour}:00`)\n const [allDay, setAllDay] = useState(false)\n const [location, setLocation] = useState(\"\")\n const [color, setColor] = useState<EventColor>(\"sky\")\n const [error, setError] = useState<string | null>(null)\n const [startDateOpen, setStartDateOpen] = useState(false)\n const [endDateOpen, setEndDateOpen] = useState(false)\n\n // Debug log to check what event is being passed\n useEffect(() => {\n console.log(\"EventDialog received event:\", event)\n }, [event])\n\n useEffect(() => {\n if (event) {\n setTitle(event.title || \"\")\n setDescription(event.description || \"\")\n\n const start = new Date(event.start)\n const end = new Date(event.end)\n\n setStartDate(start)\n setEndDate(end)\n setStartTime(formatTimeForInput(start))\n setEndTime(formatTimeForInput(end))\n setAllDay(event.allDay || false)\n setLocation(event.location || \"\")\n setColor((event.color as EventColor) || \"sky\")\n setError(null) // Reset error when opening dialog\n } else {\n resetForm()\n }\n }, [event])\n\n const resetForm = () => {\n setTitle(\"\")\n setDescription(\"\")\n setStartDate(new Date())\n setEndDate(new Date())\n setStartTime(`${DefaultStartHour}:00`)\n setEndTime(`${DefaultEndHour}:00`)\n setAllDay(false)\n setLocation(\"\")\n setColor(\"sky\")\n setError(null)\n }\n\n const formatTimeForInput = (date: Date) => {\n const hours = date.getHours().toString().padStart(2, \"0\")\n const minutes = Math.floor(date.getMinutes() / 15) * 15\n return `${hours}:${minutes.toString().padStart(2, \"0\")}`\n }\n\n // Memoize time options so they're only calculated once\n const timeOptions = useMemo(() => {\n const options = []\n for (let hour = StartHour; hour <= EndHour; hour++) {\n for (let minute = 0; minute < 60; minute += 15) {\n const formattedHour = hour.toString().padStart(2, \"0\")\n const formattedMinute = minute.toString().padStart(2, \"0\")\n const value = `${formattedHour}:${formattedMinute}`\n // Use a fixed date to avoid unnecessary date object creations\n const date = new Date(2000, 0, 1, hour, minute)\n const label = format(date, \"h:mm a\")\n options.push({ value, label })\n }\n }\n return options\n }, []) // Empty dependency array ensures this only runs once\n\n const handleSave = () => {\n const start = new Date(startDate)\n const end = new Date(endDate)\n\n if (!allDay) {\n const [startHours = 0, startMinutes = 0] = startTime\n .split(\":\")\n .map(Number)\n const [endHours = 0, endMinutes = 0] = endTime.split(\":\").map(Number)\n\n if (\n startHours < StartHour ||\n startHours > EndHour ||\n endHours < StartHour ||\n endHours > EndHour\n ) {\n setError(\n `Selected time must be between ${StartHour}:00 and ${EndHour}:00`\n )\n return\n }\n\n start.setHours(startHours, startMinutes, 0)\n end.setHours(endHours, endMinutes, 0)\n } else {\n start.setHours(0, 0, 0, 0)\n end.setHours(23, 59, 59, 999)\n }\n\n // Validate that end date is not before start date\n if (isBefore(end, start)) {\n setError(\"End date cannot be before start date\")\n return\n }\n\n // Use generic title if empty\n const eventTitle = title.trim() ? title : \"(no title)\"\n\n onSave({\n id: event?.id || \"\",\n title: eventTitle,\n description,\n start,\n end,\n allDay,\n location,\n color,\n })\n }\n\n const handleDelete = () => {\n if (event?.id) {\n onDelete(event.id)\n }\n }\n\n // Updated color options to match types.ts\n const colorOptions: Array<{\n value: EventColor\n label: string\n bgClass: string\n borderClass: string\n }> = [\n {\n value: \"sky\",\n label: \"Sky\",\n bgClass: \"bg-sky-400 data-[state=checked]:bg-sky-400\",\n borderClass: \"border-sky-400 data-[state=checked]:border-sky-400\",\n },\n {\n value: \"amber\",\n label: \"Amber\",\n bgClass: \"bg-amber-400 data-[state=checked]:bg-amber-400\",\n borderClass: \"border-amber-400 data-[state=checked]:border-amber-400\",\n },\n {\n value: \"violet\",\n label: \"Violet\",\n bgClass: \"bg-violet-400 data-[state=checked]:bg-violet-400\",\n borderClass: \"border-violet-400 data-[state=checked]:border-violet-400\",\n },\n {\n value: \"rose\",\n label: \"Rose\",\n bgClass: \"bg-rose-400 data-[state=checked]:bg-rose-400\",\n borderClass: \"border-rose-400 data-[state=checked]:border-rose-400\",\n },\n {\n value: \"emerald\",\n label: \"Emerald\",\n bgClass: \"bg-emerald-400 data-[state=checked]:bg-emerald-400\",\n borderClass: \"border-emerald-400 data-[state=checked]:border-emerald-400\",\n },\n {\n value: \"orange\",\n label: \"Orange\",\n bgClass: \"bg-orange-400 data-[state=checked]:bg-orange-400\",\n borderClass: \"border-orange-400 data-[state=checked]:border-orange-400\",\n },\n ]\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className=\"sm:max-w-[425px]\">\n <DialogHeader>\n <DialogTitle>{event?.id ? \"Edit Event\" : \"Create Event\"}</DialogTitle>\n <DialogDescription className=\"sr-only\">\n {event?.id\n ? \"Edit the details of this event\"\n : \"Add a new event to your calendar\"}\n </DialogDescription>\n </DialogHeader>\n {error && (\n <div className=\"rounded-md bg-destructive/15 px-3 py-2 text-sm text-destructive\">\n {error}\n </div>\n )}\n <div className=\"grid gap-4 py-4\">\n <div className=\"*:not-first:mt-1.5\">\n <Label htmlFor=\"title\">Title</Label>\n <Input\n id=\"title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n </div>\n\n <div className=\"*:not-first:mt-1.5\">\n <Label htmlFor=\"description\">Description</Label>\n <Textarea\n id=\"description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n />\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1 *:not-first:mt-1.5\">\n <Label htmlFor=\"start-date\">Start Date</Label>\n <Popover open={startDateOpen} onOpenChange={setStartDateOpen}>\n <PopoverTrigger asChild>\n <Button\n id=\"start-date\"\n variant={\"outline\"}\n className={cn(\n \"group w-full justify-between border-input bg-background px-3 font-normal outline-offset-0 outline-none hover:bg-background focus-visible:outline-[3px]\",\n !startDate && \"text-muted-foreground\"\n )}\n >\n <span\n className={cn(\n \"truncate\",\n !startDate && \"text-muted-foreground\"\n )}\n >\n {startDate ? format(startDate, \"PPP\") : \"Pick a date\"}\n </span>\n <RiCalendarLine\n size={16}\n className=\"shrink-0 text-muted-foreground/80\"\n aria-hidden=\"true\"\n />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-2\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={startDate}\n defaultMonth={startDate}\n onSelect={(date) => {\n if (date) {\n setStartDate(date)\n // If end date is before the new start date, update it to match the start date\n if (isBefore(endDate, date)) {\n setEndDate(date)\n }\n setError(null)\n setStartDateOpen(false)\n }\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n\n {!allDay && (\n <div className=\"min-w-28 *:not-first:mt-1.5\">\n <Label htmlFor=\"start-time\">Start Time</Label>\n <Select value={startTime} onValueChange={setStartTime}>\n <SelectTrigger id=\"start-time\">\n <SelectValue placeholder=\"Select time\" />\n </SelectTrigger>\n <SelectContent>\n {timeOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1 *:not-first:mt-1.5\">\n <Label htmlFor=\"end-date\">End Date</Label>\n <Popover open={endDateOpen} onOpenChange={setEndDateOpen}>\n <PopoverTrigger asChild>\n <Button\n id=\"end-date\"\n variant={\"outline\"}\n className={cn(\n \"group w-full justify-between border-input bg-background px-3 font-normal outline-offset-0 outline-none hover:bg-background focus-visible:outline-[3px]\",\n !endDate && \"text-muted-foreground\"\n )}\n >\n <span\n className={cn(\n \"truncate\",\n !endDate && \"text-muted-foreground\"\n )}\n >\n {endDate ? format(endDate, \"PPP\") : \"Pick a date\"}\n </span>\n <RiCalendarLine\n size={16}\n className=\"shrink-0 text-muted-foreground/80\"\n aria-hidden=\"true\"\n />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-2\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={endDate}\n defaultMonth={endDate}\n disabled={{ before: startDate }}\n onSelect={(date) => {\n if (date) {\n setEndDate(date)\n setError(null)\n setEndDateOpen(false)\n }\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n\n {!allDay && (\n <div className=\"min-w-28 *:not-first:mt-1.5\">\n <Label htmlFor=\"end-time\">End Time</Label>\n <Select value={endTime} onValueChange={setEndTime}>\n <SelectTrigger id=\"end-time\">\n <SelectValue placeholder=\"Select time\" />\n </SelectTrigger>\n <SelectContent>\n {timeOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"all-day\"\n checked={allDay}\n onCheckedChange={(checked) => setAllDay(checked === true)}\n />\n <Label htmlFor=\"all-day\">All day</Label>\n </div>\n\n <div className=\"*:not-first:mt-1.5\">\n <Label htmlFor=\"location\">Location</Label>\n <Input\n id=\"location\"\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n />\n </div>\n <fieldset className=\"space-y-4\">\n <legend className=\"text-sm leading-none font-medium text-foreground\">\n Etiquette\n </legend>\n <RadioGroup\n className=\"flex gap-1.5\"\n defaultValue={colorOptions[0]?.value}\n value={color}\n onValueChange={(value: EventColor) => setColor(value)}\n >\n {colorOptions.map((colorOption) => (\n <RadioGroupItem\n key={colorOption.value}\n id={`color-${colorOption.value}`}\n value={colorOption.value}\n aria-label={colorOption.label}\n className={cn(\n \"size-6 shadow-none\",\n colorOption.bgClass,\n colorOption.borderClass\n )}\n />\n ))}\n </RadioGroup>\n </fieldset>\n </div>\n <DialogFooter className=\"flex-row sm:justify-between\">\n {event?.id && (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleDelete}\n aria-label=\"Delete event\"\n >\n <RiDeleteBinLine size={16} aria-hidden=\"true\" />\n </Button>\n )}\n <div className=\"flex flex-1 justify-end gap-2\">\n <Button variant=\"outline\" onClick={onClose}>\n Cancel\n </Button>\n <Button onClick={handleSave}>Save</Button>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n",
65
+ "type": "registry:component"
66
+ },
67
+ {
68
+ "path": "registry/default/components/event-calendar/event-item.tsx",
69
+ "content": "\"use client\"\n\nimport { useMemo } from \"react\"\nimport type { DraggableAttributes } from \"@dnd-kit/core\"\nimport type { SyntheticListenerMap } from \"@dnd-kit/core/dist/hooks/utilities\"\nimport { differenceInMinutes, format, getMinutes, isPast } from \"date-fns\"\n\nimport {\n getBorderRadiusClasses,\n getEventColorClasses,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\n\n// Using date-fns format with custom formatting:\n// 'h' - hours (1-12)\n// 'a' - am/pm\n// ':mm' - minutes with leading zero (only if the token 'mm' is present)\nconst formatTimeWithOptionalMinutes = (date: Date) => {\n return format(date, getMinutes(date) === 0 ? \"ha\" : \"h:mma\").toLowerCase()\n}\n\ninterface EventWrapperProps {\n event: CalendarEvent\n isFirstDay?: boolean\n isLastDay?: boolean\n isDragging?: boolean\n onClick?: (e: React.MouseEvent) => void\n className?: string\n children: React.ReactNode\n currentTime?: Date\n dndListeners?: SyntheticListenerMap\n dndAttributes?: DraggableAttributes\n onMouseDown?: (e: React.MouseEvent) => void\n onTouchStart?: (e: React.TouchEvent) => void\n}\n\n// Shared wrapper component for event styling\nfunction EventWrapper({\n event,\n isFirstDay = true,\n isLastDay = true,\n isDragging,\n onClick,\n className,\n children,\n currentTime,\n dndListeners,\n dndAttributes,\n onMouseDown,\n onTouchStart,\n}: EventWrapperProps) {\n // Always use the currentTime (if provided) to determine if the event is in the past\n const displayEnd = currentTime\n ? new Date(\n new Date(currentTime).getTime() +\n (new Date(event.end).getTime() - new Date(event.start).getTime())\n )\n : new Date(event.end)\n\n const isEventInPast = isPast(displayEnd)\n\n return (\n <button\n className={cn(\n \"flex size-full overflow-hidden px-1 text-left font-medium backdrop-blur-md transition outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-dragging:cursor-grabbing data-dragging:shadow-lg data-past-event:line-through sm:px-2\",\n getEventColorClasses(event.color),\n getBorderRadiusClasses(isFirstDay, isLastDay),\n className\n )}\n data-dragging={isDragging || undefined}\n data-past-event={isEventInPast || undefined}\n onClick={onClick}\n onMouseDown={onMouseDown}\n onTouchStart={onTouchStart}\n {...dndListeners}\n {...dndAttributes}\n >\n {children}\n </button>\n )\n}\n\ninterface EventItemProps {\n event: CalendarEvent\n view: \"month\" | \"week\" | \"day\" | \"agenda\"\n isDragging?: boolean\n onClick?: (e: React.MouseEvent) => void\n showTime?: boolean\n currentTime?: Date // For updating time during drag\n isFirstDay?: boolean\n isLastDay?: boolean\n children?: React.ReactNode\n className?: string\n dndListeners?: SyntheticListenerMap\n dndAttributes?: DraggableAttributes\n onMouseDown?: (e: React.MouseEvent) => void\n onTouchStart?: (e: React.TouchEvent) => void\n}\n\nexport function EventItem({\n event,\n view,\n isDragging,\n onClick,\n showTime,\n currentTime,\n isFirstDay = true,\n isLastDay = true,\n children,\n className,\n dndListeners,\n dndAttributes,\n onMouseDown,\n onTouchStart,\n}: EventItemProps) {\n const eventColor = event.color\n\n // Use the provided currentTime (for dragging) or the event's actual time\n const displayStart = useMemo(() => {\n return currentTime || new Date(event.start)\n }, [currentTime, event.start])\n\n const displayEnd = useMemo(() => {\n return currentTime\n ? new Date(\n new Date(currentTime).getTime() +\n (new Date(event.end).getTime() - new Date(event.start).getTime())\n )\n : new Date(event.end)\n }, [currentTime, event.start, event.end])\n\n // Calculate event duration in minutes\n const durationMinutes = useMemo(() => {\n return differenceInMinutes(displayEnd, displayStart)\n }, [displayStart, displayEnd])\n\n const getEventTime = () => {\n if (event.allDay) return \"All day\"\n\n // For short events (less than 45 minutes), only show start time\n if (durationMinutes < 45) {\n return formatTimeWithOptionalMinutes(displayStart)\n }\n\n // For longer events, show both start and end time\n return `${formatTimeWithOptionalMinutes(displayStart)} - ${formatTimeWithOptionalMinutes(displayEnd)}`\n }\n\n if (view === \"month\") {\n return (\n <EventWrapper\n event={event}\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n isDragging={isDragging}\n onClick={onClick}\n className={cn(\n \"mt-[var(--event-gap)] h-[var(--event-height)] items-center text-[10px] sm:text-xs\",\n className\n )}\n currentTime={currentTime}\n dndListeners={dndListeners}\n dndAttributes={dndAttributes}\n onMouseDown={onMouseDown}\n onTouchStart={onTouchStart}\n >\n {children || (\n <span className=\"truncate\">\n {!event.allDay && (\n <span className=\"truncate font-normal opacity-70 sm:text-[11px]\">\n {formatTimeWithOptionalMinutes(displayStart)}{\" \"}\n </span>\n )}\n {event.title}\n </span>\n )}\n </EventWrapper>\n )\n }\n\n if (view === \"week\" || view === \"day\") {\n return (\n <EventWrapper\n event={event}\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n isDragging={isDragging}\n onClick={onClick}\n className={cn(\n \"py-1\",\n durationMinutes < 45 ? \"items-center\" : \"flex-col\",\n view === \"week\" ? \"text-[10px] sm:text-xs\" : \"text-xs\",\n className\n )}\n currentTime={currentTime}\n dndListeners={dndListeners}\n dndAttributes={dndAttributes}\n onMouseDown={onMouseDown}\n onTouchStart={onTouchStart}\n >\n {durationMinutes < 45 ? (\n <div className=\"truncate\">\n {event.title}{\" \"}\n {showTime && (\n <span className=\"opacity-70\">\n {formatTimeWithOptionalMinutes(displayStart)}\n </span>\n )}\n </div>\n ) : (\n <>\n <div className=\"truncate font-medium\">{event.title}</div>\n {showTime && (\n <div className=\"truncate font-normal opacity-70 sm:text-[11px]\">\n {getEventTime()}\n </div>\n )}\n </>\n )}\n </EventWrapper>\n )\n }\n\n // Agenda view - kept separate since it's significantly different\n return (\n <button\n className={cn(\n \"flex w-full flex-col gap-1 rounded p-2 text-left transition outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-past-event:line-through data-past-event:opacity-90\",\n getEventColorClasses(eventColor),\n className\n )}\n data-past-event={isPast(new Date(event.end)) || undefined}\n onClick={onClick}\n onMouseDown={onMouseDown}\n onTouchStart={onTouchStart}\n {...dndListeners}\n {...dndAttributes}\n >\n <div className=\"text-sm font-medium\">{event.title}</div>\n <div className=\"text-xs opacity-70\">\n {event.allDay ? (\n <span>All day</span>\n ) : (\n <span className=\"uppercase\">\n {formatTimeWithOptionalMinutes(displayStart)} -{\" \"}\n {formatTimeWithOptionalMinutes(displayEnd)}\n </span>\n )}\n {event.location && (\n <>\n <span className=\"px-1 opacity-35\"> · </span>\n <span>{event.location}</span>\n </>\n )}\n </div>\n {event.description && (\n <div className=\"my-1 text-xs opacity-90\">{event.description}</div>\n )}\n </button>\n )\n}\n",
70
+ "type": "registry:component"
71
+ },
72
+ {
73
+ "path": "registry/default/components/event-calendar/events-popup.tsx",
74
+ "content": "\"use client\"\n\nimport { useEffect, useMemo, useRef } from \"react\"\nimport { format, isSameDay } from \"date-fns\"\nimport { XIcon } from \"lucide-react\"\n\nimport {\n EventItem,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\n\ninterface EventsPopupProps {\n date: Date\n events: CalendarEvent[]\n position: { top: number; left: number }\n onClose: () => void\n onEventSelect: (event: CalendarEvent) => void\n}\n\nexport function EventsPopup({\n date,\n events,\n position,\n onClose,\n onEventSelect,\n}: EventsPopupProps) {\n const popupRef = useRef<HTMLDivElement>(null)\n\n // Handle click outside to close popup\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (\n popupRef.current &&\n !popupRef.current.contains(event.target as Node)\n ) {\n onClose()\n }\n }\n\n document.addEventListener(\"mousedown\", handleClickOutside)\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside)\n }\n }, [onClose])\n\n // Handle escape key to close popup\n useEffect(() => {\n const handleEscKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n onClose()\n }\n }\n\n document.addEventListener(\"keydown\", handleEscKey)\n return () => {\n document.removeEventListener(\"keydown\", handleEscKey)\n }\n }, [onClose])\n\n const handleEventClick = (event: CalendarEvent) => {\n onEventSelect(event)\n onClose()\n }\n\n // Adjust position to ensure popup stays within viewport\n const adjustedPosition = useMemo(() => {\n const positionCopy = { ...position }\n\n // Check if we need to adjust the position to fit in the viewport\n if (popupRef.current) {\n const rect = popupRef.current.getBoundingClientRect()\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n // Adjust horizontally if needed\n if (positionCopy.left + rect.width > viewportWidth) {\n positionCopy.left = Math.max(0, viewportWidth - rect.width)\n }\n\n // Adjust vertically if needed\n if (positionCopy.top + rect.height > viewportHeight) {\n positionCopy.top = Math.max(0, viewportHeight - rect.height)\n }\n }\n\n return positionCopy\n }, [position])\n\n return (\n <div\n ref={popupRef}\n className=\"absolute z-50 max-h-96 w-80 overflow-auto rounded-md border bg-background shadow-lg\"\n style={{\n top: `${adjustedPosition.top}px`,\n left: `${adjustedPosition.left}px`,\n }}\n >\n <div className=\"sticky top-0 flex items-center justify-between border-b bg-background p-3\">\n <h3 className=\"font-medium\">{format(date, \"d MMMM yyyy\")}</h3>\n <button\n onClick={onClose}\n className=\"rounded-full p-1 hover:bg-muted\"\n aria-label=\"Close\"\n >\n <XIcon className=\"h-4 w-4\" />\n </button>\n </div>\n\n <div className=\"space-y-2 p-3\">\n {events.length === 0 ? (\n <div className=\"py-2 text-sm text-muted-foreground\">No events</div>\n ) : (\n events.map((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isFirstDay = isSameDay(date, eventStart)\n const isLastDay = isSameDay(date, eventEnd)\n\n return (\n <div\n key={event.id}\n className=\"cursor-pointer\"\n onClick={() => handleEventClick(event)}\n >\n <EventItem\n event={event}\n view=\"agenda\"\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n />\n </div>\n )\n })\n )}\n </div>\n </div>\n )\n}\n",
75
+ "type": "registry:component"
76
+ },
77
+ {
78
+ "path": "registry/default/components/event-calendar/event-calendar.tsx",
79
+ "content": "\"use client\"\n\nimport { useEffect, useMemo, useState } from \"react\"\nimport { RiCalendarCheckLine } from \"@remixicon/react\"\nimport {\n addDays,\n addMonths,\n addWeeks,\n endOfWeek,\n format,\n isSameMonth,\n startOfWeek,\n subMonths,\n subWeeks,\n} from \"date-fns\"\nimport {\n ChevronDownIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n PlusIcon,\n} from \"lucide-react\"\nimport { toast } from \"sonner\"\n\nimport {\n addHoursToDate,\n AgendaDaysToShow,\n AgendaView,\n CalendarDndProvider,\n CalendarEvent,\n CalendarView,\n DayView,\n EventDialog,\n EventGap,\n EventHeight,\n MonthView,\n WeekCellsHeight,\n WeekView,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\nimport { Button } from \"@/registry/building-blocks/default/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuShortcut,\n DropdownMenuTrigger,\n} from \"@/registry/building-blocks/default/ui/dropdown-menu\"\n\nexport interface EventCalendarProps {\n events?: CalendarEvent[]\n onEventAdd?: (event: CalendarEvent) => void\n onEventUpdate?: (event: CalendarEvent) => void\n onEventDelete?: (eventId: string) => void\n className?: string\n initialView?: CalendarView\n}\n\nexport function EventCalendar({\n events = [],\n onEventAdd,\n onEventUpdate,\n onEventDelete,\n className,\n initialView = \"month\",\n}: EventCalendarProps) {\n const [currentDate, setCurrentDate] = useState(new Date())\n const [view, setView] = useState<CalendarView>(initialView)\n const [isEventDialogOpen, setIsEventDialogOpen] = useState(false)\n const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)\n\n // Add keyboard shortcuts for view switching\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n // Skip if user is typing in an input, textarea or contentEditable element\n // or if the event dialog is open\n if (\n isEventDialogOpen ||\n e.target instanceof HTMLInputElement ||\n e.target instanceof HTMLTextAreaElement ||\n (e.target instanceof HTMLElement && e.target.isContentEditable)\n ) {\n return\n }\n\n switch (e.key.toLowerCase()) {\n case \"m\":\n setView(\"month\")\n break\n case \"w\":\n setView(\"week\")\n break\n case \"d\":\n setView(\"day\")\n break\n case \"a\":\n setView(\"agenda\")\n break\n }\n }\n\n window.addEventListener(\"keydown\", handleKeyDown)\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [isEventDialogOpen])\n\n const handlePrevious = () => {\n if (view === \"month\") {\n setCurrentDate(subMonths(currentDate, 1))\n } else if (view === \"week\") {\n setCurrentDate(subWeeks(currentDate, 1))\n } else if (view === \"day\") {\n setCurrentDate(addDays(currentDate, -1))\n } else if (view === \"agenda\") {\n // For agenda view, go back 30 days (a full month)\n setCurrentDate(addDays(currentDate, -AgendaDaysToShow))\n }\n }\n\n const handleNext = () => {\n if (view === \"month\") {\n setCurrentDate(addMonths(currentDate, 1))\n } else if (view === \"week\") {\n setCurrentDate(addWeeks(currentDate, 1))\n } else if (view === \"day\") {\n setCurrentDate(addDays(currentDate, 1))\n } else if (view === \"agenda\") {\n // For agenda view, go forward 30 days (a full month)\n setCurrentDate(addDays(currentDate, AgendaDaysToShow))\n }\n }\n\n const handleToday = () => {\n setCurrentDate(new Date())\n }\n\n const handleEventSelect = (event: CalendarEvent) => {\n console.log(\"Event selected:\", event) // Debug log\n setSelectedEvent(event)\n setIsEventDialogOpen(true)\n }\n\n const handleEventCreate = (startTime: Date) => {\n console.log(\"Creating new event at:\", startTime) // Debug log\n\n // Snap to 15-minute intervals\n const minutes = startTime.getMinutes()\n const remainder = minutes % 15\n if (remainder !== 0) {\n if (remainder < 7.5) {\n // Round down to nearest 15 min\n startTime.setMinutes(minutes - remainder)\n } else {\n // Round up to nearest 15 min\n startTime.setMinutes(minutes + (15 - remainder))\n }\n startTime.setSeconds(0)\n startTime.setMilliseconds(0)\n }\n\n const newEvent: CalendarEvent = {\n id: \"\",\n title: \"\",\n start: startTime,\n end: addHoursToDate(startTime, 1),\n allDay: false,\n }\n setSelectedEvent(newEvent)\n setIsEventDialogOpen(true)\n }\n\n const handleEventSave = (event: CalendarEvent) => {\n if (event.id) {\n onEventUpdate?.(event)\n // Show toast notification when an event is updated\n toast(`Event \"${event.title}\" updated`, {\n description: format(new Date(event.start), \"MMM d, yyyy\"),\n position: \"bottom-left\",\n })\n } else {\n onEventAdd?.({\n ...event,\n id: Math.random().toString(36).substring(2, 11),\n })\n // Show toast notification when an event is added\n toast(`Event \"${event.title}\" added`, {\n description: format(new Date(event.start), \"MMM d, yyyy\"),\n position: \"bottom-left\",\n })\n }\n setIsEventDialogOpen(false)\n setSelectedEvent(null)\n }\n\n const handleEventDelete = (eventId: string) => {\n const deletedEvent = events.find((e) => e.id === eventId)\n onEventDelete?.(eventId)\n setIsEventDialogOpen(false)\n setSelectedEvent(null)\n\n // Show toast notification when an event is deleted\n if (deletedEvent) {\n toast(`Event \"${deletedEvent.title}\" deleted`, {\n description: format(new Date(deletedEvent.start), \"MMM d, yyyy\"),\n position: \"bottom-left\",\n })\n }\n }\n\n const handleEventUpdate = (updatedEvent: CalendarEvent) => {\n onEventUpdate?.(updatedEvent)\n\n // Show toast notification when an event is updated via drag and drop\n toast(`Event \"${updatedEvent.title}\" moved`, {\n description: format(new Date(updatedEvent.start), \"MMM d, yyyy\"),\n position: \"bottom-left\",\n })\n }\n\n const viewTitle = useMemo(() => {\n if (view === \"month\") {\n return format(currentDate, \"MMMM yyyy\")\n } else if (view === \"week\") {\n const start = startOfWeek(currentDate, { weekStartsOn: 0 })\n const end = endOfWeek(currentDate, { weekStartsOn: 0 })\n if (isSameMonth(start, end)) {\n return format(start, \"MMMM yyyy\")\n } else {\n return `${format(start, \"MMM\")} - ${format(end, \"MMM yyyy\")}`\n }\n } else if (view === \"day\") {\n return (\n <>\n <span className=\"min-[480px]:hidden\" aria-hidden=\"true\">\n {format(currentDate, \"MMM d, yyyy\")}\n </span>\n <span className=\"max-[479px]:hidden min-md:hidden\" aria-hidden=\"true\">\n {format(currentDate, \"MMMM d, yyyy\")}\n </span>\n <span className=\"max-md:hidden\">\n {format(currentDate, \"EEE MMMM d, yyyy\")}\n </span>\n </>\n )\n } else if (view === \"agenda\") {\n // Show the month range for agenda view\n const start = currentDate\n const end = addDays(currentDate, AgendaDaysToShow - 1)\n\n if (isSameMonth(start, end)) {\n return format(start, \"MMMM yyyy\")\n } else {\n return `${format(start, \"MMM\")} - ${format(end, \"MMM yyyy\")}`\n }\n } else {\n return format(currentDate, \"MMMM yyyy\")\n }\n }, [currentDate, view])\n\n return (\n <div\n className=\"flex flex-col rounded-lg border has-data-[slot=month-view]:flex-1\"\n style={\n {\n \"--event-height\": `${EventHeight}px`,\n \"--event-gap\": `${EventGap}px`,\n \"--week-cells-height\": `${WeekCellsHeight}px`,\n } as React.CSSProperties\n }\n >\n <CalendarDndProvider onEventUpdate={handleEventUpdate}>\n <div\n className={cn(\n \"flex items-center justify-between p-2 sm:p-4\",\n className\n )}\n >\n <div className=\"flex items-center gap-1 sm:gap-4\">\n <Button\n variant=\"outline\"\n className=\"max-[479px]:aspect-square max-[479px]:p-0!\"\n onClick={handleToday}\n >\n <RiCalendarCheckLine\n className=\"min-[480px]:hidden\"\n size={16}\n aria-hidden=\"true\"\n />\n <span className=\"max-[479px]:sr-only\">Today</span>\n </Button>\n <div className=\"flex items-center sm:gap-2\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={handlePrevious}\n aria-label=\"Previous\"\n >\n <ChevronLeftIcon size={16} aria-hidden=\"true\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleNext}\n aria-label=\"Next\"\n >\n <ChevronRightIcon size={16} aria-hidden=\"true\" />\n </Button>\n </div>\n <h2 className=\"text-sm font-semibold sm:text-lg md:text-xl\">\n {viewTitle}\n </h2>\n </div>\n <div className=\"flex items-center gap-2\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" className=\"gap-1.5 max-[479px]:h-8\">\n <span>\n <span className=\"min-[480px]:hidden\" aria-hidden=\"true\">\n {view.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-[479px]:sr-only\">\n {view.charAt(0).toUpperCase() + view.slice(1)}\n </span>\n </span>\n <ChevronDownIcon\n className=\"-me-1 opacity-60\"\n size={16}\n aria-hidden=\"true\"\n />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-32\">\n <DropdownMenuItem onClick={() => setView(\"month\")}>\n Month <DropdownMenuShortcut>M</DropdownMenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setView(\"week\")}>\n Week <DropdownMenuShortcut>W</DropdownMenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setView(\"day\")}>\n Day <DropdownMenuShortcut>D</DropdownMenuShortcut>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setView(\"agenda\")}>\n Agenda <DropdownMenuShortcut>A</DropdownMenuShortcut>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n <Button\n className=\"max-[479px]:aspect-square max-[479px]:p-0!\"\n size=\"sm\"\n onClick={() => {\n setSelectedEvent(null) // Ensure we're creating a new event\n setIsEventDialogOpen(true)\n }}\n >\n <PlusIcon\n className=\"opacity-60 sm:-ms-1\"\n size={16}\n aria-hidden=\"true\"\n />\n <span className=\"max-sm:sr-only\">New event</span>\n </Button>\n </div>\n </div>\n\n <div className=\"flex flex-1 flex-col\">\n {view === \"month\" && (\n <MonthView\n currentDate={currentDate}\n events={events}\n onEventSelect={handleEventSelect}\n onEventCreate={handleEventCreate}\n />\n )}\n {view === \"week\" && (\n <WeekView\n currentDate={currentDate}\n events={events}\n onEventSelect={handleEventSelect}\n onEventCreate={handleEventCreate}\n />\n )}\n {view === \"day\" && (\n <DayView\n currentDate={currentDate}\n events={events}\n onEventSelect={handleEventSelect}\n onEventCreate={handleEventCreate}\n />\n )}\n {view === \"agenda\" && (\n <AgendaView\n currentDate={currentDate}\n events={events}\n onEventSelect={handleEventSelect}\n />\n )}\n </div>\n\n <EventDialog\n event={selectedEvent}\n isOpen={isEventDialogOpen}\n onClose={() => {\n setIsEventDialogOpen(false)\n setSelectedEvent(null)\n }}\n onSave={handleEventSave}\n onDelete={handleEventDelete}\n />\n </CalendarDndProvider>\n </div>\n )\n}\n",
80
+ "type": "registry:component"
81
+ },
82
+ {
83
+ "path": "registry/default/components/event-calendar/index.ts",
84
+ "content": "\"use client\"\n\n// Component exports\nexport { AgendaView } from \"./agenda-view\"\nexport { DayView } from \"./day-view\"\nexport { DraggableEvent } from \"./draggable-event\"\nexport { DroppableCell } from \"./droppable-cell\"\nexport { EventDialog } from \"./event-dialog\"\nexport { EventItem } from \"./event-item\"\nexport { EventsPopup } from \"./events-popup\"\nexport { EventCalendar } from \"./event-calendar\"\nexport { MonthView } from \"./month-view\"\nexport { WeekView } from \"./week-view\"\nexport { CalendarDndProvider, useCalendarDnd } from \"./calendar-dnd-context\"\n\n// Constants and utility exports\nexport * from \"./constants\"\nexport * from \"./utils\"\n\n// Hook exports\nexport * from \"./hooks/use-current-time-indicator\"\nexport * from \"./hooks/use-event-visibility\"\n\n// Type exports\nexport type { CalendarEvent, CalendarView, EventColor } from \"./types\"\n",
85
+ "type": "registry:component"
86
+ },
87
+ {
88
+ "path": "registry/default/components/event-calendar/month-view.tsx",
89
+ "content": "\"use client\"\n\nimport React, { useEffect, useMemo, useState } from \"react\"\nimport {\n addDays,\n eachDayOfInterval,\n endOfMonth,\n endOfWeek,\n format,\n isSameDay,\n isSameMonth,\n isToday,\n startOfMonth,\n startOfWeek,\n} from \"date-fns\"\n\nimport {\n DraggableEvent,\n DroppableCell,\n EventGap,\n EventHeight,\n EventItem,\n getAllEventsForDay,\n getEventsForDay,\n getSpanningEventsForDay,\n sortEvents,\n useEventVisibility,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport { DefaultStartHour } from \"@/registry/building-blocks/default/components/event-calendar/constants\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/building-blocks/default/ui/popover\"\n\ninterface MonthViewProps {\n currentDate: Date\n events: CalendarEvent[]\n onEventSelect: (event: CalendarEvent) => void\n onEventCreate: (startTime: Date) => void\n}\n\nexport function MonthView({\n currentDate,\n events,\n onEventSelect,\n onEventCreate,\n}: MonthViewProps) {\n const days = useMemo(() => {\n const monthStart = startOfMonth(currentDate)\n const monthEnd = endOfMonth(monthStart)\n const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 })\n const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 })\n\n return eachDayOfInterval({ start: calendarStart, end: calendarEnd })\n }, [currentDate])\n\n const weekdays = useMemo(() => {\n return Array.from({ length: 7 }).map((_, i) => {\n const date = addDays(startOfWeek(new Date()), i)\n return format(date, \"EEE\")\n })\n }, [])\n\n const weeks = useMemo(() => {\n const result = []\n let week = []\n\n for (let i = 0; i < days.length; i++) {\n week.push(days[i])\n if (week.length === 7 || i === days.length - 1) {\n result.push(week)\n week = []\n }\n }\n\n return result\n }, [days])\n\n const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n e.stopPropagation()\n onEventSelect(event)\n }\n\n const [isMounted, setIsMounted] = useState(false)\n const { contentRef, getVisibleEventCount } = useEventVisibility({\n eventHeight: EventHeight,\n eventGap: EventGap,\n })\n\n useEffect(() => {\n setIsMounted(true)\n }, [])\n\n return (\n <div data-slot=\"month-view\" className=\"contents\">\n <div className=\"grid grid-cols-7 border-b border-border/70\">\n {weekdays.map((day) => (\n <div\n key={day}\n className=\"py-2 text-center text-sm text-muted-foreground/70\"\n >\n {day}\n </div>\n ))}\n </div>\n <div className=\"grid flex-1 auto-rows-fr\">\n {weeks.map((week, weekIndex) => (\n <div\n key={`week-${weekIndex}`}\n className=\"grid grid-cols-7 [&:last-child>*]:border-b-0\"\n >\n {week.map((day, dayIndex) => {\n if (!day) return null // Skip if day is undefined\n\n const dayEvents = getEventsForDay(events, day)\n const spanningEvents = getSpanningEventsForDay(events, day)\n const isCurrentMonth = isSameMonth(day, currentDate)\n const cellId = `month-cell-${day.toISOString()}`\n const allDayEvents = [...spanningEvents, ...dayEvents]\n const allEvents = getAllEventsForDay(events, day)\n\n const isReferenceCell = weekIndex === 0 && dayIndex === 0\n const visibleCount = isMounted\n ? getVisibleEventCount(allDayEvents.length)\n : undefined\n const hasMore =\n visibleCount !== undefined && allDayEvents.length > visibleCount\n const remainingCount = hasMore\n ? allDayEvents.length - visibleCount\n : 0\n\n return (\n <div\n key={day.toString()}\n className=\"group border-r border-b border-border/70 last:border-r-0 data-outside-cell:bg-muted/25 data-outside-cell:text-muted-foreground/70\"\n data-today={isToday(day) || undefined}\n data-outside-cell={!isCurrentMonth || undefined}\n >\n <DroppableCell\n id={cellId}\n date={day}\n onClick={() => {\n const startTime = new Date(day)\n startTime.setHours(DefaultStartHour, 0, 0)\n onEventCreate(startTime)\n }}\n >\n <div className=\"mt-1 inline-flex size-6 items-center justify-center rounded-full text-sm group-data-today:bg-primary group-data-today:text-primary-foreground\">\n {format(day, \"d\")}\n </div>\n <div\n ref={isReferenceCell ? contentRef : null}\n className=\"min-h-[calc((var(--event-height)+var(--event-gap))*2)] sm:min-h-[calc((var(--event-height)+var(--event-gap))*3)] lg:min-h-[calc((var(--event-height)+var(--event-gap))*4)]\"\n >\n {sortEvents(allDayEvents).map((event, index) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isFirstDay = isSameDay(day, eventStart)\n const isLastDay = isSameDay(day, eventEnd)\n\n const isHidden =\n isMounted && visibleCount && index >= visibleCount\n\n if (!visibleCount) return null\n\n if (!isFirstDay) {\n return (\n <div\n key={`spanning-${event.id}-${day.toISOString().slice(0, 10)}`}\n className=\"aria-hidden:hidden\"\n aria-hidden={isHidden ? \"true\" : undefined}\n >\n <EventItem\n onClick={(e) => handleEventClick(event, e)}\n event={event}\n view=\"month\"\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n >\n <div className=\"invisible\" aria-hidden={true}>\n {!event.allDay && (\n <span>\n {format(\n new Date(event.start),\n \"h:mm\"\n )}{\" \"}\n </span>\n )}\n {event.title}\n </div>\n </EventItem>\n </div>\n )\n }\n\n return (\n <div\n key={event.id}\n className=\"aria-hidden:hidden\"\n aria-hidden={isHidden ? \"true\" : undefined}\n >\n <DraggableEvent\n event={event}\n view=\"month\"\n onClick={(e) => handleEventClick(event, e)}\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n />\n </div>\n )\n })}\n\n {hasMore && (\n <Popover modal>\n <PopoverTrigger asChild>\n <button\n className=\"mt-[var(--event-gap)] flex h-[var(--event-height)] w-full items-center overflow-hidden px-1 text-left text-[10px] text-muted-foreground backdrop-blur-md transition outline-none select-none hover:bg-muted/50 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 sm:px-2 sm:text-xs\"\n onClick={(e) => e.stopPropagation()}\n >\n <span>\n + {remainingCount}{\" \"}\n <span className=\"max-sm:sr-only\">more</span>\n </span>\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"center\"\n className=\"max-w-52 p-3\"\n style={\n {\n \"--event-height\": `${EventHeight}px`,\n } as React.CSSProperties\n }\n >\n <div className=\"space-y-2\">\n <div className=\"text-sm font-medium\">\n {format(day, \"EEE d\")}\n </div>\n <div className=\"space-y-1\">\n {sortEvents(allEvents).map((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isFirstDay = isSameDay(day, eventStart)\n const isLastDay = isSameDay(day, eventEnd)\n\n return (\n <EventItem\n key={event.id}\n onClick={(e) =>\n handleEventClick(event, e)\n }\n event={event}\n view=\"month\"\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n />\n )\n })}\n </div>\n </div>\n </PopoverContent>\n </Popover>\n )}\n </div>\n </DroppableCell>\n </div>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n}\n",
90
+ "type": "registry:component"
91
+ },
92
+ {
93
+ "path": "registry/default/components/event-calendar/types.ts",
94
+ "content": "export type CalendarView = \"month\" | \"week\" | \"day\" | \"agenda\"\n\nexport interface CalendarEvent {\n id: string\n title: string\n description?: string\n start: Date\n end: Date\n allDay?: boolean\n color?: EventColor\n location?: string\n}\n\nexport type EventColor =\n | \"sky\"\n | \"amber\"\n | \"violet\"\n | \"rose\"\n | \"emerald\"\n | \"orange\"\n",
95
+ "type": "registry:component"
96
+ },
97
+ {
98
+ "path": "registry/default/components/event-calendar/utils.ts",
99
+ "content": "import { isSameDay } from \"date-fns\"\n\nimport type {\n CalendarEvent,\n EventColor,\n} from \"@/registry/default/components/event-calendar\"\n\n/**\n * Get CSS classes for event colors\n */\nexport function getEventColorClasses(color?: EventColor | string): string {\n const eventColor = color || \"sky\"\n\n switch (eventColor) {\n case \"sky\":\n return \"bg-sky-200/50 hover:bg-sky-200/40 text-sky-950/80 dark:bg-sky-400/25 dark:hover:bg-sky-400/20 dark:text-sky-200 shadow-sky-700/8\"\n case \"amber\":\n return \"bg-amber-200/50 hover:bg-amber-200/40 text-amber-950/80 dark:bg-amber-400/25 dark:hover:bg-amber-400/20 dark:text-amber-200 shadow-amber-700/8\"\n case \"violet\":\n return \"bg-violet-200/50 hover:bg-violet-200/40 text-violet-950/80 dark:bg-violet-400/25 dark:hover:bg-violet-400/20 dark:text-violet-200 shadow-violet-700/8\"\n case \"rose\":\n return \"bg-rose-200/50 hover:bg-rose-200/40 text-rose-950/80 dark:bg-rose-400/25 dark:hover:bg-rose-400/20 dark:text-rose-200 shadow-rose-700/8\"\n case \"emerald\":\n return \"bg-emerald-200/50 hover:bg-emerald-200/40 text-emerald-950/80 dark:bg-emerald-400/25 dark:hover:bg-emerald-400/20 dark:text-emerald-200 shadow-emerald-700/8\"\n case \"orange\":\n return \"bg-orange-200/50 hover:bg-orange-200/40 text-orange-950/80 dark:bg-orange-400/25 dark:hover:bg-orange-400/20 dark:text-orange-200 shadow-orange-700/8\"\n default:\n return \"bg-sky-200/50 hover:bg-sky-200/40 text-sky-950/80 dark:bg-sky-400/25 dark:hover:bg-sky-400/20 dark:text-sky-200 shadow-sky-700/8\"\n }\n}\n\n/**\n * Get CSS classes for border radius based on event position in multi-day events\n */\nexport function getBorderRadiusClasses(\n isFirstDay: boolean,\n isLastDay: boolean\n): string {\n if (isFirstDay && isLastDay) {\n return \"rounded\" // Both ends rounded\n } else if (isFirstDay) {\n return \"rounded-l rounded-r-none\" // Only left end rounded\n } else if (isLastDay) {\n return \"rounded-r rounded-l-none\" // Only right end rounded\n } else {\n return \"rounded-none\" // No rounded corners\n }\n}\n\n/**\n * Check if an event is a multi-day event\n */\nexport function isMultiDayEvent(event: CalendarEvent): boolean {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return event.allDay || eventStart.getDate() !== eventEnd.getDate()\n}\n\n/**\n * Filter events for a specific day\n */\nexport function getEventsForDay(\n events: CalendarEvent[],\n day: Date\n): CalendarEvent[] {\n return events\n .filter((event) => {\n const eventStart = new Date(event.start)\n return isSameDay(day, eventStart)\n })\n .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())\n}\n\n/**\n * Sort events with multi-day events first, then by start time\n */\nexport function sortEvents(events: CalendarEvent[]): CalendarEvent[] {\n return [...events].sort((a, b) => {\n const aIsMultiDay = isMultiDayEvent(a)\n const bIsMultiDay = isMultiDayEvent(b)\n\n if (aIsMultiDay && !bIsMultiDay) return -1\n if (!aIsMultiDay && bIsMultiDay) return 1\n\n return new Date(a.start).getTime() - new Date(b.start).getTime()\n })\n}\n\n/**\n * Get multi-day events that span across a specific day (but don't start on that day)\n */\nexport function getSpanningEventsForDay(\n events: CalendarEvent[],\n day: Date\n): CalendarEvent[] {\n return events.filter((event) => {\n if (!isMultiDayEvent(event)) return false\n\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n\n // Only include if it's not the start day but is either the end day or a middle day\n return (\n !isSameDay(day, eventStart) &&\n (isSameDay(day, eventEnd) || (day > eventStart && day < eventEnd))\n )\n })\n}\n\n/**\n * Get all events visible on a specific day (starting, ending, or spanning)\n */\nexport function getAllEventsForDay(\n events: CalendarEvent[],\n day: Date\n): CalendarEvent[] {\n return events.filter((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return (\n isSameDay(day, eventStart) ||\n isSameDay(day, eventEnd) ||\n (day > eventStart && day < eventEnd)\n )\n })\n}\n\n/**\n * Get all events for a day (for agenda view)\n */\nexport function getAgendaEventsForDay(\n events: CalendarEvent[],\n day: Date\n): CalendarEvent[] {\n return events\n .filter((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return (\n isSameDay(day, eventStart) ||\n isSameDay(day, eventEnd) ||\n (day > eventStart && day < eventEnd)\n )\n })\n .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())\n}\n\n/**\n * Add hours to a date\n */\nexport function addHoursToDate(date: Date, hours: number): Date {\n const result = new Date(date)\n result.setHours(result.getHours() + hours)\n return result\n}\n",
100
+ "type": "registry:component"
101
+ },
102
+ {
103
+ "path": "registry/default/components/event-calendar/week-view.tsx",
104
+ "content": "\"use client\"\n\nimport React, { useMemo } from \"react\"\nimport {\n addHours,\n areIntervalsOverlapping,\n differenceInMinutes,\n eachDayOfInterval,\n eachHourOfInterval,\n endOfWeek,\n format,\n getHours,\n getMinutes,\n isBefore,\n isSameDay,\n isToday,\n startOfDay,\n startOfWeek,\n} from \"date-fns\"\n\nimport {\n DraggableEvent,\n DroppableCell,\n EventItem,\n isMultiDayEvent,\n useCurrentTimeIndicator,\n WeekCellsHeight,\n type CalendarEvent,\n} from \"@/registry/building-blocks/default/components/event-calendar\"\nimport {\n EndHour,\n StartHour,\n} from \"@/registry/building-blocks/default/components/event-calendar/constants\"\nimport { cn } from \"@/registry/building-blocks/default/lib/utils\"\n\ninterface WeekViewProps {\n currentDate: Date\n events: CalendarEvent[]\n onEventSelect: (event: CalendarEvent) => void\n onEventCreate: (startTime: Date) => void\n}\n\ninterface PositionedEvent {\n event: CalendarEvent\n top: number\n height: number\n left: number\n width: number\n zIndex: number\n}\n\nexport function WeekView({\n currentDate,\n events,\n onEventSelect,\n onEventCreate,\n}: WeekViewProps) {\n const days = useMemo(() => {\n const weekStart = startOfWeek(currentDate, { weekStartsOn: 0 })\n const weekEnd = endOfWeek(currentDate, { weekStartsOn: 0 })\n return eachDayOfInterval({ start: weekStart, end: weekEnd })\n }, [currentDate])\n\n const weekStart = useMemo(\n () => startOfWeek(currentDate, { weekStartsOn: 0 }),\n [currentDate]\n )\n\n const hours = useMemo(() => {\n const dayStart = startOfDay(currentDate)\n return eachHourOfInterval({\n start: addHours(dayStart, StartHour),\n end: addHours(dayStart, EndHour - 1),\n })\n }, [currentDate])\n\n // Get all-day events and multi-day events for the week\n const allDayEvents = useMemo(() => {\n return events\n .filter((event) => {\n // Include explicitly marked all-day events or multi-day events\n return event.allDay || isMultiDayEvent(event)\n })\n .filter((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return days.some(\n (day) =>\n isSameDay(day, eventStart) ||\n isSameDay(day, eventEnd) ||\n (day > eventStart && day < eventEnd)\n )\n })\n }, [events, days])\n\n // Process events for each day to calculate positions\n const processedDayEvents = useMemo(() => {\n const result = days.map((day) => {\n // Get events for this day that are not all-day events or multi-day events\n const dayEvents = events.filter((event) => {\n // Skip all-day events and multi-day events\n if (event.allDay || isMultiDayEvent(event)) return false\n\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n\n // Check if event is on this day\n return (\n isSameDay(day, eventStart) ||\n isSameDay(day, eventEnd) ||\n (eventStart < day && eventEnd > day)\n )\n })\n\n // Sort events by start time and duration\n const sortedEvents = [...dayEvents].sort((a, b) => {\n const aStart = new Date(a.start)\n const bStart = new Date(b.start)\n const aEnd = new Date(a.end)\n const bEnd = new Date(b.end)\n\n // First sort by start time\n if (aStart < bStart) return -1\n if (aStart > bStart) return 1\n\n // If start times are equal, sort by duration (longer events first)\n const aDuration = differenceInMinutes(aEnd, aStart)\n const bDuration = differenceInMinutes(bEnd, bStart)\n return bDuration - aDuration\n })\n\n // Calculate positions for each event\n const positionedEvents: PositionedEvent[] = []\n const dayStart = startOfDay(day)\n\n // Track columns for overlapping events\n const columns: { event: CalendarEvent; end: Date }[][] = []\n\n sortedEvents.forEach((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n\n // Adjust start and end times if they're outside this day\n const adjustedStart = isSameDay(day, eventStart) ? eventStart : dayStart\n const adjustedEnd = isSameDay(day, eventEnd)\n ? eventEnd\n : addHours(dayStart, 24)\n\n // Calculate top position and height\n const startHour =\n getHours(adjustedStart) + getMinutes(adjustedStart) / 60\n const endHour = getHours(adjustedEnd) + getMinutes(adjustedEnd) / 60\n\n // Adjust the top calculation to account for the new start time\n const top = (startHour - StartHour) * WeekCellsHeight\n const height = (endHour - startHour) * WeekCellsHeight\n\n // Find a column for this event\n let columnIndex = 0\n let placed = false\n\n while (!placed) {\n const col = columns[columnIndex] || []\n if (col.length === 0) {\n columns[columnIndex] = col\n placed = true\n } else {\n const overlaps = col.some((c) =>\n areIntervalsOverlapping(\n { start: adjustedStart, end: adjustedEnd },\n {\n start: new Date(c.event.start),\n end: new Date(c.event.end),\n }\n )\n )\n if (!overlaps) {\n placed = true\n } else {\n columnIndex++\n }\n }\n }\n\n // Ensure column is initialized before pushing\n const currentColumn = columns[columnIndex] || []\n columns[columnIndex] = currentColumn\n currentColumn.push({ event, end: adjustedEnd })\n\n // Calculate width and left position based on number of columns\n const width = columnIndex === 0 ? 1 : 0.9\n const left = columnIndex === 0 ? 0 : columnIndex * 0.1\n\n positionedEvents.push({\n event,\n top,\n height,\n left,\n width,\n zIndex: 10 + columnIndex, // Higher columns get higher z-index\n })\n })\n\n return positionedEvents\n })\n\n return result\n }, [days, events])\n\n const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {\n e.stopPropagation()\n onEventSelect(event)\n }\n\n const showAllDaySection = allDayEvents.length > 0\n const { currentTimePosition, currentTimeVisible } = useCurrentTimeIndicator(\n currentDate,\n \"week\"\n )\n\n return (\n <div data-slot=\"week-view\" className=\"flex h-full flex-col\">\n <div className=\"sticky top-0 z-30 grid grid-cols-8 border-b border-border/70 bg-background/80 backdrop-blur-md\">\n <div className=\"py-2 text-center text-sm text-muted-foreground/70\">\n <span className=\"max-[479px]:sr-only\">{format(new Date(), \"O\")}</span>\n </div>\n {days.map((day) => (\n <div\n key={day.toString()}\n className=\"py-2 text-center text-sm text-muted-foreground/70 data-today:font-medium data-today:text-foreground\"\n data-today={isToday(day) || undefined}\n >\n <span className=\"sm:hidden\" aria-hidden=\"true\">\n {format(day, \"E\")[0]} {format(day, \"d\")}\n </span>\n <span className=\"max-sm:hidden\">{format(day, \"EEE dd\")}</span>\n </div>\n ))}\n </div>\n\n {showAllDaySection && (\n <div className=\"border-b border-border/70 bg-muted/50\">\n <div className=\"grid grid-cols-8\">\n <div className=\"relative border-r border-border/70\">\n <span className=\"absolute bottom-0 left-0 h-6 w-16 max-w-full pe-2 text-right text-[10px] text-muted-foreground/70 sm:pe-4 sm:text-xs\">\n All day\n </span>\n </div>\n {days.map((day, dayIndex) => {\n const dayAllDayEvents = allDayEvents.filter((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n return (\n isSameDay(day, eventStart) ||\n (day > eventStart && day < eventEnd) ||\n isSameDay(day, eventEnd)\n )\n })\n\n return (\n <div\n key={day.toString()}\n className=\"relative border-r border-border/70 p-1 last:border-r-0\"\n data-today={isToday(day) || undefined}\n >\n {dayAllDayEvents.map((event) => {\n const eventStart = new Date(event.start)\n const eventEnd = new Date(event.end)\n const isFirstDay = isSameDay(day, eventStart)\n const isLastDay = isSameDay(day, eventEnd)\n\n // Check if this is the first day in the current week view\n const isFirstVisibleDay =\n dayIndex === 0 && isBefore(eventStart, weekStart)\n const shouldShowTitle = isFirstDay || isFirstVisibleDay\n\n return (\n <EventItem\n key={`spanning-${event.id}`}\n onClick={(e) => handleEventClick(event, e)}\n event={event}\n view=\"month\"\n isFirstDay={isFirstDay}\n isLastDay={isLastDay}\n >\n {/* Show title if it's the first day of the event or the first visible day in the week */}\n <div\n className={cn(\n \"truncate\",\n !shouldShowTitle && \"invisible\"\n )}\n aria-hidden={!shouldShowTitle}\n >\n {event.title}\n </div>\n </EventItem>\n )\n })}\n </div>\n )\n })}\n </div>\n </div>\n )}\n\n <div className=\"grid flex-1 grid-cols-8 overflow-hidden\">\n <div className=\"grid auto-cols-fr border-r border-border/70\">\n {hours.map((hour, index) => (\n <div\n key={hour.toString()}\n className=\"relative min-h-[var(--week-cells-height)] border-b border-border/70 last:border-b-0\"\n >\n {index > 0 && (\n <span className=\"absolute -top-3 left-0 flex h-6 w-16 max-w-full items-center justify-end bg-background pe-2 text-[10px] text-muted-foreground/70 sm:pe-4 sm:text-xs\">\n {format(hour, \"h a\")}\n </span>\n )}\n </div>\n ))}\n </div>\n\n {days.map((day, dayIndex) => (\n <div\n key={day.toString()}\n className=\"relative grid auto-cols-fr border-r border-border/70 last:border-r-0\"\n data-today={isToday(day) || undefined}\n >\n {/* Positioned events */}\n {(processedDayEvents[dayIndex] ?? []).map((positionedEvent) => (\n <div\n key={positionedEvent.event.id}\n className=\"absolute z-10 px-0.5\"\n style={{\n top: `${positionedEvent.top}px`,\n height: `${positionedEvent.height}px`,\n left: `${positionedEvent.left * 100}%`,\n width: `${positionedEvent.width * 100}%`,\n zIndex: positionedEvent.zIndex,\n }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"size-full\">\n <DraggableEvent\n event={positionedEvent.event}\n view=\"week\"\n onClick={(e) => handleEventClick(positionedEvent.event, e)}\n showTime\n height={positionedEvent.height}\n />\n </div>\n </div>\n ))}\n\n {/* Current time indicator - only show for today's column */}\n {currentTimeVisible && isToday(day) && (\n <div\n className=\"pointer-events-none absolute right-0 left-0 z-20\"\n style={{ top: `${currentTimePosition}%` }}\n >\n <div className=\"relative flex items-center\">\n <div className=\"absolute -left-1 h-2 w-2 rounded-full bg-primary\"></div>\n <div className=\"h-[2px] w-full bg-primary\"></div>\n </div>\n </div>\n )}\n {hours.map((hour) => {\n const hourValue = getHours(hour)\n return (\n <div\n key={hour.toString()}\n className=\"relative min-h-[var(--week-cells-height)] border-b border-border/70 last:border-b-0\"\n >\n {/* Quarter-hour intervals */}\n {[0, 1, 2, 3].map((quarter) => {\n const quarterHourTime = hourValue + quarter * 0.25\n return (\n <DroppableCell\n key={`${hour.toString()}-${quarter}`}\n id={`week-cell-${day.toISOString()}-${quarterHourTime}`}\n date={day}\n time={quarterHourTime}\n className={cn(\n \"absolute h-[calc(var(--week-cells-height)/4)] w-full\",\n quarter === 0 && \"top-0\",\n quarter === 1 &&\n \"top-[calc(var(--week-cells-height)/4)]\",\n quarter === 2 &&\n \"top-[calc(var(--week-cells-height)/4*2)]\",\n quarter === 3 &&\n \"top-[calc(var(--week-cells-height)/4*3)]\"\n )}\n onClick={() => {\n const startTime = new Date(day)\n startTime.setHours(hourValue)\n startTime.setMinutes(quarter * 15)\n onEventCreate(startTime)\n }}\n />\n )\n })}\n </div>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n}\n",
105
+ "type": "registry:component"
106
+ },
107
+ {
108
+ "path": "registry/default/components/event-calendar/hooks/use-current-time-indicator.ts",
109
+ "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { endOfWeek, isSameDay, isWithinInterval, startOfWeek } from \"date-fns\"\n\nimport {\n EndHour,\n StartHour,\n} from \"@/registry/default/components/event-calendar/constants\"\n\nexport function useCurrentTimeIndicator(\n currentDate: Date,\n view: \"day\" | \"week\"\n) {\n const [currentTimePosition, setCurrentTimePosition] = useState<number>(0)\n const [currentTimeVisible, setCurrentTimeVisible] = useState<boolean>(false)\n\n useEffect(() => {\n const calculateTimePosition = () => {\n const now = new Date()\n const hours = now.getHours()\n const minutes = now.getMinutes()\n const totalMinutes = (hours - StartHour) * 60 + minutes\n const dayStartMinutes = 0 // 12am\n const dayEndMinutes = (EndHour - StartHour) * 60 // 12am next day\n\n // Calculate position as percentage of day\n const position =\n ((totalMinutes - dayStartMinutes) / (dayEndMinutes - dayStartMinutes)) *\n 100\n\n // Check if current day is in view based on the calendar view\n let isCurrentTimeVisible = false\n\n if (view === \"day\") {\n isCurrentTimeVisible = isSameDay(now, currentDate)\n } else if (view === \"week\") {\n const startOfWeekDate = startOfWeek(currentDate, { weekStartsOn: 0 })\n const endOfWeekDate = endOfWeek(currentDate, { weekStartsOn: 0 })\n isCurrentTimeVisible = isWithinInterval(now, {\n start: startOfWeekDate,\n end: endOfWeekDate,\n })\n }\n\n setCurrentTimePosition(position)\n setCurrentTimeVisible(isCurrentTimeVisible)\n }\n\n // Calculate immediately\n calculateTimePosition()\n\n // Update every minute\n const interval = setInterval(calculateTimePosition, 60000)\n\n return () => clearInterval(interval)\n }, [currentDate, view])\n\n return { currentTimePosition, currentTimeVisible }\n}\n",
110
+ "type": "registry:component"
111
+ },
112
+ {
113
+ "path": "registry/default/components/event-calendar/hooks/use-event-visibility.ts",
114
+ "content": "\"use client\"\n\nimport { useLayoutEffect, useMemo, useRef, useState } from \"react\"\n\ninterface EventVisibilityOptions {\n eventHeight: number\n eventGap: number\n}\n\ninterface EventVisibilityResult {\n contentRef: React.RefObject<HTMLDivElement>\n contentHeight: number | null\n getVisibleEventCount: (totalEvents: number) => number\n}\n\n/**\n * Hook for calculating event visibility based on container height\n * Uses ResizeObserver for efficient updates\n */\nexport function useEventVisibility({\n eventHeight,\n eventGap,\n}: EventVisibilityOptions): EventVisibilityResult {\n // Use the standard pattern for React refs\n const contentRef = useRef<HTMLDivElement>(null)\n const observerRef = useRef<ResizeObserver | null>(null)\n const [contentHeight, setContentHeight] = useState<number | null>(null)\n\n // Use layout effect for synchronous measurement before paint\n useLayoutEffect(() => {\n if (!contentRef.current) return\n\n // Function to update the content height\n const updateHeight = () => {\n if (contentRef.current) {\n setContentHeight(contentRef.current.clientHeight)\n }\n }\n\n // Initial measurement (synchronous)\n updateHeight()\n\n // Create observer only once and reuse it\n if (!observerRef.current) {\n observerRef.current = new ResizeObserver(() => {\n // Just call updateHeight when resize is detected\n updateHeight()\n })\n }\n\n // Start observing the content container\n observerRef.current.observe(contentRef.current)\n\n // Clean up function\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect()\n }\n }\n }, [])\n\n // Function to calculate visible events for a cell\n const getVisibleEventCount = useMemo(() => {\n return (totalEvents: number): number => {\n if (!contentHeight) return totalEvents\n\n // Calculate how many events can fit in the container\n const maxEvents = Math.floor(contentHeight / (eventHeight + eventGap))\n\n // If all events fit, show them all\n if (totalEvents <= maxEvents) {\n return totalEvents\n } else {\n // Otherwise, reserve space for \"more\" button by showing one less\n return maxEvents > 0 ? maxEvents - 1 : 0\n }\n }\n }, [contentHeight, eventHeight, eventGap])\n\n // Use type assertion to satisfy TypeScript\n return {\n contentRef,\n contentHeight,\n getVisibleEventCount,\n } as EventVisibilityResult\n}\n",
115
+ "type": "registry:component"
116
+ }
117
+ ],
118
+ "meta": {
119
+ "tags": [
120
+ "calendar"
121
+ ],
122
+ "colSpan": 3
123
+ }
124
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "comp-543",
4
+ "type": "registry:component",
5
+ "registryDependencies": [
6
+ "https://loveui.dev/building-blocks/r/button.json"
7
+ ],
8
+ "files": [
9
+ {
10
+ "path": "registry/default/components/comp-543.tsx",
11
+ "content": "\"use client\"\n\nimport { CircleUserRoundIcon, XIcon } from \"lucide-react\"\n\nimport { useFileUpload } from \"@/registry/building-blocks/default/hooks/use-file-upload\"\nimport { Button } from \"@/registry/building-blocks/default/ui/button\"\n\nexport default function Component() {\n const [\n { files, isDragging },\n {\n removeFile,\n openFileDialog,\n getInputProps,\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n },\n ] = useFileUpload({\n accept: \"image/*\",\n })\n\n const previewUrl = files[0]?.preview || null\n\n return (\n <div className=\"flex flex-col items-center gap-2\">\n <div className=\"relative inline-flex\">\n {/* Drop area */}\n <button\n className=\"relative flex size-16 items-center justify-center overflow-hidden rounded-full border border-dashed border-input transition-colors outline-none hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-disabled:pointer-events-none has-disabled:opacity-50 has-[img]:border-none data-[dragging=true]:bg-accent/50\"\n onClick={openFileDialog}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n data-dragging={isDragging || undefined}\n aria-label={previewUrl ? \"Change image\" : \"Upload image\"}\n >\n {previewUrl ? (\n <img\n className=\"size-full object-cover\"\n src={previewUrl}\n alt={files[0]?.file?.name || \"Uploaded image\"}\n width={64}\n height={64}\n style={{ objectFit: \"cover\" }}\n />\n ) : (\n <div aria-hidden=\"true\">\n <CircleUserRoundIcon className=\"size-4 opacity-60\" />\n </div>\n )}\n </button>\n {previewUrl && (\n <Button\n onClick={() => removeFile(files[0]?.id)}\n size=\"icon\"\n className=\"absolute -top-1 -right-1 size-6 rounded-full border-2 border-background shadow-none focus-visible:border-background\"\n aria-label=\"Remove image\"\n >\n <XIcon className=\"size-3.5\" />\n </Button>\n )}\n <input\n {...getInputProps()}\n className=\"sr-only\"\n aria-label=\"Upload image file\"\n tabIndex={-1}\n />\n </div>\n <p\n aria-live=\"polite\"\n role=\"region\"\n className=\"mt-2 text-xs text-muted-foreground\"\n >\n Avatar uploader with droppable area ∙{\" \"}\n <a\n href=\"https://github.com/loveui/blob/main/apps/origin/docs/use-file-upload.md\"\n className=\"underline hover:text-foreground\"\n >\n API\n </a>\n </p>\n </div>\n )\n}\n",
12
+ "type": "registry:component"
13
+ },
14
+ {
15
+ "path": "registry/default/hooks/use-file-upload.ts",
16
+ "content": "\"use client\"\n\nimport type React from \"react\"\nimport {\n useCallback,\n useRef,\n useState,\n type ChangeEvent,\n type DragEvent,\n type InputHTMLAttributes,\n} from \"react\"\n\nexport type FileMetadata = {\n name: string\n size: number\n type: string\n url: string\n id: string\n}\n\nexport type FileWithPreview = {\n file: File | FileMetadata\n id: string\n preview?: string\n}\n\nexport type FileUploadOptions = {\n maxFiles?: number // Only used when multiple is true, defaults to Infinity\n maxSize?: number // in bytes\n accept?: string\n multiple?: boolean // Defaults to false\n initialFiles?: FileMetadata[]\n onFilesChange?: (files: FileWithPreview[]) => void // Callback when files change\n onFilesAdded?: (addedFiles: FileWithPreview[]) => void // Callback when new files are added\n}\n\nexport type FileUploadState = {\n files: FileWithPreview[]\n isDragging: boolean\n errors: string[]\n}\n\nexport type FileUploadActions = {\n addFiles: (files: FileList | File[]) => void\n removeFile: (id: string) => void\n clearFiles: () => void\n clearErrors: () => void\n handleDragEnter: (e: DragEvent<HTMLElement>) => void\n handleDragLeave: (e: DragEvent<HTMLElement>) => void\n handleDragOver: (e: DragEvent<HTMLElement>) => void\n handleDrop: (e: DragEvent<HTMLElement>) => void\n handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void\n openFileDialog: () => void\n getInputProps: (\n props?: InputHTMLAttributes<HTMLInputElement>\n ) => InputHTMLAttributes<HTMLInputElement> & {\n // Use `any` here to avoid cross-React ref type conflicts across packages\n ref: any\n }\n}\n\nexport const useFileUpload = (\n options: FileUploadOptions = {}\n): [FileUploadState, FileUploadActions] => {\n const {\n maxFiles = Infinity,\n maxSize = Infinity,\n accept = \"*\",\n multiple = false,\n initialFiles = [],\n onFilesChange,\n onFilesAdded,\n } = options\n\n const [state, setState] = useState<FileUploadState>({\n files: initialFiles.map((file) => ({\n file,\n id: file.id,\n preview: file.url,\n })),\n isDragging: false,\n errors: [],\n })\n\n const inputRef = useRef<HTMLInputElement>(null)\n\n const validateFile = useCallback(\n (file: File | FileMetadata): string | null => {\n if (file instanceof File) {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n } else {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n }\n\n if (accept !== \"*\") {\n const acceptedTypes = accept.split(\",\").map((type) => type.trim())\n const fileType = file instanceof File ? file.type || \"\" : file.type\n const fileExtension = `.${file instanceof File ? file.name.split(\".\").pop() : file.name.split(\".\").pop()}`\n\n const isAccepted = acceptedTypes.some((type) => {\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase()\n }\n if (type.endsWith(\"/*\")) {\n const baseType = type.split(\"/\")[0]\n return fileType.startsWith(`${baseType}/`)\n }\n return fileType === type\n })\n\n if (!isAccepted) {\n return `File \"${file instanceof File ? file.name : file.name}\" is not an accepted file type.`\n }\n }\n\n return null\n },\n [accept, maxSize]\n )\n\n const createPreview = useCallback(\n (file: File | FileMetadata): string | undefined => {\n if (file instanceof File) {\n return URL.createObjectURL(file)\n }\n return file.url\n },\n []\n )\n\n const generateUniqueId = useCallback((file: File | FileMetadata): string => {\n if (file instanceof File) {\n return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`\n }\n return file.id\n }, [])\n\n const clearFiles = useCallback(() => {\n setState((prev) => {\n // Clean up object URLs\n prev.files.forEach((file) => {\n if (\n file.preview &&\n file.file instanceof File &&\n file.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(file.preview)\n }\n })\n\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n\n const newState = {\n ...prev,\n files: [],\n errors: [],\n }\n\n onFilesChange?.(newState.files)\n return newState\n })\n }, [onFilesChange])\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n if (!newFiles || newFiles.length === 0) return\n\n const newFilesArray = Array.from(newFiles)\n const errors: string[] = []\n\n // Clear existing errors when new files are uploaded\n setState((prev) => ({ ...prev, errors: [] }))\n\n // In single file mode, clear existing files first\n if (!multiple) {\n clearFiles()\n }\n\n // Check if adding these files would exceed maxFiles (only in multiple mode)\n if (\n multiple &&\n maxFiles !== Infinity &&\n state.files.length + newFilesArray.length > maxFiles\n ) {\n errors.push(`You can only upload a maximum of ${maxFiles} files.`)\n setState((prev) => ({ ...prev, errors }))\n return\n }\n\n const validFiles: FileWithPreview[] = []\n\n newFilesArray.forEach((file) => {\n // Only check for duplicates if multiple files are allowed\n if (multiple) {\n const isDuplicate = state.files.some(\n (existingFile) =>\n existingFile.file.name === file.name &&\n existingFile.file.size === file.size\n )\n\n // Skip duplicate files silently\n if (isDuplicate) {\n return\n }\n }\n\n // Check file size\n if (file.size > maxSize) {\n errors.push(\n multiple\n ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`\n : `File exceeds the maximum size of ${formatBytes(maxSize)}.`\n )\n return\n }\n\n const error = validateFile(file)\n if (error) {\n errors.push(error)\n } else {\n validFiles.push({\n file,\n id: generateUniqueId(file),\n preview: createPreview(file),\n })\n }\n })\n\n // Only update state if we have valid files to add\n if (validFiles.length > 0) {\n // Call the onFilesAdded callback with the newly added valid files\n onFilesAdded?.(validFiles)\n\n setState((prev) => {\n const newFiles = !multiple\n ? validFiles\n : [...prev.files, ...validFiles]\n onFilesChange?.(newFiles)\n return {\n ...prev,\n files: newFiles,\n errors,\n }\n })\n } else if (errors.length > 0) {\n setState((prev) => ({\n ...prev,\n errors,\n }))\n }\n\n // Reset input value after handling files\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n },\n [\n state.files,\n maxFiles,\n multiple,\n maxSize,\n validateFile,\n createPreview,\n generateUniqueId,\n clearFiles,\n onFilesChange,\n onFilesAdded,\n ]\n )\n\n const removeFile = useCallback(\n (id: string) => {\n setState((prev) => {\n const fileToRemove = prev.files.find((file) => file.id === id)\n if (\n fileToRemove &&\n fileToRemove.preview &&\n fileToRemove.file instanceof File &&\n fileToRemove.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(fileToRemove.preview)\n }\n\n const newFiles = prev.files.filter((file) => file.id !== id)\n onFilesChange?.(newFiles)\n\n return {\n ...prev,\n files: newFiles,\n errors: [],\n }\n })\n },\n [onFilesChange]\n )\n\n const clearErrors = useCallback(() => {\n setState((prev) => ({\n ...prev,\n errors: [],\n }))\n }, [])\n\n const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: true }))\n }, [])\n\n const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n\n if (e.currentTarget.contains(e.relatedTarget as Node)) {\n return\n }\n\n setState((prev) => ({ ...prev, isDragging: false }))\n }, [])\n\n const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n }, [])\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: false }))\n\n // Don't process files if the input is disabled\n if (inputRef.current?.disabled) {\n return\n }\n\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n // In single file mode, only use the first file\n if (!multiple) {\n const file = e.dataTransfer.files[0]\n addFiles([file])\n } else {\n addFiles(e.dataTransfer.files)\n }\n }\n },\n [addFiles, multiple]\n )\n\n const handleFileChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files && e.target.files.length > 0) {\n addFiles(e.target.files)\n }\n },\n [addFiles]\n )\n\n const openFileDialog = useCallback(() => {\n if (inputRef.current) {\n inputRef.current.click()\n }\n }, [])\n\n const getInputProps = useCallback(\n (props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n return {\n ...props,\n type: \"file\" as const,\n onChange: handleFileChange,\n accept: props.accept || accept,\n multiple: props.multiple !== undefined ? props.multiple : multiple,\n // Cast to `any` to prevent mismatched React ref type errors across workspaces\n ref: inputRef as any,\n }\n },\n [accept, multiple, handleFileChange]\n )\n\n return [\n state,\n {\n addFiles,\n removeFile,\n clearFiles,\n clearErrors,\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n handleFileChange,\n openFileDialog,\n getInputProps,\n },\n ]\n}\n\n// Helper function to format bytes to human-readable format\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n if (bytes === 0) return \"0 Bytes\"\n\n const k = 1024\n const dm = decimals < 0 ? 0 : decimals\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n\n return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]\n}\n",
17
+ "type": "registry:hook"
18
+ }
19
+ ],
20
+ "meta": {
21
+ "tags": [
22
+ "upload",
23
+ "file",
24
+ "image",
25
+ "drag and drop",
26
+ "avatar"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "comp-544",
4
+ "type": "registry:component",
5
+ "files": [
6
+ {
7
+ "path": "registry/default/components/comp-544.tsx",
8
+ "content": "\"use client\"\n\nimport { AlertCircleIcon, ImageUpIcon, XIcon } from \"lucide-react\"\n\nimport { useFileUpload } from \"@/registry/building-blocks/default/hooks/use-file-upload\"\n\nexport default function Component() {\n const maxSizeMB = 5\n const maxSize = maxSizeMB * 1024 * 1024 // 5MB default\n\n const [\n { files, isDragging, errors },\n {\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n openFileDialog,\n removeFile,\n getInputProps,\n },\n ] = useFileUpload({\n accept: \"image/*\",\n maxSize,\n })\n\n const previewUrl = files[0]?.preview || null\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div className=\"relative\">\n {/* Drop area */}\n <div\n role=\"button\"\n onClick={openFileDialog}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n data-dragging={isDragging || undefined}\n className=\"relative flex min-h-52 flex-col items-center justify-center overflow-hidden rounded-xl border border-dashed border-input p-4 transition-colors hover:bg-accent/50 has-disabled:pointer-events-none has-disabled:opacity-50 has-[img]:border-none has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50 data-[dragging=true]:bg-accent/50\"\n >\n <input\n {...getInputProps()}\n className=\"sr-only\"\n aria-label=\"Upload file\"\n />\n {previewUrl ? (\n <div className=\"absolute inset-0\">\n <img\n src={previewUrl}\n alt={files[0]?.file?.name || \"Uploaded image\"}\n className=\"size-full object-cover\"\n />\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center px-4 py-3 text-center\">\n <div\n className=\"mb-2 flex size-11 shrink-0 items-center justify-center rounded-full border bg-background\"\n aria-hidden=\"true\"\n >\n <ImageUpIcon className=\"size-4 opacity-60\" />\n </div>\n <p className=\"mb-1.5 text-sm font-medium\">\n Drop your image here or click to browse\n </p>\n <p className=\"text-xs text-muted-foreground\">\n Max size: {maxSizeMB}MB\n </p>\n </div>\n )}\n </div>\n {previewUrl && (\n <div className=\"absolute top-4 right-4\">\n <button\n type=\"button\"\n className=\"z-50 flex size-8 cursor-pointer items-center justify-center rounded-full bg-black/60 text-white transition-[color,box-shadow] outline-none hover:bg-black/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n onClick={() => removeFile(files[0]?.id)}\n aria-label=\"Remove image\"\n >\n <XIcon className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n </div>\n\n {errors.length > 0 && (\n <div\n className=\"flex items-center gap-1 text-xs text-destructive\"\n role=\"alert\"\n >\n <AlertCircleIcon className=\"size-3 shrink-0\" />\n <span>{errors[0]}</span>\n </div>\n )}\n\n <p\n aria-live=\"polite\"\n role=\"region\"\n className=\"mt-2 text-center text-xs text-muted-foreground\"\n >\n Single image uploader w/ max size ∙{\" \"}\n <a\n href=\"https://github.com/loveui/blob/main/apps/origin/docs/use-file-upload.md\"\n className=\"underline hover:text-foreground\"\n >\n API\n </a>\n </p>\n </div>\n )\n}\n",
9
+ "type": "registry:component"
10
+ },
11
+ {
12
+ "path": "registry/default/hooks/use-file-upload.ts",
13
+ "content": "\"use client\"\n\nimport type React from \"react\"\nimport {\n useCallback,\n useRef,\n useState,\n type ChangeEvent,\n type DragEvent,\n type InputHTMLAttributes,\n} from \"react\"\n\nexport type FileMetadata = {\n name: string\n size: number\n type: string\n url: string\n id: string\n}\n\nexport type FileWithPreview = {\n file: File | FileMetadata\n id: string\n preview?: string\n}\n\nexport type FileUploadOptions = {\n maxFiles?: number // Only used when multiple is true, defaults to Infinity\n maxSize?: number // in bytes\n accept?: string\n multiple?: boolean // Defaults to false\n initialFiles?: FileMetadata[]\n onFilesChange?: (files: FileWithPreview[]) => void // Callback when files change\n onFilesAdded?: (addedFiles: FileWithPreview[]) => void // Callback when new files are added\n}\n\nexport type FileUploadState = {\n files: FileWithPreview[]\n isDragging: boolean\n errors: string[]\n}\n\nexport type FileUploadActions = {\n addFiles: (files: FileList | File[]) => void\n removeFile: (id: string) => void\n clearFiles: () => void\n clearErrors: () => void\n handleDragEnter: (e: DragEvent<HTMLElement>) => void\n handleDragLeave: (e: DragEvent<HTMLElement>) => void\n handleDragOver: (e: DragEvent<HTMLElement>) => void\n handleDrop: (e: DragEvent<HTMLElement>) => void\n handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void\n openFileDialog: () => void\n getInputProps: (\n props?: InputHTMLAttributes<HTMLInputElement>\n ) => InputHTMLAttributes<HTMLInputElement> & {\n // Use `any` here to avoid cross-React ref type conflicts across packages\n ref: any\n }\n}\n\nexport const useFileUpload = (\n options: FileUploadOptions = {}\n): [FileUploadState, FileUploadActions] => {\n const {\n maxFiles = Infinity,\n maxSize = Infinity,\n accept = \"*\",\n multiple = false,\n initialFiles = [],\n onFilesChange,\n onFilesAdded,\n } = options\n\n const [state, setState] = useState<FileUploadState>({\n files: initialFiles.map((file) => ({\n file,\n id: file.id,\n preview: file.url,\n })),\n isDragging: false,\n errors: [],\n })\n\n const inputRef = useRef<HTMLInputElement>(null)\n\n const validateFile = useCallback(\n (file: File | FileMetadata): string | null => {\n if (file instanceof File) {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n } else {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n }\n\n if (accept !== \"*\") {\n const acceptedTypes = accept.split(\",\").map((type) => type.trim())\n const fileType = file instanceof File ? file.type || \"\" : file.type\n const fileExtension = `.${file instanceof File ? file.name.split(\".\").pop() : file.name.split(\".\").pop()}`\n\n const isAccepted = acceptedTypes.some((type) => {\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase()\n }\n if (type.endsWith(\"/*\")) {\n const baseType = type.split(\"/\")[0]\n return fileType.startsWith(`${baseType}/`)\n }\n return fileType === type\n })\n\n if (!isAccepted) {\n return `File \"${file instanceof File ? file.name : file.name}\" is not an accepted file type.`\n }\n }\n\n return null\n },\n [accept, maxSize]\n )\n\n const createPreview = useCallback(\n (file: File | FileMetadata): string | undefined => {\n if (file instanceof File) {\n return URL.createObjectURL(file)\n }\n return file.url\n },\n []\n )\n\n const generateUniqueId = useCallback((file: File | FileMetadata): string => {\n if (file instanceof File) {\n return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`\n }\n return file.id\n }, [])\n\n const clearFiles = useCallback(() => {\n setState((prev) => {\n // Clean up object URLs\n prev.files.forEach((file) => {\n if (\n file.preview &&\n file.file instanceof File &&\n file.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(file.preview)\n }\n })\n\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n\n const newState = {\n ...prev,\n files: [],\n errors: [],\n }\n\n onFilesChange?.(newState.files)\n return newState\n })\n }, [onFilesChange])\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n if (!newFiles || newFiles.length === 0) return\n\n const newFilesArray = Array.from(newFiles)\n const errors: string[] = []\n\n // Clear existing errors when new files are uploaded\n setState((prev) => ({ ...prev, errors: [] }))\n\n // In single file mode, clear existing files first\n if (!multiple) {\n clearFiles()\n }\n\n // Check if adding these files would exceed maxFiles (only in multiple mode)\n if (\n multiple &&\n maxFiles !== Infinity &&\n state.files.length + newFilesArray.length > maxFiles\n ) {\n errors.push(`You can only upload a maximum of ${maxFiles} files.`)\n setState((prev) => ({ ...prev, errors }))\n return\n }\n\n const validFiles: FileWithPreview[] = []\n\n newFilesArray.forEach((file) => {\n // Only check for duplicates if multiple files are allowed\n if (multiple) {\n const isDuplicate = state.files.some(\n (existingFile) =>\n existingFile.file.name === file.name &&\n existingFile.file.size === file.size\n )\n\n // Skip duplicate files silently\n if (isDuplicate) {\n return\n }\n }\n\n // Check file size\n if (file.size > maxSize) {\n errors.push(\n multiple\n ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`\n : `File exceeds the maximum size of ${formatBytes(maxSize)}.`\n )\n return\n }\n\n const error = validateFile(file)\n if (error) {\n errors.push(error)\n } else {\n validFiles.push({\n file,\n id: generateUniqueId(file),\n preview: createPreview(file),\n })\n }\n })\n\n // Only update state if we have valid files to add\n if (validFiles.length > 0) {\n // Call the onFilesAdded callback with the newly added valid files\n onFilesAdded?.(validFiles)\n\n setState((prev) => {\n const newFiles = !multiple\n ? validFiles\n : [...prev.files, ...validFiles]\n onFilesChange?.(newFiles)\n return {\n ...prev,\n files: newFiles,\n errors,\n }\n })\n } else if (errors.length > 0) {\n setState((prev) => ({\n ...prev,\n errors,\n }))\n }\n\n // Reset input value after handling files\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n },\n [\n state.files,\n maxFiles,\n multiple,\n maxSize,\n validateFile,\n createPreview,\n generateUniqueId,\n clearFiles,\n onFilesChange,\n onFilesAdded,\n ]\n )\n\n const removeFile = useCallback(\n (id: string) => {\n setState((prev) => {\n const fileToRemove = prev.files.find((file) => file.id === id)\n if (\n fileToRemove &&\n fileToRemove.preview &&\n fileToRemove.file instanceof File &&\n fileToRemove.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(fileToRemove.preview)\n }\n\n const newFiles = prev.files.filter((file) => file.id !== id)\n onFilesChange?.(newFiles)\n\n return {\n ...prev,\n files: newFiles,\n errors: [],\n }\n })\n },\n [onFilesChange]\n )\n\n const clearErrors = useCallback(() => {\n setState((prev) => ({\n ...prev,\n errors: [],\n }))\n }, [])\n\n const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: true }))\n }, [])\n\n const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n\n if (e.currentTarget.contains(e.relatedTarget as Node)) {\n return\n }\n\n setState((prev) => ({ ...prev, isDragging: false }))\n }, [])\n\n const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n }, [])\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: false }))\n\n // Don't process files if the input is disabled\n if (inputRef.current?.disabled) {\n return\n }\n\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n // In single file mode, only use the first file\n if (!multiple) {\n const file = e.dataTransfer.files[0]\n addFiles([file])\n } else {\n addFiles(e.dataTransfer.files)\n }\n }\n },\n [addFiles, multiple]\n )\n\n const handleFileChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files && e.target.files.length > 0) {\n addFiles(e.target.files)\n }\n },\n [addFiles]\n )\n\n const openFileDialog = useCallback(() => {\n if (inputRef.current) {\n inputRef.current.click()\n }\n }, [])\n\n const getInputProps = useCallback(\n (props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n return {\n ...props,\n type: \"file\" as const,\n onChange: handleFileChange,\n accept: props.accept || accept,\n multiple: props.multiple !== undefined ? props.multiple : multiple,\n // Cast to `any` to prevent mismatched React ref type errors across workspaces\n ref: inputRef as any,\n }\n },\n [accept, multiple, handleFileChange]\n )\n\n return [\n state,\n {\n addFiles,\n removeFile,\n clearFiles,\n clearErrors,\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n handleFileChange,\n openFileDialog,\n getInputProps,\n },\n ]\n}\n\n// Helper function to format bytes to human-readable format\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n if (bytes === 0) return \"0 Bytes\"\n\n const k = 1024\n const dm = decimals < 0 ? 0 : decimals\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n\n return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]\n}\n",
14
+ "type": "registry:hook"
15
+ }
16
+ ],
17
+ "meta": {
18
+ "tags": [
19
+ "upload",
20
+ "file",
21
+ "image",
22
+ "drag and drop"
23
+ ],
24
+ "colSpan": 2
25
+ }
26
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "comp-545",
4
+ "type": "registry:component",
5
+ "registryDependencies": [
6
+ "https://loveui.dev/building-blocks/r/button.json"
7
+ ],
8
+ "files": [
9
+ {
10
+ "path": "registry/default/components/comp-545.tsx",
11
+ "content": "\"use client\"\n\nimport { AlertCircleIcon, ImageIcon, UploadIcon, XIcon } from \"lucide-react\"\n\nimport { useFileUpload } from \"@/registry/building-blocks/default/hooks/use-file-upload\"\nimport { Button } from \"@/registry/building-blocks/default/ui/button\"\n\nexport default function Component() {\n const maxSizeMB = 2\n const maxSize = maxSizeMB * 1024 * 1024 // 2MB default\n\n const [\n { files, isDragging, errors },\n {\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n openFileDialog,\n removeFile,\n getInputProps,\n },\n ] = useFileUpload({\n accept: \"image/svg+xml,image/png,image/jpeg,image/jpg,image/gif\",\n maxSize,\n })\n const previewUrl = files[0]?.preview || null\n const fileName = files[0]?.file.name || null\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div className=\"relative\">\n {/* Drop area */}\n <div\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n data-dragging={isDragging || undefined}\n className=\"relative flex min-h-52 flex-col items-center justify-center overflow-hidden rounded-xl border border-dashed border-input p-4 transition-colors has-[input:focus]:border-ring has-[input:focus]:ring-[3px] has-[input:focus]:ring-ring/50 data-[dragging=true]:bg-accent/50\"\n >\n <input\n {...getInputProps()}\n className=\"sr-only\"\n aria-label=\"Upload image file\"\n />\n {previewUrl ? (\n <div className=\"absolute inset-0 flex items-center justify-center p-4\">\n <img\n src={previewUrl}\n alt={files[0]?.file?.name || \"Uploaded image\"}\n className=\"mx-auto max-h-full rounded object-contain\"\n />\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center px-4 py-3 text-center\">\n <div\n className=\"mb-2 flex size-11 shrink-0 items-center justify-center rounded-full border bg-background\"\n aria-hidden=\"true\"\n >\n <ImageIcon className=\"size-4 opacity-60\" />\n </div>\n <p className=\"mb-1.5 text-sm font-medium\">Drop your image here</p>\n <p className=\"text-xs text-muted-foreground\">\n SVG, PNG, JPG or GIF (max. {maxSizeMB}MB)\n </p>\n <Button\n variant=\"outline\"\n className=\"mt-4\"\n onClick={openFileDialog}\n >\n <UploadIcon\n className=\"-ms-1 size-4 opacity-60\"\n aria-hidden=\"true\"\n />\n Select image\n </Button>\n </div>\n )}\n </div>\n\n {previewUrl && (\n <div className=\"absolute top-4 right-4\">\n <button\n type=\"button\"\n className=\"z-50 flex size-8 cursor-pointer items-center justify-center rounded-full bg-black/60 text-white transition-[color,box-shadow] outline-none hover:bg-black/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n onClick={() => removeFile(files[0]?.id)}\n aria-label=\"Remove image\"\n >\n <XIcon className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n </div>\n\n {errors.length > 0 && (\n <div\n className=\"flex items-center gap-1 text-xs text-destructive\"\n role=\"alert\"\n >\n <AlertCircleIcon className=\"size-3 shrink-0\" />\n <span>{errors[0]}</span>\n </div>\n )}\n\n <p\n aria-live=\"polite\"\n role=\"region\"\n className=\"mt-2 text-center text-xs text-muted-foreground\"\n >\n Single image uploader w/ max size (drop area + button) ∙{\" \"}\n <a\n href=\"https://github.com/loveui/blob/main/apps/origin/docs/use-file-upload.md\"\n className=\"underline hover:text-foreground\"\n >\n API\n </a>\n </p>\n </div>\n )\n}\n",
12
+ "type": "registry:component"
13
+ },
14
+ {
15
+ "path": "registry/default/hooks/use-file-upload.ts",
16
+ "content": "\"use client\"\n\nimport type React from \"react\"\nimport {\n useCallback,\n useRef,\n useState,\n type ChangeEvent,\n type DragEvent,\n type InputHTMLAttributes,\n} from \"react\"\n\nexport type FileMetadata = {\n name: string\n size: number\n type: string\n url: string\n id: string\n}\n\nexport type FileWithPreview = {\n file: File | FileMetadata\n id: string\n preview?: string\n}\n\nexport type FileUploadOptions = {\n maxFiles?: number // Only used when multiple is true, defaults to Infinity\n maxSize?: number // in bytes\n accept?: string\n multiple?: boolean // Defaults to false\n initialFiles?: FileMetadata[]\n onFilesChange?: (files: FileWithPreview[]) => void // Callback when files change\n onFilesAdded?: (addedFiles: FileWithPreview[]) => void // Callback when new files are added\n}\n\nexport type FileUploadState = {\n files: FileWithPreview[]\n isDragging: boolean\n errors: string[]\n}\n\nexport type FileUploadActions = {\n addFiles: (files: FileList | File[]) => void\n removeFile: (id: string) => void\n clearFiles: () => void\n clearErrors: () => void\n handleDragEnter: (e: DragEvent<HTMLElement>) => void\n handleDragLeave: (e: DragEvent<HTMLElement>) => void\n handleDragOver: (e: DragEvent<HTMLElement>) => void\n handleDrop: (e: DragEvent<HTMLElement>) => void\n handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void\n openFileDialog: () => void\n getInputProps: (\n props?: InputHTMLAttributes<HTMLInputElement>\n ) => InputHTMLAttributes<HTMLInputElement> & {\n // Use `any` here to avoid cross-React ref type conflicts across packages\n ref: any\n }\n}\n\nexport const useFileUpload = (\n options: FileUploadOptions = {}\n): [FileUploadState, FileUploadActions] => {\n const {\n maxFiles = Infinity,\n maxSize = Infinity,\n accept = \"*\",\n multiple = false,\n initialFiles = [],\n onFilesChange,\n onFilesAdded,\n } = options\n\n const [state, setState] = useState<FileUploadState>({\n files: initialFiles.map((file) => ({\n file,\n id: file.id,\n preview: file.url,\n })),\n isDragging: false,\n errors: [],\n })\n\n const inputRef = useRef<HTMLInputElement>(null)\n\n const validateFile = useCallback(\n (file: File | FileMetadata): string | null => {\n if (file instanceof File) {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n } else {\n if (file.size > maxSize) {\n return `File \"${file.name}\" exceeds the maximum size of ${formatBytes(maxSize)}.`\n }\n }\n\n if (accept !== \"*\") {\n const acceptedTypes = accept.split(\",\").map((type) => type.trim())\n const fileType = file instanceof File ? file.type || \"\" : file.type\n const fileExtension = `.${file instanceof File ? file.name.split(\".\").pop() : file.name.split(\".\").pop()}`\n\n const isAccepted = acceptedTypes.some((type) => {\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase()\n }\n if (type.endsWith(\"/*\")) {\n const baseType = type.split(\"/\")[0]\n return fileType.startsWith(`${baseType}/`)\n }\n return fileType === type\n })\n\n if (!isAccepted) {\n return `File \"${file instanceof File ? file.name : file.name}\" is not an accepted file type.`\n }\n }\n\n return null\n },\n [accept, maxSize]\n )\n\n const createPreview = useCallback(\n (file: File | FileMetadata): string | undefined => {\n if (file instanceof File) {\n return URL.createObjectURL(file)\n }\n return file.url\n },\n []\n )\n\n const generateUniqueId = useCallback((file: File | FileMetadata): string => {\n if (file instanceof File) {\n return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`\n }\n return file.id\n }, [])\n\n const clearFiles = useCallback(() => {\n setState((prev) => {\n // Clean up object URLs\n prev.files.forEach((file) => {\n if (\n file.preview &&\n file.file instanceof File &&\n file.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(file.preview)\n }\n })\n\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n\n const newState = {\n ...prev,\n files: [],\n errors: [],\n }\n\n onFilesChange?.(newState.files)\n return newState\n })\n }, [onFilesChange])\n\n const addFiles = useCallback(\n (newFiles: FileList | File[]) => {\n if (!newFiles || newFiles.length === 0) return\n\n const newFilesArray = Array.from(newFiles)\n const errors: string[] = []\n\n // Clear existing errors when new files are uploaded\n setState((prev) => ({ ...prev, errors: [] }))\n\n // In single file mode, clear existing files first\n if (!multiple) {\n clearFiles()\n }\n\n // Check if adding these files would exceed maxFiles (only in multiple mode)\n if (\n multiple &&\n maxFiles !== Infinity &&\n state.files.length + newFilesArray.length > maxFiles\n ) {\n errors.push(`You can only upload a maximum of ${maxFiles} files.`)\n setState((prev) => ({ ...prev, errors }))\n return\n }\n\n const validFiles: FileWithPreview[] = []\n\n newFilesArray.forEach((file) => {\n // Only check for duplicates if multiple files are allowed\n if (multiple) {\n const isDuplicate = state.files.some(\n (existingFile) =>\n existingFile.file.name === file.name &&\n existingFile.file.size === file.size\n )\n\n // Skip duplicate files silently\n if (isDuplicate) {\n return\n }\n }\n\n // Check file size\n if (file.size > maxSize) {\n errors.push(\n multiple\n ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`\n : `File exceeds the maximum size of ${formatBytes(maxSize)}.`\n )\n return\n }\n\n const error = validateFile(file)\n if (error) {\n errors.push(error)\n } else {\n validFiles.push({\n file,\n id: generateUniqueId(file),\n preview: createPreview(file),\n })\n }\n })\n\n // Only update state if we have valid files to add\n if (validFiles.length > 0) {\n // Call the onFilesAdded callback with the newly added valid files\n onFilesAdded?.(validFiles)\n\n setState((prev) => {\n const newFiles = !multiple\n ? validFiles\n : [...prev.files, ...validFiles]\n onFilesChange?.(newFiles)\n return {\n ...prev,\n files: newFiles,\n errors,\n }\n })\n } else if (errors.length > 0) {\n setState((prev) => ({\n ...prev,\n errors,\n }))\n }\n\n // Reset input value after handling files\n if (inputRef.current) {\n inputRef.current.value = \"\"\n }\n },\n [\n state.files,\n maxFiles,\n multiple,\n maxSize,\n validateFile,\n createPreview,\n generateUniqueId,\n clearFiles,\n onFilesChange,\n onFilesAdded,\n ]\n )\n\n const removeFile = useCallback(\n (id: string) => {\n setState((prev) => {\n const fileToRemove = prev.files.find((file) => file.id === id)\n if (\n fileToRemove &&\n fileToRemove.preview &&\n fileToRemove.file instanceof File &&\n fileToRemove.file.type.startsWith(\"image/\")\n ) {\n URL.revokeObjectURL(fileToRemove.preview)\n }\n\n const newFiles = prev.files.filter((file) => file.id !== id)\n onFilesChange?.(newFiles)\n\n return {\n ...prev,\n files: newFiles,\n errors: [],\n }\n })\n },\n [onFilesChange]\n )\n\n const clearErrors = useCallback(() => {\n setState((prev) => ({\n ...prev,\n errors: [],\n }))\n }, [])\n\n const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: true }))\n }, [])\n\n const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n\n if (e.currentTarget.contains(e.relatedTarget as Node)) {\n return\n }\n\n setState((prev) => ({ ...prev, isDragging: false }))\n }, [])\n\n const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n }, [])\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLElement>) => {\n e.preventDefault()\n e.stopPropagation()\n setState((prev) => ({ ...prev, isDragging: false }))\n\n // Don't process files if the input is disabled\n if (inputRef.current?.disabled) {\n return\n }\n\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n // In single file mode, only use the first file\n if (!multiple) {\n const file = e.dataTransfer.files[0]\n addFiles([file])\n } else {\n addFiles(e.dataTransfer.files)\n }\n }\n },\n [addFiles, multiple]\n )\n\n const handleFileChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files && e.target.files.length > 0) {\n addFiles(e.target.files)\n }\n },\n [addFiles]\n )\n\n const openFileDialog = useCallback(() => {\n if (inputRef.current) {\n inputRef.current.click()\n }\n }, [])\n\n const getInputProps = useCallback(\n (props: InputHTMLAttributes<HTMLInputElement> = {}) => {\n return {\n ...props,\n type: \"file\" as const,\n onChange: handleFileChange,\n accept: props.accept || accept,\n multiple: props.multiple !== undefined ? props.multiple : multiple,\n // Cast to `any` to prevent mismatched React ref type errors across workspaces\n ref: inputRef as any,\n }\n },\n [accept, multiple, handleFileChange]\n )\n\n return [\n state,\n {\n addFiles,\n removeFile,\n clearFiles,\n clearErrors,\n handleDragEnter,\n handleDragLeave,\n handleDragOver,\n handleDrop,\n handleFileChange,\n openFileDialog,\n getInputProps,\n },\n ]\n}\n\n// Helper function to format bytes to human-readable format\nexport const formatBytes = (bytes: number, decimals = 2): string => {\n if (bytes === 0) return \"0 Bytes\"\n\n const k = 1024\n const dm = decimals < 0 ? 0 : decimals\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n\n return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]\n}\n",
17
+ "type": "registry:hook"
18
+ }
19
+ ],
20
+ "meta": {
21
+ "tags": [
22
+ "upload",
23
+ "file",
24
+ "image",
25
+ "drag and drop"
26
+ ],
27
+ "colSpan": 2
28
+ }
29
+ }