@kznlabs/labsui 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +283 -60
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13148,7 +13148,7 @@ export function AdminLayout({ children }: { children: React.ReactNode }) {
13148
13148
  </main>
13149
13149
  </SidebarLayout>
13150
13150
  );
13151
- }` }], api: { entries: [{ kind: "component", name: "SidebarLayout", props: [{ name: "collapsed", type: "boolean", required: false, description: "Controlled rail state; pair with `onCollapsedChange`." }, { name: "defaultCollapsed", type: "boolean", required: false, default: "false", description: "Start as the icon rail (uncontrolled)." }, { name: "onCollapsedChange", type: "(collapsed: boolean) => void", required: false, description: "Called from the trigger or the shortcut." }, { name: "shortcut", type: "boolean", required: false, default: "true", description: "Toggle with ⌘B / Ctrl+B (default: true). Ignored while typing in a field." }], extends: "<div>", description: "Lays the sidebar and the page side by side and holds the rail state. Below md, the sidebar is a left drawer." }, { kind: "component", name: "Sidebar", props: [{ name: "groups", type: "SidebarGroup[]", required: true, description: "The navigation, top to bottom." }, { name: "current", type: "string", required: false, description: 'Id of the current item: highlighted, `aria-current="page"` on links.' }, { name: "footer", type: "SidebarItem[]", required: false, description: "Entries pinned to the bottom, e.g. settings." }, { name: "header", type: "ReactNode", required: false, description: "Top slot, e.g. the workspace name." }, { name: "label", type: "string", required: false, default: '"Sidebar"', description: "Accessible name for the navigation landmark." }, { name: "onSelect", type: "(id: string) => void", required: false, description: "Called with an item's id when it is picked (links too, e.g. for client routing)." }], description: "Navigation sidebar from data. Collapses to an icon rail on a spring; labels clip but stay in the a11y tree." }, { kind: "component", name: "SidebarTrigger", props: [] }], types: { SidebarItem: "{ id?: string; label: string; icon?: ReactNode; href?: string; badge?: ReactNode; }", SidebarGroup: "{ label?: string; items: SidebarItem[]; collapsible?: boolean; defaultOpen?: boolean; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/collapsible.json", "https://kznlabs.github.io/labsUI/r/drawer.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-hotkey.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/sidebar.tsx", type: "registry:ui", content: `"use client";
13151
+ }` }], api: { entries: [{ kind: "component", name: "SidebarLayout", props: [{ name: "collapsed", type: "boolean", required: false, description: "Controlled rail state; pair with `onCollapsedChange`." }, { name: "defaultCollapsed", type: "boolean", required: false, default: "false", description: "Start as the icon rail (uncontrolled)." }, { name: "onCollapsedChange", type: "(collapsed: boolean) => void", required: false, description: "Called from the trigger or the shortcut." }, { name: "shortcut", type: "boolean", required: false, default: "true", description: "Toggle with ⌘B / Ctrl+B (default: true). Ignored while typing in a field." }], extends: "<div>", description: "Lays the sidebar and the page side by side and holds the rail state. Below md, the sidebar is a left drawer." }, { kind: "component", name: "Sidebar", props: [{ name: "groups", type: "SidebarGroup[]", required: true, description: "The navigation, top to bottom." }, { name: "current", type: "string", required: false, description: 'Id of the current item: highlighted, `aria-current="page"` on links.' }, { name: "footer", type: "SidebarItem[]", required: false, description: "Entries pinned to the bottom, e.g. settings." }, { name: "header", type: "ReactNode", required: false, description: "Top slot, e.g. the workspace name." }, { name: "label", type: "string", required: false, default: '"Sidebar"', description: "Accessible name for the navigation landmark." }, { name: "onSelect", type: "(id: string) => void", required: false, description: "Called with an item's id when it is picked (links too, e.g. for client routing)." }], description: "Navigation sidebar from data. Collapses to an icon rail on a spring; labels clip but stay in the a11y tree." }, { kind: "component", name: "SidebarTrigger", props: [] }], types: { SidebarItem: "{ id?: string; label: string; icon?: ReactNode; href?: string; badge?: ReactNode; }", SidebarGroup: "{ label?: string; items: SidebarItem[]; collapsible?: boolean; defaultOpen?: boolean; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/collapsible.json", "https://kznlabs.github.io/labsUI/r/drawer.json", "https://kznlabs.github.io/labsUI/r/kbd.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/tooltip.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-hotkey.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/sidebar.tsx", type: "registry:ui", content: `"use client";
13152
13152
 
13153
13153
  import { PanelLeft } from "lucide-react";
13154
13154
  import { motion } from "motion/react";
@@ -13156,7 +13156,9 @@ import { type ComponentProps, createContext, type ReactNode, use, useState } fro
13156
13156
  import { Button } from "@/components/ui/button";
13157
13157
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
13158
13158
  import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer";
13159
- import { useMediaQuery } from "@/lib/platform";
13159
+ import { KbdGroup } from "@/components/ui/kbd";
13160
+ import { Tooltip } from "@/components/ui/tooltip";
13161
+ import { useIsMac, useMediaQuery } from "@/lib/platform";
13160
13162
  import { useControllableState } from "@/lib/use-controllable-state";
13161
13163
  import { useHotkey } from "@/lib/use-hotkey";
13162
13164
  import { cn, spring } from "@/lib/utils";
@@ -13168,6 +13170,7 @@ type SidebarCtx = {
13168
13170
  mobile: boolean;
13169
13171
  openMobile: boolean;
13170
13172
  setOpenMobile: (open: boolean) => void;
13173
+ shortcut: boolean;
13171
13174
  };
13172
13175
 
13173
13176
  const SidebarContext = createContext<SidebarCtx | null>(null);
