@mova-ui/react 0.4.1 → 0.4.5

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 (478) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +483 -830
  3. package/dist/styles.css +116 -1
  4. package/dist/ui/ai/ai-context-meter.d.ts +2 -1
  5. package/dist/ui/ai/ai-sources.d.ts +2 -1
  6. package/dist/ui/ai/ai-task-list.d.ts +2 -1
  7. package/dist/ui/ai/ai-tool-call.d.ts +2 -1
  8. package/package.json +1 -1
  9. package/registry/1.json +1 -1
  10. package/registry/10.json +1 -1
  11. package/registry/11.json +1 -1
  12. package/registry/12.json +1 -1
  13. package/registry/13.json +1 -1
  14. package/registry/14.json +1 -1
  15. package/registry/15.json +1 -1
  16. package/registry/16.json +1 -1
  17. package/registry/17.json +1 -1
  18. package/registry/18.json +1 -1
  19. package/registry/19.json +1 -1
  20. package/registry/2.json +1 -1
  21. package/registry/20.json +1 -1
  22. package/registry/21.json +1 -1
  23. package/registry/22.json +1 -1
  24. package/registry/23.json +1 -1
  25. package/registry/24.json +1 -1
  26. package/registry/25.json +1 -1
  27. package/registry/26.json +1 -1
  28. package/registry/27.json +1 -1
  29. package/registry/28.json +1 -1
  30. package/registry/29.json +1 -1
  31. package/registry/3.json +1 -1
  32. package/registry/30.json +1 -1
  33. package/registry/31.json +1 -1
  34. package/registry/32.json +1 -1
  35. package/registry/33.json +1 -1
  36. package/registry/34.json +1 -1
  37. package/registry/35.json +1 -1
  38. package/registry/36.json +1 -1
  39. package/registry/37.json +1 -1
  40. package/registry/4.json +1 -1
  41. package/registry/5.json +1 -1
  42. package/registry/6.json +1 -1
  43. package/registry/7.json +1 -1
  44. package/registry/8.json +1 -1
  45. package/registry/9.json +1 -1
  46. package/registry/ab-segmented-tabs.json +1 -1
  47. package/registry/accordion-blind.json +1 -1
  48. package/registry/accordion-loader.json +1 -1
  49. package/registry/ai-approval-card.json +2 -2
  50. package/registry/ai-code-block.json +2 -2
  51. package/registry/ai-context-meter.json +2 -2
  52. package/registry/ai-fine-tune-card.json +2 -2
  53. package/registry/ai-prompt-bar.json +2 -2
  54. package/registry/ai-sidebar-nav.json +2 -2
  55. package/registry/ai-sources.json +2 -2
  56. package/registry/ai-suggestions.json +1 -1
  57. package/registry/ai-task-list.json +2 -2
  58. package/registry/ai-tool-call.json +2 -2
  59. package/registry/analyzing-image.json +1 -1
  60. package/registry/animated-sidebar.json +1 -1
  61. package/registry/animated-tags.json +1 -1
  62. package/registry/app-icon-load.json +1 -1
  63. package/registry/apple-breathe.json +1 -1
  64. package/registry/apple-equalizer.json +1 -1
  65. package/registry/apple-icon-morph.json +1 -1
  66. package/registry/apple-pulse-dots.json +1 -1
  67. package/registry/apple-scale-pulse.json +1 -1
  68. package/registry/apple-sound-wave.json +1 -1
  69. package/registry/apple-text-reveal.json +1 -1
  70. package/registry/apple-unlock.json +1 -1
  71. package/registry/arc-tracer.json +1 -1
  72. package/registry/bar-cascade.json +1 -1
  73. package/registry/bar-sweep.json +1 -1
  74. package/registry/blur-text.json +1 -1
  75. package/registry/blur-up-char.json +1 -1
  76. package/registry/blur-up-word.json +1 -1
  77. package/registry/bobbing-dots.json +1 -1
  78. package/registry/bookmark-corner-peel.json +1 -1
  79. package/registry/bookmark-save-pill.json +1 -1
  80. package/registry/bookmark-toggle.json +1 -1
  81. package/registry/border-trail-loading.json +1 -1
  82. package/registry/bounce-dots.json +1 -1
  83. package/registry/bounce-in-char.json +1 -1
  84. package/registry/bouncing-bars.json +1 -1
  85. package/registry/bouncing-dots.json +1 -1
  86. package/registry/bouncing-lines.json +1 -1
  87. package/registry/bouncing-square.json +1 -1
  88. package/registry/breathe-ring.json +1 -1
  89. package/registry/breathing-glow.json +1 -1
  90. package/registry/breathing-square.json +1 -1
  91. package/registry/btn-1.json +1 -1
  92. package/registry/btn-10.json +1 -1
  93. package/registry/btn-11.json +1 -1
  94. package/registry/btn-12.json +1 -1
  95. package/registry/btn-13.json +1 -1
  96. package/registry/btn-14.json +1 -1
  97. package/registry/btn-15.json +1 -1
  98. package/registry/btn-16.json +1 -1
  99. package/registry/btn-17.json +1 -1
  100. package/registry/btn-18.json +1 -1
  101. package/registry/btn-19.json +1 -1
  102. package/registry/btn-2.json +1 -1
  103. package/registry/btn-20.json +1 -1
  104. package/registry/btn-21.json +1 -1
  105. package/registry/btn-22.json +1 -1
  106. package/registry/btn-23.json +1 -1
  107. package/registry/btn-24.json +1 -1
  108. package/registry/btn-25.json +1 -1
  109. package/registry/btn-26.json +1 -1
  110. package/registry/btn-27.json +1 -1
  111. package/registry/btn-28.json +1 -1
  112. package/registry/btn-29.json +1 -1
  113. package/registry/btn-3.json +1 -1
  114. package/registry/btn-30.json +1 -1
  115. package/registry/btn-31.json +1 -1
  116. package/registry/btn-32.json +1 -1
  117. package/registry/btn-33.json +1 -1
  118. package/registry/btn-34.json +1 -1
  119. package/registry/btn-35.json +1 -1
  120. package/registry/btn-36.json +1 -1
  121. package/registry/btn-37.json +1 -1
  122. package/registry/btn-4.json +1 -1
  123. package/registry/btn-5.json +1 -1
  124. package/registry/btn-6.json +1 -1
  125. package/registry/btn-7.json +1 -1
  126. package/registry/btn-8.json +1 -1
  127. package/registry/btn-9.json +1 -1
  128. package/registry/button-slide-arrow.json +1 -1
  129. package/registry/cacheable-bandwidth-cost.json +1 -1
  130. package/registry/card-arc-5.json +1 -1
  131. package/registry/card-arc-7.json +1 -1
  132. package/registry/card-carousel-mono.json +1 -1
  133. package/registry/card-carousel.json +1 -1
  134. package/registry/card-cascade-stagger.json +1 -1
  135. package/registry/card-corner-fan.json +1 -1
  136. package/registry/card-cover-flow-mono.json +1 -1
  137. package/registry/card-cover-flow.json +1 -1
  138. package/registry/card-deck-cascade.json +1 -1
  139. package/registry/card-glance-preview.json +1 -1
  140. package/registry/card-hover.json +1 -1
  141. package/registry/card-linear-spread.json +1 -1
  142. package/registry/card-long-arc-5.json +1 -1
  143. package/registry/card-scatter-spread.json +1 -1
  144. package/registry/card-stack-peel.json +1 -1
  145. package/registry/card-stamp-arc.json +1 -1
  146. package/registry/card-time-machine-mono.json +1 -1
  147. package/registry/card-time-machine.json +1 -1
  148. package/registry/card-wheel-fan.json +1 -1
  149. package/registry/category-select.json +1 -1
  150. package/registry/character-stagger.json +1 -1
  151. package/registry/checkbox-draw.json +1 -1
  152. package/registry/checkmark-draw.json +1 -1
  153. package/registry/circle-toggle.json +1 -1
  154. package/registry/circuit-trace-draw.json +1 -1
  155. package/registry/circular-bars.json +1 -1
  156. package/registry/circular-create-button.json +1 -1
  157. package/registry/classic-spinner.json +1 -1
  158. package/registry/classic-toggle.json +1 -1
  159. package/registry/clock-spinner.json +1 -1
  160. package/registry/comet-spinner.json +1 -1
  161. package/registry/compass-needle-deflect.json +1 -1
  162. package/registry/concentric-pulse.json +1 -1
  163. package/registry/concentric-ring.json +1 -1
  164. package/registry/concentric-squares.json +1 -1
  165. package/registry/context-menu.json +1 -1
  166. package/registry/conveyor-loop.json +1 -1
  167. package/registry/credit-score-barcode-meter.json +1 -1
  168. package/registry/cross-spinner.json +1 -1
  169. package/registry/cube-flip-spring.json +1 -1
  170. package/registry/cursor-trail.json +1 -1
  171. package/registry/dash-ring.json +1 -1
  172. package/registry/dashed-spiral.json +1 -1
  173. package/registry/date-position-selector.json +1 -1
  174. package/registry/dia-text-reveal.json +1 -1
  175. package/registry/diamond-grid.json +1 -1
  176. package/registry/diamond-rotate-spring.json +1 -1
  177. package/registry/dislike-toggle.json +1 -1
  178. package/registry/dither-book.json +1 -1
  179. package/registry/domino-cascade.json +1 -1
  180. package/registry/dot-spinner.json +1 -1
  181. package/registry/dots-ring.json +1 -1
  182. package/registry/double-bounce-toggle.json +1 -1
  183. package/registry/double-ring.json +1 -1
  184. package/registry/download-icons.json +1 -1
  185. package/registry/drop-dot.json +1 -1
  186. package/registry/drop-in-char.json +1 -1
  187. package/registry/droplet-squish.json +1 -1
  188. package/registry/dual-arc.json +1 -1
  189. package/registry/dual-magnet-dipole.json +1 -1
  190. package/registry/dynamic-island.json +1 -1
  191. package/registry/elastic-bars.json +1 -1
  192. package/registry/elastic-square.json +1 -1
  193. package/registry/elastic-tag-snap.json +1 -1
  194. package/registry/error-shake.json +1 -1
  195. package/registry/expanding-cross.json +1 -1
  196. package/registry/expanding-icon-tabs.json +1 -1
  197. package/registry/face-id-scan.json +1 -1
  198. package/registry/fade-arc.json +1 -1
  199. package/registry/fade-dots.json +1 -1
  200. package/registry/fade-down.json +1 -1
  201. package/registry/fade-in-char.json +1 -1
  202. package/registry/fade-in-text.json +1 -1
  203. package/registry/fade-in-word.json +1 -1
  204. package/registry/fade-in.json +1 -1
  205. package/registry/fade-up.json +1 -1
  206. package/registry/file-upload-dropzone.json +1 -1
  207. package/registry/filter-tag-pill.json +1 -1
  208. package/registry/flip-square.json +1 -1
  209. package/registry/flip-x-char.json +1 -1
  210. package/registry/flip-y-char.json +1 -1
  211. package/registry/float-char.json +1 -1
  212. package/registry/float-word.json +1 -1
  213. package/registry/floating-diamonds.json +1 -1
  214. package/registry/floating-label-input.json +1 -1
  215. package/registry/fluid-bars.json +1 -1
  216. package/registry/fluid-diamond.json +1 -1
  217. package/registry/fluid-dot-orbit.json +1 -1
  218. package/registry/fluid-skeleton.json +1 -1
  219. package/registry/focus-blur.json +1 -1
  220. package/registry/form-submit-button.json +1 -1
  221. package/registry/french-doors-3d.json +1 -1
  222. package/registry/gear-tooth-step.json +1 -1
  223. package/registry/gears.json +1 -1
  224. package/registry/gelatin-wobble.json +1 -1
  225. package/registry/geometric-iris-shutter.json +1 -1
  226. package/registry/glassmorphic-card.json +1 -1
  227. package/registry/glow-button.json +1 -1
  228. package/registry/glow-text.json +1 -1
  229. package/registry/gooey-popover.json +1 -1
  230. package/registry/gradient-arc.json +1 -1
  231. package/registry/gradient-shimmer.json +1 -1
  232. package/registry/grid-dots.json +1 -1
  233. package/registry/haptic-ring.json +1 -1
  234. package/registry/heartbeat.json +1 -1
  235. package/registry/hex-lattice-draw.json +1 -1
  236. package/registry/hexagon-spinner.json +1 -1
  237. package/registry/hourglass.json +1 -1
  238. package/registry/hover-lift-char.json +1 -1
  239. package/registry/hover-lift-word.json +1 -1
  240. package/registry/hover-link-card.json +1 -1
  241. package/registry/hover-scale-char.json +1 -1
  242. package/registry/hover-scale-word.json +1 -1
  243. package/registry/inertia-skid-stop.json +1 -1
  244. package/registry/infinity-path.json +1 -1
  245. package/registry/input-focus-glow.json +1 -1
  246. package/registry/inset-action-button.json +1 -1
  247. package/registry/inset-button.json +1 -1
  248. package/registry/intersecting-rings.json +1 -1
  249. package/registry/ios-spinner.json +1 -1
  250. package/registry/kinetic-metronome-tick.json +1 -1
  251. package/registry/kinetic-tension-capsule.json +1 -1
  252. package/registry/light-dark-morph.json +1 -1
  253. package/registry/like-toggle.json +1 -1
  254. package/registry/line-spinner.json +1 -1
  255. package/registry/liquid-dots.json +1 -1
  256. package/registry/mac-terminal.json +1 -1
  257. package/registry/magnetic-button.json +1 -1
  258. package/registry/magnetic-disks.json +1 -1
  259. package/registry/magnetic-dots.json +1 -1
  260. package/registry/magnetic-slide-btn.json +1 -1
  261. package/registry/manifest.json +656 -650
  262. package/registry/matrix-grid-loader.json +1 -1
  263. package/registry/minimal-triangle.json +1 -1
  264. package/registry/modular-tile-snap.json +1 -1
  265. package/registry/mono-activity-blue.json +1 -1
  266. package/registry/mono-activity-green.json +1 -1
  267. package/registry/mono-activity-purple.json +1 -1
  268. package/registry/mono-heatmap.json +1 -1
  269. package/registry/mono-rounded-area.json +1 -1
  270. package/registry/mono-rounded-bar.json +1 -1
  271. package/registry/mono-rounded-bubble.json +1 -1
  272. package/registry/mono-rounded-bullet.json +1 -1
  273. package/registry/mono-rounded-candlestick.json +1 -1
  274. package/registry/mono-rounded-composed.json +1 -1
  275. package/registry/mono-rounded-donut.json +1 -1
  276. package/registry/mono-rounded-funnel.json +1 -1
  277. package/registry/mono-rounded-gauge-arc.json +1 -1
  278. package/registry/mono-rounded-heatmap.json +1 -1
  279. package/registry/mono-rounded-kpi.json +1 -1
  280. package/registry/mono-rounded-line.json +1 -1
  281. package/registry/mono-rounded-meter.json +1 -1
  282. package/registry/mono-rounded-polar.json +1 -1
  283. package/registry/mono-rounded-pyramid.json +1 -1
  284. package/registry/mono-rounded-radar.json +1 -1
  285. package/registry/mono-rounded-radial-gauge.json +1 -1
  286. package/registry/mono-rounded-radial-group.json +1 -1
  287. package/registry/mono-rounded-range.json +1 -1
  288. package/registry/mono-rounded-sankey.json +1 -1
  289. package/registry/mono-rounded-scatter.json +1 -1
  290. package/registry/mono-rounded-sparkline.json +1 -1
  291. package/registry/mono-rounded-stacked-bar.json +1 -1
  292. package/registry/mono-rounded-step.json +1 -1
  293. package/registry/mono-rounded-stream.json +1 -1
  294. package/registry/mono-rounded-treemap.json +1 -1
  295. package/registry/mono-rounded-waterfall.json +1 -1
  296. package/registry/mono-stock.json +1 -1
  297. package/registry/morph-action-pill.json +1 -1
  298. package/registry/morph-dot-ring.json +1 -1
  299. package/registry/morph-loader.json +1 -1
  300. package/registry/morph-toggle.json +1 -1
  301. package/registry/morphing-bars.json +1 -1
  302. package/registry/morphing-infinity.json +1 -1
  303. package/registry/morphing-ring.json +1 -1
  304. package/registry/morphing-shape.json +1 -1
  305. package/registry/mouse-follow.json +1 -1
  306. package/registry/multi-select-chips.json +1 -1
  307. package/registry/multi-tab-bar.json +1 -1
  308. package/registry/neon-sign-draw.json +1 -1
  309. package/registry/nested-orbital-gimbal.json +1 -1
  310. package/registry/network-telemetry-matrix.json +1 -1
  311. package/registry/newtons-cradle.json +1 -1
  312. package/registry/noise-decibel-level.json +1 -1
  313. package/registry/obsidian-liquid-wave.json +1 -1
  314. package/registry/offset-rings.json +1 -1
  315. package/registry/orbiting-circles.json +1 -1
  316. package/registry/orbiting-dot.json +1 -1
  317. package/registry/origami-envelope-unfold.json +1 -1
  318. package/registry/origami-shape.json +1 -1
  319. package/registry/otp-input.json +1 -1
  320. package/registry/overview-bar-scrubber-card.json +1 -1
  321. package/registry/page-transition.json +1 -1
  322. package/registry/page-turn-curl.json +1 -1
  323. package/registry/password-toggle.json +1 -1
  324. package/registry/pendulum-bubble-level.json +1 -1
  325. package/registry/pendulum.json +1 -1
  326. package/registry/perspective-flip-stage.json +1 -1
  327. package/registry/perspective-flip.json +1 -1
  328. package/registry/perspective-layout-switcher.json +1 -1
  329. package/registry/physics-dock.json +1 -1
  330. package/registry/pill-tabs.json +1 -1
  331. package/registry/plus-minus-toggle.json +1 -1
  332. package/registry/presets.json +1 -1
  333. package/registry/prism-block-stack.json +1 -1
  334. package/registry/progress-indicator-piano.json +1 -1
  335. package/registry/progress-indicator.json +1 -1
  336. package/registry/progress-stepper.json +1 -1
  337. package/registry/pulsating-dots.json +1 -1
  338. package/registry/pulse-char.json +1 -1
  339. package/registry/pulse-dot.json +1 -1
  340. package/registry/pulse-dots.json +1 -1
  341. package/registry/pulse-square.json +1 -1
  342. package/registry/pulse-word.json +1 -1
  343. package/registry/pulse.json +1 -1
  344. package/registry/pumping-heart.json +1 -1
  345. package/registry/pyramid-block-build.json +1 -1
  346. package/registry/radar-sweep.json +1 -1
  347. package/registry/radial-iris-mask.json +1 -1
  348. package/registry/radial-spinner.json +1 -1
  349. package/registry/radio-scale.json +1 -1
  350. package/registry/range-slider.json +1 -1
  351. package/registry/receipt-tape-print.json +1 -1
  352. package/registry/rectangle-toggle.json +1 -1
  353. package/registry/repost-toggle.json +1 -1
  354. package/registry/reveal-text.json +1 -1
  355. package/registry/ribbon-banner-slide.json +1 -1
  356. package/registry/ring-sweep.json +1 -1
  357. package/registry/ripple-effect.json +1 -1
  358. package/registry/rise-up-word.json +1 -1
  359. package/registry/roller-blind-drop.json +1 -1
  360. package/registry/rolling-tumble.json +1 -1
  361. package/registry/rotate-in-char.json +1 -1
  362. package/registry/rotating-cross.json +1 -1
  363. package/registry/rotating-louvers.json +1 -1
  364. package/registry/rotating-triangle.json +1 -1
  365. package/registry/rudder-issue-board.json +2 -2
  366. package/registry/sales-analytics-dual-bars.json +1 -1
  367. package/registry/sales-overview-radial-dashboard.json +1 -1
  368. package/registry/sales-target-segmented-arc.json +1 -1
  369. package/registry/scale-in-char.json +1 -1
  370. package/registry/scale-in-text.json +1 -1
  371. package/registry/scale-in-word.json +1 -1
  372. package/registry/scale-in.json +1 -1
  373. package/registry/scramble-text.json +1 -1
  374. package/registry/scroll-canvas-unroll.json +1 -1
  375. package/registry/scroll-reveal.json +1 -1
  376. package/registry/search-expand.json +1 -1
  377. package/registry/segmented-arc-meter.json +1 -1
  378. package/registry/segmented-link-stretch.json +1 -1
  379. package/registry/segmented-step-bar.json +1 -1
  380. package/registry/segmented-stepper-dots.json +1 -1
  381. package/registry/select-dropdown.json +1 -1
  382. package/registry/server-performance-step-bars.json +1 -1
  383. package/registry/shape-shift-grid.json +1 -1
  384. package/registry/shimmer-line.json +1 -1
  385. package/registry/shimmer-text.json +1 -1
  386. package/registry/shutter-slide.json +1 -1
  387. package/registry/shutter-step-slices.json +1 -1
  388. package/registry/siri-wave.json +1 -1
  389. package/registry/skeleton-loader.json +1 -1
  390. package/registry/skeleton.json +1 -1
  391. package/registry/skew-x-char.json +1 -1
  392. package/registry/slide-down-char.json +1 -1
  393. package/registry/slide-down-word.json +1 -1
  394. package/registry/slide-left-char.json +1 -1
  395. package/registry/slide-left.json +1 -1
  396. package/registry/slide-right-char.json +1 -1
  397. package/registry/slide-right.json +1 -1
  398. package/registry/slide-up-char.json +1 -1
  399. package/registry/slide-up-text.json +1 -1
  400. package/registry/slide-up-word.json +1 -1
  401. package/registry/sliding-bars.json +1 -1
  402. package/registry/slinky-coil.json +1 -1
  403. package/registry/smart-card-dispenser.json +1 -1
  404. package/registry/smooth-dot-shift.json +1 -1
  405. package/registry/smooth-ring.json +1 -1
  406. package/registry/smooth-rounded-square.json +1 -1
  407. package/registry/solid-switch.json +1 -1
  408. package/registry/spatial-door-portal.json +1 -1
  409. package/registry/spinning-squares.json +1 -1
  410. package/registry/spiral-spinner.json +1 -1
  411. package/registry/split-gate-reveal.json +1 -1
  412. package/registry/spotlight.json +1 -1
  413. package/registry/spring-bars.json +1 -1
  414. package/registry/spring-dot-matrix.json +1 -1
  415. package/registry/spring-hexagon.json +1 -1
  416. package/registry/spring-ring-expand.json +1 -1
  417. package/registry/spring-text-pop.json +1 -1
  418. package/registry/spring-text.json +1 -1
  419. package/registry/square-accordion.json +1 -1
  420. package/registry/square-grid.json +1 -1
  421. package/registry/square-snake.json +1 -1
  422. package/registry/square-spinner.json +1 -1
  423. package/registry/squash-sphere.json +1 -1
  424. package/registry/stacked-bar-pulse.json +1 -1
  425. package/registry/stagger-text.json +1 -1
  426. package/registry/staggered-glass-curtain.json +1 -1
  427. package/registry/sticky-note-peel.json +1 -1
  428. package/registry/sticky-reveal.json +1 -1
  429. package/registry/stretch-x-char.json +1 -1
  430. package/registry/stretch-y-char.json +1 -1
  431. package/registry/stroke-waveform.json +1 -1
  432. package/registry/submenu-flyout.json +1 -1
  433. package/registry/success-check.json +1 -1
  434. package/registry/sudden-brake.json +1 -1
  435. package/registry/swapping-dots.json +1 -1
  436. package/registry/swing-word.json +1 -1
  437. package/registry/swirling-spinner.json +1 -1
  438. package/registry/symmetric-wave.json +1 -1
  439. package/registry/terminal-loader.json +1 -1
  440. package/registry/text-blink.json +1 -1
  441. package/registry/text-dots.json +1 -1
  442. package/registry/text-morph.json +1 -1
  443. package/registry/text-reveal.json +1 -1
  444. package/registry/text-shimmer-wave.json +1 -1
  445. package/registry/text-shimmer.json +1 -1
  446. package/registry/textarea-auto-grow.json +1 -1
  447. package/registry/theme-toggle.json +1 -1
  448. package/registry/tilt-card.json +1 -1
  449. package/registry/timer-preparation-segmented.json +1 -1
  450. package/registry/tracking-in-text.json +1 -1
  451. package/registry/tracking-out-text.json +1 -1
  452. package/registry/trailing-dots.json +1 -1
  453. package/registry/triple-dot-spinner.json +1 -1
  454. package/registry/twin-orbit.json +1 -1
  455. package/registry/typewriter-text.json +1 -1
  456. package/registry/typing-indicator.json +1 -1
  457. package/registry/typing.json +1 -1
  458. package/registry/use-mouse-position.json +1 -1
  459. package/registry/use-reduced-motion.json +1 -1
  460. package/registry/use-scroll-progress.json +1 -1
  461. package/registry/use-stagger.json +1 -1
  462. package/registry/use-web-haptics.json +1 -1
  463. package/registry/users-growth-pill-progress.json +1 -1
  464. package/registry/utils.json +1 -1
  465. package/registry/vertical-wheel-counter.json +1 -1
  466. package/registry/views-hourly-wave-chart.json +1 -1
  467. package/registry/wandering-cube.json +1 -1
  468. package/registry/wandering-eyes.json +1 -1
  469. package/registry/watch-spinner.json +1 -1
  470. package/registry/wave-dots.json +1 -1
  471. package/registry/wave-physics-loader.json +1 -1
  472. package/registry/wave-reveal.json +1 -1
  473. package/registry/waveform-loader.json +1 -1
  474. package/registry/word-reveal.json +1 -1
  475. package/registry/zig-zag-pulse.json +1 -1
  476. package/registry/zoom-in-text.json +1 -1
  477. package/registry/zoom-in.json +1 -1
  478. package/registry/zoom-out-text.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-sidebar-nav",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "ai-sidebar-nav.tsx",
