@kznlabs/labsui 0.1.5 → 0.1.7

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 -0
  2. package/dist/index.js +597 -157
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -231,6 +231,7 @@ export function AccentPicker({ className }: { className?: string }) {
231
231
  return (
232
232
  <fieldset className={cn("flex items-center gap-1.5", className)}>
233
233
  <legend className="sr-only">Accent color</legend>
234
+
234
235
  {accents.map((accent) => (
235
236
  <label
236
237
  key={accent}
@@ -245,12 +246,15 @@ export function AccentPicker({ className }: { className?: string }) {
245
246
  onChange={() => setAccent(accent)}
246
247
  className="peer sr-only"
247
248
  />
249
+
248
250
  <span className="sr-only">{accent}</span>
251
+
249
252
  <span
250
253
  aria-hidden="true"
251
254
  style={{ backgroundColor: swatch[accent] }}
252
255
  className="size-4 rounded-full ring-1 ring-foreground/10 ring-inset transition-transform duration-200 peer-hover:scale-110 peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50"
253
256
  />
257
+
254
258
  {current === accent && (
255
259
  <motion.span
256
260
  aria-hidden="true"
@@ -418,6 +422,7 @@ export function AccordionTrigger({
418
422
  )}
419
423
  >
420
424
  {children}
425
+
421
426
  <motion.span
422
427
  animate={{ rotate: open ? 180 : 0 }}
423
428
  transition={spring}
@@ -537,7 +542,9 @@ export function Alert({
537
542
  {glyph}
538
543
  </span>
539
544
  )}
545
+
540
546
  <div className="grid min-w-0 flex-1 gap-1">{children}</div>
547
+
541
548
  {action && <div className="-my-1 shrink-0 self-center">{action}</div>}
542
549
  </div>
543
550
  );
@@ -670,14 +677,17 @@ export function AppWindow({
670
677
  );
671
678
  })}
672
679
  </div>
680
+
673
681
  {header ??
674
682
  (title && (
675
683
  <p className="pointer-events-none absolute inset-x-24 truncate text-center font-medium text-muted-foreground text-xs">
676
684
  {title}
677
685
  </p>
678
686
  ))}
687
+
679
688
  {actions && <div className="ml-auto flex items-center gap-1.5">{actions}</div>}
680
689
  </div>
690
+
681
691
  <div className={cn("flex-1", bodyClassName)}>{children}</div>
682
692
  </div>
683
693
  );
@@ -716,7 +726,7 @@ export function AspectRatio({ ratio = 16 / 9, className, style, ...props }: Aspe
716
726
  }
717
727
  ` }] }, attachment: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/attachment", usage: '<FileDropzone name="clips" multiple accept="video/*" maxSize={50 * 1024 * 1024} onFiles={add} onReject={warn} /><AttachmentList>{files.map((f) => <Attachment key={f.id} name={f.name} size={f.size} type={f.type} progress={f.progress} onRemove={() => remove(f.id)} />)}</AttachmentList>', examples: [{ name: "clip-upload.tsx", code: `import { FileDropzone } from "@/components/ui/attachment";
718
728
  import { Button } from "@/components/ui/button";
719
- import { uploadClips } from "./actions"; // "use server": (formData) => formData.getAll("clips")
729
+ import { uploadClips } from "./actions";
720
730
 
721
731
  // Picked and dropped files both land in the input, so the Server Action gets them.
722
732
  export function ClipUpload() {
@@ -747,8 +757,14 @@ export function Attachments({ files, onRemove }: { files: File[]; onRemove: (id:
747
757
  ))}
748
758
  </AttachmentList>
749
759
  );
750
- }` }], api: { entries: [{ kind: "function", name: "formatBytes", signature: "formatBytes(bytes: number, locale?: string) => string", description: 'Bytes as text in the given locale: 1536 is "1.5 kB".' }, { kind: "component", name: "Attachment", props: [{ name: "name", type: "string", required: true, description: "File name." }, { name: "error", type: "string", required: false, description: "Replaces the size line and turns the chip red." }, { name: "href", type: "string", required: false, description: "Makes the name a download link." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the size." }, { name: "onRemove", type: "() => void", required: false, description: "Shows a remove button; called when it is pressed." }, { name: "progress", type: "number", required: false, description: "0 to 100 while uploading; the bar springs along the bottom edge." }, { name: "size", type: "number", required: false, description: "Bytes." }, { name: "src", type: "string", required: false, description: "Thumbnail source: a URL, or `URL.createObjectURL(file)` for a fresh upload." }, { name: "type", type: "string", required: false, description: "MIME type; picks the icon, and image types show `src` as a thumbnail." }], description: "File chip: thumbnail or type icon, name, size, upload progress or error, and remove." }, { kind: "component", name: "AttachmentList", props: [], extends: "<ul>", description: "Attachments that spring in, out and into place when one is removed. Key each child." }, { kind: "component", name: "FileDropzone", props: [{ name: "accept", type: "string", required: false, description: 'Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too.' }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "children", type: "ReactNode", required: false, description: 'Replaces the default "Drop files or browse" content.' }, { name: "disabled", type: "boolean", required: false, description: "Blocks drops and the picker." }, { name: "id", type: "string", required: false, description: "Id of the file input; Field fills it in." }, { name: "maxSize", type: "number", required: false, description: "Bytes." }, { name: "multiple", type: "boolean", required: false, description: "Accept more than one file." }, { name: "name", type: "string", required: false, description: "Form field name: picked and dropped files both submit with the form." }, { name: "onFiles", type: "(files: File[]) => void", required: false, description: "Files that passed `accept` and `maxSize`, from a drop or the picker." }, { name: "onReject", type: "(rejected: Rejection[]) => void", required: false, description: "Files that failed `accept` or `maxSize`, each with a reason." }, { name: "required", type: "boolean", required: false, description: "Form submit needs at least one file." }], description: `Drop area around a real file input: click, Enter/Space and the native picker work as-is, and
751
- dropped files are written back into the input so plain forms and Server Actions get them.` }], types: { Rejection: '{ file: File; reason: "type" | "size" }' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/attachment.tsx", type: "registry:ui", content: `"use client";
760
+ }` }, { name: "actions.ts", code: `"use server";
761
+
762
+ export async function uploadClips(formData: FormData) {
763
+ const clips = formData.getAll("clips") as File[];
764
+
765
+ console.log(clips.map((clip) => clip.name));
766
+ }` }], api: { entries: [{ kind: "function", name: "formatBytes", signature: "formatBytes(bytes: number, locale?: string) => string", description: 'Bytes as text in the given locale: 1536 is "1.5 kB".' }, { kind: "component", name: "Attachment", props: [{ name: "name", type: "string", required: true, description: "File name." }, { name: "error", type: "string", required: false, description: "Replaces the size line and turns the chip red." }, { name: "href", type: "string", required: false, description: "Makes the name a download link." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the size." }, { name: "onRemove", type: "() => void", required: false, description: "Shows a remove button; called when it is pressed." }, { name: "progress", type: "number", required: false, description: "0 to 100 while uploading; the bar springs along the bottom edge." }, { name: "size", type: "number", required: false, description: "Bytes." }, { name: "src", type: "string", required: false, description: "Thumbnail source: a URL, or `URL.createObjectURL(file)` for a fresh upload." }, { name: "type", type: "string", required: false, description: "MIME type; picks the icon, and image types show `src` as a thumbnail." }], description: "File chip: thumbnail or type icon, name, size, upload progress or error, and remove." }, { kind: "component", name: "AttachmentList", props: [], extends: "<ul>", description: "Attachments that spring in, out and into place when one is removed. Key each child." }, { kind: "component", name: "FileDropzone", props: [{ name: "accept", type: "string", required: false, description: 'Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too.' }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "children", type: "ReactNode", required: false, description: 'Replaces the default "Drop files or browse" content.' }, { name: "disabled", type: "boolean", required: false, description: "Blocks drops and the picker; no file is submitted." }, { name: "id", type: "string", required: false, description: "Id of the file input; Field fills it in." }, { name: "maxSize", type: "number", required: false, description: "Bytes." }, { name: "multiple", type: "boolean", required: false, description: "Accept more than one file." }, { name: "name", type: "string", required: false, description: "Form field name: picked and dropped files both submit with the form." }, { name: "onFiles", type: "(files: File[]) => void", required: false, description: "Files that passed `accept` and `maxSize`, from a drop or the picker." }, { name: "onReject", type: "(rejected: FileRejection[]) => void", required: false, description: "Files that failed `accept` or `maxSize`, each with a reason." }, { name: "required", type: "boolean", required: false, description: "Form submit needs at least one file." }], description: `Drop area around a real file input: click, Enter/Space and the native picker work as-is, and
767
+ dropped files are written back into the input so plain forms and Server Actions get them.` }], types: { FileRejection: '{ file: File; reason: "type" | "size" }' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/attachment.tsx", type: "registry:ui", content: `"use client";
752
768
 
753
769
  import {
754
770
  CloudUpload,
@@ -904,6 +920,7 @@ export function Attachment({
904
920
  <Icon className="size-5" aria-hidden="true" />
905
921
  )}
906
922
  </span>
923
+
907
924
  <span className="grid min-w-0 flex-1">
908
925
  {href ? (
909
926
  <a
@@ -916,6 +933,7 @@ export function Attachment({
916
933
  ) : (
917
934
  <span className="truncate font-medium text-sm">{name}</span>
918
935
  )}
936
+
919
937
  <span
920
938
  className={cn(
921
939
  "truncate text-xs tabular-nums",
@@ -925,6 +943,7 @@ export function Attachment({
925
943
  {meta || " "}
926
944
  </span>
927
945
  </span>
946
+
928
947
  {onRemove && (
929
948
  <button
930
949
  type="button"
@@ -935,6 +954,7 @@ export function Attachment({
935
954
  <X className="size-3.5" aria-hidden="true" />
936
955
  </button>
937
956
  )}
957
+
938
958
  {uploading && (
939
959
  <motion.span
940
960
  aria-hidden="true"
@@ -963,7 +983,7 @@ export function AttachmentList({ className, children, ...props }: ComponentProps
963
983
 
964
984
  /* ---------- FileDropzone ---------- */
965
985
 
966
- type Rejection = { file: File; reason: "type" | "size" };
986
+ export type FileRejection = { file: File; reason: "type" | "size" };
967
987
 
968
988
  type FileDropzoneProps = {
969
989
  /** Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too. */
@@ -975,12 +995,12 @@ type FileDropzoneProps = {
975
995
  /** Files that passed \`accept\` and \`maxSize\`, from a drop or the picker. */
976
996
  onFiles?: (files: File[]) => void;
977
997
  /** Files that failed \`accept\` or \`maxSize\`, each with a reason. */
978
- onReject?: (rejected: Rejection[]) => void;
998
+ onReject?: (rejected: FileRejection[]) => void;
979
999
  /** Form field name: picked and dropped files both submit with the form. */
980
1000
  name?: string;
981
1001
  /** Id of the file input; Field fills it in. */
982
1002
  id?: string;
983
- /** Blocks drops and the picker. */
1003
+ /** Blocks drops and the picker; no file is submitted. */
984
1004
  disabled?: boolean;
985
1005
  /** Form submit needs at least one file. */
986
1006
  required?: boolean;
@@ -1019,7 +1039,7 @@ export function FileDropzone(allProps: FileDropzoneProps) {
1019
1039
  const all = [...list];
1020
1040
  const picked = multiple ? all : all.slice(0, 1);
1021
1041
  const ok: File[] = [];
1022
- const rejected: Rejection[] = [];
1042
+ const rejected: FileRejection[] = [];
1023
1043
 
1024
1044
  for (const file of picked) {
1025
1045
  if (!accepts(file, accept)) rejected.push({ file, reason: "type" });
@@ -1075,7 +1095,7 @@ export function FileDropzone(allProps: FileDropzoneProps) {
1075
1095
  }}
1076
1096
  onDrop={onDrop}
1077
1097
  onPaste={(event) =>
1078
- event.clipboardData.files.length > 0 && take(event.clipboardData.files, false)
1098
+ !disabled && event.clipboardData.files.length > 0 && take(event.clipboardData.files, false)
1079
1099
  }
1080
1100
  className={cn(
1081
1101
  "flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed bg-card p-6 text-center transition-colors has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50",
@@ -1095,15 +1115,18 @@ export function FileDropzone(allProps: FileDropzoneProps) {
1095
1115
  className="sr-only"
1096
1116
  {...input}
1097
1117
  />
1118
+
1098
1119
  {children ?? (
1099
1120
  <>
1100
1121
  <span className="grid size-10 place-items-center rounded-full bg-muted text-muted-foreground">
1101
1122
  <CloudUpload className="size-5" aria-hidden="true" />
1102
1123
  </span>
1124
+
1103
1125
  <span className="text-sm">
1104
1126
  <span className="font-medium">Drop {multiple ? "files" : "a file"}</span> or{" "}
1105
1127
  <span className="font-medium text-primary-ink">browse</span>
1106
1128
  </span>
1129
+
1107
1130
  {hint && <span className="text-muted-foreground text-xs">{hint}</span>}
1108
1131
  </>
1109
1132
  )}
@@ -1176,6 +1199,7 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
1176
1199
  className="relative flex size-full select-none items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground ring-1 ring-border"
1177
1200
  >
1178
1201
  <span aria-hidden="true">{fallback ?? initials(alt)}</span>
1202
+
1179
1203
  {showImg && (
1180
1204
  // biome-ignore lint/performance/noImgElement: arbitrary remote avatars; no next/image loader config
1181
1205
  <img
@@ -1196,6 +1220,7 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
1196
1220
  />
1197
1221
  )}
1198
1222
  </span>
1223
+
1199
1224
  {status && (
1200
1225
  <span
1201
1226
  className={cn(
@@ -1213,14 +1238,14 @@ export function Avatar({ src, alt, fallback, status, size = "default", className
1213
1238
  ` }] }, "avatar-group": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/avatar-group", usage: '<AvatarGroup users={team} max={4} size="lg" />', examples: [{ name: "team.tsx", code: `import { AvatarGroup } from "@/components/ui/avatar-group";
1214
1239
  export function Team({ users }: { users: { alt: string; src?: string }[] }) {
1215
1240
  return <AvatarGroup users={users} max={4} size="lg" />;
1216
- }` }], api: { entries: [{ kind: "component", name: "AvatarGroup", props: [{ name: "users", type: "GroupUser[]", required: true, description: "People in order; `alt` is each one's name." }, { name: "max", type: "number", required: false, default: "4", description: "How many avatars to show before the `+N` chip." }, { name: "size", type: '"sm" | "default" | "lg"', required: false, default: '"default"', description: "Size of every avatar and the chip." }], description: "Overlapping avatars with a `+N` overflow chip. Hover gently enlarges the avatar on a spring, staying in stack order." }], types: { GroupUser: "{ alt: string; src?: string; id?: string; }" } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/avatar.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/avatar-group.tsx", type: "registry:ui", content: `"use client";
1241
+ }` }], api: { entries: [{ kind: "component", name: "AvatarGroup", props: [{ name: "users", type: "AvatarGroupUser[]", required: true, description: "People in order; `alt` is each one's name." }, { name: "max", type: "number", required: false, default: "4", description: "How many avatars to show before the `+N` chip." }, { name: "size", type: '"sm" | "default" | "lg"', required: false, default: '"default"', description: "Size of every avatar and the chip." }], description: "Overlapping avatars with a `+N` overflow chip. Hover gently enlarges the avatar on a spring, staying in stack order." }], types: { AvatarGroupUser: "{ alt: string; src?: string; id?: string; }" } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/avatar.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/avatar-group.tsx", type: "registry:ui", content: `"use client";
1217
1242
 
1218
1243
  import { motion } from "motion/react";
1219
1244
  import { useState } from "react";
1220
1245
  import { Avatar } from "@/components/ui/avatar";
1221
1246
  import { cn, spring } from "@/lib/utils";
1222
1247
 
1223
- type GroupUser = {
1248
+ export type AvatarGroupUser = {
1224
1249
  alt: string;
1225
1250
  /** Image URL; initials from \`alt\` show without one. */
1226
1251
  src?: string;
@@ -1230,7 +1255,7 @@ type GroupUser = {
1230
1255
 
1231
1256
  type AvatarGroupProps = {
1232
1257
  /** People in order; \`alt\` is each one's name. */
1233
- users: GroupUser[];
1258
+ users: AvatarGroupUser[];
1234
1259
  /** How many avatars to show before the \`+N\` chip. */
1235
1260
  max?: number;
1236
1261
  /** Size of every avatar and the chip. */
@@ -1274,6 +1299,7 @@ export function AvatarGroup({ users, max = 4, size = "default", className }: Ava
1274
1299
  <Avatar src={user.src} alt={user.alt} size={size} />
1275
1300
  </motion.li>
1276
1301
  ))}
1302
+
1277
1303
  {overflow > 0 && (
1278
1304
  <li
1279
1305
  className={cn(
@@ -1344,9 +1370,11 @@ export function Badge({
1344
1370
  {pulse && (
1345
1371
  <span className="absolute inset-0 animate-ping rounded-full bg-current opacity-60 [animation-duration:1.6s] motion-reduce:hidden" />
1346
1372
  )}
1373
+
1347
1374
  <span className="relative size-1.5 rounded-full bg-current" />
1348
1375
  </span>
1349
1376
  )}
1377
+
1350
1378
  {children}
1351
1379
  </span>
1352
1380
  );
@@ -1363,10 +1391,10 @@ export function NavTrail({ post }: { post: { title: string } }) {
1363
1391
  ]}
1364
1392
  />
1365
1393
  );
1366
- }` }], api: { entries: [{ kind: "component", name: "Breadcrumb", props: [{ name: "items", type: "Crumb[]", required: true, description: "The trail, outermost first. The last item is the current page." }, { name: "separator", type: "ReactNode", required: false, description: "Drawn between items. Default a chevron." }], extends: "<nav>", description: "Where the page sits in the site: a list of links ending at the current page. Server-safe." }], types: { Crumb: "{ label: ReactNode; href?: string; onClick?: MouseEventHandler<HTMLAnchorElement>; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: [], files: [{ path: "components/ui/breadcrumb.tsx", type: "registry:ui", content: `import { ChevronRight } from "lucide-react";
1394
+ }` }], api: { entries: [{ kind: "component", name: "Breadcrumb", props: [{ name: "items", type: "BreadcrumbItem[]", required: true, description: "The trail, outermost first. The last item is the current page." }, { name: "separator", type: "ReactNode", required: false, description: "Drawn between items. Default a chevron." }], extends: "<nav>", description: "Where the page sits in the site: a list of links ending at the current page. Server-safe." }], types: { BreadcrumbItem: "{ label: ReactNode; href?: string; onClick?: MouseEventHandler<HTMLAnchorElement>; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: [], files: [{ path: "components/ui/breadcrumb.tsx", type: "registry:ui", content: `import { ChevronRight } from "lucide-react";
1367
1395
  import type { ComponentProps, MouseEventHandler, ReactNode } from "react";
1368
1396
 
1369
- type Crumb = {
1397
+ export type BreadcrumbItem = {
1370
1398
  /** What the step reads as. */
1371
1399
  label: ReactNode;
1372
1400
  /** Leave out for a step that has no page of its own. */
@@ -1377,7 +1405,7 @@ type Crumb = {
1377
1405
 
1378
1406
  type BreadcrumbProps = Omit<ComponentProps<"nav">, "children"> & {
1379
1407
  /** The trail, outermost first. The last item is the current page. */
1380
- items: Crumb[];
1408
+ items: BreadcrumbItem[];
1381
1409
  /** Drawn between items. Default a chevron. */
1382
1410
  separator?: ReactNode;
1383
1411
  };
@@ -1401,6 +1429,7 @@ export function Breadcrumb({
1401
1429
  {separator ?? <ChevronRight />}
1402
1430
  </span>
1403
1431
  )}
1432
+
1404
1433
  {i === items.length - 1 ? (
1405
1434
  <span aria-current="page" className="text-foreground">
1406
1435
  {item.label}
@@ -1460,7 +1489,7 @@ export function DeleteAccount({
1460
1489
  Delete account
1461
1490
  </Button>
1462
1491
  );
1463
- }` }], api: { entries: [{ kind: "component", name: "Button", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, default: "3000", description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, default: "false", description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, default: '"idle"', description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, default: '"left"', description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }] }], types: { ButtonStatus: '"idle" | "loading" | "success"', SwapDirection: '"left" | "right" | "up" | "down"' } }, 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/utils.json"], files: [{ path: "components/ui/button.tsx", type: "registry:ui", content: `"use client";
1492
+ }` }], api: { entries: [{ kind: "component", name: "Button", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, default: "3000", description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, default: "false", description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, default: '"idle"', description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, default: '"left"', description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }] }], types: { ButtonStatus: '"idle" | "loading" | "success"', SwapDirection: '"left" | "right" | "up" | "down"' } }, 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/utils.json"], files: [{ path: "components/ui/button.tsx", type: "registry:ui", content: `"use client";
1464
1493
 
1465
1494
  import { Check, Loader2 } from "lucide-react";
1466
1495
  import { AnimatePresence, type HTMLMotionProps, motion } from "motion/react";
@@ -1475,16 +1504,16 @@ import {
1475
1504
  import { spring } from "@/lib/utils";
1476
1505
  import { buttonVariants, type VariantProps } from "./button-variants";
1477
1506
 
1478
- type ButtonStatus = "idle" | "loading" | "success";
1507
+ export type ButtonStatus = "idle" | "loading" | "success";
1479
1508
  /** Where the old label slides out to; the new one comes from the opposite side. */
1480
- type SwapDirection = "left" | "right" | "up" | "down";
1509
+ export type SwapDirection = "left" | "right" | "up" | "down";
1481
1510
  type Phase = "idle" | "armed" | "loading" | "success";
1482
1511
 
1483
1512
  type ButtonProps = Omit<HTMLMotionProps<"button">, "children"> &
1484
1513
  VariantProps & {
1485
- /** Disables the button and shows a spinner before the label. */
1514
+ /** Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable. */
1486
1515
  loading?: boolean;
1487
- /** In-place feedback: \`loading\` spins, \`success\` swaps the label to \`successLabel\`. */
1516
+ /** In-place feedback: \`loading\` spins, \`success\` swaps the label to \`successLabel\`. Both ignore clicks and Enter, so a form can't submit twice. */
1488
1517
  status?: ButtonStatus;
1489
1518
  /** Text shown on \`status="success"\`, with a check icon. Defaults to "Done". */
1490
1519
  successLabel?: ReactNode;
@@ -1620,14 +1649,19 @@ export function Button({
1620
1649
  {/* Hidden copies of every label reserve the widest one, so swaps never resize the button. */}
1621
1650
  <span className="inline-grid justify-items-center">
1622
1651
  {children !== undefined && <SizerLabel>{children}</SizerLabel>}
1652
+
1623
1653
  {loadingLabel !== undefined && <SizerLabel>{loadingLabel}</SizerLabel>}
1654
+
1624
1655
  {confirmLabel !== undefined && <SizerLabel>{confirmLabel}</SizerLabel>}
1656
+
1625
1657
  {successLabel !== undefined && (
1626
1658
  <SizerLabel>
1627
1659
  <Check />
1660
+
1628
1661
  {successLabel}
1629
1662
  </SizerLabel>
1630
1663
  )}
1664
+
1631
1665
  <AnimatePresence mode="wait" initial={false}>
1632
1666
  {phase === "success" && (
1633
1667
  <motion.span
@@ -1645,9 +1679,11 @@ export function Button({
1645
1679
  >
1646
1680
  <Check aria-hidden="true" />
1647
1681
  </motion.span>
1682
+
1648
1683
  {successLabel ?? "Done"}
1649
1684
  </motion.span>
1650
1685
  )}
1686
+
1651
1687
  {phase === "armed" && (
1652
1688
  <motion.span
1653
1689
  key="armed"
@@ -1659,6 +1695,7 @@ export function Button({
1659
1695
  {confirmLabel}
1660
1696
  </motion.span>
1661
1697
  )}
1698
+
1662
1699
  {phase === "loading" && (
1663
1700
  <motion.span
1664
1701
  key="loading"
@@ -1678,9 +1715,11 @@ export function Button({
1678
1715
  <Loader2 className="animate-spin" aria-hidden="true" />
1679
1716
  </motion.span>
1680
1717
  </AnimatePresence>
1718
+
1681
1719
  {loadingLabel ?? children}
1682
1720
  </motion.span>
1683
1721
  )}
1722
+
1684
1723
  {phase === "idle" && (
1685
1724
  <motion.span
1686
1725
  key="idle"
@@ -2047,6 +2086,7 @@ export function Calendar(props: CalendarProps) {
2047
2086
  <p id={\`\${id}-caption\`} aria-live="polite" className="font-semibold text-sm">
2048
2087
  {month ? fmtMonth.format(month) : " "}
2049
2088
  </p>
2089
+
2050
2090
  <div className="flex gap-0.5">
2051
2091
  <Button
2052
2092
  variant="ghost"
@@ -2058,6 +2098,7 @@ export function Calendar(props: CalendarProps) {
2058
2098
  >
2059
2099
  <ChevronLeft />
2060
2100
  </Button>
2101
+
2061
2102
  <Button
2062
2103
  variant="ghost"
2063
2104
  size="icon"
@@ -2103,6 +2144,7 @@ export function Calendar(props: CalendarProps) {
2103
2144
  ))}
2104
2145
  </tr>
2105
2146
  </thead>
2147
+
2106
2148
  <tbody>
2107
2149
  {weeks.map((week, weekIndex) => (
2108
2150
  // biome-ignore lint/suspicious/noArrayIndexKey: six fixed rows
@@ -2694,7 +2736,7 @@ export function ChangelogPage() {
2694
2736
  ]}
2695
2737
  />
2696
2738
  );
2697
- }` }], api: { entries: [{ kind: "component", name: "Changelog", props: [{ name: "releases", type: "Release[]", required: true, description: "Newest first; the first one is highlighted." }], description: "Release notes on a Timeline: the newest release is the highlighted one, changes are typed with Badges, Kbd shows how to try it." }], types: { Release: "{ version: string; title: string; date: string; dateTime?: string; changes: Change[]; shortcut?: string[]; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/kbd.json", "https://kznlabs.github.io/labsUI/r/timeline.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/changelog.tsx", type: "registry:block", content: `import { Rocket } from "lucide-react";
2739
+ }` }], api: { entries: [{ kind: "component", name: "Changelog", props: [{ name: "releases", type: "ChangelogRelease[]", required: true, description: "Newest first; the first one is highlighted." }], description: "Release notes on a Timeline: the newest release is the highlighted one, changes are typed with Badges, Kbd shows how to try it." }], types: { ChangelogRelease: "{ version: string; title: string; date: string; dateTime?: string; changes: Change[]; shortcut?: string[]; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/kbd.json", "https://kznlabs.github.io/labsUI/r/timeline.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/changelog.tsx", type: "registry:block", content: `import { Rocket } from "lucide-react";
2698
2740
  import type { ReactNode } from "react";
2699
2741
  import { Badge } from "@/components/ui/badge";
2700
2742
  import { KbdGroup } from "@/components/ui/kbd";
@@ -2705,7 +2747,7 @@ import { cn } from "@/lib/utils";
2705
2747
 
2706
2748
  type Change = { type: "new" | "improved" | "fixed"; text: ReactNode };
2707
2749
 
2708
- type Release = {
2750
+ export type ChangelogRelease = {
2709
2751
  version: string;
2710
2752
  /** Release headline, shown beside the version. */
2711
2753
  title: string;
@@ -2730,7 +2772,7 @@ export function Changelog({
2730
2772
  className,
2731
2773
  }: {
2732
2774
  /** Newest first; the first one is highlighted. */
2733
- releases: Release[];
2775
+ releases: ChangelogRelease[];
2734
2776
  className?: string;
2735
2777
  }) {
2736
2778
  return (
@@ -2747,6 +2789,7 @@ export function Changelog({
2747
2789
  <Badge variant="secondary" className="font-mono">
2748
2790
  {release.version}
2749
2791
  </Badge>
2792
+
2750
2793
  {release.title}
2751
2794
  </span>
2752
2795
  }
@@ -2758,10 +2801,12 @@ export function Changelog({
2758
2801
  <Badge variant={kinds[change.type].variant} className="mt-px w-16 justify-center">
2759
2802
  {kinds[change.type].label}
2760
2803
  </Badge>
2804
+
2761
2805
  <span>{change.text}</span>
2762
2806
  </li>
2763
2807
  ))}
2764
2808
  </ul>
2809
+
2765
2810
  {release.shortcut && (
2766
2811
  <p className="mt-3 flex items-center gap-2 text-muted-foreground text-xs">
2767
2812
  Try it <KbdGroup keys={release.shortcut} />
@@ -2823,7 +2868,7 @@ import {
2823
2868
  import { useDismiss } from "@/lib/use-dismiss";
2824
2869
  import { clamp, cn, material, spring } from "@/lib/utils";
2825
2870
 
2826
- type ChartSeries = {
2871
+ export type ChartSeries = {
2827
2872
  key: string;
2828
2873
  /** Legend and tooltip name of the series. */
2829
2874
  label: string;
@@ -3168,9 +3213,11 @@ export function Chart({
3168
3213
  // stop.
3169
3214
  <Table containerClassName={view === "table" ? "max-h-[32rem]" : "overflow-x-visible"}>
3170
3215
  <TableCaption className={view === "chart" ? "sr-only" : undefined}>{label}</TableCaption>
3216
+
3171
3217
  <TableHeader>
3172
3218
  <TableRow>
3173
3219
  <TableHead className="capitalize">{index}</TableHead>
3220
+
3174
3221
  {series.map((item) => (
3175
3222
  <TableHead key={item.key} className="text-right">
3176
3223
  {item.label}
@@ -3178,10 +3225,12 @@ export function Chart({
3178
3225
  ))}
3179
3226
  </TableRow>
3180
3227
  </TableHeader>
3228
+
3181
3229
  <TableBody>
3182
3230
  {data.map((row) => (
3183
3231
  <TableRow key={String(row[index])}>
3184
3232
  <TableCell className="font-medium">{row[index]}</TableCell>
3233
+
3185
3234
  {series.map((item) => (
3186
3235
  <TableCell key={item.key} className="text-right tabular-nums">
3187
3236
  {fmt.format(num(row[item.key]))}
@@ -3229,6 +3278,7 @@ export function Chart({
3229
3278
  }
3230
3279
  style={{ background: color(slots.get(item.key) ?? 1) }}
3231
3280
  />
3281
+
3232
3282
  <span className={cn(off && "line-through")}>{item.label}</span>
3233
3283
  </button>
3234
3284
  </li>
@@ -3238,6 +3288,7 @@ export function Chart({
3238
3288
  ) : (
3239
3289
  <span />
3240
3290
  )}
3291
+
3241
3292
  <Button
3242
3293
  size="sm"
3243
3294
  variant="ghost"
@@ -3289,6 +3340,7 @@ export function Chart({
3289
3340
  className={tick === 0 ? "stroke-foreground/25" : "stroke-border"}
3290
3341
  shapeRendering="crispEdges"
3291
3342
  />
3343
+
3292
3344
  <text
3293
3345
  x={margin.left - 8}
3294
3346
  y={y(tick)}
@@ -3300,6 +3352,7 @@ export function Chart({
3300
3352
  </text>
3301
3353
  </g>
3302
3354
  ))}
3355
+
3303
3356
  {data.map((row, i) =>
3304
3357
  i % every === 0 ? (
3305
3358
  <text
@@ -3360,6 +3413,7 @@ export function Chart({
3360
3413
  fillOpacity={0.1}
3361
3414
  />
3362
3415
  )}
3416
+
3363
3417
  <motion.path
3364
3418
  initial={{ d: linePath(flat(pts)) }}
3365
3419
  animate={{ d: linePath(pts) }}
@@ -3370,6 +3424,7 @@ export function Chart({
3370
3424
  strokeLinejoin="round"
3371
3425
  strokeLinecap="round"
3372
3426
  />
3427
+
3373
3428
  {(active === null ? [rowCount - 1] : [active]).map((i) =>
3374
3429
  pts[i] ? (
3375
3430
  <motion.circle
@@ -3435,6 +3490,7 @@ export function Chart({
3435
3490
  )}
3436
3491
  >
3437
3492
  <p className="mb-1.5 text-muted-foreground">{row[index]}</p>
3493
+
3438
3494
  <ul className="grid gap-1">
3439
3495
  {shown.map((item) => (
3440
3496
  <li key={item.key} className="flex items-center gap-2">
@@ -3442,7 +3498,9 @@ export function Chart({
3442
3498
  className="h-0.5 w-2.5 shrink-0 rounded-full"
3443
3499
  style={{ background: color(slots.get(item.key) ?? 1) }}
3444
3500
  />
3501
+
3445
3502
  <span className="text-muted-foreground">{item.label}</span>
3503
+
3446
3504
  <span className="ml-auto pl-3 font-medium tabular-nums">
3447
3505
  {fmt.format(num(row[item.key]))}
3448
3506
  </span>
@@ -3454,9 +3512,11 @@ export function Chart({
3454
3512
  )}
3455
3513
  </AnimatePresence>
3456
3514
  </div>
3515
+
3457
3516
  <p className="sr-only" aria-live="polite">
3458
3517
  {readout}
3459
3518
  </p>
3519
+
3460
3520
  {/* The same numbers for screen readers, search engines and no-JS. */}
3461
3521
  <div className="sr-only">{table}</div>
3462
3522
  </>
@@ -3528,6 +3588,7 @@ export function Checkbox({
3528
3588
  className="peer absolute -inset-1 z-10 cursor-pointer appearance-none rounded-[5px] outline-none disabled:cursor-not-allowed"
3529
3589
  {...props}
3530
3590
  />
3591
+
3531
3592
  <span
3532
3593
  aria-hidden="true"
3533
3594
  className={cn(
@@ -3638,10 +3699,13 @@ export function ClipCard({
3638
3699
  className="grid place-items-center overflow-hidden rounded-lg bg-foreground/[0.06] text-muted-foreground"
3639
3700
  >
3640
3701
  {thumbnail ?? <Film className="size-5" aria-hidden="true" />}
3702
+
3641
3703
  <Badge variant="secondary" className="absolute right-1.5 bottom-2 tabular-nums">
3642
3704
  <span className="sr-only">Duration </span>
3705
+
3643
3706
  {duration}
3644
3707
  </Badge>
3708
+
3645
3709
  {watched !== undefined && watched > 0 && (
3646
3710
  <Progress
3647
3711
  value={watched}
@@ -3651,8 +3715,10 @@ export function ClipCard({
3651
3715
  />
3652
3716
  )}
3653
3717
  </AspectRatio>
3718
+
3654
3719
  <div className="grid gap-0.5 px-0.5 text-left">
3655
3720
  <p className="truncate font-medium text-sm">{title}</p>
3721
+
3656
3722
  {meta && <p className="truncate text-muted-foreground text-xs">{meta}</p>}
3657
3723
  </div>
3658
3724
  </>
@@ -3672,6 +3738,7 @@ export function ClipCard({
3672
3738
  >
3673
3739
  {body}
3674
3740
  </ContextMenuTrigger>
3741
+
3675
3742
  <ContextMenuContent>{actions}</ContextMenuContent>
3676
3743
  </ContextMenu>
3677
3744
  );
@@ -3717,13 +3784,13 @@ import { OverflowFocus } from "@/lib/overflow-focus";
3717
3784
  import { cn } from "@/lib/utils";
3718
3785
 
3719
3786
  /** One line, or an inclusive \`[from, to]\` run. Numbers are the ones shown in the gutter. */
3720
- type LineRange = number | [from: number, to: number];
3787
+ export type LineRange = number | [from: number, to: number];
3721
3788
 
3722
3789
  /**
3723
3790
  * Text to mark: every match of a string, matches on one line, or an exact \`[start, end)\`
3724
3791
  * column range (String#slice indices) on one line.
3725
3792
  */
3726
- type CodeMark =
3793
+ export type CodeMark =
3727
3794
  | string
3728
3795
  | { text: string; line?: number }
3729
3796
  | { line: number; start: number; end: number };
@@ -3739,7 +3806,7 @@ type Annotations = {
3739
3806
  notes?: Record<number, ReactNode>;
3740
3807
  };
3741
3808
 
3742
- type CodeFile = Annotations & {
3809
+ export type CodeFile = Annotations & {
3743
3810
  name: string;
3744
3811
  code: string;
3745
3812
  /** Lines from \`highlightCode()\`, made ahead of time (server or build): no Shiki in the browser. */
@@ -4076,9 +4143,11 @@ function CodeLines({
4076
4143
  )}
4077
4144
  />
4078
4145
  )}
4146
+
4079
4147
  {numbered && <span style={{ minWidth: \`\${digits}ch\` }}>{lineNumber}</span>}
4080
4148
  </span>
4081
4149
  )}
4150
+
4082
4151
  {note ? (
4083
4152
  <Tooltip content={note} side="top" align="start" delay={150} touch="tap">
4084
4153
  <span
@@ -4122,8 +4191,10 @@ export function Code({
4122
4191
  >
4123
4192
  <div className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap rounded-sm font-mono text-xs outline-none [scrollbar-width:none] focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-scrollbar]:hidden">
4124
4193
  <CodeLines text={code} lines={lines} marks={annotations.marks} inline />
4194
+
4125
4195
  <OverflowFocus />
4126
4196
  </div>
4197
+
4127
4198
  <CopyButton code={code} />
4128
4199
  </div>
4129
4200
  );
@@ -4138,10 +4209,13 @@ export function Code({
4138
4209
  >
4139
4210
  <div className="flex h-8 items-center justify-between border-b px-3">
4140
4211
  <span className="font-mono text-muted-foreground text-xs">{language}</span>
4212
+
4141
4213
  <CopyButton code={code} />
4142
4214
  </div>
4215
+
4143
4216
  <div className="overflow-x-auto font-mono text-xs leading-5 outline-none [--code-px:--spacing(3)] focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset">
4144
4217
  <CodeLines text={code} lines={lines} {...annotations} />
4218
+
4145
4219
  <OverflowFocus />
4146
4220
  </div>
4147
4221
  </div>
@@ -4196,8 +4270,10 @@ export function CodePanel({ files, format, collapse, className }: CodePanelProps
4196
4270
  </TabsTrigger>
4197
4271
  ))}
4198
4272
  </TabsList>
4273
+
4199
4274
  <CopyButton code={copyText} />
4200
4275
  </div>
4276
+
4201
4277
  {files.map((tab) => (
4202
4278
  <TabsContent
4203
4279
  key={tab.name}
@@ -4215,6 +4291,7 @@ export function CodePanel({ files, format, collapse, className }: CodePanelProps
4215
4291
  </div>
4216
4292
  </TabsContent>
4217
4293
  ))}
4294
+
4218
4295
  {foldable && (
4219
4296
  <button
4220
4297
  type="button"
@@ -4329,6 +4406,7 @@ export function CollapsibleTrigger({
4329
4406
  {...props}
4330
4407
  >
4331
4408
  {children}
4409
+
4332
4410
  {chevron && (
4333
4411
  <motion.span
4334
4412
  animate={{ rotate: open ? 180 : 0 }}
@@ -4368,7 +4446,7 @@ export function CollapsibleContent({
4368
4446
  </motion.div>
4369
4447
  );
4370
4448
  }
4371
- ` }] }, combobox: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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 through hidden inputs.", docs: "https://kznlabs.github.io/labsUI/components/combobox", usage: '<Combobox options={channels} value={channel} onValueChange={setChannel} placeholder="Pick a channel…" />', examples: [{ name: "channel-picker.tsx", code: `"use client";
4449
+ ` }] }, combobox: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/combobox", usage: '<Combobox options={channels} value={channel} onValueChange={setChannel} placeholder="Pick a channel…" />', examples: [{ name: "channel-picker.tsx", code: `"use client";
4372
4450
 
4373
4451
  import { Hash } from "lucide-react";
4374
4452
  import { useState } from "react";
@@ -4411,7 +4489,7 @@ export function RolePicker() {
4411
4489
  placeholder="Add roles…"
4412
4490
  />
4413
4491
  );
4414
- }` }], api: { entries: [{ kind: "component", name: "Combobox", props: [{ name: "options", type: "ComboboxOption[]", required: true, description: "Choices; filtered as the user types." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string | string[]", required: false, description: "Value, or values when `multiple`, at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks typing and opening." }, { name: "emptyText", type: "ReactNode", required: false, description: "Shown in the list when nothing matches." }, { name: "id", type: "string", required: false, description: "Id of the input; Field fills it in." }, { name: "multiple", type: "boolean", required: false, description: "Picks stay open and show as removable tags; Backspace removes the last one." }, { name: "name", type: "string", required: false, description: "Submits the value(s) through hidden inputs (one per value when multiple)." }, { name: "onValueChange", type: "((value: string) => void) | ((value: string[]) => void)", required: false, description: "Called with the picked value, or every picked value when `multiple`." }, { name: "placeholder", type: "string", required: false, description: "Field text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string | string[]", required: false, description: "Controlled value, or values when `multiple`; pair with `onValueChange`." }], description: "Select you can type into: the input filters the list, Up and Down move, Enter picks, Esc closes.\nSingle shows the pick in the input; `multiple` shows picks as tags and keeps the list open." }], types: { ComboboxOption: "{ value: string; label: string; icon?: ReactNode; keywords?: string[]; disabled?: boolean; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/tag-input.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/combobox.tsx", type: "registry:ui", content: `"use client";
4492
+ }` }], api: { entries: [{ kind: "component", name: "Combobox", props: [{ name: "options", type: "ComboboxOption[]", required: true, description: "Choices; filtered as the user types." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string | string[]", required: false, description: "Value, or values when `multiple`, at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks typing and opening; the value isn't submitted." }, { name: "emptyText", type: "ReactNode", required: false, description: "Shown in the list when nothing matches." }, { name: "id", type: "string", required: false, description: "Id of the input; Field fills it in." }, { name: "multiple", type: "boolean", required: false, description: "Picks stay open and show as removable tags; Backspace removes the last one." }, { name: "name", type: "string", required: false, description: "Form field name: one entry per picked value." }, { name: "onValueChange", type: "((value: string) => void) | ((value: string[]) => void)", required: false, description: "Called with the picked value, or every picked value when `multiple`." }, { name: "placeholder", type: "string", required: false, description: "Field text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string | string[]", required: false, description: "Controlled value, or values when `multiple`; pair with `onValueChange`." }], description: "Select you can type into: the input filters the list, Up and Down move, Enter picks, Esc closes.\nSingle shows the pick in the input; `multiple` shows picks as tags and keeps the list open." }], types: { ComboboxOption: "{ value: string; label: string; icon?: ReactNode; keywords?: string[]; disabled?: boolean; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/tag-input.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/combobox.tsx", type: "registry:ui", content: `"use client";
4415
4493
 
4416
4494
  import { Check, ChevronsUpDown } from "lucide-react";
4417
4495
  import { AnimatePresence, motion } from "motion/react";
@@ -4419,6 +4497,7 @@ import { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState
4419
4497
  import { createPortal } from "react-dom";
4420
4498
  import { useFieldControl } from "@/components/ui/field";
4421
4499
  import { Tag, tagFieldClass } from "@/components/ui/tag-input";
4500
+ import { FormValue } from "@/lib/form-value";
4422
4501
  import { listKeyTarget, scrollIntoList } from "@/lib/list";
4423
4502
  import { useMounted } from "@/lib/platform";
4424
4503
  import { useAnchor } from "@/lib/use-anchor";
@@ -4426,7 +4505,7 @@ import { useControllableState } from "@/lib/use-controllable-state";
4426
4505
  import { useDismiss } from "@/lib/use-dismiss";
4427
4506
  import { cn, material, spring } from "@/lib/utils";
4428
4507
 
4429
- type ComboboxOption = {
4508
+ export type ComboboxOption = {
4430
4509
  /** Submitted and matched value. */
4431
4510
  value: string;
4432
4511
  /** Text shown and searched. */
@@ -4446,11 +4525,11 @@ type BaseProps = {
4446
4525
  placeholder?: string;
4447
4526
  /** Shown in the list when nothing matches. */
4448
4527
  emptyText?: ReactNode;
4449
- /** Submits the value(s) through hidden inputs (one per value when multiple). */
4528
+ /** Form field name: one entry per picked value. */
4450
4529
  name?: string;
4451
4530
  /** Id of the input; Field fills it in. */
4452
4531
  id?: string;
4453
- /** Blocks typing and opening. */
4532
+ /** Blocks typing and opening; the value isn't submitted. */
4454
4533
  disabled?: boolean;
4455
4534
  /** Form submit needs a value. */
4456
4535
  required?: boolean;
@@ -4522,6 +4601,7 @@ export function Combobox(allProps: ComboboxProps) {
4522
4601
  const box = useRef<HTMLDivElement>(null);
4523
4602
  const input = useRef<HTMLInputElement>(null);
4524
4603
  const list = useRef<HTMLDivElement>(null);
4604
+ const [invalid, setInvalid] = useState(false);
4525
4605
  // Public shape (string or string[]), "" = nothing picked.
4526
4606
  const [raw, setRaw] = useControllableState<string | string[] | undefined>(
4527
4607
  valueProp,
@@ -4635,11 +4715,13 @@ export function Combobox(allProps: ComboboxProps) {
4635
4715
  ))}
4636
4716
  </AnimatePresence>
4637
4717
  )}
4718
+
4638
4719
  {!multiple && !open && selected[0]?.icon && (
4639
4720
  <span className="mr-2 flex shrink-0 text-muted-foreground [&_svg]:size-4">
4640
4721
  {selected[0].icon}
4641
4722
  </span>
4642
4723
  )}
4724
+
4643
4725
  <input
4644
4726
  ref={input}
4645
4727
  id={id}
@@ -4653,6 +4735,7 @@ export function Combobox(allProps: ComboboxProps) {
4653
4735
  aria-required={required}
4654
4736
  disabled={disabled}
4655
4737
  {...aria}
4738
+ aria-invalid={aria["aria-invalid"] || (invalid && !values.length) || undefined}
4656
4739
  value={text}
4657
4740
  placeholder={hint}
4658
4741
  onChange={(event) => {
@@ -4665,15 +4748,26 @@ export function Combobox(allProps: ComboboxProps) {
4665
4748
  multiple && "px-1",
4666
4749
  )}
4667
4750
  />
4751
+
4668
4752
  <ChevronsUpDown
4669
4753
  className="ml-1 size-4 shrink-0 self-center text-muted-foreground"
4670
4754
  aria-hidden="true"
4671
4755
  />
4672
4756
  </div>
4757
+
4673
4758
  {name &&
4674
4759
  (multiple ? values : values.slice(0, 1)).map((picked) => (
4675
- <input key={picked} type="hidden" name={name} value={picked} />
4760
+ <input key={picked} type="hidden" name={name} value={picked} disabled={disabled} />
4676
4761
  ))}
4762
+
4763
+ <FormValue
4764
+ value={values.join(",")}
4765
+ required={required}
4766
+ disabled={disabled}
4767
+ anchor={input}
4768
+ onInvalid={() => setInvalid(true)}
4769
+ />
4770
+
4677
4771
  {mounted &&
4678
4772
  createPortal(
4679
4773
  <AnimatePresence>
@@ -4729,7 +4823,9 @@ export function Combobox(allProps: ComboboxProps) {
4729
4823
  {values.includes(option.value) && (
4730
4824
  <Check className="!text-current absolute left-2" aria-hidden="true" />
4731
4825
  )}
4826
+
4732
4827
  {option.icon}
4828
+
4733
4829
  <span className="truncate">{option.label}</span>
4734
4830
  </div>
4735
4831
  ))
@@ -5001,6 +5097,7 @@ export function CommandGroup({
5001
5097
  {heading}
5002
5098
  </div>
5003
5099
  )}
5100
+
5004
5101
  {children}
5005
5102
  </div>
5006
5103
  );
@@ -5114,6 +5211,7 @@ export function CommandDialog({
5114
5211
  )}
5115
5212
  >
5116
5213
  <DialogTitle className="sr-only">{title}</DialogTitle>
5214
+
5117
5215
  <Command close={() => setOpen(false)} className="rounded-none border-0 bg-transparent">
5118
5216
  {children}
5119
5217
  </Command>
@@ -5176,7 +5274,7 @@ import {
5176
5274
  DialogTitle,
5177
5275
  } from "@/components/ui/dialog";
5178
5276
 
5179
- type ConfirmOptions = {
5277
+ export type ConfirmOptions = {
5180
5278
  /** Question in the dialog heading. */
5181
5279
  title: ReactNode;
5182
5280
  /** Consequences, under the title. */
@@ -5240,10 +5338,13 @@ export function Confirmer() {
5240
5338
  <DialogContent alert className="max-w-md">
5241
5339
  <DialogHeader>
5242
5340
  <DialogTitle>{title}</DialogTitle>
5341
+
5243
5342
  {description && <DialogDescription>{description}</DialogDescription>}
5244
5343
  </DialogHeader>
5344
+
5245
5345
  <DialogFooter>
5246
5346
  <DialogClose>{cancelLabel}</DialogClose>
5347
+
5247
5348
  <Button variant={destructive ? "destructive" : "default"} onClick={() => settle(true)}>
5248
5349
  {options.confirmLabel ?? (destructive ? "Delete" : "Continue")}
5249
5350
  </Button>
@@ -5496,9 +5597,11 @@ export function CountTo({
5496
5597
  return (
5497
5598
  <span ref={ref} className={className}>
5498
5599
  <span className="sr-only">{target}</span>
5600
+
5499
5601
  <span ref={out} aria-hidden="true" data-count-to-animated="">
5500
5602
  {text(from)}
5501
5603
  </span>
5604
+
5502
5605
  <span aria-hidden="true" data-count-to-fallback="" className="hidden">
5503
5606
  {target}
5504
5607
  </span>
@@ -5607,6 +5710,7 @@ export function CustomCursor() {
5607
5710
  }}
5608
5711
  transition={spring}
5609
5712
  />
5713
+
5610
5714
  <motion.div
5611
5715
  className="absolute top-0 left-0 size-1.5 rounded-full bg-foreground"
5612
5716
  style={{ x, y, translateX: "-50%", translateY: "-50%" }}
@@ -5669,7 +5773,7 @@ export function MembersDataTable({
5669
5773
  )}
5670
5774
  />
5671
5775
  );
5672
- }` }], api: { entries: [{ kind: "component", name: "DataTable", props: [{ name: "columns", type: "DataTableColumn<T>[]", required: true, description: "Column definitions in display order." }, { name: "data", type: "T[]", required: true, description: "Rows; search, sort and paging happen on the client." }, { name: "getRowId", type: "(row: T) => string", required: true, description: "Stable id per row; selection is kept by it across pages." }, { name: "label", type: "string", required: true, description: `Names the table, the search box and each row's controls: "Members", "Select Jane".` }, { name: "rowLabel", type: "(row: T) => string", required: true, description: "Row name for screen readers, e.g. on its checkbox and ⋯ menu." }, { name: "defaultSort", type: "Sort", required: false, description: "Sort at first render." }, { name: "onSelectionChange", type: "(ids: string[]) => void", required: false, description: "Called with every selected row id." }, { name: "pageSize", type: "number", required: false, default: "10", description: "Rows per page." }, { name: "rowActions", type: "(row: T) => ReactNode", required: false, description: "Items for the row's ⋯ menu (DropdownMenuItem and friends)." }, { name: "searchable", type: "boolean", required: false, default: "true", description: "Show a search field that filters every column." }, { name: "searchPlaceholder", type: "string", required: false, default: '"Search…"', description: "Search field placeholder." }, { name: "selectable", type: "boolean", required: false, default: "false", description: "Checkbox column; selection survives search and paging." }, { name: "selection", type: "string[]", required: false, description: "Controlled selected row ids; pair with `onSelectionChange`." }, { name: "striped", type: "boolean", required: false, default: "false", description: "Alternate row backgrounds." }, { name: "toolbar", type: "(selected: T[]) => ReactNode", required: false, description: "Right side of the toolbar; receives the selected rows (bulk actions)." }], description: "Table with search, sortable columns, row selection, row actions and pagination, all on the\nclient. Columns are data: `{ id, header, accessor, cell?, sortable? }`. For server-driven\ndata, compose Table + Pagination yourself." }], types: { Sort: '{ id: string; dir: "ascending" | "descending" }', DataTableColumn: "{ id: string; header: ReactNode; accessor?: (row: T) => string | number; cell?: (row: T) => ReactNode; sortable?: boolean; className?: string; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/checkbox.json", "https://kznlabs.github.io/labsUI/r/dropdown-menu.json", "https://kznlabs.github.io/labsUI/r/empty.json", "https://kznlabs.github.io/labsUI/r/pagination.json", "https://kznlabs.github.io/labsUI/r/search-input.json", "https://kznlabs.github.io/labsUI/r/table.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/data-table.tsx", type: "registry:ui", content: `"use client";
5776
+ }` }], api: { entries: [{ kind: "component", name: "DataTable", props: [{ name: "columns", type: "DataTableColumn<T>[]", required: true, description: "Column definitions in display order." }, { name: "data", type: "T[]", required: true, description: "Rows; search, sort and paging happen on the client." }, { name: "getRowId", type: "(row: T) => string", required: true, description: "Stable id per row; selection is kept by it across pages." }, { name: "label", type: "string", required: true, description: `Names the table, the search box and each row's controls: "Members", "Select Jane".` }, { name: "rowLabel", type: "(row: T) => string", required: true, description: "Row name for screen readers, e.g. on its checkbox and ⋯ menu." }, { name: "defaultSort", type: "DataTableSort", required: false, description: "Sort at first render." }, { name: "onSelectionChange", type: "(ids: string[]) => void", required: false, description: "Called with every selected row id." }, { name: "pageSize", type: "number", required: false, default: "10", description: "Rows per page." }, { name: "rowActions", type: "(row: T) => ReactNode", required: false, description: "Items for the row's ⋯ menu (DropdownMenuItem and friends)." }, { name: "searchable", type: "boolean", required: false, default: "true", description: "Show a search field that filters every column." }, { name: "searchPlaceholder", type: "string", required: false, default: '"Search…"', description: "Search field placeholder." }, { name: "selectable", type: "boolean", required: false, default: "false", description: "Checkbox column; selection survives search and paging." }, { name: "selection", type: "string[]", required: false, description: "Controlled selected row ids; pair with `onSelectionChange`." }, { name: "striped", type: "boolean", required: false, default: "false", description: "Alternate row backgrounds." }, { name: "toolbar", type: "(selected: T[]) => ReactNode", required: false, description: "Right side of the toolbar; receives the selected rows (bulk actions)." }], description: "Table with search, sortable columns, row selection, row actions and pagination, all on the\nclient. Columns are data: `{ id, header, accessor, cell?, sortable? }`. For server-driven\ndata, compose Table + Pagination yourself." }], types: { DataTableSort: '{ id: string; dir: "ascending" | "descending" }', DataTableColumn: "{ id: string; header: ReactNode; accessor?: (row: T) => string | number; cell?: (row: T) => ReactNode; sortable?: boolean; className?: string; }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/checkbox.json", "https://kznlabs.github.io/labsUI/r/dropdown-menu.json", "https://kznlabs.github.io/labsUI/r/empty.json", "https://kznlabs.github.io/labsUI/r/pagination.json", "https://kznlabs.github.io/labsUI/r/search-input.json", "https://kznlabs.github.io/labsUI/r/table.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/data-table.tsx", type: "registry:ui", content: `"use client";
5673
5777
 
5674
5778
  import { MoreHorizontal, SearchX } from "lucide-react";
5675
5779
  import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
@@ -5694,7 +5798,7 @@ import {
5694
5798
  import { useControllableState } from "@/lib/use-controllable-state";
5695
5799
  import { cn } from "@/lib/utils";
5696
5800
 
5697
- type Sort = { id: string; dir: "ascending" | "descending" };
5801
+ export type DataTableSort = { id: string; dir: "ascending" | "descending" };
5698
5802
 
5699
5803
  export type DataTableColumn<T> = {
5700
5804
  /** Unique key; also used by \`defaultSort\`. */
@@ -5723,7 +5827,7 @@ type DataTableProps<T> = {
5723
5827
  /** Rows per page. */
5724
5828
  pageSize?: number;
5725
5829
  /** Sort at first render. */
5726
- defaultSort?: Sort;
5830
+ defaultSort?: DataTableSort;
5727
5831
  /** Show a search field that filters every column. */
5728
5832
  searchable?: boolean;
5729
5833
  /** Search field placeholder. */
@@ -5852,6 +5956,7 @@ export function DataTable<T>({
5852
5956
  className="w-full sm:max-w-64"
5853
5957
  />
5854
5958
  )}
5959
+
5855
5960
  {toolbar && (
5856
5961
  <div className="ml-auto flex items-center gap-2">
5857
5962
  {toolbar(data.filter((row) => selected.includes(getRowId(row))))}
@@ -5881,6 +5986,7 @@ export function DataTable<T>({
5881
5986
  />
5882
5987
  </TableHead>
5883
5988
  )}
5989
+
5884
5990
  {columns.map((column) => (
5885
5991
  <TableHead
5886
5992
  key={column.id}
@@ -5904,6 +6010,7 @@ export function DataTable<T>({
5904
6010
  {column.header}
5905
6011
  </TableHead>
5906
6012
  ))}
6013
+
5907
6014
  {rowActions && (
5908
6015
  <TableHead className="w-px">
5909
6016
  <span className="sr-only">Actions</span>
@@ -5911,6 +6018,7 @@ export function DataTable<T>({
5911
6018
  )}
5912
6019
  </TableRow>
5913
6020
  </TableHeader>
6021
+
5914
6022
  <TableBody>
5915
6023
  {rows.length === 0 ? (
5916
6024
  <TableRow className="hover:bg-transparent">
@@ -5946,11 +6054,13 @@ export function DataTable<T>({
5946
6054
  />
5947
6055
  </TableCell>
5948
6056
  )}
6057
+
5949
6058
  {columns.map((column) => (
5950
6059
  <TableCell key={column.id} className={column.className}>
5951
6060
  {column.cell ? column.cell(row) : column.accessor?.(row)}
5952
6061
  </TableCell>
5953
6062
  ))}
6063
+
5954
6064
  {rowActions && (
5955
6065
  <TableCell className="py-1 pr-2">
5956
6066
  <DropdownMenu>
@@ -5962,6 +6072,7 @@ export function DataTable<T>({
5962
6072
  >
5963
6073
  <MoreHorizontal />
5964
6074
  </DropdownMenuTrigger>
6075
+
5965
6076
  <DropdownMenuContent align="end">{rowActions(row)}</DropdownMenuContent>
5966
6077
  </DropdownMenu>
5967
6078
  </TableCell>
@@ -5978,10 +6089,12 @@ export function DataTable<T>({
5978
6089
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5979
6090
  <p className="text-muted-foreground text-xs tabular-nums" aria-live="polite">
5980
6091
  {selectable && selected.length > 0 && \`\${selected.length} selected · \`}
6092
+
5981
6093
  {sorted.length === 0
5982
6094
  ? "0 results"
5983
6095
  : \`\${from}–\${from + rows.length - 1} of \${sorted.length}\`}
5984
6096
  </p>
6097
+
5985
6098
  {pages > 1 && (
5986
6099
  <Pagination
5987
6100
  page={page}
@@ -5994,7 +6107,7 @@ export function DataTable<T>({
5994
6107
  </div>
5995
6108
  );
5996
6109
  }
5997
- ` }] }, "date-picker": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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 through a hidden input. Works inside Field.", docs: "https://kznlabs.github.io/labsUI/components/date-picker", usage: '<Field label="Report period"><DatePicker mode="range" name="period" presets={[{ label: "Last 7 days", value: { from, to } }]} /></Field>', examples: [{ name: "report-filters.tsx", code: `"use client";
6110
+ ` }] }, "date-picker": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/date-picker", usage: '<Field label="Report period"><DatePicker mode="range" name="period" presets={[{ label: "Last 7 days", value: { from, to } }]} /></Field>', examples: [{ name: "report-filters.tsx", code: `"use client";
5998
6111
 
5999
6112
  import { DatePicker } from "@/components/ui/date-picker";
6000
6113
  import { Field } from "@/components/ui/field";
@@ -6017,19 +6130,20 @@ export function ReportFilters() {
6017
6130
  />
6018
6131
  </Field>
6019
6132
  );
6020
- }` }], api: { entries: [{ kind: "component", name: "DatePicker", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "Date | DateRange | null", required: false, description: "Day or range at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "isDisabled", type: "(date: Date) => boolean", required: false, description: "Extra rule, e.g. weekends off. Client-only (a function)." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the button text and the calendar." }, { name: "max", type: "Date", required: false, description: "Latest selectable day." }, { name: "min", type: "Date", required: false, description: "Earliest selectable day." }, { name: "mode", type: '"single" | "range"', required: false, description: '"single": pick one day. "range": pick a start, then an end.' }, { name: "name", type: "string", required: false, description: "Submits ISO through a hidden input: `2026-10-21`, or `2026-10-03/2026-10-09` for a range." }, { name: "onValueChange", type: "((date: Date) => void) | ((range: DateRange) => void)", required: false, description: "Called with the picked day, or with the range once both ends are set." }, { name: "placeholder", type: "string", required: false, description: "Button text when nothing is picked." }, { name: "presets", type: "Preset<Date>[] | Preset<DateRange>[]", required: false, description: "Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a date." }, { name: "value", type: "Date | DateRange | null", required: false, description: "Controlled day, or `{ from, to }` in range mode; null = nothing picked." }, { name: "weekStartsOn", type: "0 | 1 | 2 | 3 | 4 | 5 | 6", required: false, description: "0 is Sunday, 6 is Saturday. Default 1 (Monday, ISO)." }], description: `Field-styled button that opens a Calendar in a Popover, focused on the picked day. A single
6021
- date closes on pick; a range closes once both ends are set. Optional presets, form-ready.` }], types: { Preset: "{ label: string; value: Value }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/calendar.json", "https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/popover.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/date-picker.tsx", type: "registry:ui", content: `"use client";
6133
+ }` }], api: { entries: [{ kind: "component", name: "DatePicker", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "Date | DateRange | null", required: false, description: "Day or range at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening; the value isn't submitted." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "isDisabled", type: "(date: Date) => boolean", required: false, description: "Extra rule, e.g. weekends off. Client-only (a function)." }, { name: "locale", type: "string", required: false, description: "BCP 47 locale for the button text and the calendar." }, { name: "max", type: "Date", required: false, description: "Latest selectable day." }, { name: "min", type: "Date", required: false, description: "Earliest selectable day." }, { name: "mode", type: '"single" | "range"', required: false, description: '"single": pick one day. "range": pick a start, then an end.' }, { name: "name", type: "string", required: false, description: "Submits ISO: `2026-10-21`, or `2026-10-03/2026-10-09` for a range." }, { name: "onValueChange", type: "((date: Date) => void) | ((range: DateRange) => void)", required: false, description: "Called with the picked day, or with the range once both ends are set." }, { name: "placeholder", type: "string", required: false, description: "Button text when nothing is picked." }, { name: "presets", type: "DatePickerPreset<Date>[] | DatePickerPreset<DateRange>[]", required: false, description: "Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a date." }, { name: "value", type: "Date | DateRange | null", required: false, description: "Controlled day, or `{ from, to }` in range mode; null = nothing picked." }, { name: "weekStartsOn", type: "0 | 1 | 2 | 3 | 4 | 5 | 6", required: false, description: "0 is Sunday, 6 is Saturday. Default 1 (Monday, ISO)." }], description: `Field-styled button that opens a Calendar in a Popover, focused on the picked day. A single
6134
+ date closes on pick; a range closes once both ends are set. Optional presets, form-ready.` }], types: { DatePickerPreset: "{ label: string; value: Value }" } }, dependencies: ["lucide-react@1.63.0"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/calendar.json", "https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/popover.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/date-picker.tsx", type: "registry:ui", content: `"use client";
6022
6135
 
6023
6136
  import { CalendarDays } from "lucide-react";
6024
- import { useState } from "react";
6137
+ import { useRef, useState } from "react";
6025
6138
  import { Calendar, type DateRange, key } from "@/components/ui/calendar";
6026
6139
  import { useFieldControl } from "@/components/ui/field";
6027
6140
  import { fieldClass } from "@/components/ui/input";
6028
6141
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
6142
+ import { FormValue } from "@/lib/form-value";
6029
6143
  import { useControllableState } from "@/lib/use-controllable-state";
6030
6144
  import { cn } from "@/lib/utils";
6031
6145
 
6032
- type Preset<Value> = { label: string; value: Value };
6146
+ export type DatePickerPreset<Value> = { label: string; value: Value };
6033
6147
 
6034
6148
  type Base = {
6035
6149
  /** Button text when nothing is picked. */
@@ -6044,11 +6158,11 @@ type Base = {
6044
6158
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
6045
6159
  /** BCP 47 locale for the button text and the calendar. */
6046
6160
  locale?: string;
6047
- /** Submits ISO through a hidden input: \`2026-10-21\`, or \`2026-10-03/2026-10-09\` for a range. */
6161
+ /** Submits ISO: \`2026-10-21\`, or \`2026-10-03/2026-10-09\` for a range. */
6048
6162
  name?: string;
6049
6163
  /** Pair with <Label htmlFor>, or let <Field> fill it in. */
6050
6164
  id?: string;
6051
- /** Blocks opening. */
6165
+ /** Blocks opening; the value isn't submitted. */
6052
6166
  disabled?: boolean;
6053
6167
  /** Form submit needs a date. */
6054
6168
  required?: boolean;
@@ -6071,7 +6185,7 @@ type SingleProps = Base & {
6071
6185
  /** Called with the picked day, or with the range once both ends are set. */
6072
6186
  onValueChange?: (date: Date) => void;
6073
6187
  /** Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first. */
6074
- presets?: Preset<Date>[];
6188
+ presets?: DatePickerPreset<Date>[];
6075
6189
  };
6076
6190
 
6077
6191
  type RangeProps = Base & {
@@ -6079,7 +6193,7 @@ type RangeProps = Base & {
6079
6193
  value?: DateRange | null;
6080
6194
  defaultValue?: DateRange | null;
6081
6195
  onValueChange?: (range: DateRange) => void;
6082
- presets?: Preset<DateRange>[];
6196
+ presets?: DatePickerPreset<DateRange>[];
6083
6197
  };
6084
6198
 
6085
6199
  type DatePickerProps = SingleProps | RangeProps;
@@ -6102,9 +6216,12 @@ export function DatePicker(allProps: DatePickerProps) {
6102
6216
  weekStartsOn,
6103
6217
  locale = "en-US",
6104
6218
  name,
6219
+ required,
6105
6220
  className,
6106
6221
  ...trigger
6107
6222
  } = useFieldControl(allProps);
6223
+ const anchor = useRef<HTMLButtonElement>(null);
6224
+ const [invalid, setInvalid] = useState(false);
6108
6225
  const range = mode === "range";
6109
6226
  const [open, setOpen] = useState(false);
6110
6227
  const [value, setValue] = useControllableState<Date | DateRange | null>(
@@ -6143,6 +6260,7 @@ export function DatePicker(allProps: DatePickerProps) {
6143
6260
  <>
6144
6261
  <Popover open={open} onOpenChange={setOpen}>
6145
6262
  <PopoverTrigger
6263
+ ref={anchor}
6146
6264
  variant="outline"
6147
6265
  className={cn(
6148
6266
  fieldClass,
@@ -6151,10 +6269,13 @@ export function DatePicker(allProps: DatePickerProps) {
6151
6269
  className,
6152
6270
  )}
6153
6271
  {...trigger}
6272
+ aria-invalid={trigger["aria-invalid"] || (invalid && !iso) || undefined}
6154
6273
  >
6155
6274
  <CalendarDays />
6275
+
6156
6276
  <span className="truncate">{label ?? placeholder}</span>
6157
6277
  </PopoverTrigger>
6278
+
6158
6279
  <PopoverContent align="start" className="flex w-auto flex-col gap-3 p-3 sm:flex-row">
6159
6280
  {presets && presets.length > 0 && (
6160
6281
  // No room beside the calendar on phones.
@@ -6178,6 +6299,7 @@ export function DatePicker(allProps: DatePickerProps) {
6178
6299
  ))}
6179
6300
  </ul>
6180
6301
  )}
6302
+
6181
6303
  {range ? (
6182
6304
  <Calendar key={shown} mode="range" {...shared} value={span} onValueChange={commit} />
6183
6305
  ) : (
@@ -6190,7 +6312,15 @@ export function DatePicker(allProps: DatePickerProps) {
6190
6312
  )}
6191
6313
  </PopoverContent>
6192
6314
  </Popover>
6193
- {name && <input type="hidden" name={name} value={iso} />}
6315
+
6316
+ <FormValue
6317
+ name={name}
6318
+ value={iso}
6319
+ required={required}
6320
+ disabled={trigger.disabled}
6321
+ anchor={anchor}
6322
+ onInvalid={() => setInvalid(true)}
6323
+ />
6194
6324
  </>
6195
6325
  );
6196
6326
  }
@@ -6219,8 +6349,8 @@ export function EditProfile() {
6219
6349
  </DialogContent>
6220
6350
  </Dialog>
6221
6351
  );
6222
- }` }], api: { entries: [{ kind: "function", name: "useDialog", signature: "useDialog() => Ctx" }, { kind: "component", name: "Dialog", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "State root. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DialogTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DialogClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "function", name: "useModal", signature: "useModal() => RefObject<HTMLDivElement | null>", description: `Modal behavior shared by Dialog and Drawer: focus first field, trap Tab, restore focus,
6223
- lock page scroll, close on Esc (topmost modal only). Attach the returned ref to the panel.` }, { kind: "component", name: "DialogContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "alert", type: "boolean", required: false, default: "false", description: 'Confirm-a-destructive-action mode: role="alertdialog", backdrop click does not close.' }, { name: "showClose", type: "boolean", required: false, default: "!alert", description: "Close button in the corner. Off by default for `alert` dialogs." }], description: "Portal + blurred backdrop + spring scale-in. Esc/backdrop close, focus trap, scroll lock." }, { kind: "component", name: "DialogHeader", props: [], extends: "<div>" }, { kind: "component", name: "DialogFooter", props: [], extends: "<div>" }, { kind: "component", name: "DialogTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DialogDescription", props: [], extends: "<p>" }], types: { Ctx: "{ open: boolean; setOpen: (open: boolean) => void; titleId: string; descId: string }" } }, 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/platform.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dialog.tsx", type: "registry:ui", content: `"use client";
6352
+ }` }], api: { entries: [{ kind: "function", name: "useDialog", signature: "useDialog() => DialogState" }, { kind: "component", name: "Dialog", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "State root. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DialogTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DialogClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "function", name: "useModal", signature: "useModal() => RefObject<HTMLDivElement | null>", description: `Modal behavior shared by Dialog and Drawer: focus first field, trap Tab, restore focus,
6353
+ lock page scroll, close on Esc (topmost modal only). Attach the returned ref to the panel.` }, { kind: "component", name: "DialogContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "alert", type: "boolean", required: false, default: "false", description: 'Confirm-a-destructive-action mode: role="alertdialog", backdrop click does not close.' }, { name: "showClose", type: "boolean", required: false, default: "!alert", description: "Close button in the corner. Off by default for `alert` dialogs." }], description: "Portal + blurred backdrop + spring scale-in. Esc/backdrop close, focus trap, scroll lock." }, { kind: "component", name: "DialogHeader", props: [], extends: "<div>" }, { kind: "component", name: "DialogFooter", props: [], extends: "<div>" }, { kind: "component", name: "DialogTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DialogDescription", props: [], extends: "<p>" }], types: { DialogState: "{ open: boolean; setOpen: (open: boolean) => void; titleId: string; descId: string; }" } }, 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/platform.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dialog.tsx", type: "registry:ui", content: `"use client";
6224
6354
 
6225
6355
  import { X } from "lucide-react";
6226
6356
  import { AnimatePresence, motion, useIsPresent } from "motion/react";
@@ -6240,9 +6370,14 @@ import { useControllableState } from "@/lib/use-controllable-state";
6240
6370
  import { cn, spring } from "@/lib/utils";
6241
6371
  import { Button } from "./button";
6242
6372
 
6243
- type Ctx = { open: boolean; setOpen: (open: boolean) => void; titleId: string; descId: string };
6373
+ export type DialogState = {
6374
+ open: boolean;
6375
+ setOpen: (open: boolean) => void;
6376
+ titleId: string;
6377
+ descId: string;
6378
+ };
6244
6379
 
6245
- const DialogContext = createContext<Ctx | null>(null);
6380
+ const DialogContext = createContext<DialogState | null>(null);
6246
6381
 
6247
6382
  export function useDialog() {
6248
6383
  const ctx = use(DialogContext);
@@ -6414,6 +6549,7 @@ export function DialogContent({
6414
6549
  transition={spring}
6415
6550
  onClick={alert ? undefined : () => setOpen(false)}
6416
6551
  />
6552
+
6417
6553
  <motion.div
6418
6554
  ref={ref}
6419
6555
  role={alert ? "alertdialog" : "dialog"}
@@ -6431,6 +6567,7 @@ export function DialogContent({
6431
6567
  )}
6432
6568
  >
6433
6569
  {children}
6570
+
6434
6571
  {showClose && (
6435
6572
  <button
6436
6573
  type="button"
@@ -6438,6 +6575,7 @@ export function DialogContent({
6438
6575
  className="absolute top-4 right-4 cursor-pointer rounded-md p-1 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
6439
6576
  >
6440
6577
  <X className="size-4" aria-hidden="true" />
6578
+
6441
6579
  <span className="sr-only">Close</span>
6442
6580
  </button>
6443
6581
  )}
@@ -6519,8 +6657,8 @@ export function CartDrawer({ items }: { items: string[] }) {
6519
6657
  </DrawerContent>
6520
6658
  </Drawer>
6521
6659
  );
6522
- }` }], api: { entries: [{ kind: "component", name: "Drawer", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }] }, { kind: "component", name: "DrawerTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DrawerDescription", props: [], extends: "<p>" }, { kind: "component", name: "DrawerContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "showClose", type: "boolean", required: false, default: 'side === "left" || side === "right"', description: "Close button in the corner. On by default for side drawers." }, { name: "side", type: "Side", required: false, default: '"right"', description: "Edge it slides in from." }], description: `Panel that slides in from an edge on a spring. Swipe toward the edge to dismiss: side drawers
6523
- drag from anywhere, top/bottom drawers from the handle or header so their content can scroll.` }, { kind: "component", name: "DrawerHeader", props: [], extends: "<div>", description: "Also a drag handle on top/bottom drawers." }, { kind: "component", name: "DrawerBody", props: [], extends: "<div>", description: "Scrolls when content is taller than the drawer." }, { kind: "component", name: "DrawerFooter", props: [], extends: "<div>" }], types: { Side: '"left" | "right" | "top" | "bottom"' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/dialog.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/drawer.tsx", type: "registry:ui", content: `"use client";
6660
+ }` }], api: { entries: [{ kind: "component", name: "Drawer", props: [{ name: "children", type: "ReactNode", required: true, description: "`DialogTrigger` and `DialogContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, backdrop click or `DialogClose`." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }] }, { kind: "component", name: "DrawerTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerClose", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, default: '"outline"', description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "DrawerTitle", props: [], extends: "<h2>" }, { kind: "component", name: "DrawerDescription", props: [], extends: "<p>" }, { kind: "component", name: "DrawerContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Header, body and footer." }, { name: "showClose", type: "boolean", required: false, default: 'side === "left" || side === "right"', description: "Close button in the corner. On by default for side drawers." }, { name: "side", type: "DrawerSide", required: false, default: '"right"', description: "Edge it slides in from." }], description: `Panel that slides in from an edge on a spring. Swipe toward the edge to dismiss: side drawers
6661
+ drag from anywhere, top/bottom drawers from the handle or header so their content can scroll.` }, { kind: "component", name: "DrawerHeader", props: [], extends: "<div>", description: "Also a drag handle on top/bottom drawers." }, { kind: "component", name: "DrawerBody", props: [], extends: "<div>", description: "Scrolls when content is taller than the drawer." }, { kind: "component", name: "DrawerFooter", props: [], extends: "<div>" }], types: { DrawerSide: '"left" | "right" | "top" | "bottom"' } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/dialog.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/drawer.tsx", type: "registry:ui", content: `"use client";
6524
6662
 
6525
6663
  import { X } from "lucide-react";
6526
6664
  import { AnimatePresence, motion, type PanInfo, useDragControls, useIsPresent } from "motion/react";
@@ -6546,9 +6684,9 @@ export const DrawerClose = DialogClose;
6546
6684
  export const DrawerTitle = DialogTitle;
6547
6685
  export const DrawerDescription = DialogDescription;
6548
6686
 
6549
- type Side = "left" | "right" | "top" | "bottom";
6687
+ export type DrawerSide = "left" | "right" | "top" | "bottom";
6550
6688
 
6551
- const offscreen: Record<Side, { x?: string; y?: string }> = {
6689
+ const offscreen: Record<DrawerSide, { x?: string; y?: string }> = {
6552
6690
  left: { x: "-100%" },
6553
6691
  right: { x: "100%" },
6554
6692
  top: { y: "-100%" },
@@ -6557,7 +6695,7 @@ const offscreen: Record<Side, { x?: string; y?: string }> = {
6557
6695
 
6558
6696
  // The \`after:\` bleed paints past the screen edge, so overshoot or dragging inward never shows a
6559
6697
  // gap.
6560
- const placement: Record<Side, string> = {
6698
+ const placement: Record<DrawerSide, string> = {
6561
6699
  left: "inset-y-0 left-0 h-full w-[min(20rem,85vw)] border-r after:right-full after:inset-y-0 after:w-[50vw]",
6562
6700
  right:
6563
6701
  "inset-y-0 right-0 h-full w-[min(24rem,90vw)] border-l after:left-full after:inset-y-0 after:w-[50vw]",
@@ -6581,7 +6719,7 @@ export function DrawerContent({
6581
6719
  children,
6582
6720
  }: {
6583
6721
  /** Edge it slides in from. */
6584
- side?: Side;
6722
+ side?: DrawerSide;
6585
6723
  /** Close button in the corner. On by default for side drawers. */
6586
6724
  showClose?: boolean;
6587
6725
  className?: string;
@@ -6630,6 +6768,7 @@ export function DrawerContent({
6630
6768
  transition={spring}
6631
6769
  onClick={() => setOpen(false)}
6632
6770
  />
6771
+
6633
6772
  <motion.div
6634
6773
  ref={ref}
6635
6774
  role="dialog"
@@ -6663,8 +6802,11 @@ export function DrawerContent({
6663
6802
  )}
6664
6803
  >
6665
6804
  {side === "bottom" && <Handle />}
6805
+
6666
6806
  {children}
6807
+
6667
6808
  {side === "top" && <Handle />}
6809
+
6668
6810
  {showClose && (
6669
6811
  <button
6670
6812
  type="button"
@@ -6673,6 +6815,7 @@ export function DrawerContent({
6673
6815
  className="absolute top-4 right-4 cursor-pointer rounded-md p-1 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
6674
6816
  >
6675
6817
  <X className="size-4" aria-hidden="true" />
6818
+
6676
6819
  <span className="sr-only">Close</span>
6677
6820
  </button>
6678
6821
  )}
@@ -6773,7 +6916,7 @@ export function ClipActions({ onDelete }: { onDelete: () => void }) {
6773
6916
  </DropdownMenuContent>
6774
6917
  </DropdownMenu>
6775
6918
  );
6776
- }` }], api: { entries: [{ kind: "component", name: "DropdownMenu", props: [{ name: "children", type: "ReactNode", required: true, description: "Trigger and content." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, outside click or a selection." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Action menu anchored to a button. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DropdownMenuTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button", description: "A Button. Keyboard: Enter, Space or Down opens on the first item, Up on the last." }, { kind: "component", name: "DropdownMenuContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Items, labels, groups and separators." }, { name: "align", type: '"start" | "end"', required: false, default: '"start"', description: "Edge of the trigger the menu lines up with." }] }, { kind: "component", name: "DropdownMenuItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "onSelect", type: "() => void", required: false, description: "Called on click or Enter; the menu then closes." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }, { name: "variant", type: '"default" | "destructive"', required: false, default: '"default"', description: '"destructive": red, for irreversible actions.' }] }, { kind: "component", name: "DropdownMenuCheckboxItem", props: [{ name: "checked", type: "boolean", required: true, description: "Shows the check mark." }, { name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "onCheckedChange", type: "(checked: boolean) => void", required: true, description: "Called with the new state." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }], description: "Toggles without closing the menu." }, { kind: "component", name: "DropdownMenuRadioGroup", props: [{ name: "children", type: "ReactNode", required: true, description: "`DropdownMenuRadioItem`s." }, { name: "onValueChange", type: "(value: string) => void", required: true, description: "Called with the picked item's value." }, { name: "value", type: "string", required: true, description: "Value of the checked item." }] }, { kind: "component", name: "DropdownMenuRadioItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "value", type: "string", required: true, description: "Checked when it equals the group's `value`." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One choice of a RadioGroup; picking it keeps the menu open." }, { kind: "component", name: "DropdownMenuLabel", props: [], extends: "<div>" }, { kind: "component", name: "DropdownMenuSeparator", props: [] }], types: {} }, 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/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dropdown-menu.tsx", type: "registry:ui", content: `"use client";
6919
+ }` }], api: { entries: [{ kind: "component", name: "DropdownMenu", props: [{ name: "children", type: "ReactNode", required: true, description: "Trigger and content." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc, outside click or a selection." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Action menu anchored to a button. Controlled (`open` + `onOpenChange`) or uncontrolled." }, { kind: "component", name: "DropdownMenuTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button", description: "A Button. Keyboard: Enter, Space or Down opens on the first item, Up on the last." }, { kind: "component", name: "DropdownMenuContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Items, labels, groups and separators." }, { name: "align", type: '"start" | "end"', required: false, default: '"start"', description: "Edge of the trigger the menu lines up with." }] }, { kind: "component", name: "DropdownMenuItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "onSelect", type: "() => void", required: false, description: "Called on click or Enter; the menu then closes." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }, { name: "variant", type: '"default" | "destructive"', required: false, default: '"default"', description: '"destructive": red, for irreversible actions.' }] }, { kind: "component", name: "DropdownMenuCheckboxItem", props: [{ name: "checked", type: "boolean", required: true, description: "Shows the check mark." }, { name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "onCheckedChange", type: "(checked: boolean) => void", required: true, description: "Called with the new state." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }, { name: "shortcut", type: "string", required: false, description: 'Right-aligned hint, e.g. "⌘S".' }], description: "Toggles without closing the menu." }, { kind: "component", name: "DropdownMenuRadioGroup", props: [{ name: "children", type: "ReactNode", required: true, description: "`DropdownMenuRadioItem`s." }, { name: "onValueChange", type: "(value: string) => void", required: true, description: "Called with the picked item's value." }, { name: "value", type: "string", required: true, description: "Value of the checked item." }] }, { kind: "component", name: "DropdownMenuRadioItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label, optionally with a leading icon." }, { name: "value", type: "string", required: true, description: "Checked when it equals the group's `value`." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One choice of a RadioGroup; picking it keeps the menu open." }, { kind: "component", name: "DropdownMenuLabel", props: [], extends: "<div>" }, { kind: "component", name: "DropdownMenuSeparator", props: [] }], types: {} }, 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/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/dropdown-menu.tsx", type: "registry:ui", content: `"use client";
6777
6920
 
6778
6921
  import { Check, Circle } from "lucide-react";
6779
6922
  import { AnimatePresence, motion } from "motion/react";
@@ -7061,6 +7204,7 @@ export function DropdownMenuItem({
7061
7204
  )}
7062
7205
  >
7063
7206
  {children}
7207
+
7064
7208
  <Shortcut>{shortcut}</Shortcut>
7065
7209
  </div>
7066
7210
  );
@@ -7091,7 +7235,9 @@ export function DropdownMenuCheckboxItem({
7091
7235
  <span className="absolute left-2 flex size-4 items-center justify-center">
7092
7236
  {checked && <Check className="!text-current" aria-hidden="true" />}
7093
7237
  </span>
7238
+
7094
7239
  {children}
7240
+
7095
7241
  <Shortcut>{shortcut}</Shortcut>
7096
7242
  </div>
7097
7243
  );
@@ -7148,6 +7294,7 @@ export function DropdownMenuRadioItem({
7148
7294
  <span className="absolute left-2 flex size-4 items-center justify-center">
7149
7295
  {checked && <Circle className="!size-2 !text-current fill-current" aria-hidden="true" />}
7150
7296
  </span>
7297
+
7151
7298
  {children}
7152
7299
  </div>
7153
7300
  );
@@ -7219,12 +7366,15 @@ export function Empty({
7219
7366
  {icon}
7220
7367
  </span>
7221
7368
  )}
7369
+
7222
7370
  <div className="grid max-w-sm gap-1.5">
7223
7371
  <p className="font-semibold text-base tracking-tight">{title}</p>
7372
+
7224
7373
  {description && (
7225
7374
  <p className="text-muted-foreground text-sm leading-relaxed">{description}</p>
7226
7375
  )}
7227
7376
  </div>
7377
+
7228
7378
  {children && <div className="flex flex-wrap justify-center gap-2">{children}</div>}
7229
7379
  </div>
7230
7380
  );
@@ -7340,6 +7490,7 @@ export function Field({
7340
7490
  const labelEl = label && (
7341
7491
  <Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
7342
7492
  {label}
7493
+
7343
7494
  {required && (
7344
7495
  <span aria-hidden="true" className="text-destructive-ink">
7345
7496
  *
@@ -7362,14 +7513,17 @@ export function Field({
7362
7513
  {horizontal ? (
7363
7514
  <div className="flex items-center gap-2.5">
7364
7515
  {children}
7516
+
7365
7517
  {labelEl}
7366
7518
  </div>
7367
7519
  ) : (
7368
7520
  <>
7369
7521
  {labelEl}
7522
+
7370
7523
  {children}
7371
7524
  </>
7372
7525
  )}
7526
+
7373
7527
  {description && (
7374
7528
  <p
7375
7529
  id={descId}
@@ -7378,6 +7532,7 @@ export function Field({
7378
7532
  {description}
7379
7533
  </p>
7380
7534
  )}
7535
+
7381
7536
  {/* Persistent live region, so a new error is announced. */}
7382
7537
  {/* -mt-2 cancels the grid gap while empty; the error brings its own pt-2. */}
7383
7538
  <div aria-live="polite" className={cn("-mt-2", horizontal && "pl-6.5")}>
@@ -7401,6 +7556,45 @@ export function Field({
7401
7556
  </FieldContext>
7402
7557
  );
7403
7558
  }
7559
+ ` }] }, "form-value": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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`.", api: { entries: [{ kind: "component", name: "FormValue", props: [{ name: "anchor", type: "RefObject<HTMLElement | null>", required: true, description: "The visible control that gets focus when the value is invalid." }, { name: "value", type: "string", required: true, description: 'The value to submit, "" when nothing is picked.' }, { name: "disabled", type: "boolean", required: false, description: "Skips validation and leaves the value out of the submit, like a disabled input." }, { name: "name", type: "string", required: false, description: "Form field name; without it the value only takes part in validation." }, { name: "onInvalid", type: "() => void", required: false, description: "Called when a submit finds the value invalid." }, { name: "required", type: "boolean", required: false, description: "An empty value blocks submit." }], 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`." }], types: {} }, dependencies: [], registryDependencies: [], files: [{ path: "lib/form-value.tsx", type: "registry:lib", content: `import type { RefObject } from "react";
7560
+
7561
+ type FormValueProps = {
7562
+ /** Form field name; without it the value only takes part in validation. */
7563
+ name?: string;
7564
+ /** The value to submit, "" when nothing is picked. */
7565
+ value: string;
7566
+ /** An empty value blocks submit. */
7567
+ required?: boolean;
7568
+ /** Skips validation and leaves the value out of the submit, like a disabled input. */
7569
+ disabled?: boolean;
7570
+ /** The visible control that gets focus when the value is invalid. */
7571
+ anchor: RefObject<HTMLElement | null>;
7572
+ /** Called when a submit finds the value invalid. */
7573
+ onInvalid?: () => void;
7574
+ };
7575
+
7576
+ /**
7577
+ * Submits a custom control's value and takes part in native validation, which a hidden input
7578
+ * can't: an empty \`required\` value blocks submit, shows the browser's message and focuses \`anchor\`.
7579
+ */
7580
+ export function FormValue({ name, value, required, disabled, anchor, onInvalid }: FormValueProps) {
7581
+ if (!name && !required) return null;
7582
+
7583
+ return (
7584
+ <input
7585
+ aria-hidden="true"
7586
+ tabIndex={-1}
7587
+ name={name}
7588
+ value={value}
7589
+ required={required}
7590
+ disabled={disabled}
7591
+ onChange={() => undefined}
7592
+ onInvalid={onInvalid}
7593
+ onFocus={() => anchor.current?.focus()}
7594
+ className="pointer-events-none absolute size-px opacity-0"
7595
+ />
7596
+ );
7597
+ }
7404
7598
  ` }] }, highlight: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", api: { entries: [{ kind: "function", name: "highlightCode", signature: "highlightCode(code: string, language: string) => Promise<CodeToken[][] | null>", 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." }, { kind: "function", name: "languageOf", signature: "languageOf(name: string) => string", description: "Language from a file name's extension. Falls back to tsx." }], types: { CodeToken: "{ content: string; color?: string; fontStyle?: number }" } }, dependencies: ["shiki@^4.5.0"], registryDependencies: [], files: [{ path: "lib/highlight.ts", type: "registry:lib", content: `import type { BundledLanguage } from "shiki";
7405
7599
 
7406
7600
  /** A run of text in one color: \`light-dark()\` holds both themes, so the page's color scheme picks. */
@@ -7562,6 +7756,7 @@ export function HoverCard({
7562
7756
  }}
7563
7757
  >
7564
7758
  {children}
7759
+
7565
7760
  {mounted &&
7566
7761
  createPortal(
7567
7762
  <AnimatePresence>
@@ -7594,7 +7789,7 @@ export function HoverCard({
7594
7789
  </span>
7595
7790
  );
7596
7791
  }
7597
- ` }] }, input: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
7792
+ ` }] }, input: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", 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";
7598
7793
  import { Label } from "@/components/ui/label";
7599
7794
  import { Switch } from "@/components/ui/switch";
7600
7795
  import { Textarea } from "@/components/ui/textarea";
@@ -7869,6 +8064,7 @@ export function InputOTP({
7869
8064
  className="h-px w-2.5 shrink-0 bg-muted-foreground/50"
7870
8065
  />
7871
8066
  )}
8067
+
7872
8068
  <div className="flex min-w-0 gap-1.5" aria-hidden="true">
7873
8069
  {Array.from({ length: count }, (_, slot) => {
7874
8070
  const i = starts[group] + slot;
@@ -7923,6 +8119,7 @@ export function InputOTP({
7923
8119
  )
7924
8120
  )}
7925
8121
  </AnimatePresence>
8122
+
7926
8123
  {isActive && !char && (
7927
8124
  <span className="absolute h-4 w-px animate-caret-blink bg-foreground motion-reduce:animate-none" />
7928
8125
  )}
@@ -7932,6 +8129,7 @@ export function InputOTP({
7932
8129
  </div>
7933
8130
  </Fragment>
7934
8131
  ))}
8132
+
7935
8133
  <AnimatePresence>
7936
8134
  {merge && (
7937
8135
  // Slots fold into one bar of the same size: a finished field, not a new widget.
@@ -7950,6 +8148,7 @@ export function InputOTP({
7950
8148
  transition={{ ...spring, stiffness: 120, delay: 0.15 }}
7951
8149
  className="absolute inset-0 rounded-full border-2 border-success"
7952
8150
  />
8151
+
7953
8152
  <motion.span
7954
8153
  initial={{ scale: 0 }}
7955
8154
  animate={{ scale: 1 }}
@@ -7970,6 +8169,7 @@ export function InputOTP({
7970
8169
  </svg>
7971
8170
  </motion.span>
7972
8171
  </span>
8172
+
7973
8173
  <motion.span
7974
8174
  initial={{ opacity: 0, x: -6 }}
7975
8175
  animate={{ opacity: 1, x: 0 }}
@@ -7981,9 +8181,11 @@ export function InputOTP({
7981
8181
  </motion.div>
7982
8182
  )}
7983
8183
  </AnimatePresence>
8184
+
7984
8185
  <span role="status" className="sr-only">
7985
8186
  {verified ? verifiedLabel : null}
7986
8187
  </span>
8188
+
7987
8189
  <input
7988
8190
  type="text"
7989
8191
  autoComplete="one-time-code"
@@ -8060,7 +8262,7 @@ export function KbdGroup({
8060
8262
  </kbd>
8061
8263
  );
8062
8264
  }
8063
- ` }] }, label: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
8265
+ ` }] }, label: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", 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";
8064
8266
  import { Label } from "@/components/ui/label";
8065
8267
  import { Switch } from "@/components/ui/switch";
8066
8268
  import { Textarea } from "@/components/ui/textarea";
@@ -8092,7 +8294,7 @@ export function Label({ className, ...props }: ComponentProps<"label">) {
8092
8294
  }
8093
8295
  ` }] }, list: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", api: { entries: [{ kind: "function", name: "listKeyTarget", signature: 'listKeyTarget(items: readonly T[], index: number, event: Pick<KeyboardEvent, "ctrlKey" | "key">, { orientation, loop, homeEnd }: ListKeyOptions) => T | undefined', 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." }, { kind: "function", name: "scrollIntoList", signature: "scrollIntoList(box: HTMLElement, element: HTMLElement, pad?: number) => void", description: "Scrolls `box` just enough to show `el`, without moving the page the way scrollIntoView can." }], types: { ListKeyOptions: '{ orientation: "horizontal" | "vertical"; loop?: boolean; homeEnd?: boolean | "ctrl"; }' } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "lib/list.ts", type: "registry:lib", content: `import { clamp } from "@/lib/utils";
8094
8296
 
8095
- type ListKeyOptions = {
8297
+ export type ListKeyOptions = {
8096
8298
  /** Which arrows move: Left/Right for "horizontal", Up/Down for "vertical". */
8097
8299
  orientation: "horizontal" | "vertical";
8098
8300
  /** Wrap past the last item to the first and back. Default true. */
@@ -8428,6 +8630,7 @@ export function Menubar({ className, children, ...props }: ComponentProps<"div">
8428
8630
  className="-z-10 absolute inset-y-1 left-0 rounded-md bg-foreground/[0.08]"
8429
8631
  />
8430
8632
  )}
8633
+
8431
8634
  {children}
8432
8635
  </div>
8433
8636
  </BarContext>
@@ -8529,7 +8732,7 @@ export function MenubarTrigger({
8529
8732
  />
8530
8733
  );
8531
8734
  }
8532
- ` }] }, "message-bubble": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", docs: "https://kznlabs.github.io/labsUI/components/chat", usage: '<MessageBubble from="assistant" avatar={<Avatar … />} streaming>…<Code code={src} language="tsx" /></MessageBubble>', examples: [{ name: "message.tsx", code: `import { Avatar } from "@/components/ui/avatar";
8735
+ ` }] }, "message-bubble": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "message-bubble", type: "registry:ui", title: "Message Bubble", description: "User/assistant chat bubbles with avatar slot, streaming dots and copyable code blocks.", docs: "https://kznlabs.github.io/labsUI/components/chat", usage: '<MessageBubble from="assistant" avatar={<Avatar src={bot} alt="Assistant" />} streaming>…<Code code={src} language="tsx" /></MessageBubble>', examples: [{ name: "message.tsx", code: `import { Avatar } from "@/components/ui/avatar";
8533
8736
  import { Code } from "@/components/ui/code";
8534
8737
  import { MessageBubble } from "@/components/ui/message-bubble";
8535
8738
  export function Reply({ code }: { code: string }) {
@@ -8587,13 +8790,16 @@ export function MessageBubble({
8587
8790
  className={cn("flex w-full gap-3", user && "flex-row-reverse", className)}
8588
8791
  >
8589
8792
  {avatar && <div className="shrink-0 pt-5">{avatar}</div>}
8793
+
8590
8794
  <div className={cn("flex min-w-0 max-w-[85%] flex-col gap-1", user && "items-end")}>
8591
8795
  {(name || time) && (
8592
8796
  <header className="flex items-center gap-2 px-1 text-muted-foreground text-xs">
8593
8797
  {name && <span className="font-medium text-foreground">{name}</span>}
8798
+
8594
8799
  {time && <time>{time}</time>}
8595
8800
  </header>
8596
8801
  )}
8802
+
8597
8803
  <div
8598
8804
  className={cn(
8599
8805
  "min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed",
@@ -8604,6 +8810,7 @@ export function MessageBubble({
8604
8810
  )}
8605
8811
  >
8606
8812
  {children}
8813
+
8607
8814
  {streaming && <TypingDots className={children ? "mt-2" : "py-1.5"} />}
8608
8815
  </div>
8609
8816
  </div>
@@ -8648,14 +8855,14 @@ export function Storage() {
8648
8855
  ]}
8649
8856
  />
8650
8857
  );
8651
- }` }], api: { entries: [{ kind: "component", name: "Meter", props: [{ name: "high", type: "number", required: false, default: "max", description: "Above this the value is high." }, { name: "label", type: "ReactNode", required: false, description: "Visible name above the bar; also the accessible name." }, { name: "low", type: "number", required: false, default: "min", description: "<meter> thresholds: below `low` is low, above `high` is high." }, { name: "max", type: "number", required: false, default: "100", description: "Upper bound." }, { name: "min", type: "number", required: false, default: "0", description: "Lower bound." }, { name: "optimum", type: "number", required: false, default: "(min + max) / 2", description: "Where good is: below low, between, or above high. Decides the color, like <meter>." }, { name: "segments", type: "Segment[]", required: false, description: "Stacked parts with a legend (like a storage bar). Overrides the threshold color." }, { name: "value", type: "number", required: false, description: "Total used. With `segments`, defaults to their sum." }, { name: "valueText", type: "string", required: false, description: 'Text on the right and the accessible value, e.g. "48 GB of 100 GB".' }], description: `A measurement within a known range (disk, quota, server load, password strength), not a
8858
+ }` }], api: { entries: [{ kind: "component", name: "Meter", props: [{ name: "high", type: "number", required: false, default: "max", description: "Above this the value is high." }, { name: "label", type: "ReactNode", required: false, description: "Visible name above the bar; also the accessible name." }, { name: "low", type: "number", required: false, default: "min", description: "<meter> thresholds: below `low` is low, above `high` is high." }, { name: "max", type: "number", required: false, default: "100", description: "Upper bound." }, { name: "min", type: "number", required: false, default: "0", description: "Lower bound." }, { name: "optimum", type: "number", required: false, default: "(min + max) / 2", description: "Where good is: below low, between, or above high. Decides the color, like <meter>." }, { name: "segments", type: "MeterSegment[]", required: false, description: "Stacked parts with a legend (like a storage bar). Overrides the threshold color." }, { name: "value", type: "number", required: false, description: "Total used. With `segments`, defaults to their sum." }, { name: "valueText", type: "string", required: false, description: 'Text on the right and the accessible value, e.g. "48 GB of 100 GB".' }], description: `A measurement within a known range (disk, quota, server load, password strength), not a
8652
8859
  task's progress (use Progress). Colors follow <meter>: green in the optimum region, orange one
8653
- region away, red two away.` }], types: { Segment: "{ label: string; value: number; className?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/meter.tsx", type: "registry:ui", content: `import type { ReactNode } from "react";
8860
+ region away, red two away.` }], types: { MeterSegment: "{ label: string; value: number; className?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/meter.tsx", type: "registry:ui", content: `import type { ReactNode } from "react";
8654
8861
  import { clamp, cn } from "@/lib/utils";
8655
8862
 
8656
8863
  /* Server-safe (no "use client"). */
8657
8864
 
8658
- type Segment = {
8865
+ export type MeterSegment = {
8659
8866
  /** Legend text for this part. */
8660
8867
  label: string;
8661
8868
  /** Amount this part uses. */
@@ -8678,7 +8885,7 @@ type MeterProps = {
8678
8885
  /** Where good is: below low, between, or above high. Decides the color, like <meter>. */
8679
8886
  optimum?: number;
8680
8887
  /** Stacked parts with a legend (like a storage bar). Overrides the threshold color. */
8681
- segments?: Segment[];
8888
+ segments?: MeterSegment[];
8682
8889
  /** Visible name above the bar; also the accessible name. */
8683
8890
  label?: ReactNode;
8684
8891
  /** Text on the right and the accessible value, e.g. "48 GB of 100 GB". */
@@ -8716,11 +8923,13 @@ export function Meter({
8716
8923
  {(label || valueText) && (
8717
8924
  <div className="flex items-baseline justify-between gap-3 text-sm">
8718
8925
  {label && <span className="font-medium">{label}</span>}
8926
+
8719
8927
  {valueText && (
8720
8928
  <span className="ml-auto text-muted-foreground text-xs tabular-nums">{valueText}</span>
8721
8929
  )}
8722
8930
  </div>
8723
8931
  )}
8932
+
8724
8933
  {/* The real <meter> carries the semantics; the drawn bar below is decoration. */}
8725
8934
  <meter
8726
8935
  className="sr-only"
@@ -8734,6 +8943,7 @@ export function Meter({
8734
8943
  >
8735
8944
  {valueText ?? value}
8736
8945
  </meter>
8946
+
8737
8947
  <div
8738
8948
  aria-hidden="true"
8739
8949
  className="flex h-2 w-full gap-px overflow-hidden rounded-full bg-foreground/10"
@@ -8753,6 +8963,7 @@ export function Meter({
8753
8963
  <span style={{ width: pct(value) }} className={cn("h-full rounded-full", tone)} />
8754
8964
  )}
8755
8965
  </div>
8966
+
8756
8967
  {segments && (
8757
8968
  <ul className="flex flex-wrap gap-x-4 gap-y-1 text-muted-foreground text-xs">
8758
8969
  {segments.map((segment, i) => (
@@ -8764,6 +8975,7 @@ export function Meter({
8764
8975
  segment.className ?? palette[i % palette.length],
8765
8976
  )}
8766
8977
  />
8978
+
8767
8979
  {segment.label}
8768
8980
  </li>
8769
8981
  ))}
@@ -8830,6 +9042,7 @@ export function NativeSelect({ className, children, ...rest }: ComponentProps<"s
8830
9042
  >
8831
9043
  {children}
8832
9044
  </select>
9045
+
8833
9046
  <ChevronsUpDown
8834
9047
  aria-hidden="true"
8835
9048
  className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-3 size-4 text-muted-foreground"
@@ -9189,8 +9402,10 @@ export function NavigationMenu({
9189
9402
  className="-z-10 absolute inset-y-0 left-0 rounded-lg bg-foreground/[0.06]"
9190
9403
  />
9191
9404
  )}
9405
+
9192
9406
  {children}
9193
9407
  </div>
9408
+
9194
9409
  <motion.div
9195
9410
  ref={setViewport}
9196
9411
  data-floating=""
@@ -9308,6 +9523,7 @@ export function NavigationMenuTrigger({
9308
9523
  {...props}
9309
9524
  >
9310
9525
  {children}
9526
+
9311
9527
  <motion.span
9312
9528
  aria-hidden="true"
9313
9529
  animate={{ rotate: open ? 180 : 0 }}
@@ -9353,6 +9569,7 @@ export function NavigationMenuContent({
9353
9569
  return (
9354
9570
  <InPanel value>
9355
9571
  {!open && <div hidden>{children}</div>}
9572
+
9356
9573
  {nav.viewport &&
9357
9574
  createPortal(
9358
9575
  <AnimatePresence custom={nav.dir} initial={false}>
@@ -9528,8 +9745,10 @@ export function NavigationMenuLink({
9528
9745
  {icon}
9529
9746
  </span>
9530
9747
  )}
9748
+
9531
9749
  <span className="flex min-w-0 flex-col gap-0.5">
9532
9750
  <span className="font-medium text-foreground">{title}</span>
9751
+
9533
9752
  {description && (
9534
9753
  <span className="line-clamp-2 text-muted-foreground text-xs leading-snug">
9535
9754
  {description}
@@ -9642,13 +9861,16 @@ export function Newsletter({
9642
9861
  {title}
9643
9862
  </h3>
9644
9863
  )}
9864
+
9645
9865
  {description && <p className="text-muted-foreground text-sm">{description}</p>}
9646
9866
  </div>
9647
9867
  )}
9868
+
9648
9869
  <InputGroup>
9649
9870
  <InputGroupAddon>
9650
9871
  <Mail aria-hidden="true" />
9651
9872
  </InputGroupAddon>
9873
+
9652
9874
  <Input
9653
9875
  type="email"
9654
9876
  name="email"
@@ -9657,6 +9879,7 @@ export function Newsletter({
9657
9879
  placeholder={placeholder}
9658
9880
  aria-label="Email address"
9659
9881
  />
9882
+
9660
9883
  <InputGroupAddon align="end">
9661
9884
  <Button type="submit" size="sm" status={status} successLabel="Subscribed">
9662
9885
  {submitLabel}
@@ -9755,6 +9978,7 @@ export function NumberInput({
9755
9978
  <Minus aria-hidden="true" />
9756
9979
  </button>
9757
9980
  </InputGroupAddon>
9981
+
9758
9982
  <Input
9759
9983
  type="number"
9760
9984
  inputMode={Number.isInteger(step) ? "numeric" : "decimal"}
@@ -9778,6 +10002,7 @@ export function NumberInput({
9778
10002
  className="px-0 text-center tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
9779
10003
  {...props}
9780
10004
  />
10005
+
9781
10006
  <InputGroupAddon align="end" className="pr-1">
9782
10007
  <button
9783
10008
  type="button"
@@ -9896,6 +10121,7 @@ function Control({
9896
10121
  const content = (
9897
10122
  <>
9898
10123
  {active && <PaginationActivePill />}
10124
+
9899
10125
  {children}
9900
10126
  </>
9901
10127
  );
@@ -9961,9 +10187,11 @@ export function Pagination({
9961
10187
  className="gap-1 px-2.5"
9962
10188
  >
9963
10189
  <ChevronLeft aria-hidden="true" />
10190
+
9964
10191
  <span className="hidden sm:block">{labels?.previous ?? "Previous"}</span>
9965
10192
  </Control>
9966
10193
  </li>
10194
+
9967
10195
  {pageRange(current, total, siblings).map((page, i) => (
9968
10196
  // biome-ignore lint/suspicious/noArrayIndexKey: slots stay mounted so numbers swipe (see pageRange)
9969
10197
  <li key={i}>
@@ -9981,6 +10209,7 @@ export function Pagination({
9981
10209
  )}
9982
10210
  </li>
9983
10211
  ))}
10212
+
9984
10213
  <li>
9985
10214
  <Control
9986
10215
  {...nav}
@@ -9990,6 +10219,7 @@ export function Pagination({
9990
10219
  className="gap-1 px-2.5"
9991
10220
  >
9992
10221
  <span className="hidden sm:block">{labels?.next ?? "Next"}</span>
10222
+
9993
10223
  <ChevronRight aria-hidden="true" />
9994
10224
  </Control>
9995
10225
  </li>
@@ -10137,7 +10367,7 @@ export function ClipFilters() {
10137
10367
  </PopoverContent>
10138
10368
  </Popover>
10139
10369
  );
10140
- }` }], api: { entries: [{ kind: "component", name: "Popover", props: [{ name: "children", type: "ReactNode", required: true, description: "`PopoverTrigger` and `PopoverContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc or an outside click." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Non-modal panel anchored to a button: filters, pickers, quick edits. Controlled or uncontrolled." }, { kind: "component", name: "PopoverTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Disables the button and shows a spinner before the label." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "PopoverContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Panel content." }, { name: "align", type: '"start" | "center" | "end"', required: false, default: '"center"', description: "Alignment with the trigger." }, { name: "side", type: '"top" | "bottom"', required: false, default: '"bottom"', description: "Preferred side; flips when there is no room." }], description: `Focus moves in on open. Esc or an outside click closes it; Esc returns focus to the trigger.
10370
+ }` }], api: { entries: [{ kind: "component", name: "Popover", props: [{ name: "children", type: "ReactNode", required: true, description: "`PopoverTrigger` and `PopoverContent`." }, { name: "defaultOpen", type: "boolean", required: false, default: "false", description: "Open at first render (uncontrolled)." }, { name: "onOpenChange", type: "(open: boolean) => void", required: false, description: "Called on the trigger, Esc or an outside click." }, { name: "open", type: "boolean", required: false, description: "Controlled state; pair with `onOpenChange`." }], description: "Non-modal panel anchored to a button: filters, pickers, quick edits. Controlled or uncontrolled." }, { kind: "component", name: "PopoverTrigger", props: [{ name: "children", type: "ReactNode", required: false, description: "Label; its width is reserved so loading and success never resize the button." }, { name: "confirmLabel", type: "ReactNode", required: false, description: "Two-step action: first click swaps the label to this, second click runs `onClick`." }, { name: "confirmTimeout", type: "number", required: false, description: "Ms before an armed confirm step resets. Also resets on blur or Esc." }, { name: "loading", type: "boolean", required: false, description: "Shows a spinner before the label and ignores clicks and Enter, so a form can't submit twice. Stays focusable." }, { name: "loadingLabel", type: "ReactNode", required: false, description: "Label shown while loading, with the spinner. Defaults to `children`." }, { name: "size", type: '"default" | "sm" | "lg" | "icon"', required: false, description: 'Height and padding; "icon" is square, so give it an `aria-label`.' }, { name: "status", type: "ButtonStatus", required: false, description: "In-place feedback: `loading` spins, `success` swaps the label to `successLabel`. Both ignore clicks and Enter, so a form can't submit twice." }, { name: "successLabel", type: "ReactNode", required: false, description: 'Text shown on `status="success"`, with a check icon. Defaults to "Done".' }, { name: "swap", type: "SwapDirection", required: false, description: 'Where the old label slides out to on a swap. Default "left".' }, { name: "variant", type: '"default" | "secondary" | "outline" | "ghost" | "destructive"', required: false, description: "Fill and emphasis: one primary action per view, `destructive` for irreversible ones." }], extends: "Button" }, { kind: "component", name: "PopoverContent", props: [{ name: "children", type: "ReactNode", required: true, description: "Panel content." }, { name: "align", type: '"start" | "center" | "end"', required: false, default: '"center"', description: "Alignment with the trigger." }, { name: "side", type: '"top" | "bottom"', required: false, default: '"bottom"', description: "Preferred side; flips when there is no room." }], description: `Focus moves in on open. Esc or an outside click closes it; Esc returns focus to the trigger.
10141
10371
  Not a focus trap: Tab past the end closes it and moves on, like any non-modal popup.` }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/popover.tsx", type: "registry:ui", content: `"use client";
10142
10372
 
10143
10373
  import { AnimatePresence, motion } from "motion/react";
@@ -10292,6 +10522,19 @@ export function PopoverContent({
10292
10522
  aria-labelledby={\`\${id}-trigger\`}
10293
10523
  tabIndex={-1}
10294
10524
  onKeyDown={(event) => {
10525
+ // The panel sits at the end of body: Tab past its last field goes on from the trigger.
10526
+ if (event.key === "Tab" && !event.shiftKey) {
10527
+ const last = [...(panel.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])].at(
10528
+ -1,
10529
+ );
10530
+
10531
+ if (last && event.target !== last) return;
10532
+ setOpen(false);
10533
+ trigger.current?.focus();
10534
+
10535
+ return;
10536
+ }
10537
+
10295
10538
  if (event.key !== "Escape" || event.defaultPrevented) return;
10296
10539
  // Handled here so an enclosing Dialog stays open.
10297
10540
  event.preventDefault();
@@ -10349,8 +10592,8 @@ export function PricingPage() {
10349
10592
  ]}
10350
10593
  />
10351
10594
  );
10352
- }` }], api: { entries: [{ kind: "component", name: "Pricing", props: [{ name: "plans", type: "Plan[]", required: true, description: "Plans in display order." }, { name: "currency", type: "string", required: false, default: '"USD"', description: 'ISO 4217 code for the price, e.g. "EUR".' }, { name: "defaultPeriod", type: "Period", required: false, default: '"monthly"', description: "Billing period selected at first render." }, { name: "locale", type: "string", required: false, default: '"en-US"', description: "BCP 47 locale for number formatting." }, { name: "yearlyNote", type: "string", required: false, description: 'Next to "Yearly", e.g. "−17%".' }], description: `Plan cards with a monthly / yearly switch (segmented ToggleGroup). Prices swap on a spring,
10353
- rolling up when they drop and down when they rise.` }], types: { Period: '"monthly" | "yearly"', Plan: "{ name: string; description?: string; price: Record<Period, number>; features: string[]; cta: ReactNode; featured?: boolean; badge?: string; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/toggle-group.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/pricing.tsx", type: "registry:block", content: `"use client";
10595
+ }` }], api: { entries: [{ kind: "component", name: "Pricing", props: [{ name: "plans", type: "PricingPlan[]", required: true, description: "Plans in display order." }, { name: "currency", type: "string", required: false, default: '"USD"', description: 'ISO 4217 code for the price, e.g. "EUR".' }, { name: "defaultPeriod", type: "PricingPeriod", required: false, default: '"monthly"', description: "Billing period selected at first render." }, { name: "locale", type: "string", required: false, default: '"en-US"', description: "BCP 47 locale for number formatting." }, { name: "yearlyNote", type: "string", required: false, description: 'Next to "Yearly", e.g. "−17%".' }], description: `Plan cards with a monthly / yearly switch (segmented ToggleGroup). Prices swap on a spring,
10596
+ rolling up when they drop and down when they rise.` }], types: { PricingPeriod: '"monthly" | "yearly"', PricingPlan: "{ name: string; description?: string; price: Record<PricingPeriod, number>; features: string[]; cta: ReactNode; featured?: boolean; badge?: string; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/badge.json", "https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/toggle-group.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/pricing.tsx", type: "registry:block", content: `"use client";
10354
10597
 
10355
10598
  import { Check } from "lucide-react";
10356
10599
  import { AnimatePresence, motion } from "motion/react";
@@ -10367,15 +10610,15 @@ import {
10367
10610
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
10368
10611
  import { cn, spring } from "@/lib/utils";
10369
10612
 
10370
- type Period = "monthly" | "yearly";
10613
+ export type PricingPeriod = "monthly" | "yearly";
10371
10614
 
10372
- type Plan = {
10615
+ export type PricingPlan = {
10373
10616
  /** Plan name, the card's heading. */
10374
10617
  name: string;
10375
10618
  /** One line under the name. */
10376
10619
  description?: string;
10377
10620
  /** Price per month on each billing period, e.g. \`{ monthly: 12, yearly: 10 }\`. */
10378
- price: Record<Period, number>;
10621
+ price: Record<PricingPeriod, number>;
10379
10622
  features: string[];
10380
10623
  /** Call to action, e.g. \`<Button className="w-full">Start trial</Button>\`. */
10381
10624
  cta: ReactNode;
@@ -10387,13 +10630,13 @@ type Plan = {
10387
10630
 
10388
10631
  type PricingProps = {
10389
10632
  /** Plans in display order. */
10390
- plans: Plan[];
10633
+ plans: PricingPlan[];
10391
10634
  /** ISO 4217 code for the price, e.g. "EUR". */
10392
10635
  currency?: string;
10393
10636
  /** BCP 47 locale for number formatting. */
10394
10637
  locale?: string;
10395
10638
  /** Billing period selected at first render. */
10396
- defaultPeriod?: Period;
10639
+ defaultPeriod?: PricingPeriod;
10397
10640
  /** Next to "Yearly", e.g. "−17%". */
10398
10641
  yearlyNote?: string;
10399
10642
  className?: string;
@@ -10411,7 +10654,7 @@ export function Pricing({
10411
10654
  yearlyNote,
10412
10655
  className,
10413
10656
  }: PricingProps) {
10414
- const [period, setPeriod] = useState<Period>(defaultPeriod);
10657
+ const [period, setPeriod] = useState<PricingPeriod>(defaultPeriod);
10415
10658
  const money = new Intl.NumberFormat(locale, {
10416
10659
  style: "currency",
10417
10660
  currency,
@@ -10424,9 +10667,10 @@ export function Pricing({
10424
10667
  <ToggleGroup
10425
10668
  aria-label="Billing period"
10426
10669
  value={period}
10427
- onValueChange={(value) => value && setPeriod(value as Period)}
10670
+ onValueChange={(value) => value && setPeriod(value as PricingPeriod)}
10428
10671
  >
10429
10672
  <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
10673
+
10430
10674
  <ToggleGroupItem value="yearly">
10431
10675
  Yearly
10432
10676
  {yearlyNote && (
@@ -10436,6 +10680,7 @@ export function Pricing({
10436
10680
  )}
10437
10681
  </ToggleGroupItem>
10438
10682
  </ToggleGroup>
10683
+
10439
10684
  <div className="grid w-full gap-4 sm:grid-cols-[repeat(auto-fit,minmax(14rem,1fr))]">
10440
10685
  {plans.map((plan) => (
10441
10686
  <Card
@@ -10445,14 +10690,17 @@ export function Pricing({
10445
10690
  <CardHeader>
10446
10691
  <div className="flex min-h-6 items-center justify-between gap-2">
10447
10692
  <CardTitle>{plan.name}</CardTitle>
10693
+
10448
10694
  {plan.badge && (
10449
10695
  <Badge variant={plan.featured ? "default" : "secondary"} shape="pill">
10450
10696
  {plan.badge}
10451
10697
  </Badge>
10452
10698
  )}
10453
10699
  </div>
10700
+
10454
10701
  {plan.description && <CardDescription>{plan.description}</CardDescription>}
10455
10702
  </CardHeader>
10703
+
10456
10704
  <CardContent className="flex flex-col gap-5">
10457
10705
  <p className="flex items-baseline gap-1">
10458
10706
  <span className="relative inline-flex overflow-hidden font-semibold text-4xl tabular-nums tracking-tight">
@@ -10474,20 +10722,24 @@ export function Pricing({
10474
10722
  </motion.span>
10475
10723
  </AnimatePresence>
10476
10724
  </span>
10725
+
10477
10726
  <span className="text-muted-foreground text-sm">
10478
10727
  / month
10479
10728
  <span className="sr-only">, billed {period}</span>
10480
10729
  </span>
10481
10730
  </p>
10731
+
10482
10732
  <ul className="flex flex-col gap-2 text-sm">
10483
10733
  {plan.features.map((feature) => (
10484
10734
  <li key={feature} className="flex gap-2">
10485
10735
  <Check className="mt-0.5 size-4 shrink-0 text-primary-ink" aria-hidden="true" />
10736
+
10486
10737
  {feature}
10487
10738
  </li>
10488
10739
  ))}
10489
10740
  </ul>
10490
10741
  </CardContent>
10742
+
10491
10743
  <CardFooter className="mt-auto">{plan.cta}</CardFooter>
10492
10744
  </Card>
10493
10745
  ))}
@@ -10495,7 +10747,7 @@ export function Pricing({
10495
10747
  </div>
10496
10748
  );
10497
10749
  }
10498
- ` }] }, "product-card": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/product-card", usage: '<ProductCard name="Clip camera" price={279} compareAt={349} badge="−20%" media={[<img … />, <img … />]} onAddToCart={add} />', examples: [{ name: "product-grid.tsx", code: `import { ProductCard } from "@/components/blocks/product-card";
10750
+ ` }] }, "product-card": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/product-card", usage: '<ProductCard name="Clip camera" price={279} compareAt={349} badge="−20%" media={[<img src={front} alt="Front" />, <img src={back} alt="Back" />]} onAddToCart={add} />', examples: [{ name: "product-grid.tsx", code: `import { ProductCard } from "@/components/blocks/product-card";
10499
10751
 
10500
10752
  type Product = {
10501
10753
  id: string;
@@ -10601,17 +10853,20 @@ export function ProductCard({
10601
10853
  <CarouselItem key={i}>{frame(slide)}</CarouselItem>
10602
10854
  ))}
10603
10855
  </CarouselContent>
10856
+
10604
10857
  <CarouselDots className="absolute inset-x-0 bottom-2 mt-0" />
10605
10858
  </Carousel>
10606
10859
  ) : (
10607
10860
  frame(media[0])
10608
10861
  )}
10862
+
10609
10863
  {badge && (
10610
10864
  <Badge shape="pill" className="absolute top-2 left-2">
10611
10865
  {badge}
10612
10866
  </Badge>
10613
10867
  )}
10614
10868
  </div>
10869
+
10615
10870
  <div className="flex flex-1 flex-col gap-3 p-3">
10616
10871
  <div className="flex items-baseline justify-between gap-2">
10617
10872
  <h3 className="min-w-0 truncate font-medium text-sm">
@@ -10623,16 +10878,20 @@ export function ProductCard({
10623
10878
  name
10624
10879
  )}
10625
10880
  </h3>
10881
+
10626
10882
  <p className="shrink-0 text-sm tabular-nums">
10627
10883
  {compareAt !== undefined && compareAt > price && (
10628
10884
  <>
10629
10885
  <del className="mr-1.5 text-muted-foreground">{money.format(compareAt)}</del>
10886
+
10630
10887
  <span className="sr-only">now </span>
10631
10888
  </>
10632
10889
  )}
10890
+
10633
10891
  {money.format(price)}
10634
10892
  </p>
10635
10893
  </div>
10894
+
10636
10895
  {onAddToCart && (
10637
10896
  <Button size="sm" variant="outline" className="mt-auto w-full" onClick={onAddToCart}>
10638
10897
  {addLabel}
@@ -10693,11 +10952,13 @@ export function Progress({
10693
10952
  {(label || showValue) && (
10694
10953
  <div className="flex items-center justify-between gap-3 text-xs">
10695
10954
  {label && <span className="font-medium">{label}</span>}
10955
+
10696
10956
  {showValue && determinate && (
10697
10957
  <span className="ml-auto text-muted-foreground tabular-nums">{pct}%</span>
10698
10958
  )}
10699
10959
  </div>
10700
10960
  )}
10961
+
10701
10962
  <div
10702
10963
  role="progressbar"
10703
10964
  aria-label={label ?? ariaLabel}
@@ -10809,6 +11070,7 @@ export function Quote({
10809
11070
  className={cn("flex flex-col", surface[variant], className)}
10810
11071
  >
10811
11072
  {large && <QuoteIcon aria-hidden="true" className="size-8 text-muted-foreground/60" />}
11073
+
10812
11074
  <blockquote
10813
11075
  lang={lang}
10814
11076
  className={cn(
@@ -10849,11 +11111,14 @@ export function Quote({
10849
11111
  )}
10850
11112
  </div>
10851
11113
  </blockquote>
11114
+
10852
11115
  {(author || avatar) && (
10853
11116
  <figcaption className="flex items-center gap-3">
10854
11117
  {avatar}
11118
+
10855
11119
  <span className="grid text-left">
10856
11120
  {author && <span className="font-medium text-foreground text-sm">{author}</span>}
11121
+
10857
11122
  {subtitle && <span className="text-muted-foreground text-xs">{subtitle}</span>}
10858
11123
  </span>
10859
11124
  </figcaption>
@@ -10870,7 +11135,7 @@ export function PlanPicker() {
10870
11135
  <RadioGroupItem value="pro" description="10 servers, unlimited clips.">Pro</RadioGroupItem>
10871
11136
  </RadioGroup>
10872
11137
  );
10873
- }` }], api: { entries: [{ kind: "component", name: "RadioGroup", props: [{ name: "aria-invalid", type: "boolean", required: false, description: "Indicates the entered value does not conform to the format expected by the application." }, { name: "defaultValue", type: "string", required: false, default: '""', description: "Value at first render (uncontrolled)." }, { name: "legend", type: "ReactNode", required: false, description: "Visible group label, rendered as the fieldset's <legend>." }, { name: "name", type: "string", required: false, description: "Form field name; generated when omitted." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked item's value." }, { name: "orientation", type: '"vertical" | "horizontal"', required: false, default: '"vertical"', description: "Stack items or lay them in a row." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], extends: "<fieldset>", description: "Native radios in a fieldset: arrow keys, form submit and labels work as the browser does." }, { kind: "component", name: "RadioGroupItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label." }, { name: "value", type: "string", required: true, description: "Submitted value." }, { name: "description", type: "ReactNode", required: false, description: "Muted line under the label." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One option. `children` is its label; `description` adds a muted line under it." }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/radio-group.tsx", type: "registry:ui", content: `"use client";
11138
+ }` }], api: { entries: [{ kind: "component", name: "RadioGroup", props: [{ name: "aria-invalid", type: "boolean", required: false, description: "Indicates the entered value does not conform to the format expected by the application." }, { name: "defaultValue", type: "string", required: false, default: '""', description: "Value at first render (uncontrolled)." }, { name: "legend", type: "ReactNode", required: false, description: "Visible group label, rendered as the fieldset's <legend>." }, { name: "name", type: "string", required: false, description: "Form field name; generated when omitted." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked item's value." }, { name: "orientation", type: '"vertical" | "horizontal"', required: false, default: '"vertical"', description: "Stack items or lay them in a row." }, { name: "required", type: "boolean", required: false, description: "Submit needs a picked item." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], extends: "<fieldset>", description: "Native radios in a fieldset: arrow keys, form submit and labels work as the browser does." }, { kind: "component", name: "RadioGroupItem", props: [{ name: "children", type: "ReactNode", required: true, description: "Label." }, { name: "value", type: "string", required: true, description: "Submitted value." }, { name: "description", type: "ReactNode", required: false, description: "Muted line under the label." }, { name: "disabled", type: "boolean", required: false, description: "Shown but not selectable." }], description: "One option. `children` is its label; `description` adds a muted line under it." }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/radio-group.tsx", type: "registry:ui", content: `"use client";
10874
11139
 
10875
11140
  import { motion } from "motion/react";
10876
11141
  import { type ComponentProps, createContext, type ReactNode, use, useId } from "react";
@@ -10883,6 +11148,7 @@ type RadioCtx = {
10883
11148
  value: string;
10884
11149
  setValue: (value: string) => void;
10885
11150
  disabled?: boolean;
11151
+ required?: boolean;
10886
11152
  invalid?: boolean;
10887
11153
  };
10888
11154
 
@@ -10901,6 +11167,8 @@ type RadioGroupProps = Omit<ComponentProps<"fieldset">, "defaultValue" | "onChan
10901
11167
  name?: string;
10902
11168
  /** Stack items or lay them in a row. */
10903
11169
  orientation?: "vertical" | "horizontal";
11170
+ /** Submit needs a picked item. */
11171
+ required?: boolean;
10904
11172
  "aria-invalid"?: boolean;
10905
11173
  };
10906
11174
 
@@ -10916,12 +11184,8 @@ export function RadioGroup({
10916
11184
  children,
10917
11185
  ...rest
10918
11186
  }: RadioGroupProps) {
10919
- // A fieldset takes no id from Field (labels can't target it) and no required.
10920
- const {
10921
- id: _id,
10922
- required: _required,
10923
- ...props
10924
- } = useFieldControl<typeof rest & { required?: boolean }>(rest);
11187
+ // A fieldset takes no id from Field (labels can't target it); required goes on the radios.
11188
+ const { id: _id, required, ...props } = useFieldControl(rest);
10925
11189
  const auto = useId();
10926
11190
  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
10927
11191
 
@@ -10932,11 +11196,13 @@ export function RadioGroup({
10932
11196
  value,
10933
11197
  setValue,
10934
11198
  disabled: props.disabled,
11199
+ required,
10935
11200
  invalid: !!props["aria-invalid"],
10936
11201
  }}
10937
11202
  >
10938
11203
  <fieldset className={cn("min-w-0", className)} {...props}>
10939
11204
  {legend && <legend className="mb-3 font-medium text-sm leading-none">{legend}</legend>}
11205
+
10940
11206
  <div
10941
11207
  className={cn(
10942
11208
  "flex gap-3",
@@ -10986,11 +11252,13 @@ export function RadioGroupItem({
10986
11252
  value={value}
10987
11253
  checked={checked}
10988
11254
  disabled={off}
11255
+ required={ctx.required}
10989
11256
  aria-invalid={ctx.invalid || undefined}
10990
11257
  aria-describedby={description ? \`\${id}-description\` : undefined}
10991
11258
  onChange={() => ctx.setValue(value)}
10992
11259
  className="peer absolute inset-0 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
10993
11260
  />
11261
+
10994
11262
  <span
10995
11263
  aria-hidden="true"
10996
11264
  className={cn(
@@ -11006,6 +11274,7 @@ export function RadioGroupItem({
11006
11274
  />
11007
11275
  </span>
11008
11276
  </span>
11277
+
11009
11278
  <span className="grid gap-1">
11010
11279
  <label
11011
11280
  htmlFor={id}
@@ -11016,6 +11285,7 @@ export function RadioGroupItem({
11016
11285
  >
11017
11286
  {children}
11018
11287
  </label>
11288
+
11019
11289
  {description && (
11020
11290
  <span id={\`\${id}-description\`} className="text-muted-foreground text-xs">
11021
11291
  {description}
@@ -11035,11 +11305,12 @@ export function ProductRating({ average, count }: { average: number; count: numb
11035
11305
  <span className="text-muted-foreground">· {count} reviews</span>
11036
11306
  </div>
11037
11307
  );
11038
- }` }], api: { entries: [{ kind: "component", name: "Rating", props: [{ name: "defaultValue", type: "number", required: false, default: "0", description: "Rating at first render (uncontrolled)." }, { name: "label", type: "string", required: false, default: '"Rating"', description: "Group label (visually hidden)." }, { name: "max", type: "number", required: false, default: "5", description: "Number of stars." }, { name: "name", type: "string", required: false, description: "Form field name for the chosen value." }, { name: "onValueChange", type: "(value: number) => void", required: false, description: "Called with the picked rating." }, { name: "readOnly", type: "boolean", required: false, default: "false", description: "Display only: renders fractions (4.6) as a partially filled star, no inputs." }, { name: "size", type: '"sm" | "default"', required: false, default: '"default"', description: "Star size." }, { name: "value", type: "number", required: false, description: "Controlled rating; pair with `onValueChange`." }], description: "Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover\npreview and a spring lift/press on the stars. `readOnly`: an image of the score, fractions included." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/rating.tsx", type: "registry:ui", content: `"use client";
11308
+ }` }], api: { entries: [{ kind: "component", name: "Rating", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number", required: false, description: "Rating at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Stars can't be picked and the value isn't submitted." }, { name: "label", type: "string", required: false, description: "Group label (visually hidden)." }, { name: "max", type: "number", required: false, description: "Number of stars." }, { name: "name", type: "string", required: false, description: "Form field name for the chosen value." }, { name: "onValueChange", type: "(value: number) => void", required: false, description: "Called with the picked rating." }, { name: "readOnly", type: "boolean", required: false, description: "Display only: renders fractions (4.6) as a partially filled star, no inputs." }, { name: "required", type: "boolean", required: false, description: "Submit needs a rating." }, { name: "size", type: '"sm" | "default"', required: false, description: "Star size." }, { name: "value", type: "number", required: false, description: "Controlled rating; pair with `onValueChange`." }], description: "Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover\npreview and a spring lift/press on the stars. `readOnly`: an image of the score, fractions included." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/rating.tsx", type: "registry:ui", content: `"use client";
11039
11309
 
11040
11310
  import { Star } from "lucide-react";
11041
11311
  import { motion } from "motion/react";
11042
11312
  import { useId, useState } from "react";
11313
+ import { useFieldControl } from "@/components/ui/field";
11043
11314
  import { useControllableState } from "@/lib/use-controllable-state";
11044
11315
  import { clamp, cn, spring } from "@/lib/utils";
11045
11316
 
@@ -11056,10 +11327,18 @@ type RatingProps = {
11056
11327
  readOnly?: boolean;
11057
11328
  /** Form field name for the chosen value. */
11058
11329
  name?: string;
11330
+ /** Submit needs a rating. */
11331
+ required?: boolean;
11332
+ /** Stars can't be picked and the value isn't submitted. */
11333
+ disabled?: boolean;
11059
11334
  /** Group label (visually hidden). */
11060
11335
  label?: string;
11061
11336
  /** Star size. */
11062
11337
  size?: "sm" | "default";
11338
+ /** Ids of hint or error text; Field fills it in. */
11339
+ "aria-describedby"?: string;
11340
+ /** Error styling; Field sets it from \`error\`. */
11341
+ "aria-invalid"?: boolean;
11063
11342
  className?: string;
11064
11343
  };
11065
11344
 
@@ -11069,17 +11348,22 @@ const sizes = { sm: "size-4", default: "size-5" } as const;
11069
11348
  * Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover
11070
11349
  * preview and a spring lift/press on the stars. \`readOnly\`: an image of the score, fractions included.
11071
11350
  */
11072
- export function Rating({
11073
- value: valueProp,
11074
- defaultValue = 0,
11075
- onValueChange,
11076
- max = 5,
11077
- readOnly = false,
11078
- name,
11079
- label = "Rating",
11080
- size = "default",
11081
- className,
11082
- }: RatingProps) {
11351
+ export function Rating(allProps: RatingProps) {
11352
+ const {
11353
+ value: valueProp,
11354
+ defaultValue = 0,
11355
+ onValueChange,
11356
+ max = 5,
11357
+ readOnly = false,
11358
+ name,
11359
+ required,
11360
+ disabled,
11361
+ label = "Rating",
11362
+ size = "default",
11363
+ className,
11364
+ "aria-describedby": describedBy,
11365
+ "aria-invalid": invalid,
11366
+ } = useFieldControl(allProps);
11083
11367
  const auto = useId();
11084
11368
  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
11085
11369
  const [hover, setHover] = useState<number | null>(null);
@@ -11103,6 +11387,7 @@ export function Rating({
11103
11387
  fill="currentColor"
11104
11388
  strokeWidth={0}
11105
11389
  />
11390
+
11106
11391
  <span
11107
11392
  className="absolute inset-0 overflow-hidden"
11108
11393
  style={{ width: \`\${fill * 100}%\` }}
@@ -11125,23 +11410,34 @@ export function Rating({
11125
11410
 
11126
11411
  return (
11127
11412
  <fieldset
11413
+ disabled={disabled}
11414
+ aria-describedby={describedBy}
11128
11415
  onPointerLeave={() => setHover(null)}
11129
- className={cn("inline-flex items-center gap-0.5", className)}
11416
+ className={cn("inline-flex items-center gap-0.5", disabled && "opacity-50", className)}
11130
11417
  >
11131
11418
  <legend className="sr-only">{label}</legend>
11419
+
11132
11420
  {stars.map((star) => (
11133
- <label key={star} className="relative cursor-pointer" onPointerEnter={() => setHover(star)}>
11421
+ <label
11422
+ key={star}
11423
+ className={cn("relative", disabled ? "cursor-not-allowed" : "cursor-pointer")}
11424
+ onPointerEnter={() => !disabled && setHover(star)}
11425
+ >
11134
11426
  <input
11135
11427
  type="radio"
11136
11428
  name={name ?? auto}
11137
11429
  value={star}
11138
11430
  checked={value === star}
11431
+ required={required}
11432
+ aria-invalid={invalid || undefined}
11139
11433
  onChange={() => setValue(star)}
11140
11434
  className="peer sr-only"
11141
11435
  />
11436
+
11142
11437
  <span className="sr-only">
11143
11438
  {star} {star === 1 ? "star" : "stars"}
11144
11439
  </span>
11440
+
11145
11441
  <motion.span
11146
11442
  aria-hidden="true"
11147
11443
  // whileTap would make the star focusable, the radio underneath is the tab stop.
@@ -11293,11 +11589,11 @@ export function AdminLayout({ nav, log, children }: {
11293
11589
  </ResizablePanel>
11294
11590
  </ResizablePanelGroup>
11295
11591
  );
11296
- }` }], api: { entries: [{ kind: "component", name: "ResizablePanelGroup", props: [{ name: "direction", type: "Direction", required: false, default: '"horizontal"', description: '"horizontal": panels side by side. "vertical": stacked.' }, { name: "keyboardStep", type: "number", required: false, default: "5", description: "% per arrow key press." }, { name: "onLayout", type: "(sizes: number[]) => void", required: false, description: "Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR." }], extends: "<div>", description: `Split panes (admin layouts, docs, editors). Sizes are % of the group. Handles are APG window
11592
+ }` }], api: { entries: [{ kind: "component", name: "ResizablePanelGroup", props: [{ name: "direction", type: "ResizableDirection", required: false, default: '"horizontal"', description: '"horizontal": panels side by side. "vertical": stacked.' }, { name: "keyboardStep", type: "number", required: false, default: "5", description: "% per arrow key press." }, { name: "onLayout", type: "(sizes: number[]) => void", required: false, description: "Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR." }], extends: "<div>", description: `Split panes (admin layouts, docs, editors). Sizes are % of the group. Handles are APG window
11297
11593
  splitters: arrows step, Home/End jump to the limits, Enter collapses a collapsible neighbor.
11298
11594
  Drag follows the pointer; keys and toggles spring. Panels and handles find their order in the
11299
11595
  DOM, so wrappers and Server Component parents work. Until hydration the layout is plain CSS
11300
- (\`defaultSize\` as a flex basis, the others share the rest), so SSR already shows it.` }, { kind: "component", name: "ResizablePanel", props: [{ name: "collapsedSize", type: "number", required: false, default: "0", description: "Share in % when collapsed (default 0), e.g. an icon rail." }, { name: "collapsible", type: "boolean", required: false, default: "false", description: "Dragging past half the minimum snaps it shut; Enter or double-click on the handle toggles." }, { name: "defaultSize", type: "number", required: false, description: "Starting share of the group in %. Panels without one split what is left evenly." }, { name: "maxSize", type: "number", required: false, default: "100", description: "Largest size in % of the group." }, { name: "minSize", type: "number", required: false, default: "10", description: "Smallest share in % (default 10)." }], extends: "<div>" }, { kind: "component", name: "ResizableHandle", props: [{ name: "withHandle", type: "boolean", required: false, description: "Visible grip in the middle of the divider." }], extends: "<div>", description: "The divider between two panels. `withHandle` adds a visible grip." }], types: { Direction: '"horizontal" | "vertical"' } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/resizable.tsx", type: "registry:ui", content: `"use client";
11596
+ (\`defaultSize\` as a flex basis, the others share the rest), so SSR already shows it.` }, { kind: "component", name: "ResizablePanel", props: [{ name: "collapsedSize", type: "number", required: false, default: "0", description: "Share in % when collapsed (default 0), e.g. an icon rail." }, { name: "collapsible", type: "boolean", required: false, default: "false", description: "Dragging past half the minimum snaps it shut; Enter or double-click on the handle toggles." }, { name: "defaultSize", type: "number", required: false, description: "Starting share of the group in %. Panels without one split what is left evenly." }, { name: "maxSize", type: "number", required: false, default: "100", description: "Largest size in % of the group." }, { name: "minSize", type: "number", required: false, default: "10", description: "Smallest share in % (default 10)." }], extends: "<div>" }, { kind: "component", name: "ResizableHandle", props: [{ name: "withHandle", type: "boolean", required: false, description: "Visible grip in the middle of the divider." }], extends: "<div>", description: "The divider between two panels. `withHandle` adds a visible grip." }], types: { ResizableDirection: '"horizontal" | "vertical"' } }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/resizable.tsx", type: "registry:ui", content: `"use client";
11301
11597
 
11302
11598
  import { type AnimationPlaybackControls, animate, useReducedMotion } from "motion/react";
11303
11599
  import {
@@ -11313,7 +11609,7 @@ import {
11313
11609
  } from "react";
11314
11610
  import { cn, spring } from "@/lib/utils";
11315
11611
 
11316
- type Direction = "horizontal" | "vertical";
11612
+ export type ResizableDirection = "horizontal" | "vertical";
11317
11613
  type Limits = { min: number; max: number; collapsible: boolean; collapsed: number };
11318
11614
 
11319
11615
  type ResizablePanelProps = ComponentProps<"div"> & {
@@ -11337,7 +11633,7 @@ type Layout = {
11337
11633
  };
11338
11634
 
11339
11635
  type GroupCtx = {
11340
- direction: Direction;
11636
+ direction: ResizableDirection;
11341
11637
  id: string;
11342
11638
  layout: Layout | null;
11343
11639
  /** Panels report their limits here from a layout effect; the group's own effect reads them. */
@@ -11439,7 +11735,7 @@ export function ResizablePanelGroup({
11439
11735
  ...props
11440
11736
  }: ComponentProps<"div"> & {
11441
11737
  /** "horizontal": panels side by side. "vertical": stacked. */
11442
- direction?: Direction;
11738
+ direction?: ResizableDirection;
11443
11739
  /** Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR. */
11444
11740
  onLayout?: (sizes: number[]) => void;
11445
11741
  /** % per arrow key press. */
@@ -12060,7 +12356,9 @@ export function ScrollArea({
12060
12356
  >
12061
12357
  {children}
12062
12358
  </div>
12359
+
12063
12360
  {orientation !== "horizontal" && <Thumb axis="y" viewport={viewport} />}
12361
+
12064
12362
  {orientation !== "vertical" && <Thumb axis="x" viewport={viewport} />}
12065
12363
  </div>
12066
12364
  );
@@ -12157,6 +12455,7 @@ export function SearchInput({
12157
12455
  aria-hidden="true"
12158
12456
  className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-muted-foreground"
12159
12457
  />
12458
+
12160
12459
  <Input
12161
12460
  ref={(element) => {
12162
12461
  input.current = element;
@@ -12183,6 +12482,7 @@ export function SearchInput({
12183
12482
  )}
12184
12483
  {...props}
12185
12484
  />
12485
+
12186
12486
  <span className="-translate-y-1/2 absolute top-1/2 right-2 flex">
12187
12487
  <AnimatePresence mode="popLayout" initial={false}>
12188
12488
  {value ? (
@@ -12289,6 +12589,7 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
12289
12589
  transition={spring}
12290
12590
  className="-z-10 absolute inset-y-1 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
12291
12591
  />
12592
+
12292
12593
  {items.map((item) => (
12293
12594
  <button
12294
12595
  key={item}
@@ -12307,11 +12608,12 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
12307
12608
  {item}
12308
12609
  </button>
12309
12610
  ))}
12611
+
12310
12612
  <OverflowFocus />
12311
12613
  </nav>
12312
12614
  );
12313
12615
  }
12314
- ` }] }, select: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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 via a hidden input.", docs: "https://kznlabs.github.io/labsUI/components/select", usage: '<Select id="role" name="role" options={[{ value: "admin", label: "Admin" }]} value={v} onValueChange={setV} placeholder="Pick a role" />', examples: [{ name: "role-picker.tsx", code: `"use client";
12616
+ ` }] }, select: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/select", usage: '<Select id="role" name="role" options={[{ value: "admin", label: "Admin" }]} value={v} onValueChange={setV} placeholder="Pick a role" />', examples: [{ name: "role-picker.tsx", code: `"use client";
12315
12617
 
12316
12618
  import { useState } from "react";
12317
12619
  import { Field } from "@/components/ui/field";
@@ -12330,8 +12632,8 @@ export function RolePicker() {
12330
12632
  <Select name="role" options={roles} value={role} onValueChange={setRole} />
12331
12633
  </Field>
12332
12634
  );
12333
- }` }], api: { entries: [{ kind: "component", name: "Select", props: [{ name: "options", type: "SelectGroup[] | SelectOption[]", required: true, description: "Flat options, or labeled groups of options." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string", required: false, description: "Value at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "name", type: "string", required: false, description: "Submits the value with a form, through a hidden input." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked value." }, { name: "placeholder", type: "string", required: false, description: "Trigger text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], description: `Single-choice listbox styled like Input. Keyboard matches a native select: Up, Down, Home, End,
12334
- typeahead (also while closed), Enter or Space to pick, Esc to cancel.` }], types: { SelectOption: "{ value: string; label: string; icon?: ReactNode; disabled?: boolean }", SelectGroup: "{ label: string; options: SelectOption[] }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/select.tsx", type: "registry:ui", content: `"use client";
12635
+ }` }], api: { entries: [{ kind: "component", name: "Select", props: [{ name: "options", type: "SelectGroup[] | SelectOption[]", required: true, description: "Flat options, or labeled groups of options." }, { name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "aria-label", type: "string", required: false, description: "Accessible name when there is no visible label." }, { name: "defaultValue", type: "string", required: false, description: "Value at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks opening; the value isn't submitted." }, { name: "id", type: "string", required: false, description: "Pair with <Label htmlFor>, or let <Field> fill it in." }, { name: "name", type: "string", required: false, description: "Form field name for the picked value." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the picked value." }, { name: "placeholder", type: "string", required: false, description: "Trigger text when nothing is picked." }, { name: "required", type: "boolean", required: false, description: "Form submit needs a value." }, { name: "value", type: "string", required: false, description: "Controlled value; pair with `onValueChange`." }], description: `Single-choice listbox styled like Input. Keyboard matches a native select: Up, Down, Home, End,
12636
+ typeahead (also while closed), Enter or Space to pick, Esc to cancel.` }], types: { SelectOption: "{ value: string; label: string; icon?: ReactNode; disabled?: boolean }", SelectGroup: "{ label: string; options: SelectOption[] }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/input.json", "https://kznlabs.github.io/labsUI/r/list.json", "https://kznlabs.github.io/labsUI/r/platform.json", "https://kznlabs.github.io/labsUI/r/use-anchor.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/select.tsx", type: "registry:ui", content: `"use client";
12335
12637
 
12336
12638
  import { Check, ChevronsUpDown } from "lucide-react";
12337
12639
  import { AnimatePresence, motion } from "motion/react";
@@ -12339,6 +12641,7 @@ import { type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState
12339
12641
  import { createPortal } from "react-dom";
12340
12642
  import { useFieldControl } from "@/components/ui/field";
12341
12643
  import { fieldClass } from "@/components/ui/input";
12644
+ import { FormValue } from "@/lib/form-value";
12342
12645
  import { listKeyTarget, scrollIntoList } from "@/lib/list";
12343
12646
  import { useMounted } from "@/lib/platform";
12344
12647
  import { useAnchor } from "@/lib/use-anchor";
@@ -12346,8 +12649,8 @@ import { useControllableState } from "@/lib/use-controllable-state";
12346
12649
  import { useDismiss } from "@/lib/use-dismiss";
12347
12650
  import { cn, material, spring } from "@/lib/utils";
12348
12651
 
12349
- type SelectOption = { value: string; label: string; icon?: ReactNode; disabled?: boolean };
12350
- type SelectGroup = { label: string; options: SelectOption[] };
12652
+ export type SelectOption = { value: string; label: string; icon?: ReactNode; disabled?: boolean };
12653
+ export type SelectGroup = { label: string; options: SelectOption[] };
12351
12654
 
12352
12655
  type SelectProps = {
12353
12656
  /** Flat options, or labeled groups of options. */
@@ -12360,11 +12663,11 @@ type SelectProps = {
12360
12663
  onValueChange?: (value: string) => void;
12361
12664
  /** Trigger text when nothing is picked. */
12362
12665
  placeholder?: string;
12363
- /** Submits the value with a form, through a hidden input. */
12666
+ /** Form field name for the picked value. */
12364
12667
  name?: string;
12365
12668
  /** Pair with <Label htmlFor>, or let <Field> fill it in. */
12366
12669
  id?: string;
12367
- /** Blocks opening. */
12670
+ /** Blocks opening; the value isn't submitted. */
12368
12671
  disabled?: boolean;
12369
12672
  /** Form submit needs a value. */
12370
12673
  required?: boolean;
@@ -12403,6 +12706,7 @@ export function Select(allProps: SelectProps) {
12403
12706
  const list = useRef<HTMLDivElement>(null);
12404
12707
  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
12405
12708
  const [open, setOpen] = useState(false);
12709
+ const [invalid, setInvalid] = useState(false);
12406
12710
  const [active, setActive] = useState<string>();
12407
12711
  const pos = useAnchor(open, trigger, list);
12408
12712
  const mounted = useMounted();
@@ -12507,6 +12811,7 @@ export function Select(allProps: SelectProps) {
12507
12811
  className,
12508
12812
  )}
12509
12813
  {...aria}
12814
+ aria-invalid={aria["aria-invalid"] || (invalid && !value) || undefined}
12510
12815
  >
12511
12816
  <span
12512
12817
  className={cn(
@@ -12515,11 +12820,22 @@ export function Select(allProps: SelectProps) {
12515
12820
  )}
12516
12821
  >
12517
12822
  {selected?.icon}
12823
+
12518
12824
  {selected?.label ?? placeholder}
12519
12825
  </span>
12826
+
12520
12827
  <ChevronsUpDown className="text-muted-foreground" aria-hidden="true" />
12521
12828
  </button>
12522
- {name && <input type="hidden" name={name} value={value} />}
12829
+
12830
+ <FormValue
12831
+ name={name}
12832
+ value={value}
12833
+ required={required}
12834
+ disabled={disabled}
12835
+ anchor={trigger}
12836
+ onInvalid={() => setInvalid(true)}
12837
+ />
12838
+
12523
12839
  {mounted &&
12524
12840
  createPortal(
12525
12841
  <AnimatePresence>
@@ -12560,6 +12876,7 @@ export function Select(allProps: SelectProps) {
12560
12876
  {groupIndex > 0 && (
12561
12877
  <div aria-hidden="true" className="-mx-1 my-1 h-px bg-border" />
12562
12878
  )}
12879
+
12563
12880
  {group.label && (
12564
12881
  <div
12565
12882
  aria-hidden="true"
@@ -12568,6 +12885,7 @@ export function Select(allProps: SelectProps) {
12568
12885
  {group.label}
12569
12886
  </div>
12570
12887
  )}
12888
+
12571
12889
  {group.options.map((option) => (
12572
12890
  // biome-ignore lint/a11y/useKeyWithClickEvents: keys handled on the listbox (aria-activedescendant)
12573
12891
  <div
@@ -12591,7 +12909,9 @@ export function Select(allProps: SelectProps) {
12591
12909
  )}
12592
12910
  >
12593
12911
  {option.icon}
12912
+
12594
12913
  {option.label}
12914
+
12595
12915
  {option.value === value && (
12596
12916
  <Check className="!text-current absolute right-2" aria-hidden="true" />
12597
12917
  )}
@@ -12778,7 +13098,7 @@ export function SidebarLayout({
12778
13098
  );
12779
13099
  }
12780
13100
 
12781
- type SidebarItem = {
13101
+ export type SidebarItem = {
12782
13102
  /** What \`current\` and \`onSelect\` refer to. Default: \`href\`, else \`label\`. */
12783
13103
  id?: string;
12784
13104
  /** Visible text; the rail shows it as a tooltip. */
@@ -12791,7 +13111,7 @@ type SidebarItem = {
12791
13111
  badge?: ReactNode;
12792
13112
  };
12793
13113
 
12794
- type SidebarGroup = {
13114
+ export type SidebarGroup = {
12795
13115
  /** Heading above the items; fades out in the rail. */
12796
13116
  label?: string;
12797
13117
  /** The group's entries. */
@@ -12855,6 +13175,7 @@ export function Sidebar({
12855
13175
  const body = (
12856
13176
  <nav aria-label={label} className="flex min-h-0 flex-1 flex-col">
12857
13177
  {header && <div className="border-b p-2">{header}</div>}
13178
+
12858
13179
  <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-2">
12859
13180
  {groups.map((group, i) =>
12860
13181
  group.collapsible && group.label ? (
@@ -12863,6 +13184,7 @@ export function Sidebar({
12863
13184
  <CollapsibleTrigger className={cn("px-2.5 pb-1 text-muted-foreground text-xs", fade)}>
12864
13185
  {group.label}
12865
13186
  </CollapsibleTrigger>
13187
+
12866
13188
  <CollapsibleContent>{list(group.items)}</CollapsibleContent>
12867
13189
  </Collapsible>
12868
13190
  ) : (
@@ -12878,11 +13200,13 @@ export function Sidebar({
12878
13200
  {group.label}
12879
13201
  </p>
12880
13202
  )}
13203
+
12881
13204
  {list(group.items)}
12882
13205
  </div>
12883
13206
  ),
12884
13207
  )}
12885
13208
  </div>
13209
+
12886
13210
  {footer?.length ? <div className="border-t p-2">{list(footer)}</div> : null}
12887
13211
  </nav>
12888
13212
  );
@@ -12892,6 +13216,7 @@ export function Sidebar({
12892
13216
  <Drawer open={openMobile} onOpenChange={setOpenMobile}>
12893
13217
  <DrawerContent side="left" className={cn("bg-background pt-12 text-sm", className)}>
12894
13218
  <DrawerTitle className="sr-only">{label}</DrawerTitle>
13219
+
12895
13220
  {body}
12896
13221
  </DrawerContent>
12897
13222
  </Drawer>
@@ -12933,7 +13258,9 @@ function Entry({
12933
13258
  const body = (
12934
13259
  <>
12935
13260
  {item.icon}
13261
+
12936
13262
  <span className="min-w-0 flex-1 truncate">{item.label}</span>
13263
+
12937
13264
  {item.badge != null && (
12938
13265
  <span className="ml-auto text-muted-foreground text-xs tabular-nums">{item.badge}</span>
12939
13266
  )}
@@ -13030,8 +13357,8 @@ import {
13030
13357
  import { useDisclosure } from "@/lib/use-disclosure";
13031
13358
  import { cn } from "@/lib/utils";
13032
13359
 
13033
- type SiteLink = { label: string; href: string };
13034
- type SiteGroup = {
13360
+ export type SiteLink = { label: string; href: string };
13361
+ export type SiteGroup = {
13035
13362
  /** Trigger text for the panel. */
13036
13363
  label: string;
13037
13364
  /** Links in the panel, each with an optional description and icon. */
@@ -13096,6 +13423,7 @@ export function SiteHeader({
13096
13423
  return (
13097
13424
  <>
13098
13425
  <span ref={sentinel} aria-hidden="true" className="block h-px" />
13426
+
13099
13427
  <header
13100
13428
  ref={header}
13101
13429
  className={cn(
@@ -13112,6 +13440,7 @@ export function SiteHeader({
13112
13440
  isGroup(link) ? (
13113
13441
  <NavigationMenuItem key={link.label}>
13114
13442
  <NavigationMenuTrigger>{link.label}</NavigationMenuTrigger>
13443
+
13115
13444
  <NavigationMenuContent>
13116
13445
  <ul
13117
13446
  className={cn(
@@ -13153,6 +13482,7 @@ export function SiteHeader({
13153
13482
 
13154
13483
  <div className="ml-auto flex items-center gap-2">
13155
13484
  {actions}
13485
+
13156
13486
  <Button
13157
13487
  variant="ghost"
13158
13488
  size="icon"
@@ -13172,6 +13502,7 @@ export function SiteHeader({
13172
13502
  <DrawerHeader>
13173
13503
  <DrawerTitle>Menu</DrawerTitle>
13174
13504
  </DrawerHeader>
13505
+
13175
13506
  <DrawerBody>
13176
13507
  <nav aria-label={label} className="flex flex-col gap-5">
13177
13508
  {nav.map((link) =>
@@ -13180,6 +13511,7 @@ export function SiteHeader({
13180
13511
  <h2 className="px-2.5 font-medium text-muted-foreground text-xs">
13181
13512
  {link.label}
13182
13513
  </h2>
13514
+
13183
13515
  <ul className="flex flex-col">
13184
13516
  {link.items.map((i) => (
13185
13517
  <li key={i.href}>
@@ -13241,6 +13573,7 @@ function MobileLink({
13241
13573
  className="flex h-10 items-center gap-3 rounded-lg px-2.5 font-medium text-sm outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[current=page]:bg-foreground/[0.08] [&_svg]:size-4 [&_svg]:text-primary-ink"
13242
13574
  >
13243
13575
  {icon && <span aria-hidden="true">{icon}</span>}
13576
+
13244
13577
  {children}
13245
13578
  </a>
13246
13579
  );
@@ -13289,6 +13622,7 @@ export function SkeletonGroup({
13289
13622
  return (
13290
13623
  <div role="status" aria-busy="true" className={className} {...props}>
13291
13624
  <span className="sr-only">{label}</span>
13625
+
13292
13626
  {children}
13293
13627
  </div>
13294
13628
  );
@@ -13306,7 +13640,7 @@ export function PriceFilter() {
13306
13640
  <Slider value={price} onValueChange={setPrice} min={0} max={250} step={5} name="price" />
13307
13641
  </Field>
13308
13642
  );
13309
- }` }], api: { entries: [{ kind: "component", name: "Slider", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number[]", required: false, description: "Values at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks dragging and keys." }, { name: "id", type: "string", required: false, description: "Id of the first thumb; Field fills it in." }, { name: "labels", type: "string[]", required: false, description: 'Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range.' }, { name: "max", type: "number", required: false, description: "Highest value." }, { name: "min", type: "number", required: false, description: "Lowest value." }, { name: "name", type: "string", required: false, description: "Submitted per thumb: `name` (one value) or `name` twice (range)." }, { name: "onValueChange", type: "(value: number[]) => void", required: false, description: "Called with every thumb's value while dragging." }, { name: "step", type: "number", required: false, description: "Increment for drag and arrow keys." }, { name: "value", type: "number[]", required: false, description: "One value, or two for a range (e.g. a price filter)." }], description: `Slider over native range inputs: keyboard, forms, touch and screen readers work as the
13643
+ }` }], api: { entries: [{ kind: "component", name: "Slider", props: [{ name: "aria-describedby", type: "string", required: false, description: "Ids of hint or error text; Field fills it in." }, { name: "aria-invalid", type: "boolean", required: false, description: "Error styling; Field sets it from `error`." }, { name: "defaultValue", type: "number[]", required: false, description: "Values at first render (uncontrolled)." }, { name: "disabled", type: "boolean", required: false, description: "Blocks dragging and keys; the values aren't submitted." }, { name: "id", type: "string", required: false, description: "Id of the first thumb; Field fills it in." }, { name: "labels", type: "string[]", required: false, description: 'Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range.' }, { name: "max", type: "number", required: false, description: "Highest value." }, { name: "min", type: "number", required: false, description: "Lowest value." }, { name: "name", type: "string", required: false, description: "Submitted per thumb: `name` (one value) or `name` twice (range)." }, { name: "onValueChange", type: "(value: number[]) => void", required: false, description: "Called with every thumb's value while dragging." }, { name: "step", type: "number", required: false, description: "Increment for drag and arrow keys." }, { name: "value", type: "number[]", required: false, description: "One value, or two for a range (e.g. a price filter)." }], description: `Slider over native range inputs: keyboard, forms, touch and screen readers work as the
13310
13644
  browser does. Thin accent track, white knob that lifts while held. Two values = range.` }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/slider.tsx", type: "registry:ui", content: `"use client";
13311
13645
 
13312
13646
  import { motion } from "motion/react";
@@ -13330,7 +13664,7 @@ type SliderProps = {
13330
13664
  step?: number;
13331
13665
  /** Submitted per thumb: \`name\` (one value) or \`name\` twice (range). */
13332
13666
  name?: string;
13333
- /** Blocks dragging and keys. */
13667
+ /** Blocks dragging and keys; the values aren't submitted. */
13334
13668
  disabled?: boolean;
13335
13669
  /** Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range. */
13336
13670
  labels?: string[];
@@ -13409,6 +13743,7 @@ export function Slider(allProps: SliderProps) {
13409
13743
  }}
13410
13744
  />
13411
13745
  </div>
13746
+
13412
13747
  {values.map((value, i) => (
13413
13748
  <motion.span
13414
13749
  // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
@@ -13423,6 +13758,7 @@ export function Slider(allProps: SliderProps) {
13423
13758
  )}
13424
13759
  />
13425
13760
  ))}
13761
+
13426
13762
  {values.map((thumbValue, i) => (
13427
13763
  <input
13428
13764
  // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
@@ -13505,6 +13841,7 @@ export function Spinner({
13505
13841
  {...props}
13506
13842
  >
13507
13843
  {label && <span className="sr-only">{label}</span>}
13844
+
13508
13845
  {spokes.map((i) => (
13509
13846
  <span
13510
13847
  key={i}
@@ -13605,10 +13942,12 @@ export function StatTile({
13605
13942
  <Card className={cn("gap-4 pb-4", className)}>
13606
13943
  <CardHeader>
13607
13944
  <CardDescription>{label}</CardDescription>
13945
+
13608
13946
  <div className="flex flex-wrap items-center gap-2">
13609
13947
  <CardTitle as="p" className="font-semibold text-3xl tabular-nums">
13610
13948
  {value}
13611
13949
  </CardTitle>
13950
+
13612
13951
  {delta !== undefined && delta !== 0 && (
13613
13952
  <Badge variant={good ? "success" : "destructive"} shape="pill">
13614
13953
  <Icon aria-hidden="true" />
@@ -13617,12 +13956,14 @@ export function StatTile({
13617
13956
  </Badge>
13618
13957
  )}
13619
13958
  </div>
13959
+
13620
13960
  {period && (
13621
13961
  <p aria-hidden="true" className="text-muted-foreground text-xs">
13622
13962
  {period}
13623
13963
  </p>
13624
13964
  )}
13625
13965
  </CardHeader>
13966
+
13626
13967
  {data && data.length > 1 && (
13627
13968
  <Chart
13628
13969
  type="area"
@@ -13650,14 +13991,14 @@ export function LandingStats({ users, uptime }: { users: number; uptime: number
13650
13991
  ]}
13651
13992
  />
13652
13993
  );
13653
- }` }], api: { entries: [{ kind: "component", name: "StatsStrip", props: [{ name: "stats", type: "Stat[]", required: true, description: "Numbers in display order." }], description: "A row of headline numbers: each card rises in turn, then its number springs up. Each card is a <dl>." }], types: { Stat: "{ label: string; value: number; prefix?: string; suffix?: string; decimals?: number; hint?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/count-to.json", "https://kznlabs.github.io/labsUI/r/reveal.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/stats-strip.tsx", type: "registry:block", content: `import { Card } from "@/components/ui/card";
13994
+ }` }], api: { entries: [{ kind: "component", name: "StatsStrip", props: [{ name: "stats", type: "StatsStripItem[]", required: true, description: "Numbers in display order." }], description: "A row of headline numbers: each card rises in turn, then its number springs up. Each card is a <dl>." }], types: { StatsStripItem: "{ label: string; value: number; prefix?: string; suffix?: string; decimals?: number; hint?: string; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/card.json", "https://kznlabs.github.io/labsUI/r/count-to.json", "https://kznlabs.github.io/labsUI/r/reveal.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/blocks/stats-strip.tsx", type: "registry:block", content: `import { Card } from "@/components/ui/card";
13654
13995
  import { CountTo } from "@/components/ui/count-to";
13655
13996
  import { Reveal } from "@/components/ui/reveal";
13656
13997
  import { cn } from "@/lib/utils";
13657
13998
 
13658
13999
  // Server-safe: CountTo and Reveal are the client parts.
13659
14000
 
13660
- type Stat = {
14001
+ export type StatsStripItem = {
13661
14002
  /** What the number counts, e.g. "Active users". */
13662
14003
  label: string;
13663
14004
  /** Number to count up to. */
@@ -13678,7 +14019,7 @@ export function StatsStrip({
13678
14019
  className,
13679
14020
  }: {
13680
14021
  /** Numbers in display order. */
13681
- stats: Stat[];
14022
+ stats: StatsStripItem[];
13682
14023
  className?: string;
13683
14024
  }) {
13684
14025
  return (
@@ -13693,6 +14034,7 @@ export function StatsStrip({
13693
14034
  <Card className="h-full px-5 py-4">
13694
14035
  <dl className="flex flex-col gap-1">
13695
14036
  <dt className="text-muted-foreground text-sm">{stat.label}</dt>
14037
+
13696
14038
  <dd className="font-semibold text-3xl tabular-nums tracking-tight">
13697
14039
  <CountTo
13698
14040
  to={stat.value}
@@ -13701,6 +14043,7 @@ export function StatsStrip({
13701
14043
  decimals={stat.decimals}
13702
14044
  />
13703
14045
  </dd>
14046
+
13704
14047
  {stat.hint && <dd className="text-muted-foreground text-xs">{stat.hint}</dd>}
13705
14048
  </dl>
13706
14049
  </Card>
@@ -13712,7 +14055,7 @@ export function StatsStrip({
13712
14055
  ` }] }, stepper: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/stepper", usage: '<Stepper steps={[{ value: "server", title: "Server" }, { value: "bot", title: "Bot" }]} action={createBot}><StepperList /><StepperStep value="server"><Input name="serverId" required /></StepperStep><StepperStep value="bot">…</StepperStep><StepperFooter submitLabel="Create bot" /></Stepper>', examples: [{ name: "setup-wizard.tsx", code: `import { Field } from "@/components/ui/field";
13713
14056
  import { Input } from "@/components/ui/input";
13714
14057
  import { Stepper, StepperFooter, StepperList, StepperStep } from "@/components/ui/stepper";
13715
- import { createBot } from "./actions"; // "use server"
14058
+ import { createBot } from "./actions";
13716
14059
 
13717
14060
  const steps = [
13718
14061
  { value: "server", title: "Server", description: "Where the bot will live." },
@@ -13737,7 +14080,14 @@ export default function SetupPage() {
13737
14080
  <StepperFooter submitLabel="Create bot" />
13738
14081
  </Stepper>
13739
14082
  );
13740
- }` }], api: { entries: [{ kind: "component", name: "Stepper", props: [{ name: "steps", type: "StepInfo[]", required: true, description: "Steps in order: `value`, `title` and an optional `description` for the header." }, { name: "defaultValue", type: "string", required: false, description: "Step shown at first render (uncontrolled)." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the new step's `value`." }, { name: "value", type: "string", required: false, description: "Current step's `value`. Controlled with `onValueChange`, or start at `defaultValue`." }], extends: "<form>", description: "Multi-step form. Every step stays mounted (hidden), so values survive Back and submit\ntogether; Continue checks only the current step's native constraints, and the last step\nsubmits the form (`onSubmit`, or a Server Action through `action`)." }, { kind: "component", name: "StepperList", props: [], extends: "<ol>", description: "Progress header. Finished steps are links back; the connector fills on a spring." }, { kind: "component", name: "StepperStep", props: [{ name: "children", type: "ReactNode", required: true, description: "The step's fields." }, { name: "value", type: "string", required: true, description: "Matches a step in `steps`." }, { name: "onNext", type: "Guard", required: false, description: "Runs on Continue after the native checks; return false to stay." }], description: "One step's fields. Hidden, not unmounted, while inactive. `onNext` runs after its native\nconstraints pass; return false (or resolve false) to stay, e.g. after a server check." }, { kind: "component", name: "StepperFooter", props: [{ name: "backLabel", type: "ReactNode", required: false, default: '"Back"', description: "Back button text." }, { name: "nextLabel", type: "ReactNode", required: false, default: '"Continue"', description: "Continue button text." }, { name: "submitLabel", type: "ReactNode", required: false, default: '"Submit"', description: "Submit button text on the last step." }], description: "Back and Continue; the last step's button submits. Continue shows a spinner while `onNext` runs." }], types: { StepInfo: "{ value: string; title: string; description?: string }", Guard: "() => boolean | undefined | Promise<boolean | undefined>" } }, 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/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/stepper.tsx", type: "registry:ui", content: `"use client";
14083
+ }` }, { name: "actions.ts", code: `"use server";
14084
+
14085
+ export async function createBot(formData: FormData) {
14086
+ const serverId = String(formData.get("serverId"));
14087
+ const botName = String(formData.get("botName"));
14088
+
14089
+ console.log({ serverId, botName });
14090
+ }` }], api: { entries: [{ kind: "component", name: "Stepper", props: [{ name: "steps", type: "StepInfo[]", required: true, description: "Steps in order: `value`, `title` and an optional `description` for the header." }, { name: "defaultValue", type: "string", required: false, description: "Step shown at first render (uncontrolled)." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: "Called with the new step's `value`." }, { name: "value", type: "string", required: false, description: "Current step's `value`. Controlled with `onValueChange`, or start at `defaultValue`." }], extends: "<form>", description: "Multi-step form. Every step stays mounted (hidden), so values survive Back and submit\ntogether; Continue checks only the current step's native constraints, and the last step\nsubmits the form (`onSubmit`, or a Server Action through `action`)." }, { kind: "component", name: "StepperList", props: [], extends: "<ol>", description: "Progress header. Finished steps are links back; the connector fills on a spring." }, { kind: "component", name: "StepperStep", props: [{ name: "children", type: "ReactNode", required: true, description: "The step's fields." }, { name: "value", type: "string", required: true, description: "Matches a step in `steps`." }, { name: "onNext", type: "StepGuard", required: false, description: "Runs on Continue after the native checks; return false to stay." }], description: "One step's fields. Hidden, not unmounted, while inactive. `onNext` runs after its native\nconstraints pass; return false (or resolve false) to stay, e.g. after a server check." }, { kind: "component", name: "StepperFooter", props: [{ name: "backLabel", type: "ReactNode", required: false, default: '"Back"', description: "Back button text." }, { name: "nextLabel", type: "ReactNode", required: false, default: '"Continue"', description: "Continue button text." }, { name: "submitLabel", type: "ReactNode", required: false, default: '"Submit"', description: "Submit button text on the last step." }], description: "Back and Continue; the last step's button submits. Continue shows a spinner while `onNext` runs." }], types: { StepInfo: "{ value: string; title: string; description?: string }", StepGuard: "() => boolean | undefined | Promise<boolean | undefined>" } }, 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/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/stepper.tsx", type: "registry:ui", content: `"use client";
13741
14091
 
13742
14092
  import { Check } from "lucide-react";
13743
14093
  import { motion } from "motion/react";
@@ -13754,8 +14104,8 @@ import { Button } from "@/components/ui/button";
13754
14104
  import { useControllableState } from "@/lib/use-controllable-state";
13755
14105
  import { cn, spring } from "@/lib/utils";
13756
14106
 
13757
- type StepInfo = { value: string; title: string; description?: string };
13758
- type Guard = () => boolean | undefined | Promise<boolean | undefined>;
14107
+ export type StepInfo = { value: string; title: string; description?: string };
14108
+ export type StepGuard = () => boolean | undefined | Promise<boolean | undefined>;
13759
14109
 
13760
14110
  type StepperCtx = {
13761
14111
  steps: StepInfo[];
@@ -13764,7 +14114,7 @@ type StepperCtx = {
13764
14114
  next: () => Promise<void>;
13765
14115
  back: () => void;
13766
14116
  fieldsets: Map<string, HTMLFieldSetElement>;
13767
- guards: Map<string, Guard>;
14117
+ guards: Map<string, StepGuard>;
13768
14118
  };
13769
14119
 
13770
14120
  const StepperContext = createContext<StepperCtx | null>(null);
@@ -13827,7 +14177,7 @@ export function Stepper({
13827
14177
  steps.findIndex((step) => step.value === value),
13828
14178
  );
13829
14179
  const fieldsets = useRef(new Map<string, HTMLFieldSetElement>()).current;
13830
- const guards = useRef(new Map<string, Guard>()).current;
14180
+ const guards = useRef(new Map<string, StepGuard>()).current;
13831
14181
  const moved = useRef(false);
13832
14182
 
13833
14183
  const go = (i: number) => {
@@ -13940,15 +14290,19 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
13940
14290
  className="flex cursor-pointer items-center gap-2 rounded-full pr-1 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
13941
14291
  >
13942
14292
  {mark}
14293
+
13943
14294
  {label}
14295
+
13944
14296
  <span className="sr-only"> (completed, go back)</span>
13945
14297
  </button>
13946
14298
  ) : (
13947
14299
  <span className="flex items-center gap-2 pr-1">
13948
14300
  {mark}
14301
+
13949
14302
  {label}
13950
14303
  </span>
13951
14304
  )}
14305
+
13952
14306
  {i < steps.length - 1 && (
13953
14307
  <span aria-hidden="true" className="relative mx-2 h-px flex-1 bg-border">
13954
14308
  <motion.span
@@ -13963,6 +14317,7 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
13963
14317
  );
13964
14318
  })}
13965
14319
  </ol>
14320
+
13966
14321
  {/* Small screens hide the titles; this keeps the current one (and announces moves). */}
13967
14322
  <p aria-live="polite" className="text-muted-foreground text-xs sm:sr-only">
13968
14323
  Step {index + 1} of {steps.length}
@@ -13985,7 +14340,7 @@ export function StepperStep({
13985
14340
  /** Matches a step in \`steps\`. */
13986
14341
  value: string;
13987
14342
  /** Runs on Continue after the native checks; return false to stay. */
13988
- onNext?: Guard;
14343
+ onNext?: StepGuard;
13989
14344
  className?: string;
13990
14345
  /** The step's fields. */
13991
14346
  children: ReactNode;
@@ -14021,11 +14376,13 @@ export function StepperStep({
14021
14376
  {step && (
14022
14377
  <legend className="mb-4 grid gap-1">
14023
14378
  <span className="font-semibold text-lg leading-none">{step.title}</span>
14379
+
14024
14380
  {step.description && (
14025
14381
  <span className="text-muted-foreground text-sm">{step.description}</span>
14026
14382
  )}
14027
14383
  </legend>
14028
14384
  )}
14385
+
14029
14386
  {children}
14030
14387
  </motion.fieldset>
14031
14388
  );
@@ -14054,6 +14411,7 @@ export function StepperFooter({
14054
14411
  <Button variant="ghost" onClick={back} className={cn(index === 0 && "invisible")}>
14055
14412
  {backLabel}
14056
14413
  </Button>
14414
+
14057
14415
  {last ? (
14058
14416
  <Button type="submit">{submitLabel}</Button>
14059
14417
  ) : (
@@ -14486,7 +14844,7 @@ export function StepperFooter({
14486
14844
  display: inline !important;
14487
14845
  }
14488
14846
  }
14489
- ` }] }, switch: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", docs: "https://kznlabs.github.io/labsUI/components/forms", usage: '<Label htmlFor="email">Email</Label><Input id="email" type="email" aria-invalid={err} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
14847
+ ` }] }, switch: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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.", 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";
14490
14848
  import { Label } from "@/components/ui/label";
14491
14849
  import { Switch } from "@/components/ui/switch";
14492
14850
  import { Textarea } from "@/components/ui/textarea";
@@ -14547,6 +14905,7 @@ export function Switch({
14547
14905
  className="peer absolute inset-x-0 -inset-y-1 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
14548
14906
  {...props}
14549
14907
  />
14908
+
14550
14909
  <span
14551
14910
  aria-hidden="true"
14552
14911
  className={cn(
@@ -14639,6 +14998,7 @@ export function Table({ striped = false, className, containerClassName, ...props
14639
14998
  )}
14640
14999
  {...props}
14641
15000
  />
15001
+
14642
15002
  <OverflowFocus />
14643
15003
  </div>
14644
15004
  );
@@ -14701,6 +15061,7 @@ export function TableHead({ sort, onSort, className, children, ...props }: Table
14701
15061
  className="group -mx-1.5 inline-flex h-7 items-center gap-1 rounded-md px-1.5 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 in-[.text-right]:flex-row-reverse"
14702
15062
  >
14703
15063
  {children}
15064
+
14704
15065
  <ArrowUp
14705
15066
  aria-hidden="true"
14706
15067
  className={cn(
@@ -14867,6 +15228,7 @@ export function TabsList({ className, children, ...props }: ComponentProps<"div"
14867
15228
  )}
14868
15229
  />
14869
15230
  )}
15231
+
14870
15232
  {children}
14871
15233
  </div>
14872
15234
  );
@@ -14944,7 +15306,7 @@ export function TabsContent({
14944
15306
  </motion.div>
14945
15307
  );
14946
15308
  }
14947
- ` }] }, "tag-input": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. One hidden input per tag.", docs: "https://kznlabs.github.io/labsUI/components/tag-input", usage: "<TagInput value={tags} onValueChange={setTags} max={5} validate={(t) => t.length <= 24} />", examples: [{ name: "clip-tags.tsx", code: `"use client";
15309
+ ` }] }, "tag-input": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "tag-input", type: "registry:ui", title: "Tag Input", description: "Free-form tags: Enter or a separator adds, Backspace removes the last, 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.", docs: "https://kznlabs.github.io/labsUI/components/tag-input", usage: "<TagInput value={tags} onValueChange={setTags} max={5} validate={(t) => t.length <= 24} />", examples: [{ name: "clip-tags.tsx", code: `"use client";
14948
15310
 
14949
15311
  import { useState } from "react";
14950
15312
  import { Field } from "@/components/ui/field";
@@ -14964,12 +15326,13 @@ export function ClipTags() {
14964
15326
  />
14965
15327
  </Field>
14966
15328
  );
14967
- }` }], api: { entries: [{ kind: "component", name: "Tag", props: [{ name: "children", type: "ReactNode", required: true, description: "Tag text." }, { name: "disabled", type: "boolean", required: false, description: "Hides the remove button." }, { name: "onRemove", type: "() => void", required: false, description: 'Shows a remove button; its accessible name is "Remove <children>".' }], description: 'Removable chip. Springs in and out when rendered inside AnimatePresence (`mode="popLayout"`).' }, { kind: "component", name: "TagInput", props: [{ name: "defaultValue", type: "string[]", required: false, default: "[]", description: "Tags at first render (uncontrolled)." }, { name: "max", type: "number", required: false, description: "Most tags allowed; typing stops at the limit." }, { name: "onValueChange", type: "(tags: string[]) => void", required: false, description: "Called with every tag after an add or remove." }, { name: "separators", type: "string[]", required: false, default: '[","]', description: "Keys that commit the typed text, besides Enter. Pasted text is split on these too." }, { name: "validate", type: "(tag: string) => boolean", required: false, description: "Return false to reject a tag (it stays in the input). Runs after trimming." }, { name: "value", type: "string[]", required: false, description: "Controlled tags; pair with `onValueChange`." }], extends: "<input>", description: "Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an\nempty input removes the last tag. `name` submits one hidden input per tag." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/tag-input.tsx", type: "registry:ui", content: `"use client";
15329
+ }` }], api: { entries: [{ kind: "component", name: "Tag", props: [{ name: "children", type: "ReactNode", required: true, description: "Tag text." }, { name: "disabled", type: "boolean", required: false, description: "Hides the remove button." }, { name: "onRemove", type: "() => void", required: false, description: 'Shows a remove button; its accessible name is "Remove <children>".' }], description: 'Removable chip. Springs in and out when rendered inside AnimatePresence (`mode="popLayout"`).' }, { kind: "component", name: "TagInput", props: [{ name: "defaultValue", type: "string[]", required: false, default: "[]", description: "Tags at first render (uncontrolled)." }, { name: "max", type: "number", required: false, description: "Most tags allowed; typing stops at the limit." }, { name: "onValueChange", type: "(tags: string[]) => void", required: false, description: "Called with every tag after an add or remove." }, { name: "separators", type: "string[]", required: false, default: '[","]', description: "Keys that commit the typed text, besides Enter. Pasted text is split on these too." }, { name: "validate", type: "(tag: string) => boolean", required: false, description: "Return false to reject a tag (it stays in the input). Runs after trimming." }, { name: "value", type: "string[]", required: false, description: "Controlled tags; pair with `onValueChange`." }], extends: "<input>", description: "Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an\nempty input removes the last tag. `name` submits one value per tag; `required` needs at least one." }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/field.json", "https://kznlabs.github.io/labsUI/r/form-value.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/tag-input.tsx", type: "registry:ui", content: `"use client";
14968
15330
 
14969
15331
  import { X } from "lucide-react";
14970
15332
  import { AnimatePresence, motion } from "motion/react";
14971
15333
  import { type ComponentProps, type ReactNode, useRef, useState } from "react";
14972
15334
  import { useFieldControl } from "@/components/ui/field";
15335
+ import { FormValue } from "@/lib/form-value";
14973
15336
  import { useControllableState } from "@/lib/use-controllable-state";
14974
15337
  import { cn, spring } from "@/lib/utils";
14975
15338
 
@@ -15006,6 +15369,7 @@ export function Tag({
15006
15369
  )}
15007
15370
  >
15008
15371
  <span className="truncate">{children}</span>
15372
+
15009
15373
  {onRemove && (
15010
15374
  <button
15011
15375
  type="button"
@@ -15045,7 +15409,7 @@ type TagInputProps = Omit<
15045
15409
 
15046
15410
  /**
15047
15411
  * Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an
15048
- * empty input removes the last tag. \`name\` submits one hidden input per tag.
15412
+ * empty input removes the last tag. \`name\` submits one value per tag; \`required\` needs at least one.
15049
15413
  */
15050
15414
  export function TagInput({
15051
15415
  value: valueProp,
@@ -15062,7 +15426,8 @@ export function TagInput({
15062
15426
  onBlur,
15063
15427
  ...rest
15064
15428
  }: TagInputProps) {
15065
- const props = useFieldControl(rest);
15429
+ const { required, ...props } = useFieldControl(rest);
15430
+ const [invalid, setInvalid] = useState(false);
15066
15431
  const input = useRef<HTMLInputElement>(null);
15067
15432
  const [tags, set] = useControllableState(valueProp, defaultValue, onValueChange);
15068
15433
  const [draft, setDraft] = useState("");
@@ -15111,11 +15476,14 @@ export function TagInput({
15111
15476
  </Tag>
15112
15477
  ))}
15113
15478
  </AnimatePresence>
15479
+
15114
15480
  <input
15115
15481
  ref={input}
15116
15482
  type="text"
15117
15483
  data-slot="tag-input"
15118
15484
  {...props}
15485
+ aria-required={required}
15486
+ aria-invalid={props["aria-invalid"] || (invalid && !tags.length) || undefined}
15119
15487
  // readOnly, not disabled: Backspace still has to remove tags at the limit.
15120
15488
  readOnly={full || props.readOnly}
15121
15489
  placeholder={full ? undefined : placeholder}
@@ -15150,7 +15518,19 @@ export function TagInput({
15150
15518
  }}
15151
15519
  className="h-6 min-w-20 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
15152
15520
  />
15153
- {name && tags.map((tag) => <input key={tag} type="hidden" name={name} value={tag} />)}
15521
+
15522
+ {name &&
15523
+ tags.map((tag) => (
15524
+ <input key={tag} type="hidden" name={name} value={tag} disabled={props.disabled} />
15525
+ ))}
15526
+
15527
+ <FormValue
15528
+ value={tags.join(",")}
15529
+ required={required}
15530
+ disabled={props.disabled}
15531
+ anchor={input}
15532
+ onInvalid={() => setInvalid(true)}
15533
+ />
15154
15534
  </div>
15155
15535
  );
15156
15536
  }
@@ -15252,15 +15632,19 @@ function Line({
15252
15632
  {line.time && (
15253
15633
  <span className="mr-3 text-muted-foreground tabular-nums select-none">{line.time}</span>
15254
15634
  )}
15635
+
15255
15636
  {line.kind === "command" && (
15256
15637
  <span className="mr-2 text-muted-foreground select-none">{prompt}</span>
15257
15638
  )}
15639
+
15258
15640
  {kind.glyph && (
15259
15641
  <span aria-hidden="true" className={cn("mr-2", kind.glyphClass)}>
15260
15642
  {kind.glyph}
15261
15643
  </span>
15262
15644
  )}
15645
+
15263
15646
  {text}
15647
+
15264
15648
  {cursor && <Cursor />}
15265
15649
  </div>
15266
15650
  );
@@ -15392,6 +15776,7 @@ export function Terminal({
15392
15776
  <Line key={key(item, i)} line={item} prompt={prompt} />
15393
15777
  ))}
15394
15778
  </div>
15779
+
15395
15780
  <div aria-hidden="true" data-typewriter-animated="">
15396
15781
  {lines
15397
15782
  .slice(0, at.line + 1)
@@ -15408,8 +15793,10 @@ export function Terminal({
15408
15793
  />
15409
15794
  ) : null,
15410
15795
  )}
15796
+
15411
15797
  {done && <Line line={{ kind: "command", text: "" }} prompt={prompt} cursor />}
15412
15798
  </div>
15799
+
15413
15800
  <div aria-hidden="true" data-typewriter-fallback="" className="hidden">
15414
15801
  {lines.map((item, i) => (
15415
15802
  <Line key={key(item, i)} line={item} prompt={prompt} />
@@ -15431,6 +15818,7 @@ export function Terminal({
15431
15818
  lines.map((item, i) => <Line key={key(item, i)} line={item} prompt={prompt} />)
15432
15819
  )}
15433
15820
  </div>
15821
+
15434
15822
  <AnimatePresence>
15435
15823
  {log && behind && (
15436
15824
  <motion.button
@@ -15516,9 +15904,11 @@ export function Testimonial({
15516
15904
  >
15517
15905
  {children}
15518
15906
  </Quote>
15907
+
15519
15908
  {others && others.length > 0 && (
15520
15909
  <div className="flex flex-wrap items-center gap-3 border-t pt-5">
15521
15910
  <AvatarGroup users={others} max={5} size="sm" />
15911
+
15522
15912
  {othersLabel && <p className="text-muted-foreground text-sm">{othersLabel}</p>}
15523
15913
  </div>
15524
15914
  )}
@@ -15600,11 +15990,13 @@ export function TextSwap({
15600
15990
  aria-live={isControlled ? "polite" : undefined}
15601
15991
  >
15602
15992
  <span className="sr-only">{currentItem}</span>
15993
+
15603
15994
  {list.map((item) => (
15604
15995
  <span key={item} aria-hidden="true" className="invisible [grid-area:1/1]">
15605
15996
  {item}
15606
15997
  </span>
15607
15998
  ))}
15999
+
15608
16000
  <AnimatePresence mode="popLayout" initial={false}>
15609
16001
  <motion.span
15610
16002
  key={\`\${activeIndex}-\${currentItem}\`}
@@ -15629,7 +16021,7 @@ export function TextSwap({
15629
16021
  </span>
15630
16022
  );
15631
16023
  }
15632
- ` }] }, 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} />', examples: [{ name: "sign-up.tsx", code: `import { Input } from "@/components/ui/input";
16024
+ ` }] }, 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";
15633
16025
  import { Label } from "@/components/ui/label";
15634
16026
  import { Switch } from "@/components/ui/switch";
15635
16027
  import { Textarea } from "@/components/ui/textarea";
@@ -15697,6 +16089,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
15697
16089
  return (
15698
16090
  <span className="relative block w-full min-w-0">
15699
16091
  {textarea}
16092
+
15700
16093
  <span
15701
16094
  id={countId}
15702
16095
  className={cn(
@@ -15714,7 +16107,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
15714
16107
  </span>
15715
16108
  );
15716
16109
  }
15717
- ` }] }, theme: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", api: { entries: [{ kind: "function", name: "isDark", signature: "isDark() => boolean" }, { kind: "function", name: "setTheme", signature: "setTheme(theme: Theme) => void" }, { kind: "function", name: "subscribeTheme", signature: "subscribeTheme(callback: () => void) => () => void" }, { kind: "function", name: "getAccent", signature: 'getAccent() => "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow"' }, { kind: "function", name: "setAccent", signature: 'setAccent(accent: "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow") => void', description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme." }], types: { Theme: '"light" | "dark"' } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/theme.ts", type: "registry:lib", content: `type Theme = "light" | "dark";
16110
+ ` }] }, theme: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "theme", type: "registry:lib", description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme.", api: { entries: [{ kind: "function", name: "isDark", signature: "isDark() => boolean" }, { kind: "function", name: "setTheme", signature: "setTheme(theme: Theme) => void" }, { kind: "function", name: "subscribeTheme", signature: "subscribeTheme(callback: () => void) => () => void" }, { kind: "function", name: "getAccent", signature: 'getAccent() => "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow"' }, { kind: "function", name: "setAccent", signature: 'setAccent(accent: "blue" | "green" | "orange" | "pink" | "purple" | "red" | "slate" | "yellow") => void', description: "Retints primary, focus rings, selection and switches app-wide. Persists like the theme." }], types: { Theme: '"light" | "dark"' } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/theme.ts", type: "registry:lib", content: `export type Theme = "light" | "dark";
15718
16111
 
15719
16112
  const KEY = "theme";
15720
16113
  const EVENT = "themechange";
@@ -15909,6 +16302,7 @@ export function TimelineItem({
15909
16302
  aria-hidden="true"
15910
16303
  className="absolute top-7 bottom-1 left-3 w-px -translate-x-1/2 bg-border group-last/item:hidden"
15911
16304
  />
16305
+
15912
16306
  <span
15913
16307
  aria-hidden="true"
15914
16308
  className={cn(
@@ -15920,6 +16314,7 @@ export function TimelineItem({
15920
16314
  >
15921
16315
  {icon}
15922
16316
  </span>
16317
+
15923
16318
  <div className="grid min-w-0 gap-1 pt-0.5">
15924
16319
  {time &&
15925
16320
  (dateTime ? (
@@ -15929,7 +16324,9 @@ export function TimelineItem({
15929
16324
  ) : (
15930
16325
  <span className="text-muted-foreground text-xs tabular-nums">{time}</span>
15931
16326
  ))}
16327
+
15932
16328
  <p className="font-medium text-sm leading-snug">{title}</p>
16329
+
15933
16330
  {children && (
15934
16331
  <div className="text-muted-foreground text-sm leading-relaxed">{children}</div>
15935
16332
  )}
@@ -15938,12 +16335,12 @@ export function TimelineItem({
15938
16335
  );
15939
16336
  }
15940
16337
  ` }] }, 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.", 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";
15941
- export function notifySaved() {
16338
+ export function notifySaved(undo: () => void) {
15942
16339
  toast.success("Changes saved", {
15943
16340
  description: "All changes synced",
15944
16341
  action: { label: "Undo", onClick: undo },
15945
16342
  });
15946
- }` }], 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." }], types: { ToastData: "{ id: number; title: ReactNode; description?: ReactNode; variant: Variant; duration: number; action?: { label: string; onClick: () => void }; }" } }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/toast.tsx", type: "registry:ui", content: `"use client";
16343
+ }` }], 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." }], 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/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/toast.tsx", type: "registry:ui", content: `"use client";
15947
16344
 
15948
16345
  import { CircleAlert, CircleCheck, Info, Loader2, X } from "lucide-react";
15949
16346
  import { AnimatePresence, animate, motion, type PanInfo, useMotionValue } from "motion/react";
@@ -15960,12 +16357,12 @@ import { cn, material, spring } from "@/lib/utils";
15960
16357
 
15961
16358
  /* ---------- store (module-level, no provider needed) ---------- */
15962
16359
 
15963
- type Variant = "default" | "success" | "error" | "info" | "loading";
15964
- type ToastData = {
16360
+ export type ToastVariant = "default" | "success" | "error" | "info" | "loading";
16361
+ export type ToastData = {
15965
16362
  id: number;
15966
16363
  title: ReactNode;
15967
16364
  description?: ReactNode;
15968
- variant: Variant;
16365
+ variant: ToastVariant;
15969
16366
  /** ms; Infinity = sticky. */
15970
16367
  duration: number;
15971
16368
  action?: { label: string; onClick: () => void };
@@ -16235,13 +16632,16 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16235
16632
  className="flex items-start gap-3 p-4"
16236
16633
  >
16237
16634
  {icons[data.variant] && <span className="mt-0.5">{icons[data.variant]}</span>}
16635
+
16238
16636
  <div
16239
16637
  className="flex min-w-0 flex-1 flex-col gap-0.5"
16240
16638
  role={data.variant === "error" ? "alert" : undefined}
16241
16639
  >
16242
16640
  <p className="font-medium text-sm">{data.title}</p>
16641
+
16243
16642
  {data.description && <p className="text-muted-foreground text-sm">{data.description}</p>}
16244
16643
  </div>
16644
+
16245
16645
  {data.action && (
16246
16646
  <button
16247
16647
  type="button"
@@ -16254,12 +16654,14 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16254
16654
  {data.action.label}
16255
16655
  </button>
16256
16656
  )}
16657
+
16257
16658
  <button
16258
16659
  type="button"
16259
16660
  onClick={() => dismiss(data.id)}
16260
16661
  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"
16261
16662
  >
16262
16663
  <X className="size-3.5" aria-hidden="true" />
16664
+
16263
16665
  <span className="sr-only">Dismiss</span>
16264
16666
  </button>
16265
16667
  </motion.div>
@@ -16405,6 +16807,7 @@ export function ToggleGroup(props: ToggleGroupProps) {
16405
16807
  className="-z-10 absolute inset-y-0.5 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
16406
16808
  />
16407
16809
  )}
16810
+
16408
16811
  {children}
16409
16812
  </div>
16410
16813
  </GroupContext>
@@ -16609,9 +17012,11 @@ export function Tooltip({
16609
17012
  {...touchHandlers}
16610
17013
  >
16611
17014
  {children}
17015
+
16612
17016
  <span id={id} hidden>
16613
17017
  {content}
16614
17018
  </span>
17019
+
16615
17020
  {mounted &&
16616
17021
  createPortal(
16617
17022
  <AnimatePresence>
@@ -16784,12 +17189,15 @@ export function Typewriter({
16784
17189
  return (
16785
17190
  <span className={cn("inline-flex items-baseline", className)}>
16786
17191
  <span className="sr-only">{fullText}</span>
17192
+
16787
17193
  <span aria-hidden="true" data-typewriter-animated="">
16788
17194
  {displayed}
16789
17195
  </span>
17196
+
16790
17197
  <span aria-hidden="true" data-typewriter-fallback="" className="hidden">
16791
17198
  {phrases[0]}
16792
17199
  </span>
17200
+
16793
17201
  {cursor && (
16794
17202
  <span
16795
17203
  aria-hidden="true"
@@ -16808,24 +17216,24 @@ export function Typewriter({
16808
17216
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
16809
17217
  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 }?: {
16810
17218
  align?: "center" | "end" | "start" | undefined;
16811
- side?: Side | undefined;
17219
+ side?: AnchorSide | undefined;
16812
17220
  offset?: number | undefined;
16813
- }) => Placement`, description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
17221
+ }) => AnchorPlacement`, description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
16814
17222
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
16815
- Measures before paint, so nothing jumps.` }], types: { Side: '"bottom" | "top"', Placement: "{ top: number; left: number; width: number; side: Side; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "lib/use-anchor.ts", type: "registry:hook", content: `"use client";
17223
+ Measures before paint, so nothing jumps.` }], types: { AnchorSide: '"bottom" | "top"', AnchorPlacement: "{ top: number; left: number; width: number; side: AnchorSide; }" } }, dependencies: [], registryDependencies: ["https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "lib/use-anchor.ts", type: "registry:hook", content: `"use client";
16816
17224
 
16817
17225
  import { type RefObject, useLayoutEffect, useState } from "react";
16818
17226
  import { clamp } from "@/lib/utils";
16819
17227
 
16820
- type Side = "bottom" | "top";
17228
+ export type AnchorSide = "bottom" | "top";
16821
17229
 
16822
- type Placement = {
17230
+ export type AnchorPlacement = {
16823
17231
  top: number;
16824
17232
  left: number;
16825
17233
  /** Anchor width, for \`min-width\` matching (Select). */
16826
17234
  width: number;
16827
17235
  /** Side actually used: the preferred one, or the other for lack of room. */
16828
- side: Side;
17236
+ side: AnchorSide;
16829
17237
  };
16830
17238
 
16831
17239
  const MARGIN = 8;
@@ -16843,9 +17251,9 @@ export function useAnchor(
16843
17251
  align = "start",
16844
17252
  side: preferred = "bottom",
16845
17253
  offset = 6,
16846
- }: { align?: "start" | "center" | "end"; side?: Side; offset?: number } = {},
17254
+ }: { align?: "start" | "center" | "end"; side?: AnchorSide; offset?: number } = {},
16847
17255
  ) {
16848
- const [pos, setPos] = useState<Placement>({
17256
+ const [pos, setPos] = useState<AnchorPlacement>({
16849
17257
  top: -9999,
16850
17258
  left: -9999,
16851
17259
  width: 0,
@@ -16868,7 +17276,7 @@ export function useAnchor(
16868
17276
  const above = anchorRect.top - offset - height;
16869
17277
  const fitsBelow = below + height <= innerHeight - MARGIN;
16870
17278
  const fitsAbove = above >= MARGIN;
16871
- const side: Side =
17279
+ const side: AnchorSide =
16872
17280
  preferred === "bottom"
16873
17281
  ? fitsBelow || !fitsAbove
16874
17282
  ? "bottom"
@@ -16955,17 +17363,17 @@ export function useDisclosure(defaultOpen = false) {
16955
17363
  props: { open, onOpenChange: setOpen },
16956
17364
  };
16957
17365
  }
16958
- ` }] }, "use-dismiss": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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`.", api: { entries: [{ kind: "function", name: "isWithin", signature: "isWithin(refs: Refs, target: Node | null) => boolean", 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`." }, { kind: "function", name: "useDismiss", signature: "useDismiss(open: boolean, refs: Refs, onDismiss: () => void) => void", description: "Closes a floating panel on a pointerdown outside it, its trigger and its floating children." }], types: { Refs: "readonly RefObject<HTMLElement | null>[]" } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/use-dismiss.ts", type: "registry:hook", content: `"use client";
17366
+ ` }] }, "use-dismiss": { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", 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`.", api: { entries: [{ kind: "function", name: "isWithin", signature: "isWithin(refs: DismissRefs, target: Node | null) => boolean", 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`." }, { kind: "function", name: "useDismiss", signature: "useDismiss(open: boolean, refs: DismissRefs, onDismiss: () => void) => void", description: "Closes a floating panel on a pointerdown outside it, its trigger and its floating children." }], types: { DismissRefs: "readonly RefObject<HTMLElement | null>[]" } }, dependencies: [], registryDependencies: [], files: [{ path: "lib/use-dismiss.ts", type: "registry:hook", content: `"use client";
16959
17367
 
16960
17368
  import { type RefObject, useEffect, useEffectEvent } from "react";
16961
17369
 
16962
- type Refs = readonly RefObject<HTMLElement | null>[];
17370
+ export type DismissRefs = readonly RefObject<HTMLElement | null>[];
16963
17371
 
16964
17372
  /**
16965
17373
  * Inside one of \`refs\` (panel first, then its trigger), or inside a floating child opened from
16966
17374
  * the panel: a Select list or menu portals out, but it is marked \`data-floating\`.
16967
17375
  */
16968
- export function isWithin(refs: Refs, target: Node | null) {
17376
+ export function isWithin(refs: DismissRefs, target: Node | null) {
16969
17377
  if (!target) return false;
16970
17378
  if (refs.some((ref) => ref.current?.contains(target))) return true;
16971
17379
 
@@ -16975,7 +17383,7 @@ export function isWithin(refs: Refs, target: Node | null) {
16975
17383
  }
16976
17384
 
16977
17385
  /** Closes a floating panel on a pointerdown outside it, its trigger and its floating children. */
16978
- export function useDismiss(open: boolean, refs: Refs, onDismiss: () => void) {
17386
+ export function useDismiss(open: boolean, refs: DismissRefs, onDismiss: () => void) {
16979
17387
  // Effect events, an inline refs array or callback can't re-subscribe every render.
16980
17388
  const outside = useEffectEvent((event: PointerEvent) => {
16981
17389
  if (!isWithin(refs, event.target as Node)) onDismiss();
@@ -17009,7 +17417,7 @@ export function matchesHotkey(
17009
17417
  return mod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;
17010
17418
  }
17011
17419
 
17012
- type HotkeyOptions = {
17420
+ export type HotkeyOptions = {
17013
17421
  /** Ignore the shortcut while typing in a field. Default: only for single keys. */
17014
17422
  skipInFields?: boolean;
17015
17423
  };
@@ -17182,9 +17590,9 @@ export const spring = { type: "spring", stiffness: 400, damping: 30 } as const;
17182
17590
  export const clamp = (value: number, min: number, max: number) =>
17183
17591
  Math.min(Math.max(value, min), max);
17184
17592
  ` }] } },
17185
- 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", "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", "button", "check", "children", "click", "confirm", "confirmlabel", "confirmtimeout", "default", "defaults", "destructive", "disables", "done", "emphasis", "esc", "feedback", "fill", "first", "for", "give", "height", "icon", "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", "step", "success", "successlabel", "swap", "swaps", "text", "the", "this", "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", "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 through hidden inputs.", exports: ["Combobox"], keywords: ["accessible", "and", "aria", "backspace", "blocks", "called", "can", "choices", "closes", "controlled", "defaultvalue", "describedby", "disabled", "down", "emptytext", "enter", "error", "esc", "every", "field", "fills", "filtered", "filters", "first", "form", "from", "hidden", "hint", "ids", "input", "inputs", "into", "invalid", "keeps", "label", "last", "list", "matches", "move", "multiple", "name", "needs", "nothing", "one", "onvaluechange", "open", "opening", "options", "pair", "per", "pick", "picked", "picks", "placeholder", "removable", "removes", "render", "required", "select", "sets", "show", "shown", "shows", "single", "stay", "styling", "submit", "submits", "tags", "text", "the", "there", "through", "type", "types", "typing", "uncontrolled", "user", "value", "values", "visible", "when", "with", "you"], registryDependencies: ["field", "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 through a hidden input. 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", "hidden", "hint", "htmlfor", "ids", "input", "invalid", "isdisabled", "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", "sunday", "text", "that", "the", "then", "there", "these", "through", "today", "uncontrolled", "value", "visible", "weekends", "weekstartson", "when", "with"], registryDependencies: ["calendar", "field", "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", "button", "called", "check", "children", "click", "close", "confirm", "confirmlabel", "confirmtimeout", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialog", "dialogclose", "dialogcontent", "dialogs", "dialogtrigger", "disables", "does", "done", "drawer", "emphasis", "esc", "feedback", "field", "fill", "first", "focus", "footer", "for", "give", "header", "height", "icon", "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", "step", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "this", "topmost", "trap", "trigger", "two", "uncontrolled", "variant", "view", "where", "while", "width", "with"], registryDependencies: ["button", "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", "bottom", "button", "called", "can", "check", "children", "click", "close", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialogclose", "dialogcontent", "dialogtrigger", "disables", "dismiss", "done", "drag", "drawer", "drawers", "edge", "emphasis", "esc", "feedback", "fill", "first", "footer", "for", "from", "give", "handle", "header", "height", "icon", "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", "step", "success", "successlabel", "swap", "swaps", "swipe", "taller", "text", "than", "that", "the", "their", "this", "top", "toward", "trigger", "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", "but", "button", "called", "check", "checked", "children", "choice", "click", "closes", "closing", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disabled", "disables", "done", "down", "dropdownmenuradioitem", "edge", "emphasis", "enter", "equals", "esc", "feedback", "fill", "first", "for", "give", "group", "groups", "height", "hint", "icon", "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", "step", "success", "successlabel", "swap", "swaps", "text", "the", "then", "this", "toggles", "trigger", "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", "Field"], keywords: ["and", "aria", "automatically", "beside", "checkbox", "children", "control", "controls", "describedby", "description", "dims", "disabled", "disables", "enclosing", "error", "explicit", "field", "fills", "first", "for", "form", "from", "hint", "horizontal", "ids", "invalid", "its", "label", "makes", "marks", "match", "must", "one", "orientation", "out", "outside", "own", "picks", "props", "renders", "required", "returns", "shown", "springs", "state", "switch", "the", "unchanged", "under", "unit", "use", "when", "win", "wired", "with"], registryDependencies: ["label", "utils"] }, { 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", "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: ["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: ["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
17593
+ 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", "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", "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", "there", "type", "types", "typing", "uncontrolled", "user", "value", "values", "visible", "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", "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", "Field"], keywords: ["and", "aria", "automatically", "beside", "checkbox", "children", "control", "controls", "describedby", "description", "dims", "disabled", "disables", "enclosing", "error", "explicit", "field", "fills", "first", "for", "form", "from", "hint", "horizontal", "ids", "invalid", "its", "label", "makes", "marks", "match", "must", "one", "orientation", "out", "outside", "own", "picks", "props", "renders", "required", "returns", "shown", "springs", "state", "switch", "the", "unchanged", "under", "unit", "use", "when", "win", "wired", "with"], registryDependencies: ["label", "utils"] }, { 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"], keywords: ["anchor", "and", "blocks", "browser", "called", "can", "control", "custom", "disabled", "empty", "field", "finds", "focus", "focuses", "form", "gets", "hidden", "input", "invalid", "leaves", "like", "message", "name", "native", "nothing", "oninvalid", "only", "out", "part", "picked", "required", "shows", "skips", "submit", "submits", "takes", "that", "the", "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", "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: ["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: ["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
17186
17594
  (tabIndex 0) so arrow keys can scroll it, and it leaves the tab order again once it fits.
17187
- 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: ["action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "button", "called", "check", "children", "click", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disables", "done", "edits", "emphasis", "end", "esc", "feedback", "fill", "filters", "first", "flips", "focus", "for", "give", "height", "icon", "irreversible", "its", "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", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "step", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "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", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "native", "not", "omitted", "one", "onvaluechange", "option", "orientation", "pair", "picked", "radios", "render", "rendered", "row", "selectable", "shown", "stack", "submit", "submitted", "the", "them", "uncontrolled", "under", "value", "visible", "when", "with", "work"], registryDependencies: ["field", "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", "arrow", "called", "chosen", "controlled", "defaultvalue", "display", "field", "filled", "first", "for", "form", "forms", "fractions", "group", "hidden", "hover", "image", "included", "inputs", "interactive", "keys", "label", "lift", "max", "name", "native", "number", "only", "onvaluechange", "pair", "partially", "picked", "press", "preview", "radios", "rating", "readers", "readonly", "render", "renders", "score", "screen", "size", "spring", "star", "stars", "the", "uncontrolled", "value", "visually", "with"], registryDependencies: ["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 via a hidden input.", 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", "form", "from", "groups", "hidden", "hint", "home", "htmlfor", "ids", "input", "invalid", "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", "submits", "text", "the", "there", "through", "trigger", "typeahead", "uncontrolled", "value", "visible", "when", "while", "with"], registryDependencies: ["field", "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", "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", "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", "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", "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, pasting a list adds them all. Duplicates, a max and a validate function keep bad tags in the input. One hidden input per tag.", exports: ["tagFieldClass", "Tag", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "called", "children", "chip", "commit", "controlled", "defaultvalue", "disabled", "empty", "enter", "every", "false", "first", "form", "free", "hidden", "hides", "input", "inside", "its", "keys", "last", "limit", "list", "many", "max", "mode", "most", "name", "one", "onremove", "onvaluechange", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "reject", "removable", "remove", "removes", "render", "rendered", "return", "runs", "separator", "separators", "shows", "split", "springs", "stays", "stops", "submits", "tag", "tags", "text", "that", "the", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "validate", "value", "when", "with"], registryDependencies: ["field", "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", "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.", exports: ["toast", "Toaster"], keywords: ["action", "description", "dismiss", "dismisses", "duration", "error", "expands", "focus", "hover", "layout", "loading", "mount", "once", "promise", "root", "saved", "stacked", "success", "swipe", "tap", "title", "toast", "toasts"], registryDependencies: ["use-dismiss", "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,
17595
+ 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: ["action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "both", "button", "called", "can", "check", "children", "click", "clicks", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "done", "edits", "emphasis", "end", "enter", "esc", "feedback", "fill", "filters", "first", "flips", "focus", "focusable", "for", "form", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "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", "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", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "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", "entered", "expected", "field", "fieldset", "first", "form", "format", "generated", "group", "indicates", "invalid", "item", "items", "its", "keys", "label", "labels", "lay", "legend", "line", "muted", "name", "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", "work"], registryDependencies: ["field", "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", "defaultvalue", "describedby", "disabled", "display", "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", "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", "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", "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", "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, 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", "TagInput"], keywords: ["accessible", "add", "after", "allowed", "and", "animatepresence", "backspace", "besides", "button", "called", "children", "chip", "commit", "controlled", "defaultvalue", "disabled", "empty", "enter", "every", "false", "first", "form", "free", "hides", "input", "inside", "its", "keys", "last", "least", "limit", "list", "many", "max", "mode", "most", "name", "needs", "one", "onremove", "onvaluechange", "out", "pair", "paste", "pasted", "per", "poplayout", "press", "reject", "removable", "remove", "removes", "render", "rendered", "required", "return", "runs", "separator", "separators", "shows", "split", "springs", "stays", "stops", "submits", "tag", "tags", "text", "that", "the", "these", "too", "trimming", "type", "typed", "typing", "uncontrolled", "validate", "value", "when", "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", "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.", exports: ["toast", "Toaster"], keywords: ["action", "description", "dismiss", "dismisses", "duration", "error", "expands", "focus", "hover", "layout", "loading", "mount", "once", "promise", "root", "saved", "stacked", "success", "swipe", "tap", "title", "toast", "toasts"], registryDependencies: ["use-dismiss", "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,
17188
17596
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
17189
17597
  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,
17190
17598
  ToggleGroup). Re-measures when the item or its row resizes (font swap, wrap), or the pill
@@ -17284,7 +17692,7 @@ async function fetchAllItems(source) {
17284
17692
  // jsr.json
17285
17693
  var jsr_default = {
17286
17694
  name: "@kznlabs/labsui",
17287
- version: "0.1.5",
17695
+ version: "0.1.7",
17288
17696
  license: "MIT",
17289
17697
  exports: "./src/index.ts",
17290
17698
  publish: {
@@ -18129,6 +18537,9 @@ function printView(views) {
18129
18537
  console.log(md.replace(/^(#{2,3} .*)$/gm, (line) => bold(line)));
18130
18538
  }
18131
18539
 
18540
+ // src/mcp.ts
18541
+ import { fileURLToPath } from "node:url";
18542
+
18132
18543
  // src/rules.ts
18133
18544
  var rules = (alias = "@/") => `# labsUI
18134
18545
 
@@ -18417,6 +18828,26 @@ function serve(ctx) {
18417
18828
  `);
18418
18829
  let queue = Promise.resolve();
18419
18830
  let buffer = "";
18831
+ let usesRoots = false;
18832
+ let sent = 0;
18833
+ const waiting = new Map;
18834
+ const ask = (method) => new Promise((resolve) => {
18835
+ const id = `labsui-${++sent}`;
18836
+ const settle = (result) => {
18837
+ clearTimeout(timer);
18838
+ waiting.delete(id);
18839
+ resolve(result);
18840
+ };
18841
+ const timer = setTimeout(() => settle(undefined), 5000);
18842
+ waiting.set(id, settle);
18843
+ write({ jsonrpc: "2.0", id, method });
18844
+ });
18845
+ const readRoots = async () => {
18846
+ const roots = (await ask("roots/list"))?.roots;
18847
+ const uri = roots?.find((root) => root.uri?.startsWith("file:"))?.uri;
18848
+ if (uri)
18849
+ ctx.cwd = fileURLToPath(uri);
18850
+ };
18420
18851
  const onLine = (line) => {
18421
18852
  let msg;
18422
18853
  try {
@@ -18424,6 +18855,15 @@ function serve(ctx) {
18424
18855
  } catch {
18425
18856
  return write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
18426
18857
  }
18858
+ if (!Array.isArray(msg) && msg.method === undefined)
18859
+ return waiting.get(String(msg.id))?.(msg.result);
18860
+ if (!Array.isArray(msg) && !ctx.pinned) {
18861
+ if (msg.method === "initialize")
18862
+ usesRoots = Boolean(msg.params?.capabilities?.roots);
18863
+ const changed = msg.method === "notifications/initialized" || msg.method === "notifications/roots/list_changed";
18864
+ if (usesRoots && changed)
18865
+ queue = queue.then(readRoots);
18866
+ }
18427
18867
  queue = queue.then(async () => {
18428
18868
  try {
18429
18869
  if (Array.isArray(msg)) {
@@ -18564,7 +19004,7 @@ async function main() {
18564
19004
  case "rules":
18565
19005
  return console.log(rules(readConfig(cwd)?.alias));
18566
19006
  case "mcp":
18567
- return serve({ cwd, registry, version: VERSION });
19007
+ return serve({ cwd, pinned: values.cwd !== undefined, registry, version: VERSION });
18568
19008
  default:
18569
19009
  throw new CliError(`Unknown command "${command}". Run labsui --help.`);
18570
19010
  }