@sableui/react 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTING.md +7 -5
  4. package/DESIGN.md +8 -3
  5. package/bin/sable.mjs +17 -1
  6. package/dist/blocks/approval-block.js +93 -0
  7. package/dist/blocks/approval-block.meta.js +23 -0
  8. package/dist/blocks/index.d.ts +17 -2
  9. package/dist/blocks/index.js +4 -0
  10. package/dist/components/accordion/accordion.meta.js +10 -3
  11. package/dist/components/area-chart/area-chart.meta.js +10 -3
  12. package/dist/components/attachment/attachment.meta.js +26 -0
  13. package/dist/components/badge/badge.meta.js +2 -1
  14. package/dist/components/banner/banner.meta.js +4 -2
  15. package/dist/components/bar-chart/bar-chart.meta.js +11 -4
  16. package/dist/components/bottom-nav/bottom-nav.js +3 -1
  17. package/dist/components/bottom-nav/bottom-nav.meta.js +32 -0
  18. package/dist/components/breadcrumbs/breadcrumbs.meta.js +26 -0
  19. package/dist/components/button/button.js +21 -4
  20. package/dist/components/button/button.meta.js +7 -7
  21. package/dist/components/button-group/button-group.meta.js +2 -1
  22. package/dist/components/calendar/calendar.js +18 -1
  23. package/dist/components/calendar/calendar.meta.js +38 -0
  24. package/dist/components/carousel/carousel.js +6 -4
  25. package/dist/components/carousel/carousel.meta.js +42 -0
  26. package/dist/components/chat-composer/chat-composer.js +21 -21
  27. package/dist/components/chat-composer/chat-composer.meta.js +38 -0
  28. package/dist/components/chat-composer/use-eased-growth.js +70 -0
  29. package/dist/components/checkbox/checkbox-mark.js +8 -1
  30. package/dist/components/checkbox/checkbox.js +3 -1
  31. package/dist/components/checkbox/checkbox.meta.js +34 -1
  32. package/dist/components/checkbox-card/checkbox-card.js +3 -0
  33. package/dist/components/checkbox-card/checkbox-card.meta.js +32 -0
  34. package/dist/components/code-block/code-block.js +14 -3
  35. package/dist/components/code-block/code-block.meta.js +28 -1
  36. package/dist/components/collapsible/collapsible.meta.js +34 -1
  37. package/dist/components/color-field/color-field.meta.js +26 -0
  38. package/dist/components/combobox/combobox.meta.js +48 -2
  39. package/dist/components/command-menu/command-menu.js +8 -3
  40. package/dist/components/command-menu/command-menu.meta.js +44 -3
  41. package/dist/components/confirm-button/confirm-button.js +179 -0
  42. package/dist/components/confirm-button/confirm-button.meta.js +143 -0
  43. package/dist/components/confirm-button/index.js +7 -0
  44. package/dist/components/context-card/context-card.meta.js +2 -1
  45. package/dist/components/data-table/data-table.js +6 -2
  46. package/dist/components/data-table/data-table.meta.js +32 -0
  47. package/dist/components/dialog/dialog.meta.js +6 -3
  48. package/dist/components/disclosure-picker/disclosure-picker.meta.js +2 -1
  49. package/dist/components/entity/entity.meta.js +26 -0
  50. package/dist/components/feedback/feedback.js +113 -47
  51. package/dist/components/feedback/feedback.meta.js +49 -4
  52. package/dist/components/inline-status/inline-status.meta.js +2 -1
  53. package/dist/components/input-group/input-group.meta.js +27 -1
  54. package/dist/components/line-chart/line-chart.meta.js +10 -3
  55. package/dist/components/menu/menu.meta.js +2 -1
  56. package/dist/components/menubar/menubar.meta.js +2 -1
  57. package/dist/components/meter/meter.meta.js +2 -1
  58. package/dist/components/middle-truncate/middle-truncate.meta.js +1 -1
  59. package/dist/components/note/note.meta.js +34 -1
  60. package/dist/components/notification-badge/notification-badge.js +17 -5
  61. package/dist/components/notification-badge/notification-badge.meta.js +34 -2
  62. package/dist/components/number-ticker/index.js +7 -0
  63. package/dist/components/number-ticker/number-ticker.js +246 -0
  64. package/dist/components/number-ticker/number-ticker.meta.js +159 -0
  65. package/dist/components/otp-field/otp-field.js +23 -3
  66. package/dist/components/otp-field/otp-field.meta.js +23 -2
  67. package/dist/components/page-loader/page-loader.meta.js +2 -1
  68. package/dist/components/panel-group/panel-group.meta.js +26 -0
  69. package/dist/components/panel-section/panel-section.js +37 -10
  70. package/dist/components/panel-section/panel-section.meta.js +32 -0
  71. package/dist/components/pie-chart/pie-chart.js +20 -11
  72. package/dist/components/pie-chart/pie-chart.meta.js +17 -4
  73. package/dist/components/pill/pill.meta.js +34 -1
  74. package/dist/components/popover/popover.meta.js +4 -2
  75. package/dist/components/progress/progress.meta.js +2 -1
  76. package/dist/components/radar-chart/radar-chart.js +4 -3
  77. package/dist/components/radar-chart/radar-chart.meta.js +10 -3
  78. package/dist/components/radial-chart/radial-chart.js +22 -22
  79. package/dist/components/radial-chart/radial-chart.meta.js +17 -4
  80. package/dist/components/radio/radio.meta.js +28 -1
  81. package/dist/components/radio-card/radio-card.meta.js +38 -0
  82. package/dist/components/relative-time/relative-time.meta.js +32 -0
  83. package/dist/components/scroller/scroller.meta.js +32 -0
  84. package/dist/components/scrub-field/scrub-field.meta.js +32 -0
  85. package/dist/components/search-input/search-input.js +9 -7
  86. package/dist/components/search-input/search-input.meta.js +26 -0
  87. package/dist/components/segmented-bar/segmented-bar.meta.js +2 -1
  88. package/dist/components/select/select.meta.js +4 -2
  89. package/dist/components/sheet/sheet.meta.js +4 -2
  90. package/dist/components/show-more/show-more.js +1 -4
  91. package/dist/components/show-more/show-more.meta.js +38 -0
  92. package/dist/components/side-nav/side-nav.js +21 -1
  93. package/dist/components/side-nav/side-nav.meta.js +34 -1
  94. package/dist/components/side-nav/side-nav.variants.js +3 -1
  95. package/dist/components/sidebar/sidebar.js +31 -8
  96. package/dist/components/sidebar/sidebar.meta.js +38 -0
  97. package/dist/components/skeleton/skeleton.meta.js +2 -1
  98. package/dist/components/slider/slider.meta.js +26 -0
  99. package/dist/components/sparkline/sparkline.js +7 -4
  100. package/dist/components/sparkline/sparkline.meta.js +9 -2
  101. package/dist/components/spinner/spinner.meta.js +2 -1
  102. package/dist/components/stepper/stepper.meta.js +4 -2
  103. package/dist/components/switch/switch.meta.js +2 -1
  104. package/dist/components/table/table.meta.js +28 -1
  105. package/dist/components/tabs/tabs.js +27 -8
  106. package/dist/components/tabs/tabs.meta.js +50 -5
  107. package/dist/components/theme-selector/theme-selector.meta.js +32 -0
  108. package/dist/components/theme-switcher/theme-switcher.js +4 -1
  109. package/dist/components/theme-switcher/theme-switcher.meta.js +42 -0
  110. package/dist/components/thinking-orb/thinking-orb.meta.js +4 -2
  111. package/dist/components/toast/toast.meta.js +2 -1
  112. package/dist/components/toggle/toggle.js +28 -5
  113. package/dist/components/toggle/toggle.meta.js +46 -3
  114. package/dist/components/tool-call/index.js +8 -0
  115. package/dist/components/tool-call/tool-call.js +117 -0
  116. package/dist/components/tool-call/tool-call.meta.js +147 -0
  117. package/dist/components/tool-call/tool-call.variants.js +33 -0
  118. package/dist/components/toolbar/toolbar.meta.js +2 -1
  119. package/dist/components/tooltip/index.js +3 -1
  120. package/dist/components/tooltip/tooltip.js +84 -0
  121. package/dist/components/tooltip/tooltip.meta.js +18 -2
  122. package/dist/components/tree/tree.js +71 -6
  123. package/dist/components/tree/tree.meta.js +32 -0
  124. package/dist/components/video/video.meta.js +32 -0
  125. package/dist/file-hashes.json +139 -122
  126. package/dist/foundations/motion.js +49 -3
  127. package/dist/index.d.ts +616 -46
  128. package/dist/index.js +4 -1
  129. package/dist/lib/chart/cartesian.js +9 -10
  130. package/dist/lib/chart/chart-center.js +30 -0
  131. package/dist/lib/chart/chart-frame.js +15 -8
  132. package/dist/lib/chart/dim.js +13 -0
  133. package/dist/lib/chart/format.js +7 -1
  134. package/dist/lib/chart/motion.js +6 -4
  135. package/dist/lib/highlight-glide.js +68 -10
  136. package/dist/lib/taxonomy.js +3 -0
  137. package/dist/{meta-DVG8ceze.d.ts → meta-DX5r5wQL.d.ts} +8 -0
  138. package/dist/motion/MotionProvider.js +45 -3
  139. package/dist/motion/Reveal.js +1 -1
  140. package/dist/motion/TextReveal.js +1 -1
  141. package/dist/motion/index.js +15 -2
  142. package/dist/motion/useAutoSize.js +42 -0
  143. package/dist/motion/useShake.js +25 -0
  144. package/dist/patterns/index.d.ts +1 -1
  145. package/dist/styles.css +298 -5
  146. package/llms/blocks/approval-block.md +128 -0
  147. package/llms/components/accordion.md +4 -3
  148. package/llms/components/area-chart.md +4 -3
  149. package/llms/components/attachment.md +7 -0
  150. package/llms/components/badge.md +1 -1
  151. package/llms/components/banner.md +2 -2
  152. package/llms/components/bar-chart.md +5 -4
  153. package/llms/components/bottom-nav.md +8 -0
  154. package/llms/components/breadcrumbs.md +7 -0
  155. package/llms/components/button-group.md +1 -1
  156. package/llms/components/button.md +6 -6
  157. package/llms/components/calendar.md +9 -0
  158. package/llms/components/carousel.md +17 -0
  159. package/llms/components/chat-composer.md +9 -0
  160. package/llms/components/checkbox-card.md +8 -0
  161. package/llms/components/checkbox.md +9 -1
  162. package/llms/components/code-block.md +8 -0
  163. package/llms/components/collapsible.md +9 -1
  164. package/llms/components/color-field.md +7 -0
  165. package/llms/components/combobox.md +12 -2
  166. package/llms/components/command-menu.md +12 -3
  167. package/llms/components/confirm-button.md +92 -0
  168. package/llms/components/context-card.md +1 -1
  169. package/llms/components/data-table.md +8 -0
  170. package/llms/components/dialog.md +3 -3
  171. package/llms/components/disclosure-picker.md +1 -1
  172. package/llms/components/entity.md +7 -0
  173. package/llms/components/feedback.md +22 -3
  174. package/llms/components/inline-status.md +1 -1
  175. package/llms/components/input-group.md +8 -1
  176. package/llms/components/line-chart.md +4 -3
  177. package/llms/components/menu.md +1 -1
  178. package/llms/components/menubar.md +1 -1
  179. package/llms/components/meter.md +1 -1
  180. package/llms/components/middle-truncate.md +1 -1
  181. package/llms/components/note.md +9 -1
  182. package/llms/components/notification-badge.md +10 -2
  183. package/llms/components/number-ticker.md +86 -0
  184. package/llms/components/otp-field.md +15 -1
  185. package/llms/components/page-loader.md +1 -1
  186. package/llms/components/panel-group.md +7 -0
  187. package/llms/components/panel-section.md +8 -0
  188. package/llms/components/pie-chart.md +6 -4
  189. package/llms/components/pill.md +9 -1
  190. package/llms/components/popover.md +2 -2
  191. package/llms/components/progress.md +1 -1
  192. package/llms/components/radar-chart.md +4 -3
  193. package/llms/components/radial-chart.md +6 -4
  194. package/llms/components/radio-card.md +9 -0
  195. package/llms/components/radio.md +8 -1
  196. package/llms/components/relative-time.md +8 -0
  197. package/llms/components/scroller.md +8 -0
  198. package/llms/components/scrub-field.md +8 -0
  199. package/llms/components/search-input.md +7 -0
  200. package/llms/components/segmented-bar.md +1 -1
  201. package/llms/components/select.md +2 -2
  202. package/llms/components/sheet.md +2 -2
  203. package/llms/components/show-more.md +9 -0
  204. package/llms/components/side-nav.md +9 -1
  205. package/llms/components/sidebar.md +9 -0
  206. package/llms/components/skeleton.md +1 -1
  207. package/llms/components/slider.md +7 -0
  208. package/llms/components/sparkline.md +3 -2
  209. package/llms/components/spinner.md +1 -1
  210. package/llms/components/stepper.md +2 -2
  211. package/llms/components/switch.md +1 -1
  212. package/llms/components/table.md +8 -1
  213. package/llms/components/tabs.md +22 -4
  214. package/llms/components/theme-selector.md +8 -0
  215. package/llms/components/theme-switcher.md +17 -0
  216. package/llms/components/thinking-orb.md +2 -2
  217. package/llms/components/toast.md +1 -1
  218. package/llms/components/toggle.md +20 -3
  219. package/llms/components/tool-call.md +115 -0
  220. package/llms/components/toolbar.md +1 -1
  221. package/llms/components/tooltip.md +12 -2
  222. package/llms/components/tree.md +8 -0
  223. package/llms/components/video.md +8 -0
  224. package/llms/foundations/motion.md +48 -3
  225. package/llms/reference/utilities.md +23 -1
  226. package/llms/tasks/reviewing-motion.md +14 -0
  227. package/llms-full.txt +1004 -98
  228. package/llms.txt +16 -11
  229. package/package.json +1 -1
  230. package/registry.json +2708 -187
  231. package/targets/ios.json +27 -0
package/registry.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://sable.design/registry.schema.json",
3
3
  "name": "@sableui/react",
4
- "version": "0.13.1",
4
+ "version": "0.14.0",
5
5
  "description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
6
6
  "engine": {
7
7
  "behavior": "@base-ui/react",
@@ -949,6 +949,12 @@
949
949
  "motion-slide-right",
950
950
  "motion-disclosure",
951
951
  "motion-icon-swap",
952
+ "motion-glide",
953
+ "motion-shake",
954
+ "motion-tick",
955
+ "motion-appear",
956
+ "motion-arrive",
957
+ "motion-size",
952
958
  "motion-reveal",
953
959
  "motion-draw",
954
960
  "motion-spin",
@@ -966,6 +972,7 @@
966
972
  ],
967
973
  "policy": [
968
974
  "data-motion=full",
975
+ "data-motion=subtle",
969
976
  "data-motion=reduced",
970
977
  "data-motion=off",
971
978
  "prefers-reduced-motion"
@@ -2706,7 +2713,10 @@
2706
2713
  },
2707
2714
  {
2708
2715
  "guidance": "dont",
2709
- "description": "Do not nest an Accordion inside an Accordion. Two layers of roving focus fight each other, and the arrow keys become ambiguous — flatten the hierarchy or use a Tree."
2716
+ "description": "Do not nest an Accordion inside an Accordion. Two layers of roving focus fight each other, and the arrow keys become ambiguous — flatten the hierarchy or use a Tree.",
2717
+ "instead": [
2718
+ "Tree"
2719
+ ]
2710
2720
  }
2711
2721
  ],
2712
2722
  "anatomy": [
@@ -2806,6 +2816,12 @@
2806
2816
  "part": "AccordionPanel",
2807
2817
  "why": "Panel height is the only cue that content came from THIS header rather than appearing at the bottom of the page. Snapping it open moves everything below by a screenful with nothing connecting cause to effect."
2808
2818
  },
2819
+ {
2820
+ "slot": "transition",
2821
+ "recipe": "motion-interactive",
2822
+ "part": "Chevron",
2823
+ "why": "The chevron turning is the header's half of the same event as the panel opening. motion-interactive lists `rotate` (what Tailwind's rotate-* writes), so it turns with the panel instead of flipping 180° in one frame beside an easing panel. A custom `indicator` that rotates should carry motion-interactive itself."
2824
+ },
2809
2825
  {
2810
2826
  "slot": "activity",
2811
2827
  "recipe": "none",
@@ -2818,7 +2834,7 @@
2818
2834
  "Arrow keys move roving focus between triggers; Home/End jump to the first/last; loopFocus wraps by default.",
2819
2835
  "Each trigger toggles its panel with Enter/Space; the panel is announced as a labelled region.",
2820
2836
  "A disabled AccordionItem dims and is skipped by keyboard focus while neighbouring items stay interactive.",
2821
- "The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible — both honor reduced-motion via the consumer's transitions."
2837
+ "The chevron rotation is driven by data-[panel-open] and the focus ring by focus-visible; both ride motion-interactive, so reduced motion stills them with everything else."
2822
2838
  ],
2823
2839
  "examples": [
2824
2840
  {
@@ -2833,7 +2849,7 @@
2833
2849
  },
2834
2850
  {
2835
2851
  "label": "Custom indicator (plus-becomes-×)",
2836
- "code": "<Accordion defaultValue={[\"a\"]}><AccordionItem value=\"a\"><AccordionTrigger indicator={<Icon name=\"plus\" className=\"text-xs text-muted-foreground transition-transform [[data-panel-open]_&]:rotate-45\" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>",
2852
+ "code": "<Accordion defaultValue={[\"a\"]}><AccordionItem value=\"a\"><AccordionTrigger indicator={<Icon name=\"plus\" className=\"text-xs text-muted-foreground motion-interactive [[data-panel-open]_&]:rotate-45\" />}>Can I cancel anytime?</AccordionTrigger><AccordionPanel>Yes — from billing settings.</AccordionPanel></AccordionItem></Accordion>",
2837
2853
  "imports": "import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Icon } from \"@sableui/react\""
2838
2854
  }
2839
2855
  ]
@@ -3211,11 +3227,18 @@
3211
3227
  "default": "false for columns; true for horizontal bars",
3212
3228
  "description": "The vertical axis."
3213
3229
  },
3230
+ {
3231
+ "name": "motion",
3232
+ "type": "\"full\" | \"subtle\" | \"off\"",
3233
+ "default": "\"full\"",
3234
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The arrival (draw-on, rising fill) and the data-change morph are the flourish; the gliding cursor and tooltip are the feedback `subtle` keeps."
3235
+ },
3214
3236
  {
3215
3237
  "name": "animate",
3216
3238
  "type": "boolean",
3217
3239
  "default": "true",
3218
- "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
3240
+ "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion=\"subtle\"`.",
3241
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
3219
3242
  },
3220
3243
  {
3221
3244
  "name": "activeIndex",
@@ -3270,7 +3293,7 @@
3270
3293
  "slot": "arrival",
3271
3294
  "recipe": "custom",
3272
3295
  "part": "Area + line",
3273
- "why": "Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart."
3296
+ "why": "Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
3274
3297
  },
3275
3298
  {
3276
3299
  "slot": "response",
@@ -3282,7 +3305,7 @@
3282
3305
  "slot": "transition",
3283
3306
  "recipe": "custom",
3284
3307
  "part": "Area + line",
3285
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
3308
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
3286
3309
  },
3287
3310
  {
3288
3311
  "slot": "activity",
@@ -3473,6 +3496,32 @@
3473
3496
  "overlay-foreground",
3474
3497
  "ring"
3475
3498
  ],
3499
+ "motion": [
3500
+ {
3501
+ "slot": "arrival",
3502
+ "recipe": "none",
3503
+ "part": "Root",
3504
+ "why": "An attachment lands as part of a composer or a message, and that surface owns the entrance. A card that animated in on its own would read as a second, separate event — as if another file had been added."
3505
+ },
3506
+ {
3507
+ "slot": "response",
3508
+ "recipe": "motion-interactive",
3509
+ "part": "Root",
3510
+ "why": "With `onOpen`, the edge steps up under the pointer; it is the only cue that the card itself opens the file, not just its actions. Eased, it reads as the card answering the cursor rather than a border flickering as the pointer crosses a busy composer."
3511
+ },
3512
+ {
3513
+ "slot": "transition",
3514
+ "recipe": "motion-interactive",
3515
+ "part": "Root",
3516
+ "why": "Uploading to failed re-colours the edge and fill without changing the shape. The ease keeps it legible as the same file turning red; snapped, a failed card looks like a different card replaced the one that was sent."
3517
+ },
3518
+ {
3519
+ "slot": "activity",
3520
+ "recipe": "motion-pulse",
3521
+ "part": "Progress",
3522
+ "why": "While uploading with no known `progress`, the bar is indeterminate and pulses (Progress's own recipe); with a number, the fill eases toward each new value. A still bar at a fixed width is indistinguishable from an upload that has stalled."
3523
+ }
3524
+ ],
3476
3525
  "a11y": [
3477
3526
  "The full-card trigger is a real stretched `<button>` with an accessible label, not a click handler on a div — so it is one tab stop, keyboard-activatable, and focus-ringed.",
3478
3527
  "Actions sit above that trigger in the stacking order, which keeps them individually focusable and stops a click on `remove` from also opening the file. Text is only made click-through when a trigger is present, so a filename stays selectable otherwise.",
@@ -3819,7 +3868,11 @@
3819
3868
  },
3820
3869
  {
3821
3870
  "guidance": "dont",
3822
- "description": "Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item."
3871
+ "description": "Use a Badge as a button. If it triggers an action, use Button, Toggle, or a menu item.",
3872
+ "instead": [
3873
+ "Button",
3874
+ "Toggle"
3875
+ ]
3823
3876
  },
3824
3877
  {
3825
3878
  "guidance": "do",
@@ -4047,11 +4100,18 @@
4047
4100
  },
4048
4101
  {
4049
4102
  "guidance": "dont",
4050
- "description": "Use Banner for field-level validation, row status, or small local hints; use Field, InlineStatus, or Badge instead."
4103
+ "description": "Use Banner for field-level validation, row status, or small local hints; use Field, InlineStatus, or Badge instead.",
4104
+ "instead": [
4105
+ "InlineStatus",
4106
+ "Badge"
4107
+ ]
4051
4108
  },
4052
4109
  {
4053
4110
  "guidance": "dont",
4054
- "description": "Use Banner inside a dialog, a form, or a section to qualify the content around it — that is Note. Banner is a full-width announcement strip for page- or app-level state, so in a modal it reads as a slab heavier than the fields it warns about, and its soft appearance has no border and no tone-colored icon: the severity survives only as a faint wash."
4111
+ "description": "Use Banner inside a dialog, a form, or a section to qualify the content around it — that is Note. Banner is a full-width announcement strip for page- or app-level state, so in a modal it reads as a slab heavier than the fields it warns about, and its soft appearance has no border and no tone-colored icon: the severity survives only as a faint wash.",
4112
+ "instead": [
4113
+ "Note"
4114
+ ]
4055
4115
  }
4056
4116
  ],
4057
4117
  "anatomy": [
@@ -4326,11 +4386,18 @@
4326
4386
  "default": "false for columns; true for horizontal bars",
4327
4387
  "description": "The vertical axis."
4328
4388
  },
4389
+ {
4390
+ "name": "motion",
4391
+ "type": "\"full\" | \"subtle\" | \"off\"",
4392
+ "default": "\"full\"",
4393
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The arrival (bars rising in sequence) and the data-change morph are the flourish; the hover dim and gliding cursor are the feedback `subtle` keeps."
4394
+ },
4329
4395
  {
4330
4396
  "name": "animate",
4331
4397
  "type": "boolean",
4332
4398
  "default": "true",
4333
- "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
4399
+ "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion=\"subtle\"`.",
4400
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
4334
4401
  },
4335
4402
  {
4336
4403
  "name": "activeIndex",
@@ -4408,19 +4475,19 @@
4408
4475
  "slot": "arrival",
4409
4476
  "recipe": "custom",
4410
4477
  "part": "Bar",
4411
- "why": "Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart."
4478
+ "why": "Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
4412
4479
  },
4413
4480
  {
4414
4481
  "slot": "response",
4415
4482
  "recipe": "none",
4416
4483
  "part": "Cursor + tooltip",
4417
- "why": "The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart."
4484
+ "why": "The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart. The dim mixes each bar toward the surface instead of fading it, so gridlines never strike through a dimmed column and stacked segments stay solid. Essential: `subtle` keeps it."
4418
4485
  },
4419
4486
  {
4420
4487
  "slot": "transition",
4421
4488
  "recipe": "custom",
4422
4489
  "part": "Bar",
4423
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
4490
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
4424
4491
  },
4425
4492
  {
4426
4493
  "slot": "activity",
@@ -4520,6 +4587,38 @@
4520
4587
  "foreground",
4521
4588
  "ring"
4522
4589
  ],
4590
+ "motion": [
4591
+ {
4592
+ "slot": "arrival",
4593
+ "recipe": "none",
4594
+ "part": "Root",
4595
+ "why": "Fixed app chrome, present from the first paint of every screen. Animating it in on navigation would make each route change look like the app restarting."
4596
+ },
4597
+ {
4598
+ "slot": "response",
4599
+ "recipe": "motion-interactive",
4600
+ "part": "BottomNavItem",
4601
+ "why": "The ink steps up on hover and the focus ring eases in, so a pointer or a keyboard on a tablet gets the same answer as every other control in the app."
4602
+ },
4603
+ {
4604
+ "slot": "response",
4605
+ "recipe": "motion-press",
4606
+ "part": "BottomNavItem",
4607
+ "why": "On a phone there is no hover to say a tap landed, and a route can take a beat to paint. The settle on press is the receipt; without it people tap the tab a second time."
4608
+ },
4609
+ {
4610
+ "slot": "transition",
4611
+ "recipe": "motion-interactive",
4612
+ "part": "BottomNavItem",
4613
+ "why": "Changing tab eases the active ink out of one item and into the next. It is a cross-fade, not a travelling indicator: each item owns its colour, so there is no single mark to glide."
4614
+ },
4615
+ {
4616
+ "slot": "activity",
4617
+ "recipe": "none",
4618
+ "part": "Root",
4619
+ "why": "Navigation has no working state of its own. A slow route shows its pending state in the page it is loading, not in the tab bar."
4620
+ }
4621
+ ],
4523
4622
  "a11y": [
4524
4623
  "BottomNav renders a <nav> landmark.",
4525
4624
  "Each tab is a 44px minimum touch target with an aria-label.",
@@ -4634,6 +4733,32 @@
4634
4733
  "muted-foreground",
4635
4734
  "foreground"
4636
4735
  ],
4736
+ "motion": [
4737
+ {
4738
+ "slot": "arrival",
4739
+ "recipe": "none",
4740
+ "part": "Root",
4741
+ "why": "The trail is page furniture, rendered with the page header. An entrance would replay on every navigation, which is exactly when the reader looks at it to get their bearings."
4742
+ },
4743
+ {
4744
+ "slot": "response",
4745
+ "recipe": "motion-interactive",
4746
+ "part": "BreadcrumbLink",
4747
+ "why": "Ancestors are muted until hovered or focused. Easing the ink makes the trail read as links answering the pointer; a hard snap across a row of small text looks like flicker."
4748
+ },
4749
+ {
4750
+ "slot": "transition",
4751
+ "recipe": "none",
4752
+ "part": "Root",
4753
+ "why": "A breadcrumb has no state of its own. The current page changes by navigation, which replaces the trail; animating between two trails would imply the path itself moved."
4754
+ },
4755
+ {
4756
+ "slot": "activity",
4757
+ "recipe": "none",
4758
+ "part": "Root",
4759
+ "why": "It never works in the background. A route that is still loading says so in the page, not in its address."
4760
+ }
4761
+ ],
4637
4762
  "a11y": [
4638
4763
  "The <nav> carries aria-label=\"Breadcrumb\" so assistive tech announces the landmark.",
4639
4764
  "Crumbs sit in an ordered list (<ol>) to convey hierarchy and position.",
@@ -4944,7 +5069,7 @@
4944
5069
  },
4945
5070
  {
4946
5071
  "guidance": "do",
4947
- "description": "For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click."
5072
+ "description": "For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`. Text that costs something to produce (a file to fetch) goes in as a function, `useCopy(() => fetch(url).then((r) => r.text()))`, called only on click. Keep both glyphs mounted in a `motion-icon-swap` cell so the copy mark visibly becomes a check (a `copied ? \"check\" : \"copy\"` swap snaps between frames and is easy to miss), and say \"Copied\" in a polite `role=\"status\"` region: the glyph change is silent to a screen reader."
4948
5073
  },
4949
5074
  {
4950
5075
  "guidance": "do",
@@ -5045,7 +5170,7 @@
5045
5170
  "name": "loading",
5046
5171
  "type": "boolean",
5047
5172
  "default": "false",
5048
- "description": "Show a leading Spinner while an action is in flight and refuse presses. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it."
5173
+ "description": "Show a Spinner while an action is in flight and refuse presses. The spinner takes the iconStart slot when there is one; otherwise it lays over the label, which fades out in place, so the button keeps its width. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it."
5049
5174
  },