6
6
  "sourcePath": "ui/ai/ai-sidebar-nav.tsx",
7
7
  "registryFile": "ai-sidebar-nav.json",
@@ -13,5 +13,5 @@
13
13
  ],
14
14
  "license": "MIT",
15
15
  "previewMode": "client",
16
- "content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Retains the original 224px / 52px rail and gliding navigation motion.\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { LogOut as IconArrowBoxLeft, Check as IconCheckmark1Small, ChevronDown as IconChevronDownSmall, X as IconCrossSmall, SquarePen as IconEditBig, House as IconHome, Search as IconMagnifyingGlass, Plus as IconPlusMedium, IceCream as IconPopsicle2, Settings as IconSettingsGear1, PanelLeftClose as IconSidebarLeftArrow, UserPlus as IconUserAdd } from \"lucide-react\";\n\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst COMPONENT_STYLES = `[data-mova-sidebar] {--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--shadow-lg: 0 25px 50px 0 rgba(0, 0, 0, 0.05), 0 12px 24px 0 rgba(0, 0, 0, 0.04),\n 0 6px 12px 0 rgba(0, 0, 0, 0.03), 0 3px 6px 0 rgba(0, 0, 0, 0.02),\n 0 1.5px 3px 0 rgba(0, 0, 0, 0.02); font-family:Inter,ui-sans-serif,system-ui,sans-serif; font-size:14px;line-height:1.5;letter-spacing:-0.01em;color:var(--ink);\n /* surfaces — cool, blue-tinted neutrals */\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n /* ink ramp */\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n /* borders — solid and crisp, not alpha */\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n /* accent */\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n /* semantic */\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n /* chart overlays — neutral system colors with enough contrast on either theme */\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n /* shadows — our solid hairline ring + shadow-plugin's smooth layered stacks\n * (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads\n * consistently across the app. */\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);}\n[data-mova-sidebar][data-theme=\"dark\"] {\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n /* Elevation rings go to a subtle *light* hairline in dark mode: the ring\n * paints over the surface behind the element, so a low-alpha white edge\n * dissolves into the shadow as one continuous stroke (per smooth-shadow-ring),\n * graduated by how far the surface floats. */\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n[data-mova-sidebar] :is(button,input,textarea):focus-visible { outline:2px solid var(--accent);outline-offset:2px; }\n[data-mova-sidebar] button { cursor:pointer; } [data-mova-sidebar] button:disabled { cursor:default; }\n[data-mova-sidebar] .sidebar-row{\n width: 208px;\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-copy{\n opacity: 1;\n transform: translateX(0);\n transition:\n opacity var(--sidebar-copy-duration) ease-out,\n transform var(--sidebar-copy-duration) var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-logo,\n[data-mova-sidebar] .sidebar-collapse-control,\n[data-mova-sidebar] .sidebar-expand-control{\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: ease-out;\n}\n\n[data-mova-sidebar] .sidebar-expand-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar] .sidebar-glide-highlight{\n right: 8px;\n left: 8px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-row{\n width: 36px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-copy{\n pointer-events: none;\n opacity: 0;\n transform: translateX(calc(var(--sidebar-copy-offset) * -1));\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-workspace-control{\n pointer-events: none;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-logo,\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-collapse-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-expand-control{\n pointer-events: auto;\n opacity: 1;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-glide-highlight{\n right: auto;\n width: 36px;\n}\n\n\n@keyframes mova-sidebar-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-sidebar-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-sidebar], [data-mova-sidebar] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--hover)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* ─────────────────────────────────────────────────────────\n * SIDEBAR NAV\n * Shared by the design-system preview and the harness shell:\n * compact workspace switcher, primary navigation, searchable\n * chat history, and a collapse that preserves icon alignment.\n * ───────────────────────────────────────────────────────── */\n\nconst WORKSPACE = { key: \"creamery\", name: \"Creamery Ops\", monogram: \"C\" };\n\nconst NAV_ITEMS = [\n { key: \"home\", label: \"Home\", icon: <IconHome size={18} /> },\n { key: \"invite\", label: \"Invite users\", icon: <IconUserAdd size={18} />, count: \"3/10\" },\n];\n\ntype SidebarRecent = {\n id: string;\n label: string;\n prompt?: string;\n};\n\nconst DEFAULT_RECENTS: SidebarRecent[] = [\n { id: \"suppliers\", label: \"Supplier records\" },\n { id: \"todos\", label: \"Urgent to-dos this morning\" },\n { id: \"flavor\", label: \"Flavor page ticket\" },\n { id: \"workload\", label: \"Workload summary\" },\n { id: \"offboarding\", label: \"Off-board a supplier\" },\n { id: \"restock\", label: \"Batch restock function\" },\n { id: \"edits\", label: \"Propose flavor edits\" },\n { id: \"subway\", label: \"Subway surfing\" },\n];\n\ntype SidebarNavProps = {\n theme?: \"dark\" | \"light\";\n onWorkspaceAction?: (action: string) => void;\n activeTitle?: string | null;\n className?: string;\n fill?: boolean;\n onNewChat?: () => void;\n onPick?: (id: string, label: string, prompt?: string) => void;\n /** controlled primary-nav selection (e.g. \"home\" | \"invite\") */\n activeNav?: string;\n onNavigate?: (key: string) => void;\n /** footer call-to-action — defaults to the demo \"Upgrade\" button */\n footerLabel?: string;\n footerIcon?: ReactNode;\n onFooterClick?: () => void;\n recents?: SidebarRecent[];\n variant?: string;\n};\n\nconst SIDEBAR_MOTION = {\n expandedWidth: 224,\n collapsedWidth: 52,\n duration: 280,\n copyDuration: 180,\n copyOffset: 8,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\n/* ─────────────────────────────────────────────────────────\n * CHAT SEARCH STORYBOARD\n *\n * 0ms search is triggered; Chats label begins fading\n * 0ms field grows right → left from the search control\n * 180ms field fills the row; cursor is focused and ready\n * ───────────────────────────────────────────────────────── */\nconst CHAT_SEARCH_MOTION = {\n duration: 180,\n closedWidth: 28,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\nfunction GlideGroup({ children }: { children: ReactNode }) {\n return (\n <GlideMenu\n rowSelector=\"[data-row]\"\n highlightClassName=\"sidebar-glide-highlight rounded-[7px] bg-[var(--hover-2)]\"\n className=\"group/glide flex flex-col gap-px\"\n >\n {children}\n </GlideMenu>\n );\n}\n\nfunction RailButton({\n icon,\n label,\n active = false,\n count,\n onClick,\n}: {\n icon: ReactNode;\n label: string;\n active?: boolean;\n count?: string;\n onClick?: () => void;\n}) {\n return (\n <button\n data-row\n aria-label={label}\n aria-current={active ? \"page\" : undefined}\n type=\"button\"\n onClick={onClick}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left\n transition-[width,background-color,color,transform] duration-150 active:scale-[0.98]\n ${active ? \"bg-[var(--hover-2)] group-hover/glide:bg-transparent\" : \"\"}`}\n >\n <span className={`flex size-5 shrink-0 items-center justify-center ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {icon}\n </span>\n <span className={`sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {label}\n </span>\n {count && (\n <span className=\"sidebar-copy mr-2 shrink-0 text-[12px] font-medium tabular-nums text-[var(--ink-3)]\">\n {count}\n </span>\n )}\n </button>\n );\n}\n\nfunction WorkspaceMenu({\n position,\n theme,\n onAction,\n onClose,\n}: {\n theme: \"dark\" | \"light\";\n onAction: (action: string) => void;\n position: { top: number; left: number };\n onClose: () => void;\n}) {\n const menuRef = useRef<HTMLDivElement>(null);\n useEffect(() => { menuRef.current?.querySelector<HTMLButtonElement>(\"button\")?.focus(); }, []);\n return createPortal(\n <div ref={menuRef}\n data-mova-sidebar data-theme={theme}\n data-workspace-menu\n role=\"group\" aria-label=\"Workspace menu\"\n onKeyDown={(event) => { if (event.key === \"Escape\") { event.preventDefault(); onClose(); } if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button\")); const index = buttons.indexOf(document.activeElement as HTMLButtonElement); buttons[(index + (event.key === \"ArrowDown\" ? 1 : buttons.length - 1)) % buttons.length]?.focus(); } }}\n className=\"fixed z-50 w-64 rounded-[14px] bg-[var(--surface)] p-1.5 shadow-[var(--shadow-overlay)]\"\n style={{\n top: position.top,\n left: position.left,\n maxWidth: \"calc(100vw - 16px)\",\n maxHeight: \"calc(100dvh - 16px)\", overflowY: \"auto\",\n animation: \"mova-sidebar-pop 180ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"top left\",\n }}\n >\n <style>{COMPONENT_STYLES}</style>\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[8px] bg-[var(--hover-2)]\">\n <button\n data-menu-row\n type=\"button\"\n onClick={onClose}\n className=\"relative z-10 flex h-10 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-[7px] bg-[var(--ink)] text-[11px] font-semibold text-[var(--surface)]\">\n {WORKSPACE.monogram}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] font-medium text-[var(--ink)]\">{WORKSPACE.name}</span>\n <span className=\"shrink-0 text-[var(--ink)]\"><IconCheckmark1Small size={18} /></span>\n </button>\n <div className=\"my-1 h-px bg-[var(--line)]\" />\n {[\n { label: \"New workspace\", icon: <IconPlusMedium size={16} /> },\n { label: \"Workspace settings\", icon: <IconSettingsGear1 size={16} /> },\n { label: \"Invite team members\", icon: <IconUserAdd size={16} /> },\n ].map((item) => (\n <button\n key={item.label}\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(item.label); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--ink-2)]\">{item.icon}</span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--ink)]\">{item.label}</span>\n </button>\n ))}\n <div className=\"my-1 h-px bg-[var(--line)]\" />\n <button\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(\"Sign out\"); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--ink-2)]\"><IconArrowBoxLeft size={16} /></span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--ink)]\">Sign out</span>\n </button>\n </GlideMenu>\n </div>,\n document.body,\n );\n}\n\nexport function AISidebarNav({\n theme = \"dark\",\n onWorkspaceAction,\n activeTitle,\n className = \"\",\n fill = false,\n onNewChat,\n onPick,\n activeNav,\n onNavigate,\n footerLabel = \"Upgrade\",\n footerIcon,\n onFooterClick,\n recents = DEFAULT_RECENTS,\n}: SidebarNavProps) {\n const [feedback, setFeedback] = useState(\"\");\n const rootRef = useRef<HTMLElement>(null);\n const searchButtonRef = useRef<HTMLButtonElement>(null);\n const expandRef = useRef<HTMLButtonElement>(null);\n const collapseRef = useRef<HTMLButtonElement>(null);\n const [collapsed, setCollapsed] = useState(false);\n const [internalNav, setInternalNav] = useState(\"chats\");\n const currentNav = activeNav ?? internalNav;\n const selectNav = (key: string) => {\n setInternalNav(key);\n setFeedback(key === \"home\" ? \"Home selected\" : key === \"invite\" ? \"Invite users selected\" : \"Chats selected\");\n onNavigate?.(key);\n };\n const [demoActiveTitle, setDemoActiveTitle] = useState<string | null>(null);\n const [workspaceOpen, setWorkspaceOpen] = useState(false);\n const [workspacePosition, setWorkspacePosition] = useState({ top: 0, left: 0 });\n const [searchOpen, setSearchOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const workspaceButtonRef = useRef<HTMLButtonElement>(null);\n const searchRef = useRef<HTMLInputElement>(null);\n\n const selectedTitle = activeTitle === undefined ? demoActiveTitle : activeTitle;\n const visibleRecents = recents.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()));\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = (event: PointerEvent) => {\n const target = event.target as Element;\n if (!workspaceButtonRef.current?.contains(target) && !target.closest(\"[data-workspace-menu]\")) {\n setWorkspaceOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = () => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); };\n window.addEventListener(\"resize\", close);\n return () => { window.removeEventListener(\"resize\", close); };\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (searchOpen) searchRef.current?.focus();\n }, [searchOpen]);\n\n const collapse = () => {\n setCollapsed(true);\n requestAnimationFrame(() => expandRef.current?.focus());\n setWorkspaceOpen(false);\n setSearchOpen(false);\n setQuery(\"\");\n };\n\n return (\n <aside\n ref={rootRef}\n data-mova-sidebar data-theme={theme}\n data-sidebar-collapsed={collapsed}\n aria-label=\"Workspace navigation\"\n className={`relative flex shrink-0 overflow-hidden transition-[width] ${fill ? \"h-full\" : \"h-[600px]\"} ${className}`}\n style={{\n width: collapsed ? SIDEBAR_MOTION.collapsedWidth : SIDEBAR_MOTION.expandedWidth,\n transitionDuration: `${SIDEBAR_MOTION.duration}ms`,\n transitionTimingFunction: SIDEBAR_MOTION.easing,\n \"--sidebar-copy-duration\": `${SIDEBAR_MOTION.copyDuration}ms`,\n \"--sidebar-copy-offset\": `${SIDEBAR_MOTION.copyOffset}px`,\n \"--sidebar-easing\": SIDEBAR_MOTION.easing,\n } as CSSProperties}\n >\n <style>{COMPONENT_STYLES}</style>\n <div className=\"flex min-h-0 w-[224px] shrink-0 flex-col\">\n <div className=\"relative mb-2.5 h-10 shrink-0\">\n <button\n ref={workspaceButtonRef}\n data-workspace-trigger\n type=\"button\"\n aria-expanded={workspaceOpen}\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={() => {\n if (!workspaceOpen && workspaceButtonRef.current) {\n const rect = workspaceButtonRef.current.getBoundingClientRect();\n setWorkspacePosition({ top: Math.min(rect.bottom + 6, Math.max(8, window.innerHeight - 260)), left: Math.max(8, Math.min(rect.left, window.innerWidth - 264)) });\n }\n setWorkspaceOpen((open) => !open);\n }}\n className=\"sidebar-workspace-control absolute left-2 top-1 flex h-8 w-[164px] items-center rounded-[8px] px-2 text-left transition-[background-color,transform] duration-100 hover:bg-[var(--hover-2)] active:scale-[0.99]\"\n >\n <span className=\"sidebar-logo flex size-5 shrink-0 items-center justify-center text-[var(--ink)]\">\n <IconPopsicle2 size={18} />\n </span>\n <span className=\"sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium text-[var(--ink-2)]\">\n {WORKSPACE.name}\n </span>\n <span className=\"sidebar-copy ml-1 flex shrink-0 text-[var(--ink-3)]\">\n <IconChevronDownSmall size={16} />\n </span>\n </button>\n\n {workspaceOpen && <WorkspaceMenu theme={theme} position={workspacePosition} onAction={(action) => { setFeedback(`${action} selected`); onWorkspaceAction?.(action); }} onClose={() => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); }} />}\n\n <button\n type=\"button\"\n ref={collapseRef}\n aria-label=\"Collapse sidebar\"\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={collapse}\n className=\"sidebar-collapse-control absolute right-2 top-1 flex size-8 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)]\"\n >\n <IconSidebarLeftArrow size={18} />\n </button>\n <button\n type=\"button\"\n ref={expandRef}\n aria-label=\"Expand sidebar\"\n aria-hidden={!collapsed}\n tabIndex={collapsed ? 0 : -1}\n onClick={() => { setCollapsed(false); requestAnimationFrame(() => collapseRef.current?.focus()); }}\n className=\"sidebar-expand-control absolute left-2 top-0.5 flex size-9 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)]\"\n >\n <IconSidebarLeftArrow size={18} className=\"rotate-180\" />\n </button>\n </div>\n\n <GlideGroup>\n <RailButton\n icon={<IconEditBig size={18} />}\n label=\"New chat\"\n onClick={() => {\n if (activeTitle === undefined) setDemoActiveTitle(null);\n selectNav(\"chats\");\n setFeedback(\"New chat ready\");\n onNewChat?.();\n }}\n />\n {NAV_ITEMS.map((item) => (\n <RailButton\n key={item.key}\n icon={item.icon}\n label={item.label}\n count={item.count}\n active={currentNav === item.key}\n onClick={() => selectNav(item.key)}\n />\n ))}\n </GlideGroup>\n\n <div className=\"mt-3 min-h-0 flex-1 overflow-y-auto\" inert={collapsed}>\n <div className=\"sidebar-copy relative mx-2 mb-1 h-8\">\n <div\n aria-hidden={searchOpen}\n className={`absolute inset-0 flex items-center gap-1.5 px-2 text-[12.5px] font-medium text-[var(--ink-3)] transition-[opacity,transform] ${searchOpen ? \"pointer-events-none -translate-x-1 opacity-0\" : \"translate-x-0 opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`, transitionTimingFunction: CHAT_SEARCH_MOTION.easing }}\n >\n <IconChevronDownSmall size={16} />\n <span>Chats</span>\n </div>\n\n <button\n type=\"button\"\n ref={searchButtonRef}\n aria-label=\"Search chats\"\n tabIndex={searchOpen ? -1 : 0}\n aria-expanded={searchOpen}\n onClick={() => setSearchOpen(true)}\n className={`absolute right-0 top-0 z-10 flex size-8 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[opacity,background-color,color,transform] hover:bg-[var(--hover-2)] hover:text-[var(--ink)] active:scale-[0.96] ${searchOpen ? \"pointer-events-none opacity-0\" : \"opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms` }}\n >\n <IconMagnifyingGlass size={16} />\n </button>\n\n <div\n className={`absolute right-0 top-0 z-20 flex h-8 items-center overflow-hidden rounded-[8px] bg-[var(--field)] text-[var(--ink-3)] shadow-[var(--shadow-hairline)] transition-[width,opacity] focus-within:text-[var(--ink-2)] ${searchOpen ? \"pointer-events-auto opacity-100\" : \"pointer-events-none opacity-0\"}`}\n style={{\n width: searchOpen ? \"100%\" : CHAT_SEARCH_MOTION.closedWidth,\n transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`,\n transitionTimingFunction: CHAT_SEARCH_MOTION.easing,\n }}\n >\n <span className=\"ml-2 flex shrink-0 items-center justify-center\">\n <IconMagnifyingGlass size={15} />\n </span>\n <input\n ref={searchRef}\n tabIndex={searchOpen ? 0 : -1}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }\n }}\n placeholder=\"Search chats\"\n aria-label=\"Search chat history\"\n className=\"ml-1.5 min-w-0 flex-1 bg-transparent text-[13px] font-medium text-[var(--ink)] outline-none placeholder:text-[var(--ink-3)]\"\n />\n <button\n type=\"button\"\n aria-label=\"Close chat search\"\n tabIndex={searchOpen ? 0 : -1}\n onClick={() => {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }}\n className=\"flex size-8 shrink-0 items-center justify-center rounded-[8px] text-[var(--ink-3)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--hover-2)] hover:text-[var(--ink)] active:scale-[0.96]\"\n >\n <IconCrossSmall size={16} />\n </button>\n </div>\n </div>\n\n <GlideGroup>\n {visibleRecents.map((item) => {\n const active = item.label === selectedTitle;\n return (\n <button\n key={item.id}\n data-row\n type=\"button\"\n title={item.label}\n aria-current={active ? \"page\" : undefined}\n onClick={() => {\n selectNav(\"chats\");\n if (activeTitle === undefined) setDemoActiveTitle(item.label);\n onPick?.(item.id, item.label, item.prompt);\n }}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left transition-[width,background-color,color,transform] duration-150 active:scale-[0.98] ${\n active ? \"bg-[var(--hover-2)] group-hover/glide:bg-transparent\" : \"\"\n }`}\n >\n <span className={`sidebar-copy min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--ink)]\" : \"text-[var(--ink-2)]\"}`}>\n {item.label}\n </span>\n </button>\n );\n })}\n {query && visibleRecents.length === 0 && (\n <div className=\"sidebar-copy mx-2 px-2 py-2 text-[12.5px] text-[var(--ink-3)]\">No chats found</div>\n )}\n </GlideGroup>\n </div>\n\n <div inert={collapsed} className=\"sidebar-copy mx-2 mt-3 w-[208px] border-t border-[var(--line)] pt-3\">\n <button\n type=\"button\"\n onClick={() => { setFeedback(`${footerLabel} selected`); (onFooterClick ?? onNewChat)?.(); }}\n className=\"flex h-8 w-full items-center justify-center gap-1.5 rounded-[8px] bg-[var(--hover-2)] text-[12.5px] font-medium text-[var(--ink)] transition-[background-color,transform] duration-150 hover:bg-[var(--line-strong)] active:scale-[0.98]\"\n >\n {footerIcon}\n {footerLabel}\n </button>\n <p role=\"status\" className=\"mt-2 text-[11px] text-[var(--ink-3)]\">{feedback}</p>\n </div>\n </div>\n </aside>\n );\n}\n\nexport default AISidebarNav;\n"
16
+ "content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Retains the original 224px / 52px rail and gliding navigation motion.\n\nimport { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { LogOut as IconArrowBoxLeft, Check as IconCheckmark1Small, ChevronDown as IconChevronDownSmall, X as IconCrossSmall, SquarePen as IconEditBig, House as IconHome, Search as IconMagnifyingGlass, Plus as IconPlusMedium, IceCream as IconPopsicle2, Settings as IconSettingsGear1, PanelLeftClose as IconSidebarLeftArrow, UserPlus as IconUserAdd } from \"lucide-react\";\n\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst COMPONENT_STYLES = `[data-mova-sidebar] button:focus-visible { outline:2px solid var(--mova-ai-accent,#454545);outline-offset:2px;border-radius:8px; }\n[data-mova-sidebar] input:focus-visible { outline:none; }\n[data-mova-sidebar] .sidebar-search:focus-within { outline:2px solid var(--mova-ai-accent,#454545);outline-offset:2px;border-radius:8px; }\n[data-mova-sidebar] .sidebar-search button:focus-visible { outline:none; }\n[data-mova-sidebar] button { cursor:pointer; } [data-mova-sidebar] button:disabled { cursor:default; }\n[data-mova-sidebar] .sidebar-row{\n width: 208px;\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-copy{\n opacity: 1;\n transform: translateX(0);\n transition:\n opacity var(--sidebar-copy-duration) ease-out,\n transform var(--sidebar-copy-duration) var(--sidebar-easing);\n}\n\n[data-mova-sidebar] .sidebar-logo,\n[data-mova-sidebar] .sidebar-collapse-control,\n[data-mova-sidebar] .sidebar-expand-control{\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: ease-out;\n}\n\n[data-mova-sidebar] .sidebar-expand-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar] .sidebar-glide-highlight{\n right: 8px;\n left: 8px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-row{\n width: 36px;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-copy{\n pointer-events: none;\n opacity: 0;\n transform: translateX(calc(var(--sidebar-copy-offset) * -1));\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-workspace-control{\n pointer-events: none;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-logo,\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-collapse-control{\n pointer-events: none;\n opacity: 0;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-expand-control{\n pointer-events: auto;\n opacity: 1;\n}\n\n[data-mova-sidebar][data-sidebar-collapsed=\"true\"] .sidebar-glide-highlight{\n right: auto;\n width: 36px;\n}\n\n\n@keyframes mova-sidebar-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-sidebar-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-sidebar], [data-mova-sidebar] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--mova-ai-hover,#f1f1f1)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* ─────────────────────────────────────────────────────────\n * SIDEBAR NAV\n * Shared by the design-system preview and the harness shell:\n * compact workspace switcher, primary navigation, searchable\n * chat history, and a collapse that preserves icon alignment.\n * ───────────────────────────────────────────────────────── */\n\nconst WORKSPACE = { key: \"creamery\", name: \"Creamery Ops\", monogram: \"C\" };\n\nconst NAV_ITEMS = [\n { key: \"home\", label: \"Home\", icon: <IconHome size={18} /> },\n { key: \"invite\", label: \"Invite users\", icon: <IconUserAdd size={18} />, count: \"3/10\" },\n];\n\ntype SidebarRecent = {\n id: string;\n label: string;\n prompt?: string;\n};\n\nconst DEFAULT_RECENTS: SidebarRecent[] = [\n { id: \"suppliers\", label: \"Supplier records\" },\n { id: \"todos\", label: \"Urgent to-dos this morning\" },\n { id: \"flavor\", label: \"Flavor page ticket\" },\n { id: \"workload\", label: \"Workload summary\" },\n { id: \"offboarding\", label: \"Off-board a supplier\" },\n { id: \"restock\", label: \"Batch restock function\" },\n { id: \"edits\", label: \"Propose flavor edits\" },\n { id: \"subway\", label: \"Subway surfing\" },\n];\n\ntype SidebarNavProps = {\n theme?: \"dark\" | \"light\";\n onWorkspaceAction?: (action: string) => void;\n activeTitle?: string | null;\n className?: string;\n fill?: boolean;\n onNewChat?: () => void;\n onPick?: (id: string, label: string, prompt?: string) => void;\n /** controlled primary-nav selection (e.g. \"home\" | \"invite\") */\n activeNav?: string;\n onNavigate?: (key: string) => void;\n /** footer call-to-action — defaults to the demo \"Upgrade\" button */\n footerLabel?: string;\n footerIcon?: ReactNode;\n onFooterClick?: () => void;\n recents?: SidebarRecent[];\n variant?: string;\n};\n\nconst SIDEBAR_MOTION = {\n expandedWidth: 224,\n collapsedWidth: 52,\n duration: 280,\n copyDuration: 180,\n copyOffset: 8,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\n/* ─────────────────────────────────────────────────────────\n * CHAT SEARCH STORYBOARD\n *\n * 0ms search is triggered; Chats label begins fading\n * 0ms field grows right → left from the search control\n * 180ms field fills the row; cursor is focused and ready\n * ───────────────────────────────────────────────────────── */\nconst CHAT_SEARCH_MOTION = {\n duration: 180,\n closedWidth: 28,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n listDuration: 220,\n};\n\nfunction GlideGroup({ children }: { children: ReactNode }) {\n return (\n <GlideMenu\n rowSelector=\"[data-row]\"\n highlightClassName=\"sidebar-glide-highlight rounded-[7px] bg-[var(--mova-ai-hover-strong,#e9e9e9)]\"\n className=\"group/glide flex flex-col gap-px\"\n >\n {children}\n </GlideMenu>\n );\n}\n\nfunction RailButton({\n icon,\n label,\n active = false,\n count,\n onClick,\n}: {\n icon: ReactNode;\n label: string;\n active?: boolean;\n count?: string;\n onClick?: () => void;\n}) {\n return (\n <button\n data-row\n aria-label={label}\n aria-current={active ? \"page\" : undefined}\n type=\"button\"\n onClick={onClick}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left\n transition-[width,background-color,color,transform] duration-150 active:scale-[0.98]\n ${active ? \"bg-[var(--mova-ai-hover-strong,#e9e9e9)] group-hover/glide:bg-transparent\" : \"\"}`}\n >\n <span className={`flex size-5 shrink-0 items-center justify-center ${active ? \"text-[var(--mova-ai-ink,#202020)]\" : \"text-[var(--mova-ai-ink-secondary,#666)]\"}`}>\n {icon}\n </span>\n <span className={`sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--mova-ai-ink,#202020)]\" : \"text-[var(--mova-ai-ink-secondary,#666)]\"}`}>\n {label}\n </span>\n {count && (\n <span className=\"sidebar-copy mr-2 shrink-0 text-[12px] font-medium tabular-nums text-[var(--mova-ai-ink-muted,#858585)]\">\n {count}\n </span>\n )}\n </button>\n );\n}\n\nfunction WorkspaceMenu({\n position,\n theme,\n onAction,\n onClose,\n}: {\n theme: \"dark\" | \"light\";\n onAction: (action: string) => void;\n position: { top: number; left: number };\n onClose: () => void;\n}) {\n const menuRef = useRef<HTMLDivElement>(null);\n useEffect(() => { menuRef.current?.querySelector<HTMLButtonElement>(\"button\")?.focus(); }, []);\n return createPortal(\n <div ref={menuRef}\n data-mova-sidebar data-theme={theme}\n data-workspace-menu\n role=\"group\" aria-label=\"Workspace menu\"\n onKeyDown={(event) => { if (event.key === \"Escape\") { event.preventDefault(); onClose(); } if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button\")); const index = buttons.indexOf(document.activeElement as HTMLButtonElement); buttons[(index + (event.key === \"ArrowDown\" ? 1 : buttons.length - 1)) % buttons.length]?.focus(); } }}\n className=\"mova-ai-theme fixed z-50 w-64 rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] p-1.5 shadow-[var(--mova-ai-shadow-overlay,none)]\"\n style={{\n top: position.top,\n left: position.left,\n maxWidth: \"calc(100vw - 16px)\",\n maxHeight: \"calc(100dvh - 16px)\", overflowY: \"auto\",\n animation: \"mova-sidebar-pop 180ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"top left\",\n }}\n >\n <style>{AI_THEME_STYLES}{COMPONENT_STYLES}</style>\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[8px] bg-[var(--mova-ai-hover-strong,#e9e9e9)]\">\n <button\n data-menu-row\n type=\"button\"\n onClick={onClose}\n className=\"relative z-10 flex h-10 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-[7px] bg-[var(--mova-ai-ink,#202020)] text-[11px] font-semibold text-[var(--mova-ai-surface,#fff)]\">\n {WORKSPACE.monogram}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] font-medium text-[var(--mova-ai-ink,#202020)]\">{WORKSPACE.name}</span>\n <span className=\"shrink-0 text-[var(--mova-ai-ink,#202020)]\"><IconCheckmark1Small size={18} /></span>\n </button>\n <div className=\"my-1 h-px bg-[var(--mova-ai-border,#dedede)]\" />\n {[\n { label: \"New workspace\", icon: <IconPlusMedium size={16} /> },\n { label: \"Workspace settings\", icon: <IconSettingsGear1 size={16} /> },\n { label: \"Invite team members\", icon: <IconUserAdd size={16} /> },\n ].map((item) => (\n <button\n key={item.label}\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(item.label); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--mova-ai-ink-secondary,#666)]\">{item.icon}</span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--mova-ai-ink,#202020)]\">{item.label}</span>\n </button>\n ))}\n <div className=\"my-1 h-px bg-[var(--mova-ai-border,#dedede)]\" />\n <button\n data-menu-row\n type=\"button\"\n onClick={() => { onAction(\"Sign out\"); onClose(); }}\n className=\"relative z-10 flex h-9 w-full items-center gap-1.5 rounded-[8px] px-2 text-left\"\n >\n <span className=\"flex size-5 shrink-0 items-center justify-center text-[var(--mova-ai-ink-secondary,#666)]\"><IconArrowBoxLeft size={16} /></span>\n <span className=\"min-w-0 flex-1 truncate text-[13.5px] text-[var(--mova-ai-ink,#202020)]\">Sign out</span>\n </button>\n </GlideMenu>\n </div>,\n document.body,\n );\n}\n\nexport function AISidebarNav({\n theme = \"dark\",\n onWorkspaceAction,\n activeTitle,\n className = \"\",\n fill = false,\n onNewChat,\n onPick,\n activeNav,\n onNavigate,\n footerLabel = \"Upgrade\",\n footerIcon,\n onFooterClick,\n recents = DEFAULT_RECENTS,\n}: SidebarNavProps) {\n const [feedback, setFeedback] = useState(\"\");\n const rootRef = useRef<HTMLElement>(null);\n const searchButtonRef = useRef<HTMLButtonElement>(null);\n const expandRef = useRef<HTMLButtonElement>(null);\n const collapseRef = useRef<HTMLButtonElement>(null);\n const [collapsed, setCollapsed] = useState(false);\n const [internalNav, setInternalNav] = useState(\"chats\");\n const currentNav = activeNav ?? internalNav;\n const selectNav = (key: string) => {\n setInternalNav(key);\n if (key === \"home\" || key === \"invite\") setFeedback(key === \"home\" ? \"Home selected\" : \"Invite users selected\");\n onNavigate?.(key);\n };\n const [demoActiveTitle, setDemoActiveTitle] = useState<string | null>(null);\n const [workspaceOpen, setWorkspaceOpen] = useState(false);\n const [workspacePosition, setWorkspacePosition] = useState({ top: 0, left: 0 });\n const [searchOpen, setSearchOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [chatsExpanded, setChatsExpanded] = useState(true);\n const historyId = useId();\n const searchId = useId();\n const workspaceButtonRef = useRef<HTMLButtonElement>(null);\n const searchRef = useRef<HTMLInputElement>(null);\n\n const selectedTitle = activeTitle === undefined ? demoActiveTitle : activeTitle;\n const visibleRecents = recents.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()));\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = (event: PointerEvent) => {\n const target = event.target as Element;\n if (!workspaceButtonRef.current?.contains(target) && !target.closest(\"[data-workspace-menu]\")) {\n setWorkspaceOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = () => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); };\n window.addEventListener(\"resize\", close);\n return () => { window.removeEventListener(\"resize\", close); };\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (searchOpen) searchRef.current?.focus();\n }, [searchOpen]);\n\n const collapse = () => {\n setCollapsed(true);\n requestAnimationFrame(() => expandRef.current?.focus());\n setWorkspaceOpen(false);\n setSearchOpen(false);\n setQuery(\"\");\n };\n\n const toggleChats = () => {\n if (chatsExpanded) {\n setSearchOpen(false);\n setQuery(\"\");\n }\n setChatsExpanded(!chatsExpanded);\n };\n\n return (\n <aside\n ref={rootRef}\n data-mova-sidebar data-theme={theme}\n data-sidebar-collapsed={collapsed}\n aria-label=\"Workspace navigation\"\n className={`mova-ai-theme relative flex shrink-0 overflow-hidden rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] text-[color:var(--mova-ai-ink,#202020)] transition-[width] ${fill ? \"h-full\" : \"h-[600px]\"} ${className}`}\n style={{\n width: collapsed ? SIDEBAR_MOTION.collapsedWidth : SIDEBAR_MOTION.expandedWidth,\n transitionDuration: `${SIDEBAR_MOTION.duration}ms`,\n transitionTimingFunction: SIDEBAR_MOTION.easing,\n \"--sidebar-copy-duration\": `${SIDEBAR_MOTION.copyDuration}ms`,\n \"--sidebar-copy-offset\": `${SIDEBAR_MOTION.copyOffset}px`,\n \"--sidebar-easing\": SIDEBAR_MOTION.easing,\n } as CSSProperties}\n >\n <style>{AI_THEME_STYLES}{COMPONENT_STYLES}</style>\n <div className=\"flex min-h-0 w-[224px] shrink-0 flex-col\">\n <div className=\"relative mb-2.5 h-10 shrink-0\">\n <button\n ref={workspaceButtonRef}\n data-workspace-trigger\n type=\"button\"\n aria-expanded={workspaceOpen}\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={() => {\n if (!workspaceOpen && workspaceButtonRef.current) {\n const rect = workspaceButtonRef.current.getBoundingClientRect();\n setWorkspacePosition({ top: Math.min(rect.bottom + 6, Math.max(8, window.innerHeight - 260)), left: Math.max(8, Math.min(rect.left, window.innerWidth - 264)) });\n }\n setWorkspaceOpen((open) => !open);\n }}\n className=\"sidebar-workspace-control absolute left-2 top-1 flex h-8 w-[164px] items-center rounded-[8px] px-2 text-left transition-[background-color,transform] duration-100 hover:bg-[var(--mova-ai-hover-strong,#e9e9e9)] active:scale-[0.99]\"\n >\n <span className=\"sidebar-logo flex size-5 shrink-0 items-center justify-center text-[var(--mova-ai-ink,#202020)]\">\n <IconPopsicle2 size={18} />\n </span>\n <span className=\"sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[14px] font-medium text-[var(--mova-ai-ink-secondary,#666)]\">\n {WORKSPACE.name}\n </span>\n <span className=\"sidebar-copy ml-1 flex shrink-0 text-[var(--mova-ai-ink-muted,#858585)]\">\n <IconChevronDownSmall size={16} />\n </span>\n </button>\n\n {workspaceOpen && <WorkspaceMenu theme={theme} position={workspacePosition} onAction={(action) => { setFeedback(`${action} selected`); onWorkspaceAction?.(action); }} onClose={() => { setWorkspaceOpen(false); workspaceButtonRef.current?.focus(); }} />}\n\n <button\n type=\"button\"\n ref={collapseRef}\n aria-label=\"Collapse sidebar\"\n aria-hidden={collapsed}\n tabIndex={collapsed ? -1 : 0}\n onClick={collapse}\n className=\"sidebar-collapse-control absolute right-2 top-1 flex size-8 items-center justify-center rounded-[8px] text-[var(--mova-ai-ink-muted,#858585)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--mova-ai-hover-strong,#e9e9e9)] hover:text-[var(--mova-ai-ink,#202020)]\"\n >\n <IconSidebarLeftArrow size={18} />\n </button>\n <button\n type=\"button\"\n ref={expandRef}\n aria-label=\"Expand sidebar\"\n aria-hidden={!collapsed}\n tabIndex={collapsed ? 0 : -1}\n onClick={() => { setCollapsed(false); requestAnimationFrame(() => collapseRef.current?.focus()); }}\n className=\"sidebar-expand-control absolute left-2 top-0.5 flex size-9 items-center justify-center rounded-[8px] text-[var(--mova-ai-ink-muted,#858585)] transition-[opacity,background-color,color] duration-150 hover:bg-[var(--mova-ai-hover-strong,#e9e9e9)] hover:text-[var(--mova-ai-ink,#202020)]\"\n >\n <IconSidebarLeftArrow size={18} className=\"rotate-180\" />\n </button>\n </div>\n\n <GlideGroup>\n <RailButton\n icon={<IconEditBig size={18} />}\n label=\"New chat\"\n onClick={() => {\n if (activeTitle === undefined) setDemoActiveTitle(null);\n selectNav(\"chats\");\n setFeedback(\"New chat ready\");\n onNewChat?.();\n }}\n />\n {NAV_ITEMS.map((item) => (\n <RailButton\n key={item.key}\n icon={item.icon}\n label={item.label}\n count={item.count}\n active={currentNav === item.key}\n onClick={() => selectNav(item.key)}\n />\n ))}\n </GlideGroup>\n\n <div className=\"mt-3 flex min-h-0 flex-1 flex-col\" inert={collapsed}>\n <div className=\"sidebar-copy relative mx-2 mb-1 h-8 shrink-0\">\n <button\n type=\"button\"\n aria-label=\"Chats\"\n aria-expanded={chatsExpanded}\n aria-controls={historyId}\n aria-hidden={searchOpen}\n tabIndex={searchOpen ? -1 : 0}\n onClick={toggleChats}\n className={`absolute inset-0 flex items-center gap-1.5 rounded-[8px] px-2 text-left text-[12.5px] font-medium text-[var(--mova-ai-ink-muted,#858585)] transition-[opacity,transform,color] hover:text-[var(--mova-ai-ink,#202020)] ${searchOpen ? \"pointer-events-none -translate-x-1 opacity-0\" : \"translate-x-0 opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`, transitionTimingFunction: CHAT_SEARCH_MOTION.easing }}\n >\n <IconChevronDownSmall size={16} className={`transition-transform duration-200 ${chatsExpanded ? \"rotate-0\" : \"-rotate-90\"}`} />\n <span>Chats</span>\n </button>\n\n <button\n type=\"button\"\n ref={searchButtonRef}\n aria-label=\"Search chats\"\n tabIndex={searchOpen ? -1 : 0}\n aria-expanded={searchOpen}\n aria-controls={searchId}\n aria-hidden={searchOpen}\n onClick={() => { setChatsExpanded(true); setSearchOpen(true); }}\n className={`absolute right-0 top-0 z-10 flex size-8 items-center justify-center rounded-[8px] text-[var(--mova-ai-ink-muted,#858585)] transition-[opacity,background-color,color,transform] hover:bg-[var(--mova-ai-hover-strong,#e9e9e9)] hover:text-[var(--mova-ai-ink,#202020)] active:scale-[0.96] ${searchOpen ? \"pointer-events-none opacity-0\" : \"opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms` }}\n >\n <IconMagnifyingGlass size={16} />\n </button>\n\n <div\n id={searchId}\n role=\"search\"\n aria-hidden={!searchOpen}\n inert={!searchOpen}\n className={`sidebar-search absolute right-0 top-0 z-20 flex h-8 items-center overflow-hidden rounded-[8px] bg-[var(--mova-ai-field,#f6f6f6)] text-[var(--mova-ai-ink-muted,#858585)] shadow-[var(--mova-ai-shadow-hairline,none)] transition-[width,opacity] focus-within:text-[var(--mova-ai-ink-secondary,#666)] ${searchOpen ? \"pointer-events-auto opacity-100\" : \"pointer-events-none opacity-0\"}`}\n style={{\n width: searchOpen ? \"100%\" : CHAT_SEARCH_MOTION.closedWidth,\n transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms`,\n transitionTimingFunction: CHAT_SEARCH_MOTION.easing,\n }}\n >\n <span className=\"ml-2 flex shrink-0 items-center justify-center\">\n <IconMagnifyingGlass size={15} />\n </span>\n <input\n ref={searchRef}\n tabIndex={searchOpen ? 0 : -1}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }\n }}\n placeholder=\"Search chats\"\n aria-label=\"Search chat history\"\n className=\"ml-1.5 min-w-0 flex-1 bg-transparent text-[13px] font-medium text-[var(--mova-ai-ink,#202020)] outline-none placeholder:text-[var(--mova-ai-ink-muted,#858585)]\"\n />\n <button\n type=\"button\"\n aria-label=\"Close chat search\"\n tabIndex={searchOpen ? 0 : -1}\n onClick={() => {\n setSearchOpen(false);\n setQuery(\"\");\n searchButtonRef.current?.focus();\n }}\n className=\"flex size-8 shrink-0 items-center justify-center rounded-[8px] text-[var(--mova-ai-ink-muted,#858585)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--mova-ai-hover-strong,#e9e9e9)] hover:text-[var(--mova-ai-ink,#202020)] active:scale-[0.96]\"\n >\n <IconCrossSmall size={16} />\n </button>\n </div>\n </div>\n\n <div\n id={historyId}\n aria-hidden={!chatsExpanded}\n inert={!chatsExpanded}\n className=\"grid min-h-0 flex-1 transition-[grid-template-rows,opacity]\"\n style={{\n gridTemplateRows: chatsExpanded ? \"minmax(0, 1fr)\" : \"minmax(0, 0fr)\",\n opacity: chatsExpanded ? 1 : 0,\n transitionDuration: `${CHAT_SEARCH_MOTION.listDuration}ms`,\n transitionTimingFunction: CHAT_SEARCH_MOTION.easing,\n }}\n >\n <div className=\"min-h-0 overflow-hidden\">\n <div className=\"h-full overflow-y-auto\">\n <GlideGroup>\n {visibleRecents.map((item) => {\n const active = item.label === selectedTitle;\n return (\n <button\n key={item.id}\n data-row\n type=\"button\"\n title={item.label}\n aria-current={active ? \"page\" : undefined}\n onClick={() => {\n setInternalNav(\"chats\");\n onNavigate?.(\"chats\");\n if (activeTitle === undefined) setDemoActiveTitle(item.label);\n onPick?.(item.id, item.label, item.prompt);\n }}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left transition-[width,background-color,color,transform] duration-150 active:scale-[0.98] ${\n active ? \"bg-[var(--mova-ai-hover-strong,#e9e9e9)] group-hover/glide:bg-transparent\" : \"\"\n }`}\n >\n <span className={`sidebar-copy min-w-0 flex-1 truncate text-[14px] font-medium ${active ? \"text-[var(--mova-ai-ink,#202020)]\" : \"text-[var(--mova-ai-ink-secondary,#666)]\"}`}>\n {item.label}\n </span>\n </button>\n );\n })}\n {query && visibleRecents.length === 0 && (\n <div className=\"sidebar-copy mx-2 px-2 py-2 text-[12.5px] text-[var(--mova-ai-ink-muted,#858585)]\">No chats found</div>\n )}\n </GlideGroup>\n </div>\n </div>\n </div>\n </div>\n\n <div inert={collapsed} className=\"sidebar-copy mx-2 mt-3 w-[208px] border-t border-[var(--mova-ai-border,#dedede)] pt-3\">\n <button\n type=\"button\"\n onClick={() => { (onFooterClick ?? onNewChat)?.(); }}\n className=\"flex h-8 w-full items-center justify-center gap-1.5 rounded-[8px] bg-[var(--mova-ai-hover-strong,#e9e9e9)] text-[12.5px] font-medium text-[var(--mova-ai-ink,#202020)] transition-[background-color,transform] duration-150 hover:bg-[var(--mova-ai-border-strong,#c8c8c8)] active:scale-[0.98]\"\n >\n {footerIcon}\n {footerLabel}\n </button>\n <p role=\"status\" className=\"sr-only\">{feedback}</p>\n </div>\n </div>\n </aside>\n );\n}\n\nexport default AISidebarNav;\n"
17
17
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-sources",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "ai-sources.tsx",
6
6
  "sourcePath": "ui/ai/ai-sources.tsx",