@@ -13215,7 +13218,9 @@ export function SidebarLayout({
13215
13218
  useHotkey(shortcut && "mod+b", () => setCollapsed(!collapsed), { skipInFields: true });
13216
13219
 
13217
13220
  return (
13218
- <SidebarContext value={{ collapsed, setCollapsed, mobile, openMobile, setOpenMobile }}>
13221
+ <SidebarContext
13222
+ value={{ collapsed, setCollapsed, mobile, openMobile, setOpenMobile, shortcut }}
13223
+ >
13219
13224
  <div className={cn("flex min-h-0 w-full", className)} {...props}>
13220
13225
  {children}
13221
13226
  </div>
@@ -13413,21 +13418,31 @@ function Entry({
13413
13418
  }
13414
13419
 
13415
13420
  export function SidebarTrigger({ className }: { className?: string }) {
13416
- const { collapsed, setCollapsed, mobile, openMobile } = useSidebar();
13421
+ const { collapsed, setCollapsed, mobile, openMobile, shortcut } = useSidebar();
13422
+ const mac = useIsMac();
13417
13423
  const expanded = mobile ? openMobile : !collapsed;
13418
13424
 
13419
- return (
13425
+ const button = (
13420
13426
  <Button
13421
13427
  variant="ghost"
13422
13428
  size="icon"
13423
13429
  aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
13424
13430
  aria-expanded={expanded}
13431
+ aria-keyshortcuts={shortcut ? (mac ? "Meta+B" : "Control+B") : undefined}
13425
13432
  onClick={() => setCollapsed(!collapsed)}
13426
13433
  className={cn("size-8", className)}
13427
13434
  >
13428
13435
  <PanelLeft aria-hidden="true" />
13429
13436
  </Button>
13430
13437
  );
13438
+
13439
+ return shortcut ? (
13440
+ <Tooltip content={<KbdGroup keys={mac ? ["⌘", "B"] : ["Ctrl", "B"]} />} side="bottom">
13441
+ {button}
13442
+ </Tooltip>
13443
+ ) : (
13444
+ button
13445
+ );
13431
13446
  }
13432
13447
  ` }] }, "site-header": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "site-header", type: "registry:block", title: "Site Header", description: "Sticky, translucent site header from one nav array: brand, NavigationMenu panels from md up, a right Drawer with the same links below it, and your actions on the right. The hairline underneath appears only once content scrolls beneath it.", docs: "https://kznlabs.github.io/labsUI/components/site-header", usage: '<SiteHeader brand={<a href="/">Acme</a>} current="/blog" nav={[{ label: "Products", items: [{ title: "Bot", href: "/bot", icon: <Bot /> }] }, { label: "Blog", href: "/blog" }]} actions={<ThemeToggle />} />', examples: [{ name: "header.tsx", code: `import { BookOpen, Gauge } from "lucide-react";
13433
13448
  import { SiteHeader } from "@/components/blocks/site-header";
@@ -15886,7 +15901,7 @@ function Cursor() {
15886
15901
  return (
15887
15902
  <span
15888
15903
  aria-hidden="true"
15889
- className="ml-px inline-block h-[1.1em] w-[0.55em] translate-y-[0.2em] animate-pulse bg-foreground/70"
15904
+ className="ml-px inline-block h-[1.1em] w-[0.55em] translate-y-[0.2em] animate-pulse bg-foreground/70 motion-reduce:animate-none"
15890
15905
  />
15891
15906
  );
15892
15907
  }
@@ -16156,10 +16171,13 @@ export function DynamicTag() {
16156
16171
  <TextSwap items={["Next.js 16", "Tailwind CSS 4", "motion 14"]} direction="up" />
16157
16172
  </span>
16158
16173
  );
16159
- }` }], api: { entries: [{ kind: "component", name: "TextSwap", props: [{ name: "children", type: "string", required: false, description: "A single string; changing it swaps to the new text." }, { name: "direction", type: '"up" | "down" | "fade"', required: false, default: '"up"', description: 'Swap direction (default: "up").' }, { name: "index", type: "number", required: false, description: "Active index when controlling externally." }, { name: "interval", type: "number", required: false, default: "3000", description: "Milliseconds between swaps when cycling automatically (default: 3000)." }, { name: "items", type: "readonly string[]", required: false, description: "Array of items to cycle through, or pass a single changing child." }, { name: "pauseOnHover", type: "boolean", required: false, default: "true", description: "Pause the cycle on mouse hover (default: true)." }] }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/text-swap.tsx", type: "registry:ui", content: `"use client";
16174
+ }` }], api: { entries: [{ kind: "component", name: "TextSwap", props: [{ name: "children", type: "string", required: false, description: "A single string; changing it swaps to the new text." }, { name: "controls", type: "boolean", required: false, default: "false", description: "Show a pause button after the text, so keyboard and touch users can stop it too (WCAG 2.2.2). With a mouse it shows on hover or focus. Use it when it cycles next to other content. Hidden with reduced motion." }, { name: "direction", type: '"up" | "down" | "fade"', required: false, default: '"up"', description: 'Swap direction (default: "up").' }, { name: "index", type: "number", required: false, description: "Active index when controlling externally." }, { name: "interval", type: "number", required: false, default: "3000", description: "Milliseconds between swaps when cycling automatically (default: 3000). With reduced motion it doesn't cycle." }, { name: "items", type: "readonly string[]", required: false, description: "Array of items to cycle through, or pass a single changing child." }, { name: "pauseLabel", type: "string", required: false, default: '"Pause animation"', description: 'Accessible name of the pause button (default: "Pause animation").' }, { name: "pauseOnHover", type: "boolean", required: false, default: "true", description: "Pause the cycle on mouse hover (default: true)." }] }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button-variants.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/text-swap.tsx", type: "registry:ui", content: `"use client";
16160
16175
 
16161
- import { AnimatePresence, motion } from "motion/react";
16176
+ import { Pause, Play } from "lucide-react";
16177
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
16162
16178
  import { useEffect, useState } from "react";
16179
+ import { buttonVariants } from "@/components/ui/button-variants";
16180
+ import { useMounted } from "@/lib/platform";
16163
16181
  import { cn, spring } from "@/lib/utils";
16164
16182
 
16165
16183
  type TextSwapProps = {
@@ -16167,12 +16185,16 @@ type TextSwapProps = {
16167
16185
  items?: readonly string[];
16168
16186
  /** Active index when controlling externally. */
16169
16187
  index?: number;
16170
- /** Milliseconds between swaps when cycling automatically (default: 3000). */
16188
+ /** Milliseconds between swaps when cycling automatically (default: 3000). With reduced motion it doesn't cycle. */
16171
16189
  interval?: number;
16172
16190
  /** Swap direction (default: "up"). */
16173
16191
  direction?: "up" | "down" | "fade";
16174
16192
  /** Pause the cycle on mouse hover (default: true). */
16175
16193
  pauseOnHover?: boolean;
16194
+ /** Show a pause button after the text, so keyboard and touch users can stop it too (WCAG 2.2.2). With a mouse it shows on hover or focus. Use it when it cycles next to other content. Hidden with reduced motion. */
16195
+ controls?: boolean;
16196
+ /** Accessible name of the pause button (default: "Pause animation"). */
16197
+ pauseLabel?: string;
16176
16198
  className?: string;
16177
16199
  /** A single string; changing it swaps to the new text. */
16178
16200
  children?: string;
@@ -16184,6 +16206,8 @@ export function TextSwap({
16184
16206
  interval = 3000,
16185
16207
  direction = "up",
16186
16208
  pauseOnHover = true,
16209
+ controls = false,
16210
+ pauseLabel = "Pause animation",
16187
16211
  className,
16188
16212
  children,
16189
16213
  }: TextSwapProps) {
@@ -16191,29 +16215,37 @@ export function TextSwap({
16191
16215
  const isControlled = controlledIndex !== undefined;
16192
16216
 
16193
16217
  const [uncontrolledIndex, setUncontrolledIndex] = useState(0);
16194
- const [isPaused, setIsPaused] = useState(false);
16218
+ const [isHovered, setIsHovered] = useState(false);
16219
+ const [isStopped, setIsStopped] = useState(false);
16220
+ const isPaused = isHovered || isStopped;
16221
+ const reduce = useReducedMotion();
16222
+ const mounted = useMounted();
16195
16223
 
16196
16224
  const activeIndex = isControlled ? controlledIndex : uncontrolledIndex;
16197
16225
  const currentItem = list[activeIndex % (list.length || 1)] ?? "";
16198
16226
 
16199
16227
  useEffect(() => {
16200
- if (isControlled || list.length <= 1 || isPaused || interval <= 0) return;
16228
+ // Reduced motion: no auto-cycling, the first item stays.
16229
+ if (isControlled || list.length <= 1 || isPaused || reduce || interval <= 0) return;
16201
16230
 
16202
16231
  const timer = setInterval(() => {
16203
16232
  setUncontrolledIndex((prev) => (prev + 1) % list.length);
16204
16233
  }, interval);
16205
16234
 
16206
16235
  return () => clearInterval(timer);
16207
- }, [isControlled, list.length, isPaused, interval]);
16236
+ }, [isControlled, list.length, isPaused, reduce, interval]);
16208
16237
 
16209
16238
  const yOffset = direction === "up" ? 14 : direction === "down" ? -14 : 0;
16210
16239
  const exitY = direction === "up" ? -14 : direction === "down" ? 14 : 0;
16211
16240
 
16212
- return (
16213
- /* biome-ignore lint/a11y/noStaticElementInteractions: pause text cycling on hover */
16241
+ const withButton = controls && mounted && !reduce && !isControlled && list.length > 1;
16242
+ const hover = pauseOnHover
16243
+ ? { onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) }
16244
+ : {};
16245
+
16246
+ const swap = (
16214
16247
  <span
16215
- onMouseEnter={pauseOnHover ? () => setIsPaused(true) : undefined}
16216
- onMouseLeave={pauseOnHover ? () => setIsPaused(false) : undefined}
16248
+ {...(!withButton && hover)}
16217
16249
  // Every item sits invisibly in one grid cell, so the box is always the longest item and swaps
16218
16250
  // never reflow the layout (e.g. a two-line quote).
16219
16251
  className={cn("relative inline-grid overflow-hidden", className)}
@@ -16252,6 +16284,29 @@ export function TextSwap({
16252
16284
  </AnimatePresence>
16253
16285
  </span>
16254
16286
  );
16287
+
16288
+ if (!withButton) return swap;
16289
+
16290
+ return (
16291
+ <span {...hover} className="group/pause">
16292
+ {swap}
16293
+
16294
+ <button
16295
+ type="button"
16296
+ aria-label={pauseLabel}
16297
+ aria-pressed={isStopped}
16298
+ onClick={() => setIsStopped(!isStopped)}
16299
+ className={buttonVariants({
16300
+ variant: "ghost",
16301
+ size: "icon",
16302
+ className:
16303
+ "ml-1 size-6 rounded-md align-middle text-muted-foreground [&_svg]:size-3.5 opacity-0 group-hover/pause:opacity-100 focus-visible:opacity-100 aria-pressed:opacity-100 pointer-coarse:opacity-100",
16304
+ })}
16305
+ >
16306
+ {isStopped ? <Play /> : <Pause />}
16307
+ </button>
16308
+ </span>
16309
+ );
16255
16310
  }
16256
16311
  ` }] }, textarea: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} /><Textarea aria-label="Bio" maxLength={280} showCount /><Switch aria-label="Notifications" checked={on} onCheckedChange={setOn} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
16257
16312
  import { Label } from "@/components/ui/label";
@@ -16571,14 +16626,15 @@ export function TimelineItem({
16571
16626
  </li>
16572
16627
  );
16573
16628
  }
16574
- ` }] }, toast: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover or focus, and F8 jumps to the newest toast from the keyboard.", docs: "https://kznlabs.github.io/labsUI/components/toast", mount: "<Toaster />", usage: 'toast.success("Saved", { description: "All changes synced", action: { label: "Undo", onClick } })', examples: [{ name: "save.tsx", code: `import { toast } from "@/components/ui/toast";
16629
+ ` }] }, toast: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover or focus, a toast with an action stays until it's used or dismissed, and F8 jumps to the newest toast with an action (else the newest) from the keyboard.", docs: "https://kznlabs.github.io/labsUI/components/toast", mount: "<Toaster />", usage: 'toast.success("Saved", { description: "All changes synced", action: { label: "Undo", onClick } })', examples: [{ name: "save.tsx", code: `import { toast } from "@/components/ui/toast";
16575
16630
  export function notifySaved(undo: () => void) {
16576
16631
  toast.success("Changes saved", {
16577
16632
  description: "All changes synced",
16578
16633
  action: { label: "Undo", onClick: undo },
16579
16634
  });
16580
- }` }], api: { entries: [{ kind: "function", name: "toast", signature: 'toast(title: ReactNode, opts?: Partial<Omit<ToastData, "id" | "title">>) => number', description: '`toast("Saved")`, `toast.success(title, { description, action, duration })`, `toast.loading(title)`, `toast.promise(p, { loading, success, error })`, `toast.dismiss(id?)`.' }, { kind: "component", name: "Toaster", props: [], description: `Mount once (root layout). Stacked toasts: hover, focus or a tap expands, swipe dismisses. F8
16581
- jumps to the newest toast's buttons (timers pause while focus is there), Esc goes back.` }], types: { ToastData: "{ id: number; title: ReactNode; description?: ReactNode; variant: ToastVariant; duration: number; action?: { label: string; onClick: () => void }; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/focus.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/use-hotkey.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/toast.tsx", type: "registry:ui", content: `"use client";
16635
+ }` }], api: { entries: [{ kind: "function", name: "toast", signature: 'toast(title: ReactNode, opts?: Partial<Omit<ToastData, "id" | "title">>) => number', description: '`toast("Saved")`, `toast.success(title, { description, action, duration })`, `toast.loading(title)`, `toast.promise(p, { loading, success, error })`, `toast.dismiss(id?)`.' }, { kind: "component", name: "Toaster", props: [{ name: "actionDuration", type: "number", required: false, default: "Number.POSITIVE_INFINITY", description: "ms before a toast with an action closes; Infinity by default, so it stays until used or dismissed. A toast's own `duration` still wins." }, { name: "duration", type: "number", required: false, default: "4000", description: "ms before a toast without an action closes; a toast's own `duration` wins." }], description: `Mount once (root layout). Stacked toasts: hover, focus or a tap expands, swipe dismisses. F8
16636
+ jumps to the newest toast with an action, else the newest (timers pause while focus is there);
16637
+ after a key press that toast shows the shortcut. Esc, the action or Dismiss sends focus back.` }], types: { ToastData: "{ id: number; title: ReactNode; description?: ReactNode; variant: ToastVariant; duration?: number; action?: { label: string; onClick: () => void }; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/focus.json", "https://kznlabs.github.io/labsUI/r/kbd.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/use-hotkey.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/toast.tsx", type: "registry:ui", content: `"use client";
16582
16638
 
16583
16639
  import { CircleAlert, CircleCheck, Info, Loader2, X } from "lucide-react";
16584
16640
  import { AnimatePresence, animate, motion, type PanInfo, useMotionValue } from "motion/react";
@@ -16590,7 +16646,9 @@ import {
16590
16646
  useState,
16591
16647
  useSyncExternalStore,
16592
16648
  } from "react";
16649
+ import { Kbd } from "@/components/ui/kbd";
16593
16650
  import { tabbables } from "@/lib/focus";
16651
+ import { useIsMac } from "@/lib/platform";
16594
16652
  import { useDismiss } from "@/lib/use-dismiss";
16595
16653
  import { useHotkey } from "@/lib/use-hotkey";
16596
16654
  import { cn, material, spring } from "@/lib/utils";
@@ -16603,8 +16661,8 @@ export type ToastData = {
16603
16661
  title: ReactNode;
16604
16662
  description?: ReactNode;
16605
16663
  variant: ToastVariant;
16606
- /** ms; Infinity = sticky. */
16607
- duration: number;
16664
+ /** ms; Infinity = sticky. Defaults to the Toaster's \`duration\` or \`actionDuration\`. */
16665
+ duration?: number;
16608
16666
  action?: { label: string; onClick: () => void };
16609
16667
  };
16610
16668
  type ToastOptions = Partial<Omit<ToastData, "id" | "title">>;
@@ -16631,7 +16689,7 @@ const MAX = 6;
16631
16689
 
16632
16690
  function create(title: ReactNode, opts: ToastOptions = {}) {
16633
16691
  const id = ++nextId;
16634
- const next: ToastData = { id, title, variant: "default", duration: 4000, ...opts };
16692
+ const next: ToastData = { id, title, variant: "default", ...opts };
16635
16693
 
16636
16694
  toasts = [next, ...toasts].slice(0, MAX);
16637
16695
  emit();
@@ -16662,7 +16720,7 @@ function promise<T>(
16662
16720
 
16663
16721
  pending.then(
16664
16722
  (value) =>
16665
- update(id, { title: resolve(msgs.success, value), variant: "success", duration: 4000 }),
16723
+ update(id, { title: resolve(msgs.success, value), variant: "success", duration: undefined }),
16666
16724
  (error) => update(id, { title: resolve(msgs.error, error), variant: "error", duration: 6000 }),
16667
16725
  );
16668
16726
 
@@ -16711,11 +16769,24 @@ const icons = {
16711
16769
  loading: <Loader2 className="size-4 animate-spin text-muted-foreground" aria-hidden="true" />,
16712
16770
  };
16713
16771
 
16772
+ export type ToasterProps = {
16773
+ /** ms before a toast without an action closes; a toast's own \`duration\` wins. */
16774
+ duration?: number;
16775
+ /** ms before a toast with an action closes; Infinity by default, so it stays until used or dismissed. A toast's own \`duration\` still wins. */
16776
+ actionDuration?: number;
16777
+ className?: string;
16778
+ };
16779
+
16714
16780
  /**
16715
16781
  * Mount once (root layout). Stacked toasts: hover, focus or a tap expands, swipe dismisses. F8
16716
- * jumps to the newest toast's buttons (timers pause while focus is there), Esc goes back.
16782
+ * jumps to the newest toast with an action, else the newest (timers pause while focus is there);
16783
+ * after a key press that toast shows the shortcut. Esc, the action or Dismiss sends focus back.
16717
16784
  */
16718
- export function Toaster({ className }: { className?: string }) {
16785
+ export function Toaster({
16786
+ duration = 4000,
16787
+ actionDuration = Number.POSITIVE_INFINITY,
16788
+ className,
16789
+ }: ToasterProps) {
16719
16790
  const items = useSyncExternalStore(
16720
16791
  subscribe,
16721
16792
  () => toasts,
@@ -16723,11 +16794,30 @@ export function Toaster({ className }: { className?: string }) {
16723
16794
  );
16724
16795
  const section = useRef<HTMLElement>(null);
16725
16796
  const back = useRef<HTMLElement | null>(null);
16797
+ const waiting = items.find((item) => item.action)?.id;
16798
+ const [keyboard, setKeyboard] = useState(false);
16799
+ const mac = useIsMac();
16800
+
16801
+ useEffect(() => {
16802
+ const onKey = () => setKeyboard(true);
16803
+ // motion's tap fires a pointerdown without a pointerType when Enter presses a button.
16804
+ const onPointer = (event: PointerEvent) => event.pointerType && setKeyboard(false);
16805
+
16806
+ window.addEventListener("keydown", onKey, true);
16807
+ window.addEventListener("pointerdown", onPointer, true);
16808
+
16809
+ return () => {
16810
+ window.removeEventListener("keydown", onKey, true);
16811
+ window.removeEventListener("pointerdown", onPointer, true);
16812
+ };
16813
+ }, []);
16726
16814
 
16727
16815
  useHotkey(
16728
16816
  "F8",
16729
16817
  () => {
16730
- const first = tabbables(section.current)[0];
16818
+ const first =
16819
+ tabbables(section.current?.querySelector<HTMLElement>("[data-waiting]") ?? null)[0] ??
16820
+ tabbables(section.current)[0];
16731
16821
 
16732
16822
  if (!first) return false;
16733
16823
  back.current = document.activeElement as HTMLElement | null;
@@ -16736,6 +16826,19 @@ export function Toaster({ className }: { className?: string }) {
16736
16826
  { skipInFields: false },
16737
16827
  );
16738
16828
 
16829
+ // The buttons unmount with their toast, so focus moves first.
16830
+ const leave = useCallback((from: Element) => {
16831
+ if (!section.current?.contains(document.activeElement)) return;
16832
+
16833
+ const toast = from.closest("li");
16834
+ const target = back.current?.isConnected
16835
+ ? back.current
16836
+ : tabbables(section.current).find((element) => !toast?.contains(element));
16837
+
16838
+ back.current = null;
16839
+ target?.focus();
16840
+ }, []);
16841
+
16739
16842
  const [expanded, setExpanded] = useState(false);
16740
16843
  const [heights, setHeights] = useState<Record<number, number>>({});
16741
16844
  const onHeight = useCallback(
@@ -16785,12 +16888,16 @@ export function Toaster({ className }: { className?: string }) {
16785
16888
  <ToastItem
16786
16889
  key={item.id}
16787
16890
  toast={item}
16891
+ duration={item.duration ?? (item.action ? actionDuration : duration)}
16788
16892
  index={i}
16893
+ waiting={item.id === waiting}
16894
+ hint={keyboard ? (mac ? ["fn", "F8"] : ["F8"]) : undefined}
16789
16895
  y={y}
16790
16896
  expanded={expanded}
16791
16897
  frontHeight={frontH}
16792
16898
  total={items.length}
16793
16899
  onHeight={onHeight}
16900
+ onLeave={leave}
16794
16901
  />
16795
16902
  );
16796
16903
  })}
@@ -16802,15 +16909,31 @@ export function Toaster({ className }: { className?: string }) {
16802
16909
 
16803
16910
  type ItemProps = {
16804
16911
  toast: ToastData;
16912
+ duration: number;
16805
16913
  index: number;
16914
+ waiting: boolean;
16915
+ hint?: string[];
16806
16916
  y: number;
16807
16917
  expanded: boolean;
16808
16918
  frontHeight: number;
16809
16919
  total: number;
16810
16920
  onHeight: (id: number, height: number) => void;
16921
+ onLeave: (from: Element) => void;
16811
16922
  };
16812
16923
 
16813
- function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeight }: ItemProps) {
16924
+ function ToastItem({
16925
+ toast: data,
16926
+ duration,
16927
+ index,
16928
+ waiting,
16929
+ hint,
16930
+ y,
16931
+ expanded,
16932
+ frontHeight,
16933
+ total,
16934
+ onHeight,
16935
+ onLeave,
16936
+ }: ItemProps) {
16814
16937
  const inner = useRef<HTMLDivElement>(null);
16815
16938
  const [height, setHeight] = useState(0);
16816
16939
  const x = useMotionValue(0);
@@ -16819,7 +16942,7 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16819
16942
  // Paused while you read the stack and while the tab is in the background.
16820
16943
  const visible = useSyncExternalStore(onVisibility, isVisible, () => true);
16821
16944
  const paused = expanded || !visible;
16822
- const remaining = useRef({ duration: data.duration, left: data.duration });
16945
+ const remaining = useRef({ duration, left: duration });
16823
16946
 
16824
16947
  useEffect(() => {
16825
16948
  const element = inner.current;
@@ -16838,9 +16961,8 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16838
16961
 
16839
16962
  useEffect(() => {
16840
16963
  // Time left restarts when the duration changes (a promise toast settling).
16841
- if (remaining.current.duration !== data.duration)
16842
- remaining.current = { duration: data.duration, left: data.duration };
16843
- if (paused || !Number.isFinite(data.duration)) return;
16964
+ if (remaining.current.duration !== duration) remaining.current = { duration, left: duration };
16965
+ if (paused || !Number.isFinite(duration)) return;
16844
16966
 
16845
16967
  const start = Date.now();
16846
16968
  const timer = setTimeout(() => dismiss(data.id), remaining.current.left);
@@ -16849,7 +16971,7 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16849
16971
  clearTimeout(timer);
16850
16972
  remaining.current.left -= Date.now() - start;
16851
16973
  };
16852
- }, [paused, data.id, data.duration]);
16974
+ }, [paused, data.id, duration]);
16853
16975
 
16854
16976
  const onDragEnd = (_: unknown, info: PanInfo) => {
16855
16977
  if (Math.abs(info.offset.x) > 80 || Math.abs(info.velocity.x) > 500) {
@@ -16879,6 +17001,7 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16879
17001
  dragMomentum={false}
16880
17002
  onDragEnd={onDragEnd}
16881
17003
  inert={hidden}
17004
+ data-waiting={waiting || undefined}
16882
17005
  className={cn(
16883
17006
  "group absolute inset-x-0 bottom-0 touch-pan-y rounded-2xl border text-popover-foreground shadow-lg",
16884
17007
  material,
@@ -16902,16 +17025,27 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16902
17025
  <p className="font-medium text-sm">{data.title}</p>
16903
17026
 
16904
17027
  {data.description && <p className="text-muted-foreground text-sm">{data.description}</p>}
17028
+
17029
+ {data.action && <span className="sr-only">Press F8 for {data.action.label}.</span>}
16905
17030
  </div>
16906
17031
 
17032
+ {waiting && hint && (
17033
+ <span aria-hidden="true" className="flex shrink-0 gap-1 group-focus-within:hidden">
17034
+ {hint.map((key) => (
17035
+ <Kbd key={key}>{key}</Kbd>
17036
+ ))}
17037
+ </span>
17038
+ )}
17039
+
16907
17040
  {data.action && (
16908
17041
  <button
16909
17042
  type="button"
16910
- onClick={() => {
17043
+ onClick={(event) => {
16911
17044
  data.action?.onClick();
17045
+ onLeave(event.currentTarget);
16912
17046
  dismiss(data.id);
16913
17047
  }}
16914
- className="h-7 shrink-0 cursor-pointer rounded-md bg-primary px-2.5 font-medium text-primary-foreground text-xs outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] focus-visible:ring-ring/50"
17048
+ className="-my-1 h-7 shrink-0 cursor-pointer rounded-md bg-primary px-2.5 font-medium text-primary-foreground text-xs outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] focus-visible:ring-ring/50"
16915
17049
  >
16916
17050
  {data.action.label}
16917
17051
  </button>
@@ -16919,8 +17053,11 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16919
17053
 
16920
17054
  <button
16921
17055
  type="button"
16922
- onClick={() => dismiss(data.id)}
16923
- className="-mt-1 -mr-1 shrink-0 cursor-pointer rounded-md p-1 text-muted-foreground opacity-0 outline-none transition-opacity hover:text-foreground focus-visible:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/50 group-hover:opacity-100"
17056
+ onClick={(event) => {
17057
+ onLeave(event.currentTarget);
17058
+ dismiss(data.id);
17059
+ }}
17060
+ className="absolute -top-2 -right-2 inline-flex size-6 cursor-pointer items-center justify-center rounded-full border bg-popover text-muted-foreground opacity-0 shadow-sm outline-none transition-opacity hover:text-foreground focus-visible:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/50 group-hover:opacity-100"
16924
17061
  >
16925
17062
  <X className="size-3.5" aria-hidden="true" />
16926
17063
 
@@ -17325,10 +17462,12 @@ export function Headline() {
17325
17462
  />
17326
17463
  </h2>
17327
17464
  );
17328
- }` }], api: { entries: [{ kind: "component", name: "Typewriter", props: [{ name: "text", type: "string | readonly string[]", required: true, description: "Text to type, or an array of phrases to cycle through." }, { name: "cursor", type: "boolean", required: false, default: "true", description: "Whether to show a blinking cursor (default: true)." }, { name: "cursorChar", type: "string", required: false, default: '"|"', description: 'Custom character for the cursor (default: "|").' }, { name: "cursorClassName", type: "string", required: false, description: "Classes for the blinking cursor." }, { name: "delay", type: "number", required: false, default: "0", description: "Milliseconds to wait before starting typing (default: 0)." }, { name: "deleteSpeed", type: "number", required: false, default: "24", description: "Milliseconds per character while deleting (default: 24)." }, { name: "loop", type: "boolean", required: false, default: "true", description: "Whether to loop through phrases when text is an array (default: true)." }, { name: "onComplete", type: "() => void", required: false, description: "Called when the text is fully typed." }, { name: "pauseDuration", type: "number", required: false, default: "2000", description: "Milliseconds to pause once a phrase is fully typed (default: 2000)." }, { name: "speed", type: "number", required: false, default: "40", description: "Milliseconds per character while typing (default: 40)." }], description: "Typewriter effect for headers, quotes, and hero text. Full text stays in the DOM for SEO." }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/typewriter.tsx", type: "registry:ui", content: `"use client";
17465
+ }` }], api: { entries: [{ kind: "component", name: "Typewriter", props: [{ name: "text", type: "string | readonly string[]", required: true, description: "Text to type, or an array of phrases to cycle through." }, { name: "controls", type: "boolean", required: false, default: "false", description: "Show a pause button, so keyboard and touch users can stop it too (WCAG 2.2.2). With a mouse it shows on hover or focus. Use it when the text keeps moving next to other content. Hidden with reduced motion." }, { name: "cursor", type: "boolean", required: false, default: "true", description: "Whether to show a blinking cursor (default: true)." }, { name: "cursorChar", type: "string", required: false, default: '"|"', description: 'Custom character for the cursor (default: "|").' }, { name: "cursorClassName", type: "string", required: false, description: "Classes for the blinking cursor." }, { name: "delay", type: "number", required: false, default: "0", description: "Milliseconds to wait before starting typing (default: 0)." }, { name: "deleteSpeed", type: "number", required: false, default: "24", description: "Milliseconds per character while deleting (default: 24)." }, { name: "loop", type: "boolean", required: false, default: "true", description: "Whether to loop through phrases when text is an array (default: true)." }, { name: "onComplete", type: "() => void", required: false, description: "Called when the text is fully typed." }, { name: "pauseDuration", type: "number", required: false, default: "2000", description: "Milliseconds to pause once a phrase is fully typed (default: 2000)." }, { name: "pauseLabel", type: "string", required: false, default: '"Pause animation"', description: 'Accessible name of the pause button (default: "Pause animation").' }, { name: "pauseOnHover", type: "boolean", required: false, default: "true", description: "Pause typing on mouse hover (default: true)." }, { name: "speed", type: "number", required: false, default: "40", description: "Milliseconds per character while typing (default: 40)." }], description: "Typewriter effect for headers, quotes, and hero text. Full text stays in the DOM for SEO." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button-variants.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/typewriter.tsx", type: "registry:ui", content: `"use client";
17329
17466
 
17467
+ import { Pause, Play } from "lucide-react";
17330
17468
  import { useReducedMotion } from "motion/react";
17331
17469
  import { useEffect, useEffectEvent, useMemo, useState } from "react";
17470
+ import { buttonVariants } from "@/components/ui/button-variants";
17332
17471
  import { useMounted } from "@/lib/platform";
17333
17472
  import { cn } from "@/lib/utils";
17334
17473
 
@@ -17345,6 +17484,12 @@ type TypewriterProps = {
17345
17484
  pauseDuration?: number;
17346
17485
  /** Whether to loop through phrases when text is an array (default: true). */
17347
17486
  loop?: boolean;
17487
+ /** Pause typing on mouse hover (default: true). */
17488
+ pauseOnHover?: boolean;
17489
+ /** Show a pause button, so keyboard and touch users can stop it too (WCAG 2.2.2). With a mouse it shows on hover or focus. Use it when the text keeps moving next to other content. Hidden with reduced motion. */
17490
+ controls?: boolean;
17491
+ /** Accessible name of the pause button (default: "Pause animation"). */
17492
+ pauseLabel?: string;
17348
17493
  /** Whether to show a blinking cursor (default: true). */
17349
17494
  cursor?: boolean;
17350
17495
  /** Custom character for the cursor (default: "|"). */
@@ -17364,6 +17509,9 @@ export function Typewriter({
17364
17509
  delay = 0,
17365
17510
  pauseDuration = 2000,
17366
17511
  loop = true,
17512
+ pauseOnHover = true,
17513
+ controls = false,
17514
+ pauseLabel = "Pause animation",
17367
17515
  cursor = true,
17368
17516
  cursorChar = "|",
17369
17517
  cursorClassName,
@@ -17375,6 +17523,9 @@ export function Typewriter({
17375
17523
  const [phraseIndex, setPhraseIndex] = useState(0);
17376
17524
  const [subIndex, setSubIndex] = useState(0);
17377
17525
  const [isDeleting, setIsDeleting] = useState(false);
17526
+ const [isHovered, setIsHovered] = useState(false);
17527
+ const [isStopped, setIsStopped] = useState(false);
17528
+ const isPaused = isHovered || isStopped;
17378
17529
 
17379
17530
  const mounted = useMounted();
17380
17531
  // After mount only, so hydration matches.
@@ -17394,7 +17545,7 @@ export function Typewriter({
17394
17545
  }, [delay]);
17395
17546
 
17396
17547
  useEffect(() => {
17397
- if (!started || phrases.length === 0) return;
17548
+ if (!started || isPaused || phrases.length === 0) return;
17398
17549
 
17399
17550
  if (!isDeleting && subIndex === currentPhrase.length) {
17400
17551
  if (phrases.length === 1 && !loop) {
@@ -17434,6 +17585,7 @@ export function Typewriter({
17434
17585
  return () => clearTimeout(timer);
17435
17586
  }, [
17436
17587
  started,
17588
+ isPaused,
17437
17589
  subIndex,
17438
17590
  isDeleting,
17439
17591
  phraseIndex,
@@ -17449,7 +17601,12 @@ export function Typewriter({
17449
17601
  const displayed = reduce ? (phrases[0] ?? "") : currentPhrase.slice(0, subIndex);
17450
17602
 
17451
17603
  return (
17452
- <span className={cn("inline-flex items-baseline", className)}>
17604
+ /* biome-ignore lint/a11y/noStaticElementInteractions: pause typing on hover */
17605
+ <span
17606
+ onMouseEnter={pauseOnHover ? () => setIsHovered(true) : undefined}
17607
+ onMouseLeave={pauseOnHover ? () => setIsHovered(false) : undefined}
17608
+ className={cn("group/pause inline-flex items-baseline", className)}
17609
+ >
17453
17610
  <span className="sr-only">{fullText}</span>
17454
17611
 
17455
17612
  <span aria-hidden="true" data-typewriter-animated="">
@@ -17464,16 +17621,48 @@ export function Typewriter({
17464
17621
  <span
17465
17622
  aria-hidden="true"
17466
17623
  className={cn(
17467
- "ml-0.5 inline-block animate-pulse font-light text-muted-foreground select-none",
17624
+ "ml-0.5 inline-block animate-pulse font-light motion-reduce:animate-none text-muted-foreground select-none",
17468
17625
  cursorClassName,
17469
17626
  )}
17470
17627
  >
17471
17628
  {cursorChar}
17472
17629
  </span>
17473
17630
  )}
17631
+
17632
+ {controls && mounted && !reduce && (
17633
+ <PauseButton label={pauseLabel} pressed={isStopped} onPressedChange={setIsStopped} />
17634
+ )}
17474
17635
  </span>
17475
17636
  );
17476
17637
  }
17638
+
17639
+ function PauseButton({
17640
+ label,
17641
+ pressed,
17642
+ onPressedChange,
17643
+ }: {
17644
+ label: string;
17645
+ pressed: boolean;
17646
+ onPressedChange: (pressed: boolean) => void;
17647
+ }) {
17648
+ return (
17649
+ <button
17650
+ type="button"
17651
+ aria-label={label}
17652
+ aria-pressed={pressed}
17653
+ onClick={() => onPressedChange(!pressed)}
17654
+ className={buttonVariants({
17655
+ variant: "ghost",
17656
+ size: "icon",
17657
+ // A mouse pauses it by hovering, so the button waits for hover or focus; touch always sees it.
17658
+ className:
17659
+ "ml-1 size-6 self-center rounded-md text-muted-foreground [&_svg]:size-3.5 opacity-0 group-hover/pause:opacity-100 focus-visible:opacity-100 aria-pressed:opacity-100 pointer-coarse:opacity-100",
17660
+ })}
17661
+ >
17662
+ {pressed ? <Play /> : <Pause />}
17663
+ </button>
17664
+ );
17665
+ }
17477
17666
  ` }] }, "use-anchor": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "use-anchor", type: "registry:hook", description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
17478
17667
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
17479
17668
  Measures before paint, so nothing jumps.`, api: { entries: [{ kind: "function", name: "useAnchor", signature: `useAnchor(open: boolean, anchor: RefObject<HTMLElement | null>, floating: RefObject<HTMLElement | null>, { align, side: preferred, offset }?: {
@@ -17854,7 +18043,7 @@ export const clamp = (value: number, min: number, max: number) =>
17854
18043
  ` }] } },
17855
18044
  index: [{ name: "accent-picker", type: "registry:ui", title: "Accent Picker", description: "Accent color row: eight swatches (red to slate) that retint primary, focus rings, selection, switches and checkboxes app-wide. Persisted, applied before paint, or set your own hue with --accent-hue.", exports: ["AccentPicker"], keywords: ["accent", "app", "color", "radio", "retint", "row", "setaccent", "swatches", "that", "the", "via", "whole"], registryDependencies: ["theme", "utils"] }, { name: "accordion", type: "registry:ui", title: "Accordion", description: "Single or multiple panels with spring height animation. Closed content stays in the DOM for SEO.", exports: ["Accordion", "AccordionItem", "AccordionTrigger", "AccordionContent"], keywords: ["accordion", "accordioncontent", "accordionitem", "accordiontrigger", "allow", "and", "called", "children", "closed", "closes", "closing", "collapsible", "content", "controlled", "crawlable", "default", "defaultvalue", "every", "first", "heading", "height", "html", "independent", "inert", "item", "items", "level", "match", "matched", "mounted", "multiple", "none", "one", "only", "onvaluechange", "open", "outline", "page", "panel", "panels", "question", "remains", "render", "single", "stays", "text", "the", "time", "type", "uncontrolled", "unique", "value", "when", "while", "with", "within", "wrapping"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "alert", type: "registry:ui", title: "Alert", description: "Server-safe inline banner: tinted surface, colored icon, title, description and an optional action. Info, success, warning and destructive tones follow the accent and status tokens.", exports: ["Alert", "AlertTitle", "AlertDescription"], keywords: ["action", "add", "alert", "aligned", "and", "announced", "appears", "banner", "button", "colored", "default", "description", "did", "for", "icon", "inline", "live", "load", "none", "not", "null", "pass", "region", "replaces", "response", "right", "role", "sets", "should", "slot", "small", "something", "static", "surface", "the", "tint", "tinted", "title", "tone", "user", "variant", "when"], registryDependencies: ["utils"] }, { name: "app-window", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["AppWindow"], keywords: ["actions", "aligned", "area", "bar", "blurred", "bodyclassname", "button", "called", "centered", "classes", "content", "control", "custom", "dot", "for", "glass", "handler", "header", "into", "matching", "maximize", "minimize", "navbar", "onclose", "onmaximize", "onminimize", "passing", "pressed", "replaces", "right", "search", "shows", "slot", "surface", "tabs", "text", "the", "title", "titlebar", "translucent", "turns", "under", "when"], registryDependencies: ["utils"] }, { name: "aspect-ratio", type: "registry:ui", title: "Aspect Ratio", description: "Server-safe box locked to a ratio (16:9 thumbnails, 9:16 clips). A direct image or video child fills it with object-cover.", exports: ["AspectRatio"], keywords: ["box", "child", "cover", "direct", "fills", "height", "img", "locked", "object", "ratio", "video", "width"], registryDependencies: ["utils"] }, { name: "attachment", type: "registry:ui", title: "Attachment & Dropzone", description: "File chips with image thumbnails or type icons, size, a springing upload bar, error state and remove; the list springs chips in and closes the gap. FileDropzone wraps a real file input: click, keyboard and the native picker work, drop and paste are checked against accept and maxSize, and dropped files land in the input so forms and Server Actions receive them.", exports: ["formatBytes", "Attachment", "AttachmentList", "FileDropzone"], keywords: ["accept", "actions", "along", "and", "are", "area", "aria", "around", "attachments", "attribute", "back", "bar", "bcp", "blocks", "both", "bottom", "browse", "button", "bytes", "called", "child", "children", "chip", "click", "content", "createobjecturl", "default", "describedby", "disabled", "download", "drop", "dropped", "drops", "each", "edge", "enforced", "enter", "error", "failed", "field", "file", "files", "fills", "for", "form", "forms", "fresh", "from", "get", "given", "hint", "href", "icon", "ids", "image", "input", "into", "invalid", "key", "least", "line", "link", "locale", "makes", "maxsize", "mime", "more", "multiple", "name", "native", "needs", "one", "onfiles", "onreject", "onremove", "out", "passed", "paste", "pdf", "picked", "picker", "picks", "place", "plain", "pressed", "progress", "real", "reason", "red", "remove", "removed", "replaces", "required", "same", "server", "sets", "show", "shows", "size", "source", "space", "spring", "springs", "src", "styling", "submit", "submitted", "syntax", "text", "than", "that", "the", "them", "thumbnail", "too", "turns", "type", "types", "upload", "uploading", "url", "when", "while", "with", "work", "written"], registryDependencies: ["field", "utils"] }, { name: "avatar", type: "registry:ui", title: "Avatar", description: "Image with initials fallback on error, three sizes and online/away/busy/offline dots.", exports: ["Avatar"], keywords: ["alt", "and", "announced", "corner", "diameter", "dot", "fails", "fallback", "for", "icon", "image", "initials", "loads", "name", "overrides", "person", "presence", "show", "size", "src", "status", "text", "the", "url", "used", "while", "with"], registryDependencies: ["utils"] }, { name: "avatar-group", type: "registry:ui", title: "AvatarGroup", description: "Overlapping avatars with a +N overflow chip. Hover gently enlarges the avatar on a spring, keeping stack order.", exports: ["AvatarGroup"], keywords: ["alt", "and", "avatar", "avatars", "before", "chip", "each", "enlarges", "every", "gently", "hover", "how", "many", "max", "name", "one", "order", "overflow", "overlapping", "people", "show", "size", "spring", "stack", "staying", "the", "users", "with"], registryDependencies: ["avatar", "utils"] }, { name: "badge", type: "registry:ui", title: "Badge & Pill", description: "Minimal status tags. Rounded badge or fully rounded pill, with optional dot. pulse adds an expanding live-status ring.", exports: ["Badge"], keywords: ["and", "color", "current", "dot", "emits", "expanding", "forever", "fully", "leading", "like", "live", "needs", "pill", "pulse", "ring", "rounded", "shape", "status", "surface", "tag", "text", "the", "variant"], registryDependencies: ["utils"] }, { name: "breadcrumb", type: "registry:ui", title: "Breadcrumb", description: "Navigation trail from a list of items: links up to the current page, which is marked for assistive tech. Server-safe, custom separator.", exports: ["Breadcrumb"], keywords: ["between", "chevron", "current", "default", "drawn", "ending", "first", "item", "items", "last", "links", "list", "outermost", "page", "safe", "separator", "server", "site", "sits", "the", "trail", "where"], registryDependencies: [] }, { name: "button", type: "registry:ui", title: "Button", description: "Five variants, four sizes, spring press feedback, an animated loading spinner and in-place success state, a two-step confirm that swaps the label on first click, and a configurable swap direction. Width stays fixed to the longest label.", exports: ["Button"], keywords: ["action", "also", "and", "aria", "armed", "before", "blur", "both", "button", "can", "check", "children", "click", "clicks", "confirm", "confirmlabel", "confirmtimeout", "default", "defaults", "destructive", "done", "emphasis", "enter", "esc", "feedback", "fill", "first", "focusable", "for", "form", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "out", "padding", "per", "place", "primary", "reserved", "resets", "resize", "runs", "second", "shown", "shows", "size", "slides", "spinner", "spins", "square", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "text", "the", "this", "twice", "two", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button-variants", "utils"] }, { name: "button-group", type: "registry:ui", title: "Button Group", description: "Joins buttons into one strip that shares borders, corners and active states. Horizontal or vertical.", exports: ["ButtonGroup"], keywords: ["and", "attached", "borders", "buttons", "connected", "control", "corners", "default", "fuse", "groups", "horizontal", "into", "orientation", "related", "single", "strip", "together", "toolbar", "true", "vertical"], registryDependencies: ["utils"] }, { name: "button-variants", type: "registry:ui", description: 'Button classes for non-button elements, e.g. `<a className={buttonVariants({ variant: "outline" })}>`.', exports: ["buttonVariants"], keywords: ["button", "buttonvariants", "classes", "classname", "elements", "for", "non", "outline", "variant"], registryDependencies: ["utils"] }, { name: "calendar", type: "registry:ui", title: "Calendar", description: "Month grid on the WAI-ARIA date-grid pattern: single date or range with a hover preview, min/max and custom disabled days, months that slide in from the direction of travel. Arrows, Home/End and PageUp/PageDown (+Shift for years). Hydration-safe: today is only marked in the browser.", exports: ["key", "Calendar"], keywords: ["and", "another", "any", "aria", "arrows", "autofocus", "bcp", "called", "client", "controlled", "current", "custom", "date", "datepicker", "dates", "day", "days", "default", "defaultmonth", "defaultvalue", "direction", "disabled", "displayed", "earliest", "end", "ends", "extra", "first", "focus", "for", "from", "function", "grid", "home", "hover", "into", "isdisabled", "iso", "keyboard", "latest", "like", "local", "locale", "max", "min", "mode", "monday", "month", "months", "mount", "names", "nothing", "null", "off", "one", "only", "onmonthchange", "onvaluechange", "pagedown", "pages", "pageup", "pattern", "pick", "picked", "preview", "range", "render", "rule", "saturday", "selectable", "sets", "shift", "shown", "single", "slide", "sorts", "start", "sunday", "the", "then", "this", "travel", "uncontrolled", "user", "value", "wai", "week", "weekday", "weekends", "weekstartson", "when", "with", "year", "yyyy"], registryDependencies: ["button", "use-controllable-state", "utils"] }, { name: "card", type: "registry:ui", title: "Card", description: "Composable surface: header, title, description, content and footer. Optional glass.", exports: ["Card", "CardHeader", "CardTitle", "CardDescription", "CardContent", "CardFooter"], keywords: ["backgrounds", "colored", "default", "directly", "fits", "follows", "glass", "heading", "level", "not", "outline", "over", "page", "place", "subtle", "textured", "that", "the", "translucency", "under", "when"], registryDependencies: ["utils"] }, { name: "carousel", type: "registry:ui", title: "Carousel", description: "Native scroll-snap row, so touch, trackpad and no-JS scrolling work, with spring-driven arrows, page dots, arrow keys and mouse drag that throws to the next snap point. APG carousel semantics: a labeled region of slides announced as 3 of 8.", exports: ["Carousel", "CarouselContent", "CarouselItem", "CarouselPrevious", "CarouselNext", "CarouselDots"], keywords: ["accessible", "all", "and", "announced", "any", "apg", "are", "arrow", "basis", "buttons", "carousel", "css", "default", "dot", "dots", "drag", "driven", "edge", "end", "fewer", "from", "gap", "hides", "keys", "label", "labeled", "left", "mouse", "name", "native", "not", "one", "overlaid", "page", "pages", "per", "projects", "region", "rem", "right", "row", "scroll", "scrolling", "semantics", "sized", "slide", "slides", "snap", "spring", "start", "the", "there", "total", "touch", "trackpad", "two", "variables", "view", "with", "work"], registryDependencies: ["button", "utils"] }, { name: "changelog", type: "registry:block", title: "Changelog", description: "Release notes on a Timeline: newest release highlighted, changes typed New / Improved / Fixed with Badges, and a Kbd shortcut to try the headline feature. Server-safe.", exports: ["Changelog"], keywords: ["are", "badges", "changes", "first", "highlighted", "how", "kbd", "newest", "notes", "one", "release", "releases", "shows", "the", "timeline", "try", "typed", "with"], registryDependencies: ["badge", "kbd", "timeline", "utils"] }, { name: "chart", type: "registry:ui", title: "Chart", description: "Line, area and bar (grouped or stacked) charts in SVG with a colorblind-validated eight-color palette that keeps each series' color. Crosshair tooltip listing every series, arrow-key readout, legend that hides series, direct end labels, a table view, and spring transitions between data.", exports: ["Chart"], keywords: ["add", "and", "are", "area", "areas", "arrow", "assistive", "axis", "bar", "bars", "bcp", "can", "captions", "categorical", "category", "chart", "charts", "compact", "crosshair", "currency", "data", "direct", "each", "eight", "end", "every", "filled", "fold", "for", "format", "grouping", "height", "hides", "index", "instead", "into", "key", "keys", "label", "labels", "legend", "line", "lines", "listing", "locale", "mark", "most", "names", "notation", "numbers", "object", "one", "only", "other", "pages", "palette", "past", "per", "plot", "plus", "props", "render", "row", "serializable", "series", "server", "split", "stack", "stacked", "style", "svg", "table", "tail", "tech", "that", "the", "them", "ticks", "too", "tooltip", "type", "usd", "value", "view", "with"], registryDependencies: ["button", "table", "use-dismiss", "utils"] }, { name: "checkbox", type: "registry:ui", title: "Checkbox", description: "Native checkbox with a spring-drawn check mark and an indeterminate state for select-all rows. Controlled or uncontrolled; submits with forms.", exports: ["Checkbox"], keywords: ["all", "called", "check", "checkbox", "checked", "controlled", "dash", "defaultchecked", "drawn", "field", "first", "indeterminate", "inside", "mixed", "native", "new", "oncheckedchange", "pair", "render", "rows", "select", "selected", "shows", "some", "spring", "state", "the", "uncontrolled", "with", "works"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "clip-card", type: "registry:block", title: "Clip Card", description: "Video tile: AspectRatio thumbnail (16:9 or 9:16), duration Badge, watched Progress along the bottom, and a ContextMenu of actions.", exports: ["ClipCard"], keywords: ["accessible", "actions", "ago", "along", "also", "and", "aspectratio", "badge", "bar", "bottom", "card", "click", "clip", "clips", "context", "contextmenuitem", "days", "duration", "f10", "film", "for", "glyph", "height", "how", "img", "items", "menu", "meta", "much", "name", "one", "opens", "print", "progress", "ratio", "right", "shift", "small", "stands", "the", "them", "thin", "thumbnail", "tile", "title", "under", "vertical", "video", "views", "was", "watched", "width", "without"], registryDependencies: ["aspect-ratio", "badge", "context-menu", "progress", "utils"] }, { name: "code", type: "registry:ui", title: "Code", description: "Shiki-highlighted code that follows light and dark through light-dark() token colors, as a block, an inline one-liner or a tabbed file panel. Line numbers (from any start line), tinted lines with an accent bar, <mark>ed words or column ranges, and per-line notes in a tooltip that keyboard users can tab to. Optional Prettier at display time; copy hands out exactly what is shown.", exports: ["CopyButton", "Code", "CodePanel"], keywords: ["accessible", "active", "add", "all", "and", "animates", "annotations", "bar", "bash", "before", "behind", "block", "body", "build", "button", "clipboard", "code", "collapse", "copied", "copy", "dark", "default", "dom", "each", "every", "excerpts", "file", "files", "first", "focus", "fold", "for", "format", "formatted", "from", "gets", "ghost", "gutter", "header", "highlight", "highlightcode", "highlighted", "highlighting", "hover", "ignored", "inline", "inside", "into", "its", "json", "keep", "label", "language", "least", "left", "light", "line", "linenumbers", "lines", "loads", "longer", "made", "many", "mark", "marks", "name", "never", "note", "notes", "number", "numbers", "one", "options", "own", "page", "paint", "panel", "per", "place", "prettier", "primary", "ranges", "refer", "registry", "remember", "renders", "run", "scrolling", "scrolls", "server", "sets", "shared", "shiki", "show", "source", "stays", "switching", "syntax", "tab", "terminal", "text", "than", "that", "the", "themes", "this", "time", "tint", "tokens", "too", "touch", "tsx", "turns", "variant", "vertically", "wheel", "with", "words", "wrapped", "written"], registryDependencies: ["button", "highlight", "overflow-focus", "tabs", "tooltip", "utils"] }, { name: "collapsible", type: "registry:ui", title: "Collapsible", description: "A single show/hide region with a spring height animation and rotating chevron. Controlled or uncontrolled; closed content stays in the DOM for SEO.", exports: ["Collapsible", "CollapsibleTrigger", "CollapsibleContent"], keywords: ["after", "and", "called", "chevron", "children", "closed", "collapsiblecontent", "collapsibletrigger", "content", "controlled", "crawlable", "default", "defaultopen", "first", "height", "hide", "html", "inert", "label", "mounted", "one", "onopenchange", "open", "pair", "region", "remains", "render", "rotating", "show", "springs", "state", "stays", "that", "the", "toggles", "trigger", "true", "uncontrolled", "when", "while", "with"], registryDependencies: ["use-controllable-state", "utils"] }, { name: "combobox", type: "registry:ui", title: "Combobox", description: "A Select you can type into: the query filters by label and keywords (prefix matches first), arrows move, Enter picks. multiple keeps the list open and shows picks as removable tags. Submits and validates with the form.", exports: ["Combobox"], keywords: ["accessible", "and", "aria", "backspace", "blocks", "called", "can", "choices", "closes", "controlled", "defaultvalue", "describedby", "disabled", "down", "emptytext", "enter", "entry", "error", "esc", "every", "field", "fills", "filtered", "filters", "first", "form", "from", "hint", "ids", "input", "into", "invalid", "isn", "keeps", "label", "last", "list", "matches", "move", "multiple", "name", "needs", "nothing", "one", "onto", "onvaluechange", "open", "opening", "options", "pair", "per", "pick", "picked", "picks", "placeholder", "removable", "removes", "render", "required", "select", "sets", "show", "shown", "shows", "single", "stay", "styling", "submit", "submitted", "tags", "text", "the", "them", "there", "type", "types", "typing", "uncontrolled", "user", "value", "values", "visible", "walks", "when", "with", "you"], registryDependencies: ["field", "form-value", "list", "platform", "tag-input", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "command", type: "registry:ui", title: "Command", description: "⌘K palette: filterable groups, arrow-key highlight, Enter to run, keyword matching. Inline or as CommandDialog with a global shortcut; the first Esc clears, the second closes.", exports: ["Command", "CommandInput", "CommandList", "CommandEmpty", "CommandGroup", "CommandSeparator", "CommandItem", "CommandDialog"], keywords: ["accessible", "after", "against", "and", "arrows", "but", "called", "children", "click", "close", "closes", "collapse", "command", "commanddialog", "commandempty", "commandinput", "commanditem", "commandlist", "content", "controlled", "ctrl", "defaults", "dialog", "disable", "disabled", "driven", "elsewhere", "end", "enter", "esc", "extra", "false", "filterable", "filtering", "for", "global", "group", "groups", "heading", "hidden", "hides", "home", "icon", "inline", "input", "inside", "item", "items", "its", "itself", "keepopen", "keyboard", "keywords", "label", "list", "listbox", "mac", "match", "matched", "matches", "mod", "move", "name", "none", "not", "only", "onopenchange", "onselect", "open", "option", "outside", "own", "pair", "palette", "pass", "passes", "placeholder", "preferences", "query", "results", "scrollable", "search", "selectable", "selecting", "selection", "selects", "separators", "settings", "shortcut", "shown", "shows", "stack", "state", "stay", "terms", "text", "the", "title", "toggle", "toggled", "use", "used", "value", "visible", "visually", "when", "while", "with", "would"], registryDependencies: ["dialog", "list", "search-input", "separator", "use-controllable-state", "use-hotkey", "utils"] }, { name: "confirm", type: "registry:ui", title: "Confirm", description: "Promise-based confirm dialog called like toast: await confirm({ title, description, destructive }) resolves true or false. Built on Dialog's alert mode, Cancel takes the first focus, Esc cancels, one host mounted in the layout.", exports: ["confirm", "Confirmer"], keywords: ["acting", "asks", "await", "before", "cancel", "confirm", "confirmer", "delete", "destructive", "esc", "false", "for", "host", "layout", "mount", "mounted", "needs", "newer", "next", "once", "project", "remove", "resolves", "root", "title", "toaster", "true"], registryDependencies: ["button", "dialog"] }, { name: "context-menu", type: "registry:ui", title: "Context Menu", description: "Right-click menu for an area, opening at the pointer and flipping at screen edges. Shares Dropdown Menu's items, checkbox and radio items, keyboard and typeahead; Menu key or Shift+F10 opens it from the keyboard.", exports: ["ContextMenuContent", "ContextMenuItem", "ContextMenuCheckboxItem", "ContextMenuRadioGroup", "ContextMenuRadioItem", "ContextMenuLabel", "ContextMenuSeparator", "ContextMenu", "ContextMenuTrigger"], keywords: ["actions", "align", "aligned", "and", "area", "but", "called", "center", "check", "checked", "children", "click", "clickable", "closes", "contextmenucontent", "contextmenutrigger", "destructive", "disabled", "dropdownmenuradioitem", "edge", "enter", "equals", "f10", "first", "focus", "focusable", "for", "group", "groups", "hint", "icon", "irreversible", "item", "items", "its", "key", "keyboard", "label", "labels", "leading", "lines", "mark", "menu", "new", "not", "oncheckedchange", "onopenchange", "onselect", "onvaluechange", "opens", "optionally", "picked", "pointer", "press", "red", "right", "selectable", "separators", "shift", "shortcut", "shown", "shows", "state", "the", "then", "trigger", "value", "variant", "when", "with"], registryDependencies: ["dropdown-menu", "utils"] }, { name: "count-to", type: "registry:ui", title: "CountTo", description: "Springs a number up from a start value once it scrolls into view. Prefix, suffix, decimals and locale.", exports: ["CountTo"], keywords: ["after", "and", "bcp", "before", "decimal", "decimals", "digits", "dom", "final", "for", "from", "grouping", "into", "locale", "number", "once", "point", "prefix", "scrolls", "seo", "springs", "starting", "stays", "suffix", "text", "the", "value", "view"], registryDependencies: ["utils"] }, { name: "custom-cursor", type: "registry:ui", title: "Custom Cursor", description: "Optional spring cursor that expands over interactive elements. Off on touch and reduced motion.", exports: ["CustomCursor"], keywords: ["coarse", "cursor", "dot", "elements", "expands", "interactive", "motion", "mount", "nothing", "once", "opt", "over", "pointers", "reduced", "renders", "ring", "spring", "touch", "trailing", "with"], registryDependencies: ["platform", "utils"] }, { name: "data-table", type: "registry:ui", title: "Data Table", description: "Table with columns as data: search across every column, sortable headers, checkbox selection that survives paging, a ⋯ menu per row, bulk actions in the toolbar and pagination. Built from Table, SearchInput, Checkbox, DropdownMenu, Pagination and Empty.", exports: ["DataTable"], keywords: ["accessor", "across", "actions", "all", "alternate", "and", "are", "backgrounds", "box", "bulk", "called", "cell", "checkbox", "client", "column", "columns", "compose", "controlled", "controls", "data", "defaultsort", "definitions", "display", "driven", "dropdownmenuitem", "each", "every", "field", "filters", "first", "for", "friends", "getrowid", "happen", "header", "ids", "items", "its", "jane", "kept", "label", "members", "menu", "name", "names", "onselectionchange", "order", "page", "pages", "pagesize", "pagination", "paging", "pair", "per", "placeholder", "readers", "receives", "render", "right", "row", "rowactions", "rowlabel", "rows", "screen", "search", "searchable", "searchplaceholder", "select", "selectable", "selected", "selection", "server", "show", "side", "sort", "sortable", "stable", "striped", "survives", "table", "that", "the", "toolbar", "with", "yourself"], registryDependencies: ["button", "checkbox", "dropdown-menu", "empty", "pagination", "search-input", "table", "use-controllable-state", "utils"] }, { name: "date-picker", type: "registry:ui", title: "Date Picker", description: "Field-styled button that opens a Calendar in a Popover, focused on the picked day. Single or range, presets such as Last 7 days, closes once the pick is complete, and submits ISO dates with the form. Works inside Field.", exports: ["DatePicker"], keywords: ["accessible", "and", "are", "aria", "bcp", "beside", "blocks", "both", "button", "calendar", "called", "client", "closes", "controlled", "date", "day", "days", "default", "defaultvalue", "describedby", "disabled", "earliest", "end", "ends", "error", "extra", "field", "fill", "fills", "first", "focused", "for", "form", "from", "function", "grid", "hint", "htmlfor", "ids", "invalid", "isdisabled", "isn", "iso", "label", "last", "latest", "let", "locale", "max", "min", "mode", "monday", "name", "needs", "nothing", "null", "off", "once", "one", "only", "onvaluechange", "opening", "opens", "optional", "pair", "pick", "picked", "placeholder", "popover", "presets", "range", "reach", "readers", "ready", "render", "required", "rule", "saturday", "selectable", "set", "sets", "shortcuts", "single", "start", "styled", "styling", "submit", "submits", "submitted", "sunday", "text", "that", "the", "then", "there", "these", "today", "uncontrolled", "value", "visible", "weekends", "weekstartson", "when", "with"], registryDependencies: ["calendar", "field", "form-value", "input", "popover", "use-controllable-state", "utils"] }, { name: "dialog", type: "registry:ui", title: "Dialog", description: "Portal modal with blurred backdrop, spring scale-in, focus trap, Esc and scroll lock.", exports: ["useDialog", "Dialog", "DialogTrigger", "DialogClose", "useModal", "DialogContent", "DialogHeader", "DialogFooter", "DialogTitle", "DialogDescription"], keywords: ["action", "alert", "alertdialog", "also", "and", "aria", "armed", "attach", "backdrop", "before", "behavior", "blur", "blurred", "body", "both", "button", "called", "can", "check", "children", "click", "clicks", "close", "confirm", "confirmlabel", "confirmtimeout", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialog", "dialogclose", "dialogcontent", "dialogs", "dialogtrigger", "does", "done", "drawer", "emphasis", "enter", "esc", "feedback", "field", "fill", "first", "focus", "focusable", "footer", "for", "form", "give", "header", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "lock", "modal", "mode", "never", "not", "off", "old", "onclick", "one", "ones", "only", "onopenchange", "open", "out", "padding", "page", "pair", "panel", "per", "place", "portal", "primary", "ref", "render", "reserved", "resets", "resize", "restore", "returned", "role", "root", "runs", "scale", "scroll", "second", "shared", "showclose", "shown", "shows", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "this", "topmost", "trap", "trigger", "twice", "two", "uncontrolled", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button", "focus", "platform", "use-controllable-state", "utils"] }, { name: "drawer", type: "registry:ui", title: "Drawer", description: "Edge panel (left, right, top, bottom) that springs in and swipes away. Shares Dialog's focus trap, scroll lock and Esc handling. Sidebar uses it below md.", exports: ["Drawer", "DrawerTrigger", "DrawerClose", "DrawerTitle", "DrawerDescription", "DrawerContent", "DrawerHeader", "DrawerBody", "DrawerFooter"], keywords: ["action", "also", "and", "anywhere", "aria", "armed", "backdrop", "before", "blur", "body", "both", "bottom", "button", "called", "can", "check", "children", "click", "clicks", "close", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialogclose", "dialogcontent", "dialogtrigger", "dismiss", "done", "drag", "drawer", "drawers", "edge", "emphasis", "enter", "esc", "feedback", "fill", "first", "focusable", "footer", "for", "form", "from", "give", "handle", "header", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "padding", "pair", "panel", "per", "place", "primary", "render", "reserved", "resets", "resize", "runs", "scroll", "scrolls", "second", "showclose", "shown", "shows", "side", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "swipe", "taller", "text", "than", "that", "the", "their", "this", "top", "toward", "trigger", "twice", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["dialog", "platform", "utils"] }, { name: "dropdown-menu", type: "registry:ui", title: "Dropdown Menu", description: "Action menu anchored to a Button: items with icons and shortcuts, checkbox and radio items, labels, separators, destructive variant. Arrow keys, typeahead, focus follows the pointer, Esc returns focus.", exports: ["MenuContext", "DropdownMenu", "DropdownMenuTrigger", "DropdownMenuContent", "DropdownMenuItem", "DropdownMenuCheckboxItem", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuLabel", "DropdownMenuSeparator"], keywords: ["action", "actions", "align", "aligned", "also", "anchored", "and", "aria", "armed", "before", "blur", "both", "but", "button", "called", "can", "check", "checked", "children", "choice", "click", "clicks", "closes", "closing", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disabled", "done", "down", "dropdownmenuradioitem", "edge", "emphasis", "enter", "equals", "esc", "feedback", "fill", "first", "focusable", "for", "form", "give", "group", "groups", "height", "hint", "icon", "ignore", "ignores", "irreversible", "item", "items", "its", "keeps", "keyboard", "label", "labels", "last", "leading", "left", "lines", "loading", "loadinglabel", "mark", "menu", "never", "new", "not", "old", "oncheckedchange", "onclick", "one", "ones", "onopenchange", "onselect", "onvaluechange", "open", "opens", "optionally", "out", "outside", "padding", "pair", "per", "picked", "picking", "place", "primary", "radiogroup", "red", "render", "reserved", "resets", "resize", "right", "runs", "second", "selectable", "selection", "separators", "shortcut", "shown", "shows", "size", "slides", "space", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "text", "the", "then", "this", "toggles", "trigger", "twice", "two", "uncontrolled", "value", "variant", "view", "when", "where", "while", "width", "with", "without"], registryDependencies: ["button", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "empty", type: "registry:ui", title: "Empty", description: "Server-safe empty state: icon tile, title, one helpful sentence and the next action. Bordered variant for empty lists and drop zones.", exports: ["Empty"], keywords: ["about", "action", "and", "app", "area", "bordered", "children", "clips", "dashed", "description", "drop", "empty", "for", "helpful", "icon", "inside", "like", "list", "next", "one", "outline", "page", "rounded", "sentence", "shown", "state", "the", "tile", "title", "what", "yet", "zone"], registryDependencies: ["utils"] }, { name: "field", type: "registry:ui", title: "Field", description: "Label, control, description and error as one unit. Controls inside (Input, Textarea, Select, Checkbox, Switch, RadioGroup) get id, aria-describedby, aria-invalid and required automatically; errors spring in and are announced.", exports: ["useFieldControl", "useFieldLabel", "Field"], keywords: ["and", "aria", "automatically", "beside", "can", "checkbox", "children", "control", "controls", "describedby", "description", "dims", "disabled", "disables", "enclosing", "error", "explicit", "field", "fieldset", "fills", "first", "for", "form", "from", "group", "have", "hint", "horizontal", "ids", "input", "invalid", "its", "label", "makes", "marks", "match", "must", "names", "one", "orientation", "out", "outside", "own", "picks", "props", "radiogroup", "rating", "renders", "required", "returns", "shown", "single", "springs", "state", "switch", "target", "the", "unchanged", "under", "unit", "use", "when", "which", "win", "wired", "with"], registryDependencies: ["label", "utils"] }, { name: "focus", type: "registry:lib", description: "What Tab reaches inside `root`, in order. Skips disabled and hidden fields, tabIndex -1 (a\nFormValue helper) and all but one radio per group, so the first and last are real stops.", exports: ["tabbables"], keywords: ["all", "and", "are", "but", "disabled", "fields", "first", "formvalue", "group", "helper", "hidden", "inside", "last", "one", "order", "per", "radio", "reaches", "real", "root", "skips", "stops", "tab", "tabindex", "the", "what"], registryDependencies: [] }, { name: "form-value", type: "registry:lib", description: "Submits a custom control's value and takes part in native validation, which a hidden input\ncan't: an empty `required` value blocks submit, shows the browser's message and focuses `anchor`.", exports: ["FormValue", "useFormReset"], keywords: ["action", "after", "anchor", "and", "back", "blocks", "browser", "button", "called", "calls", "can", "control", "controlled", "custom", "default", "disabled", "does", "empty", "field", "finds", "focus", "focuses", "form", "gets", "goes", "hidden", "input", "invalid", "its", "leaves", "like", "message", "name", "native", "nothing", "oninvalid", "only", "onreset", "onvaluechange", "out", "parent", "part", "picked", "react", "reports", "required", "reset", "resets", "shows", "skips", "state", "submit", "submits", "succeeds", "takes", "that", "the", "there", "through", "too", "validation", "value", "visible", "when", "which", "without"], registryDependencies: [] }, { name: "highlight", type: "registry:lib", description: "Lines of colored tokens, with Shiki loaded on demand. Runs anywhere: Code calls it in the\nbrowser, and a Server Component or a build can call it ahead of time and pass the result as\n`tokens`, so the browser never loads Shiki. Null for unknown languages.", exports: ["highlightCode", "languageOf"], keywords: ["ahead", "and", "anywhere", "back", "browser", "build", "call", "calls", "can", "code", "colored", "component", "demand", "extension", "falls", "file", "for", "from", "language", "languages", "lines", "loaded", "loads", "name", "never", "null", "pass", "result", "runs", "server", "shiki", "the", "time", "tokens", "tsx", "unknown", "with"], registryDependencies: [] }, { name: "hover-card", type: "registry:ui", title: "Hover Card", description: "Rich preview (profile, product, clip) that opens after a hover delay or keyboard focus. The pointer can move into the card; supplementary only.", exports: ["HoverCard"], keywords: ["acts", "align", "alignment", "avatar", "before", "bio", "can", "card", "children", "clip", "closedelay", "closes", "content", "first", "flips", "focus", "hover", "into", "keyboard", "leaves", "link", "move", "must", "once", "only", "opendelay", "opens", "pointer", "preferred", "preview", "product", "profile", "rich", "room", "second", "side", "stats", "supplementary", "tap", "the", "there", "time", "touch", "trigger", "usually", "when", "with", "without", "work"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "input", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["fieldClass", "Input"], keywords: ["and", "aria", "chrome", "field", "filled", "group", "handed", "inputgroup", "inside", "the", "works"], registryDependencies: ["field", "utils"] }, { name: "input-group", type: "registry:ui", title: "Input Group", description: "Server-safe wrapper that fuses an Input with icons, text (https://, USD) or buttons into one field with a shared border and focus ring. Clicking an addon focuses the input.", exports: ["InputGroup", "InputGroupAddon"], keywords: ["addons", "align", "and", "beside", "border", "button", "buttons", "clicking", "control", "data", "draws", "drops", "field", "focus", "focuses", "group", "https", "icon", "icons", "inner", "input", "its", "like", "made", "matched", "one", "own", "plain", "plus", "ring", "side", "sits", "slot", "text", "the"], registryDependencies: ["utils"] }, { name: "input-otp", type: "registry:ui", title: "Input OTP", description: "One-time code in separate slots, backed by one real input: paste and SMS autofill work, screen readers hear one field. Grouped slots, numeric or alphanumeric, characters spring in, onComplete to submit. verified turns each character into a check, then merges them into one mark.", exports: ["InputOTP"], keywords: ["accepted", "again", "allowed", "alphanumeric", "and", "announced", "are", "autocomplete", "autofill", "back", "beside", "between", "called", "change", "character", "characters", "charset", "check", "code", "controlled", "counts", "dash", "defaultvalue", "draws", "dropped", "each", "edit", "every", "false", "filled", "fires", "first", "for", "group", "groups", "input", "into", "label", "laid", "length", "mark", "merge", "merged", "number", "once", "oncomplete", "one", "only", "onvaluechange", "others", "over", "pair", "paste", "pasted", "per", "read", "readers", "real", "render", "screen", "set", "shown", "single", "sits", "slides", "slot", "slots", "sms", "submit", "the", "then", "time", "transparently", "turns", "typed", "uncontrolled", "uppercased", "value", "verified", "verifiedlabel", "via", "visual", "was", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "kbd", type: "registry:ui", title: "Kbd", description: "Server-safe key caps for shortcut hints. KbdGroup nests keys per the HTML spec.", exports: ["Kbd", "KbdGroup"], keywords: ["cap", "html", "kbd", "kbdgroup", "key", "keys", "made", "nested", "one", "per", "several", "shortcut", "spec", "the"], registryDependencies: ["utils"] }, { name: "label", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Label"], keywords: [], registryDependencies: ["utils"] }, { name: "list", type: "registry:lib", description: "The item a navigation key moves to in a list, from the item at `index` (-1: none active).\nUndefined when the key doesn't navigate.", exports: ["listKeyTarget", "scrollIntoList"], keywords: ["active", "box", "can", "doesn", "enough", "from", "index", "item", "just", "key", "list", "moves", "moving", "navigate", "navigation", "none", "page", "scrollintoview", "scrolls", "show", "the", "undefined", "way", "when", "without"], registryDependencies: ["utils"] }, { name: "marker", type: "registry:ui", title: "Marker", description: "Highlighter-pen <mark> in four tints that wraps cleanly across lines and can draw itself on, left to right, when it scrolls into view. Highlight marks every match of a search in a string, for transcripts and search results.", exports: ["Marker", "Highlight"], keywords: ["across", "before", "case", "cleanly", "color", "delay", "draw", "drawing", "draws", "every", "first", "full", "fully", "highlight", "highlighter", "insensitive", "inside", "instant", "into", "left", "lines", "mark", "markers", "marks", "match", "motion", "one", "paragraph", "pen", "query", "reduced", "right", "scrolls", "search", "seconds", "several", "shown", "stagger", "sweeps", "term", "terms", "text", "the", "time", "view", "with", "without", "wraps"], registryDependencies: ["utils"] }, { name: "menubar", type: "registry:ui", title: "Menubar", description: "Desktop-style menu bar on the WAI-ARIA menubar pattern: one menu open at a time, hovering the next title while one is open switches to it, ← → walk the bar with or without a menu open, one Tab stop. A single highlight springs between titles; menus reuse every DropdownMenu item.", exports: ["MenubarContent", "MenubarItem", "MenubarCheckboxItem", "MenubarRadioGroup", "MenubarRadioItem", "MenubarLabel", "MenubarSeparator", "Menubar", "MenubarMenu", "MenubarTrigger"], keywords: ["actions", "align", "aligned", "analytics", "and", "another", "apart", "bar", "between", "but", "called", "check", "checked", "children", "click", "closes", "desktop", "destructive", "disabled", "down", "dropdownmenuradioitem", "edge", "enter", "equals", "first", "for", "generated", "give", "group", "groups", "highlight", "hint", "hovering", "icon", "irreversible", "item", "items", "its", "label", "labels", "last", "leading", "left", "lines", "mark", "menu", "menubarcontent", "menubartrigger", "menus", "move", "new", "not", "omitted", "oncheckedchange", "one", "onselect", "onvaluechange", "open", "opens", "optional", "optionally", "picked", "red", "right", "selectable", "separators", "shortcut", "shown", "shows", "space", "springs", "stable", "state", "style", "switches", "tell", "tests", "the", "then", "time", "title", "titles", "toggles", "trigger", "value", "variant", "when", "while", "with"], registryDependencies: ["dropdown-menu", "list", "use-indicator", "utils"] }, { name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", exports: ["MessageBubble"], keywords: ["above", "accent", "after", "and", "animated", "are", "assistant", "attachments", "avatar", "bubble", "chat", "children", "code", "content", "dots", "from", "inline", "left", "lists", "message", "name", "next", "rich", "right", "sender", "shown", "shows", "side", "size", "slot", "streaming", "surface", "text", "the", "time", "typing", "user"], registryDependencies: ["utils"] }, { name: "meter", type: "registry:ui", title: "Meter", description: "Server-safe gauge for a value in a known range (storage, quota, server load, password strength). Colors by <meter> thresholds, or stacked segments with a legend, like a storage bar.", exports: ["Meter"], keywords: ["above", "accessible", "also", "and", "away", "bar", "below", "between", "bound", "color", "colors", "decides", "defaults", "disk", "follow", "good", "green", "high", "known", "label", "legend", "like", "load", "low", "lower", "max", "measurement", "meter", "min", "name", "not", "one", "optimum", "orange", "overrides", "parts", "password", "progress", "quota", "range", "red", "region", "right", "segments", "server", "stacked", "storage", "strength", "sum", "task", "text", "the", "their", "this", "threshold", "thresholds", "total", "two", "upper", "use", "used", "value", "valuetext", "visible", "where", "with", "within"], registryDependencies: ["utils"] }, { name: "motion-provider", type: "registry:component", exports: ["MotionProvider"], keywords: [], registryDependencies: ["utils"] }, { name: "native-select", type: "registry:ui", title: "Native Select", description: "A real <select> with option groups: native picker on phones, no-JS forms, native required validation. Where customizable select is supported (appearance: base-select), the open list gets the menu look.", exports: ["NativeSelect", "NativeSelectGroup", "NativeSelectOption"], keywords: ["appearance", "base", "browser", "custom", "customizable", "elsewhere", "forms", "gets", "group", "heading", "icons", "instead", "invalid", "label", "labeled", "list", "look", "menu", "native", "need", "open", "optgroup", "options", "phones", "picker", "real", "rendering", "required", "select", "stays", "supports", "the", "use", "user", "validation", "when", "where", "without", "works"], registryDependencies: ["field", "input", "utils"] }, { name: "navigation-menu", type: "registry:ui", title: "Navigation Menu", description: "Website top navigation on the APG disclosure pattern: plain links and panels of rich links. Hover opens after a beat, then sliding along the bar switches panels at once; every panel shares one viewport that springs to the next panel's size and position while the content slides in from the side you moved toward. Click, Enter, ↓, ← → and Esc work; closed panels keep their links in the HTML for crawlers.", exports: ["NavigationMenu", "NavigationMenuList", "NavigationMenuItem", "NavigationMenuTrigger", "NavigationMenuContent", "NavigationMenuLink"], keywords: ["active", "after", "all", "along", "and", "aria", "bar", "beat", "bold", "called", "children", "classname", "click", "closed", "closes", "content", "controlled", "crawlers", "current", "defaultvalue", "description", "down", "enter", "entry", "esc", "find", "first", "focuses", "following", "for", "generated", "hidden", "hover", "html", "icon", "inset", "into", "item", "items", "its", "left", "level", "line", "link", "links", "look", "matched", "menu", "mouse", "muted", "navigation", "omitted", "once", "one", "onvaluechange", "open", "opened", "opens", "optional", "own", "padding", "page", "pair", "panel", "panels", "pill", "plain", "readers", "rem", "render", "rich", "right", "row", "selected", "single", "size", "sliding", "space", "springs", "stable", "stay", "still", "switches", "the", "them", "then", "tile", "title", "toggle", "top", "trigger", "uncontrolled", "under", "value", "viewport", "walk", "website", "while", "with", "your"], registryDependencies: ["focus", "use-controllable-state", "use-dismiss", "use-indicator", "utils"] }, { name: "newsletter", type: "registry:block", title: "Newsletter", description: "Email signup: InputGroup with the submit Button inside, native email validation, a loading → success toast and an in-place success state on the button.", exports: ["Newsletter"], keywords: ["above", "address", "after", "and", "button", "check", "confirm", "confirms", "description", "email", "field", "for", "form", "heading", "inbox", "inputgroup", "inside", "into", "its", "line", "loading", "message", "native", "next", "onsubscribe", "place", "placeholder", "rejection", "resets", "resolves", "saved", "shows", "signup", "submit", "submitlabel", "success", "successmessage", "supporting", "text", "that", "the", "then", "title", "toast", "turns", "under", "validation", "when", "with", "your"], registryDependencies: ["button", "input", "input-group", "toast", "utils"] }, { name: "number-input", type: "registry:ui", title: "Number Input", description: "Native number field with −/+ buttons: arrow keys, min/max/step validation and form submit from the platform; clamps on blur, rounds to the step's precision.", exports: ["NumberInput"], keywords: ["amount", "and", "are", "arrows", "arrowup", "buttons", "called", "cart", "change", "controlled", "defaultvalue", "down", "field", "first", "form", "helpers", "highest", "keyboard", "lowest", "max", "min", "native", "new", "number", "onvaluechange", "pair", "pointer", "quantity", "render", "step", "submit", "tabindex", "the", "uncontrolled", "use", "users", "validation", "value", "with"], registryDependencies: ["form-value", "input", "input-group", "use-controllable-state", "utils"] }, { name: "overflow-focus", type: "registry:lib", description: `Drop inside a scroll container: while the container overflows it becomes keyboard-focusable
17856
18045
  (tabIndex 0) so arrow keys can scroll it, and it leaves the tab order again once it fits.
17857
- Renders a hidden span, so server components (Table) can use it as a child.`, exports: ["OverflowFocus"], keywords: ["again", "and", "arrow", "becomes", "can", "child", "components", "container", "drop", "fits", "focusable", "hidden", "inside", "keyboard", "keys", "leaves", "once", "order", "overflows", "renders", "scroll", "server", "span", "tab", "tabindex", "table", "the", "use", "while"], registryDependencies: [] }, { name: "pagination", type: "registry:ui", title: "Pagination", description: "Server-safe page links from page and total: previous/next, ellipsis gaps and a sliding active pill, with numbers that swipe the way you moved. Links by default; buttons with onPageChange. The row keeps a fixed width so it never jumps.", exports: ["Pagination", "pageRange"], keywords: ["active", "always", "and", "arrows", "bar", "become", "between", "buttons", "called", "changes", "client", "controls", "current", "default", "each", "ellipsis", "first", "for", "from", "gaps", "href", "index", "instead", "items", "its", "keeps", "kept", "key", "labels", "last", "length", "links", "mounted", "moved", "never", "next", "number", "numbers", "one", "only", "onpagechange", "page", "pagerange", "pages", "parents", "phones", "picked", "pill", "previous", "remounting", "render", "returns", "row", "same", "show", "siblings", "side", "slides", "slot", "stays", "swipe", "swipes", "text", "that", "the", "total", "unused", "url", "way", "width", "with", "without", "work", "you"], registryDependencies: ["button-variants", "pagination-motion", "utils"] }, { name: "pagination-motion", type: "registry:ui", description: "Scopes the active pill's `layoutId` so several paginations on a page don't share it.", exports: ["PaginationMotion", "PaginationActivePill", "PaginationNumber"], keywords: ["active", "behind", "changes", "children", "direction", "don", "from", "items", "its", "layoutid", "link", "moved", "next", "number", "outline", "page", "pagination", "paginations", "pill", "scopes", "several", "share", "side", "slot", "springs", "swipes", "that", "the", "toward", "travel", "value", "when"], registryDependencies: ["utils"] }, { name: "platform", type: "registry:lib", description: "False on the server and during hydration, true after: gates portals and other client-only output.", exports: ["useMounted", "useIsMac", "useMediaQuery"], keywords: ["after", "and", "assumes", "client", "corrects", "ctrl", "during", "false", "gates", "hydration", "live", "match", "media", "not", "only", "other", "output", "portals", "query", "render", "server", "shortcuts", "ssr", "the", "then", "true", "use", "where"], registryDependencies: [] }, { name: "popover", type: "registry:ui", title: "Popover", description: "Non-modal panel anchored to a Button for filters, pickers and quick edits. Focus moves in, Esc returns it, outside clicks close; Selects and menus opened inside don't count as outside.", exports: ["Popover", "PopoverTrigger", "PopoverContent"], keywords: ["above", "action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "both", "button", "called", "can", "check", "children", "click", "clicks", "closed", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "done", "edits", "emphasis", "end", "enter", "esc", "feedback", "field", "fill", "filters", "first", "flips", "focus", "focusable", "for", "form", "from", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "keep", "label", "left", "like", "loading", "loadinglabel", "modal", "moves", "never", "non", "not", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "outside", "padding", "pair", "panel", "past", "per", "pickers", "place", "popovercontent", "popovertrigger", "popup", "preferred", "primary", "quick", "render", "reserved", "resets", "resize", "returns", "room", "runs", "second", "shift", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "twice", "two", "uncontrolled", "unmount", "values", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "focus", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "pricing", type: "registry:block", title: "Pricing", description: "Plan cards with a monthly / yearly segmented switch. Prices roll on a spring in the direction they moved; the featured plan is outlined in the accent. Card, Badge, ToggleGroup, Button.", exports: ["Pricing"], keywords: ["and", "bcp", "billing", "cards", "code", "currency", "defaultperiod", "display", "down", "drop", "eur", "first", "for", "formatting", "iso", "locale", "monthly", "next", "number", "order", "period", "plan", "plans", "price", "prices", "render", "rise", "rolling", "segmented", "selected", "spring", "swap", "switch", "the", "they", "togglegroup", "when", "with", "yearly", "yearlynote"], registryDependencies: ["badge", "card", "toggle-group", "utils"] }, { name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", exports: ["ProductCard"], keywords: ["add", "addlabel", "are", "aspectratio", "badge", "bcp", "become", "button", "called", "card", "carousel", "code", "compareat", "currency", "current", "dots", "for", "formatting", "heading", "height", "href", "img", "iso", "label", "link", "locale", "major", "media", "name", "new", "number", "onaddtocart", "one", "original", "over", "page", "per", "photos", "pressed", "price", "product", "ratio", "sale", "several", "shop", "shows", "slide", "small", "square", "struck", "swipeable", "text", "the", "there", "through", "tile", "units", "when", "width", "with"], registryDependencies: ["aspect-ratio", "badge", "button", "carousel", "utils"] }, { name: "progress", type: "registry:ui", title: "Progress", description: "Accent bar that springs to each new value, with an optional label and percentage. Leave out value for an indeterminate sweep (static under reduced motion).", exports: ["Progress"], keywords: ["above", "accent", "accessible", "across", "allowed", "also", "aria", "bar", "colored", "duration", "each", "for", "frame", "indeterminate", "infinite", "its", "label", "layout", "max", "motion", "name", "new", "next", "omit", "one", "per", "percentage", "reduced", "scalex", "segment", "show", "showvalue", "size", "springs", "static", "sweeps", "that", "the", "there", "thickness", "tween", "under", "unknown", "value", "visible", "when", "work"], registryDependencies: ["utils"] }, { name: "quote", type: "registry:ui", title: "Quote", description: "Semantic figure, blockquote and figcaption. Plain, card and large variants, with author, subtitle, avatar, lang, typewriter and textSwap.", exports: ["Quote"], keywords: ["accent", "alt", "and", "array", "author", "avatar", "blockquote", "card", "children", "correct", "custom", "cycles", "delay", "display", "doe", "effect", "enables", "fades", "figcaption", "figure", "for", "into", "jane", "job", "lang", "language", "large", "line", "mark", "options", "pass", "plain", "pronunciation", "quote", "quoted", "quotes", "reader", "said", "screen", "scrolls", "secondary", "semantic", "seo", "size", "slot", "speed", "spring", "string", "strings", "subtitle", "supports", "surface", "swap", "text", "textswap", "the", "through", "title", "transitions", "typewriter", "under", "variant", "view", "when", "who", "with", "work", "wrote"], registryDependencies: ["text-swap", "typewriter", "utils"] }, { name: "radio-group", type: "registry:ui", title: "Radio Group", description: "Native radios in a fieldset with a legend: browser arrow keys and form submit, a dot that springs in, optional per-option descriptions, vertical or horizontal.", exports: ["RadioGroup", "RadioGroupItem"], keywords: ["adds", "and", "application", "aria", "arrow", "browser", "but", "called", "children", "conform", "controlled", "defaultvalue", "description", "disabled", "does", "enclosing", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "names", "native", "needs", "not", "omitted", "one", "onvaluechange", "option", "orientation", "pair", "picked", "radios", "render", "rendered", "required", "row", "selectable", "shown", "stack", "submit", "submitted", "the", "them", "uncontrolled", "under", "value", "visible", "when", "with", "without", "work"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "rating", type: "registry:ui", title: "Rating", description: "Star rating on native radios (arrow keys, forms) with hover preview, or readOnly display with fractional stars for averages.", exports: ["Rating"], keywords: ["and", "aria", "arrow", "called", "can", "chosen", "controlled", "defaults", "defaultvalue", "describedby", "disabled", "display", "else", "enclosing", "error", "field", "filled", "fills", "first", "for", "form", "forms", "fractions", "from", "group", "hidden", "hint", "hover", "ids", "image", "included", "inputs", "interactive", "invalid", "isn", "keys", "label", "lift", "max", "name", "native", "needs", "number", "only", "onvaluechange", "pair", "partially", "picked", "press", "preview", "radios", "rating", "readers", "readonly", "render", "renders", "required", "score", "screen", "sets", "size", "spring", "star", "stars", "styling", "submit", "submitted", "text", "the", "uncontrolled", "value", "visually", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "relative-time", type: "registry:ui", title: "Relative Time", description: "Self-updating “5 minutes ago” in a semantic <time> with the full date as its title. Server renders the absolute date (no hydration mismatch); one shared ticker for the whole page.", exports: ["RelativeTime"], keywords: ["absolute", "ago", "and", "both", "client", "crawlers", "current", "date", "default", "differ", "full", "get", "hydration", "its", "itself", "keeps", "locale", "minutes", "mismatch", "not", "now", "one", "page", "pass", "real", "relative", "render", "renders", "semantic", "server", "shared", "stored", "text", "that", "the", "there", "ticker", "time", "timestamp", "title", "with", "would"], registryDependencies: ["utils"] }, { name: "resizable", type: "registry:ui", title: "Resizable", description: "Split panes for admin layouts, docs and editors. Hairline dividers with a wide grab area follow the pointer; collapsible panels snap shut past half their minimum. Dividers are APG window splitters: arrows step, Home/End jump to the limits, Enter or double-click collapses with a spring. Groups nest.", exports: ["ResizablePanelGroup", "ResizablePanel", "ResizableHandle"], keywords: ["adds", "admin", "after", "already", "and", "apg", "are", "arrow", "arrows", "basis", "between", "click", "collapsed", "collapsedsize", "collapses", "collapsible", "component", "cookie", "css", "default", "defaultsize", "direction", "divider", "docs", "dom", "double", "drag", "dragging", "each", "editors", "end", "enter", "evenly", "final", "find", "flex", "follows", "for", "grip", "group", "half", "handle", "handles", "home", "horizontal", "hydration", "icon", "jump", "key", "keyboardstep", "keys", "largest", "layout", "layouts", "left", "limits", "maxsize", "middle", "minimum", "minsize", "neighbor", "one", "onlayout", "order", "others", "panels", "panes", "parents", "past", "per", "plain", "pointer", "press", "rail", "rest", "save", "server", "share", "shows", "shut", "side", "size", "sizes", "smallest", "snaps", "split", "splitters", "spring", "ssr", "stacked", "starting", "step", "the", "their", "them", "toggle", "toggles", "two", "until", "vertical", "visible", "what", "when", "window", "withhandle", "without", "work", "wrappers"], registryDependencies: ["utils"] }, { name: "reveal", type: "registry:ui", title: "Reveal", description: "Scroll reveal: content fades and rises on a spring the first time it enters the viewport.", exports: ["Reveal"], keywords: ["after", "and", "content", "delay", "entering", "fades", "first", "into", "offset", "place", "rises", "scrolls", "seconds", "spring", "start", "stays", "the", "time", "view", "visible", "wait"], registryDependencies: ["utils"] }, { name: "scroll-area", type: "registry:ui", title: "Scroll Area", description: "Overlay scrollbars: thin thumbs that fade in while scrolling or on hover, widen under the pointer and drag. Scrolling stays native; the labeled region is keyboard-focusable.", exports: ["ScrollArea"], keywords: ["also", "and", "aria", "axes", "can", "classes", "drag", "fade", "focus", "for", "get", "give", "goes", "height", "here", "horizontal", "hover", "itself", "keyboard", "label", "mouse", "names", "native", "one", "orientation", "overlay", "padding", "region", "scroll", "scrollable", "scrollbars", "scrolling", "stays", "takes", "that", "the", "thin", "thumb", "thumbs", "touch", "users", "viewport", "viewportclassname", "wheel", "while", "widen"], registryDependencies: ["utils"] }, { name: "search-input", type: "registry:ui", title: "Search Input", description: 'Search field built on Input: leading icon, spring-in clear button, Esc to clear, and an optional focus shortcut ("/" or mod+k) with a Kbd hint.', exports: ["SearchInput"], keywords: ["accessible", "already", "and", "anywhere", "aside", "border", "browser", "button", "called", "change", "clear", "cleared", "clears", "command", "controlled", "ctrl", "default", "defaultvalue", "elsewhere", "embedded", "esc", "every", "field", "find", "first", "focus", "focused", "focuses", "for", "from", "ghost", "hidden", "hint", "htmlfor", "icon", "inputs", "kbd", "key", "label", "mac", "mod", "name", "onvaluechange", "opens", "optional", "own", "pair", "palette", "pass", "query", "render", "ring", "search", "second", "shadow", "shortcut", "single", "skipped", "spring", "steps", "still", "surface", "the", "uncontrolled", "value", "variant", "when", "while", "with", "you"], registryDependencies: ["input", "kbd", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "segmented-tabs", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["SegmentedTabs"], keywords: ["accessible", "active", "also", "always", "aria", "behind", "called", "current", "for", "indicator", "item", "items", "label", "labels", "list", "marked", "name", "onchange", "one", "pressed", "segmented", "slides", "spring", "tab", "tabs", "the", "under", "used", "value", "values", "with"], registryDependencies: ["overflow-focus", "use-indicator", "utils"] }, { name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits and validates with the form.", exports: ["Select"], keywords: ["accessible", "also", "aria", "blocks", "called", "cancel", "choice", "closed", "controlled", "defaultvalue", "describedby", "disabled", "down", "end", "enter", "error", "esc", "field", "fill", "fills", "first", "flat", "for", "form", "from", "groups", "hint", "home", "htmlfor", "ids", "input", "invalid", "isn", "keyboard", "label", "labeled", "let", "like", "listbox", "matches", "name", "native", "needs", "nothing", "onvaluechange", "opening", "options", "pair", "pick", "picked", "placeholder", "render", "required", "select", "sets", "single", "space", "styled", "styling", "submit", "submitted", "text", "the", "there", "trigger", "typeahead", "uncontrolled", "value", "visible", "when", "while", "with"], registryDependencies: ["field", "form-value", "input", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "separator", type: "registry:ui", title: "Separator", description: 'Server-safe hairline, horizontal or vertical, decorative by default, with an optional centered label like "or".', exports: ["Separator"], keywords: ["assistive", "centered", "content", "decorative", "default", "direction", "false", "for", "horizontal", "label", "line", "only", "orientation", "purely", "separates", "set", "tech", "text", "the", "visual", "when"], registryDependencies: ["utils"] }, { name: "sidebar", type: "registry:ui", title: "Sidebar", description: "App navigation from data: header, groups (optionally foldable), badges and pinned footer entries. Collapses to an icon rail on a spring (⌘B / Ctrl+B) with labels kept for assistive tech; below md it is a left drawer.", exports: ["SidebarLayout", "Sidebar", "SidebarTrigger"], keywords: ["a11y", "accessible", "and", "aria", "below", "bottom", "but", "called", "client", "clip", "collapsed", "collapses", "controlled", "ctrl", "current", "data", "default", "defaultcollapsed", "drawer", "entries", "field", "footer", "for", "from", "groups", "header", "highlighted", "holds", "icon", "ignored", "item", "label", "labels", "landmark", "lays", "left", "links", "name", "navigation", "oncollapsedchange", "onselect", "page", "pair", "picked", "pinned", "rail", "routing", "settings", "shortcut", "side", "sidebar", "slot", "spring", "start", "state", "stay", "the", "toggle", "too", "top", "tree", "trigger", "true", "typing", "uncontrolled", "when", "while", "with", "workspace"], registryDependencies: ["button", "collapsible", "drawer", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "site-header", type: "registry:block", title: "Site Header", description: "Sticky, translucent site header from one nav array: brand, NavigationMenu panels from md up, a right Drawer with the same links below it, and your actions on the right. The hairline underneath appears only once content scrolls beneath it.", exports: ["SiteHeader"], keywords: ["accessible", "acme", "actions", "alike", "and", "appears", "aria", "below", "brand", "client", "close", "controls", "current", "demo", "desktop", "drawer", "every", "for", "from", "hairline", "hand", "header", "href", "label", "left", "level", "link", "links", "logo", "marks", "matching", "name", "nav", "navigation", "navigationmenu", "once", "onclick", "only", "onnavigate", "over", "page", "panels", "right", "routing", "same", "scrolls", "search", "shown", "sign", "site", "sticky", "stub", "the", "theme", "top", "under", "width", "with"], registryDependencies: ["button", "drawer", "navigation-menu", "use-disclosure", "utils"] }, { name: "skeleton", type: "registry:ui", title: "Skeleton", description: 'Server-safe loading placeholders that pulse (static under reduced motion). SkeletonGroup announces one "Loading" to screen readers.', exports: ["Skeleton", "SkeletonGroup"], keywords: ["announced", "block", "classes", "classname", "hear", "instead", "label", "loading", "nothing", "one", "placeholder", "readers", "screen", "shown", "size", "skeleton", "skeletons", "this", "while", "with", "wrap"], registryDependencies: ["utils"] }, { name: "slider", type: "registry:ui", title: "Slider", description: "Slider over native range inputs: thin accent track, white knob that lifts while held. One value or two for a range; keyboard, forms, touch and Field wiring come from the platform.", exports: ["Slider"], keywords: ["accent", "accessible", "and", "aren", "aria", "arrow", "blocks", "browser", "called", "defaults", "defaultvalue", "describedby", "disabled", "does", "drag", "dragging", "error", "every", "field", "fills", "filter", "first", "for", "forms", "from", "held", "highest", "hint", "ids", "increment", "inputs", "invalid", "keyboard", "keys", "knob", "labels", "lifts", "lowest", "max", "maximum", "min", "minimum", "name", "names", "native", "one", "onvaluechange", "over", "per", "price", "range", "readers", "render", "screen", "sets", "slider", "step", "styling", "submitted", "text", "that", "the", "thin", "thumb", "touch", "track", "twice", "two", "uncontrolled", "value", "values", "while", "white", "with", "work"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "spinner", type: "registry:ui", title: "Spinner", description: "Server-safe activity indicator: eight spokes in currentColor, three sizes, its own status label (or silent inside a parent that announces). Holds still under reduced motion.", exports: ["Spinner"], keywords: ["accessible", "activity", "already", "announces", "busy", "currentcolor", "diameter", "eight", "holds", "indicator", "label", "motion", "name", "parent", "pass", "reduced", "size", "spokes", "state", "still", "the", "under", "when"], registryDependencies: ["utils"] }, { name: "stat-tile", type: "registry:block", title: "Stat Tile", description: "Admin dashboard metric: value, a delta Badge that is green when the change is good news (invert for errors and latency), and an area Chart of the trend. Server-safe.", exports: ["StatTile"], keywords: ["admin", "and", "area", "axis", "badge", "below", "change", "chart", "dashboard", "data", "datakey", "delta", "down", "each", "errors", "for", "format", "formatted", "good", "green", "headline", "index", "intl", "invert", "key", "label", "last", "latency", "metric", "metrics", "month", "name", "news", "number", "period", "positive", "revenue", "row", "rows", "the", "tile", "tooltip", "trend", "unless", "value", "when", "where"], registryDependencies: ["badge", "card", "chart", "utils"] }, { name: "stats-strip", type: "registry:block", title: "Stats Strip", description: "Headline numbers, each card a <dl>: cards rise in turn with Reveal, then each number springs up with CountTo. Final values are in the HTML.", exports: ["StatsStrip"], keywords: ["card", "display", "each", "headline", "its", "number", "numbers", "order", "rises", "row", "springs", "stats", "then", "turn"], registryDependencies: ["card", "count-to", "reveal", "utils"] }, { name: "stepper", type: "registry:ui", title: "Stepper", description: "Multi-step form (questionnaire, checkout, setup wizard). Steps stay mounted so values survive Back and submit together; Continue checks only the current step's native constraints, plus an optional async onNext guard with a spinner. Progress header with springing connectors; works with onSubmit or a Server Action.", exports: ["Stepper", "StepperList", "StepperStep", "StepperFooter"], keywords: ["action", "after", "and", "are", "back", "backlabel", "button", "called", "check", "checks", "children", "connector", "constraints", "continue", "controlled", "current", "defaultvalue", "description", "every", "false", "fields", "fills", "finished", "first", "for", "form", "header", "hidden", "inactive", "its", "last", "links", "matches", "mounted", "multi", "native", "new", "nextlabel", "not", "one", "only", "onnext", "onsubmit", "onvaluechange", "optional", "order", "pass", "progress", "render", "resolve", "return", "runs", "server", "shown", "shows", "spinner", "spring", "start", "stay", "stays", "step", "steps", "submit", "submitlabel", "submits", "survive", "text", "the", "through", "title", "together", "uncontrolled", "unmounted", "value", "values", "while", "with"], registryDependencies: ["button", "focus", "use-controllable-state", "utils"] }, { name: "styles", type: "registry:file", exports: [], keywords: [], registryDependencies: [] }, { name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Switch"], keywords: ["called", "checkbox", "checked", "controlled", "defaultchecked", "first", "knob", "native", "new", "oncheckedchange", "pair", "render", "role", "spring", "state", "switch", "the", "uncontrolled", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "table", type: "registry:ui", title: "Table", description: "Server-safe semantic <table> in a scrolling card: header, body, footer and caption, striped rows, tinted selected rows, and sortable headers that set aria-sort and flip their arrow.", exports: ["Table", "TableHeader", "TableBody", "TableFooter", "TableRow", "TableHead", "TableCell", "TableCaption"], keywords: ["alternating", "and", "aria", "around", "arrow", "button", "called", "card", "classes", "column", "containerclassname", "data", "direction", "for", "header", "keyboard", "makes", "narrow", "onsort", "pass", "pressed", "real", "requires", "row", "screens", "scrollable", "scrolling", "scrolls", "selected", "sets", "shading", "shows", "sideways", "sort", "sortable", "state", "striped", "table", "that", "the", "then", "tint", "when"], registryDependencies: ["overflow-focus", "utils"] }, { name: "tabs", type: "registry:ui", title: "Tabs", description: "WAI-ARIA tabs with arrow, Home and End keys. One indicator springs between tabs, segmented or underline. Inactive panels stay in the DOM for SEO; the active one fades in.", exports: ["Tabs", "TabsList", "TabsTrigger", "TabsContent"], keywords: ["activated", "active", "always", "and", "aria", "arrows", "bar", "behind", "called", "children", "content", "controlled", "crawlable", "defaultvalue", "dom", "end", "first", "focus", "hidden", "home", "html", "inactive", "indicator", "labels", "matches", "move", "muted", "one", "onvaluechange", "pair", "panel", "panels", "pill", "remains", "render", "segmented", "springs", "stay", "stays", "tab", "tabs", "tabscontent", "tabstrigger", "the", "their", "track", "uncontrolled", "under", "underline", "value", "variant", "wai", "while", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, ← walks onto the tags and Delete removes one, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. Submits one entry per tag; required needs one.", exports: ["tagFieldClass", "Tag", "useTagKeys", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "call", "called", "children", "chip", "commit", "controlled", "defaultvalue", "delete", "disabled", "each", "empty", "enter", "esc", "every", "false", "field", "first", "focused", "for", "form", "free", "from", "frominput", "hides", "input", "inside", "its", "keyboard", "keydown", "keys", "last", "least", "limit", "list", "many", "max", "mode", "most", "move", "name", "needs", "one", "onkeydown", "onremove", "onto", "onvaluechange", "order", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "put", "reach", "reject", "removable", "remove", "removeref", "removes", "render", "rendered", "required", "return", "returns", "runs", "separator", "separators", "shows", "space", "split", "springs", "stay", "stays", "stops", "submits", "tab", "tag", "tagref", "tags", "text", "that", "the", "them", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "usetagkeys", "validate", "value", "walks", "when", "which", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "terminal", type: "registry:ui", title: "Terminal", description: "Terminal window. Snippet mode types install commands once scrolled into view, with the full transcript kept in the DOM for SEO and no-JS; log mode streams lines into a live region that follows the bottom unless you scroll up, then offers a jump back. Status lines carry glyphs, copy grabs the commands.", exports: ["Terminal"], keywords: ["and", "append", "arrives", "back", "before", "body", "bodyclassname", "bottom", "character", "clipboard", "commands", "copy", "copytext", "defaults", "dom", "false", "follows", "for", "full", "hides", "install", "jump", "lines", "live", "log", "mode", "new", "offering", "once", "order", "output", "per", "prompt", "puts", "read", "reveals", "role", "scroll", "scrolling", "seo", "size", "snippet", "snippets", "speed", "stays", "sticks", "stream", "streams", "symbol", "terminal", "text", "the", "title", "typed", "types", "typing", "unless", "view", "what", "when", "window", "you"], registryDependencies: ["app-window", "code", "platform", "utils"] }, { name: "testimonial", type: "registry:block", title: "Testimonial", description: "A large Quote with its author, and everyone else who agrees as an overlapping AvatarGroup. Server-safe.", exports: ["Testimonial"], keywords: ["agrees", "and", "author", "avatar", "avatargroup", "behind", "beside", "carried", "children", "company", "crowd", "else", "everyone", "few", "first", "fold", "initials", "into", "its", "lang", "language", "large", "line", "one", "others", "otherslabel", "photo", "quote", "rest", "role", "said", "show", "stack", "streamers", "subtitle", "the", "trusted", "under", "url", "who", "with", "without"], registryDependencies: ["avatar", "avatar-group", "quote", "utils"] }, { name: "text-swap", type: "registry:ui", title: "TextSwap", description: "Spring-powered text morphing and ticker. Auto-cycles phrases or animates whenever children change with blur and slide transitions.", exports: ["TextSwap"], keywords: ["active", "array", "automatically", "between", "changing", "child", "children", "controlling", "cycle", "cycling", "default", "direction", "externally", "hover", "index", "interval", "items", "milliseconds", "mouse", "new", "pass", "pause", "pauseonhover", "single", "string", "swap", "swaps", "text", "the", "through", "true", "when"], registryDependencies: ["utils"] }, { name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Textarea"], keywords: ["auto", "cap", "content", "corner", "end", "field", "firefox", "grows", "max", "maxlength", "near", "needs", "red", "resizable", "show", "showcount", "sizing", "the", "then", "turns", "unsupported", "used", "warning", "where", "with"], registryDependencies: ["field", "form-value", "input", "utils"] }, { name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", exports: ["isDark", "setTheme", "subscribeTheme", "accents", "getAccent", "setAccent", "themeScript"], keywords: ["and", "app", "focus", "like", "persists", "primary", "retints", "rings", "selection", "switches", "the", "theme", "wide"], registryDependencies: [] }, { name: "theme-toggle", type: "registry:ui", title: "Theme Toggle", description: "Light/dark icon switch with spring rotate-swap; follows the OS until first use.", exports: ["ThemeToggle"], keywords: ["button", "click", "first", "follows", "icon", "moon", "rotate", "spring", "sun", "swap", "until"], registryDependencies: ["button", "theme", "utils"] }, { name: "timeline", type: "registry:ui", title: "Timeline", description: "Server-safe ordered list of events on a rail: icon or dot markers with status colors, machine-readable <time>, title and an optional body. For changelogs, experience and mod logs.", exports: ["Timeline", "TimelineItem"], keywords: ["above", "and", "body", "children", "color", "date", "datetime", "dot", "event", "events", "first", "for", "headline", "icon", "inside", "list", "machine", "marker", "newest", "oldest", "one", "optional", "order", "ordered", "pair", "rail", "readable", "render", "shown", "small", "the", "then", "time", "title", "variant", "with", "without", "yours"], registryDependencies: ["utils"] }, { name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover or focus, and F8 jumps to the newest toast from the keyboard.", exports: ["toast", "Toaster"], keywords: ["action", "back", "buttons", "description", "dismiss", "dismisses", "duration", "error", "esc", "expands", "focus", "goes", "hover", "jumps", "layout", "loading", "mount", "newest", "once", "pause", "promise", "root", "saved", "stacked", "success", "swipe", "tap", "the", "there", "timers", "title", "toast", "toasts", "while"], registryDependencies: ["focus", "use-dismiss", "use-hotkey", "utils"] }, { name: "toggle-group", type: "registry:ui", title: "Toggle Group", description: "Segmented control of toggle buttons. Single mode slides one pill to the pressed item (view mode, billing period); multiple mode toggles items independently (bold, italic). Arrow keys move between items.", exports: ["ToggleGroup", "ToggleGroupItem"], keywords: ["always", "and", "aria", "arrow", "behind", "between", "billing", "bold", "buttons", "called", "choice", "control", "controlled", "defaultvalue", "end", "every", "filters", "first", "height", "home", "icon", "independent", "italic", "item", "items", "keys", "label", "labels", "mode", "move", "multiple", "need", "one", "only", "onvaluechange", "pair", "period", "pill", "pressed", "render", "reported", "segment", "segmented", "set", "single", "size", "slides", "springs", "the", "time", "toggle", "toggles", "type", "uncontrolled", "value", "values", "view", "when", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tooltip", type: "registry:ui", title: "Tooltip", description: "Short label on hover or keyboard focus, also wired as the trigger's aria-describedby. Warm-up: once one opens, neighbors open instantly. Hoverable, Esc dismisses, ignores touch.", exports: ["Tooltip"], keywords: ["accessible", "action", "align", "alignment", "also", "and", "aria", "becomes", "before", "button", "children", "content", "control", "default", "delay", "description", "dismisses", "element", "esc", "first", "flips", "focus", "focusable", "for", "hold", "hover", "how", "icon", "keyboard", "label", "one", "open", "opens", "own", "pointer", "preferred", "press", "reaches", "room", "short", "shows", "side", "stays", "still", "tap", "text", "the", "their", "there", "toggles", "tooltip", "touch", "trigger", "triggers", "wcag", "when", "while", "with"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "typewriter", type: "registry:ui", title: "Typewriter", description: "Types out text character-by-character with optional phrase cycling and pulsing cursor. Full copy stays in the DOM for SEO.", exports: ["Typewriter"], keywords: ["and", "array", "before", "blinking", "called", "character", "classes", "cursor", "cursorchar", "cursorclassname", "custom", "cycle", "default", "delay", "deletespeed", "deleting", "dom", "effect", "for", "full", "fully", "headers", "hero", "loop", "milliseconds", "once", "oncomplete", "pause", "pauseduration", "per", "phrase", "phrases", "quotes", "seo", "show", "speed", "starting", "stays", "text", "the", "through", "true", "type", "typed", "typewriter", "typing", "wait", "when", "whether", "while"], registryDependencies: ["platform", "utils"] }, { name: "use-anchor", type: "registry:hook", description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
18046
+ Renders a hidden span, so server components (Table) can use it as a child.`, exports: ["OverflowFocus"], keywords: ["again", "and", "arrow", "becomes", "can", "child", "components", "container", "drop", "fits", "focusable", "hidden", "inside", "keyboard", "keys", "leaves", "once", "order", "overflows", "renders", "scroll", "server", "span", "tab", "tabindex", "table", "the", "use", "while"], registryDependencies: [] }, { name: "pagination", type: "registry:ui", title: "Pagination", description: "Server-safe page links from page and total: previous/next, ellipsis gaps and a sliding active pill, with numbers that swipe the way you moved. Links by default; buttons with onPageChange. The row keeps a fixed width so it never jumps.", exports: ["Pagination", "pageRange"], keywords: ["active", "always", "and", "arrows", "bar", "become", "between", "buttons", "called", "changes", "client", "controls", "current", "default", "each", "ellipsis", "first", "for", "from", "gaps", "href", "index", "instead", "items", "its", "keeps", "kept", "key", "labels", "last", "length", "links", "mounted", "moved", "never", "next", "number", "numbers", "one", "only", "onpagechange", "page", "pagerange", "pages", "parents", "phones", "picked", "pill", "previous", "remounting", "render", "returns", "row", "same", "show", "siblings", "side", "slides", "slot", "stays", "swipe", "swipes", "text", "that", "the", "total", "unused", "url", "way", "width", "with", "without", "work", "you"], registryDependencies: ["button-variants", "pagination-motion", "utils"] }, { name: "pagination-motion", type: "registry:ui", description: "Scopes the active pill's `layoutId` so several paginations on a page don't share it.", exports: ["PaginationMotion", "PaginationActivePill", "PaginationNumber"], keywords: ["active", "behind", "changes", "children", "direction", "don", "from", "items", "its", "layoutid", "link", "moved", "next", "number", "outline", "page", "pagination", "paginations", "pill", "scopes", "several", "share", "side", "slot", "springs", "swipes", "that", "the", "toward", "travel", "value", "when"], registryDependencies: ["utils"] }, { name: "platform", type: "registry:lib", description: "False on the server and during hydration, true after: gates portals and other client-only output.", exports: ["useMounted", "useIsMac", "useMediaQuery"], keywords: ["after", "and", "assumes", "client", "corrects", "ctrl", "during", "false", "gates", "hydration", "live", "match", "media", "not", "only", "other", "output", "portals", "query", "render", "server", "shortcuts", "ssr", "the", "then", "true", "use", "where"], registryDependencies: [] }, { name: "popover", type: "registry:ui", title: "Popover", description: "Non-modal panel anchored to a Button for filters, pickers and quick edits. Focus moves in, Esc returns it, outside clicks close; Selects and menus opened inside don't count as outside.", exports: ["Popover", "PopoverTrigger", "PopoverContent"], keywords: ["above", "action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "both", "button", "called", "can", "check", "children", "click", "clicks", "closed", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "done", "edits", "emphasis", "end", "enter", "esc", "feedback", "field", "fill", "filters", "first", "flips", "focus", "focusable", "for", "form", "from", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "keep", "label", "left", "like", "loading", "loadinglabel", "modal", "moves", "never", "non", "not", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "outside", "padding", "pair", "panel", "past", "per", "pickers", "place", "popovercontent", "popovertrigger", "popup", "preferred", "primary", "quick", "render", "reserved", "resets", "resize", "returns", "room", "runs", "second", "shift", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "twice", "two", "uncontrolled", "unmount", "values", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "focus", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "pricing", type: "registry:block", title: "Pricing", description: "Plan cards with a monthly / yearly segmented switch. Prices roll on a spring in the direction they moved; the featured plan is outlined in the accent. Card, Badge, ToggleGroup, Button.", exports: ["Pricing"], keywords: ["and", "bcp", "billing", "cards", "code", "currency", "defaultperiod", "display", "down", "drop", "eur", "first", "for", "formatting", "iso", "locale", "monthly", "next", "number", "order", "period", "plan", "plans", "price", "prices", "render", "rise", "rolling", "segmented", "selected", "spring", "swap", "switch", "the", "they", "togglegroup", "when", "with", "yearly", "yearlynote"], registryDependencies: ["badge", "card", "toggle-group", "utils"] }, { name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", exports: ["ProductCard"], keywords: ["add", "addlabel", "are", "aspectratio", "badge", "bcp", "become", "button", "called", "card", "carousel", "code", "compareat", "currency", "current", "dots", "for", "formatting", "heading", "height", "href", "img", "iso", "label", "link", "locale", "major", "media", "name", "new", "number", "onaddtocart", "one", "original", "over", "page", "per", "photos", "pressed", "price", "product", "ratio", "sale", "several", "shop", "shows", "slide", "small", "square", "struck", "swipeable", "text", "the", "there", "through", "tile", "units", "when", "width", "with"], registryDependencies: ["aspect-ratio", "badge", "button", "carousel", "utils"] }, { name: "progress", type: "registry:ui", title: "Progress", description: "Accent bar that springs to each new value, with an optional label and percentage. Leave out value for an indeterminate sweep (static under reduced motion).", exports: ["Progress"], keywords: ["above", "accent", "accessible", "across", "allowed", "also", "aria", "bar", "colored", "duration", "each", "for", "frame", "indeterminate", "infinite", "its", "label", "layout", "max", "motion", "name", "new", "next", "omit", "one", "per", "percentage", "reduced", "scalex", "segment", "show", "showvalue", "size", "springs", "static", "sweeps", "that", "the", "there", "thickness", "tween", "under", "unknown", "value", "visible", "when", "work"], registryDependencies: ["utils"] }, { name: "quote", type: "registry:ui", title: "Quote", description: "Semantic figure, blockquote and figcaption. Plain, card and large variants, with author, subtitle, avatar, lang, typewriter and textSwap.", exports: ["Quote"], keywords: ["accent", "alt", "and", "array", "author", "avatar", "blockquote", "card", "children", "correct", "custom", "cycles", "delay", "display", "doe", "effect", "enables", "fades", "figcaption", "figure", "for", "into", "jane", "job", "lang", "language", "large", "line", "mark", "options", "pass", "plain", "pronunciation", "quote", "quoted", "quotes", "reader", "said", "screen", "scrolls", "secondary", "semantic", "seo", "size", "slot", "speed", "spring", "string", "strings", "subtitle", "supports", "surface", "swap", "text", "textswap", "the", "through", "title", "transitions", "typewriter", "under", "variant", "view", "when", "who", "with", "work", "wrote"], registryDependencies: ["text-swap", "typewriter", "utils"] }, { name: "radio-group", type: "registry:ui", title: "Radio Group", description: "Native radios in a fieldset with a legend: browser arrow keys and form submit, a dot that springs in, optional per-option descriptions, vertical or horizontal.", exports: ["RadioGroup", "RadioGroupItem"], keywords: ["adds", "and", "application", "aria", "arrow", "browser", "but", "called", "children", "conform", "controlled", "defaultvalue", "description", "disabled", "does", "enclosing", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "names", "native", "needs", "not", "omitted", "one", "onvaluechange", "option", "orientation", "pair", "picked", "radios", "render", "rendered", "required", "row", "selectable", "shown", "stack", "submit", "submitted", "the", "them", "uncontrolled", "under", "value", "visible", "when", "with", "without", "work"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "rating", type: "registry:ui", title: "Rating", description: "Star rating on native radios (arrow keys, forms) with hover preview, or readOnly display with fractional stars for averages.", exports: ["Rating"], keywords: ["and", "aria", "arrow", "called", "can", "chosen", "controlled", "defaults", "defaultvalue", "describedby", "disabled", "display", "else", "enclosing", "error", "field", "filled", "fills", "first", "for", "form", "forms", "fractions", "from", "group", "hidden", "hint", "hover", "ids", "image", "included", "inputs", "interactive", "invalid", "isn", "keys", "label", "lift", "max", "name", "native", "needs", "number", "only", "onvaluechange", "pair", "partially", "picked", "press", "preview", "radios", "rating", "readers", "readonly", "render", "renders", "required", "score", "screen", "sets", "size", "spring", "star", "stars", "styling", "submit", "submitted", "text", "the", "uncontrolled", "value", "visually", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "relative-time", type: "registry:ui", title: "Relative Time", description: "Self-updating “5 minutes ago” in a semantic <time> with the full date as its title. Server renders the absolute date (no hydration mismatch); one shared ticker for the whole page.", exports: ["RelativeTime"], keywords: ["absolute", "ago", "and", "both", "client", "crawlers", "current", "date", "default", "differ", "full", "get", "hydration", "its", "itself", "keeps", "locale", "minutes", "mismatch", "not", "now", "one", "page", "pass", "real", "relative", "render", "renders", "semantic", "server", "shared", "stored", "text", "that", "the", "there", "ticker", "time", "timestamp", "title", "with", "would"], registryDependencies: ["utils"] }, { name: "resizable", type: "registry:ui", title: "Resizable", description: "Split panes for admin layouts, docs and editors. Hairline dividers with a wide grab area follow the pointer; collapsible panels snap shut past half their minimum. Dividers are APG window splitters: arrows step, Home/End jump to the limits, Enter or double-click collapses with a spring. Groups nest.", exports: ["ResizablePanelGroup", "ResizablePanel", "ResizableHandle"], keywords: ["adds", "admin", "after", "already", "and", "apg", "are", "arrow", "arrows", "basis", "between", "click", "collapsed", "collapsedsize", "collapses", "collapsible", "component", "cookie", "css", "default", "defaultsize", "direction", "divider", "docs", "dom", "double", "drag", "dragging", "each", "editors", "end", "enter", "evenly", "final", "find", "flex", "follows", "for", "grip", "group", "half", "handle", "handles", "home", "horizontal", "hydration", "icon", "jump", "key", "keyboardstep", "keys", "largest", "layout", "layouts", "left", "limits", "maxsize", "middle", "minimum", "minsize", "neighbor", "one", "onlayout", "order", "others", "panels", "panes", "parents", "past", "per", "plain", "pointer", "press", "rail", "rest", "save", "server", "share", "shows", "shut", "side", "size", "sizes", "smallest", "snaps", "split", "splitters", "spring", "ssr", "stacked", "starting", "step", "the", "their", "them", "toggle", "toggles", "two", "until", "vertical", "visible", "what", "when", "window", "withhandle", "without", "work", "wrappers"], registryDependencies: ["utils"] }, { name: "reveal", type: "registry:ui", title: "Reveal", description: "Scroll reveal: content fades and rises on a spring the first time it enters the viewport.", exports: ["Reveal"], keywords: ["after", "and", "content", "delay", "entering", "fades", "first", "into", "offset", "place", "rises", "scrolls", "seconds", "spring", "start", "stays", "the", "time", "view", "visible", "wait"], registryDependencies: ["utils"] }, { name: "scroll-area", type: "registry:ui", title: "Scroll Area", description: "Overlay scrollbars: thin thumbs that fade in while scrolling or on hover, widen under the pointer and drag. Scrolling stays native; the labeled region is keyboard-focusable.", exports: ["ScrollArea"], keywords: ["also", "and", "aria", "axes", "can", "classes", "drag", "fade", "focus", "for", "get", "give", "goes", "height", "here", "horizontal", "hover", "itself", "keyboard", "label", "mouse", "names", "native", "one", "orientation", "overlay", "padding", "region", "scroll", "scrollable", "scrollbars", "scrolling", "stays", "takes", "that", "the", "thin", "thumb", "thumbs", "touch", "users", "viewport", "viewportclassname", "wheel", "while", "widen"], registryDependencies: ["utils"] }, { name: "search-input", type: "registry:ui", title: "Search Input", description: 'Search field built on Input: leading icon, spring-in clear button, Esc to clear, and an optional focus shortcut ("/" or mod+k) with a Kbd hint.', exports: ["SearchInput"], keywords: ["accessible", "already", "and", "anywhere", "aside", "border", "browser", "button", "called", "change", "clear", "cleared", "clears", "command", "controlled", "ctrl", "default", "defaultvalue", "elsewhere", "embedded", "esc", "every", "field", "find", "first", "focus", "focused", "focuses", "for", "from", "ghost", "hidden", "hint", "htmlfor", "icon", "inputs", "kbd", "key", "label", "mac", "mod", "name", "onvaluechange", "opens", "optional", "own", "pair", "palette", "pass", "query", "render", "ring", "search", "second", "shadow", "shortcut", "single", "skipped", "spring", "steps", "still", "surface", "the", "uncontrolled", "value", "variant", "when", "while", "with", "you"], registryDependencies: ["input", "kbd", "platform", "use-controllable-state", "use-hotkey", "utils"] }, { name: "segmented-tabs", type: "registry:ui", title: "AppWindow", description: "Window container with close / minimize / zoom controls, title, custom header slot and actions. SegmentedTabs is the compact view switcher for its title bar.", exports: ["SegmentedTabs"], keywords: ["accessible", "active", "also", "always", "aria", "behind", "called", "current", "for", "indicator", "item", "items", "label", "labels", "list", "marked", "name", "onchange", "one", "pressed", "segmented", "slides", "spring", "tab", "tabs", "the", "under", "used", "value", "values", "with"], registryDependencies: ["overflow-focus", "use-indicator", "utils"] }, { name: "select", type: "registry:ui", title: "Select", description: "Single-choice listbox styled like Input, with groups, icons and disabled options. Native-select keyboard (arrows, typeahead even while closed), flips above the trigger near the viewport edge, submits and validates with the form.", exports: ["Select"], keywords: ["accessible", "also", "aria", "blocks", "called", "cancel", "choice", "closed", "controlled", "defaultvalue", "describedby", "disabled", "down", "end", "enter", "error", "esc", "field", "fill", "fills", "first", "flat", "for", "form", "from", "groups", "hint", "home", "htmlfor", "ids", "input", "invalid", "isn", "keyboard", "label", "labeled", "let", "like", "listbox", "matches", "name", "native", "needs", "nothing", "onvaluechange", "opening", "options", "pair", "pick", "picked", "placeholder", "render", "required", "select", "sets", "single", "space", "styled", "styling", "submit", "submitted", "text", "the", "there", "trigger", "typeahead", "uncontrolled", "value", "visible", "when", "while", "with"], registryDependencies: ["field", "form-value", "input", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "separator", type: "registry:ui", title: "Separator", description: 'Server-safe hairline, horizontal or vertical, decorative by default, with an optional centered label like "or".', exports: ["Separator"], keywords: ["assistive", "centered", "content", "decorative", "default", "direction", "false", "for", "horizontal", "label", "line", "only", "orientation", "purely", "separates", "set", "tech", "text", "the", "visual", "when"], registryDependencies: ["utils"] }, { name: "sidebar", type: "registry:ui", title: "Sidebar", description: "App navigation from data: header, groups (optionally foldable), badges and pinned footer entries. Collapses to an icon rail on a spring (⌘B / Ctrl+B) with labels kept for assistive tech; below md it is a left drawer.", exports: ["SidebarLayout", "Sidebar", "SidebarTrigger"], keywords: ["a11y", "accessible", "and", "aria", "below", "bottom", "but", "called", "client", "clip", "collapsed", "collapses", "controlled", "ctrl", "current", "data", "default", "defaultcollapsed", "drawer", "entries", "field", "footer", "for", "from", "groups", "header", "highlighted", "holds", "icon", "ignored", "item", "label", "labels", "landmark", "lays", "left", "links", "name", "navigation", "oncollapsedchange", "onselect", "page", "pair", "picked", "pinned", "rail", "routing", "settings", "shortcut", "side", "sidebar", "slot", "spring", "start", "state", "stay", "the", "toggle", "too", "top", "tree", "trigger", "true", "typing", "uncontrolled", "when", "while", "with", "workspace"], registryDependencies: ["button", "collapsible", "drawer", "kbd", "platform", "tooltip", "use-controllable-state", "use-hotkey", "utils"] }, { name: "site-header", type: "registry:block", title: "Site Header", description: "Sticky, translucent site header from one nav array: brand, NavigationMenu panels from md up, a right Drawer with the same links below it, and your actions on the right. The hairline underneath appears only once content scrolls beneath it.", exports: ["SiteHeader"], keywords: ["accessible", "acme", "actions", "alike", "and", "appears", "aria", "below", "brand", "client", "close", "controls", "current", "demo", "desktop", "drawer", "every", "for", "from", "hairline", "hand", "header", "href", "label", "left", "level", "link", "links", "logo", "marks", "matching", "name", "nav", "navigation", "navigationmenu", "once", "onclick", "only", "onnavigate", "over", "page", "panels", "right", "routing", "same", "scrolls", "search", "shown", "sign", "site", "sticky", "stub", "the", "theme", "top", "under", "width", "with"], registryDependencies: ["button", "drawer", "navigation-menu", "use-disclosure", "utils"] }, { name: "skeleton", type: "registry:ui", title: "Skeleton", description: 'Server-safe loading placeholders that pulse (static under reduced motion). SkeletonGroup announces one "Loading" to screen readers.', exports: ["Skeleton", "SkeletonGroup"], keywords: ["announced", "block", "classes", "classname", "hear", "instead", "label", "loading", "nothing", "one", "placeholder", "readers", "screen", "shown", "size", "skeleton", "skeletons", "this", "while", "with", "wrap"], registryDependencies: ["utils"] }, { name: "slider", type: "registry:ui", title: "Slider", description: "Slider over native range inputs: thin accent track, white knob that lifts while held. One value or two for a range; keyboard, forms, touch and Field wiring come from the platform.", exports: ["Slider"], keywords: ["accent", "accessible", "and", "aren", "aria", "arrow", "blocks", "browser", "called", "defaults", "defaultvalue", "describedby", "disabled", "does", "drag", "dragging", "error", "every", "field", "fills", "filter", "first", "for", "forms", "from", "held", "highest", "hint", "ids", "increment", "inputs", "invalid", "keyboard", "keys", "knob", "labels", "lifts", "lowest", "max", "maximum", "min", "minimum", "name", "names", "native", "one", "onvaluechange", "over", "per", "price", "range", "readers", "render", "screen", "sets", "slider", "step", "styling", "submitted", "text", "that", "the", "thin", "thumb", "touch", "track", "twice", "two", "uncontrolled", "value", "values", "while", "white", "with", "work"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "spinner", type: "registry:ui", title: "Spinner", description: "Server-safe activity indicator: eight spokes in currentColor, three sizes, its own status label (or silent inside a parent that announces). Holds still under reduced motion.", exports: ["Spinner"], keywords: ["accessible", "activity", "already", "announces", "busy", "currentcolor", "diameter", "eight", "holds", "indicator", "label", "motion", "name", "parent", "pass", "reduced", "size", "spokes", "state", "still", "the", "under", "when"], registryDependencies: ["utils"] }, { name: "stat-tile", type: "registry:block", title: "Stat Tile", description: "Admin dashboard metric: value, a delta Badge that is green when the change is good news (invert for errors and latency), and an area Chart of the trend. Server-safe.", exports: ["StatTile"], keywords: ["admin", "and", "area", "axis", "badge", "below", "change", "chart", "dashboard", "data", "datakey", "delta", "down", "each", "errors", "for", "format", "formatted", "good", "green", "headline", "index", "intl", "invert", "key", "label", "last", "latency", "metric", "metrics", "month", "name", "news", "number", "period", "positive", "revenue", "row", "rows", "the", "tile", "tooltip", "trend", "unless", "value", "when", "where"], registryDependencies: ["badge", "card", "chart", "utils"] }, { name: "stats-strip", type: "registry:block", title: "Stats Strip", description: "Headline numbers, each card a <dl>: cards rise in turn with Reveal, then each number springs up with CountTo. Final values are in the HTML.", exports: ["StatsStrip"], keywords: ["card", "display", "each", "headline", "its", "number", "numbers", "order", "rises", "row", "springs", "stats", "then", "turn"], registryDependencies: ["card", "count-to", "reveal", "utils"] }, { name: "stepper", type: "registry:ui", title: "Stepper", description: "Multi-step form (questionnaire, checkout, setup wizard). Steps stay mounted so values survive Back and submit together; Continue checks only the current step's native constraints, plus an optional async onNext guard with a spinner. Progress header with springing connectors; works with onSubmit or a Server Action.", exports: ["Stepper", "StepperList", "StepperStep", "StepperFooter"], keywords: ["action", "after", "and", "are", "back", "backlabel", "button", "called", "check", "checks", "children", "connector", "constraints", "continue", "controlled", "current", "defaultvalue", "description", "every", "false", "fields", "fills", "finished", "first", "for", "form", "header", "hidden", "inactive", "its", "last", "links", "matches", "mounted", "multi", "native", "new", "nextlabel", "not", "one", "only", "onnext", "onsubmit", "onvaluechange", "optional", "order", "pass", "progress", "render", "resolve", "return", "runs", "server", "shown", "shows", "spinner", "spring", "start", "stay", "stays", "step", "steps", "submit", "submitlabel", "submits", "survive", "text", "the", "through", "title", "together", "uncontrolled", "unmounted", "value", "values", "while", "with"], registryDependencies: ["button", "focus", "use-controllable-state", "utils"] }, { name: "styles", type: "registry:file", exports: [], keywords: [], registryDependencies: [] }, { name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Switch"], keywords: ["called", "checkbox", "checked", "controlled", "defaultchecked", "first", "knob", "native", "new", "oncheckedchange", "pair", "render", "role", "spring", "state", "switch", "the", "uncontrolled", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "table", type: "registry:ui", title: "Table", description: "Server-safe semantic <table> in a scrolling card: header, body, footer and caption, striped rows, tinted selected rows, and sortable headers that set aria-sort and flip their arrow.", exports: ["Table", "TableHeader", "TableBody", "TableFooter", "TableRow", "TableHead", "TableCell", "TableCaption"], keywords: ["alternating", "and", "aria", "around", "arrow", "button", "called", "card", "classes", "column", "containerclassname", "data", "direction", "for", "header", "keyboard", "makes", "narrow", "onsort", "pass", "pressed", "real", "requires", "row", "screens", "scrollable", "scrolling", "scrolls", "selected", "sets", "shading", "shows", "sideways", "sort", "sortable", "state", "striped", "table", "that", "the", "then", "tint", "when"], registryDependencies: ["overflow-focus", "utils"] }, { name: "tabs", type: "registry:ui", title: "Tabs", description: "WAI-ARIA tabs with arrow, Home and End keys. One indicator springs between tabs, segmented or underline. Inactive panels stay in the DOM for SEO; the active one fades in.", exports: ["Tabs", "TabsList", "TabsTrigger", "TabsContent"], keywords: ["activated", "active", "always", "and", "aria", "arrows", "bar", "behind", "called", "children", "content", "controlled", "crawlable", "defaultvalue", "dom", "end", "first", "focus", "hidden", "home", "html", "inactive", "indicator", "labels", "matches", "move", "muted", "one", "onvaluechange", "pair", "panel", "panels", "pill", "remains", "render", "segmented", "springs", "stay", "stays", "tab", "tabs", "tabscontent", "tabstrigger", "the", "their", "track", "uncontrolled", "under", "underline", "value", "variant", "wai", "while", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, ← walks onto the tags and Delete removes one, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. Submits one entry per tag; required needs one.", exports: ["tagFieldClass", "Tag", "useTagKeys", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "call", "called", "children", "chip", "commit", "controlled", "defaultvalue", "delete", "disabled", "each", "empty", "enter", "esc", "every", "false", "field", "first", "focused", "for", "form", "free", "from", "frominput", "hides", "input", "inside", "its", "keyboard", "keydown", "keys", "last", "least", "limit", "list", "many", "max", "mode", "most", "move", "name", "needs", "one", "onkeydown", "onremove", "onto", "onvaluechange", "order", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "put", "reach", "reject", "removable", "remove", "removeref", "removes", "render", "rendered", "required", "return", "returns", "runs", "separator", "separators", "shows", "space", "split", "springs", "stay", "stays", "stops", "submits", "tab", "tag", "tagref", "tags", "text", "that", "the", "them", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "usetagkeys", "validate", "value", "walks", "when", "which", "with"], registryDependencies: ["field", "form-value", "use-controllable-state", "utils"] }, { name: "terminal", type: "registry:ui", title: "Terminal", description: "Terminal window. Snippet mode types install commands once scrolled into view, with the full transcript kept in the DOM for SEO and no-JS; log mode streams lines into a live region that follows the bottom unless you scroll up, then offers a jump back. Status lines carry glyphs, copy grabs the commands.", exports: ["Terminal"], keywords: ["and", "append", "arrives", "back", "before", "body", "bodyclassname", "bottom", "character", "clipboard", "commands", "copy", "copytext", "defaults", "dom", "false", "follows", "for", "full", "hides", "install", "jump", "lines", "live", "log", "mode", "new", "offering", "once", "order", "output", "per", "prompt", "puts", "read", "reveals", "role", "scroll", "scrolling", "seo", "size", "snippet", "snippets", "speed", "stays", "sticks", "stream", "streams", "symbol", "terminal", "text", "the", "title", "typed", "types", "typing", "unless", "view", "what", "when", "window", "you"], registryDependencies: ["app-window", "code", "platform", "utils"] }, { name: "testimonial", type: "registry:block", title: "Testimonial", description: "A large Quote with its author, and everyone else who agrees as an overlapping AvatarGroup. Server-safe.", exports: ["Testimonial"], keywords: ["agrees", "and", "author", "avatar", "avatargroup", "behind", "beside", "carried", "children", "company", "crowd", "else", "everyone", "few", "first", "fold", "initials", "into", "its", "lang", "language", "large", "line", "one", "others", "otherslabel", "photo", "quote", "rest", "role", "said", "show", "stack", "streamers", "subtitle", "the", "trusted", "under", "url", "who", "with", "without"], registryDependencies: ["avatar", "avatar-group", "quote", "utils"] }, { name: "text-swap", type: "registry:ui", title: "TextSwap", description: "Spring-powered text morphing and ticker. Auto-cycles phrases or animates whenever children change with blur and slide transitions.", exports: ["TextSwap"], keywords: ["accessible", "active", "after", "and", "animation", "array", "automatically", "between", "button", "can", "changing", "child", "children", "content", "controlling", "controls", "cycle", "cycles", "cycling", "default", "direction", "doesn", "externally", "focus", "hidden", "hover", "index", "interval", "items", "keyboard", "milliseconds", "motion", "mouse", "name", "new", "next", "other", "pass", "pause", "pauselabel", "pauseonhover", "reduced", "show", "shows", "single", "stop", "string", "swap", "swaps", "text", "the", "through", "too", "touch", "true", "use", "users", "wcag", "when", "with"], registryDependencies: ["button-variants", "platform", "utils"] }, { name: "textarea", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Textarea"], keywords: ["auto", "cap", "content", "corner", "end", "field", "firefox", "grows", "max", "maxlength", "near", "needs", "red", "resizable", "show", "showcount", "sizing", "the", "then", "turns", "unsupported", "used", "warning", "where", "with"], registryDependencies: ["field", "form-value", "input", "utils"] }, { name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", exports: ["isDark", "setTheme", "subscribeTheme", "accents", "getAccent", "setAccent", "themeScript"], keywords: ["and", "app", "focus", "like", "persists", "primary", "retints", "rings", "selection", "switches", "the", "theme", "wide"], registryDependencies: [] }, { name: "theme-toggle", type: "registry:ui", title: "Theme Toggle", description: "Light/dark icon switch with spring rotate-swap; follows the OS until first use.", exports: ["ThemeToggle"], keywords: ["button", "click", "first", "follows", "icon", "moon", "rotate", "spring", "sun", "swap", "until"], registryDependencies: ["button", "theme", "utils"] }, { name: "timeline", type: "registry:ui", title: "Timeline", description: "Server-safe ordered list of events on a rail: icon or dot markers with status colors, machine-readable <time>, title and an optional body. For changelogs, experience and mod logs.", exports: ["Timeline", "TimelineItem"], keywords: ["above", "and", "body", "children", "color", "date", "datetime", "dot", "event", "events", "first", "for", "headline", "icon", "inside", "list", "machine", "marker", "newest", "oldest", "one", "optional", "order", "ordered", "pair", "rail", "readable", "render", "shown", "small", "the", "then", "time", "title", "variant", "with", "without", "yours"], registryDependencies: ["utils"] }, { name: "toast", type: "registry:ui", title: "Toast", description: "Stacked notifications. Hover to expand, swipe to dismiss, timers pause on hover or focus, a toast with an action stays until it's used or dismissed, and F8 jumps to the newest toast with an action (else the newest) from the keyboard.", exports: ["toast", "Toaster"], keywords: ["action", "actionduration", "after", "back", "before", "closes", "default", "description", "dismiss", "dismissed", "dismisses", "duration", "else", "error", "esc", "expands", "focus", "hover", "infinity", "jumps", "key", "layout", "loading", "mount", "newest", "once", "own", "pause", "press", "promise", "root", "saved", "sends", "shortcut", "shows", "stacked", "stays", "still", "success", "swipe", "tap", "that", "the", "there", "timers", "title", "toast", "toasts", "until", "used", "while", "wins", "with", "without"], registryDependencies: ["focus", "kbd", "platform", "use-dismiss", "use-hotkey", "utils"] }, { name: "toggle-group", type: "registry:ui", title: "Toggle Group", description: "Segmented control of toggle buttons. Single mode slides one pill to the pressed item (view mode, billing period); multiple mode toggles items independently (bold, italic). Arrow keys move between items.", exports: ["ToggleGroup", "ToggleGroupItem"], keywords: ["always", "and", "aria", "arrow", "behind", "between", "billing", "bold", "buttons", "called", "choice", "control", "controlled", "defaultvalue", "end", "every", "filters", "first", "height", "home", "icon", "independent", "italic", "item", "items", "keys", "label", "labels", "mode", "move", "multiple", "need", "one", "only", "onvaluechange", "pair", "period", "pill", "pressed", "render", "reported", "segment", "segmented", "set", "single", "size", "slides", "springs", "the", "time", "toggle", "toggles", "type", "uncontrolled", "value", "values", "view", "when", "with"], registryDependencies: ["list", "use-controllable-state", "use-indicator", "utils"] }, { name: "tooltip", type: "registry:ui", title: "Tooltip", description: "Short label on hover or keyboard focus, also wired as the trigger's aria-describedby. Warm-up: once one opens, neighbors open instantly. Hoverable, Esc dismisses, ignores touch.", exports: ["Tooltip"], keywords: ["accessible", "action", "align", "alignment", "also", "and", "aria", "becomes", "before", "button", "children", "content", "control", "default", "delay", "description", "dismisses", "element", "esc", "first", "flips", "focus", "focusable", "for", "hold", "hover", "how", "icon", "keyboard", "label", "one", "open", "opens", "own", "pointer", "preferred", "press", "reaches", "room", "short", "shows", "side", "stays", "still", "tap", "text", "the", "their", "there", "toggles", "tooltip", "touch", "trigger", "triggers", "wcag", "when", "while", "with"], registryDependencies: ["platform", "use-anchor", "use-dismiss", "use-hover-open", "utils"] }, { name: "typewriter", type: "registry:ui", title: "Typewriter", description: "Types out text character-by-character with optional phrase cycling and pulsing cursor. Full copy stays in the DOM for SEO.", exports: ["Typewriter"], keywords: ["accessible", "and", "animation", "array", "before", "blinking", "button", "called", "can", "character", "classes", "content", "controls", "cursor", "cursorchar", "cursorclassname", "custom", "cycle", "default", "delay", "deletespeed", "deleting", "dom", "effect", "focus", "for", "full", "fully", "headers", "hero", "hidden", "hover", "keeps", "keyboard", "loop", "milliseconds", "motion", "mouse", "moving", "name", "next", "once", "oncomplete", "other", "pause", "pauseduration", "pauselabel", "pauseonhover", "per", "phrase", "phrases", "quotes", "reduced", "seo", "show", "shows", "speed", "starting", "stays", "stop", "text", "the", "through", "too", "touch", "true", "type", "typed", "typewriter", "typing", "use", "users", "wait", "wcag", "when", "whether", "while", "with"], registryDependencies: ["button-variants", "platform", "utils"] }, { name: "use-anchor", type: "registry:hook", description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
17858
18047
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
17859
18048
  Measures before paint, so nothing jumps.`, exports: ["useAnchor"], keywords: ["anchor", "and", "before", "clamped", "coordinates", "doesn", "fit", "fixed", "flipped", "floating", "for", "its", "jumps", "kept", "measures", "next", "nothing", "paint", "panel", "place", "position", "preferred", "resize", "scroll", "side", "the", "viewport", "when"], registryDependencies: ["utils"] }, { name: "use-controllable-state", type: "registry:hook", description: "State a parent may own. Controlled while `value` is defined; otherwise kept here, seeded from\n`defaultValue`. The setter always reports through `onChange`, so both modes stay observable.", exports: ["useControllableState"], keywords: ["always", "both", "controlled", "defaultvalue", "defined", "from", "here", "kept", "may", "modes", "observable", "onchange", "otherwise", "own", "parent", "reports", "seeded", "setter", "state", "stay", "the", "through", "value", "while"], registryDependencies: [] }, { name: "use-disclosure", type: "registry:hook", description: "Open state for any overlay. Spread `props` onto Dialog, Drawer, Popover or DropdownMenu and\nopen it from anywhere: `const drawer = useDisclosure(); <Drawer {...drawer.props}>`, `drawer.show()`.", exports: ["useDisclosure"], keywords: ["and", "any", "anywhere", "const", "dialog", "drawer", "dropdownmenu", "for", "from", "onto", "open", "overlay", "popover", "props", "show", "spread", "state", "usedisclosure"], registryDependencies: [] }, { name: "use-dismiss", type: "registry:hook", description: "Inside one of `refs` (panel first, then its trigger), or inside a floating child opened from\nthe panel: a Select list or menu portals out, but it is marked `data-floating`.", exports: ["isWithin", "useDismiss"], keywords: ["and", "but", "child", "children", "closes", "data", "first", "floating", "from", "inside", "its", "list", "marked", "menu", "one", "opened", "out", "outside", "panel", "pointerdown", "portals", "refs", "select", "the", "then", "trigger"], registryDependencies: [] }, { name: "use-hotkey", type: "registry:hook", description: '`"mod+k"`: ⌘K on Mac, Ctrl+K elsewhere, without Alt. `"/"`: the key alone, no modifiers.', exports: ["matchesHotkey", "useHotkey"], keywords: ["alone", "alt", "browser", "ctrl", "disable", "elsewhere", "false", "handler", "key", "keyboard", "leave", "mac", "mod", "modifiers", "pass", "preventdefault", "returns", "shortcut", "the", "undefined", "wide", "window", "without"], registryDependencies: [] }, { name: "use-hover-open", type: "registry:hook", description: "Open-on-hover state with intent delays, shared by Tooltip and HoverCard. Mouse only (touch\ntaps go to the trigger's own action); keyboard focus opens it too. Spread `trigger` on the\nanchor and `floating` on the panel so moving between them keeps it open.", exports: ["useHoverOpen"], keywords: ["action", "anchor", "and", "between", "delays", "floating", "focus", "hover", "hovercard", "intent", "keeps", "keyboard", "mouse", "moving", "only", "open", "opens", "own", "panel", "shared", "spread", "state", "taps", "the", "them", "too", "tooltip", "touch", "trigger", "with"], registryDependencies: [] }, { name: "use-indicator", type: "registry:hook", description: `Offset and width of the active item, for one indicator that springs between items (Tabs,
17860
18049
  ToggleGroup). Re-measures when the item or its row resizes (font swap, wrap), or the pill
@@ -17951,10 +18140,18 @@ async function fetchAllItems(source) {
17951
18140
  const index = await fetchIndex(source);
17952
18141
  return Promise.all(index.map((i) => fetchItem(source, i.name)));
17953
18142
  }
18143
+ var jsrBase = async (version) => {
18144
+ try {
18145
+ const res = await fetch(`https://jsr.io/@kznlabs/labsui/${version}/src/registry.json`);
18146
+ return res.ok ? (await res.json()).items : undefined;
18147
+ } catch {
18148
+ return;
18149
+ }
18150
+ };
17954
18151
  // jsr.json
17955
18152
  var jsr_default = {
17956
18153
  name: "@kznlabs/labsui",
17957
- version: "0.1.12",
18154
+ version: "0.1.14",
17958
18155
  license: "MIT",
17959
18156
  exports: "./src/index.ts",
17960
18157
  publish: {
@@ -18082,6 +18279,9 @@ ${bold("Mount once")} in the root layout, inside <MotionProvider>:`);
18082
18279
  // src/commands/diff.ts
18083
18280
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
18084
18281
  import { relative as relative3 } from "node:path";
18282
+ var same = (a, b) => lines(a).join(`
18283
+ `) === lines(b).join(`
18284
+ `);
18085
18285
  function matches(a, b) {
18086
18286
  const out = new Int32Array(a.length).fill(-1);
18087
18287
  let i = 0;
@@ -18162,7 +18362,7 @@ async function installedItems(config, base, all) {
18162
18362
  return files.some((file) => existsSync4(file)) ? [{ item, files }] : [];
18163
18363
  });
18164
18364
  }
18165
- async function diff(cwd, config, names, version) {
18365
+ async function diff(cwd, config, names, version, loadBase = jsrBase) {
18166
18366
  const base = projectBase(cwd, config);
18167
18367
  const wanted = new Set(names.map(itemName));
18168
18368
  const all = await fetchAllItems(config.registry);
@@ -18171,10 +18371,22 @@ async function diff(cwd, config, names, version) {
18171
18371
  throw new CliError(`Unknown item(s): ${unknown.join(", ")}. Search finds the names.`);
18172
18372
  const installed = await installedItems(config, base, all);
18173
18373
  const out = [...wanted].filter((name) => !installed.some(({ item }) => item.name === name)).map((name) => ({ name, installed: false, files: [] }));
18374
+ const bases = new Map;
18375
+ const baseOf = async (name, since) => {
18376
+ if (!since || config.registry)
18377
+ return;
18378
+ if (since === version)
18379
+ return all.find((item) => item.name === name);
18380
+ if (!bases.has(since))
18381
+ bases.set(since, loadBase(since));
18382
+ return (await bases.get(since))?.[name];
18383
+ };
18174
18384
  for (const { item, files } of installed) {
18175
18385
  if (wanted.size && !wanted.has(item.name))
18176
18386
  continue;
18177
18387
  const changed = [];
18388
+ const since = config.installed?.[item.name];
18389
+ const old = await baseOf(item.name, since);
18178
18390
  item.files.forEach((file, i) => {
18179
18391
  const local = files[i];
18180
18392
  if (!local || !existsSync4(local))
@@ -18187,19 +18399,31 @@ async function diff(cwd, config, names, version) {
18187
18399
  return;
18188
18400
  const body = patch.split(`
18189
18401
  `).slice(2);
18402
+ const before = old?.files.find((entry) => entry.path === file.path)?.content;
18403
+ const was = before === undefined ? undefined : rewriteAlias(before, config.alias);
18190
18404
  changed.push({
18191
18405
  path: shown,
18192
18406
  added: body.filter((line) => line.startsWith("+")).length,
18193
18407
  removed: body.filter((line) => line.startsWith("-")).length,
18194
18408
  formatting: sameCode(theirs, yours),
18409
+ changedBy: was === undefined ? undefined : same(was, yours) ? "upstream" : same(was, theirs) ? "local" : "both",
18195
18410
  patch
18196
18411
  });
18197
18412
  });
18198
18413
  if (changed.length)
18199
- out.push({ name: item.name, installed: true, files: changed });
18414
+ out.push({ name: item.name, installed: true, since, files: changed });
18200
18415
  }
18201
18416
  return out;
18202
18417
  }
18418
+ function changeNote(file, since) {
18419
+ if (file.changedBy === "upstream")
18420
+ return `only labsUI changed it since ${since}; update replaces it`;
18421
+ if (file.changedBy === "local")
18422
+ return "your edits; update keeps them";
18423
+ if (file.changedBy === "both")
18424
+ return `your edits and labsUI's since ${since}; update merges them`;
18425
+ return "";
18426
+ }
18203
18427
  function printDiff(items, full) {
18204
18428
  if (!items.length)
18205
18429
  return console.log(`${green("✓")} Installed items match this labsui version.`);
@@ -18207,10 +18431,13 @@ function printDiff(items, full) {
18207
18431
  if (!item.installed)
18208
18432
  console.log(` ${item.name.padEnd(24)} ${dim(`not installed (labsui add ${item.name})`)}`);
18209
18433
  for (const file of item.files) {
18434
+ const note = file.formatting ? "formatting only" : changeNote(file, item.since);
18210
18435
  if (!full) {
18211
- console.log(` ${item.name.padEnd(24)} ${green(`+${file.added}`)} ${red(`-${file.removed}`)} ${dim(file.path)}${file.formatting ? dim(" (formatting only)") : ""}`);
18436
+ console.log(` ${item.name.padEnd(24)} ${green(`+${file.added}`)} ${red(`-${file.removed}`)} ${dim(file.path)}${note ? dim(` (${note})`) : ""}`);
18212
18437
  continue;
18213
18438
  }
18439
+ if (note)
18440
+ console.log(dim(`# ${item.name}: ${note}`));
18214
18441
  for (const line of file.patch.split(`
18215
18442
  `))
18216
18443
  console.log(line.startsWith("+") ? green(line) : line.startsWith("-") ? red(line) : line.startsWith("@@") ? dim(line) : line);
@@ -18218,7 +18445,7 @@ function printDiff(items, full) {
18218
18445
  }
18219
18446
  if (!full)
18220
18447
  console.log(`
18221
- ${dim("labsui diff <name> shows the changes; add <name> --overwrite takes this version.")}`);
18448
+ ${dim("labsui diff <name> shows the changes; labsui update <name> takes this version and keeps your edits.")}`);
18222
18449
  }
18223
18450
 
18224
18451
  // src/commands/doctor.ts
@@ -18226,7 +18453,7 @@ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as
18226
18453
  import { join as join4, relative as relative4 } from "node:path";
18227
18454
 
18228
18455
  // src/merge.ts
18229
- var same = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
18456
+ var same2 = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
18230
18457
  function merge3(base, ours, theirs, labels = { ours: "yours", theirs: "labsui" }) {
18231
18458
  const baseLines = lines(base);
18232
18459
  const ourLines = lines(ours);
@@ -18247,9 +18474,9 @@ function merge3(base, ours, theirs, labels = { ours: "yours", theirs: "labsui" }
18247
18474
  const chunkO = baseLines.slice(i, key);
18248
18475
  const chunkA = ourLines.slice(ourIndex, ourStop);
18249
18476
  const chunkB = theirLines.slice(theirIndex, theirStop);
18250
- if (same(chunkA, chunkO))
18477
+ if (same2(chunkA, chunkO))
18251
18478
  out.push(...chunkB);
18252
- else if (same(chunkB, chunkO) || same(chunkA, chunkB))
18479
+ else if (same2(chunkB, chunkO) || same2(chunkA, chunkB))
18253
18480
  out.push(...chunkA);
18254
18481
  else {
18255
18482
  conflicts++;
@@ -18628,14 +18855,6 @@ ${dim(`labsui view ${hits[0]?.name} shows props and an example.`)}`);
18628
18855
  // src/commands/update.ts
18629
18856
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
18630
18857
  import { dirname as dirname3, relative as relative6 } from "node:path";
18631
- var jsrBase = async (version) => {
18632
- try {
18633
- const res = await fetch(`https://jsr.io/@kznlabs/labsui/${version}/src/registry.json`);
18634
- return res.ok ? (await res.json()).items : undefined;
18635
- } catch {
18636
- return;
18637
- }
18638
- };
18639
18858
  var norm = (text) => text.replace(/\r\n/g, `
18640
18859
  `).replace(/\n*$/, `
18641
18860
  `);
@@ -18955,7 +19174,7 @@ var TOOLS = [
18955
19174
  {
18956
19175
  name: "diff",
18957
19176
  title: "Diff installed labsUI items",
18958
- description: "Compare installed labsUI items with this labsUI version. Returns a unified patch per changed file, from the registry file to the project file (+ lines exist only in the project), with formatting: true when only blank lines or indentation differ; matching items are left out, a named item that isn't installed comes back with installed: false, and when nothing differs it says so. Use it to see local edits before update. Needs init.",
19177
+ description: "Compare installed labsUI items with this labsUI version. Returns a unified patch per changed file, from the registry file to the project file (+ lines exist only in the project), with formatting: true when only blank lines or indentation differ and changedBy saying whose change it is since the installed version: upstream (only labsUI changed it; update replaces the file), local (only you did; update keeps it) or both (update merges), unset when that version can't be fetched; matching items are left out, a named item that isn't installed comes back with installed: false, and when nothing differs it says so. Use it before update to see what it will do. Needs init.",
18959
19178
  inputSchema: {
18960
19179
  type: "object",
18961
19180
  properties: { names: installed }
@@ -19044,9 +19263,13 @@ async function callTool(ctx, name, args) {
19044
19263
  if (!config)
19045
19264
  throw new CliError("labsUI is not set up here. Call the init tool first.");
19046
19265
  const items = await diff(ctx.cwd, { ...config, ...ctx.registry && { registry: ctx.registry } }, list(args.names), ctx.version);
19047
- const body = items.length ? items.flatMap((i) => i.installed ? i.files.map((file) => `\`\`\`diff
19266
+ const body = items.length ? items.flatMap((i) => i.installed ? i.files.map((file) => {
19267
+ const note = changeNote(file, i.since);
19268
+ return `${note ? `${i.name}: ${note}
19269
+ ` : ""}\`\`\`diff
19048
19270
  ${file.patch}
19049
- \`\`\``) : [`${i.name} is not installed; add installs it.`]).join(`
19271
+ \`\`\``;
19272
+ }) : [`${i.name} is not installed; add installs it.`]).join(`
19050
19273
 
19051
19274
  `) : "Installed items match this labsui version.";
19052
19275
  return text(body, { items });