5050
5175
  {
5051
5176
  "name": "onClick",
@@ -5060,7 +5185,7 @@
5060
5185
  {
5061
5186
  "name": "loadingLabel",
5062
5187
  "type": "ReactNode",
5063
- "description": "Optional text shown in place of children while loading."
5188
+ "description": "Optional text shown in place of children while loading, with the spinner beside it. The width follows the new text, so prefer no loadingLabel where the row must not move."
5064
5189
  },
5065
5190
  {
5066
5191
  "name": "iconStart",
@@ -5138,9 +5263,9 @@
5138
5263
  },
5139
5264
  {
5140
5265
  "slot": "transition",
5141
- "recipe": "none",
5142
- "part": "Root",
5143
- "why": "A button has no state of its own to move between. A control that toggles is a Switch or a Toggle, and those carry the transition."
5266
+ "recipe": "motion-appear",
5267
+ "part": "Spinner",
5268
+ "why": "Going busy is the one state change a button has. The spinner fades in where it mounts — in the icon slot, or laid over a plain label that fades out beneath it and keeps its place — so the width never changes and the row does not shift under the pointer that just pressed it. The label fades back when the work settles. A toggling control is a Switch or a Toggle; those carry their own transition."
5144
5269
  },
5145
5270
  {
5146
5271
  "slot": "activity",
@@ -5181,8 +5306,8 @@
5181
5306
  },
5182
5307
  {
5183
5308
  "label": "Copy to clipboard",
5184
- "code": "const { copy, copied } = useCopy(address)\n<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Copy address\" onClick={copy}><Icon name={copied ? \"check\" : \"copy\"} /></Button>",
5185
- "imports": "import { Button, Icon, useCopy } from \"@sableui/react\""
5309
+ "code": "const { copy, copied } = useCopy(address)\n<Row gap={1}><Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Copy address\" onClick={copy}><span aria-hidden=\"true\" className=\"motion-icon-swap\"><span data-active={!copied} className=\"inline-flex\"><Icon name=\"copy\" /></span><span data-active={copied} className=\"inline-flex\"><Icon name=\"check\" /></span></span></Button><span role=\"status\" className=\"sr-only\">{copied ? \"Copied\" : \"\"}</span></Row>",
5310
+ "imports": "import { Button, Icon, Row, useCopy } from \"@sableui/react\""
5186
5311
  },
5187
5312
  {
5188
5313
  "label": "As link",
@@ -5282,7 +5407,10 @@
5282
5407
  },
5283
5408
  {
5284
5409
  "guidance": "dont",
5285
- "description": "Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state."
5410
+ "description": "Do not use it for a selection that stays on. That is ToggleGroup, which carries the pressed state.",
5411
+ "instead": [
5412
+ "ToggleGroup"
5413
+ ]
5286
5414
  }
5287
5415
  ]
5288
5416
  },
@@ -5418,6 +5546,12 @@
5418
5546
  "default": "false",
5419
5547
  "description": "Drop the card frame (border, fill, padding) so the grid sits flush inside a surface that already has one, such as a Popover. DatePicker renders its Calendar this way."
5420
5548
  },
5549
+ {
5550
+ "name": "motion",
5551
+ "type": "\"full\" | \"subtle\" | \"off\"",
5552
+ "default": "\"full\"",
5553
+ "description": "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the month slide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
5554
+ },
5421
5555
  {
5422
5556
  "name": "className",
5423
5557
  "type": "string",
@@ -5452,6 +5586,38 @@
5452
5586
  "muted-foreground",
5453
5587
  "ring"
5454
5588
  ],
5589
+ "motion": [
5590
+ {
5591
+ "slot": "arrival",
5592
+ "recipe": "none",
5593
+ "part": "Root",
5594
+ "why": "Inline, it arrives with its form; in a DatePicker the Popover carries the entrance. A grid that animated in by itself would put a second motion on the same opening."
5595
+ },
5596
+ {
5597
+ "slot": "response",
5598
+ "recipe": "motion-interactive",
5599
+ "part": "Day",
5600
+ "why": "Days ease their hover wash, and the month arrows are ghost Buttons on Button's own recipe. Across a seven-column grid the pointer crosses many cells at once, and an eased wash reads as a cursor travelling rather than a grid flashing."
5601
+ },
5602
+ {
5603
+ "slot": "transition",
5604
+ "recipe": "motion-interactive",
5605
+ "part": "Day",
5606
+ "why": "Picking a day eases the primary fill onto it and off the previous one, so a click reads as the selection moving rather than two cells flipping. A range's band is painted on the cells, which carry the same recipe, so the band eases along with its endpoints as the range grows."
5607
+ },
5608
+ {
5609
+ "slot": "activity",
5610
+ "recipe": "none",
5611
+ "part": "Root",
5612
+ "why": "Days are resident data; nothing in the grid loads. Availability fetched per month belongs to the surface around the calendar."
5613
+ },
5614
+ {
5615
+ "slot": "transition",
5616
+ "recipe": "custom",
5617
+ "part": "Month",
5618
+ "why": "Paging months slides the outgoing grid toward the edge it is leaving by and brings the new one in from the other, over a short travel with a fade (react-day-picker's `animate` mode on Sable keyframes in styles.css), so forward and back read as direction rather than a grid swapping in one frame. RDP adds a single class per part, which a recipe utility cannot be; mirrored in RTL. Expressive: Calendar turns `animate` on only at full motion, and an explicit `animate` prop still wins."
5619
+ }
5620
+ ],
5455
5621
  "a11y": [
5456
5622
  "react-day-picker manages the roving-tabindex grid, arrow-key navigation, and ARIA roles/labels for the calendar.",
5457
5623
  "Selected days use bg-primary/text-primary-foreground for AA contrast; today is marked with a dot under the number (not a ring, which is reserved for keyboard focus) rather than color alone.",
@@ -5975,6 +6141,12 @@
5975
6141
  "name": "arrowClassName",
5976
6142
  "type": "string",
5977
6143
  "description": "Class for both prev/next arrow buttons. Merged through cn(), so a radius or shadow utility replaces the default rounded-full shadow-md rather than stacking with it."
6144
+ },
6145
+ {
6146
+ "name": "motion",
6147
+ "type": "\"full\" | \"subtle\" | \"off\"",
6148
+ "default": "\"full\"",
6149
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under subtle the slides still travel and the active dot snaps; off also cuts between slides and pauses autoplay."
5978
6150
  }
5979
6151
  ],
5980
6152
  "tokensUsed": [
@@ -5985,6 +6157,38 @@
5985
6157
  "foreground",
5986
6158
  "muted-foreground"
5987
6159
  ],
6160
+ "motion": [
6161
+ {
6162
+ "slot": "arrival",
6163
+ "recipe": "none",
6164
+ "part": "Root",
6165
+ "why": "A carousel arrives with its section. Its content already moves on every advance; an entrance on top would make the first view feel like an advance nobody asked for."
6166
+ },
6167
+ {
6168
+ "slot": "response",
6169
+ "recipe": "motion-interactive",
6170
+ "part": "Dot",
6171
+ "why": "The dot's 24px hit area eases its focus ring and the dot brightens under the pointer, which is how a 6px mark tells you it is a button."
6172
+ },
6173
+ {
6174
+ "slot": "transition",
6175
+ "recipe": "custom",
6176
+ "part": "Track",
6177
+ "why": "The slide track's translate is set inline from the measured slide pitch, on --motion-duration-slow and the emphasized easing, and switched off during a drag so the track follows the finger. A recipe cannot carry a measured, drag-interrupted offset. Essential: it is the answer to \"next\", so it plays under subtle."
6178
+ },
6179
+ {
6180
+ "slot": "transition",
6181
+ "recipe": "motion-glide",
6182
+ "part": "Dot",
6183
+ "why": "The active dot stretches to a bar while the one it left shrinks, on one timeline, so the row keeps its length and the mark reads as travelling with the slide. It used to change width unanimated and jumped while the slides beside it glided. Expressive: motion=\"subtle\" snaps it."
6184
+ },
6185
+ {
6186
+ "slot": "activity",
6187
+ "recipe": "none",
6188
+ "part": "Root",
6189
+ "why": "Autoplay's progress is the slides themselves advancing and the play/pause control; a separate timer animation would put a second clock beside the first."
6190
+ }
6191
+ ],
5988
6192
  "a11y": [
5989
6193
  "Region carries aria-roledescription='carousel'; each slide is a group with aria-roledescription='slide' and an 'N of M' label; clones are aria-hidden.",
5990
6194
  "Arrows and dots are real buttons (dots set aria-current); Left/Right arrow keys navigate when the viewport is focused.",
@@ -6011,6 +6215,11 @@
6011
6215
  "label": "Autoplay",
6012
6216
  "code": "<Carousel loop autoplay={4000} counter>{slides}</Carousel>",
6013
6217
  "imports": "import { Carousel } from \"@sableui/react\""
6218
+ },
6219
+ {
6220
+ "label": "Subtle motion",
6221
+ "code": "<Carousel loop motion=\"subtle\" label=\"Gallery\">{slides}</Carousel>",
6222
+ "imports": "import { Carousel } from \"@sableui/react\""
6014
6223
  }
6015
6224
  ]
6016
6225
  },
@@ -6229,6 +6438,12 @@
6229
6438
  "type": "Ref<HTMLTextAreaElement>",
6230
6439
  "description": "Ref to the underlying textarea — for focusing after a send, or on mount, and the extension point for anything that listens to the field. Pass `useCaretTrigger`'s `fieldRef` here for mentions and slash commands (CaretMenu). An Enter another listener already handled (`preventDefault()`) does not send."
6231
6440
  },
6441
+ {
6442
+ "name": "motion",
6443
+ "type": "\"full\" | \"subtle\" | \"off\"",
6444
+ "default": "\"full\"",
6445
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` the field snaps to its new height and the send/stop glyphs swap without the morph."
6446
+ },
6232
6447
  {
6233
6448
  "name": "...form props",
6234
6449
  "type": "Omit<ComponentPropsWithRef<\"form\">, \"onSubmit\" | \"defaultValue\">",
@@ -6255,6 +6470,38 @@
6255
6470
  "accent",
6256
6471
  "accent-foreground"
6257
6472
  ],
6473
+ "motion": [
6474
+ {
6475
+ "slot": "arrival",
6476
+ "recipe": "none",
6477
+ "part": "Root",
6478
+ "why": "The composer is the fixed foot of a thread, there before anything else is. An entrance would only delay the first keystroke."
6479
+ },
6480
+ {
6481
+ "slot": "response",
6482
+ "recipe": "motion-interactive",
6483
+ "part": "Root",
6484
+ "why": "The surface's focus ring and invalid border ease in, so focusing the field reads as the whole box waking up rather than a frame flicker."
6485
+ },
6486
+ {
6487
+ "slot": "transition",
6488
+ "recipe": "custom",
6489
+ "part": "Field",
6490
+ "why": "As a line breaks or a paste lands, the field eases to its new height (up to its cap) instead of jumping, so the transcript above is visibly pushed rather than teleported. `field-sizing` alone cannot ease — its height stays `auto` — so a small hook measures and pins a pixel height. Expressive and short (`fast`): the text itself lands at once, and `subtle` snaps the box."
6491
+ },
6492
+ {
6493
+ "slot": "transition",
6494
+ "recipe": "motion-icon-swap",
6495
+ "part": "Send button",
6496
+ "why": "Send turning into stop is the moment a reply starts, and stop turning back is the moment it is over. One button changes role in place — the fill eases, the arrow collapses into the square — so it reads as the same control changing what it does, not a different button appearing under the cursor."
6497
+ },
6498
+ {
6499
+ "slot": "activity",
6500
+ "recipe": "motion-spin",
6501
+ "part": "Send button",
6502
+ "why": "Busy with no stop handler, Button's own loading Spinner replaces the arrow, so a send in flight is not mistaken for a disabled one."
6503
+ }
6504
+ ],
6258
6505
  "a11y": [
6259
6506
  "A real `<form>`, so the send button is a submit button and the whole thing behaves as one control to assistive tech and to password managers alike.",
6260
6507
  "Enter never submits while an IME composition is open (`isComposing`), so a CJK reader confirming a candidate word does not mail a fragment of their first sentence.",
@@ -6317,7 +6564,11 @@
6317
6564
  },
6318
6565
  {
6319
6566
  "guidance": "dont",
6320
- "description": "Do not use Checkbox for mutually exclusive choices; use Radio or Select instead."
6567
+ "description": "Do not use Checkbox for mutually exclusive choices; use Radio or Select instead.",
6568
+ "instead": [
6569
+ "Radio",
6570
+ "Select"
6571
+ ]
6321
6572
  },
6322
6573
  {
6323
6574
  "guidance": "dont",
@@ -6385,6 +6636,12 @@
6385
6636
  "name": "render",
6386
6637
  "type": "ReactElement | (props, state) => ReactElement",
6387
6638
  "description": "Compose the root with another element through the Base UI render prop."
6639
+ },
6640
+ {
6641
+ "name": "motion",
6642
+ "type": "\"full\" | \"subtle\" | \"off\"",
6643
+ "default": "\"full\"",
6644
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` the tick fades in instead of drawing itself."
6388
6645
  }
6389
6646
  ],
6390
6647
  "tokensUsed": [
@@ -6394,6 +6651,32 @@
6394
6651
  "primary-foreground",
6395
6652
  "ring"
6396
6653
  ],
6654
+ "motion": [
6655
+ {
6656
+ "slot": "arrival",
6657
+ "recipe": "none",
6658
+ "part": "Root",
6659
+ "why": "It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence."
6660
+ },
6661
+ {
6662
+ "slot": "response",
6663
+ "recipe": "motion-interactive",
6664
+ "part": "Box",
6665
+ "why": "The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart."
6666
+ },
6667
+ {
6668
+ "slot": "transition",
6669
+ "recipe": "motion-tick",
6670
+ "part": "Mark",
6671
+ "why": "The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too."
6672
+ },
6673
+ {
6674
+ "slot": "activity",
6675
+ "recipe": "none",
6676
+ "part": "Root",
6677
+ "why": "A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
6678
+ }
6679
+ ],
6397
6680
  "a11y": [
6398
6681
  "Base UI renders a hidden native <input type=checkbox> for keyboard, focus, and form semantics.",
6399
6682
  "Space toggles; focus-visible draws a 2px ring in the --ring token.",
@@ -6524,6 +6807,12 @@
6524
6807
  "name": "checked / onCheckedChange / defaultChecked / disabled",
6525
6808
  "type": "Base UI Checkbox.Root props",
6526
6809
  "description": "Standard checkbox state. Without a group-owned value, each card is independent — drive checked/onCheckedChange per item for multi-select."
6810
+ },
6811
+ {
6812
+ "name": "motion",
6813
+ "type": "\"full\" | \"subtle\" | \"off\"",
6814
+ "default": "\"full\"",
6815
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` the tick fades in instead of drawing itself."
6527
6816
  }
6528
6817
  ],
6529
6818
  "subcomponents": [
@@ -6563,6 +6852,32 @@
6563
6852
  "tone-warning",
6564
6853
  "tone-warning-foreground"
6565
6854
  ],
6855
+ "motion": [
6856
+ {
6857
+ "slot": "arrival",
6858
+ "recipe": "none",
6859
+ "part": "Root",
6860
+ "why": "It arrives with the form it belongs to. Animating each box in turns a settings list into a title sequence."
6861
+ },
6862
+ {
6863
+ "slot": "response",
6864
+ "recipe": "motion-interactive",
6865
+ "part": "Box",
6866
+ "why": "The box fills with the primary colour as it is checked; easing the fill keeps the mark and the box changing as one event rather than two frames apart."
6867
+ },
6868
+ {
6869
+ "slot": "transition",
6870
+ "recipe": "motion-tick",
6871
+ "part": "Mark",
6872
+ "why": "The check draws itself in from its leading stroke, so checking reads as the box being ticked rather than a glyph appearing. The dash wipes the same way. Unchecking only fades, so clearing a list is quick; under `subtle` checking fades too."
6873
+ },
6874
+ {
6875
+ "slot": "activity",
6876
+ "recipe": "none",
6877
+ "part": "Root",
6878
+ "why": "A checkbox commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
6879
+ }
6880
+ ],
6566
6881
  "a11y": [
6567
6882
  "Base UI provides the checkbox role, aria-checked, hidden native input, keyboard toggle, and data-[checked]/data-disabled state.",
6568
6883
  "The whole card is the hit target; focus draws a 2px outline outside the card (not a ring, so the checked tone ring survives focus); Space toggles the focused card.",
@@ -6756,9 +7071,36 @@
6756
7071
  "muted-foreground",
6757
7072
  "foreground"
6758
7073
  ],
7074
+ "motion": [
7075
+ {
7076
+ "slot": "arrival",
7077
+ "recipe": "none",
7078
+ "part": "Root",
7079
+ "why": "Code is reading content and arrives with the page or message it belongs to. An entrance of its own would delay the text someone came to copy."
7080
+ },
7081
+ {
7082
+ "slot": "response",
7083
+ "recipe": "motion-interactive",
7084
+ "part": "Copy button",
7085
+ "why": "The copy button is a quiet ghost icon in the corner of a block. Its hover fill is what tells the pointer it is a control and not decoration."
7086
+ },
7087
+ {
7088
+ "slot": "transition",
7089
+ "recipe": "motion-icon-swap",
7090
+ "part": "Copy glyph",
7091
+ "why": "The copy mark turning into a check is the only proof the clipboard took the text. Snapped between frames it was easy to miss at the edge of a block; the swap reads as one mark being replaced."
7092
+ },
7093
+ {
7094
+ "slot": "activity",
7095
+ "recipe": "none",
7096
+ "part": "Root",
7097
+ "why": "Copying is instant. There is no in-between worth showing, and a spinner for a clipboard write would read as a slow one."
7098
+ }
7099
+ ],
6759
7100
  "a11y": [
6760
7101
  "Code renders inside a <pre><code> for correct semantics and screen-reader handling.",
6761
- "Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety."
7102
+ "Copy is the ghost icon Button with `useCopy`, which guards clipboard access for SSR safety.",
7103
+ "A successful copy is announced as \"Copied\" through a polite role=status region that is always mounted; the button keeps its label, which names what it does. Both glyphs are aria-hidden."
6762
7104
  ],
6763
7105
  "examples": [
6764
7106
  {
@@ -6804,7 +7146,11 @@
6804
7146
  },
6805
7147
  {
6806
7148
  "guidance": "dont",
6807
- "description": "Do not use Collapsible for multi-section navigation; use Accordion or Tabs instead."
7149
+ "description": "Do not use Collapsible for multi-section navigation; use Accordion or Tabs instead.",
7150
+ "instead": [
7151
+ "Accordion",
7152
+ "Tabs"
7153
+ ]
6808
7154
  },
6809
7155
  {
6810
7156
  "guidance": "dont",
@@ -6876,6 +7222,38 @@
6876
7222
  "foreground",
6877
7223
  "muted-foreground"
6878
7224
  ],
7225
+ "motion": [
7226
+ {
7227
+ "slot": "arrival",
7228
+ "recipe": "none",
7229
+ "part": "Root",
7230
+ "why": "A collapsible is part of the page it sits in, present from first paint. Its trigger is what the reader notices, not an entrance."
7231
+ },
7232
+ {
7233
+ "slot": "response",
7234
+ "recipe": "none",
7235
+ "part": "CollapsibleTrigger",
7236
+ "why": "The trigger is whatever the consumer renders (usually a Button), and that control brings its own response. Adding one here would double it."
7237
+ },
7238
+ {
7239
+ "slot": "transition",
7240
+ "recipe": "motion-disclosure",
7241
+ "part": "CollapsiblePanel",
7242
+ "why": "The height change ties the content that appears to the trigger that was pressed; snapped open, everything below jumps with nothing linking cause to effect."
7243
+ },
7244
+ {
7245
+ "slot": "transition",
7246
+ "recipe": "motion-interactive",
7247
+ "part": "CollapsibleIndicator",
7248
+ "why": "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 writes."
7249
+ },
7250
+ {
7251
+ "slot": "activity",
7252
+ "recipe": "none",
7253
+ "part": "Root",
7254
+ "why": "Nothing here is indeterminate; content that loads on open should put its own loader inside the panel."
7255
+ }
7256
+ ],
6879
7257
  "a11y": [
6880
7258
  "Base UI wires aria-expanded on the trigger and aria-controls/id between trigger and panel.",
6881
7259
  "Trigger is a real <button>; toggles on click and Enter/Space.",
@@ -7017,22 +7395,48 @@
7017
7395
  "ring",
7018
7396
  "popover"
7019
7397
  ],
7020
- "a11y": [
7021
- "The trigger's accessible name includes the current value, so a screen reader announces the colour rather than just \"button\" — the swatch itself conveys nothing without sight.",
7022
- "The Transparent entry is a toggle with aria-pressed, because it is a state of the value and not a navigation action.",
7023
- "The hex field is the keyboard path to any colour. The saturation/value area and the sliders are pointer-driven and are NOT keyboard operable — a known gap, and the reason the hex field is never hidden.",
7024
- "The eyedropper is only rendered where the EyeDropper API exists (Chromium), rather than shown and broken elsewhere."
7025
- ],
7026
- "examples": [
7398
+ "motion": [
7027
7399
  {
7028
- "label": "Canvas background",
7029
- "code": "<PropertyRow label=\"Background\">\n <ColorField value={background} onValueChange={setBackground} allowTransparent />\n</PropertyRow>",
7030
- "imports": "import { ColorField, PropertyRow } from \"@sableui/react\""
7400
+ "slot": "arrival",
7401
+ "recipe": "motion-pop",
7402
+ "part": "Popover",
7403
+ "why": "The picker grows out of the swatch it edits (PopoverContent's recipe, its origin on the anchor). In a property panel with a column of swatches, that origin is what tells you which colour you are now editing."
7031
7404
  },
7032
7405
  {
7033
- "label": "With alpha and a palette",
7034
- "code": "<ColorField\n value={tint}\n onValueChange={setTint}\n allowAlpha\n swatches={[\"#f43f5e\", \"#facc15\", \"#22c55e\", \"#3b82f6\", \"#a855f7\"]}\n label=\"Tint\"\n/>",
7035
- "imports": "import { ColorField } from \"@sableui/react\""
7406
+ "slot": "response",
7407
+ "recipe": "motion-interactive",
7408
+ "part": "Swatch",
7409
+ "why": "Preset swatches lift slightly under the pointer. They are 20px chips of colour with no label, and the eased lift is what tells you which one a click will take."
7410
+ },
7411
+ {
7412
+ "slot": "transition",
7413
+ "recipe": "none",
7414
+ "part": "Handles",
7415
+ "why": "The saturation, hue and alpha handles follow the pointer exactly. A handle that trailed the drag would misreport the colour under the cursor, and a swatch or a typed hex moving them is the value changing, which the field should show at once."
7416
+ },
7417
+ {
7418
+ "slot": "activity",
7419
+ "recipe": "none",
7420
+ "part": "Root",
7421
+ "why": "Picking is synchronous. The EyeDropper is the browser's own modal, with its own feedback."
7422
+ }
7423
+ ],
7424
+ "a11y": [
7425
+ "The trigger's accessible name includes the current value, so a screen reader announces the colour rather than just \"button\" — the swatch itself conveys nothing without sight.",
7426
+ "The Transparent entry is a toggle with aria-pressed, because it is a state of the value and not a navigation action.",
7427
+ "The hex field is the keyboard path to any colour. The saturation/value area and the sliders are pointer-driven and are NOT keyboard operable — a known gap, and the reason the hex field is never hidden.",
7428
+ "The eyedropper is only rendered where the EyeDropper API exists (Chromium), rather than shown and broken elsewhere."
7429
+ ],
7430
+ "examples": [
7431
+ {
7432
+ "label": "Canvas background",
7433
+ "code": "<PropertyRow label=\"Background\">\n <ColorField value={background} onValueChange={setBackground} allowTransparent />\n</PropertyRow>",
7434
+ "imports": "import { ColorField, PropertyRow } from \"@sableui/react\""
7435
+ },
7436
+ {
7437
+ "label": "With alpha and a palette",
7438
+ "code": "<ColorField\n value={tint}\n onValueChange={setTint}\n allowAlpha\n swatches={[\"#f43f5e\", \"#facc15\", \"#22c55e\", \"#3b82f6\", \"#a855f7\"]}\n label=\"Tint\"\n/>",
7439
+ "imports": "import { ColorField } from \"@sableui/react\""
7036
7440
  },
7037
7441
  {
7038
7442
  "label": "The value format is the contract",
@@ -7078,7 +7482,11 @@
7078
7482
  },
7079
7483
  {
7080
7484
  "guidance": "dont",
7081
- "description": "Use Combobox for tiny option sets where Select or Radio would be easier to scan."
7485
+ "description": "Use Combobox for tiny option sets where Select or Radio would be easier to scan.",
7486
+ "instead": [
7487
+ "Select",
7488
+ "Radio"
7489
+ ]
7082
7490
  },
7083
7491
  {
7084
7492
  "guidance": "do",
@@ -7090,7 +7498,10 @@
7090
7498
  },
7091
7499
  {
7092
7500
  "guidance": "dont",
7093
- "description": "Use Combobox as a command launcher. Use CommandMenu for actions and navigation."
7501
+ "description": "Use Combobox as a command launcher. Use CommandMenu for actions and navigation.",
7502
+ "instead": [
7503
+ "CommandMenu"
7504
+ ]
7094
7505
  },
7095
7506
  {
7096
7507
  "guidance": "dont",
@@ -7256,6 +7667,50 @@
7256
7667
  "secondary",
7257
7668
  "secondary-foreground"
7258
7669
  ],
7670
+ "motion": [
7671
+ {
7672
+ "slot": "arrival",
7673
+ "recipe": "motion-pop",
7674
+ "part": "ComboboxContent",
7675
+ "why": "The list grows from the field it filters, its origin on Base UI's anchor. With several comboboxes in a form, that is what attaches the open list to the one being typed in."
7676
+ },
7677
+ {
7678
+ "slot": "response",
7679
+ "recipe": "motion-interactive",
7680
+ "part": "ComboboxItem",
7681
+ "why": "A row's ink eases as the pointer or the arrow keys reach it, so moving through a long filtered list reads as continuous movement rather than rows blinking."
7682
+ },
7683
+ {
7684
+ "slot": "response",
7685
+ "recipe": "custom",
7686
+ "part": "Highlight",
7687
+ "why": "One highlight travels to the active row (lib/highlight-glide: a measured translate on the motion tokens) instead of each row painting its own fill. When the list re-filters under the caret, the highlight sliding to the new match is what shows the keyboard target moved."
7688
+ },
7689
+ {
7690
+ "slot": "response",
7691
+ "recipe": "motion-interactive",
7692
+ "part": "Toggle",
7693
+ "why": "The chevron toggle at the field's edge eases its ink on hover; the change is what tells you it is a separate target from the text box."
7694
+ },
7695
+ {
7696
+ "slot": "transition",
7697
+ "recipe": "motion-interactive",
7698
+ "part": "Toggle",
7699
+ "why": "The chevron turns 180° as the list opens and closes (motion-interactive lists `rotate`), in step with the popup, so it reads as one control changing direction."
7700
+ },
7701
+ {
7702
+ "slot": "transition",
7703
+ "recipe": "motion-pop",
7704
+ "part": "ComboboxContent",
7705
+ "why": "Closing shrinks back toward the field, returning the eye to the input that now holds the value."
7706
+ },
7707
+ {
7708
+ "slot": "activity",
7709
+ "recipe": "none",
7710
+ "part": "Root",
7711
+ "why": "Combobox ships no loading part. A list fetched as you type shows a Spinner (motion-spin) in ComboboxEmpty or the content — where the results will land — and only the caller knows a fetch is in flight."
7712
+ }
7713
+ ],
7259
7714
  "a11y": [
7260
7715
  "Base UI provides role=combobox/listbox/option, roving tabindex, arrow-key navigation, and type-to-filter.",
7261
7716
  "Esc and outside-press dismiss; focus returns to the input on close.",
@@ -7332,7 +7787,10 @@
7332
7787
  },
7333
7788
  {
7334
7789
  "guidance": "dont",
7335
- "description": "Hand-style a <button> to look like a search field, or put a SearchInput in the header that opens a dialog when focused. The first drifts from the field it imitates; the second is a text box you cannot type in. A header search that opens the palette is CommandMenuTrigger."
7790
+ "description": "Hand-style a <button> to look like a search field, or put a SearchInput in the header that opens a dialog when focused. The first drifts from the field it imitates; the second is a text box you cannot type in. A header search that opens the palette is CommandMenuTrigger.",
7791
+ "instead": [
7792
+ "CommandMenuTrigger"
7793
+ ]
7336
7794
  },
7337
7795
  {
7338
7796
  "guidance": "do",
@@ -7348,7 +7806,12 @@
7348
7806
  },
7349
7807
  {
7350
7808
  "guidance": "dont",
7351
- "description": "Use CommandMenu as a replacement for every dropdown. Simple choices belong in Select, Menu, or Tabs."
7809
+ "description": "Use CommandMenu as a replacement for every dropdown. Simple choices belong in Select, Menu, or Tabs.",
7810
+ "instead": [
7811
+ "Select",
7812
+ "Menu",
7813
+ "Tabs"
7814
+ ]
7352
7815
  },
7353
7816
  {
7354
7817
  "guidance": "do",
@@ -7368,7 +7831,10 @@
7368
7831
  },
7369
7832
  {
7370
7833
  "guidance": "dont",
7371
- "description": "Hand-build a palette mock from a search glyph, a row of buttons and a highlight. That is CommandMenuPanel, and the hand-built one has no combobox, no filtering and no keyboard model."
7834
+ "description": "Hand-build a palette mock from a search glyph, a row of buttons and a highlight. That is CommandMenuPanel, and the hand-built one has no combobox, no filtering and no keyboard model.",
7835
+ "instead": [
7836
+ "CommandMenuPanel"
7837
+ ]
7372
7838
  },
7373
7839
  {
7374
7840
  "guidance": "dont",
@@ -7539,6 +8005,44 @@
7539
8005
  "accent",
7540
8006
  "accent-foreground"
7541
8007
  ],
8008
+ "motion": [
8009
+ {
8010
+ "slot": "arrival",
8011
+ "recipe": "motion-pop",
8012
+ "part": "Dialog",
8013
+ "why": "The palette opens on DialogContent's recipe over a fading backdrop. It is summoned by a keystroke from nowhere on screen, and the short scale-in is what says a layer arrived rather than the page changing. The inline CommandMenuPanel has no entrance: it is part of the page."
8014
+ },
8015
+ {
8016
+ "slot": "arrival",
8017
+ "recipe": "motion-appear",
8018
+ "part": "Clear button",
8019
+ "why": "The clear button mounts on the first keystroke, beside the caret the eye is already on. Fading in, it reads as the field offering a way back instead of a control blinking into the header. Removal is instant, so an emptied query does not wait on its X."
8020
+ },
8021
+ {
8022
+ "slot": "response",
8023
+ "recipe": "motion-interactive",
8024
+ "part": "CommandItem",
8025
+ "why": "The active row follows both the arrow keys and the pointer, and its fill eases between rows, so scanning results while typing reads as one highlight moving rather than rows flashing in turn."
8026
+ },
8027
+ {
8028
+ "slot": "response",
8029
+ "recipe": "motion-interactive",
8030
+ "part": "CommandMenuTrigger",
8031
+ "why": "The search-shaped trigger eases its ink on hover like the field shell it imitates, so it reads as the field it opens."
8032
+ },
8033
+ {
8034
+ "slot": "transition",
8035
+ "recipe": "motion-pop",
8036
+ "part": "Dialog",
8037
+ "why": "Closing on Enter or Esc shrinks the palette away fast and returns focus where it came from; the exit is the confirmation that the command ran or was dismissed."
8038
+ },
8039
+ {
8040
+ "slot": "activity",
8041
+ "recipe": "none",
8042
+ "part": "List",
8043
+ "why": "Results are filtered synchronously from the items passed in. Async results are the caller's to mark with a Spinner (motion-spin) in the list, where the person is already looking."
8044
+ }
8045
+ ],
7542
8046
  "a11y": [
7543
8047
  "CommandMenuTrigger is a real <button> named by its visible placeholder, with aria-haspopup=dialog and aria-keyshortcuts (Meta+K on macOS, Control+K elsewhere). Its Kbd hint is aria-hidden, so the shortcut is announced once rather than read as part of the name.",
7544
8048
  "The shortcut never fires from inside a text field, textarea, select or contenteditable (Ctrl+K is delete-to-end-of-line in macOS text views), except the palette's own input.",
@@ -7595,6 +8099,223 @@
7595
8099
  }
7596
8100
  ]
7597
8101
  },
8102
+ {
8103
+ "name": "ConfirmButton",
8104
+ "status": "stable",
8105
+ "group": "Actions",
8106
+ "register": "either",
8107
+ "path": "components/confirm-button",
8108
+ "summary": "An inline two-step confirm for a low-stakes destructive or irreversible action: the first press arms it (\"Delete\" → \"Confirm delete?\", destructive), a second press within a few seconds runs it, and leaving it disarms it.",
8109
+ "keywords": [
8110
+ "confirm",
8111
+ "confirmation",
8112
+ "two-step",
8113
+ "press again",
8114
+ "click again",
8115
+ "arm",
8116
+ "delete",
8117
+ "remove",
8118
+ "destructive",
8119
+ "inline confirm",
8120
+ "hold to confirm"
8121
+ ],
8122
+ "usage": {
8123
+ "description": "Use ConfirmButton when an action deserves one beat of second thought but not a modal: deleting a draft, removing a row, revoking one token, clearing a list that can be rebuilt. The question appears on the button the user is already pressing, so confirming costs one more press in the same place and backing out costs nothing.",
8124
+ "bestPractices": [
8125
+ {
8126
+ "guidance": "do",
8127
+ "description": "Keep the idle label a plain verb (\"Delete\", \"Remove\", \"Revoke\") and let the confirm label ask about it (\"Confirm delete?\" by default, or `confirmLabel`). The user should read the consequence on the very control they are about to press again."
8128
+ },
8129
+ {
8130
+ "guidance": "do",
8131
+ "description": "Leave room for it to grow. The button eases wider to fit the question, so give it space at the end of a row or in a table's action column rather than wedging it between fixed-width neighbours."
8132
+ },
8133
+ {
8134
+ "guidance": "do",
8135
+ "description": "Return the promise from `onConfirm` when the action is async. The button stays armed with a spinner until it settles and refuses further presses, so a slow network cannot run it twice."
8136
+ },
8137
+ {
8138
+ "guidance": "dont",
8139
+ "description": "Do not use it for high-stakes or wide-reaching actions — deleting a project, an account, anything other people depend on. Those need the consequence spelled out and a typed or deliberate confirmation; use DestructiveActionModal.",
8140
+ "instead": [
8141
+ "DestructiveActionModal"
8142
+ ]
8143
+ },
8144
+ {
8145
+ "guidance": "dont",
8146
+ "description": "Do not use it where an undo is possible. If the action can be reversed, run it on the first press and offer Undo in a Toast — no question at all is faster than a good question.",
8147
+ "instead": [
8148
+ "Toast"
8149
+ ]
8150
+ },
8151
+ {
8152
+ "guidance": "dont",
8153
+ "description": "Do not use it for a non-destructive primary action (\"Save\", \"Send\"). Asking twice for something harmless teaches people to press twice without reading, which is what defeats the confirm when it matters; use Button.",
8154
+ "instead": [
8155
+ "Button"
8156
+ ]
8157
+ }
8158
+ ]
8159
+ },
8160
+ "related": {
8161
+ "components": [
8162
+ "Button",
8163
+ "DestructiveActionModal",
8164
+ "Toast",
8165
+ "OverflowMenu"
8166
+ ]
8167
+ },
8168
+ "props": [
8169
+ {
8170
+ "name": "children",
8171
+ "type": "ReactNode",
8172
+ "description": "The idle label — the action, e.g. \"Delete\"."
8173
+ },
8174
+ {
8175
+ "name": "confirmLabel",
8176
+ "type": "ReactNode",
8177
+ "default": "\"Confirm <label>?\"",
8178
+ "description": "The label while armed. Defaults to \"Confirm delete?\" for a text label \"Delete\" (first letter lowered) and \"Confirm?\" otherwise. Pass it to localise."
8179
+ },
8180
+ {
8181
+ "name": "onConfirm",
8182
+ "type": "() => unknown",
8183
+ "description": "Runs on the second press while armed. Return a promise to hold the button armed with a spinner until it settles."
8184
+ },
8185
+ {
8186
+ "name": "timeout",
8187
+ "type": "number",
8188
+ "default": "3000",
8189
+ "description": "How long it waits for the second press, in ms. A decision time, so it is not scaled by motion settings."
8190
+ },
8191
+ {
8192
+ "name": "variant",
8193
+ "type": "ButtonVariant",
8194
+ "default": "\"secondary\"",
8195
+ "description": "The idle look — any Button variant."
8196
+ },
8197
+ {
8198
+ "name": "confirmVariant",
8199
+ "type": "ButtonVariant",
8200
+ "default": "\"destructive\"",
8201
+ "description": "The armed look — any Button variant."
8202
+ },
8203
+ {
8204
+ "name": "size",
8205
+ "type": "\"xs\" | \"sm\" | \"md\" | \"cta\"",
8206
+ "default": "\"md\"",
8207
+ "description": "A labelled Button size. Icon-only sizes are excluded: the label is the interaction."
8208
+ },
8209
+ {
8210
+ "name": "icon",
8211
+ "type": "ReactNode",
8212
+ "description": "A leading icon kept through both states, e.g. `<Icon name=\"trash\" />`."
8213
+ },
8214
+ {
8215
+ "name": "disabled",
8216
+ "type": "boolean",
8217
+ "default": "false",
8218
+ "description": "Disables it; disabling while armed also disarms."
8219
+ },
8220
+ {
8221
+ "name": "confirmHint",
8222
+ "type": "string",
8223
+ "default": "\"Press again to confirm.\"",
8224
+ "description": "Read to assistive tech after the armed label when it arms. Override to localise."
8225
+ },
8226
+ {
8227
+ "name": "motion",
8228
+ "type": "\"full\" | \"subtle\" | \"off\"",
8229
+ "default": "\"full\"",
8230
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The width easing and the draining countdown are the flourish; the label cross-fade and colour change stay."
8231
+ }
8232
+ ],
8233
+ "variants": {
8234
+ "variant": [
8235
+ "primary",
8236
+ "secondary",
8237
+ "ghost",
8238
+ "outline",
8239
+ "destructive",
8240
+ "link",
8241
+ "floating",
8242
+ "field",
8243
+ "on-color",
8244
+ "on-color-outline",
8245
+ "on-color-ghost"
8246
+ ],
8247
+ "confirmVariant": [
8248
+ "destructive",
8249
+ "primary",
8250
+ "secondary",
8251
+ "outline"
8252
+ ],
8253
+ "size": [
8254
+ "xs",
8255
+ "sm",
8256
+ "md",
8257
+ "cta"
8258
+ ]
8259
+ },
8260
+ "tokensUsed": [
8261
+ "secondary",
8262
+ "secondary-foreground",
8263
+ "destructive",
8264
+ "destructive-foreground",
8265
+ "ring"
8266
+ ],
8267
+ "motion": [
8268
+ {
8269
+ "slot": "arrival",
8270
+ "recipe": "none",
8271
+ "part": "Root",
8272
+ "why": "It is a button in a row of actions, and arrives with that row. Its moment is the arming, not the mount."
8273
+ },
8274
+ {
8275
+ "slot": "response",
8276
+ "recipe": "motion-press",
8277
+ "part": "Root",
8278
+ "why": "Inherited from Button with motion-interactive. The press settle confirms the first press landed even before the label has finished changing — which is what stops a second, accidental press."
8279
+ },
8280
+ {
8281
+ "slot": "transition",
8282
+ "recipe": "motion-size",
8283
+ "part": "Root",
8284
+ "why": "Arming changes the label, so the button's width changes. Easing it makes the button visibly grow into the question and its neighbours travel, instead of the row jumping under the pointer that is about to press again. useAutoSize measures the content and writes a px width for motion-size to ease between (a content-sized box is width:auto before and after, which cannot transition). The labels cross-fade in one cell so there is never a frame with both or neither."
8285
+ },
8286
+ {
8287
+ "slot": "activity",
8288
+ "recipe": "custom",
8289
+ "part": "Countdown",
8290
+ "why": "While armed, a thin line drains along the foot of the button over the timeout — the only way to see that the question expires. A transition on scale, timed by the `timeout` prop and multiplied by the motion scale and expressive factor, so under reduced motion or `subtle` it is absent rather than frozen full."
8291
+ }
8292
+ ],
8293
+ "a11y": [
8294
+ "A native <button> through Button. Focus stays on it through both presses; Enter and Space confirm exactly as a click does.",
8295
+ "Only the current label is in the accessible name — the other is aria-hidden — so the name changes to the question while armed.",
8296
+ "Arming is also announced through a polite role=status region outside the button (\"Confirm delete? Press again to confirm.\"), because a focused button's name change is not read by every screen reader.",
8297
+ "Escape disarms and is consumed, so it does not also close the dialog or popover around it. Focus leaving, a press elsewhere, or the timeout disarm it too.",
8298
+ "A second press within 300ms of arming is ignored: a double-click is a habit, not a decision.",
8299
+ "Under reduced motion the label still changes and the colour still turns; only the width easing and countdown are dropped."
8300
+ ],
8301
+ "examples": [
8302
+ {
8303
+ "label": "Delete a row",
8304
+ "code": "<ConfirmButton icon={<Icon name=\"trash\" />} onConfirm={() => removeRow(id)}>Delete</ConfirmButton>",
8305
+ "imports": "import { ConfirmButton, Icon } from \"@sableui/react\""
8306
+ },
8307
+ {
8308
+ "label": "Custom question, longer wait",
8309
+ "code": "<ConfirmButton variant=\"ghost\" size=\"sm\" confirmLabel=\"Revoke this key?\" timeout={5000} onConfirm={revokeKey}>Revoke</ConfirmButton>",
8310
+ "imports": "import { ConfirmButton } from \"@sableui/react\""
8311
+ },
8312
+ {
8313
+ "label": "Async confirm",
8314
+ "code": "<ConfirmButton onConfirm={() => api.deleteDraft(draft.id)}>Discard draft</ConfirmButton>",
8315
+ "imports": "import { ConfirmButton } from \"@sableui/react\""
8316
+ }
8317
+ ]
8318
+ },
7598
8319
  {
7599
8320
  "name": "ContextCard",
7600
8321
  "status": "stable",
@@ -7625,7 +8346,10 @@
7625
8346
  },