7
7
  "registryFile": "ai-sources.json",
@@ -16,5 +16,5 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "previewMode": "client",
19
- "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-sources (MIT). See ../patterns/smoothui.LICENSE.\nimport { ChevronRight, Globe } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst STACK_LIMIT = 3;\nconst STACK_OVERLAP_PX = 8;\nconst FAN_GAP_PX = 3;\nconst ROW_STAGGER = 0.035;\n\nexport type AISource = { favicon?: ReactNode; id: string; snippet?: string; title: string; url: string };\nexport type AISourcesProps = { className?: string; defaultOpen?: boolean; label?: string; sources: AISource[] };\n\nconst hostOf = (url: string): string => {\n try { return new URL(url).hostname.replace(/^www\\./, \"\"); } catch { return url; }\n};\n\nconst Favicon = ({ size, source }: { size: number; source: AISource }) => (\n <span className=\"relative flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-background text-muted-foreground\" style={{ height: size, width: size }}>\n <Globe aria-hidden=\"true\" size={size * 0.6} />\n {source.favicon ? <span className=\"absolute inset-0 flex size-full items-center justify-center *:size-full *:object-cover\">{source.favicon}</span> : null}\n </span>\n);\n\nexport function AISources({ className = \"\", defaultOpen = false, label = \"Sources\", sources }: AISourcesProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const [isHovered, setIsHovered] = useState(false);\n const listId = useId();\n const stacked = sources.slice(0, STACK_LIMIT);\n const overflow = sources.length - stacked.length;\n const fanned = isHovered && !isOpen && !shouldReduceMotion;\n\n return (\n <div className={`w-full ${className}`}>\n <button\n aria-expanded={isOpen}\n aria-controls={listId}\n className=\"flex cursor-pointer items-center gap-2 rounded-lg py-1 text-muted-foreground text-sm transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2\"\n onBlur={() => setIsHovered(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => setIsHovered(true)}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n type=\"button\"\n >\n <span className=\"flex items-center\">\n {stacked.map((source, index) => (\n <motion.span\n animate={{ marginLeft: index === 0 ? 0 : fanned ? FAN_GAP_PX : -STACK_OVERLAP_PX }}\n className=\"relative block\"\n key={source.id}\n style={{ zIndex: stacked.length - index }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <Favicon size={18} source={source} />\n </motion.span>\n ))}\n {overflow > 0 ? <span className=\"ml-1 tabular-nums\">+{overflow}</span> : null}\n </span>\n <span>{label}</span>\n <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"flex size-4 items-center justify-center\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <ChevronRight aria-hidden=\"true\" size={14} />\n </motion.span>\n </button>\n\n <AnimatePresence initial={false}>\n {isOpen ? (\n <motion.ul\n id={listId}\n animate={{ height: \"auto\", opacity: 1 }}\n className=\"mt-1 list-none overflow-hidden\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { height: 0, opacity: 0 }}\n initial={shouldReduceMotion ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }}\n transition={shouldReduceMotion ? { duration: 0 } : { height: SPRING_DEFAULT, opacity: { duration: 0.18, ease: EASE_OUT } }}\n >\n {sources.map((source, index) => (\n <motion.li animate={{ opacity: 1, y: 0 }} initial={shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 4 }} key={source.id} transition={shouldReduceMotion ? { duration: 0 } : { ...SPRING_DEFAULT, delay: index * ROW_STAGGER }}>\n <a className=\"flex items-start gap-2.5 rounded-lg px-1 py-1.5 no-underline transition-colors hover:bg-muted\" href={source.url} rel=\"noopener noreferrer\" target=\"_blank\">\n <motion.span className=\"mt-0.5 block shrink-0\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <Favicon size={18} source={source} />\n </motion.span>\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-foreground text-sm\">{source.title}</span>\n <span className=\"block truncate text-muted-foreground text-xs\">{source.snippet ?? hostOf(source.url)}</span>\n </span>\n </a>\n </motion.li>\n ))}\n </motion.ul>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AISources;\n"
19
+ "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-sources (MIT). See ../patterns/smoothui.LICENSE.\nimport { ChevronRight, Globe } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst STACK_LIMIT = 3;\nconst STACK_OVERLAP_PX = 8;\nconst FAN_GAP_PX = 3;\nconst ROW_STAGGER = 0.035;\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\n\nexport type AISource = { favicon?: ReactNode; id: string; snippet?: string; title: string; url: string };\nexport type AISourcesProps = { className?: string; defaultOpen?: boolean; label?: string; sources: AISource[]; theme?: \"light\" | \"dark\" };\n\nconst hostOf = (url: string): string => {\n try { return new URL(url).hostname.replace(/^www\\./, \"\"); } catch { return url; }\n};\n\nconst Favicon = ({ size, source }: { size: number; source: AISource }) => (\n <span className=\"relative flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] text-[color:var(--mova-ai-ink-secondary,#666)]\" style={{ height: size, width: size }}>\n <Globe aria-hidden=\"true\" size={size * 0.6} />\n {source.favicon ? <span className=\"absolute inset-0 flex size-full items-center justify-center *:size-full *:object-cover\">{source.favicon}</span> : null}\n </span>\n);\n\nexport function AISources({ className = \"\", defaultOpen = false, label = \"Sources\", sources, theme = \"light\" }: AISourcesProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const [isHovered, setIsHovered] = useState(false);\n const listId = useId();\n const stacked = sources.slice(0, STACK_LIMIT);\n const overflow = sources.length - stacked.length;\n const fanned = isHovered && !isOpen && !shouldReduceMotion;\n\n return (\n <div className={`mova-ai-theme w-full ${className}`} data-theme={theme}>\n <style>{AI_THEME_STYLES}</style>\n <button\n aria-expanded={isOpen}\n aria-controls={listId}\n className=\"flex cursor-pointer items-center gap-2 rounded-lg py-1 text-[color:var(--mova-ai-ink-secondary,#666)] text-sm transition-colors hover:text-[color:var(--mova-ai-ink,#202020)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--mova-ai-accent,#454545)]\"\n onBlur={() => setIsHovered(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => setIsHovered(true)}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n type=\"button\"\n >\n <span className=\"flex items-center\">\n {stacked.map((source, index) => (\n <motion.span\n animate={{ marginLeft: index === 0 ? 0 : fanned ? FAN_GAP_PX : -STACK_OVERLAP_PX }}\n className=\"relative block\"\n key={source.id}\n style={{ zIndex: stacked.length - index }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <Favicon size={18} source={source} />\n </motion.span>\n ))}\n {overflow > 0 ? <span className=\"ml-1 tabular-nums\">+{overflow}</span> : null}\n </span>\n <span>{label}</span>\n <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"flex size-4 items-center justify-center\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <ChevronRight aria-hidden=\"true\" size={14} />\n </motion.span>\n </button>\n\n <AnimatePresence initial={false}>\n {isOpen ? (\n <motion.ul\n id={listId}\n animate={{ height: \"auto\", opacity: 1 }}\n className=\"mt-1 list-none overflow-hidden\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { height: 0, opacity: 0 }}\n initial={shouldReduceMotion ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }}\n transition={shouldReduceMotion ? { duration: 0 } : { height: SPRING_DEFAULT, opacity: { duration: 0.18, ease: EASE_OUT } }}\n >\n {sources.map((source, index) => (\n <motion.li animate={{ opacity: 1, y: 0 }} initial={shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 4 }} key={source.id} transition={shouldReduceMotion ? { duration: 0 } : { ...SPRING_DEFAULT, delay: index * ROW_STAGGER }}>\n <a className=\"flex items-start gap-2.5 rounded-lg px-1 py-1.5 no-underline transition-colors hover:bg-[var(--mova-ai-hover,#f1f1f1)]\" href={source.url} rel=\"noopener noreferrer\" target=\"_blank\">\n <motion.span className=\"mt-0.5 block shrink-0\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <Favicon size={18} source={source} />\n </motion.span>\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-[color:var(--mova-ai-ink,#202020)] text-sm\">{source.title}</span>\n <span className=\"block truncate text-[color:var(--mova-ai-ink-secondary,#666)] text-xs\">{source.snippet ?? hostOf(source.url)}</span>\n </span>\n </a>\n </motion.li>\n ))}\n </motion.ul>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AISources;\n"
20
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-suggestions",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "ai-suggestions.tsx",
6
6
  "sourcePath": "ui/ai/ai-suggestions.tsx",
7
7
  "registryFile": "ai-suggestions.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-task-list",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "ai-task-list.tsx",
6
6
  "sourcePath": "ui/ai/ai-task-list.tsx",
7
7
  "registryFile": "ai-task-list.json",
@@ -16,5 +16,5 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "previewMode": "client",
19
- "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-task-list (MIT). See ../patterns/smoothui.LICENSE.\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { Fragment, type ReactNode } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst CHECK_PATH = \"M 3.5 7.5 L 6 10 L 10.5 4.5\";\nconst BOX_SIZE = 14;\nconst UNDERLINE_SECONDS = 1.4;\nconst SUCCESS_COLOR = \"oklch(72% 0.17 150)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AITaskStatus = \"pending\" | \"running\" | \"done\" | \"failed\";\nexport type AITask = { children?: AITask[]; id: string; label: string; note?: string; status: AITaskStatus };\nexport type AITaskListProps = { className?: string; label?: string; tasks: AITask[] };\n\nconst flatten = (tasks: AITask[]): AITask[] => tasks.flatMap((task) => [task, ...flatten(task.children ?? [])]);\nconst boxStroke = (status: AITaskStatus): string => status === \"failed\" ? DANGER_COLOR : status === \"done\" ? SUCCESS_COLOR : \"currentColor\";\n\nfunction TaskBox({ status, shouldReduceMotion }: { shouldReduceMotion: boolean; status: AITaskStatus }) {\n const isDone = status === \"done\";\n const isFailed = status === \"failed\";\n return (\n <span className=\"mt-0.5 flex size-3.5 shrink-0 items-center justify-center\">\n <svg aria-hidden=\"true\" className=\"size-3.5\" viewBox={`0 0 ${BOX_SIZE} ${BOX_SIZE}`}>\n <motion.rect\n animate={{ fillOpacity: isDone ? 0.12 : 0, stroke: boxStroke(status) }}\n fill={isFailed ? DANGER_COLOR : SUCCESS_COLOR}\n height={12}\n rx={3.5}\n strokeWidth={1.4}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n width={12}\n x={1}\n y={1}\n />\n {isDone ? <path className=\"ai-task-draw\" d={CHECK_PATH} fill=\"none\" pathLength={1} stroke={SUCCESS_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.8} /> : null}\n {isFailed ? <path className=\"ai-task-draw\" d=\"M 5 5 L 9 9 M 9 5 L 5 9\" fill=\"none\" pathLength={1} stroke={DANGER_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeWidth={1.8} /> : null}\n </svg>\n </span>\n );\n}\n\nfunction TaskRow({ depth, shouldReduceMotion, task }: { depth: number; shouldReduceMotion: boolean; task: AITask }) {\n const isRunning = task.status === \"running\";\n const isDone = task.status === \"done\";\n return (\n <motion.li\n animate={{ opacity: isDone ? 0.65 : 1, y: isDone && !shouldReduceMotion ? 1 : 0 }}\n className=\"list-none\"\n style={{ paddingLeft: depth * 20 }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <span className=\"relative flex items-start gap-2 py-1.5\">\n <span className=\"sr-only\">{task.status}: </span>\n <TaskBox shouldReduceMotion={shouldReduceMotion} status={task.status} />\n <span className=\"min-w-0 flex-1 break-words text-foreground text-xs leading-snug\">{task.label}</span>\n {task.note ? <span className=\"max-w-[35%] truncate text-muted-foreground text-xs tabular-nums\">{task.note}</span> : null}\n {isRunning && !shouldReduceMotion ? (\n <motion.span\n animate={{ backgroundPositionX: [\"0%\", \"200%\"] }}\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px\"\n style={{ backgroundImage: \"linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%)\", backgroundSize: \"50% 100%\", opacity: 0.5 }}\n transition={{ duration: UNDERLINE_SECONDS, ease: EASE_IN_OUT, repeat: Number.POSITIVE_INFINITY }}\n />\n ) : null}\n </span>\n </motion.li>\n );\n}\n\nexport function AITaskList({ className = \"\", label = \"Plan\", tasks }: AITaskListProps) {\n const shouldReduceMotion = Boolean(useReducedMotion());\n const all = flatten(tasks);\n const done = all.filter((task) => task.status === \"done\").length;\n return (\n <div className={`w-full rounded-lg border border-border bg-background p-3 ${className}`}>\n <div className=\"mb-1.5 flex items-baseline justify-between\">\n <p className=\"font-medium text-foreground text-sm\">{label}</p>\n <p className=\"text-muted-foreground text-xs tabular-nums\">{done}/{all.length}</p>\n </div>\n <style>{`\n .ai-task-draw { stroke-dashoffset: 0; }\n @media (prefers-reduced-motion: no-preference) {\n .ai-task-draw { animation: ai-task-draw 200ms cubic-bezier(0.23, 1, 0.32, 1) both; }\n }\n @keyframes ai-task-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n `}</style>\n <ul className=\"list-none\">\n {(() => {\n const renderTasks = (items: AITask[], depth: number): ReactNode => items.map((task) => <Fragment key={task.id}><TaskRow depth={depth} shouldReduceMotion={shouldReduceMotion} task={task} />{renderTasks(task.children ?? [], depth + 1)}</Fragment>);\n return renderTasks(tasks, 0);\n })()}\n </ul>\n </div>\n );\n}\n\nexport default AITaskList;\n"
19
+ "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-task-list (MIT). See ../patterns/smoothui.LICENSE.\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { Fragment, type ReactNode } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst CHECK_PATH = \"M 3.5 7.5 L 6 10 L 10.5 4.5\";\nconst BOX_SIZE = 14;\nconst UNDERLINE_SECONDS = 1.4;\nconst SUCCESS_COLOR = \"var(--mova-ai-success,#28734d)\";\nconst DANGER_COLOR = \"var(--mova-ai-danger,#a33c3c)\";\n\nexport type AITaskStatus = \"pending\" | \"running\" | \"done\" | \"failed\";\nexport type AITask = { children?: AITask[]; id: string; label: string; note?: string; status: AITaskStatus };\nexport type AITaskListProps = { className?: string; label?: string; tasks: AITask[]; theme?: \"light\" | \"dark\" };\n\nconst flatten = (tasks: AITask[]): AITask[] => tasks.flatMap((task) => [task, ...flatten(task.children ?? [])]);\nconst boxStroke = (status: AITaskStatus): string => status === \"failed\" ? DANGER_COLOR : status === \"done\" ? SUCCESS_COLOR : \"currentColor\";\n\nfunction TaskBox({ status, shouldReduceMotion }: { shouldReduceMotion: boolean; status: AITaskStatus }) {\n const isDone = status === \"done\";\n const isFailed = status === \"failed\";\n return (\n <span className=\"mt-0.5 flex size-3.5 shrink-0 items-center justify-center\">\n <svg aria-hidden=\"true\" className=\"size-3.5\" viewBox={`0 0 ${BOX_SIZE} ${BOX_SIZE}`}>\n <motion.rect\n animate={{ fillOpacity: isDone ? 0.12 : 0, stroke: boxStroke(status) }}\n fill={isFailed ? DANGER_COLOR : SUCCESS_COLOR}\n height={12}\n rx={3.5}\n strokeWidth={1.4}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n width={12}\n x={1}\n y={1}\n />\n {isDone ? <path className=\"ai-task-draw\" d={CHECK_PATH} fill=\"none\" pathLength={1} stroke={SUCCESS_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.8} /> : null}\n {isFailed ? <path className=\"ai-task-draw\" d=\"M 5 5 L 9 9 M 9 5 L 5 9\" fill=\"none\" pathLength={1} stroke={DANGER_COLOR} strokeDasharray=\"1 1\" strokeLinecap=\"round\" strokeWidth={1.8} /> : null}\n </svg>\n </span>\n );\n}\n\nfunction TaskRow({ depth, shouldReduceMotion, task }: { depth: number; shouldReduceMotion: boolean; task: AITask }) {\n const isRunning = task.status === \"running\";\n const isDone = task.status === \"done\";\n return (\n <motion.li\n animate={{ opacity: isDone ? 0.65 : 1, y: isDone && !shouldReduceMotion ? 1 : 0 }}\n className=\"list-none\"\n style={{ paddingLeft: depth * 20 }}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n >\n <span className=\"relative flex items-start gap-2 py-1.5\">\n <span className=\"sr-only\">{task.status}: </span>\n <TaskBox shouldReduceMotion={shouldReduceMotion} status={task.status} />\n <span className=\"min-w-0 flex-1 break-words text-[color:var(--mova-ai-ink,#202020)] text-xs leading-snug\">{task.label}</span>\n {task.note ? <span className=\"max-w-[35%] truncate text-[color:var(--mova-ai-ink-secondary,#666)] text-xs tabular-nums\">{task.note}</span> : null}\n {isRunning && !shouldReduceMotion ? (\n <motion.span\n animate={{ backgroundPositionX: [\"0%\", \"200%\"] }}\n className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px\"\n style={{ backgroundImage: \"linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%)\", backgroundSize: \"50% 100%\", opacity: 0.5 }}\n transition={{ duration: UNDERLINE_SECONDS, ease: EASE_IN_OUT, repeat: Number.POSITIVE_INFINITY }}\n />\n ) : null}\n </span>\n </motion.li>\n );\n}\n\nexport function AITaskList({ className = \"\", label = \"Plan\", tasks, theme = \"light\" }: AITaskListProps) {\n const shouldReduceMotion = Boolean(useReducedMotion());\n const all = flatten(tasks);\n const done = all.filter((task) => task.status === \"done\").length;\n return (\n <div className={`mova-ai-theme w-full rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] p-3 ${className}`} data-theme={theme}>\n <style>{AI_THEME_STYLES}</style>\n <div className=\"mb-1.5 flex items-baseline justify-between\">\n <p className=\"font-medium text-[color:var(--mova-ai-ink,#202020)] text-sm\">{label}</p>\n <p className=\"text-[color:var(--mova-ai-ink-secondary,#666)] text-xs tabular-nums\">{done}/{all.length}</p>\n </div>\n <style>{AI_THEME_STYLES}{`\n .ai-task-draw { stroke-dashoffset: 0; }\n @media (prefers-reduced-motion: no-preference) {\n .ai-task-draw { animation: ai-task-draw 200ms cubic-bezier(0.23, 1, 0.32, 1) both; }\n }\n @keyframes ai-task-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n `}</style>\n <ul className=\"list-none\">\n {(() => {\n const renderTasks = (items: AITask[], depth: number): ReactNode => items.map((task) => <Fragment key={task.id}><TaskRow depth={depth} shouldReduceMotion={shouldReduceMotion} task={task} />{renderTasks(task.children ?? [], depth + 1)}</Fragment>);\n return renderTasks(tasks, 0);\n })()}\n </ul>\n </div>\n );\n}\n\nexport default AITaskList;\n"
20
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-tool-call",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "ai-tool-call.tsx",
6
6
  "sourcePath": "ui/ai/ai-tool-call.tsx",