7626
8347
  {
7627
8348
  "guidance": "dont",
7628
- "description": "Do not open it on click. It is a hover/focus preview; making it click-triggered gives you a Popover with no dismiss affordance and no way to reach the content it previews."
8349
+ "description": "Do not open it on click. It is a hover/focus preview; making it click-triggered gives you a Popover with no dismiss affordance and no way to reach the content it previews.",
8350
+ "instead": [
8351
+ "Popover"
8352
+ ]
7629
8353
  }
7630
8354
  ],
7631
8355
  "anatomy": [
@@ -7996,6 +8720,38 @@
7996
8720
  "foreground",
7997
8721
  "ring"
7998
8722
  ],
8723
+ "motion": [
8724
+ {
8725
+ "slot": "arrival",
8726
+ "recipe": "none",
8727
+ "part": "Root",
8728
+ "why": "Rows render with the page or the query that produced them. Staggering rows in delays reading a table someone opened to scan."
8729
+ },
8730
+ {
8731
+ "slot": "response",
8732
+ "recipe": "motion-interactive",
8733
+ "part": "Row",
8734
+ "why": "Body rows ease to a muted wash under the pointer (TableRow's recipe), which keeps the eye on the right line across a wide row of numbers. Sortable headers are ghost Buttons and answer on Button's recipe."
8735
+ },
8736
+ {
8737
+ "slot": "transition",
8738
+ "recipe": "motion-interactive",
8739
+ "part": "Sort chevron",
8740
+ "why": "One chevron turns to show direction instead of two glyphs swapping, so ascending to descending reads as the same column reversing. Its opacity rises from the resting 60% as the column becomes the sort."
8741
+ },
8742
+ {
8743
+ "slot": "transition",
8744
+ "recipe": "motion-interactive",
8745
+ "part": "Row",
8746
+ "why": "A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it."
8747
+ },
8748
+ {
8749
+ "slot": "activity",
8750
+ "recipe": "none",
8751
+ "part": "Root",
8752
+ "why": "DataTable has no loading state. While data is fetched, render Skeleton rows (motion-shimmer) in its place — only the caller knows a fetch is in flight."
8753
+ }
8754
+ ],
7999
8755
  "a11y": [
8000
8756
  "Sorted columns set aria-sort=ascending/descending on the header cell.",
8001
8757
  "Sortable headers are real <button>s with a focus-visible ring, keyboard-operable.",
@@ -8504,7 +9260,10 @@
8504
9260
  },
8505
9261
  {
8506
9262
  "guidance": "dont",
8507
- "description": "Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space."
9263
+ "description": "Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space.",
9264
+ "instead": [
9265
+ "Sheet"
9266
+ ]
8508
9267
  },
8509
9268
  {
8510
9269
  "guidance": "do",
@@ -8512,7 +9271,10 @@
8512
9271
  },
8513
9272
  {
8514
9273
  "guidance": "dont",
8515
- "description": "Use size=\"fullscreen\" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at."
9274
+ "description": "Use size=\"fullscreen\" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at.",
9275
+ "instead": [
9276
+ "Sheet"
9277
+ ]
8516
9278
  },
8517
9279
  {
8518
9280
  "guidance": "dont",
@@ -8520,7 +9282,12 @@
8520
9282
  },
8521
9283
  {
8522
9284
  "guidance": "dont",
8523
- "description": "Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead."
9285
+ "description": "Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead.",
9286
+ "instead": [
9287
+ "Toast",
9288
+ "Banner",
9289
+ "InlineStatus"
9290
+ ]
8524
9291
  },
8525
9292
  {
8526
9293
  "guidance": "do",
@@ -8770,7 +9537,10 @@
8770
9537
  },
8771
9538
  {
8772
9539
  "guidance": "dont",
8773
- "description": "Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job."
9540
+ "description": "Use it in a page-register form. A form field that pushes the form down on open reads as a layout jump; that is Select's job.",
9541
+ "instead": [
9542
+ "Select"
9543
+ ]
8774
9544
  },
8775
9545
  {
8776
9546
  "guidance": "dont",
@@ -9244,11 +10014,37 @@
9244
10014
  "muted-foreground",
9245
10015
  "ring"
9246
10016
  ],
9247
- "a11y": [
9248
- "Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.",
9249
- "interactive rows render a real <button> around the leading visual and text, with its ::after stretched over the whole row — native focus, a focus-visible ring, and Enter/Space. The action slot sits outside that button and above the overlay, so a Button there stays valid HTML and keeps its own clicks.",
9250
- "Give any interactive action an accessible label (e.g. aria-label on a ghost icon Button).",
9251
- "A selected row announces aria-current=\"true\" on its button, so the current record is spoken, not only shown by its fill."
10017
+ "motion": [
10018
+ {
10019
+ "slot": "arrival",
10020
+ "recipe": "none",
10021
+ "part": "Root",
10022
+ "why": "Rows arrive with the list they belong to. Entering one at a time is a stagger, and a stagger on a list someone opened to scan is a delay."
10023
+ },
10024
+ {
10025
+ "slot": "response",
10026
+ "recipe": "motion-interactive",
10027
+ "part": "Root",
10028
+ "why": "Only the interactive row reacts: its hover fill, a step off the surface it sits on, eases in. A static row never fakes clickability, and an interactive one answers the pointer without flicker as it moves down a list."
10029
+ },
10030
+ {
10031
+ "slot": "transition",
10032
+ "recipe": "motion-interactive",
10033
+ "part": "Root",
10034
+ "why": "`selected` eases the fill onto the current row of a list–detail view, so opening another record reads as the selection moving rather than two rows flashing."
10035
+ },
10036
+ {
10037
+ "slot": "activity",
10038
+ "recipe": "none",
10039
+ "part": "Root",
10040
+ "why": "A record row has nothing of its own to wait for. A row whose action is pending puts a loading Button in `action`."
10041
+ }
10042
+ ],
10043
+ "a11y": [
10044
+ "Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.",
10045
+ "interactive rows render a real <button> around the leading visual and text, with its ::after stretched over the whole row — native focus, a focus-visible ring, and Enter/Space. The action slot sits outside that button and above the overlay, so a Button there stays valid HTML and keeps its own clicks.",
10046
+ "Give any interactive action an accessible label (e.g. aria-label on a ghost icon Button).",
10047
+ "A selected row announces aria-current=\"true\" on its button, so the current record is spoken, not only shown by its fill."
9252
10048
  ],
9253
10049
  "examples": [
9254
10050
  {
@@ -9270,7 +10066,7 @@
9270
10066
  "group": "Overlays",
9271
10067
  "register": "page",
9272
10068
  "path": "components/feedback",
9273
- "summary": "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.",
10069
+ "summary": "A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message, shows a brief drawn-check thanks, and closes the popover.",
9274
10070
  "keywords": [
9275
10071
  "feedback",
9276
10072
  "sentiment",
@@ -9339,7 +10135,19 @@
9339
10135
  {
9340
10136
  "name": "onSubmit",
9341
10137
  "type": "(data: { rating?: string; message: string }) => void",
9342
- "description": "Called with the selected rating + message when the form is submitted."
10138
+ "description": "Called with the selected rating + message when the form is submitted. The thanks shows right away; send the report in the background."
10139
+ },
10140
+ {
10141
+ "name": "successMessage",
10142
+ "type": "string",
10143
+ "default": "\"Thanks for the feedback.\"",
10144
+ "description": "The line shown, and read out, for about 1.2s after sending, before the popover closes on its own."
10145
+ },
10146
+ {
10147
+ "name": "motion",
10148
+ "type": "\"full\" | \"subtle\" | \"off\"",
10149
+ "default": "\"full\"",
10150
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The drawn check is the flourish; the thanks line and its dwell stay."
9343
10151
  }
9344
10152
  ],
9345
10153
  "tokensUsed": [
@@ -9352,14 +10160,43 @@
9352
10160
  "muted-foreground",
9353
10161
  "accent",
9354
10162
  "accent-foreground",
9355
- "ring"
10163
+ "ring",
10164
+ "success"
10165
+ ],
10166
+ "motion": [
10167
+ {
10168
+ "slot": "arrival",
10169
+ "recipe": "motion-pop",
10170
+ "part": "Popover",
10171
+ "why": "The form opens from its trigger, so the eye goes from the button it pressed to the field it should type in. Popover owns the recipe."
10172
+ },
10173
+ {
10174
+ "slot": "response",
10175
+ "recipe": "motion-interactive",
10176
+ "part": "Rating toggles",
10177
+ "why": "The rating row is a choice among a few similar marks. The eased selected fill is what makes the pick read as made, not just hovered."
10178
+ },
10179
+ {
10180
+ "slot": "transition",
10181
+ "recipe": "motion-tick",
10182
+ "part": "Thanks",
10183
+ "why": "Sending is the one moment the person needs to know their words arrived. The form fades out in place and a check draws itself in over it, then the popover closes. Without it the popover just vanished, which looks the same as the form being dismissed or the message lost."
10184
+ },
10185
+ {
10186
+ "slot": "activity",
10187
+ "recipe": "none",
10188
+ "part": "Root",
10189
+ "why": "The thanks shows as soon as the form is sent; delivery is the app's background work. A spinner for a one-line report would make a quick, optional gesture feel heavy."
10190
+ }
9356
10191
  ],
9357
10192
  "a11y": [
9358
10193
  "Trigger opens the popover on press; Base UI manages focus into the form and restores it to the trigger on close.",
9359
10194
  "The rating row is a Base UI ToggleGroup with arrow-key roving focus; each toggle carries an aria-label.",
9360
10195
  "The form is named by its heading (aria-labelledby), so it is announced as \"Send feedback\" rather than an unnamed form.",
9361
10196
  "The message field carries an aria-label matching its placeholder.",
9362
- "Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer."
10197
+ "Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.",
10198
+ "On send, focus moves to the thanks line (tabIndex=-1), which reads it out, and the form becomes inert so focus cannot land in invisible fields. The line is not also a live region, which read it twice. When the popover closes, Base UI returns focus to the trigger.",
10199
+ "The thanks dwell (about 1.2s) is a reading time, not motion, so it is the same under reduced motion."
9363
10200
  ],
9364
10201
  "examples": [
9365
10202
  {
@@ -9371,6 +10208,11 @@
9371
10208
  "label": "Custom labels",
9372
10209
  "code": "<Feedback label=\"Tell us\" submitLabel=\"Send it\" placeholder=\"Describe the issue…\" />",
9373
10210
  "imports": "import { Feedback } from \"@sableui/react\""
10211
+ },
10212
+ {
10213
+ "label": "Custom thanks",
10214
+ "code": "<Feedback successMessage=\"Got it — thank you.\" onSubmit={({ rating, message }) => report(rating, message)} />",
10215
+ "imports": "import { Feedback } from \"@sableui/react\""
9374
10216
  }
9375
10217
  ]
9376
10218
  },
@@ -10424,7 +11266,10 @@
10424
11266
  },
10425
11267
  {
10426
11268
  "guidance": "dont",
10427
- "description": "Do not use InlineStatus for global announcements or page-level alerts."
11269
+ "description": "Do not use InlineStatus for global announcements or page-level alerts.",
11270
+ "instead": [
11271
+ "Banner"
11272
+ ]
10428
11273
  },
10429
11274
  {
10430
11275
  "guidance": "dont",
@@ -10769,6 +11614,32 @@
10769
11614
  "muted-foreground",
10770
11615
  "foreground"
10771
11616
  ],
11617
+ "motion": [
11618
+ {
11619
+ "slot": "arrival",
11620
+ "recipe": "none",
11621
+ "part": "Root",
11622
+ "why": "It arrives with the form it belongs to. A field that animated in on its own would be briefly unusable for no stated reason."
11623
+ },
11624
+ {
11625
+ "slot": "response",
11626
+ "recipe": "motion-interactive",
11627
+ "part": "Root",
11628
+ "why": "The shell owns one focus ring for the input and its addons and eases it in, so focusing the text, or tabbing to a copy button inside the field, reads as the one field taking focus."
11629
+ },
11630
+ {
11631
+ "slot": "transition",
11632
+ "recipe": "motion-interactive",
11633
+ "part": "Root",
11634
+ "why": "An invalid value turns the shell's edge destructive on the same ease, so the error lands as the field changing state rather than a red box swapping in."
11635
+ },
11636
+ {
11637
+ "slot": "activity",
11638
+ "recipe": "none",
11639
+ "part": "Root",
11640
+ "why": "A field does no work of its own. A pending check (an availability lookup) puts a Spinner in `trailing`, which is where its answer will appear."
11641
+ }
11642
+ ],
10772
11643
  "a11y": [
10773
11644
  "The Input stays the focusable control; the group draws the focus ring around the whole field while it is focused.",
10774
11645
  "Inside a Field, the label, description and error are wired to the Input itself, not the group.",
@@ -10788,7 +11659,7 @@
10788
11659
  },
10789
11660
  {
10790
11661
  "label": "Copy button",
10791
- "code": "<Field label=\"API key\"><InputGroup trailing={<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Copy key\" onClick={copy}><Icon name={copied ? \"check\" : \"copy\"} /></Button>}><Input readOnly value={key} /></InputGroup></Field>",
11662
+ "code": "<Field label=\"API key\"><InputGroup trailing={<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Copy key\" onClick={copy}><span aria-hidden=\"true\" className=\"motion-icon-swap\"><span data-active={!copied} className=\"inline-flex\"><Icon name=\"copy\" /></span><span data-active={copied} className=\"inline-flex\"><Icon name=\"check\" /></span></span></Button>}><Input readOnly value={key} /></InputGroup></Field>",
10792
11663
  "imports": "import { Button, Field, Icon, Input, InputGroup } from \"@sableui/react\""
10793
11664
  },
10794
11665
  {
@@ -11127,11 +11998,18 @@
11127
11998
  "default": "false for columns; true for horizontal bars",
11128
11999
  "description": "The vertical axis."
11129
12000
  },
12001
+ {
12002
+ "name": "motion",
12003
+ "type": "\"full\" | \"subtle\" | \"off\"",
12004
+ "default": "\"full\"",
12005
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The arrival (lines drawing on) and the data-change morph are the flourish; the gliding cursor, dots and tooltip are the feedback `subtle` keeps."
12006
+ },
11130
12007
  {
11131
12008
  "name": "animate",
11132
12009
  "type": "boolean",
11133
12010
  "default": "true",
11134
- "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart."
12011
+ "description": "Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. `animate={false}` is `motion=\"subtle\"`.",
12012
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
11135
12013
  },
11136
12014
  {
11137
12015
  "name": "activeIndex",
@@ -11192,7 +12070,7 @@
11192
12070
  "slot": "arrival",
11193
12071
  "recipe": "custom",
11194
12072
  "part": "Line",
11195
- "why": "Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart."
12073
+ "why": "Once on screen each line draws on from the first point (stroke-dashoffset), series staggered, then the dots and labels fade in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
11196
12074
  },
11197
12075
  {
11198
12076
  "slot": "response",
@@ -11204,7 +12082,7 @@
11204
12082
  "slot": "transition",
11205
12083
  "recipe": "custom",
11206
12084
  "part": "Line + dots",
11207
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
12085
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
11208
12086
  },
11209
12087
  {
11210
12088
  "slot": "activity",
@@ -11966,7 +12844,10 @@
11966
12844
  },
11967
12845
  {
11968
12846
  "guidance": "dont",
11969
- "description": "Do not build a Select out of it. A menu fires actions and has no value; a Select holds one and reports changes. Swapping them loses form semantics and keyboard type-ahead."
12847
+ "description": "Do not build a Select out of it. A menu fires actions and has no value; a Select holds one and reports changes. Swapping them loses form semantics and keyboard type-ahead.",
12848
+ "instead": [
12849
+ "Select"
12850
+ ]
11970
12851
  },
11971
12852
  {
11972
12853
  "guidance": "do",
@@ -12162,7 +13043,10 @@
12162
13043
  },
12163
13044
  {
12164
13045
  "guidance": "dont",
12165
- "description": "Do not use a menubar for site navigation. Its items are commands in menus (role=menuitem), not links between pages."
13046
+ "description": "Do not use a menubar for site navigation. Its items are commands in menus (role=menuitem), not links between pages.",
13047
+ "instead": [
13048
+ "NavigationMenu"
13049
+ ]
12166
13050
  }
12167
13051
  ],
12168
13052
  "anatomy": [
@@ -12654,7 +13538,10 @@
12654
13538
  },
12655
13539
  {
12656
13540
  "guidance": "dont",
12657
- "description": "Do not use Meter for a task in flight — an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening."
13541
+ "description": "Do not use Meter for a task in flight — an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening.",
13542
+ "instead": [
13543
+ "Progress"
13544
+ ]
12658
13545
  },
12659
13546
  {
12660
13547
  "guidance": "dont",
@@ -12937,7 +13824,7 @@
12937
13824
  },