7
7
  "registryFile": "ai-tool-call.json",
@@ -16,5 +16,5 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "previewMode": "client",
19
- "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-tool-call (MIT). See ../patterns/smoothui.LICENSE.\nimport { ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { type ReactNode, useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst BADGE_VIEWBOX = 24;\nconst BADGE_CENTER = BADGE_VIEWBOX / 2;\nconst RING_RADIUS = 8;\nconst CHECK_PATH = \"M 8.5 12.2 L 11 14.8 L 15.8 9.6\";\nconst CROSS_PATHS = [\"M 9 9 L 15 15\", \"M 15 9 L 9 15\"] as const;\nconst SPIN_SECONDS = 0.9;\nconst PENDING_PULSE_SECONDS = 1.6;\n\nexport type AIToolCallStatus = \"pending\" | \"running\" | \"success\" | \"error\";\nexport type AIToolCallProps = {\n args?: ReactNode;\n className?: string;\n defaultOpen?: boolean;\n name: string;\n result?: ReactNode;\n status?: AIToolCallStatus;\n summary?: string;\n};\n\nconst STATUS_LABEL: Record<AIToolCallStatus, string> = {\n error: \"Failed\",\n pending: \"Queued\",\n running: \"Running\",\n success: \"Done\",\n};\n\nfunction AIToolCallBadge({ status, shouldReduceMotion }: { shouldReduceMotion: boolean; status: AIToolCallStatus }) {\n const isRunning = status === \"running\";\n const isPending = status === \"pending\";\n const ringColor = status === \"success\" ? \"oklch(72% 0.17 150)\" : status === \"error\" ? \"oklch(63% 0.21 25)\" : \"currentColor\";\n\n return (\n <span className=\"relative flex size-5 shrink-0 items-center justify-center text-muted-foreground\">\n <svg aria-hidden=\"true\" className=\"size-5 overflow-visible\" viewBox={`0 0 ${BADGE_VIEWBOX} ${BADGE_VIEWBOX}`}>\n <motion.g\n animate={shouldReduceMotion || !isRunning ? undefined : { rotate: 360 }}\n style={{ transformBox: \"view-box\", transformOrigin: \"center\" }}\n transition={{ duration: SPIN_SECONDS, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }}\n >\n <motion.circle\n animate={{\n stroke: ringColor,\n strokeDasharray: isRunning ? \"0.68 0.32\" : \"1 0\",\n strokeOpacity: isPending && !shouldReduceMotion ? [0.35, 1, 0.35] : 1,\n }}\n cx={BADGE_CENTER}\n cy={BADGE_CENTER}\n fill=\"none\"\n pathLength={1}\n r={RING_RADIUS}\n strokeLinecap=\"round\"\n strokeWidth={2}\n transition={{\n stroke: shouldReduceMotion ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT },\n strokeDasharray: shouldReduceMotion ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT },\n strokeOpacity: shouldReduceMotion ? { duration: 0 } : { duration: PENDING_PULSE_SECONDS, repeat: Number.POSITIVE_INFINITY },\n }}\n />\n </motion.g>\n <AnimatePresence initial={false}>\n {status === \"success\" ? (\n <motion.path\n animate={{ opacity: 1, pathLength: 1 }}\n d={CHECK_PATH}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n fill=\"none\"\n initial={shouldReduceMotion ? { opacity: 1, pathLength: 1 } : { opacity: 1, pathLength: 0 }}\n key=\"check\"\n stroke={ringColor}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.2}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.22, ease: EASE_OUT }}\n />\n ) : null}\n {status === \"error\" ? CROSS_PATHS.map((path, index) => (\n <motion.path\n animate={{ opacity: 1, pathLength: 1 }}\n d={path}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n fill=\"none\"\n initial={shouldReduceMotion ? { opacity: 1, pathLength: 1 } : { opacity: 1, pathLength: 0 }}\n key={path}\n stroke={ringColor}\n strokeLinecap=\"round\"\n strokeWidth={2.2}\n transition={shouldReduceMotion ? { duration: 0 } : { delay: index * 0.06, duration: 0.16, ease: EASE_OUT }}\n />\n )) : null}\n </AnimatePresence>\n </svg>\n </span>\n );\n}\n\nexport function AIToolCall({ args, className = \"\", defaultOpen = false, name, result, status = \"pending\", summary }: AIToolCallProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const hasDetail = Boolean(args || result);\n\n return (\n <div className={`w-full overflow-hidden rounded-lg border border-border bg-background ${className}`}>\n <button\n aria-expanded={hasDetail ? isOpen : undefined}\n className=\"flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left disabled:cursor-default focus-visible:outline-2 focus-visible:outline-offset-[-2px] hover:bg-muted/40\"\n disabled={!hasDetail}\n onClick={() => setIsOpen((current) => !current)}\n type=\"button\"\n >\n <AIToolCallBadge shouldReduceMotion={Boolean(shouldReduceMotion)} status={status} />\n <span className=\"min-w-0 flex-1\"><span className=\"block truncate font-medium font-mono text-foreground text-xs\">{name}</span></span>\n {summary ? <span className=\"max-w-[40%] truncate text-muted-foreground text-xs\">{summary}</span> : null}\n <span className=\"sr-only\">{STATUS_LABEL[status]}</span>\n {hasDetail ? (\n <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"flex size-4 shrink-0 items-center justify-center text-muted-foreground\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <ChevronRight aria-hidden=\"true\" size={14} />\n </motion.span>\n ) : null}\n </button>\n\n <AnimatePresence initial={false}>\n {isOpen && hasDetail ? (\n <motion.div\n animate={{ height: \"auto\", opacity: 1 }}\n className=\"overflow-hidden\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { height: 0, opacity: 0 }}\n initial={shouldReduceMotion ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }}\n transition={shouldReduceMotion ? { duration: 0 } : { height: SPRING_DEFAULT, opacity: { duration: 0.18, ease: EASE_OUT } }}\n >\n <div className=\"space-y-2 border-border border-t px-3 py-2.5 text-xs\">\n {args ? <div><p className=\"mb-1 text-[10px] text-muted-foreground uppercase tracking-wide\">Arguments</p><div className=\"overflow-x-auto font-mono text-foreground\">{args}</div></div> : null}\n {result ? <div><p className=\"mb-1 text-[10px] text-muted-foreground uppercase tracking-wide\">Result</p><div className=\"overflow-x-auto text-foreground\">{result}</div></div> : null}\n </div>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AIToolCall;\n"
19
+ "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-tool-call (MIT). See ../patterns/smoothui.LICENSE.\nimport { ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { type ReactNode, useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst BADGE_VIEWBOX = 24;\nconst BADGE_CENTER = BADGE_VIEWBOX / 2;\nconst RING_RADIUS = 8;\nconst CHECK_PATH = \"M 8.5 12.2 L 11 14.8 L 15.8 9.6\";\nconst CROSS_PATHS = [\"M 9 9 L 15 15\", \"M 15 9 L 9 15\"] as const;\nconst SPIN_SECONDS = 0.9;\nconst PENDING_PULSE_SECONDS = 1.6;\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\n\nexport type AIToolCallStatus = \"pending\" | \"running\" | \"success\" | \"error\";\nexport type AIToolCallProps = {\n args?: ReactNode;\n className?: string;\n defaultOpen?: boolean;\n name: string;\n result?: ReactNode;\n status?: AIToolCallStatus;\n summary?: string;\n theme?: \"light\" | \"dark\";\n};\n\nconst STATUS_LABEL: Record<AIToolCallStatus, string> = {\n error: \"Failed\",\n pending: \"Queued\",\n running: \"Running\",\n success: \"Done\",\n};\n\nfunction AIToolCallBadge({ status, shouldReduceMotion }: { shouldReduceMotion: boolean; status: AIToolCallStatus }) {\n const isRunning = status === \"running\";\n const isPending = status === \"pending\";\n const ringColor = status === \"success\" ? \"var(--mova-ai-success,#28734d)\" : status === \"error\" ? \"var(--mova-ai-danger,#a33c3c)\" : \"currentColor\";\n\n return (\n <span className=\"relative flex size-5 shrink-0 items-center justify-center text-muted-foreground\">\n <svg aria-hidden=\"true\" className=\"size-5 overflow-visible\" viewBox={`0 0 ${BADGE_VIEWBOX} ${BADGE_VIEWBOX}`}>\n <motion.g\n animate={shouldReduceMotion || !isRunning ? undefined : { rotate: 360 }}\n style={{ transformBox: \"view-box\", transformOrigin: \"center\" }}\n transition={{ duration: SPIN_SECONDS, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }}\n >\n <motion.circle\n animate={{\n stroke: ringColor,\n strokeDasharray: isRunning ? \"0.68 0.32\" : \"1 0\",\n strokeOpacity: isPending && !shouldReduceMotion ? [0.35, 1, 0.35] : 1,\n }}\n cx={BADGE_CENTER}\n cy={BADGE_CENTER}\n fill=\"none\"\n pathLength={1}\n r={RING_RADIUS}\n strokeLinecap=\"round\"\n strokeWidth={2}\n transition={{\n stroke: shouldReduceMotion ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT },\n strokeDasharray: shouldReduceMotion ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT },\n strokeOpacity: shouldReduceMotion ? { duration: 0 } : { duration: PENDING_PULSE_SECONDS, repeat: Number.POSITIVE_INFINITY },\n }}\n />\n </motion.g>\n <AnimatePresence initial={false}>\n {status === \"success\" ? (\n <motion.path\n animate={{ opacity: 1, pathLength: 1 }}\n d={CHECK_PATH}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n fill=\"none\"\n initial={shouldReduceMotion ? { opacity: 1, pathLength: 1 } : { opacity: 1, pathLength: 0 }}\n key=\"check\"\n stroke={ringColor}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.2}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.22, ease: EASE_OUT }}\n />\n ) : null}\n {status === \"error\" ? CROSS_PATHS.map((path, index) => (\n <motion.path\n animate={{ opacity: 1, pathLength: 1 }}\n d={path}\n exit={{ opacity: 0, transition: { duration: 0.1 } }}\n fill=\"none\"\n initial={shouldReduceMotion ? { opacity: 1, pathLength: 1 } : { opacity: 1, pathLength: 0 }}\n key={path}\n stroke={ringColor}\n strokeLinecap=\"round\"\n strokeWidth={2.2}\n transition={shouldReduceMotion ? { duration: 0 } : { delay: index * 0.06, duration: 0.16, ease: EASE_OUT }}\n />\n )) : null}\n </AnimatePresence>\n </svg>\n </span>\n );\n}\n\nexport function AIToolCall({ args, className = \"\", defaultOpen = false, name, result, status = \"pending\", summary, theme = \"light\" }: AIToolCallProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const hasDetail = Boolean(args || result);\n\n return (\n <div className={`mova-ai-theme w-full overflow-hidden rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] text-[color:var(--mova-ai-ink,#202020)] ${className}`} data-theme={theme}>\n <style>{AI_THEME_STYLES}</style>\n <button\n aria-expanded={hasDetail ? isOpen : undefined}\n className=\"flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left disabled:cursor-default focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--mova-ai-accent,#454545)] hover:bg-[color-mix(in_srgb,var(--mova-ai-hover,#f1f1f1)_70%,transparent)]\"\n disabled={!hasDetail}\n onClick={() => setIsOpen((current) => !current)}\n type=\"button\"\n >\n <AIToolCallBadge shouldReduceMotion={Boolean(shouldReduceMotion)} status={status} />\n <span className=\"min-w-0 flex-1\"><span className=\"block truncate font-medium font-mono text-[color:var(--mova-ai-ink,#202020)] text-xs\">{name}</span></span>\n {summary ? <span className=\"max-w-[40%] truncate text-[color:var(--mova-ai-ink-secondary,#666)] text-xs\">{summary}</span> : null}\n <span className=\"sr-only\">{STATUS_LABEL[status]}</span>\n {hasDetail ? (\n <motion.span animate={{ rotate: isOpen ? 90 : 0 }} className=\"flex size-4 shrink-0 items-center justify-center text-[color:var(--mova-ai-ink-secondary,#666666)]\" transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n <ChevronRight aria-hidden=\"true\" size={14} />\n </motion.span>\n ) : null}\n </button>\n\n <AnimatePresence initial={false}>\n {isOpen && hasDetail ? (\n <motion.div\n animate={{ height: \"auto\", opacity: 1 }}\n className=\"overflow-hidden\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { height: 0, opacity: 0 }}\n initial={shouldReduceMotion ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }}\n transition={shouldReduceMotion ? { duration: 0 } : { height: SPRING_DEFAULT, opacity: { duration: 0.18, ease: EASE_OUT } }}\n >\n <div className=\"space-y-2 border-[var(--mova-ai-border,#dedede)] border-t px-3 py-2.5 text-xs\">\n {args ? <div><p className=\"mb-1 text-[10px] text-[color:var(--mova-ai-ink-secondary,#666666)] uppercase tracking-wide\">Arguments</p><div className=\"overflow-x-auto font-mono text-[color:var(--mova-ai-ink,#202020)]\">{args}</div></div> : null}\n {result ? <div><p className=\"mb-1 text-[10px] text-[color:var(--mova-ai-ink-secondary,#666666)] uppercase tracking-wide\">Result</p><div className=\"overflow-x-auto text-[color:var(--mova-ai-ink,#202020)]\">{result}</div></div> : null}\n </div>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AIToolCall;\n"
20
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "analyzing-image",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "analyzing-image.tsx",
6
6
  "sourcePath": "ui/loading/analyzing-image.tsx",
7
7
  "registryFile": "analyzing-image.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "animated-sidebar",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "animated-sidebar.tsx",
6
6
  "sourcePath": "ui/animated-sidebar.tsx",
7
7
  "registryFile": "animated-sidebar.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "animated-tags",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "animated-tags.tsx",
6
6
  "sourcePath": "ui/patterns/animated-tags.tsx",
7
7
  "registryFile": "animated-tags.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "app-icon-load",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "app-icon-load.tsx",
6
6
  "sourcePath": "ui/loading/app-icon-load.tsx",
7
7
  "registryFile": "app-icon-load.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-breathe",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-breathe.tsx",
6
6
  "sourcePath": "ui/loading/apple-breathe.tsx",
7
7
  "registryFile": "apple-breathe.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-equalizer",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-equalizer.tsx",
6
6
  "sourcePath": "ui/loading/apple-equalizer.tsx",
7
7
  "registryFile": "apple-equalizer.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-icon-morph",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-icon-morph.tsx",
6
6
  "sourcePath": "ui/loading/apple-icon-morph.tsx",
7
7
  "registryFile": "apple-icon-morph.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-pulse-dots",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-pulse-dots.tsx",
6
6
  "sourcePath": "ui/loading/apple-pulse-dots.tsx",
7
7
  "registryFile": "apple-pulse-dots.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-scale-pulse",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-scale-pulse.tsx",