12938
13825
  {
12939
13826
  "label": "Copyable value",
12940
- "code": "const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant=\"ghost\" size=\"icon-xs\" aria-label=\"Copy commit hash\" onClick={copy}><Icon name={copied ? \"check\" : \"copy\"} /></Button></Row>",
13827
+ "code": "const { copy, copied } = useCopy(commitHash)\n<Row gap={1}><MiddleTruncate text={commitHash} /><Button variant=\"ghost\" size=\"icon-xs\" aria-label=\"Copy commit hash\" onClick={copy}><span aria-hidden=\"true\" className=\"motion-icon-swap\"><span data-active={!copied} className=\"inline-flex\"><Icon name=\"copy\" /></span><span data-active={copied} className=\"inline-flex\"><Icon name=\"check\" /></span></span></Button><span role=\"status\" className=\"sr-only\">{copied ? \"Copied\" : \"\"}</span></Row>",
12941
13828
  "imports": "import { Button, Icon, MiddleTruncate, Row, useCopy } from \"@sableui/react\""
12942
13829
  }
12943
13830
  ]
@@ -13169,7 +14056,10 @@
13169
14056
  },
13170
14057
  {
13171
14058
  "guidance": "dont",
13172
- "description": "Do not use Note for state the whole page or app is in (an outage, an offline session, a blocked account) — that is Banner — or for a message tied to one field, which is the field's own error text."
14059
+ "description": "Do not use Note for state the whole page or app is in (an outage, an offline session, a blocked account) — that is Banner — or for a message tied to one field, which is the field's own error text.",
14060
+ "instead": [
14061
+ "Banner"
14062
+ ]
13173
14063
  },
13174
14064
  {
13175
14065
  "guidance": "dont",
@@ -13230,6 +14120,38 @@
13230
14120
  "success",
13231
14121
  "destructive"
13232
14122
  ],
14123
+ "motion": [
14124
+ {
14125
+ "slot": "arrival",
14126
+ "recipe": "none",
14127
+ "part": "Root",
14128
+ "why": "A note is part of the content it annotates and renders with it. A callout that animated in would pull the eye before the reader has reached it in the flow."
14129
+ },
14130
+ {
14131
+ "slot": "response",
14132
+ "recipe": "motion-interactive",
14133
+ "part": "Action",
14134
+ "why": "The action's focus ring eases in, and its underline thickens on hover. Inside a block of guidance, that is what separates the one thing to do from the prose around it."
14135
+ },
14136
+ {
14137
+ "slot": "response",
14138
+ "recipe": "motion-press",
14139
+ "part": "Action",
14140
+ "why": "A note's action usually navigates or opens something; the small settle on press is the only confirmation before the next surface paints."
14141
+ },
14142
+ {
14143
+ "slot": "transition",
14144
+ "recipe": "none",
14145
+ "part": "Root",
14146
+ "why": "A note has no states to move between: its tone is fixed when it renders. A note that changes tone is a new message, and should arrive as one."
14147
+ },
14148
+ {
14149
+ "slot": "activity",
14150
+ "recipe": "none",
14151
+ "part": "Root",
14152
+ "why": "Static guidance. Work in progress belongs in a Progress or a Spinner, not in a callout."
14153
+ }
14154
+ ],
13233
14155
  "a11y": [
13234
14156
  "Rendered with role=\"note\" so assistive tech can announce it as an aside.",
13235
14157
  "Color is never the sole signal — the leading icon and optional title reinforce severity."
@@ -13263,7 +14185,7 @@
13263
14185
  "group": "Badges & pills",
13264
14186
  "register": "either",
13265
14187
  "path": "components/notification-badge",
13266
- "summary": "An unread indicator. Renders a bare dot or a count pill (capped as \"{max}+\"), or nothing when there's nothing to show. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.",
14188
+ "summary": "An unread indicator. Renders a bare dot or a count pill (capped as \"{max}+\"), or nothing when there's nothing to show. A changing count rolls to its new number, and a badge that appears after being empty pops in. overlay pins it to the corner of a relative host; it is aria-hidden, so the host's name carries the count.",
13267
14189
  "keywords": [
13268
14190
  "notification",
13269
14191
  "badge",
@@ -13298,7 +14220,8 @@
13298
14220
  "related": {
13299
14221
  "components": [
13300
14222
  "Badge",
13301
- "StatusDot"
14223
+ "StatusDot",
14224
+ "NumberTicker"
13302
14225
  ]
13303
14226
  },
13304
14227
  "props": [
@@ -13336,6 +14259,12 @@
13336
14259
  "type": "boolean",
13337
14260
  "default": "false",
13338
14261
  "description": "Pin to the top-end corner of the nearest relative host (an icon button) — one placement for every caller instead of hand-tuned absolute offsets."
14262
+ },
14263
+ {
14264
+ "name": "motion",
14265
+ "type": "\"full\" | \"subtle\" | \"off\"",
14266
+ "default": "\"full\"",
14267
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` the count updates in place and the badge fades in without the pop."
13339
14268
  }
13340
14269
  ],
13341
14270
  "variants": {
@@ -13362,6 +14291,32 @@
13362
14291
  "secondary",
13363
14292
  "secondary-foreground"
13364
14293
  ],
14294
+ "motion": [
14295
+ {
14296
+ "slot": "arrival",
14297
+ "recipe": "motion-appear",
14298
+ "part": "root",
14299
+ "why": "A badge that shows up after being empty is the news — something just arrived — so it fades and scales in where it sits instead of popping into existence. Never on first paint: a badge that mounts with a count is content, and a page of them popping on load is noise."
14300
+ },
14301
+ {
14302
+ "slot": "response",
14303
+ "recipe": "none",
14304
+ "part": "root",
14305
+ "why": "It is decorative and not interactive; the host it sits on answers the pointer."
14306
+ },
14307
+ {
14308
+ "slot": "transition",
14309
+ "recipe": "custom",
14310
+ "part": "Count",
14311
+ "why": "The count rolls to its new number through NumberTicker (expressive), so 3 → 4 reads as one more arriving rather than a different badge. Past the cap the ticker holds at max with a static \"+\", so a count climbing beyond 99 does not keep rolling a badge that has not changed."
14312
+ },
14313
+ {
14314
+ "slot": "activity",
14315
+ "recipe": "none",
14316
+ "part": "root",
14317
+ "why": "An unread count has no working state."
14318
+ }
14319
+ ],
13365
14320
  "a11y": [
13366
14321
  "The badge is aria-hidden: a count rendered on a control is not associated with it, so the host needs an accessible name that includes the count — \"Notifications, 3 unread\" — or a screen-reader user hears \"Notifications, button\" and never learns there is anything new. Pass aria-hidden={false} only for a standalone badge whose number is the information.",
13367
14322
  "A dot carries no number and no words: on its own it tells a sighted user only that something changed, and tells everyone else nothing. Pair it with a label that says what is new, and never make its colour the only difference between two states."
@@ -13493,6 +14448,176 @@
13493
14448
  }
13494
14449
  ]
13495
14450
  },
14451
+ {
14452
+ "name": "NumberTicker",
14453
+ "status": "stable",
14454
+ "group": "Data display",
14455
+ "register": "either",
14456
+ "path": "components/number-ticker",
14457
+ "summary": "A number that rolls to its new value digit by digit, like an odometer. Formatted with Intl.NumberFormat (currency, percent, compact, any locale); separators, signs and units hold still while each digit column turns, and a digit that appears or goes away eases its width so nothing around it jumps. At rest on first paint; under subtle or reduced motion the value simply updates.",
14458
+ "keywords": [
14459
+ "number",
14460
+ "counter",
14461
+ "odometer",
14462
+ "rolling number",
14463
+ "animated number",
14464
+ "ticker",
14465
+ "count up",
14466
+ "stat",
14467
+ "kpi",
14468
+ "metric",
14469
+ "price"
14470
+ ],
14471
+ "usage": {
14472
+ "description": "Use NumberTicker for a figure that changes in place while someone is looking at it — a dashboard total after a filter changes, a live count, a price as a plan's options change, a score. The roll says \"this number moved, and by roughly this much\"; the reader sees the change happen instead of noticing, later, that a number is different.",
14473
+ "bestPractices": [
14474
+ {
14475
+ "guidance": "do",
14476
+ "description": "Use it for the headline figure that a control on the same screen changes — the total above a chart, the price beside a quantity stepper — so cause and effect are seen together."
14477
+ },
14478
+ {
14479
+ "guidance": "do",
14480
+ "description": "Format through `format` (currency, percent, compact) rather than pre-formatting a string, so separators and units stay put while only the digits roll."
14481
+ },
14482
+ {
14483
+ "guidance": "do",
14484
+ "description": "Pass `aria-live=\"polite\"` only when the change itself is news the reader would otherwise miss; most tickers sit beside the control that changed them and need no announcement."
14485
+ },
14486
+ {
14487
+ "guidance": "dont",
14488
+ "description": "Do not use it for values that change many times a second (a stopwatch, a stock tick, a progress percentage) — the rolls never land and read as a blur. Render the number as plain tabular text."
14489
+ },
14490
+ {
14491
+ "guidance": "dont",
14492
+ "description": "Do not use it for numbers that are names rather than amounts — an order ID, a phone number, a year, a version. Rolling implies a quantity going up or down."
14493
+ },
14494
+ {
14495
+ "guidance": "dont",
14496
+ "description": "Do not animate a number on page load to make it feel alive. The first paint is at rest on purpose; for an entrance, reveal the card it sits in (the Reveal motion primitive)."
14497
+ },
14498
+ {
14499
+ "guidance": "dont",
14500
+ "description": "Do not use it for a value inside a known range that needs a picture of how full it is — show a Meter, with the number beside it.",
14501
+ "instead": [
14502
+ "Meter"
14503
+ ]
14504
+ }
14505
+ ]
14506
+ },
14507
+ "related": {
14508
+ "components": [
14509
+ "Meter",
14510
+ "NotificationBadge",
14511
+ "PieChart",
14512
+ "RadialChart",
14513
+ "Sparkline"
14514
+ ]
14515
+ },
14516
+ "props": [
14517
+ {
14518
+ "name": "value",
14519
+ "type": "number",
14520
+ "description": "The number to show. When it changes, the digits roll to it."
14521
+ },
14522
+ {
14523
+ "name": "format",
14524
+ "type": "Intl.NumberFormatOptions",
14525
+ "description": "How to write the number — `{ style: \"currency\", currency: \"USD\" }`, `{ style: \"percent\" }`, `{ notation: \"compact\" }`, fraction digits. Passed to Intl.NumberFormat; an inline object is fine (it is compared by value)."
14526
+ },
14527
+ {
14528
+ "name": "locales",
14529
+ "type": "string | string[]",
14530
+ "default": "runtime locale",
14531
+ "description": "BCP 47 locale(s) for the formatting. Digits roll through the locale's own numbering system."
14532
+ },
14533
+ {
14534
+ "name": "prefix",
14535
+ "type": "ReactNode",
14536
+ "description": "Before the number. A string becomes part of the number's text (and rolls in or out with it); an element is rendered as given."
14537
+ },
14538
+ {
14539
+ "name": "suffix",
14540
+ "type": "ReactNode",
14541
+ "description": "After the number (\"+\", \" pts\"). A string becomes part of the number's text; an element is rendered as given."
14542
+ },
14543
+ {
14544
+ "name": "trend",
14545
+ "type": "\"auto\" | \"up\" | \"down\"",
14546
+ "default": "\"auto\"",
14547
+ "description": "Which way the digits roll. `auto` rolls up as the value grows and down as it shrinks; `up` / `down` fix it (a countdown, a score that only climbs)."
14548
+ },
14549
+ {
14550
+ "name": "motion",
14551
+ "type": "\"full\" | \"subtle\" | \"off\"",
14552
+ "default": "\"full\"",
14553
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The roll is expressive, so `subtle` updates the value in place."
14554
+ },
14555
+ {
14556
+ "name": "className",
14557
+ "type": "string",
14558
+ "description": "Merged onto the root span. Set the type tier here (`text-heading-2`, `text-display-lg`); the ticker adds tabular figures so widths hold still."
14559
+ },
14560
+ {
14561
+ "name": "...span props",
14562
+ "type": "Omit<ComponentPropsWithRef<\"span\">, \"children\" | \"prefix\">",
14563
+ "description": "Remaining span attributes pass through to the root — `aria-live` to announce changes, `id`, data attributes."
14564
+ }
14565
+ ],
14566
+ "tokensUsed": [],
14567
+ "motion": [
14568
+ {
14569
+ "slot": "arrival",
14570
+ "recipe": "none",
14571
+ "part": "Root",
14572
+ "why": "The first paint is at rest. A number counting up on page load is decoration that delays the reading; the roll is kept for the moment the value actually changes."
14573
+ },
14574
+ {
14575
+ "slot": "response",
14576
+ "recipe": "none",
14577
+ "part": "Root",
14578
+ "why": "It is a reading, not a control; the control that changes it gives the feedback."
14579
+ },
14580
+ {
14581
+ "slot": "transition",
14582
+ "recipe": "custom",
14583
+ "part": "Digit",
14584
+ "why": "Each digit column turns to its new place on the expressive roll (the slower token, emphasized easing, staggered from the ones column), and arriving or departing columns ease their width. The roll is what shows a change happened and how big it was — the hundreds turning reads as a bigger move than the ones. No recipe can translate per-digit columns, so it is custom; it is multiplied by --motion-expressive, so `subtle` and reduced motion land the value at once."
14585
+ },
14586
+ {
14587
+ "slot": "activity",
14588
+ "recipe": "none",
14589
+ "part": "Root",
14590
+ "why": "A number has no working state; show a Skeleton while it loads."
14591
+ }
14592
+ ],
14593
+ "a11y": [
14594
+ "The whole written number (with any string prefix or suffix) is real text in a visually hidden span; the rolling columns are aria-hidden and draw their glyphs as generated content, so screen readers, find-in-page and copy all get \"$1,234.50\", never a stack of digits.",
14595
+ "It is not a live region by default — a figure that changes because the reader moved a control is already where their attention is. Pass `aria-live=\"polite\"` when a change elsewhere should be announced.",
14596
+ "Tabular figures keep every digit the same width, so a roll never shifts the text around it."
14597
+ ],
14598
+ "examples": [
14599
+ {
14600
+ "label": "Basic",
14601
+ "code": "<NumberTicker value={1284} className=\"text-heading-2\" />",
14602
+ "imports": "import { NumberTicker } from \"@sableui/react\""
14603
+ },
14604
+ {
14605
+ "label": "Currency",
14606
+ "code": "<NumberTicker value={4280.5} format={{ style: \"currency\", currency: \"USD\" }} className=\"text-display-lg\" />",
14607
+ "imports": "import { NumberTicker } from \"@sableui/react\""
14608
+ },
14609
+ {
14610
+ "label": "Percent, announced",
14611
+ "code": "<NumberTicker value={0.428} format={{ style: \"percent\", maximumFractionDigits: 1 }} aria-live=\"polite\" />",
14612
+ "imports": "import { NumberTicker } from \"@sableui/react\""
14613
+ },
14614
+ {
14615
+ "label": "Subtle",
14616
+ "code": "<NumberTicker value={92} suffix=\" pts\" motion=\"subtle\" />",
14617
+ "imports": "import { NumberTicker } from \"@sableui/react\""
14618
+ }
14619
+ ]
14620
+ },
13496
14621
  {
13497
14622
  "name": "OtpField",
13498
14623
  "status": "stable",
@@ -13531,6 +14656,10 @@
13531
14656
  "guidance": "dont",
13532
14657
  "description": "Do not use OtpField for a password, an arbitrary-length PIN, or anything the user types from memory. Slots promise a fixed length; use Input with `type=\"password\"` and `autocomplete=\"current-password\"` instead."
13533
14658
  },
14659
+ {
14660
+ "guidance": "do",
14661
+ "description": "Clear the error as soon as the user edits the code (`onValueChange` → reset the error), and set it again when the next code is refused. Each refusal is then a fresh change to invalid, so the field shakes for every wrong code, not only the first."
14662
+ },
13534
14663
  {
13535
14664
  "guidance": "dont",
13536
14665
  "description": "Do not intercept paste or clear the field on a wrong code before the user has seen the error. Blocking paste breaks SMS autofill and password managers, and wiping the slots erases the evidence of which digit was wrong."
@@ -13624,7 +14753,13 @@
13624
14753
  {
13625
14754
  "name": "aria-invalid",
13626
14755
  "type": "boolean",
13627
- "description": "Marks every slot invalid (destructive border). `<Field error>` sets it for you."
14756
+ "description": "Marks every slot invalid (destructive border and digits). Turning it on after mount shakes the field once. `<Field error>` sets it for you."
14757
+ },
14758
+ {
14759
+ "name": "motion",
14760
+ "type": "\"full\" | \"subtle\" | \"off\"",
14761
+ "default": "\"full\"",
14762
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The shake is the flourish; the destructive colour stays at every level."
13628
14763
  },
13629
14764
  {
13630
14765
  "name": "disabled / readOnly / required / name / id / form",
@@ -13661,6 +14796,12 @@
13661
14796
  "part": "Slot",
13662
14797
  "why": "A filled slot strengthens its border, so a pasted code visibly lands in every slot at once. Easing it reads as the code arriving rather than the field flickering."
13663
14798
  },
14799
+ {
14800
+ "slot": "transition",
14801
+ "recipe": "motion-shake",
14802
+ "part": "Root",
14803
+ "why": "A refused code shakes the field once as it turns invalid — the short horizontal no that makes a second wrong code feel noticed rather than ignored. It never carries the refusal alone: the digits and borders turn destructive and the Field says why. It plays on the change to invalid, not on mount, and is expressive, so `subtle` keeps only the colour."
14804
+ },
13664
14805
  {
13665
14806
  "slot": "activity",
13666
14807
  "recipe": "none",
@@ -13672,7 +14813,8 @@
13672
14813
  "The root is a role=group named by the field label; the first slot shares the label through `id`/`htmlFor`, and later slots are announced by position (\"Character 2 of 6\").",
13673
14814
  "Only one slot is in the tab order. Arrow keys, Home/End and Backspace move between slots; Tab leaves the whole field.",
13674
14815
  "The first slot carries autocomplete=\"one-time-code\" and the numeric virtual keyboard, so iOS and Android offer the SMS code above the keyboard.",
13675
- "Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus."
14816
+ "Slots render at 20px (`text-mono-lg`), above the 16px floor that stops iOS Safari zooming on focus.",
14817
+ "An invalid code is never signalled by motion alone: every slot carries aria-invalid, the digits and borders turn destructive, and the Field's error message is wired through aria-describedby. The shake resolves instantly under reduced motion and `subtle`."
13676
14818
  ],
13677
14819
  "examples": [
13678
14820
  {
@@ -13690,6 +14832,11 @@
13690
14832
  "code": "<Field label=\"Verification code\" error=\"That code has expired. Request a new code and try again.\"><OtpField length={6} groupSize={3} defaultValue=\"482913\" /></Field>",
13691
14833
  "imports": "import { Field, OtpField } from \"@sableui/react\""
13692
14834
  },
14835
+ {
14836
+ "label": "Verify on completion, shake on a wrong code",
14837
+ "code": "const [code, setCode] = useState(\"\")\nconst [wrong, setWrong] = useState(false)\n<Field label=\"Verification code\" error={wrong ? \"That code does not match. Check the message and try again.\" : undefined}><OtpField length={6} groupSize={3} value={code} onValueChange={(next) => { setCode(next); setWrong(false) }} onValueComplete={(value) => setWrong(value !== expected)} /></Field>",
14838
+ "imports": "import { Field, OtpField } from \"@sableui/react\""
14839
+ },
13693
14840
  {
13694
14841
  "label": "Alphanumeric",
13695
14842
  "code": "<Field label=\"Pairing code\"><OtpField length={8} groupSize={4} validationType=\"alphanumeric\" /></Field>",
@@ -13936,7 +15083,12 @@
13936
15083
  },
13937
15084
  {
13938
15085
  "guidance": "dont",
13939
- "description": "Do not use PageLoader for small inline waits; use Spinner, an indeterminate Progress, or Skeleton."
15086
+ "description": "Do not use PageLoader for small inline waits; use Spinner, an indeterminate Progress, or Skeleton.",
15087
+ "instead": [
15088
+ "Spinner",
15089
+ "Progress",
15090
+ "Skeleton"
15091
+ ]
13940
15092
  },
13941
15093
  {
13942
15094
  "guidance": "dont",
@@ -14275,11 +15427,37 @@
14275
15427
  "hairline",
14276
15428
  "hairline-strong"
14277
15429
  ],
14278
- "a11y": [
14279
- "PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.",
14280
- "Arrow keys resize by `step`; Home and End jump to the minimum and maximum.",
14281
- "Each Panel renders an <aside> with an accessible name from `label`, so panels are navigable landmarks."
14282
- ],
15430
+ "motion": [
15431
+ {
15432
+ "slot": "arrival",
15433
+ "recipe": "none",
15434
+ "part": "Root",
15435
+ "why": "Panels are the app's frame, present from first paint. A dock that slid in on load would make the workspace look like it is still assembling."
15436
+ },
15437
+ {
15438
+ "slot": "response",
15439
+ "recipe": "motion-interactive",
15440
+ "part": "PanelResizer",
15441
+ "why": "The hairline widens into a visible bar on hover and turns primary on keyboard focus, eased, so a 1px edge says it can be grabbed before the pointer commits."
15442
+ },
15443
+ {
15444
+ "slot": "transition",
15445
+ "recipe": "none",
15446
+ "part": "Panel",
15447
+ "why": "Width tracks the pointer one-to-one while dragging, and keyboard steps land at once. An eased width would trail the cursor, and resizing is a layout change: easing it would reflow the canvas beside the panel on every frame."
15448
+ },
15449
+ {
15450
+ "slot": "activity",
15451
+ "recipe": "none",
15452
+ "part": "Root",
15453
+ "why": "Layout chrome has no working state."
15454
+ }
15455
+ ],
15456
+ "a11y": [
15457
+ "PanelResizer is a role=separator with aria-orientation, aria-valuenow/min/max, and is focusable — a pointer-only resizer is a keyboard trap.",
15458
+ "Arrow keys resize by `step`; Home and End jump to the minimum and maximum.",
15459
+ "Each Panel renders an <aside> with an accessible name from `label`, so panels are navigable landmarks."
15460
+ ],
14283
15461
  "examples": [
14284
15462
  {
14285
15463
  "label": "Tool body with a resizable left panel",
@@ -14415,6 +15593,38 @@
14415
15593
  "muted-foreground",
14416
15594
  "hairline"
14417
15595
  ],
15596
+ "motion": [
15597
+ {
15598
+ "slot": "arrival",
15599
+ "recipe": "none",
15600
+ "part": "Root",
15601
+ "why": "Sections are part of the inspector they sit in and arrive with it. Animating each in would make a panel switch look like a page load."
15602
+ },
15603
+ {
15604
+ "slot": "response",
15605
+ "recipe": "motion-interactive",
15606
+ "part": "PropertyGroup trigger",
15607
+ "why": "The fold row eases its focus ring, so arrowing down a dense inspector shows where the keyboard is without the ring flickering from row to row."
15608
+ },
15609
+ {
15610
+ "slot": "transition",
15611
+ "recipe": "motion-disclosure",
15612
+ "part": "PropertyGroup panel",
15613
+ "why": "The group opens by easing its height, so the rows below slide down to make room instead of jumping. In an inspector, the next section moving is otherwise how you lose your place."
15614
+ },
15615
+ {
15616
+ "slot": "transition",
15617
+ "recipe": "motion-icon-swap",
15618
+ "part": "PropertyGroup indicator",
15619
+ "why": "The + collapses into − as the group opens, both glyphs stacked as one mark, so the indicator reads as this row changing state rather than a different icon landing."
15620
+ },
15621
+ {
15622
+ "slot": "activity",
15623
+ "recipe": "none",
15624
+ "part": "Root",
15625
+ "why": "The inspector edits values in place; nothing in it loads."
15626
+ }
15627
+ ],
14418
15628
  "a11y": [
14419
15629
  "PropertyGroup's row is a real button with aria-expanded (Collapsible); its summary is part of the button's name, so a closed group still announces its value.",
14420
15630
  "PropertyRow renders a real <label>; pass htmlFor with the control's id so clicking the label focuses it.",
@@ -14660,11 +15870,18 @@
14660
15870
  "default": "runtime locale",
14661
15871
  "description": "BCP 47 locale for default number and date formatting."
14662
15872
  },
15873
+ {
15874
+ "name": "motion",
15875
+ "type": "\"full\" | \"subtle\" | \"off\"",
15876
+ "default": "\"full\"",
15877
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The sweep, the slice morph and the rolling centre number are the flourish; the hover pull-out and dim are the feedback `subtle` keeps."
15878
+ },
14663
15879
  {
14664
15880
  "name": "animate",
14665
15881
  "type": "boolean",
14666
15882
  "default": "true",
14667
- "description": "Sweep the slices in clockwise when the chart scrolls into view."
15883
+ "description": "Sweep the slices in clockwise when the chart scrolls into view. `animate={false}` is `motion=\"subtle\"`.",
15884
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
14668
15885
  },
14669
15886
  {
14670
15887
  "name": "activeIndex",
@@ -14701,19 +15918,25 @@
14701
15918
  "slot": "arrival",
14702
15919
  "recipe": "custom",
14703
15920
  "part": "Slices",
14704
- "why": "Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart."
15921
+ "why": "Once on screen the slices are revealed by a mask that sweeps clockwise from 12 o'clock — the whole is read as it fills. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
14705
15922
  },
14706
15923
  {
14707
15924
  "slot": "response",
14708
15925
  "recipe": "none",
14709
15926
  "part": "Active slice",
14710
- "why": "The hovered slice pulls outward and the rest dim at once; the geometry change itself eases (see transition)."
15927
+ "why": "The hovered slice pulls outward and the rest dim at once (mixed toward the surface, not faded); the geometry change itself eases (see transition). Essential: `subtle` keeps it."
15928
+ },
15929
+ {
15930
+ "slot": "transition",
15931
+ "recipe": "custom",
15932
+ "part": "Centre value",
15933
+ "why": "A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the slices instead of being found different afterwards. Expressive: under `subtle` it updates in place."
14711
15934
  },
14712
15935
  {
14713
15936
  "slot": "transition",
14714
15937
  "recipe": "custom",
14715
15938
  "part": "Slices",
14716
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
15939
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
14717
15940
  },
14718
15941
  {
14719
15942
  "slot": "activity",
@@ -14768,7 +15991,10 @@
14768
15991
  },
14769
15992
  {
14770
15993
  "guidance": "dont",
14771
- "description": "Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge."
15994
+ "description": "Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge.",
15995
+ "instead": [
15996
+ "Badge"
15997
+ ]
14772
15998
  },
14773
15999
  {
14774
16000
  "guidance": "dont",
@@ -14860,6 +16086,38 @@
14860
16086
  "muted-foreground",
14861
16087
  "foreground"
14862
16088
  ],
16089
+ "motion": [
16090
+ {
16091
+ "slot": "arrival",
16092
+ "recipe": "none",
16093
+ "part": "Root",
16094
+ "why": "Pills arrive with the filter row or the content they annotate. A chip that animated in alone would read as a filter someone just added."
16095
+ },
16096
+ {
16097
+ "slot": "response",
16098
+ "recipe": "motion-interactive",
16099
+ "part": "Root",
16100
+ "why": "The hover wash eases, so sweeping the pointer along a filter row reads as one cursor moving rather than chips flashing in turn."
16101
+ },
16102
+ {
16103
+ "slot": "response",
16104
+ "recipe": "motion-press",
16105
+ "part": "Root",
16106
+ "why": "A pill often toggles a filter that takes a moment to reach the results; the settle on press confirms the click landed before they change."
16107
+ },
16108
+ {
16109
+ "slot": "transition",
16110
+ "recipe": "motion-interactive",
16111
+ "part": "Root",
16112
+ "why": "`selected` lifts the chip onto its selected surface on the same ease, so toggling reads as this chip changing state, not a different chip appearing in its place."
16113
+ },
16114
+ {
16115
+ "slot": "activity",
16116
+ "recipe": "none",
16117
+ "part": "Root",
16118
+ "why": "A chip is a choice, not a process. If applying it is slow, the results show it."
16119
+ }
16120
+ ],
14863
16121
  "a11y": [
14864
16122
  "Renders a native <button type=\"button\"> for focus and keyboard activation.",
14865
16123
  "Selected state is exposed via aria-pressed."
@@ -14913,11 +16171,17 @@
14913
16171
  },
14914
16172
  {
14915
16173
  "guidance": "dont",
14916
- "description": "Do not use Popover for blocking confirmations, long forms, or critical information."
16174
+ "description": "Do not use Popover for blocking confirmations, long forms, or critical information.",
16175
+ "instead": [
16176
+ "Dialog"
16177
+ ]
14917
16178
  },
14918
16179
  {
14919
16180
  "guidance": "dont",
14920
- "description": "Do not put a form that can fail validation inside one. A popover dismisses on outside click, which discards what the user typed with no warning — that is a Dialog."
16181
+ "description": "Do not put a form that can fail validation inside one. A popover dismisses on outside click, which discards what the user typed with no warning — that is a Dialog.",
16182
+ "instead": [
16183
+ "Dialog"
16184
+ ]
14921
16185
  }
14922
16186
  ],
14923
16187
  "anatomy": [
@@ -15054,7 +16318,10 @@
15054
16318
  },
15055
16319
  {
15056
16320
  "guidance": "dont",
15057
- "description": "Do not use a determinate Progress for an unknown duration. A bar that sits at 90% is worse than a Spinner, because it makes a promise about time that you cannot keep."
16321
+ "description": "Do not use a determinate Progress for an unknown duration. A bar that sits at 90% is worse than a Spinner, because it makes a promise about time that you cannot keep.",
16322
+ "instead": [
16323
+ "Spinner"
16324
+ ]
15058
16325
  }
15059
16326
  ],
15060
16327
  "anatomy": [
@@ -15331,11 +16598,18 @@
15331
16598
  "default": "runtime locale",
15332
16599
  "description": "BCP 47 locale for default number and date formatting."
15333
16600
  },
16601
+ {
16602
+ "name": "motion",
16603
+ "type": "\"full\" | \"subtle\" | \"off\"",
16604
+ "default": "\"full\"",
16605
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The polygons growing in and morphing are the flourish; the hover highlight is the feedback `subtle` keeps."
16606
+ },
15334
16607
  {
15335
16608
  "name": "animate",
15336
16609
  "type": "boolean",
15337
16610
  "default": "true",
15338
- "description": "Grow the polygons from the centre when the chart scrolls into view."
16611
+ "description": "Grow the polygons from the centre when the chart scrolls into view. `animate={false}` is `motion=\"subtle\"`.",
16612
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
15339
16613
  },
15340
16614
  {
15341
16615
  "name": "activeIndex",
@@ -15372,7 +16646,7 @@
15372
16646
  "slot": "arrival",
15373
16647
  "recipe": "custom",
15374
16648
  "part": "Polygon",
15375
- "why": "Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart."
16649
+ "why": "Once on screen each polygon grows from the centre and fades in, series staggered, on the emphasized easing. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
15376
16650
  },
15377
16651
  {
15378
16652
  "slot": "response",
@@ -15384,7 +16658,7 @@
15384
16658
  "slot": "transition",
15385
16659
  "recipe": "custom",
15386
16660
  "part": "Polygon",
15387
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
16661
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
15388
16662
  },
15389
16663
  {
15390
16664
  "slot": "activity",
@@ -15548,11 +16822,18 @@
15548
16822
  "default": "runtime locale",
15549
16823
  "description": "BCP 47 locale for default number and date formatting."
15550
16824
  },
16825
+ {
16826
+ "name": "motion",
16827
+ "type": "\"full\" | \"subtle\" | \"off\"",
16828
+ "default": "\"full\"",
16829
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. The draw-on, the ring morph and the rolling centre number are the flourish; the hover dim is the feedback `subtle` keeps."
16830
+ },
15551
16831
  {
15552
16832
  "name": "animate",
15553
16833
  "type": "boolean",
15554
16834
  "default": "true",
15555
- "description": "Draw the rings round from their start when the chart scrolls into view."
16835
+ "description": "Draw the rings round from their start when the chart scrolls into view. `animate={false}` is `motion=\"subtle\"`.",
16836
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to: no arrival, hover kept) or motion=\"off\""
15556
16837
  },