6
6
  "sourcePath": "ui/loading/apple-scale-pulse.tsx",
7
7
  "registryFile": "apple-scale-pulse.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-sound-wave",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-sound-wave.tsx",
6
6
  "sourcePath": "ui/loading/apple-sound-wave.tsx",
7
7
  "registryFile": "apple-sound-wave.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-text-reveal",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-text-reveal.tsx",
6
6
  "sourcePath": "ui/loading/apple-text-reveal.tsx",
7
7
  "registryFile": "apple-text-reveal.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "apple-unlock",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "apple-unlock.tsx",
6
6
  "sourcePath": "ui/loading/apple-unlock.tsx",
7
7
  "registryFile": "apple-unlock.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "arc-tracer",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "arc-tracer.tsx",
6
6
  "sourcePath": "ui/loading/arc-tracer.tsx",
7
7
  "registryFile": "arc-tracer.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bar-cascade",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bar-cascade.tsx",
6
6
  "sourcePath": "ui/loading/bar-cascade.tsx",
7
7
  "registryFile": "bar-cascade.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bar-sweep",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bar-sweep.tsx",
6
6
  "sourcePath": "ui/loading/bar-sweep.tsx",
7
7
  "registryFile": "bar-sweep.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "blur-text",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "blur-text.tsx",