15557
16838
  {
15558
16839
  "name": "activeIndex",
@@ -15589,19 +16870,25 @@
15589
16870
  "slot": "arrival",
15590
16871
  "recipe": "custom",
15591
16872
  "part": "Ring",
15592
- "why": "Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart."
16873
+ "why": "Once on screen each ring draws round from its start (stroke-dashoffset on a round-capped arc), staggered outside in. On the motion tokens; reduced motion shows the finished chart. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished chart while hover feedback stays."
15593
16874
  },
15594
16875
  {
15595
16876
  "slot": "response",
15596
16877
  "recipe": "none",
15597
16878
  "part": "Active ring",
15598
- "why": "The hovered ring stays full strength and the others dim at once."
16879
+ "why": "The hovered ring stays full strength and the others dim at once — mixed toward the surface rather than faded, so a dimmed ring stays solid over its track instead of turning muddy. Essential: `subtle` keeps it."
16880
+ },
16881
+ {
16882
+ "slot": "transition",
16883
+ "recipe": "custom",
16884
+ "part": "Centre value",
16885
+ "why": "A numeric centre value rolls to its new total digit by digit (NumberTicker) when the data changes, so the headline is seen moving with the rings instead of being found different afterwards. Expressive: under `subtle` it updates in place."
15599
16886
  },
15600
16887
  {
15601
16888
  "slot": "transition",
15602
16889
  "recipe": "custom",
15603
16890
  "part": "Ring",
15604
- "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place."
16891
+ "why": "Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place. Expressive, like the arrival: under `subtle` new data swaps in place."
15605
16892
  },
15606
16893
  {
15607
16894
  "slot": "activity",
@@ -15654,7 +16941,11 @@
15654
16941
  },
15655
16942
  {
15656
16943
  "guidance": "dont",
15657
- "description": "Do not use Radio for independent toggles or multiple selection; use Checkbox or Switch."
16944
+ "description": "Do not use Radio for independent toggles or multiple selection; use Checkbox or Switch.",
16945
+ "instead": [
16946
+ "Checkbox",
16947
+ "Switch"
16948
+ ]
15658
16949
  },
15659
16950
  {
15660
16951
  "guidance": "dont",
@@ -15703,6 +16994,32 @@
15703
16994
  "primary",
15704
16995
  "ring"
15705
16996
  ],
16997
+ "motion": [
16998
+ {
16999
+ "slot": "arrival",
17000
+ "recipe": "none",
17001
+ "part": "Root",
17002
+ "why": "Radios arrive with the form they belong to. Animating each one in turns a question into a title sequence."
17003
+ },
17004
+ {
17005
+ "slot": "response",
17006
+ "recipe": "motion-interactive",
17007
+ "part": "Root",
17008
+ "why": "The ring's edge and focus ring ease, so arrowing through a group — which moves the selection with focus — reads as one choice moving down the list."
17009
+ },
17010
+ {
17011
+ "slot": "transition",
17012
+ "recipe": "motion-interactive",
17013
+ "part": "Indicator",
17014
+ "why": "The dot grows from the centre on a spring and shrinks out of the radio being left. A group's selection moves from one option to another, and the paired grow and shrink is what makes it read as moving rather than two dots blinking."
17015
+ },
17016
+ {
17017
+ "slot": "activity",
17018
+ "recipe": "none",
17019
+ "part": "Root",
17020
+ "why": "A radio commits instantly. A slow write belongs to the surrounding form's pending state, not the control."
17021
+ }
17022
+ ],
15706
17023
  "a11y": [
15707
17024
  "Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.",
15708
17025
  "Arrow keys move and select between items; Space selects the focused item; Tab enters/leaves the group.",
@@ -15855,6 +17172,44 @@
15855
17172
  "tone-info",
15856
17173
  "tone-warning"
15857
17174
  ],
17175
+ "motion": [
17176
+ {
17177
+ "slot": "arrival",
17178
+ "recipe": "none",
17179
+ "part": "Root",
17180
+ "why": "Cards arrive with the form. A plan picker that animated each card in would delay the choice it exists to make."
17181
+ },
17182
+ {
17183
+ "slot": "response",
17184
+ "recipe": "motion-interactive",
17185
+ "part": "Root",
17186
+ "why": "The edge steps toward the ring colour on hover, eased. On a card-sized target a hard snap across the whole border reads as the layout flickering."
17187
+ },
17188
+ {
17189
+ "slot": "response",
17190
+ "recipe": "motion-press",
17191
+ "part": "Root",
17192
+ "why": "The settle on press confirms a click anywhere in the card — the radio is small, and without it a click in the card's body can feel like it missed the control."
17193
+ },
17194
+ {
17195
+ "slot": "transition",
17196
+ "recipe": "motion-interactive",
17197
+ "part": "Root",
17198
+ "why": "Checking eases the tone ring and wash onto the new card as the old one releases, so the choice reads as moving between cards rather than two cards repainting."
17199
+ },
17200
+ {
17201
+ "slot": "transition",
17202
+ "recipe": "motion-interactive",
17203
+ "part": "Indicator",
17204
+ "why": "The radio dot grows in on a spring, the same mark as Radio, so a card and a plain radio confirm a choice the same way."
17205
+ },
17206
+ {
17207
+ "slot": "activity",
17208
+ "recipe": "none",
17209
+ "part": "Root",
17210
+ "why": "A choice commits instantly. A slow write belongs to the surrounding form's pending state."
17211
+ }
17212
+ ],
15858
17213
  "a11y": [
15859
17214
  "Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.",
15860
17215
  "Arrow keys move and select between cards; Space selects the focused card; Tab enters/leaves the group.",
@@ -15943,6 +17298,38 @@
15943
17298
  "border",
15944
17299
  "ring"
15945
17300
  ],
17301
+ "motion": [
17302
+ {
17303
+ "slot": "arrival",
17304
+ "recipe": "none",
17305
+ "part": "Root",
17306
+ "why": "It is a word in a line of text and arrives with that line."
17307
+ },
17308
+ {
17309
+ "slot": "response",
17310
+ "recipe": "none",
17311
+ "part": "Root",
17312
+ "why": "The only hover change on the timestamp is a dotted underline, and text-decoration-line is discrete: CSS cannot tween it, so a transition here would animate nothing and imply the snap had been handled."
17313
+ },
17314
+ {
17315
+ "slot": "response",
17316
+ "recipe": "motion-pop",
17317
+ "part": "Tooltip",
17318
+ "why": "The pointer's real answer is the absolute time, which arrives in a Tooltip on its own fast pop. That short entrance is what makes the full date read as belonging to the word under the cursor."
17319
+ },
17320
+ {
17321
+ "slot": "transition",
17322
+ "recipe": "none",
17323
+ "part": "Root",
17324
+ "why": "The label does not tick: it is computed when the component renders, so it never changes in front of the reader. A timestamp rolling from one minute to two would pull the eye off the content it dates."
17325
+ },
17326
+ {
17327
+ "slot": "activity",
17328
+ "recipe": "none",
17329
+ "part": "Root",
17330
+ "why": "A timestamp has nothing to wait for."
17331
+ }
17332
+ ],
15946
17333
  "a11y": [
15947
17334
  "Renders a semantic <time> element with a machine-readable dateTime (ISO) attribute.",
15948
17335
  "The <time> carries tabIndex=0 (it is not focusable natively), so the absolute time — exposed via Base UI's Tooltip, which opens on keyboard focus as well as hover and wires aria-describedby — is reachable from the keyboard.",
@@ -16067,6 +17454,38 @@
16067
17454
  "tokensUsed": [
16068
17455
  "ring"
16069
17456
  ],
17457
+ "motion": [
17458
+ {
17459
+ "slot": "arrival",
17460
+ "recipe": "none",
17461
+ "part": "Root",
17462
+ "why": "It is a region, not an event; its content arrives with whatever fills it."
17463
+ },
17464
+ {
17465
+ "slot": "response",
17466
+ "recipe": "motion-interactive",
17467
+ "part": "Viewport",
17468
+ "why": "The region is focusable so the keyboard can scroll it, and its focus ring eases in like any control's. It is the one sign the region, not the page, now takes the arrow keys."
17469
+ },
17470
+ {
17471
+ "slot": "response",
17472
+ "recipe": "custom",
17473
+ "part": "Scrollbar",
17474
+ "why": "The vertical bar (`scrollbar-quiet`) stays hidden until the region is hovered, focused or scrolled, then fades its thumb in on the motion tokens. A bar that appeared in one frame would flash every time the pointer crossed the list."
17475
+ },
17476
+ {
17477
+ "slot": "transition",
17478
+ "recipe": "none",
17479
+ "part": "Edge fade",
17480
+ "why": "The edge fades switch on and off as you scroll, in the frame the content is already moving, so the change is carried by the scroll itself. They are a mask gradient, which CSS cannot interpolate without registered custom properties."
17481
+ },
17482
+ {
17483
+ "slot": "activity",
17484
+ "recipe": "none",
17485
+ "part": "Root",
17486
+ "why": "A scroll region does no work of its own. Content still loading inside it brings its own Skeleton or Spinner."
17487
+ }
17488
+ ],
16070
17489
  "a11y": [
16071
17490
  "The edge fades are a CSS mask-image on the scrolling element, not overlay elements — there is nothing in the DOM to intercept a pointer or be read by assistive tech, and the content under a fade stays fully interactive.",
16072
17491
  "Two boxes: a chrome box carrying `className` (border, background, radius, sizing) and, inside it, the element that actually scrolls and carries the fade mask. They are split because a `mask-image` applies to its element's own border — so with both on one box, a bordered Scroller watched its frame dissolve at the scrolling edge. The axis is published as `data-orientation`, not `aria-orientation`, which is invalid on `role=\"group\"`.",
@@ -16234,6 +17653,38 @@
16234
17653
  "panel-field",
16235
17654
  "panel-field-hover"
16236
17655
  ],
17656
+ "motion": [
17657
+ {
17658
+ "slot": "arrival",
17659
+ "recipe": "none",
17660
+ "part": "Root",
17661
+ "why": "It arrives with the inspector row it sits in."
17662
+ },
17663
+ {
17664
+ "slot": "response",
17665
+ "recipe": "motion-interactive",
17666
+ "part": "Root",
17667
+ "why": "The shell eases its focus ring like every field, so tabbing through a column of numbers shows where the keyboard is without flicker."
17668
+ },
17669
+ {
17670
+ "slot": "response",
17671
+ "recipe": "motion-interactive",
17672
+ "part": "Handle",
17673
+ "why": "The handle's ink brightens while the field is hovered. It is the only sign that the icon or prefix can be dragged, which nobody would guess from a label."
17674
+ },
17675
+ {
17676
+ "slot": "transition",
17677
+ "recipe": "none",
17678
+ "part": "Value",
17679
+ "why": "The number follows the drag pixel for pixel. Easing it — or rolling it with a NumberTicker — would show a value the pointer has already left, in a control whose whole point is precision."
17680
+ },
17681
+ {
17682
+ "slot": "activity",
17683
+ "recipe": "none",
17684
+ "part": "Root",
17685
+ "why": "Scrubbing is synchronous; there is nothing to wait for."
17686
+ }
17687
+ ],
16237
17688
  "a11y": [
16238
17689
  "The input carries the accessible name via `label`; the glyph is aria-hidden because it is decorative.",
16239
17690
  "Every scrub gesture is reachable from the keyboard: ArrowUp/ArrowDown step by `step`, Shift+Arrow by ten steps.",
@@ -16364,6 +17815,32 @@
16364
17815
  "ring",
16365
17816
  "destructive"
16366
17817
  ],
17818
+ "motion": [
17819
+ {
17820
+ "slot": "arrival",
17821
+ "recipe": "motion-appear",
17822
+ "part": "Clear button",
17823
+ "why": "The X mounts as the first character lands. Fading and settling in from a smaller scale keeps it from popping into the corner of the eye on every new search; removal is instant so an emptied field does not wait on it."
17824
+ },
17825
+ {
17826
+ "slot": "response",
17827
+ "recipe": "motion-interactive",
17828
+ "part": "Input",
17829
+ "why": "The focus ring and invalid border ease in, so focus reads as the field answering rather than a repaint."
17830
+ },
17831
+ {
17832
+ "slot": "transition",
17833
+ "recipe": "none",
17834
+ "part": "Root",
17835
+ "why": "The field has no states of its own to move between — the clear button arriving is its only change, and arrival covers it."
17836
+ },
17837
+ {
17838
+ "slot": "activity",
17839
+ "recipe": "none",
17840
+ "part": "Root",
17841
+ "why": "Searching is the results' job: a list skeleton or a Spinner beside them, not a field that animates while you type."
17842
+ }
17843
+ ],
16367
17844
  "a11y": [
16368
17845
  "Renders a native <input type=\"search\"> — keyboard and form semantics come for free.",
16369
17846
  "Associate a <label htmlFor> (or wrap) — the magnifier glyph is decorative, not a label.",
@@ -16611,7 +18088,10 @@
16611
18088
  },
16612
18089
  {
16613
18090
  "guidance": "dont",
16614
- "description": "Use it for a single value against a limit; that is Meter, which carries the meter role and its value."
18091
+ "description": "Use it for a single value against a limit; that is Meter, which carries the meter role and its value.",
18092
+ "instead": [
18093
+ "Meter"
18094
+ ]
16615
18095
  },
16616
18096
  {
16617
18097
  "guidance": "dont",
@@ -16772,11 +18252,18 @@
16772
18252
  },
16773
18253
  {
16774
18254
  "guidance": "dont",
16775
- "description": "Use Select for very large or fuzzy lists. Use Combobox when typing is the primary way to find an option."
18255
+ "description": "Use Select for very large or fuzzy lists. Use Combobox when typing is the primary way to find an option.",
18256
+ "instead": [
18257
+ "Combobox"
18258
+ ]
16776
18259
  },
16777
18260
  {
16778
18261
  "guidance": "dont",
16779
- "description": "Use Select for binary on/off settings. Use Switch or Checkbox instead."
18262
+ "description": "Use Select for binary on/off settings. Use Switch or Checkbox instead.",
18263
+ "instead": [
18264
+ "Switch",
18265
+ "Checkbox"
18266
+ ]
16780
18267
  },
16781
18268
  {
16782
18269
  "guidance": "do",
@@ -17257,11 +18744,17 @@
17257
18744
  },
17258
18745
  {
17259
18746
  "guidance": "dont",
17260
- "description": "Do not use Sheet for simple confirmations or deeply nested workflows."
18747
+ "description": "Do not use Sheet for simple confirmations or deeply nested workflows.",
18748
+ "instead": [
18749
+ "Dialog"
18750
+ ]
17261
18751
  },
17262
18752
  {
17263
18753
  "guidance": "dont",
17264
- "description": "Do not use it on desktop where a Dialog fits. A sheet's affordance is edge-anchored travel for thumb reach; on a wide viewport it becomes a dialog that arrives from the wrong place."
18754
+ "description": "Do not use it on desktop where a Dialog fits. A sheet's affordance is edge-anchored travel for thumb reach; on a wide viewport it becomes a dialog that arrives from the wrong place.",
18755
+ "instead": [
18756
+ "Dialog"
18757
+ ]
17265
18758
  }
17266
18759
  ],
17267
18760
  "anatomy": [
@@ -17507,23 +19000,61 @@
17507
19000
  "foreground",
17508
19001
  "ring"
17509
19002
  ],
17510
- "a11y": [
17511
- "The toggle is a real <button> with aria-expanded reflecting the open state.",
17512
- "The fade overlay is pointer-events-none and decorative; it never traps clicks.",
17513
- "Collapsed content stays in the DOM, so screen readers can still reach it."
17514
- ],
17515
- "examples": [
19003
+ "motion": [
17516
19004
  {
17517
- "label": "Long paragraph",
17518
- "code": "<ShowMore><p className=\"text-body-md text-foreground\">A long paragraph that exceeds the collapsed height…</p></ShowMore>",
17519
- "imports": "import { ShowMore } from \"@sableui/react\""
19005
+ "slot": "arrival",
19006
+ "recipe": "none",
19007
+ "part": "Root",
19008
+ "why": "The preview arrives with the content it clips. Its fade and toggle appear only once a measurement proves the content overflows."
17520
19009
  },
17521
19010
  {
17522
- "label": "Collapsed to a line count",
17523
- "code": "<ShowMore collapsedLines={6}><p className=\"text-body-md text-foreground\">{post.description}</p></ShowMore>",
17524
- "imports": "import { ShowMore } from \"@sableui/react\""
17525
- }
17526
- ]
19011
+ "slot": "response",
19012
+ "recipe": "motion-interactive",
19013
+ "part": "Toggle",
19014
+ "why": "The toggle is a link Button, so its hover and focus ease on Button's recipe."
19015
+ },
19016
+ {
19017
+ "slot": "transition",
19018
+ "recipe": "custom",
19019
+ "part": "Content",
19020
+ "why": "The region eases its max-height between the collapsed and the measured full height on the slow motion token. motion-disclosure keys on Base UI's starting and ending styles, which this self-contained toggle does not have. The text unrolling in place keeps the reader's line; a snap drops them at a different scroll position."
19021
+ },
19022
+ {
19023
+ "slot": "transition",
19024
+ "recipe": "motion-interactive",
19025
+ "part": "Fade",
19026
+ "why": "The bottom fade dissolves as the region opens and returns as it closes, so the cut edge and the reveal read as one movement."
19027
+ },
19028
+ {
19029
+ "slot": "transition",
19030
+ "recipe": "motion-interactive",
19031
+ "part": "Chevron",
19032
+ "why": "The chevron turns 180° in step with the region (motion-interactive lists `rotate`), so the toggle visibly points at what it will do next."
19033
+ },
19034
+ {
19035
+ "slot": "activity",
19036
+ "recipe": "none",
19037
+ "part": "Root",
19038
+ "why": "Expanding is local and instant; nothing loads."
19039
+ }
19040
+ ],
19041
+ "a11y": [
19042
+ "The toggle is a real <button> with aria-expanded reflecting the open state.",
19043
+ "The fade overlay is pointer-events-none and decorative; it never traps clicks.",
19044
+ "Collapsed content stays in the DOM, so screen readers can still reach it."
19045
+ ],
19046
+ "examples": [
19047
+ {
19048
+ "label": "Long paragraph",
19049
+ "code": "<ShowMore><p className=\"text-body-md text-foreground\">A long paragraph that exceeds the collapsed height…</p></ShowMore>",
19050
+ "imports": "import { ShowMore } from \"@sableui/react\""
19051
+ },
19052
+ {
19053
+ "label": "Collapsed to a line count",
19054
+ "code": "<ShowMore collapsedLines={6}><p className=\"text-body-md text-foreground\">{post.description}</p></ShowMore>",
19055
+ "imports": "import { ShowMore } from \"@sableui/react\""
19056
+ }
19057
+ ]
17527
19058
  },
17528
19059
  {
17529
19060
  "name": "Sidebar",
@@ -17606,6 +19137,12 @@
17606
19137
  "type": "\"rail\" | \"page\"",
17607
19138
  "default": "\"rail\"",
17608
19139
  "description": "Which surface the rail is painted on. \"rail\" is its own --sidebar-* surface with a border — right for a full-height rail that frames the app, which a theme may tint or paint dark. \"page\" uses the page's surface and ink with no border — use it when a full-width header sits above the rail, where a tinted column starting under a page-coloured bar reads as pasted on. Use it instead of overriding bg-/text- classes, which the rail's re-pointed tokens defeat."
19140
+ },
19141
+ {
19142
+ "name": "motion",
19143
+ "type": "\"full\" | \"subtle\" | \"off\"",
19144
+ "default": "\"full\"",
19145
+ "description": "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the collapse ease and the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
17609
19146
  }
17610
19147
  ],
17611
19148
  "subcomponents": [
@@ -17639,6 +19176,38 @@
17639
19176
  "sidebar-accent-foreground",
17640
19177
  "sidebar-ring"
17641
19178
  ],
19179
+ "motion": [
19180
+ {
19181
+ "slot": "arrival",
19182
+ "recipe": "none",
19183
+ "part": "Root",
19184
+ "why": "The rail is the app's frame, present from first paint. It is not summoned, so it has no entrance."
19185
+ },
19186
+ {
19187
+ "slot": "response",
19188
+ "recipe": "motion-interactive",
19189
+ "part": "SidebarItem",
19190
+ "why": "Rows ease their hover fill (SideNav's row recipe on the rail rung), so moving down the nav reads as the pointer travelling rather than rows flashing."
19191
+ },
19192
+ {
19193
+ "slot": "transition",
19194
+ "recipe": "motion-glide",
19195
+ "part": "SidebarNav",
19196
+ "why": "The current-page fill is one element that travels to the new item when the page changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, and each item keeps its own fill as the server-render fallback. Expressive: `subtle` moves it at once."
19197
+ },
19198
+ {
19199
+ "slot": "transition",
19200
+ "recipe": "motion-size",
19201
+ "part": "Root",
19202
+ "why": "Collapsing to the icon rail eases the width instead of jumping 192px in one frame, so the content beside it visibly takes the room the rail gave up. Nothing inside jumps — a collapsed row's centred icon and an expanded row's inset icon sit on the same column — and labels leave at once and fade back in on reopen, clipped to the rail meanwhile. It reflows the page for the length of the ease, which is acceptable for a deliberate user toggle and never happens on load. Expressive: `subtle` snaps."
19203
+ },
19204
+ {
19205
+ "slot": "activity",
19206
+ "recipe": "none",
19207
+ "part": "Root",
19208
+ "why": "Navigation chrome has no working state; a loading route shows it in the page."
19209
+ }
19210
+ ],
17642
19211
  "a11y": [
17643
19212
  "SidebarNav renders a <nav> landmark.",
17644
19213
  "Active item carries aria-current=\"page\".",
@@ -17701,7 +19270,10 @@
17701
19270
  },
17702
19271
  {
17703
19272
  "guidance": "dont",
17704
- "description": "Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot."
19273
+ "description": "Use SideNav for bottom mobile navigation; use BottomNav in AppShell's bottomNav slot.",
19274
+ "instead": [
19275
+ "BottomNav"
19276
+ ]
17705
19277
  },
17706
19278
  {
17707
19279
  "guidance": "dont",
@@ -17755,6 +19327,12 @@
17755
19327
  "type": "\"content\" | \"sidebar\"",
17756
19328
  "default": "\"content\"",
17757
19329
  "description": "Active-state token vocabulary shared by all items. \"content\" uses bg-accent/text-accent-foreground for local rails inside the content area; \"sidebar\" uses bg-sidebar-accent/text-sidebar-accent-foreground for SideNavs rendered inside app chrome."
19330
+ },
19331
+ {
19332
+ "name": "motion",
19333
+ "type": "\"full\" | \"subtle\" | \"off\"",
19334
+ "default": "\"full\"",
19335
+ "description": "How much motion this component uses: `full` inherits the app's policy, `subtle` keeps feedback and drops the current-page glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
17758
19336
  }
17759
19337
  ],
17760
19338
  "subcomponents": [
@@ -17781,6 +19359,32 @@
17781
19359
  "sidebar-accent-foreground",
17782
19360
  "sidebar-ring"
17783
19361
  ],
19362
+ "motion": [
19363
+ {
19364
+ "slot": "arrival",
19365
+ "recipe": "none",
19366
+ "part": "Root",
19367
+ "why": "In-page navigation arrives with the page it navigates."
19368
+ },
19369
+ {
19370
+ "slot": "response",
19371
+ "recipe": "motion-interactive",
19372
+ "part": "SideNavItem",
19373
+ "why": "Rows ease their hover fill and focus ring, so moving down a long nav reads as one pointer travelling rather than rows flashing in turn."
19374
+ },
19375
+ {
19376
+ "slot": "transition",
19377
+ "recipe": "motion-glide",
19378
+ "part": "SideNav",
19379
+ "why": "The current-item fill is one element that travels to the new row when the section changes inside a persistent layout, so the marker reads as moving rather than one row dimming while another lights. It lands in place on first paint, each row keeps its own fill as the server-render fallback, and the active row's heavier weight lands at once — font weight does not ease. Expressive: `subtle` moves it at once."
19380
+ },
19381
+ {
19382
+ "slot": "activity",
19383
+ "recipe": "none",
19384
+ "part": "Root",
19385
+ "why": "Navigation chrome has no working state; a loading section shows it in the content."
19386
+ }
19387
+ ],
17784
19388
  "a11y": [
17785
19389
  "SideNav renders a <nav> landmark; pair multiple navs with aria-label to distinguish them.",
17786
19390
  "The active item carries aria-current=\"page\".",
@@ -17841,7 +19445,10 @@
17841
19445
  },
17842
19446
  {
17843
19447
  "guidance": "dont",
17844
- "description": "Do not leave Skeleton visible after an error or empty state; switch to EmptyState (tone='destructive' for an error)."
19448
+ "description": "Do not leave Skeleton visible after an error or empty state; switch to EmptyState (tone='destructive' for an error).",
19449
+ "instead": [
19450
+ "EmptyState"
19451
+ ]
17845
19452
  },
17846
19453
  {
17847
19454
  "guidance": "dont",
@@ -18090,6 +19697,32 @@
18090
19697
  "background",
18091
19698
  "ring"
18092
19699
  ],
19700
+ "motion": [
19701
+ {
19702
+ "slot": "arrival",
19703
+ "recipe": "none",
19704
+ "part": "Root",
19705
+ "why": "It arrives with the form or panel it sits in."
19706
+ },
19707
+ {
19708
+ "slot": "response",
19709
+ "recipe": "motion-interactive",
19710
+ "part": "Thumb",
19711
+ "why": "The thumb's dot grows from 12px to full size on hover and keyboard focus, and a little past it while dragging, on a spring. Only the paint resizes, never the hit target, so the growth says the thumb is live without moving where you have to aim."
19712
+ },
19713
+ {
19714
+ "slot": "transition",
19715
+ "recipe": "none",
19716
+ "part": "Thumb",
19717
+ "why": "The thumb and the filled range follow the pointer one-to-one, and keyboard steps land at once. An eased position would trail the drag and show a value the user has already moved past."
19718
+ },
19719
+ {
19720
+ "slot": "activity",
19721
+ "recipe": "none",
19722
+ "part": "Root",
19723
+ "why": "A slider commits as it moves; there is nothing to wait for."
19724
+ }
19725
+ ],
18093
19726
  "a11y": [
18094
19727
  "Base UI provides role=slider, aria-valuemin/max/now, and arrow-key / Page Up-Down / Home-End control.",
18095
19728
  "Each thumb is a focusable input[type=range] nested in the thumb element; keyboard focus raises a 2px ring on the thumb via has-[:focus-visible] (a bare focus-visible: on the thumb never fires, since the div itself is not focusable).",
@@ -18206,11 +19839,18 @@
18206
19839
  "default": "\"h-8 w-24\"",
18207
19840
  "description": "Sizes it; the drawing stretches to fit."
18208
19841
  },
19842
+ {
19843
+ "name": "motion",
19844
+ "type": "\"full\" | \"subtle\" | \"off\"",
19845
+ "default": "\"full\"",
19846
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Drawing on (or rising) is the flourish, so `subtle` and `off` show the finished line."
19847
+ },
18209
19848
  {
18210
19849
  "name": "animate",
18211
19850
  "type": "boolean",
18212
19851
  "default": "true",
18213
- "description": "Draw on (or rise) when it scrolls into view."
19852
+ "description": "Draw on (or rise) when it scrolls into view. `animate={false}` is `motion=\"subtle\"`.",
19853
+ "deprecated": "motion=\"subtle\" (what animate={false} now maps to) or motion=\"off\""
18214
19854
  },
18215
19855
  {
18216
19856
  "name": "locale",
@@ -18242,7 +19882,7 @@
18242
19882
  "slot": "arrival",
18243
19883
  "recipe": "custom",
18244
19884
  "part": "Line / bars",
18245
- "why": "Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing."
19885
+ "why": "Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing. The arrival is expressive: it plays at the full motion level only, so `motion=\"subtle\"` (or a subtle app policy) shows the finished line."
18246
19886
  },
18247
19887
  {
18248
19888
  "slot": "response",
@@ -18306,7 +19946,11 @@
18306
19946
  },
18307
19947
  {
18308
19948
  "guidance": "dont",
18309
- "description": "Do not use Spinner as the only content for long waits or full page loading."
19949
+ "description": "Do not use Spinner as the only content for long waits or full page loading.",
19950
+ "instead": [
19951
+ "Skeleton",
19952
+ "PageLoader"
19953
+ ]
18310
19954
  },
18311
19955
  {
18312
19956
  "guidance": "dont",
@@ -18697,11 +20341,17 @@
18697
20341
  },
18698
20342
  {
18699
20343
  "guidance": "dont",
18700
- "description": "Do not use Stepper for a task with unknown length or for a percentage. That is Progress; a stepper promises a fixed set of named stops."
20344
+ "description": "Do not use Stepper for a task with unknown length or for a percentage. That is Progress; a stepper promises a fixed set of named stops.",
20345
+ "instead": [
20346
+ "Progress"
20347
+ ]
18701
20348
  },
18702
20349
  {
18703
20350
  "guidance": "dont",
18704
- "description": "Do not use it as navigation between independent sections. Sections the user can visit in any order are Tabs; a stepper implies each step depends on the one before."
20351
+ "description": "Do not use it as navigation between independent sections. Sections the user can visit in any order are Tabs; a stepper implies each step depends on the one before.",
20352
+ "instead": [
20353
+ "Tabs"
20354
+ ]
18705
20355
  },
18706
20356
  {
18707
20357
  "guidance": "dont",
@@ -19042,7 +20692,11 @@
19042
20692
  },
19043
20693
  {
19044
20694
  "guidance": "dont",
19045
- "description": "Do not use Switch for one-time agreement, multi-select choices, or submit-only booleans."
20695
+ "description": "Do not use Switch for one-time agreement, multi-select choices, or submit-only booleans.",
20696
+ "instead": [
20697
+ "Checkbox",
20698
+ "Radio"
20699
+ ]
19046
20700
  },
19047
20701
  {
19048
20702
  "guidance": "dont",
@@ -19197,7 +20851,11 @@
19197
20851
  },
19198
20852
  {
19199
20853
  "guidance": "dont",
19200
- "description": "Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead."
20854
+ "description": "Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead.",
20855
+ "instead": [
20856
+ "Card",
20857
+ "Entity"
20858
+ ]
19201
20859
  },
19202
20860
  {
19203
20861
  "guidance": "dont",
@@ -19289,6 +20947,32 @@
19289
20947
  "muted",
19290
20948
  "muted-foreground"
19291
20949
  ],
20950
+ "motion": [
20951
+ {
20952
+ "slot": "arrival",
20953
+ "recipe": "none",
20954
+ "part": "Root",
20955
+ "why": "A table renders with the page it sits in. Staggering rows in delays reading data someone opened to scan."
20956
+ },
20957
+ {
20958
+ "slot": "response",
20959
+ "recipe": "motion-interactive",
20960
+ "part": "TableRow",
20961
+ "why": "Body rows ease to a muted wash under the pointer. Across a wide row, the wash is what keeps the eye on the right line, and eased it follows the cursor down the table rather than strobing."
20962
+ },
20963
+ {
20964
+ "slot": "transition",
20965
+ "recipe": "motion-interactive",
20966
+ "part": "TableCell",
20967
+ "why": "A `data-selected` row's fill eases in on every cell together, so the selected row reads as one band being set rather than a stamp."
20968
+ },
20969
+ {
20970
+ "slot": "activity",
20971
+ "recipe": "none",
20972
+ "part": "Root",
20973
+ "why": "Table is markup. Loading rows are Skeletons, which carry the shimmer."
20974
+ }
20975
+ ],
19292
20976
  "a11y": [
19293
20977
  "Renders semantic native table elements (thead/tbody/tfoot/th/td/caption) preserving built-in table semantics.",
19294
20978
  "Use TableHead (th) for column headers and TableCaption for an accessible description.",
@@ -19323,7 +21007,7 @@
19323
21007
  "group": "Navigation",
19324
21008
  "register": "either",
19325
21009
  "path": "components/tabs",
19326
- "summary": "Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers). Tabs (Root) + TabsList + TabsTrigger + TabsPanel.",
21010
+ "summary": "Tabs over Base UI Tabs (roving focus, arrow-key nav, ARIA tablist wiring) in two styles: underline (page sections) or pill (compact switchers), with one indicator that glides to the active tab. Tabs (Root) + TabsList + TabsTrigger + TabsPanel.",
19327
21011
  "keywords": [
19328
21012
  "tabs",
19329
21013
  "tablist",
@@ -19347,11 +21031,18 @@
19347
21031
  },
19348
21032
  {
19349
21033
  "guidance": "dont",
19350
- "description": "Do not use Tabs for unrelated pages or primary navigation that needs distinct URLs."
21034
+ "description": "Do not use Tabs for unrelated pages or primary navigation that needs distinct URLs.",
21035
+ "instead": [
21036
+ "SideNav",
21037
+ "NavigationMenu"
21038
+ ]
19351
21039
  },
19352
21040
  {
19353
21041
  "guidance": "dont",
19354
- "description": "Do not use tabs for sequential steps. Tabs are peer views the user returns to in any order; a process with an order needs a stepper, or the user will skip ahead."
21042
+ "description": "Do not use tabs for sequential steps. Tabs are peer views the user returns to in any order; a process with an order needs a stepper, or the user will skip ahead.",
21043
+ "instead": [
21044
+ "Stepper"
21045
+ ]
19355
21046
  }
19356
21047
  ],
19357
21048
  "anatomy": [
@@ -19390,13 +21081,19 @@
19390
21081
  "type": "Base UI Tabs.Root props (value, defaultValue, onValueChange, orientation)",
19391
21082
  "default": "defaultValue: 0",
19392
21083
  "description": "Controls the active tab. Uncontrolled via defaultValue; controlled via value + onValueChange."
21084
+ },
21085
+ {
21086
+ "name": "motion",
21087
+ "type": "\"full\" | \"subtle\" | \"off\"",
21088
+ "default": "\"full\"",
21089
+ "description": "On Tabs: how much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
19393
21090
  }
19394
21091
  ],
19395
21092
  "subcomponents": [
19396
21093
  {
19397
21094
  "name": "TabsList",
19398
21095
  "type": "Base UI Tabs.List props (activateOnFocus, loopFocus) + variant?: \"underline\" | \"pill\" + className",
19399
- "description": "The tablist row. variant=\"underline\" (default) draws a divider rule beneath the triggers; variant=\"pill\" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant."
21096
+ "description": "The tablist row. variant=\"underline\" (default) draws a divider rule beneath the triggers; variant=\"pill\" drops the rule and fills the active trigger, which takes the theme's button shape (--button-radius). Triggers inherit the variant. It renders the gliding indicator (Base UI Tabs.Indicator) itself, so do not add one."
19400
21097
  },
19401
21098
  {
19402
21099
  "name": "TabsTrigger",
@@ -19421,11 +21118,44 @@
19421
21118
  "foreground",
19422
21119
  "ring"
19423
21120
  ],
21121
+ "motion": [
21122
+ {
21123
+ "slot": "arrival",
21124
+ "recipe": "none",
21125
+ "part": "Root",
21126
+ "why": "Tabs arrive with the section they organise. An entrance of their own would announce navigation chrome louder than the content it navigates."
21127
+ },
21128
+ {
21129
+ "slot": "response",
21130
+ "recipe": "motion-interactive",
21131
+ "part": "TabsTrigger",
21132
+ "why": "The label steps from muted to foreground under the pointer; the ease is what makes that read as an answer rather than a repaint."
21133
+ },
21134
+ {
21135
+ "slot": "transition",
21136
+ "recipe": "motion-glide",
21137
+ "part": "Indicator",
21138
+ "why": "One underline (or pill) travels to the new tab and stretches to its width. A mark that teleports makes the reader re-find the selection on every click; one that moves is tracked without looking. Expressive: motion=\"subtle\" lands it in place. Each tab's own mark is the fallback until the indicator has measured, so the server render is already right."
21139
+ },
21140
+ {
21141
+ "slot": "transition",
21142
+ "recipe": "animate-fade-in",
21143
+ "part": "TabsPanel",
21144
+ "why": "The incoming panel fades in, opacity only, so a switch between two similar panels still registers. Skipped on first render (activation direction \"none\") so a page of tabs does not fade in on load, and the outgoing panel leaves layout at once so a quick second click never stacks both."
21145
+ },
21146
+ {
21147
+ "slot": "activity",
21148
+ "recipe": "none",
21149
+ "part": "Root",
21150
+ "why": "Switching tabs is synchronous. A panel that loads data shows its own Skeleton or Spinner inside the panel, where the wait is."
21151
+ }
21152
+ ],
19424
21153
  "a11y": [
19425
21154
  "Base UI provides the tablist/tab/tabpanel roles, aria-selected, and roving tabindex.",
19426
21155
  "Arrow keys move between tabs; Home/End jump to first/last; panels are linked via aria-controls.",
19427
21156
  "Active tab styling reads from the data-active attribute; disabled tabs from data-disabled. Disabled tabs are skipped by arrow-key navigation.",
19428
- "Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable."
21157
+ "Keyboard focus shows a ring on the focused trigger in both variants; the panel remains focusable.",
21158
+ "The gliding indicator is role=\"presentation\" decoration; aria-selected on the tab carries the state."
19429
21159
  ],
19430
21160
  "examples": [
19431
21161
  {
@@ -19442,6 +21172,11 @@
19442
21172
  "label": "With a disabled tab",
19443
21173
  "code": "<Tabs defaultValue=\"overview\"><TabsList><TabsTrigger value=\"overview\">Overview</TabsTrigger><TabsTrigger value=\"billing\" disabled>Billing</TabsTrigger></TabsList><TabsPanel value=\"overview\">Overview content</TabsPanel></Tabs>",
19444
21174
  "imports": "import { Tabs, TabsList, TabsPanel, TabsTrigger } from \"@sableui/react\""
21175
+ },
21176
+ {
21177
+ "label": "Subtle motion",
21178
+ "code": "<Tabs defaultValue=\"table\" motion=\"subtle\"><TabsList><TabsTrigger value=\"table\">Table</TabsTrigger><TabsTrigger value=\"chart\">Chart</TabsTrigger></TabsList><TabsPanel value=\"table\">Rows</TabsPanel><TabsPanel value=\"chart\">Trend</TabsPanel></Tabs>",
21179
+ "imports": "import { Tabs, TabsList, TabsPanel, TabsTrigger } from \"@sableui/react\""
19445
21180
  }
19446
21181
  ]
19447
21182
  },
@@ -19692,6 +21427,38 @@
19692
21427
  "muted-foreground",
19693
21428
  "ring"
19694
21429
  ],
21430
+ "motion": [
21431
+ {
21432
+ "slot": "arrival",
21433
+ "recipe": "none",
21434
+ "part": "Root",
21435
+ "why": "It sits in a settings panel or a popover, which owns the entrance."
21436
+ },
21437
+ {
21438
+ "slot": "response",
21439
+ "recipe": "motion-interactive",
21440
+ "part": "Option (list)",
21441
+ "why": "List rows ease their hover fill like Menu and Select rows, so a theme list beside a menu reads as the same kind of row."
21442
+ },
21443
+ {
21444
+ "slot": "response",
21445
+ "recipe": "motion-interactive",
21446
+ "part": "Name (grid)",
21447
+ "why": "In the grid, the theme's name brightens on hover, eased. The preview tile's outline steps rather than easing — outline is not in the recipe's transition list — so the name carries the motion."
21448
+ },
21449
+ {
21450
+ "slot": "transition",
21451
+ "recipe": "motion-interactive",
21452
+ "part": "Check (list)",
21453
+ "why": "The check fades into the chosen row and out of the old one, so the choice reads as moving, at the same moment the whole interface repaints in the new theme."
21454
+ },
21455
+ {
21456
+ "slot": "activity",
21457
+ "recipe": "none",
21458
+ "part": "Root",
21459
+ "why": "Applying a theme is synchronous: the whole interface repaints at once, and that repaint is the feedback."
21460
+ }
21461
+ ],
19695
21462
  "a11y": [
19696
21463
  "The container is role=\"radiogroup\" with an aria-label; each card is a role=\"radio\" button with aria-checked reflecting the selected theme.",
19697
21464
  "Standard radio keyboard: arrow keys move the check — calling onValueChange on each press — and Space checks the focused option. That instant switching is right for a free, reversible choice; where a switch has a cost, use a DisclosurePicker list (a listbox: arrows move, Enter commits) with ThemePreview tiles instead.",
@@ -19789,6 +21556,12 @@
19789
21556
  "default": "\"Theme\"",
19790
21557
  "description": "Accessible label for the radiogroup."
19791
21558
  },
21559
+ {
21560
+ "name": "motion",
21561
+ "type": "\"full\" | \"subtle\" | \"off\"",
21562
+ "default": "\"full\"",
21563
+ "description": "How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
21564
+ },
19792
21565
  {
19793
21566
  "name": "className",
19794
21567
  "type": "string",
@@ -19804,6 +21577,38 @@
19804
21577
  "accent-foreground",
19805
21578
  "ring"
19806
21579
  ],
21580
+ "motion": [
21581
+ {
21582
+ "slot": "arrival",
21583
+ "recipe": "none",
21584
+ "part": "Root",
21585
+ "why": "It arrives with the header or settings row it lives in; a theme control that announces itself competes with the page it themes."
21586
+ },
21587
+ {
21588
+ "slot": "response",
21589
+ "recipe": "motion-interactive",
21590
+ "part": "Option",
21591
+ "why": "The icon steps from muted to foreground under the pointer, which is what tells an icon-only segment it is a control."
21592
+ },
21593
+ {
21594
+ "slot": "response",
21595
+ "recipe": "motion-press",
21596
+ "part": "Option",
21597
+ "why": "A settle under the press confirms the tap on a 28px target before the theme change repaints the page around it."
21598
+ },
21599
+ {
21600
+ "slot": "transition",
21601
+ "recipe": "motion-glide",
21602
+ "part": "Active pill",
21603
+ "why": "One pill slides to the chosen mode, stepping by option + gap on the spacing scale, so light → dark reads as the same control moving, not three icons trading a fill. Expressive: motion=\"subtle\" lands it in place."
21604
+ },
21605
+ {
21606
+ "slot": "activity",
21607
+ "recipe": "none",
21608
+ "part": "Root",
21609
+ "why": "Changing the theme is synchronous; the repaint is the feedback."
21610
+ }
21611
+ ],
19807
21612
  "a11y": [
19808
21613
  "The container is role=\"radiogroup\" with an aria-label; each option is role=\"radio\" with aria-checked reflecting the selected value.",
19809
21614
  "Each icon button carries an aria-label (Light / Dark / System) plus a title tooltip, since the visual is icon-only.",
@@ -19825,6 +21630,11 @@
19825
21630
  "label": "Light selected",
19826
21631
  "code": "<ThemeSwitcher value=\"light\" onValueChange={setTheme} />",
19827
21632
  "imports": "import { ThemeSwitcher } from \"@sableui/react\""
21633
+ },
21634
+ {
21635
+ "label": "Subtle motion",
21636
+ "code": "<ThemeSwitcher value={theme} onValueChange={setTheme} motion=\"subtle\" />",
21637
+ "imports": "import { ThemeSwitcher } from \"@sableui/react\""
19828
21638
  }
19829
21639
  ]
19830
21640
  },
@@ -19863,11 +21673,18 @@
19863
21673
  },
19864
21674
  {
19865
21675
  "guidance": "dont",
19866
- "description": "Do not put one on every row of a list or in a repeated skeleton. Each orb runs its own animation frame loop painting hundreds of arcs; use Skeleton for content-shaped waits and Spinner inside buttons."
21676
+ "description": "Do not put one on every row of a list or in a repeated skeleton. Each orb runs its own animation frame loop painting hundreds of arcs; use Skeleton for content-shaped waits and Spinner inside buttons.",
21677
+ "instead": [
21678
+ "Skeleton",
21679
+ "Spinner"
21680
+ ]
19867
21681
  },
19868
21682
  {
19869
21683
  "guidance": "dont",
19870
- "description": "Do not use it for a determinate wait. It carries no sense of how far along anything is — reach for Progress when a percentage exists."
21684
+ "description": "Do not use it for a determinate wait. It carries no sense of how far along anything is — reach for Progress when a percentage exists.",
21685
+ "instead": [
21686
+ "Progress"
21687
+ ]
19871
21688
  },
19872
21689
  {
19873
21690
  "guidance": "dont",
@@ -20023,7 +21840,11 @@
20023
21840
  },
20024
21841
  {
20025
21842
  "guidance": "dont",
20026
- "description": "Use Toast for critical blockers or persistent outages. Use Banner or Dialog when the user must notice and act."
21843
+ "description": "Use Toast for critical blockers or persistent outages. Use Banner or Dialog when the user must notice and act.",
21844
+ "instead": [
21845
+ "Banner",
21846
+ "Dialog"
21847
+ ]
20027
21848
  },
20028
21849
  {
20029
21850
  "guidance": "dont",
@@ -20295,7 +22116,10 @@
20295
22116
  },
20296
22117
  {
20297
22118
  "guidance": "dont",
20298
- "description": "Do not use Toggle for persistent application settings; use Switch."
22119
+ "description": "Do not use Toggle for persistent application settings; use Switch.",
22120
+ "instead": [
22121
+ "Switch"
22122
+ ]
20299
22123
  },
20300
22124
  {
20301
22125
  "guidance": "dont",
@@ -20332,7 +22156,7 @@
20332
22156
  "name": "appearance",
20333
22157
  "type": "\"default\" | \"segmented\"",
20334
22158
  "default": "\"default\"",
20335
- "description": "On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Toggles inside inherit it."
22159
+ "description": "On ToggleGroup: `default` is a loose row of toggles; `segmented` draws the capsule segmented control (one track, the pressed option filled) shared with ThemeSwitcher. Single-select segmented draws ONE fill that glides to the pressed option and resizes to its label; multi-select keeps a fill per pressed option. Toggles inside inherit it."
20336
22160
  },
20337
22161
  {
20338
22162
  "name": "multiple",
@@ -20340,6 +22164,12 @@
20340
22164
  "default": "false",
20341
22165
  "description": "On ToggleGroup: allow several toggles pressed at once. The default is single-select, which behaves like a radio group."
20342
22166
  },