6
6
  "sourcePath": "ui/text/blur-text.tsx",
7
7
  "registryFile": "blur-text.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "blur-up-char",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "blur-up-char.tsx",
6
6
  "registryFile": "blur-up-char.json",
7
7
  "exports": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "blur-up-word",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "blur-up-word.tsx",
6
6
  "registryFile": "blur-up-word.json",
7
7
  "exports": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bobbing-dots",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bobbing-dots.tsx",
6
6
  "sourcePath": "ui/loading/bobbing-dots.tsx",
7
7
  "registryFile": "bobbing-dots.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bookmark-corner-peel",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bookmark-corner-peel.tsx",
6
6
  "sourcePath": "components/css-animations/whimsical/ConceptTrios1.tsx",
7
7
  "registryFile": "bookmark-corner-peel.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bookmark-save-pill",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bookmark-save-pill.tsx",
6
6
  "sourcePath": "components/css-animations/yui-components/RedesignedUiTrios.tsx",
7
7
  "registryFile": "bookmark-save-pill.json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bookmark-toggle",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "bookmark-toggle.tsx",
6
6
  "registryFile": "bookmark-toggle.json",
7
7
  "exports": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "border-trail-loading",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.5",
5
5
  "file": "border-trail-loading.tsx",
6
6
  "sourcePath": "ui/loading/border-trail-loading.tsx",
7
7
  "registryFile": "border-trail-loading.json",