22167
+ {
22168
+ "name": "motion",
22169
+ "type": "\"full\" | \"subtle\" | \"off\"",
22170
+ "default": "\"full\"",
22171
+ "description": "On ToggleGroup: how much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the glide, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting."
22172
+ },
20343
22173
  {
20344
22174
  "name": "Toggle",
20345
22175
  "type": "Base UI Toggle props (pressed, defaultPressed, onPressedChange, disabled, value) + className",
@@ -20350,7 +22180,7 @@
20350
22180
  "subcomponents": [
20351
22181
  {
20352
22182
  "name": "ToggleGroup",
20353
- "type": "Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: \"default\" | \"segmented\" + className",
22183
+ "type": "Base UI ToggleGroup props (value, defaultValue, onValueChange, multiple, disabled, orientation, loopFocus) + appearance?: \"default\" | \"segmented\" + motion?: \"full\" | \"subtle\" | \"off\" + className",
20354
22184
  "description": "Groups child Toggles and shares pressed state across them — each Toggle inside carries a value. Defaults to multiple: false (single-select) — the fit for view-mode switches and segmented controls; pass multiple to allow several pressed at once, e.g. a row of formatting toggles."
20355
22185
  },
20356
22186
  {
@@ -20374,6 +22204,38 @@
20374
22204
  "border",
20375
22205
  "ring"
20376
22206
  ],
22207
+ "motion": [
22208
+ {
22209
+ "slot": "arrival",
22210
+ "recipe": "none",
22211
+ "part": "Toggle",
22212
+ "why": "Toggles arrive with the toolbar or header they sit in. Animating each one in would make a row of controls ripple for no reason."
22213
+ },
22214
+ {
22215
+ "slot": "response",
22216
+ "recipe": "motion-interactive",
22217
+ "part": "Toggle",
22218
+ "why": "Hover and the pressed fill ease rather than flip, so pressing reads as the same button changing state, not a different button appearing."
22219
+ },
22220
+ {
22221
+ "slot": "response",
22222
+ "recipe": "motion-press",
22223
+ "part": "Toggle",
22224
+ "why": "The small settle under the finger confirms the press landed before the state change is read — the difference matters most on touch, where there is no hover."
22225
+ },
22226
+ {
22227
+ "slot": "transition",
22228
+ "recipe": "motion-glide",
22229
+ "part": "Segmented fill",
22230
+ "why": "In a single-select segmented group one fill travels to the chosen option and stretches to its label, so a view switch reads as one control now pointing elsewhere. Measured, because labels differ in width. Expressive: motion=\"subtle\" lands it in place."
22231
+ },
22232
+ {
22233
+ "slot": "activity",
22234
+ "recipe": "none",
22235
+ "part": "Toggle",
22236
+ "why": "A toggle commits instantly. Work that follows from the new state shows where its result appears, not on the control."
22237
+ }
22238
+ ],
20377
22239
  "a11y": [
20378
22240
  "Base UI sets aria-pressed on each Toggle and manages the pressed/disabled states.",
20379
22241
  "ToggleGroup provides arrow-key roving focus across its items; loopFocus wraps at the ends.",
@@ -20396,6 +22258,11 @@
20396
22258
  "code": "<ToggleGroup appearance=\"segmented\" defaultValue={[\"list\"]} aria-label=\"View\"><Toggle value=\"list\" aria-label=\"List view\"><Icon name=\"list\" /></Toggle><Toggle value=\"grid\" aria-label=\"Grid view\"><Icon name=\"grid\" /></Toggle></ToggleGroup>",
20397
22259
  "imports": "import { Icon, Toggle, ToggleGroup } from \"@sableui/react\""
20398
22260
  },
22261
+ {
22262
+ "label": "Segmented with labels, subtle motion",
22263
+ "code": "<ToggleGroup appearance=\"segmented\" motion=\"subtle\" defaultValue={[\"week\"]} aria-label=\"Range\"><Toggle value=\"day\">Day</Toggle><Toggle value=\"week\">Week</Toggle><Toggle value=\"month\">Month</Toggle></ToggleGroup>",
22264
+ "imports": "import { Toggle, ToggleGroup } from \"@sableui/react\""
22265
+ },
20399
22266
  {
20400
22267
  "label": "Multi-select group",
20401
22268
  "code": "<ToggleGroup multiple defaultValue={[\"bold\"]}><Toggle value=\"bold\">Bold</Toggle><Toggle value=\"italic\">Italic</Toggle></ToggleGroup>",
@@ -20450,7 +22317,12 @@
20450
22317
  },
20451
22318
  {
20452
22319
  "guidance": "dont",
20453
- "description": "Use Toolbar for navigation between pages or views. Links that go somewhere are Tabs, SideNav or Breadcrumbs; ToolbarLink is for the odd link that sits among actions, like Help."
22320
+ "description": "Use Toolbar for navigation between pages or views. Links that go somewhere are Tabs, SideNav or Breadcrumbs; ToolbarLink is for the odd link that sits among actions, like Help.",
22321
+ "instead": [
22322
+ "Tabs",
22323
+ "SideNav",
22324
+ "Breadcrumbs"
22325
+ ]
20454
22326
  },
20455
22327
  {
20456
22328
  "guidance": "dont",
@@ -20583,73 +22455,302 @@
20583
22455
  },
20584
22456
  "tokensUsed": [
20585
22457
  "foreground",
20586
- "accent",
20587
- "accent-foreground",
20588
- "ring",
20589
- "hairline-strong",
20590
- "input",
20591
- "card"
22458
+ "accent",
22459
+ "accent-foreground",
22460
+ "ring",
22461
+ "hairline-strong",
22462
+ "input",
22463
+ "card"
22464
+ ],
22465
+ "motion": [
22466
+ {
22467
+ "slot": "arrival",
22468
+ "recipe": "none",
22469
+ "part": "Toolbar",
22470
+ "why": "A toolbar is fixed chrome. It is present when its surface is; animating it in would leave the tools briefly unusable for no reason the user can see."
22471
+ },
22472
+ {
22473
+ "slot": "response",
22474
+ "recipe": "motion-interactive",
22475
+ "part": "ToolbarButton",
22476
+ "why": "Inherited from buttonVariants: hover fill, focus ring and disabled dim ease together, so the item under the arrow keys reads as the one that moved."
22477
+ },
22478
+ {
22479
+ "slot": "response",
22480
+ "recipe": "motion-press",
22481
+ "part": "ToolbarButton",
22482
+ "why": "Inherited from buttonVariants. Many toolbar actions change something off to the side (the canvas, the selection), so the settle on press is the confirmation that lands where the pointer is."
22483
+ },
22484
+ {
22485
+ "slot": "transition",
22486
+ "recipe": "none",
22487
+ "part": "Toolbar",
22488
+ "why": "The toolbar has no state of its own. Pressed state belongs to a Toggle rendered through ToolbarButton, which carries its own transition."
22489
+ },
22490
+ {
22491
+ "slot": "activity",
22492
+ "recipe": "none",
22493
+ "part": "Toolbar",
22494
+ "why": "A toolbar never works on its own. Pending work belongs to the surface the action targets, or to a Spinner in the button that started it."
22495
+ }
22496
+ ],
22497
+ "a11y": [
22498
+ "Base UI renders role=\"toolbar\" with aria-orientation, and a roving tabindex: the toolbar is one tab stop, the arrow keys move between items, Home/End jump to the ends.",
22499
+ "Disabled items stay focusable by default and announce as disabled (aria-disabled), so arrowing along the row never silently skips a control. Set focusableWhenDisabled={false} on an item to opt out.",
22500
+ "ToolbarSeparator is role=\"separator\" with the orientation opposite the toolbar's, and is not a focus stop.",
22501
+ "ToolbarInput lets the arrow keys move the caret and only leaves the field at either end of its text.",
22502
+ "Always set aria-label on the Toolbar, and on every icon-only ToolbarButton.",
22503
+ "A Toggle rendered through ToolbarButton keeps aria-pressed; a MenuTrigger keeps aria-haspopup/aria-expanded."
22504
+ ],
22505
+ "examples": [
22506
+ {
22507
+ "label": "Formatting toolbar",
22508
+ "code": "<Toolbar aria-label=\"Text formatting\">\n <ToolbarGroup aria-label=\"History\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Undo\">\n <Icon name=\"arrow-left\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Redo\">\n <Icon name=\"arrow-right\" />\n </ToolbarButton>\n </ToolbarGroup>\n <ToolbarSeparator />\n <ToggleGroup multiple aria-label=\"Style\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Bold\" render={<Toggle value=\"bold\" />}>\n <Icon name=\"bold\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Italic\" render={<Toggle value=\"italic\" />}>\n <Icon name=\"italic\" />\n </ToolbarButton>\n </ToggleGroup>\n <ToolbarSeparator />\n <ToolbarLink href=\"/help\">Help</ToolbarLink>\n</Toolbar>",
22509
+ "imports": "import { Icon, Toggle, ToggleGroup, Toolbar, ToolbarButton, ToolbarGroup, ToolbarLink, ToolbarSeparator } from \"@sableui/react\""
22510
+ },
22511
+ {
22512
+ "label": "With a menu trigger and a search field",
22513
+ "code": "<Toolbar aria-label=\"Table\">\n <ToolbarButton>Export</ToolbarButton>\n <Menu>\n <ToolbarButton render={<MenuTrigger />}>View</ToolbarButton>\n <MenuContent>\n <MenuItem>Compact rows</MenuItem>\n <MenuItem>Comfortable rows</MenuItem>\n </MenuContent>\n </Menu>\n <ToolbarSeparator />\n <ToolbarInput aria-label=\"Search rows\" placeholder=\"Search\" />\n</Toolbar>",
22514
+ "imports": "import { Menu, MenuContent, MenuItem, MenuTrigger, Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from \"@sableui/react\""
22515
+ },
22516
+ {
22517
+ "label": "Vertical tool rail",
22518
+ "code": "<Toolbar orientation=\"vertical\" aria-label=\"Tools\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Select\">\n <Icon name=\"arrow-up\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Draw\">\n <Icon name=\"edit\" />\n </ToolbarButton>\n <ToolbarSeparator />\n <ToolbarButton size=\"icon-sm\" aria-label=\"Delete\" disabled>\n <Icon name=\"trash\" />\n </ToolbarButton>\n</Toolbar>",
22519
+ "imports": "import { Icon, Toolbar, ToolbarButton, ToolbarSeparator } from \"@sableui/react\""
22520
+ },
22521
+ {
22522
+ "label": "In a workspace bar",
22523
+ "code": "<WorkspaceBar>\n <Toolbar aria-label=\"Document\" className=\"flex-1\">\n <ToolbarButton size=\"xs\">File</ToolbarButton>\n <ToolbarButton size=\"xs\">Edit</ToolbarButton>\n <ToolbarButton size=\"icon-xs\" aria-label=\"Share\" className=\"ms-auto\">\n <Icon name=\"share\" />\n </ToolbarButton>\n </Toolbar>\n</WorkspaceBar>",
22524
+ "imports": "import { Icon, Toolbar, ToolbarButton, WorkspaceBar } from \"@sableui/react\""
22525
+ }
22526
+ ]
22527
+ },
22528
+ {
22529
+ "name": "ToolCall",
22530
+ "status": "stable",
22531
+ "group": "Chat & AI",
22532
+ "register": "either",
22533
+ "path": "components/tool-call",
22534
+ "summary": "One tool an agent called, as a quiet row inside its turn: a status glyph, what it did, the tool's name and where it is — with the arguments and result behind a disclosure.",
22535
+ "keywords": [
22536
+ "tool call",
22537
+ "function call",
22538
+ "tool use",
22539
+ "agent step",
22540
+ "mcp",
22541
+ "ai",
22542
+ "tool result",
22543
+ "agent activity"
22544
+ ],
22545
+ "usage": {
22546
+ "description": "Use it for each tool call in an assistant's turn, so the person can see what the agent did and check any step. The row is the summary — glyph, a plain-words title, the tool's name, the status — and stays one line, so a turn with ten calls is still scannable. Input and output sit behind the disclosure, because they are read rarely and, when they are, in full.",
22547
+ "bestPractices": [
22548
+ {
22549
+ "guidance": "do",
22550
+ "description": "Give it a `title` in plain words, in the tense of the status: \"Searching the docs\" while running, \"Searched the docs\" once done. The `name` stays beside it in mono for the person who wants to know exactly which tool ran."
22551
+ },
22552
+ {
22553
+ "guidance": "do",
22554
+ "description": "Drive `status` from the call's real state and keep the same element mounted as it changes. The spinner swaps for the outcome in place, and the change is announced to screen readers once."
22555
+ },
22556
+ {
22557
+ "guidance": "do",
22558
+ "description": "Pass `error` with `status=\"error\"`. It shows under the row without opening anything, because a failure is the one result everyone needs to read."
22559
+ },
22560
+ {
22561
+ "guidance": "do",
22562
+ "description": "Pass a rendered result as `children` when the output is something to look at — a list of hits, a diff — rather than data to inspect. It replaces the output's code view, and `input` still shows above it."
22563
+ },
22564
+ {
22565
+ "guidance": "dont",
22566
+ "description": "Do not put the approve and deny buttons in a ToolCall. A call held for permission is `status=\"awaiting\"` here, and the decision itself is ApprovalBlock, which says what will happen and asks once."
22567
+ },
22568
+ {
22569
+ "guidance": "dont",
22570
+ "description": "Do not use it for the agent's thinking or a progress note. A wait with no tool behind it is ThinkingOrb or a Spinner with a status line, and a row that claims a tool ran when none did makes every other row less trustworthy.",
22571
+ "instead": [
22572
+ "ThinkingOrb",
22573
+ "Spinner"
22574
+ ]
22575
+ },
22576
+ {
22577
+ "guidance": "dont",
22578
+ "description": "Do not open every call by default. A turn whose calls all show their JSON buries the answer; leave `defaultOpen` for the one call the person is likely to check, if any."
22579
+ }
22580
+ ],
22581
+ "anatomy": [
22582
+ {
22583
+ "name": "Status glyph",
22584
+ "required": true,
22585
+ "description": "Spinner while running, then the outcome: check, warning, clock (awaiting approval) or ×. The only part that moves."
22586
+ },
22587
+ {
22588
+ "name": "Title",
22589
+ "required": false,
22590
+ "description": "What the call does, in plain words. Without one, the tool's name leads."
22591
+ },
22592
+ {
22593
+ "name": "Name",
22594
+ "required": true,
22595
+ "description": "The tool's own name in mono, after the title. Hidden below the sm breakpoint when a title is present."
22596
+ },
22597
+ {
22598
+ "name": "Status",
22599
+ "required": true,
22600
+ "description": "One word for where the call is: Running, Needs approval, Done, Failed, Denied."
22601
+ },
22602
+ {
22603
+ "name": "Error line",
22604
+ "required": false,
22605
+ "description": "The failure message, under the row and always visible."
22606
+ },
22607
+ {
22608
+ "name": "Details",
22609
+ "required": false,
22610
+ "description": "Input and output as code, or a rendered result. A disclosure; the row is its trigger."
22611
+ }
22612
+ ]
22613
+ },
22614
+ "related": {
22615
+ "components": [
22616
+ "Message",
22617
+ "ThinkingOrb",
22618
+ "StreamingText",
22619
+ "CodeBlock",
22620
+ "Collapsible",
22621
+ "InlineStatus"
22622
+ ]
22623
+ },
22624
+ "props": [
22625
+ {
22626
+ "name": "name",
22627
+ "type": "string",
22628
+ "description": "The tool's own name, as the model called it (`search_docs`). Shown in mono."
22629
+ },
22630
+ {
22631
+ "name": "title",
22632
+ "type": "ReactNode",
22633
+ "description": "What the call does, in words a person reads. Leads the row; omit it and `name` leads."
22634
+ },
22635
+ {
22636
+ "name": "status",
22637
+ "type": "\"running\" | \"awaiting\" | \"complete\" | \"error\" | \"denied\"",
22638
+ "default": "\"running\"",
22639
+ "description": "Where the call is. `awaiting` is held for the person's approval; `denied` is one they declined."
22640
+ },
22641
+ {
22642
+ "name": "statusLabel",
22643
+ "type": "string",
22644
+ "description": "Overrides the status word, for another language or a more specific phrase."
22645
+ },
22646
+ {
22647
+ "name": "input",
22648
+ "type": "unknown",
22649
+ "description": "The arguments. A string shows as is; anything else as formatted JSON."
22650
+ },
22651
+ {
22652
+ "name": "output",
22653
+ "type": "unknown",
22654
+ "description": "The result, shown the same way as `input`."
22655
+ },
22656
+ {
22657
+ "name": "error",
22658
+ "type": "ReactNode",
22659
+ "description": "What went wrong, for `status=\"error\"`. Always visible under the row."
22660
+ },
22661
+ {
22662
+ "name": "children",
22663
+ "type": "ReactNode",
22664
+ "description": "A rendered result in place of `output`'s code view."
22665
+ },
22666
+ {
22667
+ "name": "defaultOpen",
22668
+ "type": "boolean",
22669
+ "default": "false",
22670
+ "description": "Open the details on mount."
22671
+ },
22672
+ {
22673
+ "name": "open",
22674
+ "type": "boolean",
22675
+ "description": "Controlled open state of the details."
22676
+ },
22677
+ {
22678
+ "name": "onOpenChange",
22679
+ "type": "(open: boolean) => void",
22680
+ "description": "Called when the details open or close."
22681
+ }
22682
+ ],
22683
+ "variants": {
22684
+ "status": [
22685
+ "running",
22686
+ "awaiting",
22687
+ "complete",
22688
+ "error",
22689
+ "denied"
22690
+ ]
22691
+ },
22692
+ "tokensUsed": [
22693
+ "card",
22694
+ "border",
22695
+ "destructive",
22696
+ "muted",
22697
+ "muted-foreground",
22698
+ "foreground",
22699
+ "tone-warning",
22700
+ "ring"
20592
22701
  ],
20593
22702
  "motion": [
20594
22703
  {
20595
22704
  "slot": "arrival",
20596
22705
  "recipe": "none",
20597
- "part": "Toolbar",
20598
- "why": "A toolbar is fixed chrome. It is present when its surface is; animating it in would leave the tools briefly unusable for no reason the user can see."
22706
+ "part": "Root",
22707
+ "why": "It arrives as part of the assistant's turn, and the turn's own arrival covers it. A second entrance per call would make a busy turn twitch."
20599
22708
  },
20600
22709
  {
20601
22710
  "slot": "response",
20602
22711
  "recipe": "motion-interactive",
20603
- "part": "ToolbarButton",
20604
- "why": "Inherited from buttonVariants: hover fill, focus ring and disabled dim ease together, so the item under the arrow keys reads as the one that moved."
22712
+ "part": "Header",
22713
+ "why": "The header is the disclosure trigger but reads as a status row. The hover step is what tells the person it opens."
20605
22714
  },
20606
22715
  {
20607
- "slot": "response",
20608
- "recipe": "motion-press",
20609
- "part": "ToolbarButton",
20610
- "why": "Inherited from buttonVariants. Many toolbar actions change something off to the side (the canvas, the selection), so the settle on press is the confirmation that lands where the pointer is."
22716
+ "slot": "transition",
22717
+ "recipe": "motion-icon-swap",
22718
+ "part": "Status glyph",
22719
+ "why": "The spinner becoming a check or a warning is the moment the call settles, and it is the one change in the row worth catching from the corner of the eye. The swap reads as one mark replaced, not a flicker."
20611
22720
  },
20612
22721
  {
20613
22722
  "slot": "transition",
20614
- "recipe": "none",
20615
- "part": "Toolbar",
20616
- "why": "The toolbar has no state of its own. Pressed state belongs to a Toggle rendered through ToolbarButton, which carries its own transition."
22723
+ "recipe": "motion-disclosure",
22724
+ "part": "Details",
22725
+ "why": "The height change connects the code that appears to the row that was pressed, in a thread where several rows look alike."
20617
22726
  },
20618
22727
  {
20619
22728
  "slot": "activity",
20620
- "recipe": "none",
20621
- "part": "Toolbar",
20622
- "why": "A toolbar never works on its own. Pending work belongs to the surface the action targets, or to a Spinner in the button that started it."
22729
+ "recipe": "motion-spin",
22730
+ "part": "Status glyph",
22731
+ "why": "While the call runs, the spinner is what separates working from stuck. It pauses once the call settles, so a long thread of finished calls costs nothing."
20623
22732
  }
20624
22733
  ],
20625
22734
  "a11y": [
20626
- "Base UI renders role=\"toolbar\" with aria-orientation, and a roving tabindex: the toolbar is one tab stop, the arrow keys move between items, Home/End jump to the ends.",
20627
- "Disabled items stay focusable by default and announce as disabled (aria-disabled), so arrowing along the row never silently skips a control. Set focusableWhenDisabled={false} on an item to opt out.",
20628
- "ToolbarSeparator is role=\"separator\" with the orientation opposite the toolbar's, and is not a focus stop.",
20629
- "ToolbarInput lets the arrow keys move the caret and only leaves the field at either end of its text.",
20630
- "Always set aria-label on the Toolbar, and on every icon-only ToolbarButton.",
20631
- "A Toggle rendered through ToolbarButton keeps aria-pressed; a MenuTrigger keeps aria-haspopup/aria-expanded."
22735
+ "With details, the header is a button with aria-expanded (Base UI Collapsible); its name is the title, the tool name and the status word, so it reads as one sentence.",
22736
+ "A status change after mount is announced once through a polite role=status region (\"Searched the docs: Done\"). The status a call mounts with is not announced, so a thread restored from history stays quiet.",
22737
+ "The glyph is aria-hidden: the status word beside it carries the meaning, so colour and shape are never the only signal."
20632
22738
  ],
20633
22739
  "examples": [
20634
22740
  {
20635
- "label": "Formatting toolbar",
20636
- "code": "<Toolbar aria-label=\"Text formatting\">\n <ToolbarGroup aria-label=\"History\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Undo\">\n <Icon name=\"arrow-left\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Redo\">\n <Icon name=\"arrow-right\" />\n </ToolbarButton>\n </ToolbarGroup>\n <ToolbarSeparator />\n <ToggleGroup multiple aria-label=\"Style\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Bold\" render={<Toggle value=\"bold\" />}>\n <Icon name=\"bold\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Italic\" render={<Toggle value=\"italic\" />}>\n <Icon name=\"italic\" />\n </ToolbarButton>\n </ToggleGroup>\n <ToolbarSeparator />\n <ToolbarLink href=\"/help\">Help</ToolbarLink>\n</Toolbar>",
20637
- "imports": "import { Icon, Toggle, ToggleGroup, Toolbar, ToolbarButton, ToolbarGroup, ToolbarLink, ToolbarSeparator } from \"@sableui/react\""
20638
- },
20639
- {
20640
- "label": "With a menu trigger and a search field",
20641
- "code": "<Toolbar aria-label=\"Table\">\n <ToolbarButton>Export</ToolbarButton>\n <Menu>\n <ToolbarButton render={<MenuTrigger />}>View</ToolbarButton>\n <MenuContent>\n <MenuItem>Compact rows</MenuItem>\n <MenuItem>Comfortable rows</MenuItem>\n </MenuContent>\n </Menu>\n <ToolbarSeparator />\n <ToolbarInput aria-label=\"Search rows\" placeholder=\"Search\" />\n</Toolbar>",
20642
- "imports": "import { Menu, MenuContent, MenuItem, MenuTrigger, Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from \"@sableui/react\""
22741
+ "label": "A finished call",
22742
+ "code": "<ToolCall\n\tname=\"search_docs\"\n\ttitle=\"Searched the docs\"\n\tstatus=\"complete\"\n\tinput={{ query: \"date range picker\" }}\n\toutput={{ hits: [\"DatePicker\", \"Calendar\", \"DateTile\"] }}\n/>",
22743
+ "imports": "import { ToolCall } from \"@sableui/react\""
20643
22744
  },
20644
22745
  {
20645
- "label": "Vertical tool rail",
20646
- "code": "<Toolbar orientation=\"vertical\" aria-label=\"Tools\">\n <ToolbarButton size=\"icon-sm\" aria-label=\"Select\">\n <Icon name=\"arrow-up\" />\n </ToolbarButton>\n <ToolbarButton size=\"icon-sm\" aria-label=\"Draw\">\n <Icon name=\"edit\" />\n </ToolbarButton>\n <ToolbarSeparator />\n <ToolbarButton size=\"icon-sm\" aria-label=\"Delete\" disabled>\n <Icon name=\"trash\" />\n </ToolbarButton>\n</Toolbar>",
20647
- "imports": "import { Icon, Toolbar, ToolbarButton, ToolbarSeparator } from \"@sableui/react\""
22746
+ "label": "Running, then failed",
22747
+ "code": "<div className=\"flex flex-col gap-2\">\n\t<ToolCall name=\"read_file\" title=\"Reading package.json\" status=\"running\" input={{ path: \"package.json\" }} />\n\t<ToolCall\n\t\tname=\"run_tests\"\n\t\ttitle=\"Ran the tests\"\n\t\tstatus=\"error\"\n\t\terror=\"3 of 412 tests failed in date-picker.test.tsx.\"\n\t\tinput={{ filter: \"date-picker\" }}\n\t/>\n</div>",
22748
+ "imports": "import { ToolCall } from \"@sableui/react\""
20648
22749
  },
20649
22750
  {
20650
- "label": "In a workspace bar",
20651
- "code": "<WorkspaceBar>\n <Toolbar aria-label=\"Document\" className=\"flex-1\">\n <ToolbarButton size=\"xs\">File</ToolbarButton>\n <ToolbarButton size=\"xs\">Edit</ToolbarButton>\n <ToolbarButton size=\"icon-xs\" aria-label=\"Share\" className=\"ms-auto\">\n <Icon name=\"share\" />\n </ToolbarButton>\n </Toolbar>\n</WorkspaceBar>",
20652
- "imports": "import { Icon, Toolbar, ToolbarButton, WorkspaceBar } from \"@sableui/react\""
22751
+ "label": "Waiting for approval",
22752
+ "code": "<ToolCall name=\"send_email\" title=\"Send the invoice to Ada\" status=\"awaiting\" input={{ to: \"ada@example.com\", subject: \"INV-1042\" }} />",
22753
+ "imports": "import { ToolCall } from \"@sableui/react\""
20653
22754
  }
20654
22755
  ]
20655
22756
  },
@@ -20679,7 +22780,11 @@
20679
22780
  },
20680
22781
  {
20681
22782
  "guidance": "dont",
20682
- "description": "Do not put critical instructions, errors, or interactive content inside Tooltip."
22783
+ "description": "Do not put critical instructions, errors, or interactive content inside Tooltip.",
22784
+ "instead": [
22785
+ "Popover",
22786
+ "InlineStatus"
22787
+ ]
20683
22788
  },
20684
22789
  {
20685
22790
  "guidance": "dont",
@@ -20757,7 +22862,12 @@
20757
22862
  {
20758
22863
  "name": "SimpleTooltip",
20759
22864
  "type": "Base UI Tooltip.Root props + content: ReactNode + children: ReactElement + side | align | sideOffset",
20760
- "description": "Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use."
22865
+ "description": "Shorthand wrapping Root + Trigger + Content for the common case. The single child becomes the trigger (via Base UI render); content sets the body; side / align / sideOffset forward to the content with the same defaults. Composition parts remain for advanced use. Inside a TooltipGroup it becomes a trigger of the group's shared tooltip instead."
22866
+ },
22867
+ {
22868
+ "name": "TooltipGroup",
22869
+ "type": "{ children: ReactNode; motion?: \"full\" | \"subtle\" | \"off\" }",
22870
+ "description": "One tooltip shared by every SimpleTooltip inside it — for a toolbar or a row of icon buttons. Moving the pointer along the row glides the tooltip to the next trigger and swaps its label (old fades out, new fades in, each sliding toward the direction of travel), instead of closing one tooltip and opening another a few pixels away. Renders no element. Each SimpleTooltip keeps its own content / side / align / sideOffset and `disabled`; root props such as open / onOpenChange belong to the shared tooltip and are ignored inside a group. `motion` turns it down: `subtle` jumps between triggers and only cross-fades the label. The composable Tooltip parts are unaffected."
20761
22871
  }
20762
22872
  ],
20763
22873
  "tokensUsed": [
@@ -20789,6 +22899,12 @@
20789
22899
  "recipe": "none",
20790
22900
  "part": "Content",
20791
22901
  "why": "Tooltip copy is resident before the tooltip opens. Anything that has to be fetched is a Popover with its own loading state."
22902
+ },
22903
+ {
22904
+ "slot": "transition",
22905
+ "recipe": "custom",
22906
+ "part": "TooltipGroup",
22907
+ "why": "Moving between triggers in a group, the one tooltip glides to the next trigger, resizes to its label and cross-fades the text with a short slide toward the direction of travel. Separate tooltips would close here and open there, two surfaces blinking side by side; the glide says it is the same hint following the pointer. Base UI's handle + Viewport supply the shared popup and the previous/current content; the timing is the expressive glide (slow, emphasized), so `subtle` keeps only the cross-fade."
20792
22908
  }
20793
22909
  ],
20794
22910
  "a11y": [
@@ -20812,6 +22928,11 @@
20812
22928
  "label": "Shorthand (SimpleTooltip)",
20813
22929
  "code": "<SimpleTooltip content=\"Saved to your workspace.\"><Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Info\"><Icon name=\"info\" /></Button></SimpleTooltip>",
20814
22930
  "imports": "import { Button, Icon, SimpleTooltip } from \"@sableui/react\""
22931
+ },
22932
+ {
22933
+ "label": "One tooltip gliding along a toolbar (TooltipGroup)",
22934
+ "code": "<TooltipGroup><Toolbar aria-label=\"Formatting\"><SimpleTooltip content=\"Bold\"><ToolbarButton size=\"icon-sm\" aria-label=\"Bold\"><Icon name=\"bold\" /></ToolbarButton></SimpleTooltip><SimpleTooltip content=\"Italic\"><ToolbarButton size=\"icon-sm\" aria-label=\"Italic\"><Icon name=\"italic\" /></ToolbarButton></SimpleTooltip><SimpleTooltip content=\"Insert link\"><ToolbarButton size=\"icon-sm\" aria-label=\"Insert link\"><Icon name=\"link\" /></ToolbarButton></SimpleTooltip></Toolbar></TooltipGroup>",
22935
+ "imports": "import { Icon, SimpleTooltip, Toolbar, ToolbarButton, TooltipGroup } from \"@sableui/react\""
20815
22936
  }
20816
22937
  ]
20817
22938
  },
@@ -20992,6 +23113,12 @@
20992
23113
  "name": "label",
20993
23114
  "type": "ReactNode",
20994
23115
  "description": "TreeAddRow's visible label. Name what gets added — 'Add effect', not 'Add'."
23116
+ },
23117
+ {
23118
+ "name": "motion",
23119
+ "type": "\"full\" | \"subtle\" | \"off\"",
23120
+ "default": "\"full\"",
23121
+ "description": "On Tree. How much motion this component uses: `full` (default) inherits the app's policy, `subtle` keeps feedback and drops the flourish, `off` stills it. It can only turn motion down — it never overrides a reader's reduce-motion setting. Under `subtle` expanded rows appear at once instead of unfolding; the chevron still turns."
20995
23122
  }
20996
23123
  ],
20997
23124
  "subcomponents": [
@@ -21029,6 +23156,32 @@
21029
23156
  "primary",
21030
23157
  "ring"
21031
23158
  ],
23159
+ "motion": [
23160
+ {
23161
+ "slot": "arrival",
23162
+ "recipe": "motion-arrive",
23163
+ "part": "TreeRow",
23164
+ "why": "Rows a user's expand reveals fade down into place in document order (a capped 18ms stagger), so the eye can follow where the children came from. An animation (motion-arrive) rather than a transition, because the rows already wear motion-interactive and a second transition list would fight it. Only on a user expand of a `subtree` — a tree that mounts open shows its rows at once, and rows added later to an open folder do not cascade. Expressive: `subtle` shows them at once."
23165
+ },
23166
+ {
23167
+ "slot": "response",
23168
+ "recipe": "motion-interactive",
23169
+ "part": "TreeRow",
23170
+ "why": "Hover and selection fills ease, so sweeping the pointer down a dense panel reads as a highlight following it rather than rows flickering."
23171
+ },
23172
+ {
23173
+ "slot": "transition",
23174
+ "recipe": "motion-interactive",
23175
+ "part": "Disclosure",
23176
+ "why": "The chevron turns with the rows arriving under it; motion-interactive lists `rotate`, which is what -rotate-90 writes, so it no longer flips in one frame."
23177
+ },
23178
+ {
23179
+ "slot": "activity",
23180
+ "recipe": "none",
23181
+ "part": "Tree",
23182
+ "why": "A tree has no indeterminate state of its own; a branch that loads its children should render a TreeEmpty or a loading row inside it."
23183
+ }
23184
+ ],
21032
23185
  "a11y": [
21033
23186
  "Tree implements the ARIA tree view pattern: Tree is role=\"tree\", each TreeRow and TreeAddRow is role=\"treeitem\" with aria-level, expandable rows carry aria-expanded, and a TreeSection is a role=\"group\" labelled by its heading. Rows stay flat in the DOM — hierarchy is aria-level — so the fills span the panel's full width.",
21034
23187
  "Keyboard: exactly one row is in the tab order (roving tabindex, seeded on the selected row, else the first). ArrowUp/Down move between visible rows; ArrowRight expands a collapsed row, then steps to its first child; ArrowLeft collapses an expanded row, else steps to its parent; Home/End jump to the ends; Enter/Space activate the row — its onClick, or toggling it when it has none. Keys pressed inside a row's own controls (an action button, a rename field) are left to that control.",
@@ -21143,6 +23296,38 @@
21143
23296
  "overlay-foreground",
21144
23297
  "ring"
21145
23298
  ],
23299
+ "motion": [
23300
+ {
23301
+ "slot": "arrival",
23302
+ "recipe": "none",
23303
+ "part": "Root",
23304
+ "why": "The frame arrives with its page. Once there is a source, the poster and controls are the browser's."
23305
+ },
23306
+ {
23307
+ "slot": "response",
23308
+ "recipe": "motion-interactive",
23309
+ "part": "Play button",
23310
+ "why": "With `onRequestPlay`, the play disc firms its scrim on hover, eased, so the mark reads as pressable over any image."
23311
+ },
23312
+ {
23313
+ "slot": "response",
23314
+ "recipe": "motion-press",
23315
+ "part": "Play button",
23316
+ "why": "Loading a source can take a moment; the settle on press confirms the tap before the player appears."
23317
+ },
23318
+ {
23319
+ "slot": "transition",
23320
+ "recipe": "none",
23321
+ "part": "Root",
23322
+ "why": "Placeholder to player is a swap of elements: the native video mounts where the poster was. Animating the swap would delay the playback the person just asked for."
23323
+ },
23324
+ {
23325
+ "slot": "activity",
23326
+ "recipe": "none",
23327
+ "part": "Root",
23328
+ "why": "Buffering belongs to the native controls, which draw their own indicator. A second loader over the frame would compete with the browser's."
23329
+ }
23330
+ ],
21146
23331
  "a11y": [
21147
23332
  "Mounts a native <video> element, preserving built-in keyboard controls when controls is enabled.",
21148
23333
  "Without onRequestPlay the placeholder play mark is decorative (aria-hidden) — nothing on the frame pretends to be pressable. With it, the mark is a native button named \"Play video\"."
@@ -21576,6 +23761,31 @@
21576
23761
  "imports": "import { Button, Card, CardAction, CardDescription, CardHeader, CardList, CardTitle, cn, Code, Entity, Icon, MenuItem, MenuSeparator, OverflowMenu } from \"@sableui/react\"",
21577
23762
  "code": "const keys = [\n\t{ id: \"k1\", name: \"Production\", hint: \"sk_live_••••4f2a\", detail: \"Created Mar 2 · used 4 minutes ago\" },\n\t{ id: \"k2\", name: \"Staging\", hint: \"sk_test_••••91cd\", detail: \"Created Feb 11 · used yesterday\" },\n\t{ id: \"k3\", name: \"CI deploys\", hint: \"sk_live_••••0b7e\", detail: \"Created Jan 5 · never used\" },\n]\n\nexport interface ApiKeysBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\tonCreate?: () => void\n\t/** Called with the key's id and the action chosen from its menu. */\n\tonKeyAction?: (id: string, action: \"rename\" | \"roll\" | \"revoke\") => void\n}\n\nexport function ApiKeysBlock({ className, framed = true, onCreate, onKeyAction }: ApiKeysBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle render={<h2 />}>API keys</CardTitle>\n\t\t\t\t<CardDescription>A key is shown once, when you create it. Store it somewhere safe.</CardDescription>\n\t\t\t\t<CardAction>\n\t\t\t\t\t<Button size=\"sm\" variant=\"outline\" iconStart={<Icon name=\"key\" />} onClick={onCreate}>\n\t\t\t\t\t\tCreate key\n\t\t\t\t\t</Button>\n\t\t\t\t</CardAction>\n\t\t\t</CardHeader>\n\t\t\t<CardList>\n\t\t\t\t{keys.map((k) => (\n\t\t\t\t\t<Entity\n\t\t\t\t\t\tkey={k.id}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\t<span className=\"flex flex-wrap items-center gap-x-2\">\n\t\t\t\t\t\t\t\t{k.name}\n\t\t\t\t\t\t\t\t<Code>{k.hint}</Code>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsubtitle={k.detail}\n\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t<OverflowMenu label={`Manage the ${k.name} key`} size=\"icon-sm\">\n\t\t\t\t\t\t\t\t<MenuItem onClick={() => onKeyAction?.(k.id, \"rename\")}>Rename</MenuItem>\n\t\t\t\t\t\t\t\t<MenuItem onClick={() => onKeyAction?.(k.id, \"roll\")}>Roll key</MenuItem>\n\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t<MenuItem variant=\"destructive\" onClick={() => onKeyAction?.(k.id, \"revoke\")}>\n\t\t\t\t\t\t\t\t\tRevoke\n\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</CardList>\n\t\t</Card>\n\t)\n}"
21578
23763
  },
23764
+ {
23765
+ "name": "ApprovalBlock",
23766
+ "path": "blocks/approval-block.tsx",
23767
+ "summary": "An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an \"allow for the rest of this chat\" Checkbox, and Deny / Allow.",
23768
+ "intent": "Reach for this in a chat or agent run when a tool call waits on the person — sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status=\"awaiting\", then \"complete\" or \"denied\". A destructive action outside an agent is DestructiveActionModal.",
23769
+ "family": "AI & agents",
23770
+ "kind": "card",
23771
+ "width": {
23772
+ "min": "18rem",
23773
+ "max": "28rem"
23774
+ },
23775
+ "notes": [
23776
+ "The title is the question in plain words (\"Send the invoice to Ada?\"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.",
23777
+ "The description says what can't be undone. That sentence is what the person is deciding on.",
23778
+ "Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
23779
+ "After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
23780
+ ],
23781
+ "a11y": [
23782
+ "Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.",
23783
+ "The details are a dl, so each value is read with its label.",
23784
+ "The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
23785
+ ],
23786
+ "imports": "import { useState, useEffect, useRef, useId } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, cn, Code, InlineStatus, Label } from \"@sableui/react\"",
23787
+ "code": "const request = {\n\ttool: \"send_email\",\n\ttitle: \"Send the invoice to Ada?\",\n\tdescription: \"The agent wants to email invoice INV-1042. An email can't be unsent.\",\n\tdetails: [\n\t\t{ label: \"To\", value: \"ada@example.com\" },\n\t\t{ label: \"Subject\", value: \"INV-1042 for October\" },\n\t\t{ label: \"Attached\", value: \"INV-1042.pdf\" },\n\t],\n\tallowed: \"Allowed. Sending the email.\",\n\tdenied: \"Denied. The agent won't send it.\",\n}\n\nexport interface ApprovalBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a chat column that already frames it. */\n\tframed?: boolean\n\t/** The person allowed the call. `remember` is the \"for the rest of this chat\" choice. */\n\tonApprove?: (options: { remember: boolean }) => void\n\t/** The person declined the call. */\n\tonDeny?: () => void\n}\n\nexport function ApprovalBlock({ className, framed = true, onApprove, onDeny }: ApprovalBlockProps) {\n\tconst [decision, setDecision] = useState<\"allowed\" | \"denied\" | null>(null)\n\tconst [remember, setRemember] = useState(false)\n\tconst id = useId()\n\tconst outcome = useRef<HTMLDivElement>(null)\n\t// The buttons leave with the decision, so focus moves to the line that\n\t// replaces them instead of falling back to the top of the page.\n\tuseEffect(() => {\n\t\tif (decision) outcome.current?.focus()\n\t}, [decision])\n\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-md\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{request.title}</CardTitle>\n\t\t\t\t<CardDescription>{request.description}</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm\">\n\t\t\t\t\t{request.details.map((d) => (\n\t\t\t\t\t\t<div key={d.label} className=\"contents\">\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">{d.label}</dt>\n\t\t\t\t\t\t\t<dd className=\"truncate text-foreground\">{d.value}</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t\t<div className=\"contents\">\n\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Tool</dt>\n\t\t\t\t\t\t<dd className=\"min-w-0\">\n\t\t\t\t\t\t\t<Code>{request.tool}</Code>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t{decision ? null : (\n\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<Checkbox id={`${id}-remember`} checked={remember} onCheckedChange={(v: boolean) => setRemember(v)} />\n\t\t\t\t\t\t<Label htmlFor={`${id}-remember`} className=\"text-body-sm\">\n\t\t\t\t\t\t\tAllow <Code>{request.tool}</Code> for the rest of this chat\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions className=\"justify-end\">\n\t\t\t\t{decision ? (\n\t\t\t\t\t<InlineStatus\n\t\t\t\t\t\tref={outcome}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\ttone={decision === \"allowed\" ? \"success\" : \"neutral\"}\n\t\t\t\t\t\tclassName=\"me-auto outline-none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{decision === \"allowed\" ? request.allowed : request.denied}\n\t\t\t\t\t</InlineStatus>\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"denied\")\n\t\t\t\t\t\t\t\tonDeny?.()\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDeny\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetDecision(\"allowed\")\n\t\t\t\t\t\t\t\tonApprove?.({ remember })\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tAllow\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
23788
+ },
21579
23789
  {
21580
23790
  "name": "ChatThreadBlock",
21581
23791
  "path": "blocks/chat-thread-block.tsx",
@@ -23059,8 +25269,8 @@
23059
25269
  {
23060
25270
  "slug": "motion",
23061
25271
  "title": "Motion",
23062
- "summary": "Motion explains a change: where something came from, what responded, and that work is happening. It is subtle by default, built from named recipes, and one switch turns it off.",
23063
- "headline": "4 slots · 3 speeds · enter-only overshoot · 1 kill switch",
25272
+ "summary": "Motion explains a change: where something came from, what responded, and that work is happening. Components ship crafted by default: one indicator travels instead of two blinking, numbers roll, ticks draw, a refused answer shakes. It is built from named recipes, comes in two levels, and one switch turns it off.",
25273
+ "headline": "4 slots · 3 speeds · 2 levels · enter-only overshoot · 1 kill switch",
23064
25274
  "retrieve": "sable docs motion",
23065
25275
  "vocabularies": [
23066
25276
  {
@@ -23121,7 +25331,37 @@
23121
25331
  {
23122
25332
  "name": "motion-icon-swap",
23123
25333
  "value": "enter · emphasized cross-fade",
23124
- "use": "Two icons of one control trading places: play/pause, outline/fill."
25334
+ "use": "Two icons of one control trading places: play/pause, outline/fill, copy/check."
25335
+ },
25336
+ {
25337
+ "name": "motion-glide",
25338
+ "value": "slow · emphasized · expressive",
25339
+ "use": "ONE indicator travelling to the selected option: tabs, segmented thumb, active dot."
25340
+ },
25341
+ {
25342
+ "name": "motion-shake",
25343
+ "value": "~400ms decaying · expressive",
25344
+ "use": "A refused answer: wrong code, invalid submit. Plays while data-shake is set (useShake). Never the only signal."
25345
+ },
25346
+ {
25347
+ "name": "motion-tick",
25348
+ "value": "fast fade + base wipe · expressive",
25349
+ "use": "A check mark that draws itself in. Exits only fade."
25350
+ },
25351
+ {
25352
+ "name": "motion-appear",
25353
+ "value": "fast fade + base scale",
25354
+ "use": "A small control mounting in place: a clear button, a send arrow, a badge. @starting-style, no JS."
25355
+ },
25356
+ {
25357
+ "name": "motion-arrive",
25358
+ "value": "base · decelerate · staggered · expressive",
25359
+ "use": "Items a user action brings into a list: a folder's children on expand, a row just added. Never on every render."
25360
+ },
25361
+ {
25362
+ "name": "motion-size",
25363
+ "value": "base · emphasized · expressive",
25364
+ "use": "An element easing between intrinsic sizes when its label changes: a two-step confirm, Copy → Copied."
23125
25365
  },
23126
25366
  {
23127
25367
  "name": "motion-draw",
@@ -23165,6 +25405,42 @@
23165
25405
  }
23166
25406
  ]
23167
25407
  },
25408
+ {
25409
+ "title": "Two levels",
25410
+ "note": "Essential motion answers the user; expressive motion narrates continuity. Expressive durations are also multiplied by --motion-expressive, which `subtle` sets to 0. Each level can only turn motion down from what surrounds it.",
25411
+ "terms": [
25412
+ {
25413
+ "name": "essential",
25414
+ "value": "always, unless off",
25415
+ "use": "Colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover."
25416
+ },
25417
+ {
25418
+ "name": "expressive",
25419
+ "value": "full only",
25420
+ "use": "Glides, rolling numbers (NumberTicker), drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot."
25421
+ },
25422
+ {
25423
+ "name": "full",
25424
+ "value": "default",
25425
+ "use": "Both levels play. The crafted default."
25426
+ },
25427
+ {
25428
+ "name": "subtle",
25429
+ "value": "data-motion=\"subtle\"",
25430
+ "use": "Essential only; nothing overshoots. For dense tools, embedded widgets, readers who want calm without stillness."
25431
+ },
25432
+ {
25433
+ "name": "off",
25434
+ "value": "data-motion=\"off\" · reduce-motion",
25435
+ "use": "Nothing moves; every state still changes."
25436
+ },
25437
+ {
25438
+ "name": "where",
25439
+ "value": "provider · scope · prop",
25440
+ "use": "<MotionProvider policy> for the app, <MotionScope level> for a region, `motion` on a component that owns a flourish."
25441
+ }
25442
+ ]
25443
+ },
23168
25444
  {
23169
25445
  "title": "The four slots",
23170
25446
  "note": "Every interactive component answers all four, by filling the slot or declining it with a reason in meta.motion.",
@@ -23223,6 +25499,22 @@
23223
25499
  {
23224
25500
  "rule": "JS motion reads useMotion().enabled and does nothing when it is false.",
23225
25501
  "why": "Reduced motion is an accessibility setting, not a style preference."
25502
+ },
25503
+ {
25504
+ "rule": "One thing travels; two things do not blink. When a selection, a value or a mark changes in place, move the one element that represents it.",
25505
+ "why": "A fill switching off here and on there is two events the eye has to connect. A glide, a roll or a draw is one, and it is most of what makes an interface feel made rather than assembled."
25506
+ },
25507
+ {
25508
+ "rule": "Flourishes are expressive: multiply their durations by --motion-expressive and gate JS ones on useMotion().expressive.",
25509
+ "why": "`subtle` promises the answers without the choreography. A glide on a plain token keeps playing after someone asked for calm."
25510
+ },
25511
+ {
25512
+ "rule": "A state change finishes its story: refused shakes and says why, success confirms in place, armed asks again.",
25513
+ "why": "Motion that starts a change and leaves the outcome to a snap reads as unfinished. ConfirmButton, OtpField's invalid shake and the copy-to-check swap are the reference cases."
25514
+ },
25515
+ {
25516
+ "rule": "Component motion props and MotionScope only turn motion down.",
25517
+ "why": "`full` on a component means inherit. Nothing below the provider can switch motion back on for a reader who asked for it off."
23226
25518
  }
23227
25519
  ],
23228
25520
  "noGos": [
@@ -24300,6 +26592,126 @@
24300
26592
  "motion-interactive"
24301
26593
  ]
24302
26594
  },
26595
+ {
26596
+ "name": "motion-glide",
26597
+ "kind": "utility",
26598
+ "family": "motion",
26599
+ "intent": "Timing for ONE element that travels to the selected option — position and size ease on the emphasized curve at the slow step. Expressive: `data-motion=\"subtle\"` makes it snap.",
26600
+ "whenToUse": "A selection indicator you position yourself: a custom nav's current-page bar, a segmented control's thumb, an active dot. Tabs, ToggleGroup `segmented`, ThemeSwitcher and Carousel already glide.",
26601
+ "insteadOf": "A fill or border on each option that switches on and off (two things blink instead of one thing moving), or a hand-written transition with a literal duration.",
26602
+ "example": "<span aria-hidden className=\"motion-glide absolute bottom-0 h-0.5 bg-foreground\" style={{ left: x, width: w }} />",
26603
+ "keywords": [
26604
+ "sliding indicator",
26605
+ "tab indicator",
26606
+ "segmented thumb",
26607
+ "active indicator",
26608
+ "glide",
26609
+ "underline animation",
26610
+ "selection animation"
26611
+ ],
26612
+ "pairsWith": [
26613
+ "motion-interactive"
26614
+ ]
26615
+ },
26616
+ {
26617
+ "name": "motion-shake",
26618
+ "kind": "utility",
26619
+ "family": "motion",
26620
+ "intent": "A short decaying horizontal shake that plays while `data-shake` is set. `useShake()` sets and clears the attribute and restarts it on a repeat refusal. Expressive: under `subtle` only the error colour remains.",
26621
+ "whenToUse": "An answer the system refused: a wrong one-time code, a submit with invalid fields, a rejected password. Always alongside the error colour and message, never instead of them. OtpField's `invalid` already shakes.",
26622
+ "insteadOf": "A keyframe shake with a literal duration, which keeps moving under reduced motion, or a shake with no error text.",
26623
+ "example": "const { shake, shakeProps } = useShake()\n<form className=\"motion-shake\" {...shakeProps} onSubmit={(e) => { if (!valid) { e.preventDefault(); shake() } }}>…</form>",
26624
+ "keywords": [
26625
+ "shake",
26626
+ "error animation",
26627
+ "wrong password",
26628
+ "invalid",
26629
+ "refuse",
26630
+ "wiggle",
26631
+ "nope"
26632
+ ],
26633
+ "pairsWith": [
26634
+ "motion-interactive"
26635
+ ]
26636
+ },
26637
+ {
26638
+ "name": "motion-tick",
26639
+ "kind": "utility",
26640
+ "family": "motion",
26641
+ "intent": "A selection mark that draws itself in from its leading edge (a clip-path wipe, so it works on any icon set) with an essential fade, keyed to Base UI's starting/ending style. Exits only fade. The wipe is expressive.",
26642
+ "whenToUse": "A check or success glyph that mounts with its state: a custom selection card's tick, a completed step. Checkbox and CheckboxCard already use it.",
26643
+ "insteadOf": "A scale-from-zero pop on a check mark, which reads as a stamp rather than a tick, or a path-length draw that needs to own the SVG.",
26644
+ "example": "<Base.Indicator className=\"motion-tick\"><Icon name=\"check\" /></Base.Indicator>",
26645
+ "keywords": [
26646
+ "check animation",
26647
+ "tick",
26648
+ "draw check",
26649
+ "success mark",
26650
+ "checkbox animation",
26651
+ "selection mark"
26652
+ ]
26653
+ },
26654
+ {
26655
+ "name": "motion-appear",
26656
+ "kind": "utility",
26657
+ "family": "motion",
26658
+ "intent": "Fades and scales a small control in on first render through `@starting-style` — no data attribute, no JS. The fade is essential, the scale is expressive. Removal is instant.",
26659
+ "whenToUse": "A control that mounts in place as state changes: a search field's clear button, a send button replacing a mic, a count badge that appears. Put it on a wrapper around a Sable Button or anything carrying `motion-interactive`, so the two do not fight over `transition-property`.",
26660
+ "insteadOf": "Conditionally rendering a control so it pops in on one frame, or wrapping a single icon button in Presence.",
26661
+ "example": "{value && <span className=\"flex motion-appear\"><Button size=\"icon-xs\" variant=\"ghost\" aria-label=\"Clear\"><Icon name=\"xmark\" /></Button></span>}",
26662
+ "keywords": [
26663
+ "appear",
26664
+ "mount animation",
26665
+ "pop in",
26666
+ "enter",
26667
+ "starting style",
26668
+ "clear button"
26669
+ ],
26670
+ "pairsWith": [
26671
+ "motion-icon-swap"
26672
+ ]
26673
+ },
26674
+ {
26675
+ "name": "motion-arrive",
26676
+ "kind": "utility",
26677
+ "family": "motion",
26678
+ "intent": "Items a user action brings into a list settle into place one after another: a short fade down from 0.25rem. Stagger with `--motion-arrive-delay` (raw ms; the recipe scales it). Expressive.",
26679
+ "whenToUse": "Only on items that are NEW because of something the user did: a folder's children on expand, a row just added, results that load in. Tree's expand already uses it.",
26680
+ "insteadOf": "A staggered entrance on every render of a list, which turns a list into a loading delay, or an inline keyframe animation with literal durations.",
26681
+ "example": "<li className=\"motion-arrive\" style={{ \"--motion-arrive-delay\": \"36ms\" } as CSSProperties}>{row.name}</li>",
26682
+ "keywords": [
26683
+ "stagger",
26684
+ "list animation",
26685
+ "arrival",
26686
+ "cascade",
26687
+ "new item",
26688
+ "expand children",
26689
+ "enter list"
26690
+ ],
26691
+ "pairsWith": [
26692
+ "motion-interactive"
26693
+ ]
26694
+ },
26695
+ {
26696
+ "name": "motion-size",
26697
+ "kind": "utility",
26698
+ "family": "motion",
26699
+ "intent": "Eases an element between sizes: between a keyword and a length (0 ↔ auto) through `interpolate-size: allow-keywords`, and between measured pixel sizes when paired with `useAutoSize()`, so a button whose label changes stretches to the new width instead of jumping. A superset of motion-interactive's transition list, so it can sit on a Button. The size is expressive.",
26700
+ "whenToUse": "A control whose content changes length in place: a two-step confirm button, a Copy → Copied label, a chip that gains a count. ConfirmButton already uses it.",
26701
+ "insteadOf": "A hand-written width transition, or a fixed width that clips the longer label. A content-sized box computes `width: auto` before and after its label changes, so on its own the recipe has nothing to ease — that is what `useAutoSize()` is for.",
26702
+ "example": "const size = useAutoSize<HTMLButtonElement, HTMLSpanElement>()\n<button type=\"button\" ref={size.outerRef} className=\"motion-size\">\n\t<span ref={size.innerRef} className=\"inline-flex w-max\">{copied ? \"Copied\" : \"Copy link\"}</span>\n</button>",
26703
+ "keywords": [
26704
+ "width animation",
26705
+ "auto width",
26706
+ "label change",
26707
+ "resize animation",
26708
+ "interpolate size",
26709
+ "morph button"
26710
+ ],
26711
+ "pairsWith": [
26712
+ "motion-icon-swap"
26713
+ ]
26714
+ },
24303
26715
  {
24304
26716
  "name": "motion-spin",
24305
26717
  "kind": "utility",
@@ -24785,6 +27197,7 @@
24785
27197
  "label",
24786
27198
  "legend",
24787
27199
  "locale",
27200
+ "motion",
24788
27201
  "onActiveIndexChange",
24789
27202
  "series",
24790
27203
  "stacked",
@@ -24889,6 +27302,7 @@
24889
27302
  "labels",
24890
27303
  "legend",
24891
27304
  "locale",
27305
+ "motion",
24892
27306
  "negativeTone",
24893
27307
  "onActiveIndexChange",
24894
27308
  "orientation",
@@ -24983,6 +27397,7 @@
24983
27397
  "modifiersClassNames",
24984
27398
  "modifiersStyles",
24985
27399
  "month",
27400
+ "motion",
24986
27401
  "navLayout",
24987
27402
  "nonce",
24988
27403
  "noonSafe",
@@ -25070,6 +27485,7 @@
25070
27485
  "index",
25071
27486
  "label",
25072
27487
  "loop",
27488
+ "motion",
25073
27489
  "onIndexChange",
25074
27490
  "slideClassName",
25075
27491
  "slidesToShow"
@@ -25089,6 +27505,7 @@
25089
27505
  "invalid",
25090
27506
  "label",
25091
27507
  "leading",
27508
+ "motion",
25092
27509
  "onAttach",
25093
27510
  "onSend",
25094
27511
  "onStop",
@@ -25108,6 +27525,7 @@
25108
27525
  "id",
25109
27526
  "indeterminate",
25110
27527
  "inputRef",
27528
+ "motion",
25111
27529
  "name",
25112
27530
  "nativeButton",
25113
27531
  "onCheckedChange",
@@ -25130,6 +27548,7 @@
25130
27548
  "id",
25131
27549
  "indeterminate",
25132
27550
  "inputRef",
27551
+ "motion",
25133
27552
  "name",
25134
27553
  "nativeButton",
25135
27554
  "onCheckedChange",
@@ -25390,6 +27809,21 @@
25390
27809
  "shortcut",
25391
27810
  "size"
25392
27811
  ],
27812
+ "ConfirmButton": [
27813
+ "badge",
27814
+ "bleed",
27815
+ "children",
27816
+ "confirmHint",
27817
+ "confirmLabel",
27818
+ "confirmVariant",
27819
+ "icon",
27820
+ "iconEnd",
27821
+ "motion",
27822
+ "onConfirm",
27823
+ "size",
27824
+ "timeout",
27825
+ "variant"
27826
+ ],
25393
27827
  "ContextCard": [
25394
27828
  "actionsRef",
25395
27829
  "children",
@@ -25686,10 +28120,12 @@
25686
28120
  "Feedback": [
25687
28121
  "heading",
25688
28122
  "label",
28123
+ "motion",
25689
28124
  "onSubmit",
25690
28125
  "placeholder",
25691
28126
  "ratings",
25692
- "submitLabel"
28127
+ "submitLabel",
28128
+ "successMessage"
25693
28129
  ],
25694
28130
  "Field": [
25695
28131
  "children",
@@ -25841,6 +28277,7 @@
25841
28277
  "labels",
25842
28278
  "legend",
25843
28279
  "locale",
28280
+ "motion",
25844
28281
  "onActiveIndexChange",
25845
28282
  "series",
25846
28283
  "tooltip",
@@ -26146,6 +28583,10 @@
26146
28583
  "policy",
26147
28584
  "target"
26148
28585
  ],
28586
+ "MotionScope": [
28587
+ "children",
28588
+ "level"
28589
+ ],
26149
28590
  "NavigationMenu": [
26150
28591
  "actionsRef",
26151
28592
  "align",
@@ -26202,6 +28643,7 @@
26202
28643
  "count",
26203
28644
  "dot",
26204
28645
  "max",
28646
+ "motion",
26205
28647
  "overlay",
26206
28648
  "size",
26207
28649
  "tone"
@@ -26232,6 +28674,15 @@
26232
28674
  "style",
26233
28675
  "value"
26234
28676
  ],
28677
+ "NumberTicker": [
28678
+ "format",
28679
+ "locales",
28680
+ "motion",
28681
+ "prefix",
28682
+ "suffix",
28683
+ "trend",
28684
+ "value"
28685
+ ],
26235
28686
  "OtpField": [
26236
28687
  "aria-invalid",
26237
28688
  "autoComplete",
@@ -26246,6 +28697,7 @@
26246
28697
  "inputMode",
26247
28698
  "length",
26248
28699
  "mask",
28700
+ "motion",
26249
28701
  "name",
26250
28702
  "normalizeValue",
26251
28703
  "onValueChange",
@@ -26333,6 +28785,7 @@
26333
28785
  "labels",
26334
28786
  "legend",
26335
28787
  "locale",
28788
+ "motion",
26336
28789
  "onActiveIndexChange",
26337
28790
  "separator",
26338
28791
  "tooltip",
@@ -26489,6 +28942,7 @@
26489
28942
  "label",
26490
28943
  "legend",
26491
28944
  "locale",
28945
+ "motion",
26492
28946
  "onActiveIndexChange",
26493
28947
  "radiusAxis",
26494
28948
  "series",
@@ -26509,6 +28963,7 @@
26509
28963
  "legend",
26510
28964
  "locale",
26511
28965
  "max",
28966
+ "motion",
26512
28967
  "onActiveIndexChange",
26513
28968
  "stacked",
26514
28969
  "sweep",
@@ -26833,6 +29288,7 @@
26833
29288
  "Sidebar": [
26834
29289
  "collapsed",
26835
29290
  "icons",
29291
+ "motion",
26836
29292
  "surface",
26837
29293
  "variant"
26838
29294
  ],
@@ -26850,6 +29306,7 @@
26850
29306
  ],
26851
29307
  "SideNav": [
26852
29308
  "activeVariant",
29309
+ "motion",
26853
29310
  "size"
26854
29311
  ],
26855
29312
  "SideNavGroup": [
@@ -26975,6 +29432,7 @@
26975
29432
  "data",
26976
29433
  "label",
26977
29434
  "locale",
29435
+ "motion",
26978
29436
  "tone",
26979
29437
  "variant"
26980
29438
  ],
@@ -27050,6 +29508,7 @@
27050
29508
  "Tabs": [
27051
29509
  "className",
27052
29510
  "defaultValue",
29511
+ "motion",
27053
29512
  "onValueChange",
27054
29513
  "orientation",
27055
29514
  "render",
@@ -27114,6 +29573,7 @@
27114
29573
  ],
27115
29574
  "ThemeSwitcher": [
27116
29575
  "label",
29576
+ "motion",
27117
29577
  "onValueChange",
27118
29578
  "value"
27119
29579
  ],
@@ -27191,6 +29651,7 @@
27191
29651
  "defaultValue",
27192
29652
  "disabled",
27193
29653
  "loopFocus",
29654
+ "motion",
27194
29655
  "multiple",
27195
29656
  "onValueChange",
27196
29657
  "orientation",
@@ -27254,6 +29715,19 @@
27254
29715
  "render",
27255
29716
  "style"
27256
29717
  ],
29718
+ "ToolCall": [
29719
+ "children",
29720
+ "defaultOpen",
29721
+ "error",
29722
+ "input",
29723
+ "name",
29724
+ "onOpenChange",
29725
+ "open",
29726
+ "output",
29727
+ "status",
29728
+ "statusLabel",
29729
+ "title"
29730
+ ],
27257
29731
  "Tooltip": [
27258
29732
  "actionsRef",
27259
29733
  "children",
@@ -27281,6 +29755,10 @@
27281
29755
  "sideOffset",
27282
29756
  "style"
27283
29757
  ],
29758
+ "TooltipGroup": [
29759
+ "children",
29760
+ "motion"
29761
+ ],
27284
29762
  "TooltipPortal": [
27285
29763
  "className",
27286
29764
  "container",
@@ -27305,6 +29783,9 @@
27305
29783
  "render",
27306
29784
  "style"
27307
29785
  ],
29786
+ "Tree": [
29787
+ "motion"
29788
+ ],
27308
29789
  "TreeAddRow": [
27309
29790
  "depth",
27310
29791
  "icon",
@@ -27449,19 +29930,19 @@
27449
29930
  },
27450
29931
  {
27451
29932
  "name": "MotionProvider",
27452
- "summary": "Holds the app's motion policy and writes it as `data-motion` on <html>, which drives the `--motion-scale` switch every Sable motion recipe reads. Lets an app offer its own reduce-motion setting.",
29933
+ "summary": "Holds the app's motion policy and writes it as `data-motion` on <html>, which drives the `--motion-scale` and `--motion-expressive` switches every Sable motion recipe reads. Lets an app offer its own motion setting: full, subtle (feedback without flourishes) or off.",
27453
29934
  "required": "Optional. Without it, motion follows the visitor's OS `prefers-reduced-motion` setting, and useMotion() still works (its setPolicy does nothing).",
27454
29935
  "placement": "Once, at the app root, when the app has an in-app motion setting.",
27455
29936
  "props": [
27456
29937
  {
27457
29938
  "name": "defaultPolicy",
27458
- "type": "\"system\" | \"full\" | \"reduced\" | \"off\"",
29939
+ "type": "\"system\" | \"full\" | \"subtle\" | \"reduced\" | \"off\"",
27459
29940
  "default": "\"system\"",
27460
- "description": "Initial policy when uncontrolled. \"system\" defers to the OS preference."
29941
+ "description": "Initial policy when uncontrolled. \"system\" defers to the OS preference. \"subtle\" keeps essential motion (feedback, overlays, disclosure, loaders) and drops expressive motion (glides, rolling numbers, drawn ticks, shakes, staggers, overshoot)."
27461
29942
  },
27462
29943
  {
27463
29944
  "name": "policy",
27464
- "type": "\"system\" | \"full\" | \"reduced\" | \"off\"",
29945
+ "type": "\"system\" | \"full\" | \"subtle\" | \"reduced\" | \"off\"",
27465
29946
  "description": "Controlled policy. Pair with onPolicyChange."
27466
29947
  },
27467
29948
  {
@@ -27478,13 +29959,17 @@
27478
29959
  ],
27479
29960
  "companions": [
27480
29961
  "useMotion",
27481
- "useReducedMotion"
29962
+ "useMotionLevel",
29963
+ "useReducedMotion",
29964
+ "MotionScope"
27482
29965
  ],
27483
29966
  "notes": [
27484
- "JavaScript motion must read useMotion().enabled and do nothing when it is false; CSS recipes stop on their own."
29967
+ "JavaScript motion must read useMotion().enabled and do nothing when it is false; JavaScript flourishes read useMotion().expressive, which is also false under \"subtle\". CSS recipes follow on their own.",
29968
+ "Mapping the app's \"full\" choice back to \"system\" keeps honouring the OS reduce-motion preference; \"full\" itself forces motion on."
27485
29969
  ],
27486
29970
  "related": [
27487
- "Switch"
29971
+ "Switch",
29972
+ "ToggleGroup"
27488
29973
  ],
27489
29974
  "import": "import { MotionProvider } from \"@sableui/react\"",
27490
29975
  "examples": [
@@ -27497,6 +29982,42 @@
27497
29982
  "label": "A reduce-motion setting",
27498
29983
  "code": "function ReduceMotionSetting() {\n\tconst { policy, setPolicy } = useMotion()\n\treturn (\n\t\t<Switch\n\t\t\tchecked={policy === \"reduced\"}\n\t\t\tonCheckedChange={(on) => setPolicy(on ? \"reduced\" : \"system\")}\n\t\t\taria-label=\"Reduce motion\"\n\t\t/>\n\t)\n}",
27499
29984
  "imports": "import { Switch, useMotion } from \"@sableui/react\""
29985
+ },
29986
+ {
29987
+ "label": "A three-way motion setting",
29988
+ "code": "function MotionSetting() {\n\tconst { policy, setPolicy } = useMotion()\n\treturn (\n\t\t<ToggleGroup\n\t\t\tappearance=\"segmented\"\n\t\t\tvalue={[policy === \"system\" ? \"full\" : policy]}\n\t\t\tonValueChange={(next) => next[0] && setPolicy(next[0] === \"full\" ? \"system\" : (next[0] as \"subtle\" | \"off\"))}\n\t\t\taria-label=\"Motion\"\n\t\t>\n\t\t\t<Toggle value=\"full\">Full</Toggle>\n\t\t\t<Toggle value=\"subtle\">Subtle</Toggle>\n\t\t\t<Toggle value=\"off\">Off</Toggle>\n\t\t</ToggleGroup>\n\t)\n}",
29989
+ "imports": "import { Toggle, ToggleGroup, useMotion } from \"@sableui/react\""
29990
+ }
29991
+ ]
29992
+ },
29993
+ {
29994
+ "name": "MotionScope",
29995
+ "summary": "Turns motion down for one region — a dense data view, an embedded widget, a screen a test captures — by writing `data-motion` on a box-less wrapper and passing the same level to useMotion() below it.",
29996
+ "required": "Optional. Reach for it when a whole region should be calmer than the app; for one component, use that component's `motion` prop.",
29997
+ "placement": "Around the region. It renders a `display: contents` element, so it adds no box to the layout.",
29998
+ "props": [
29999
+ {
30000
+ "name": "level",
30001
+ "type": "\"subtle\" | \"off\"",
30002
+ "description": "\"subtle\" keeps feedback, overlays, disclosure and loaders and drops glides, rolling numbers, drawn ticks, shakes and staggers. \"off\" stills everything. A scope can only turn motion down."
30003
+ }
30004
+ ],
30005
+ "companions": [
30006
+ "useMotionLevel",
30007
+ "MotionProvider"
30008
+ ],
30009
+ "notes": [
30010
+ "Overlays portal out of the DOM subtree, so their CSS follows the app-level policy; set it on MotionProvider to cover them too."
30011
+ ],
30012
+ "related": [
30013
+ "MotionProvider"
30014
+ ],
30015
+ "import": "import { MotionScope } from \"@sableui/react\"",
30016
+ "examples": [
30017
+ {
30018
+ "label": "A calm data view",
30019
+ "code": "export function Dashboard({ children }: { children: ReactNode }) {\n\treturn <MotionScope level=\"subtle\">{children}</MotionScope>\n}",
30020
+ "imports": "import type { ReactNode } from \"react\"\nimport { MotionScope } from \"@sableui/react\""
27500
30021
  }
27501
30022
  ]
27502
30023
  },