@kznlabs/labsui 0.1.6 → 0.1.8

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 (2) hide show
  1. package/dist/index.js +533 -176
  2. 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}
@@ -1475,9 +1504,9 @@ 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"> &
@@ -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/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";
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";
@@ -4447,11 +4525,11 @@ type BaseProps = {
4447
4525
  placeholder?: string;
4448
4526
  /** Shown in the list when nothing matches. */
4449
4527
  emptyText?: ReactNode;
4450
- /** Submits the value(s) through hidden inputs (one per value when multiple). */
4528
+ /** Form field name: one entry per picked value. */
4451
4529
  name?: string;
4452
4530
  /** Id of the input; Field fills it in. */
4453
4531
  id?: string;
4454
- /** Blocks typing and opening. */
4532
+ /** Blocks typing and opening; the value isn't submitted. */
4455
4533
  disabled?: boolean;
4456
4534
  /** Form submit needs a value. */
4457
4535
  required?: boolean;
@@ -4637,11 +4715,13 @@ export function Combobox(allProps: ComboboxProps) {
4637
4715
  ))}
4638
4716
  </AnimatePresence>
4639
4717
  )}
4718
+
4640
4719
  {!multiple && !open && selected[0]?.icon && (
4641
4720
  <span className="mr-2 flex shrink-0 text-muted-foreground [&_svg]:size-4">
4642
4721
  {selected[0].icon}
4643
4722
  </span>
4644
4723
  )}
4724
+
4645
4725
  <input
4646
4726
  ref={input}
4647
4727
  id={id}
@@ -4668,21 +4748,26 @@ export function Combobox(allProps: ComboboxProps) {
4668
4748
  multiple && "px-1",
4669
4749
  )}
4670
4750
  />
4751
+
4671
4752
  <ChevronsUpDown
4672
4753
  className="ml-1 size-4 shrink-0 self-center text-muted-foreground"
4673
4754
  aria-hidden="true"
4674
4755
  />
4675
4756
  </div>
4757
+
4676
4758
  {name &&
4677
4759
  (multiple ? values : values.slice(0, 1)).map((picked) => (
4678
- <input key={picked} type="hidden" name={name} value={picked} />
4760
+ <input key={picked} type="hidden" name={name} value={picked} disabled={disabled} />
4679
4761
  ))}
4762
+
4680
4763
  <FormValue
4681
4764
  value={values.join(",")}
4682
- required={required && !disabled}
4765
+ required={required}
4766
+ disabled={disabled}
4683
4767
  anchor={input}
4684
4768
  onInvalid={() => setInvalid(true)}
4685
4769
  />
4770
+
4686
4771
  {mounted &&
4687
4772
  createPortal(
4688
4773
  <AnimatePresence>
@@ -4738,7 +4823,9 @@ export function Combobox(allProps: ComboboxProps) {
4738
4823
  {values.includes(option.value) && (
4739
4824
  <Check className="!text-current absolute left-2" aria-hidden="true" />
4740
4825
  )}
4826
+
4741
4827
  {option.icon}
4828
+
4742
4829
  <span className="truncate">{option.label}</span>
4743
4830
  </div>
4744
4831
  ))
@@ -5010,6 +5097,7 @@ export function CommandGroup({
5010
5097
  {heading}
5011
5098
  </div>
5012
5099
  )}
5100
+
5013
5101
  {children}
5014
5102
  </div>
5015
5103
  );
@@ -5123,6 +5211,7 @@ export function CommandDialog({
5123
5211
  )}
5124
5212
  >
5125
5213
  <DialogTitle className="sr-only">{title}</DialogTitle>
5214
+
5126
5215
  <Command close={() => setOpen(false)} className="rounded-none border-0 bg-transparent">
5127
5216
  {children}
5128
5217
  </Command>
@@ -5185,7 +5274,7 @@ import {
5185
5274
  DialogTitle,
5186
5275
  } from "@/components/ui/dialog";
5187
5276
 
5188
- type ConfirmOptions = {
5277
+ export type ConfirmOptions = {
5189
5278
  /** Question in the dialog heading. */
5190
5279
  title: ReactNode;
5191
5280
  /** Consequences, under the title. */
@@ -5249,10 +5338,13 @@ export function Confirmer() {
5249
5338
  <DialogContent alert className="max-w-md">
5250
5339
  <DialogHeader>
5251
5340
  <DialogTitle>{title}</DialogTitle>
5341
+
5252
5342
  {description && <DialogDescription>{description}</DialogDescription>}
5253
5343
  </DialogHeader>
5344
+
5254
5345
  <DialogFooter>
5255
5346
  <DialogClose>{cancelLabel}</DialogClose>
5347
+
5256
5348
  <Button variant={destructive ? "destructive" : "default"} onClick={() => settle(true)}>
5257
5349
  {options.confirmLabel ?? (destructive ? "Delete" : "Continue")}
5258
5350
  </Button>
@@ -5505,9 +5597,11 @@ export function CountTo({
5505
5597
  return (
5506
5598
  <span ref={ref} className={className}>
5507
5599
  <span className="sr-only">{target}</span>
5600
+
5508
5601
  <span ref={out} aria-hidden="true" data-count-to-animated="">
5509
5602
  {text(from)}
5510
5603
  </span>
5604
+
5511
5605
  <span aria-hidden="true" data-count-to-fallback="" className="hidden">
5512
5606
  {target}
5513
5607
  </span>
@@ -5616,6 +5710,7 @@ export function CustomCursor() {
5616
5710
  }}
5617
5711
  transition={spring}
5618
5712
  />
5713
+
5619
5714
  <motion.div
5620
5715
  className="absolute top-0 left-0 size-1.5 rounded-full bg-foreground"
5621
5716
  style={{ x, y, translateX: "-50%", translateY: "-50%" }}
@@ -5678,7 +5773,7 @@ export function MembersDataTable({
5678
5773
  )}
5679
5774
  />
5680
5775
  );
5681
- }` }], 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";
5682
5777
 
5683
5778
  import { MoreHorizontal, SearchX } from "lucide-react";
5684
5779
  import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
@@ -5703,7 +5798,7 @@ import {
5703
5798
  import { useControllableState } from "@/lib/use-controllable-state";
5704
5799
  import { cn } from "@/lib/utils";
5705
5800
 
5706
- type Sort = { id: string; dir: "ascending" | "descending" };
5801
+ export type DataTableSort = { id: string; dir: "ascending" | "descending" };
5707
5802
 
5708
5803
  export type DataTableColumn<T> = {
5709
5804
  /** Unique key; also used by \`defaultSort\`. */
@@ -5732,7 +5827,7 @@ type DataTableProps<T> = {
5732
5827
  /** Rows per page. */
5733
5828
  pageSize?: number;
5734
5829
  /** Sort at first render. */
5735
- defaultSort?: Sort;
5830
+ defaultSort?: DataTableSort;
5736
5831
  /** Show a search field that filters every column. */
5737
5832
  searchable?: boolean;
5738
5833
  /** Search field placeholder. */
@@ -5861,6 +5956,7 @@ export function DataTable<T>({
5861
5956
  className="w-full sm:max-w-64"
5862
5957
  />
5863
5958
  )}
5959
+
5864
5960
  {toolbar && (
5865
5961
  <div className="ml-auto flex items-center gap-2">
5866
5962
  {toolbar(data.filter((row) => selected.includes(getRowId(row))))}
@@ -5890,6 +5986,7 @@ export function DataTable<T>({
5890
5986
  />
5891
5987
  </TableHead>
5892
5988
  )}
5989
+
5893
5990
  {columns.map((column) => (
5894
5991
  <TableHead
5895
5992
  key={column.id}
@@ -5913,6 +6010,7 @@ export function DataTable<T>({
5913
6010
  {column.header}
5914
6011
  </TableHead>
5915
6012
  ))}
6013
+
5916
6014
  {rowActions && (
5917
6015
  <TableHead className="w-px">
5918
6016
  <span className="sr-only">Actions</span>
@@ -5920,6 +6018,7 @@ export function DataTable<T>({
5920
6018
  )}
5921
6019
  </TableRow>
5922
6020
  </TableHeader>
6021
+
5923
6022
  <TableBody>
5924
6023
  {rows.length === 0 ? (
5925
6024
  <TableRow className="hover:bg-transparent">
@@ -5955,11 +6054,13 @@ export function DataTable<T>({
5955
6054
  />
5956
6055
  </TableCell>
5957
6056
  )}
6057
+
5958
6058
  {columns.map((column) => (
5959
6059
  <TableCell key={column.id} className={column.className}>
5960
6060
  {column.cell ? column.cell(row) : column.accessor?.(row)}
5961
6061
  </TableCell>
5962
6062
  ))}
6063
+
5963
6064
  {rowActions && (
5964
6065
  <TableCell className="py-1 pr-2">
5965
6066
  <DropdownMenu>
@@ -5971,6 +6072,7 @@ export function DataTable<T>({
5971
6072
  >
5972
6073
  <MoreHorizontal />
5973
6074
  </DropdownMenuTrigger>
6075
+
5974
6076
  <DropdownMenuContent align="end">{rowActions(row)}</DropdownMenuContent>
5975
6077
  </DropdownMenu>
5976
6078
  </TableCell>
@@ -5987,10 +6089,12 @@ export function DataTable<T>({
5987
6089
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
5988
6090
  <p className="text-muted-foreground text-xs tabular-nums" aria-live="polite">
5989
6091
  {selectable && selected.length > 0 && \`\${selected.length} selected · \`}
6092
+
5990
6093
  {sorted.length === 0
5991
6094
  ? "0 results"
5992
6095
  : \`\${from}–\${from + rows.length - 1} of \${sorted.length}\`}
5993
6096
  </p>
6097
+
5994
6098
  {pages > 1 && (
5995
6099
  <Pagination
5996
6100
  page={page}
@@ -6003,7 +6107,7 @@ export function DataTable<T>({
6003
6107
  </div>
6004
6108
  );
6005
6109
  }
6006
- ` }] }, "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";
6007
6111
 
6008
6112
  import { DatePicker } from "@/components/ui/date-picker";
6009
6113
  import { Field } from "@/components/ui/field";
@@ -6026,8 +6130,8 @@ export function ReportFilters() {
6026
6130
  />
6027
6131
  </Field>
6028
6132
  );
6029
- }` }], 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
6030
- 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/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";
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";
6031
6135
 
6032
6136
  import { CalendarDays } from "lucide-react";
6033
6137
  import { useRef, useState } from "react";
@@ -6039,7 +6143,7 @@ import { FormValue } from "@/lib/form-value";
6039
6143
  import { useControllableState } from "@/lib/use-controllable-state";
6040
6144
  import { cn } from "@/lib/utils";
6041
6145
 
6042
- type Preset<Value> = { label: string; value: Value };
6146
+ export type DatePickerPreset<Value> = { label: string; value: Value };
6043
6147
 
6044
6148
  type Base = {
6045
6149
  /** Button text when nothing is picked. */
@@ -6054,11 +6158,11 @@ type Base = {
6054
6158
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
6055
6159
  /** BCP 47 locale for the button text and the calendar. */
6056
6160
  locale?: string;
6057
- /** 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. */
6058
6162
  name?: string;
6059
6163
  /** Pair with <Label htmlFor>, or let <Field> fill it in. */
6060
6164
  id?: string;
6061
- /** Blocks opening. */
6165
+ /** Blocks opening; the value isn't submitted. */
6062
6166
  disabled?: boolean;
6063
6167
  /** Form submit needs a date. */
6064
6168
  required?: boolean;
@@ -6081,7 +6185,7 @@ type SingleProps = Base & {
6081
6185
  /** Called with the picked day, or with the range once both ends are set. */
6082
6186
  onValueChange?: (date: Date) => void;
6083
6187
  /** Shortcuts beside the grid, e.g. Today, or Last 7 days for a range. Readers reach for these first. */
6084
- presets?: Preset<Date>[];
6188
+ presets?: DatePickerPreset<Date>[];
6085
6189
  };
6086
6190
 
6087
6191
  type RangeProps = Base & {
@@ -6089,7 +6193,7 @@ type RangeProps = Base & {
6089
6193
  value?: DateRange | null;
6090
6194
  defaultValue?: DateRange | null;
6091
6195
  onValueChange?: (range: DateRange) => void;
6092
- presets?: Preset<DateRange>[];
6196
+ presets?: DatePickerPreset<DateRange>[];
6093
6197
  };
6094
6198
 
6095
6199
  type DatePickerProps = SingleProps | RangeProps;
@@ -6168,8 +6272,10 @@ export function DatePicker(allProps: DatePickerProps) {
6168
6272
  aria-invalid={trigger["aria-invalid"] || (invalid && !iso) || undefined}
6169
6273
  >
6170
6274
  <CalendarDays />
6275
+
6171
6276
  <span className="truncate">{label ?? placeholder}</span>
6172
6277
  </PopoverTrigger>
6278
+
6173
6279
  <PopoverContent align="start" className="flex w-auto flex-col gap-3 p-3 sm:flex-row">
6174
6280
  {presets && presets.length > 0 && (
6175
6281
  // No room beside the calendar on phones.
@@ -6193,6 +6299,7 @@ export function DatePicker(allProps: DatePickerProps) {
6193
6299
  ))}
6194
6300
  </ul>
6195
6301
  )}
6302
+
6196
6303
  {range ? (
6197
6304
  <Calendar key={shown} mode="range" {...shared} value={span} onValueChange={commit} />
6198
6305
  ) : (
@@ -6205,10 +6312,12 @@ export function DatePicker(allProps: DatePickerProps) {
6205
6312
  )}
6206
6313
  </PopoverContent>
6207
6314
  </Popover>
6315
+
6208
6316
  <FormValue
6209
6317
  name={name}
6210
6318
  value={iso}
6211
- required={required && !trigger.disabled}
6319
+ required={required}
6320
+ disabled={trigger.disabled}
6212
6321
  anchor={anchor}
6213
6322
  onInvalid={() => setInvalid(true)}
6214
6323
  />
@@ -6240,8 +6349,8 @@ export function EditProfile() {
6240
6349
  </DialogContent>
6241
6350
  </Dialog>
6242
6351
  );
6243
- }` }], 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: "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,
6244
- 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/focus.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";
6245
6354
 
6246
6355
  import { X } from "lucide-react";
6247
6356
  import { AnimatePresence, motion, useIsPresent } from "motion/react";
@@ -6256,14 +6365,20 @@ import {
6256
6365
  useRef,
6257
6366
  } from "react";
6258
6367
  import { createPortal } from "react-dom";
6368
+ import { tabbables } from "@/lib/focus";
6259
6369
  import { useMounted } from "@/lib/platform";
6260
6370
  import { useControllableState } from "@/lib/use-controllable-state";
6261
6371
  import { cn, spring } from "@/lib/utils";
6262
6372
  import { Button } from "./button";
6263
6373
 
6264
- type Ctx = { open: boolean; setOpen: (open: boolean) => void; titleId: string; descId: string };
6374
+ export type DialogState = {
6375
+ open: boolean;
6376
+ setOpen: (open: boolean) => void;
6377
+ titleId: string;
6378
+ descId: string;
6379
+ };
6265
6380
 
6266
- const DialogContext = createContext<Ctx | null>(null);
6381
+ const DialogContext = createContext<DialogState | null>(null);
6267
6382
 
6268
6383
  export function useDialog() {
6269
6384
  const ctx = use(DialogContext);
@@ -6337,9 +6452,6 @@ export function DialogClose({ onClick, variant = "outline", ...props }: ButtonPr
6337
6452
  /** Open dialogs, innermost last. Only the top one handles Esc and Tab. */
6338
6453
  const stack: HTMLElement[] = [];
6339
6454
 
6340
- const FOCUSABLE =
6341
- 'a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
6342
-
6343
6455
  /**
6344
6456
  * Modal behavior shared by Dialog and Drawer: focus first field, trap Tab, restore focus,
6345
6457
  * lock page scroll, close on Esc (topmost modal only). Attach the returned ref to the panel.
@@ -6364,9 +6476,7 @@ export function useModal() {
6364
6476
 
6365
6477
  html.style.overflow = "hidden";
6366
6478
 
6367
- const focusables = () => [...(node?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])];
6368
-
6369
- (focusables()[0] ?? node)?.focus();
6479
+ (tabbables(node)[0] ?? node)?.focus();
6370
6480
 
6371
6481
  const onKey = (event: KeyboardEvent) => {
6372
6482
  if (stack.at(-1) !== node) return;
@@ -6374,7 +6484,7 @@ export function useModal() {
6374
6484
  if (event.key === "Escape") return event.defaultPrevented || close();
6375
6485
  if (event.key !== "Tab") return;
6376
6486
 
6377
- const focusable = focusables();
6487
+ const focusable = tabbables(node);
6378
6488
  const first = focusable[0];
6379
6489
  const last = focusable.at(-1);
6380
6490
 
@@ -6435,6 +6545,7 @@ export function DialogContent({
6435
6545
  transition={spring}
6436
6546
  onClick={alert ? undefined : () => setOpen(false)}
6437
6547
  />
6548
+
6438
6549
  <motion.div
6439
6550
  ref={ref}
6440
6551
  role={alert ? "alertdialog" : "dialog"}
@@ -6452,6 +6563,7 @@ export function DialogContent({
6452
6563
  )}
6453
6564
  >
6454
6565
  {children}
6566
+
6455
6567
  {showClose && (
6456
6568
  <button
6457
6569
  type="button"
@@ -6459,6 +6571,7 @@ export function DialogContent({
6459
6571
  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"
6460
6572
  >
6461
6573
  <X className="size-4" aria-hidden="true" />
6574
+
6462
6575
  <span className="sr-only">Close</span>
6463
6576
  </button>
6464
6577
  )}
@@ -6540,8 +6653,8 @@ export function CartDrawer({ items }: { items: string[] }) {
6540
6653
  </DrawerContent>
6541
6654
  </Drawer>
6542
6655
  );
6543
- }` }], 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: "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
6544
- 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";
6656
+ }` }], 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
6657
+ 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";
6545
6658
 
6546
6659
  import { X } from "lucide-react";
6547
6660
  import { AnimatePresence, motion, type PanInfo, useDragControls, useIsPresent } from "motion/react";
@@ -6567,9 +6680,9 @@ export const DrawerClose = DialogClose;
6567
6680
  export const DrawerTitle = DialogTitle;
6568
6681
  export const DrawerDescription = DialogDescription;
6569
6682
 
6570
- type Side = "left" | "right" | "top" | "bottom";
6683
+ export type DrawerSide = "left" | "right" | "top" | "bottom";
6571
6684
 
6572
- const offscreen: Record<Side, { x?: string; y?: string }> = {
6685
+ const offscreen: Record<DrawerSide, { x?: string; y?: string }> = {
6573
6686
  left: { x: "-100%" },
6574
6687
  right: { x: "100%" },
6575
6688
  top: { y: "-100%" },
@@ -6578,7 +6691,7 @@ const offscreen: Record<Side, { x?: string; y?: string }> = {
6578
6691
 
6579
6692
  // The \`after:\` bleed paints past the screen edge, so overshoot or dragging inward never shows a
6580
6693
  // gap.
6581
- const placement: Record<Side, string> = {
6694
+ const placement: Record<DrawerSide, string> = {
6582
6695
  left: "inset-y-0 left-0 h-full w-[min(20rem,85vw)] border-r after:right-full after:inset-y-0 after:w-[50vw]",
6583
6696
  right:
6584
6697
  "inset-y-0 right-0 h-full w-[min(24rem,90vw)] border-l after:left-full after:inset-y-0 after:w-[50vw]",
@@ -6602,7 +6715,7 @@ export function DrawerContent({
6602
6715
  children,
6603
6716
  }: {
6604
6717
  /** Edge it slides in from. */
6605
- side?: Side;
6718
+ side?: DrawerSide;
6606
6719
  /** Close button in the corner. On by default for side drawers. */
6607
6720
  showClose?: boolean;
6608
6721
  className?: string;
@@ -6651,6 +6764,7 @@ export function DrawerContent({
6651
6764
  transition={spring}
6652
6765
  onClick={() => setOpen(false)}
6653
6766
  />
6767
+
6654
6768
  <motion.div
6655
6769
  ref={ref}
6656
6770
  role="dialog"
@@ -6684,8 +6798,11 @@ export function DrawerContent({
6684
6798
  )}
6685
6799
  >
6686
6800
  {side === "bottom" && <Handle />}
6801
+
6687
6802
  {children}
6803
+
6688
6804
  {side === "top" && <Handle />}
6805
+
6689
6806
  {showClose && (
6690
6807
  <button
6691
6808
  type="button"
@@ -6694,6 +6811,7 @@ export function DrawerContent({
6694
6811
  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"
6695
6812
  >
6696
6813
  <X className="size-4" aria-hidden="true" />
6814
+
6697
6815
  <span className="sr-only">Close</span>
6698
6816
  </button>
6699
6817
  )}
@@ -7082,6 +7200,7 @@ export function DropdownMenuItem({
7082
7200
  )}
7083
7201
  >
7084
7202
  {children}
7203
+
7085
7204
  <Shortcut>{shortcut}</Shortcut>
7086
7205
  </div>
7087
7206
  );
@@ -7112,7 +7231,9 @@ export function DropdownMenuCheckboxItem({
7112
7231
  <span className="absolute left-2 flex size-4 items-center justify-center">
7113
7232
  {checked && <Check className="!text-current" aria-hidden="true" />}
7114
7233
  </span>
7234
+
7115
7235
  {children}
7236
+
7116
7237
  <Shortcut>{shortcut}</Shortcut>
7117
7238
  </div>
7118
7239
  );
@@ -7169,6 +7290,7 @@ export function DropdownMenuRadioItem({
7169
7290
  <span className="absolute left-2 flex size-4 items-center justify-center">
7170
7291
  {checked && <Circle className="!size-2 !text-current fill-current" aria-hidden="true" />}
7171
7292
  </span>
7293
+
7172
7294
  {children}
7173
7295
  </div>
7174
7296
  );
@@ -7240,12 +7362,15 @@ export function Empty({
7240
7362
  {icon}
7241
7363
  </span>
7242
7364
  )}
7365
+
7243
7366
  <div className="grid max-w-sm gap-1.5">
7244
7367
  <p className="font-semibold text-base tracking-tight">{title}</p>
7368
+
7245
7369
  {description && (
7246
7370
  <p className="text-muted-foreground text-sm leading-relaxed">{description}</p>
7247
7371
  )}
7248
7372
  </div>
7373
+
7249
7374
  {children && <div className="flex flex-wrap justify-center gap-2">{children}</div>}
7250
7375
  </div>
7251
7376
  );
@@ -7361,6 +7486,7 @@ export function Field({
7361
7486
  const labelEl = label && (
7362
7487
  <Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
7363
7488
  {label}
7489
+
7364
7490
  {required && (
7365
7491
  <span aria-hidden="true" className="text-destructive-ink">
7366
7492
  *
@@ -7383,14 +7509,17 @@ export function Field({
7383
7509
  {horizontal ? (
7384
7510
  <div className="flex items-center gap-2.5">
7385
7511
  {children}
7512
+
7386
7513
  {labelEl}
7387
7514
  </div>
7388
7515
  ) : (
7389
7516
  <>
7390
7517
  {labelEl}
7518
+
7391
7519
  {children}
7392
7520
  </>
7393
7521
  )}
7522
+
7394
7523
  {description && (
7395
7524
  <p
7396
7525
  id={descId}
@@ -7399,6 +7528,7 @@ export function Field({
7399
7528
  {description}
7400
7529
  </p>
7401
7530
  )}
7531
+
7402
7532
  {/* Persistent live region, so a new error is announced. */}
7403
7533
  {/* -mt-2 cancels the grid gap while empty; the error brings its own pt-2. */}
7404
7534
  <div aria-live="polite" className={cn("-mt-2", horizontal && "pl-6.5")}>
@@ -7422,7 +7552,30 @@ export function Field({
7422
7552
  </FieldContext>
7423
7553
  );
7424
7554
  }
7425
- ` }] }, "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: "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";
7555
+ ` }] }, focus: { $schema: "https://kznlabs.github.io/labsUI/r/registry-item.schema.json", name: "focus", type: "registry:lib", description: "What Tab reaches inside `root`, in order. Skips disabled and hidden fields, tabIndex -1 (a\nFormValue helper) and all but one radio per group, so the first and last are real stops.", api: { entries: [{ kind: "function", name: "tabbables", signature: "tabbables(root: HTMLElement | null) => HTMLElement[]", description: "What Tab reaches inside `root`, in order. Skips disabled and hidden fields, tabIndex -1 (a\nFormValue helper) and all but one radio per group, so the first and last are real stops." }], types: {} }, dependencies: [], registryDependencies: [], files: [{ path: "lib/focus.ts", type: "registry:lib", content: `const CANDIDATES = "a[href],button,input,select,textarea,[tabindex]";
7556
+
7557
+ /**
7558
+ * What Tab reaches inside \`root\`, in order. Skips disabled and hidden fields, tabIndex -1 (a
7559
+ * FormValue helper) and all but one radio per group, so the first and last are real stops.
7560
+ */
7561
+ export function tabbables(root: HTMLElement | null) {
7562
+ const all = [...(root?.querySelectorAll<HTMLElement>(CANDIDATES) ?? [])];
7563
+
7564
+ return all.filter((element) => {
7565
+ if (element.tabIndex < 0 || element.matches(":disabled, [type=hidden]")) return false;
7566
+ if (element.closest("[hidden], [inert]")) return false;
7567
+ if (!(element instanceof HTMLInputElement) || element.type !== "radio" || !element.name)
7568
+ return true;
7569
+
7570
+ const group = all.filter(
7571
+ (other): other is HTMLInputElement =>
7572
+ other instanceof HTMLInputElement && other.type === "radio" && other.name === element.name,
7573
+ );
7574
+
7575
+ return element === (group.find((radio) => radio.checked) ?? group[0]);
7576
+ });
7577
+ }
7578
+ ` }] }, "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";
7426
7579
 
7427
7580
  type FormValueProps = {
7428
7581
  /** Form field name; without it the value only takes part in validation. */
@@ -7431,6 +7584,8 @@ type FormValueProps = {
7431
7584
  value: string;
7432
7585
  /** An empty value blocks submit. */
7433
7586
  required?: boolean;
7587
+ /** Skips validation and leaves the value out of the submit, like a disabled input. */
7588
+ disabled?: boolean;
7434
7589
  /** The visible control that gets focus when the value is invalid. */
7435
7590
  anchor: RefObject<HTMLElement | null>;
7436
7591
  /** Called when a submit finds the value invalid. */
@@ -7441,7 +7596,7 @@ type FormValueProps = {
7441
7596
  * Submits a custom control's value and takes part in native validation, which a hidden input
7442
7597
  * can't: an empty \`required\` value blocks submit, shows the browser's message and focuses \`anchor\`.
7443
7598
  */
7444
- export function FormValue({ name, value, required, anchor, onInvalid }: FormValueProps) {
7599
+ export function FormValue({ name, value, required, disabled, anchor, onInvalid }: FormValueProps) {
7445
7600
  if (!name && !required) return null;
7446
7601
 
7447
7602
  return (
@@ -7451,6 +7606,7 @@ export function FormValue({ name, value, required, anchor, onInvalid }: FormValu
7451
7606
  name={name}
7452
7607
  value={value}
7453
7608
  required={required}
7609
+ disabled={disabled}
7454
7610
  onChange={() => undefined}
7455
7611
  onInvalid={onInvalid}
7456
7612
  onFocus={() => anchor.current?.focus()}
@@ -7619,6 +7775,7 @@ export function HoverCard({
7619
7775
  }}
7620
7776
  >
7621
7777
  {children}
7778
+
7622
7779
  {mounted &&
7623
7780
  createPortal(
7624
7781
  <AnimatePresence>
@@ -7651,7 +7808,7 @@ export function HoverCard({
7651
7808
  </span>
7652
7809
  );
7653
7810
  }
7654
- ` }] }, 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";
7811
+ ` }] }, 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";
7655
7812
  import { Label } from "@/components/ui/label";
7656
7813
  import { Switch } from "@/components/ui/switch";
7657
7814
  import { Textarea } from "@/components/ui/textarea";
@@ -7926,6 +8083,7 @@ export function InputOTP({
7926
8083
  className="h-px w-2.5 shrink-0 bg-muted-foreground/50"
7927
8084
  />
7928
8085
  )}
8086
+
7929
8087
  <div className="flex min-w-0 gap-1.5" aria-hidden="true">
7930
8088
  {Array.from({ length: count }, (_, slot) => {
7931
8089
  const i = starts[group] + slot;
@@ -7980,6 +8138,7 @@ export function InputOTP({
7980
8138
  )
7981
8139
  )}
7982
8140
  </AnimatePresence>
8141
+
7983
8142
  {isActive && !char && (
7984
8143
  <span className="absolute h-4 w-px animate-caret-blink bg-foreground motion-reduce:animate-none" />
7985
8144
  )}
@@ -7989,6 +8148,7 @@ export function InputOTP({
7989
8148
  </div>
7990
8149
  </Fragment>
7991
8150
  ))}
8151
+
7992
8152
  <AnimatePresence>
7993
8153
  {merge && (
7994
8154
  // Slots fold into one bar of the same size: a finished field, not a new widget.
@@ -8007,6 +8167,7 @@ export function InputOTP({
8007
8167
  transition={{ ...spring, stiffness: 120, delay: 0.15 }}
8008
8168
  className="absolute inset-0 rounded-full border-2 border-success"
8009
8169
  />
8170
+
8010
8171
  <motion.span
8011
8172
  initial={{ scale: 0 }}
8012
8173
  animate={{ scale: 1 }}
@@ -8027,6 +8188,7 @@ export function InputOTP({
8027
8188
  </svg>
8028
8189
  </motion.span>
8029
8190
  </span>
8191
+
8030
8192
  <motion.span
8031
8193
  initial={{ opacity: 0, x: -6 }}
8032
8194
  animate={{ opacity: 1, x: 0 }}
@@ -8038,9 +8200,11 @@ export function InputOTP({
8038
8200
  </motion.div>
8039
8201
  )}
8040
8202
  </AnimatePresence>
8203
+
8041
8204
  <span role="status" className="sr-only">
8042
8205
  {verified ? verifiedLabel : null}
8043
8206
  </span>
8207
+
8044
8208
  <input
8045
8209
  type="text"
8046
8210
  autoComplete="one-time-code"
@@ -8117,7 +8281,7 @@ export function KbdGroup({
8117
8281
  </kbd>
8118
8282
  );
8119
8283
  }
8120
- ` }] }, 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";
8284
+ ` }] }, 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";
8121
8285
  import { Label } from "@/components/ui/label";
8122
8286
  import { Switch } from "@/components/ui/switch";
8123
8287
  import { Textarea } from "@/components/ui/textarea";
@@ -8149,7 +8313,7 @@ export function Label({ className, ...props }: ComponentProps<"label">) {
8149
8313
  }
8150
8314
  ` }] }, 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";
8151
8315
 
8152
- type ListKeyOptions = {
8316
+ export type ListKeyOptions = {
8153
8317
  /** Which arrows move: Left/Right for "horizontal", Up/Down for "vertical". */
8154
8318
  orientation: "horizontal" | "vertical";
8155
8319
  /** Wrap past the last item to the first and back. Default true. */
@@ -8485,6 +8649,7 @@ export function Menubar({ className, children, ...props }: ComponentProps<"div">
8485
8649
  className="-z-10 absolute inset-y-1 left-0 rounded-md bg-foreground/[0.08]"
8486
8650
  />
8487
8651
  )}
8652
+
8488
8653
  {children}
8489
8654
  </div>
8490
8655
  </BarContext>
@@ -8586,7 +8751,7 @@ export function MenubarTrigger({
8586
8751
  />
8587
8752
  );
8588
8753
  }
8589
- ` }] }, "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";
8754
+ ` }] }, "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";
8590
8755
  import { Code } from "@/components/ui/code";
8591
8756
  import { MessageBubble } from "@/components/ui/message-bubble";
8592
8757
  export function Reply({ code }: { code: string }) {
@@ -8644,13 +8809,16 @@ export function MessageBubble({
8644
8809
  className={cn("flex w-full gap-3", user && "flex-row-reverse", className)}
8645
8810
  >
8646
8811
  {avatar && <div className="shrink-0 pt-5">{avatar}</div>}
8812
+
8647
8813
  <div className={cn("flex min-w-0 max-w-[85%] flex-col gap-1", user && "items-end")}>
8648
8814
  {(name || time) && (
8649
8815
  <header className="flex items-center gap-2 px-1 text-muted-foreground text-xs">
8650
8816
  {name && <span className="font-medium text-foreground">{name}</span>}
8817
+
8651
8818
  {time && <time>{time}</time>}
8652
8819
  </header>
8653
8820
  )}
8821
+
8654
8822
  <div
8655
8823
  className={cn(
8656
8824
  "min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed",
@@ -8661,6 +8829,7 @@ export function MessageBubble({
8661
8829
  )}
8662
8830
  >
8663
8831
  {children}
8832
+
8664
8833
  {streaming && <TypingDots className={children ? "mt-2" : "py-1.5"} />}
8665
8834
  </div>
8666
8835
  </div>
@@ -8705,14 +8874,14 @@ export function Storage() {
8705
8874
  ]}
8706
8875
  />
8707
8876
  );
8708
- }` }], 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
8877
+ }` }], 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
8709
8878
  task's progress (use Progress). Colors follow <meter>: green in the optimum region, orange one
8710
- 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";
8879
+ 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";
8711
8880
  import { clamp, cn } from "@/lib/utils";
8712
8881
 
8713
8882
  /* Server-safe (no "use client"). */
8714
8883
 
8715
- type Segment = {
8884
+ export type MeterSegment = {
8716
8885
  /** Legend text for this part. */
8717
8886
  label: string;
8718
8887
  /** Amount this part uses. */
@@ -8735,7 +8904,7 @@ type MeterProps = {
8735
8904
  /** Where good is: below low, between, or above high. Decides the color, like <meter>. */
8736
8905
  optimum?: number;
8737
8906
  /** Stacked parts with a legend (like a storage bar). Overrides the threshold color. */
8738
- segments?: Segment[];
8907
+ segments?: MeterSegment[];
8739
8908
  /** Visible name above the bar; also the accessible name. */
8740
8909
  label?: ReactNode;
8741
8910
  /** Text on the right and the accessible value, e.g. "48 GB of 100 GB". */
@@ -8773,11 +8942,13 @@ export function Meter({
8773
8942
  {(label || valueText) && (
8774
8943
  <div className="flex items-baseline justify-between gap-3 text-sm">
8775
8944
  {label && <span className="font-medium">{label}</span>}
8945
+
8776
8946
  {valueText && (
8777
8947
  <span className="ml-auto text-muted-foreground text-xs tabular-nums">{valueText}</span>
8778
8948
  )}
8779
8949
  </div>
8780
8950
  )}
8951
+
8781
8952
  {/* The real <meter> carries the semantics; the drawn bar below is decoration. */}
8782
8953
  <meter
8783
8954
  className="sr-only"
@@ -8791,6 +8962,7 @@ export function Meter({
8791
8962
  >
8792
8963
  {valueText ?? value}
8793
8964
  </meter>
8965
+
8794
8966
  <div
8795
8967
  aria-hidden="true"
8796
8968
  className="flex h-2 w-full gap-px overflow-hidden rounded-full bg-foreground/10"
@@ -8810,6 +8982,7 @@ export function Meter({
8810
8982
  <span style={{ width: pct(value) }} className={cn("h-full rounded-full", tone)} />
8811
8983
  )}
8812
8984
  </div>
8985
+
8813
8986
  {segments && (
8814
8987
  <ul className="flex flex-wrap gap-x-4 gap-y-1 text-muted-foreground text-xs">
8815
8988
  {segments.map((segment, i) => (
@@ -8821,6 +8994,7 @@ export function Meter({
8821
8994
  segment.className ?? palette[i % palette.length],
8822
8995
  )}
8823
8996
  />
8997
+
8824
8998
  {segment.label}
8825
8999
  </li>
8826
9000
  ))}
@@ -8887,6 +9061,7 @@ export function NativeSelect({ className, children, ...rest }: ComponentProps<"s
8887
9061
  >
8888
9062
  {children}
8889
9063
  </select>
9064
+
8890
9065
  <ChevronsUpDown
8891
9066
  aria-hidden="true"
8892
9067
  className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-3 size-4 text-muted-foreground"
@@ -8939,7 +9114,7 @@ export function MainNav({ pathname }: { pathname: string }) {
8939
9114
  </NavigationMenu>
8940
9115
  );
8941
9116
  }` }], api: { entries: [{ kind: "component", name: "NavigationMenu", props: [{ name: "defaultValue", type: "string", required: false, default: '""', description: "Item open at first render (uncontrolled)." }, { name: "onValueChange", type: "(value: string) => void", required: false, description: `Called with the opened item's value, or "".` }, { name: "value", type: "string", required: false, description: 'Controlled open item; "" = all closed.' }], extends: "<nav>", description: 'Top navigation for a website: links and hover/click panels. Hover opens after a beat, then\nsliding along the bar switches panels at once; click and Enter toggle, Down opens into the panel,\nLeft/Right walk the bar, Esc closes. Controlled with `value`/`onValueChange` ("" = closed).' }, { kind: "component", name: "NavigationMenuList", props: [], extends: "<ul>", description: "The row of items. Its padding is the hover pill's inset." }, { kind: "component", name: "NavigationMenuItem", props: [{ name: "value", type: "string | (readonly string[] & string)", required: false, description: "Stable id matched by `value`; one is generated if omitted." }], extends: "<li>", description: "One top-level entry: a Trigger + Content pair, or a single Link." }, { kind: "component", name: "NavigationMenuTrigger", props: [], extends: "<button>", description: "Opens its panel: hover (mouse), click, Enter or Space; Down opens and focuses the first link." }, { kind: "component", name: "NavigationMenuContent", props: [], extends: "<div>", description: "The panel. Size it with `className` (e.g. `w-[32rem]`); the viewport springs to it. While\nclosed, its links stay in the HTML (hidden) so crawlers and no-JS readers still find them." }, { kind: "component", name: "NavigationMenuLink", props: [{ name: "active", type: "boolean", required: false, description: 'Current page: `aria-current="page"` and the selected look.' }, { name: "description", type: "ReactNode", required: false, description: "Muted line under the title." }, { name: "icon", type: "ReactNode", required: false, description: "In a panel: rich row with a tile icon, title and one-line description." }, { name: "title", type: "string", required: false, description: "Bold first line of a panel link." }], extends: "<a>", description: `On the bar: a plain top-level link. In a panel: a row with optional icon, title and
8942
- description (or your own children). Following it closes the menu.` }], 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/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/use-indicator.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/navigation-menu.tsx", type: "registry:ui", content: `"use client";
9117
+ description (or your own children). Following it closes the menu.` }], types: {} }, dependencies: ["lucide-react@1.63.0", "motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/focus.json", "https://kznlabs.github.io/labsUI/r/use-controllable-state.json", "https://kznlabs.github.io/labsUI/r/use-dismiss.json", "https://kznlabs.github.io/labsUI/r/use-indicator.json", "https://kznlabs.github.io/labsUI/r/utils.json"], files: [{ path: "components/ui/navigation-menu.tsx", type: "registry:ui", content: `"use client";
8943
9118
 
8944
9119
  import { ChevronDown } from "lucide-react";
8945
9120
  import {
@@ -8966,6 +9141,7 @@ import {
8966
9141
  useState,
8967
9142
  } from "react";
8968
9143
  import { createPortal } from "react-dom";
9144
+ import { tabbables } from "@/lib/focus";
8969
9145
  import { useControllableState } from "@/lib/use-controllable-state";
8970
9146
  import { useDismiss } from "@/lib/use-dismiss";
8971
9147
  import { useIndicator } from "@/lib/use-indicator";
@@ -8979,9 +9155,6 @@ const OPEN_DELAY = 120;
8979
9155
  const CLOSE_DELAY = 250;
8980
9156
  const MARGIN = 8;
8981
9157
 
8982
- const FOCUSABLE =
8983
- 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
8984
-
8985
9158
  type NavCtx = {
8986
9159
  value: string;
8987
9160
  setValue: (value: string) => void;
@@ -9246,8 +9419,10 @@ export function NavigationMenu({
9246
9419
  className="-z-10 absolute inset-y-0 left-0 rounded-lg bg-foreground/[0.06]"
9247
9420
  />
9248
9421
  )}
9422
+
9249
9423
  {children}
9250
9424
  </div>
9425
+
9251
9426
  <motion.div
9252
9427
  ref={setViewport}
9253
9428
  data-floating=""
@@ -9365,6 +9540,7 @@ export function NavigationMenuTrigger({
9365
9540
  {...props}
9366
9541
  >
9367
9542
  {children}
9543
+
9368
9544
  <motion.span
9369
9545
  aria-hidden="true"
9370
9546
  animate={{ rotate: open ? 180 : 0 }}
@@ -9378,7 +9554,7 @@ export function NavigationMenuTrigger({
9378
9554
  }
9379
9555
 
9380
9556
  function focusFirst(scope: HTMLElement | null) {
9381
- scope?.querySelector<HTMLElement>(FOCUSABLE)?.focus();
9557
+ tabbables(scope)[0]?.focus();
9382
9558
  }
9383
9559
 
9384
9560
  const slide = {
@@ -9410,6 +9586,7 @@ export function NavigationMenuContent({
9410
9586
  return (
9411
9587
  <InPanel value>
9412
9588
  {!open && <div hidden>{children}</div>}
9589
+
9413
9590
  {nav.viewport &&
9414
9591
  createPortal(
9415
9592
  <AnimatePresence custom={nav.dir} initial={false}>
@@ -9470,7 +9647,7 @@ function Panel({
9470
9647
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
9471
9648
  if (event.key !== "Tab") return;
9472
9649
 
9473
- const focusables = [...(ref.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])];
9650
+ const focusables = tabbables(ref.current);
9474
9651
  const trigger = nav.items.get(nav.value);
9475
9652
 
9476
9653
  if (event.shiftKey && document.activeElement === focusables[0]) {
@@ -9478,7 +9655,7 @@ function Panel({
9478
9655
  trigger?.focus();
9479
9656
  } else if (!event.shiftKey && document.activeElement === focusables.at(-1)) {
9480
9657
  // Past the last link: on to whatever follows the trigger on the bar.
9481
- const bar = [...(nav.list.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])];
9658
+ const bar = tabbables(nav.list.current);
9482
9659
  const next = trigger ? bar[bar.indexOf(trigger) + 1] : undefined;
9483
9660
 
9484
9661
  if (next) {
@@ -9585,8 +9762,10 @@ export function NavigationMenuLink({
9585
9762
  {icon}
9586
9763
  </span>
9587
9764
  )}
9765
+
9588
9766
  <span className="flex min-w-0 flex-col gap-0.5">
9589
9767
  <span className="font-medium text-foreground">{title}</span>
9768
+
9590
9769
  {description && (
9591
9770
  <span className="line-clamp-2 text-muted-foreground text-xs leading-snug">
9592
9771
  {description}
@@ -9699,13 +9878,16 @@ export function Newsletter({
9699
9878
  {title}
9700
9879
  </h3>
9701
9880
  )}
9881
+
9702
9882
  {description && <p className="text-muted-foreground text-sm">{description}</p>}
9703
9883
  </div>
9704
9884
  )}
9885
+
9705
9886
  <InputGroup>
9706
9887
  <InputGroupAddon>
9707
9888
  <Mail aria-hidden="true" />
9708
9889
  </InputGroupAddon>
9890
+
9709
9891
  <Input
9710
9892
  type="email"
9711
9893
  name="email"
@@ -9714,6 +9896,7 @@ export function Newsletter({
9714
9896
  placeholder={placeholder}
9715
9897
  aria-label="Email address"
9716
9898
  />
9899
+
9717
9900
  <InputGroupAddon align="end">
9718
9901
  <Button type="submit" size="sm" status={status} successLabel="Subscribed">
9719
9902
  {submitLabel}
@@ -9812,6 +9995,7 @@ export function NumberInput({
9812
9995
  <Minus aria-hidden="true" />
9813
9996
  </button>
9814
9997
  </InputGroupAddon>
9998
+
9815
9999
  <Input
9816
10000
  type="number"
9817
10001
  inputMode={Number.isInteger(step) ? "numeric" : "decimal"}
@@ -9835,6 +10019,7 @@ export function NumberInput({
9835
10019
  className="px-0 text-center tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
9836
10020
  {...props}
9837
10021
  />
10022
+
9838
10023
  <InputGroupAddon align="end" className="pr-1">
9839
10024
  <button
9840
10025
  type="button"
@@ -9953,6 +10138,7 @@ function Control({
9953
10138
  const content = (
9954
10139
  <>
9955
10140
  {active && <PaginationActivePill />}
10141
+
9956
10142
  {children}
9957
10143
  </>
9958
10144
  );
@@ -10018,9 +10204,11 @@ export function Pagination({
10018
10204
  className="gap-1 px-2.5"
10019
10205
  >
10020
10206
  <ChevronLeft aria-hidden="true" />
10207
+
10021
10208
  <span className="hidden sm:block">{labels?.previous ?? "Previous"}</span>
10022
10209
  </Control>
10023
10210
  </li>
10211
+
10024
10212
  {pageRange(current, total, siblings).map((page, i) => (
10025
10213
  // biome-ignore lint/suspicious/noArrayIndexKey: slots stay mounted so numbers swipe (see pageRange)
10026
10214
  <li key={i}>
@@ -10038,6 +10226,7 @@ export function Pagination({
10038
10226
  )}
10039
10227
  </li>
10040
10228
  ))}
10229
+
10041
10230
  <li>
10042
10231
  <Control
10043
10232
  {...nav}
@@ -10047,6 +10236,7 @@ export function Pagination({
10047
10236
  className="gap-1 px-2.5"
10048
10237
  >
10049
10238
  <span className="hidden sm:block">{labels?.next ?? "Next"}</span>
10239
+
10050
10240
  <ChevronRight aria-hidden="true" />
10051
10241
  </Control>
10052
10242
  </li>
@@ -10195,7 +10385,9 @@ export function ClipFilters() {
10195
10385
  </Popover>
10196
10386
  );
10197
10387
  }` }], 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.
10198
- 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";
10388
+ Not a focus trap: Tab past the end closes it and moves on, like any non-modal popup; Shift+Tab
10389
+ from the first field returns to the trigger. Children unmount while closed, so keep field values
10390
+ in state above it.` }], types: {} }, dependencies: ["motion@14.7.1"], registryDependencies: ["https://kznlabs.github.io/labsUI/r/button.json", "https://kznlabs.github.io/labsUI/r/focus.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";
10199
10391
 
10200
10392
  import { AnimatePresence, motion } from "motion/react";
10201
10393
  import {
@@ -10211,6 +10403,7 @@ import {
10211
10403
  } from "react";
10212
10404
  import { createPortal } from "react-dom";
10213
10405
  import { Button } from "@/components/ui/button";
10406
+ import { tabbables } from "@/lib/focus";
10214
10407
  import { useMounted } from "@/lib/platform";
10215
10408
  import { useAnchor } from "@/lib/use-anchor";
10216
10409
  import { useControllableState } from "@/lib/use-controllable-state";
@@ -10280,12 +10473,11 @@ export function PopoverTrigger({ onClick, ref, ...props }: ComponentProps<typeof
10280
10473
  );
10281
10474
  }
10282
10475
 
10283
- const FOCUSABLE =
10284
- 'a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
10285
-
10286
10476
  /**
10287
10477
  * Focus moves in on open. Esc or an outside click closes it; Esc returns focus to the trigger.
10288
- * Not a focus trap: Tab past the end closes it and moves on, like any non-modal popup.
10478
+ * Not a focus trap: Tab past the end closes it and moves on, like any non-modal popup; Shift+Tab
10479
+ * from the first field returns to the trigger. Children unmount while closed, so keep field values
10480
+ * in state above it.
10289
10481
  */
10290
10482
  export function PopoverContent({
10291
10483
  align = "center",
@@ -10315,11 +10507,7 @@ export function PopoverContent({
10315
10507
  const node = panel.current;
10316
10508
 
10317
10509
  // data-autofocus claims first focus (a Calendar's selected day), else the first field.
10318
- (
10319
- node?.querySelector<HTMLElement>("[data-autofocus]") ??
10320
- node?.querySelector<HTMLElement>(FOCUSABLE) ??
10321
- node
10322
- )?.focus();
10510
+ (node?.querySelector<HTMLElement>("[data-autofocus]") ?? tabbables(node)[0] ?? node)?.focus();
10323
10511
 
10324
10512
  // Focus leaving the panel (Tab past the end, clicking elsewhere) closes it.
10325
10513
  const focusOut = (event: FocusEvent) => {
@@ -10349,6 +10537,20 @@ export function PopoverContent({
10349
10537
  aria-labelledby={\`\${id}-trigger\`}
10350
10538
  tabIndex={-1}
10351
10539
  onKeyDown={(event) => {
10540
+ // The panel sits at the end of body: past either end, focus goes on from the trigger.
10541
+ if (event.key === "Tab") {
10542
+ const fields = tabbables(panel.current);
10543
+ const edge = event.shiftKey ? fields[0] : fields.at(-1);
10544
+
10545
+ if (edge && event.target !== edge) return;
10546
+
10547
+ if (event.shiftKey) event.preventDefault();
10548
+ else setOpen(false);
10549
+ trigger.current?.focus();
10550
+
10551
+ return;
10552
+ }
10553
+
10352
10554
  if (event.key !== "Escape" || event.defaultPrevented) return;
10353
10555
  // Handled here so an enclosing Dialog stays open.
10354
10556
  event.preventDefault();
@@ -10406,8 +10608,8 @@ export function PricingPage() {
10406
10608
  ]}
10407
10609
  />
10408
10610
  );
10409
- }` }], 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,
10410
- 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";
10611
+ }` }], 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,
10612
+ 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";
10411
10613
 
10412
10614
  import { Check } from "lucide-react";
10413
10615
  import { AnimatePresence, motion } from "motion/react";
@@ -10424,15 +10626,15 @@ import {
10424
10626
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
10425
10627
  import { cn, spring } from "@/lib/utils";
10426
10628
 
10427
- type Period = "monthly" | "yearly";
10629
+ export type PricingPeriod = "monthly" | "yearly";
10428
10630
 
10429
- type Plan = {
10631
+ export type PricingPlan = {
10430
10632
  /** Plan name, the card's heading. */
10431
10633
  name: string;
10432
10634
  /** One line under the name. */
10433
10635
  description?: string;
10434
10636
  /** Price per month on each billing period, e.g. \`{ monthly: 12, yearly: 10 }\`. */
10435
- price: Record<Period, number>;
10637
+ price: Record<PricingPeriod, number>;
10436
10638
  features: string[];
10437
10639
  /** Call to action, e.g. \`<Button className="w-full">Start trial</Button>\`. */
10438
10640
  cta: ReactNode;
@@ -10444,13 +10646,13 @@ type Plan = {
10444
10646
 
10445
10647
  type PricingProps = {
10446
10648
  /** Plans in display order. */
10447
- plans: Plan[];
10649
+ plans: PricingPlan[];
10448
10650
  /** ISO 4217 code for the price, e.g. "EUR". */
10449
10651
  currency?: string;
10450
10652
  /** BCP 47 locale for number formatting. */
10451
10653
  locale?: string;
10452
10654
  /** Billing period selected at first render. */
10453
- defaultPeriod?: Period;
10655
+ defaultPeriod?: PricingPeriod;
10454
10656
  /** Next to "Yearly", e.g. "−17%". */
10455
10657
  yearlyNote?: string;
10456
10658
  className?: string;
@@ -10468,7 +10670,7 @@ export function Pricing({
10468
10670
  yearlyNote,
10469
10671
  className,
10470
10672
  }: PricingProps) {
10471
- const [period, setPeriod] = useState<Period>(defaultPeriod);
10673
+ const [period, setPeriod] = useState<PricingPeriod>(defaultPeriod);
10472
10674
  const money = new Intl.NumberFormat(locale, {
10473
10675
  style: "currency",
10474
10676
  currency,
@@ -10481,9 +10683,10 @@ export function Pricing({
10481
10683
  <ToggleGroup
10482
10684
  aria-label="Billing period"
10483
10685
  value={period}
10484
- onValueChange={(value) => value && setPeriod(value as Period)}
10686
+ onValueChange={(value) => value && setPeriod(value as PricingPeriod)}
10485
10687
  >
10486
10688
  <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
10689
+
10487
10690
  <ToggleGroupItem value="yearly">
10488
10691
  Yearly
10489
10692
  {yearlyNote && (
@@ -10493,6 +10696,7 @@ export function Pricing({
10493
10696
  )}
10494
10697
  </ToggleGroupItem>
10495
10698
  </ToggleGroup>
10699
+
10496
10700
  <div className="grid w-full gap-4 sm:grid-cols-[repeat(auto-fit,minmax(14rem,1fr))]">
10497
10701
  {plans.map((plan) => (
10498
10702
  <Card
@@ -10502,14 +10706,17 @@ export function Pricing({
10502
10706
  <CardHeader>
10503
10707
  <div className="flex min-h-6 items-center justify-between gap-2">
10504
10708
  <CardTitle>{plan.name}</CardTitle>
10709
+
10505
10710
  {plan.badge && (
10506
10711
  <Badge variant={plan.featured ? "default" : "secondary"} shape="pill">
10507
10712
  {plan.badge}
10508
10713
  </Badge>
10509
10714
  )}
10510
10715
  </div>
10716
+
10511
10717
  {plan.description && <CardDescription>{plan.description}</CardDescription>}
10512
10718
  </CardHeader>
10719
+
10513
10720
  <CardContent className="flex flex-col gap-5">
10514
10721
  <p className="flex items-baseline gap-1">
10515
10722
  <span className="relative inline-flex overflow-hidden font-semibold text-4xl tabular-nums tracking-tight">
@@ -10531,20 +10738,24 @@ export function Pricing({
10531
10738
  </motion.span>
10532
10739
  </AnimatePresence>
10533
10740
  </span>
10741
+
10534
10742
  <span className="text-muted-foreground text-sm">
10535
10743
  / month
10536
10744
  <span className="sr-only">, billed {period}</span>
10537
10745
  </span>
10538
10746
  </p>
10747
+
10539
10748
  <ul className="flex flex-col gap-2 text-sm">
10540
10749
  {plan.features.map((feature) => (
10541
10750
  <li key={feature} className="flex gap-2">
10542
10751
  <Check className="mt-0.5 size-4 shrink-0 text-primary-ink" aria-hidden="true" />
10752
+
10543
10753
  {feature}
10544
10754
  </li>
10545
10755
  ))}
10546
10756
  </ul>
10547
10757
  </CardContent>
10758
+
10548
10759
  <CardFooter className="mt-auto">{plan.cta}</CardFooter>
10549
10760
  </Card>
10550
10761
  ))}
@@ -10552,7 +10763,7 @@ export function Pricing({
10552
10763
  </div>
10553
10764
  );
10554
10765
  }
10555
- ` }] }, "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";
10766
+ ` }] }, "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";
10556
10767
 
10557
10768
  type Product = {
10558
10769
  id: string;
@@ -10658,17 +10869,20 @@ export function ProductCard({
10658
10869
  <CarouselItem key={i}>{frame(slide)}</CarouselItem>
10659
10870
  ))}
10660
10871
  </CarouselContent>
10872
+
10661
10873
  <CarouselDots className="absolute inset-x-0 bottom-2 mt-0" />
10662
10874
  </Carousel>
10663
10875
  ) : (
10664
10876
  frame(media[0])
10665
10877
  )}
10878
+
10666
10879
  {badge && (
10667
10880
  <Badge shape="pill" className="absolute top-2 left-2">
10668
10881
  {badge}
10669
10882
  </Badge>
10670
10883
  )}
10671
10884
  </div>
10885
+
10672
10886
  <div className="flex flex-1 flex-col gap-3 p-3">
10673
10887
  <div className="flex items-baseline justify-between gap-2">
10674
10888
  <h3 className="min-w-0 truncate font-medium text-sm">
@@ -10680,16 +10894,20 @@ export function ProductCard({
10680
10894
  name
10681
10895
  )}
10682
10896
  </h3>
10897
+
10683
10898
  <p className="shrink-0 text-sm tabular-nums">
10684
10899
  {compareAt !== undefined && compareAt > price && (
10685
10900
  <>
10686
10901
  <del className="mr-1.5 text-muted-foreground">{money.format(compareAt)}</del>
10902
+
10687
10903
  <span className="sr-only">now </span>
10688
10904
  </>
10689
10905
  )}
10906
+
10690
10907
  {money.format(price)}
10691
10908
  </p>
10692
10909
  </div>
10910
+
10693
10911
  {onAddToCart && (
10694
10912
  <Button size="sm" variant="outline" className="mt-auto w-full" onClick={onAddToCart}>
10695
10913
  {addLabel}
@@ -10750,11 +10968,13 @@ export function Progress({
10750
10968
  {(label || showValue) && (
10751
10969
  <div className="flex items-center justify-between gap-3 text-xs">
10752
10970
  {label && <span className="font-medium">{label}</span>}
10971
+
10753
10972
  {showValue && determinate && (
10754
10973
  <span className="ml-auto text-muted-foreground tabular-nums">{pct}%</span>
10755
10974
  )}
10756
10975
  </div>
10757
10976
  )}
10977
+
10758
10978
  <div
10759
10979
  role="progressbar"
10760
10980
  aria-label={label ?? ariaLabel}
@@ -10866,6 +11086,7 @@ export function Quote({
10866
11086
  className={cn("flex flex-col", surface[variant], className)}
10867
11087
  >
10868
11088
  {large && <QuoteIcon aria-hidden="true" className="size-8 text-muted-foreground/60" />}
11089
+
10869
11090
  <blockquote
10870
11091
  lang={lang}
10871
11092
  className={cn(
@@ -10906,11 +11127,14 @@ export function Quote({
10906
11127
  )}
10907
11128
  </div>
10908
11129
  </blockquote>
11130
+
10909
11131
  {(author || avatar) && (
10910
11132
  <figcaption className="flex items-center gap-3">
10911
11133
  {avatar}
11134
+
10912
11135
  <span className="grid text-left">
10913
11136
  {author && <span className="font-medium text-foreground text-sm">{author}</span>}
11137
+
10914
11138
  {subtitle && <span className="text-muted-foreground text-xs">{subtitle}</span>}
10915
11139
  </span>
10916
11140
  </figcaption>
@@ -10927,7 +11151,7 @@ export function PlanPicker() {
10927
11151
  <RadioGroupItem value="pro" description="10 servers, unlimited clips.">Pro</RadioGroupItem>
10928
11152
  </RadioGroup>
10929
11153
  );
10930
- }` }], 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";
11154
+ }` }], 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";
10931
11155
 
10932
11156
  import { motion } from "motion/react";
10933
11157
  import { type ComponentProps, createContext, type ReactNode, use, useId } from "react";
@@ -10940,6 +11164,7 @@ type RadioCtx = {
10940
11164
  value: string;
10941
11165
  setValue: (value: string) => void;
10942
11166
  disabled?: boolean;
11167
+ required?: boolean;
10943
11168
  invalid?: boolean;
10944
11169
  };
10945
11170
 
@@ -10958,6 +11183,8 @@ type RadioGroupProps = Omit<ComponentProps<"fieldset">, "defaultValue" | "onChan
10958
11183
  name?: string;
10959
11184
  /** Stack items or lay them in a row. */
10960
11185
  orientation?: "vertical" | "horizontal";
11186
+ /** Submit needs a picked item. */
11187
+ required?: boolean;
10961
11188
  "aria-invalid"?: boolean;
10962
11189
  };
10963
11190
 
@@ -10973,12 +11200,8 @@ export function RadioGroup({
10973
11200
  children,
10974
11201
  ...rest
10975
11202
  }: RadioGroupProps) {
10976
- // A fieldset takes no id from Field (labels can't target it) and no required.
10977
- const {
10978
- id: _id,
10979
- required: _required,
10980
- ...props
10981
- } = useFieldControl<typeof rest & { required?: boolean }>(rest);
11203
+ // A fieldset takes no id from Field (labels can't target it); required goes on the radios.
11204
+ const { id: _id, required, ...props } = useFieldControl(rest);
10982
11205
  const auto = useId();
10983
11206
  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
10984
11207
 
@@ -10989,11 +11212,13 @@ export function RadioGroup({
10989
11212
  value,
10990
11213
  setValue,
10991
11214
  disabled: props.disabled,
11215
+ required,
10992
11216
  invalid: !!props["aria-invalid"],
10993
11217
  }}
10994
11218
  >
10995
11219
  <fieldset className={cn("min-w-0", className)} {...props}>
10996
11220
  {legend && <legend className="mb-3 font-medium text-sm leading-none">{legend}</legend>}
11221
+
10997
11222
  <div
10998
11223
  className={cn(
10999
11224
  "flex gap-3",
@@ -11043,11 +11268,13 @@ export function RadioGroupItem({
11043
11268
  value={value}
11044
11269
  checked={checked}
11045
11270
  disabled={off}
11271
+ required={ctx.required}
11046
11272
  aria-invalid={ctx.invalid || undefined}
11047
11273
  aria-describedby={description ? \`\${id}-description\` : undefined}
11048
11274
  onChange={() => ctx.setValue(value)}
11049
11275
  className="peer absolute inset-0 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
11050
11276
  />
11277
+
11051
11278
  <span
11052
11279
  aria-hidden="true"
11053
11280
  className={cn(
@@ -11063,6 +11290,7 @@ export function RadioGroupItem({
11063
11290
  />
11064
11291
  </span>
11065
11292
  </span>
11293
+
11066
11294
  <span className="grid gap-1">
11067
11295
  <label
11068
11296
  htmlFor={id}
@@ -11073,6 +11301,7 @@ export function RadioGroupItem({
11073
11301
  >
11074
11302
  {children}
11075
11303
  </label>
11304
+
11076
11305
  {description && (
11077
11306
  <span id={\`\${id}-description\`} className="text-muted-foreground text-xs">
11078
11307
  {description}
@@ -11092,11 +11321,12 @@ export function ProductRating({ average, count }: { average: number; count: numb
11092
11321
  <span className="text-muted-foreground">· {count} reviews</span>
11093
11322
  </div>
11094
11323
  );
11095
- }` }], 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";
11324
+ }` }], 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";
11096
11325
 
11097
11326
  import { Star } from "lucide-react";
11098
11327
  import { motion } from "motion/react";
11099
11328
  import { useId, useState } from "react";
11329
+ import { useFieldControl } from "@/components/ui/field";
11100
11330
  import { useControllableState } from "@/lib/use-controllable-state";
11101
11331
  import { clamp, cn, spring } from "@/lib/utils";
11102
11332
 
@@ -11113,10 +11343,18 @@ type RatingProps = {
11113
11343
  readOnly?: boolean;
11114
11344
  /** Form field name for the chosen value. */
11115
11345
  name?: string;
11346
+ /** Submit needs a rating. */
11347
+ required?: boolean;
11348
+ /** Stars can't be picked and the value isn't submitted. */
11349
+ disabled?: boolean;
11116
11350
  /** Group label (visually hidden). */
11117
11351
  label?: string;
11118
11352
  /** Star size. */
11119
11353
  size?: "sm" | "default";
11354
+ /** Ids of hint or error text; Field fills it in. */
11355
+ "aria-describedby"?: string;
11356
+ /** Error styling; Field sets it from \`error\`. */
11357
+ "aria-invalid"?: boolean;
11120
11358
  className?: string;
11121
11359
  };
11122
11360
 
@@ -11126,17 +11364,22 @@ const sizes = { sm: "size-4", default: "size-5" } as const;
11126
11364
  * Star rating. Interactive: native radios (arrow keys, forms, screen readers) with a hover
11127
11365
  * preview and a spring lift/press on the stars. \`readOnly\`: an image of the score, fractions included.
11128
11366
  */
11129
- export function Rating({
11130
- value: valueProp,
11131
- defaultValue = 0,
11132
- onValueChange,
11133
- max = 5,
11134
- readOnly = false,
11135
- name,
11136
- label = "Rating",
11137
- size = "default",
11138
- className,
11139
- }: RatingProps) {
11367
+ export function Rating(allProps: RatingProps) {
11368
+ const {
11369
+ value: valueProp,
11370
+ defaultValue = 0,
11371
+ onValueChange,
11372
+ max = 5,
11373
+ readOnly = false,
11374
+ name,
11375
+ required,
11376
+ disabled,
11377
+ label = "Rating",
11378
+ size = "default",
11379
+ className,
11380
+ "aria-describedby": describedBy,
11381
+ "aria-invalid": invalid,
11382
+ } = useFieldControl(allProps);
11140
11383
  const auto = useId();
11141
11384
  const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange);
11142
11385
  const [hover, setHover] = useState<number | null>(null);
@@ -11160,6 +11403,7 @@ export function Rating({
11160
11403
  fill="currentColor"
11161
11404
  strokeWidth={0}
11162
11405
  />
11406
+
11163
11407
  <span
11164
11408
  className="absolute inset-0 overflow-hidden"
11165
11409
  style={{ width: \`\${fill * 100}%\` }}
@@ -11182,23 +11426,34 @@ export function Rating({
11182
11426
 
11183
11427
  return (
11184
11428
  <fieldset
11429
+ disabled={disabled}
11430
+ aria-describedby={describedBy}
11185
11431
  onPointerLeave={() => setHover(null)}
11186
- className={cn("inline-flex items-center gap-0.5", className)}
11432
+ className={cn("inline-flex items-center gap-0.5", disabled && "opacity-50", className)}
11187
11433
  >
11188
11434
  <legend className="sr-only">{label}</legend>
11435
+
11189
11436
  {stars.map((star) => (
11190
- <label key={star} className="relative cursor-pointer" onPointerEnter={() => setHover(star)}>
11437
+ <label
11438
+ key={star}
11439
+ className={cn("relative", disabled ? "cursor-not-allowed" : "cursor-pointer")}
11440
+ onPointerEnter={() => !disabled && setHover(star)}
11441
+ >
11191
11442
  <input
11192
11443
  type="radio"
11193
11444
  name={name ?? auto}
11194
11445
  value={star}
11195
11446
  checked={value === star}
11447
+ required={required}
11448
+ aria-invalid={invalid || undefined}
11196
11449
  onChange={() => setValue(star)}
11197
11450
  className="peer sr-only"
11198
11451
  />
11452
+
11199
11453
  <span className="sr-only">
11200
11454
  {star} {star === 1 ? "star" : "stars"}
11201
11455
  </span>
11456
+
11202
11457
  <motion.span
11203
11458
  aria-hidden="true"
11204
11459
  // whileTap would make the star focusable, the radio underneath is the tab stop.
@@ -11350,11 +11605,11 @@ export function AdminLayout({ nav, log, children }: {
11350
11605
  </ResizablePanel>
11351
11606
  </ResizablePanelGroup>
11352
11607
  );
11353
- }` }], 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
11608
+ }` }], 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
11354
11609
  splitters: arrows step, Home/End jump to the limits, Enter collapses a collapsible neighbor.
11355
11610
  Drag follows the pointer; keys and toggles spring. Panels and handles find their order in the
11356
11611
  DOM, so wrappers and Server Component parents work. Until hydration the layout is plain CSS
11357
- (\`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";
11612
+ (\`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";
11358
11613
 
11359
11614
  import { type AnimationPlaybackControls, animate, useReducedMotion } from "motion/react";
11360
11615
  import {
@@ -11370,7 +11625,7 @@ import {
11370
11625
  } from "react";
11371
11626
  import { cn, spring } from "@/lib/utils";
11372
11627
 
11373
- type Direction = "horizontal" | "vertical";
11628
+ export type ResizableDirection = "horizontal" | "vertical";
11374
11629
  type Limits = { min: number; max: number; collapsible: boolean; collapsed: number };
11375
11630
 
11376
11631
  type ResizablePanelProps = ComponentProps<"div"> & {
@@ -11394,7 +11649,7 @@ type Layout = {
11394
11649
  };
11395
11650
 
11396
11651
  type GroupCtx = {
11397
- direction: Direction;
11652
+ direction: ResizableDirection;
11398
11653
  id: string;
11399
11654
  layout: Layout | null;
11400
11655
  /** Panels report their limits here from a layout effect; the group's own effect reads them. */
@@ -11496,7 +11751,7 @@ export function ResizablePanelGroup({
11496
11751
  ...props
11497
11752
  }: ComponentProps<"div"> & {
11498
11753
  /** "horizontal": panels side by side. "vertical": stacked. */
11499
- direction?: Direction;
11754
+ direction?: ResizableDirection;
11500
11755
  /** Final sizes after each drag, key or toggle, e.g. to save them in a cookie for SSR. */
11501
11756
  onLayout?: (sizes: number[]) => void;
11502
11757
  /** % per arrow key press. */
@@ -12117,7 +12372,9 @@ export function ScrollArea({
12117
12372
  >
12118
12373
  {children}
12119
12374
  </div>
12375
+
12120
12376
  {orientation !== "horizontal" && <Thumb axis="y" viewport={viewport} />}
12377
+
12121
12378
  {orientation !== "vertical" && <Thumb axis="x" viewport={viewport} />}
12122
12379
  </div>
12123
12380
  );
@@ -12214,6 +12471,7 @@ export function SearchInput({
12214
12471
  aria-hidden="true"
12215
12472
  className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 size-4 text-muted-foreground"
12216
12473
  />
12474
+
12217
12475
  <Input
12218
12476
  ref={(element) => {
12219
12477
  input.current = element;
@@ -12240,6 +12498,7 @@ export function SearchInput({
12240
12498
  )}
12241
12499
  {...props}
12242
12500
  />
12501
+
12243
12502
  <span className="-translate-y-1/2 absolute top-1/2 right-2 flex">
12244
12503
  <AnimatePresence mode="popLayout" initial={false}>
12245
12504
  {value ? (
@@ -12346,6 +12605,7 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
12346
12605
  transition={spring}
12347
12606
  className="-z-10 absolute inset-y-1 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
12348
12607
  />
12608
+
12349
12609
  {items.map((item) => (
12350
12610
  <button
12351
12611
  key={item}
@@ -12364,11 +12624,12 @@ export function SegmentedTabs({ label, items, value, onChange, className }: Segm
12364
12624
  {item}
12365
12625
  </button>
12366
12626
  ))}
12627
+
12367
12628
  <OverflowFocus />
12368
12629
  </nav>
12369
12630
  );
12370
12631
  }
12371
- ` }] }, 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";
12632
+ ` }] }, 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";
12372
12633
 
12373
12634
  import { useState } from "react";
12374
12635
  import { Field } from "@/components/ui/field";
@@ -12387,7 +12648,7 @@ export function RolePicker() {
12387
12648
  <Select name="role" options={roles} value={role} onValueChange={setRole} />
12388
12649
  </Field>
12389
12650
  );
12390
- }` }], 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,
12651
+ }` }], 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,
12391
12652
  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";
12392
12653
 
12393
12654
  import { Check, ChevronsUpDown } from "lucide-react";
@@ -12418,11 +12679,11 @@ type SelectProps = {
12418
12679
  onValueChange?: (value: string) => void;
12419
12680
  /** Trigger text when nothing is picked. */
12420
12681
  placeholder?: string;
12421
- /** Submits the value with a form, through a hidden input. */
12682
+ /** Form field name for the picked value. */
12422
12683
  name?: string;
12423
12684
  /** Pair with <Label htmlFor>, or let <Field> fill it in. */
12424
12685
  id?: string;
12425
- /** Blocks opening. */
12686
+ /** Blocks opening; the value isn't submitted. */
12426
12687
  disabled?: boolean;
12427
12688
  /** Form submit needs a value. */
12428
12689
  required?: boolean;
@@ -12575,17 +12836,22 @@ export function Select(allProps: SelectProps) {
12575
12836
  )}
12576
12837
  >
12577
12838
  {selected?.icon}
12839
+
12578
12840
  {selected?.label ?? placeholder}
12579
12841
  </span>
12842
+
12580
12843
  <ChevronsUpDown className="text-muted-foreground" aria-hidden="true" />
12581
12844
  </button>
12845
+
12582
12846
  <FormValue
12583
12847
  name={name}
12584
12848
  value={value}
12585
- required={required && !disabled}
12849
+ required={required}
12850
+ disabled={disabled}
12586
12851
  anchor={trigger}
12587
12852
  onInvalid={() => setInvalid(true)}
12588
12853
  />
12854
+
12589
12855
  {mounted &&
12590
12856
  createPortal(
12591
12857
  <AnimatePresence>
@@ -12626,6 +12892,7 @@ export function Select(allProps: SelectProps) {
12626
12892
  {groupIndex > 0 && (
12627
12893
  <div aria-hidden="true" className="-mx-1 my-1 h-px bg-border" />
12628
12894
  )}
12895
+
12629
12896
  {group.label && (
12630
12897
  <div
12631
12898
  aria-hidden="true"
@@ -12634,6 +12901,7 @@ export function Select(allProps: SelectProps) {
12634
12901
  {group.label}
12635
12902
  </div>
12636
12903
  )}
12904
+
12637
12905
  {group.options.map((option) => (
12638
12906
  // biome-ignore lint/a11y/useKeyWithClickEvents: keys handled on the listbox (aria-activedescendant)
12639
12907
  <div
@@ -12657,7 +12925,9 @@ export function Select(allProps: SelectProps) {
12657
12925
  )}
12658
12926
  >
12659
12927
  {option.icon}
12928
+
12660
12929
  {option.label}
12930
+
12661
12931
  {option.value === value && (
12662
12932
  <Check className="!text-current absolute right-2" aria-hidden="true" />
12663
12933
  )}
@@ -12844,7 +13114,7 @@ export function SidebarLayout({
12844
13114
  );
12845
13115
  }
12846
13116
 
12847
- type SidebarItem = {
13117
+ export type SidebarItem = {
12848
13118
  /** What \`current\` and \`onSelect\` refer to. Default: \`href\`, else \`label\`. */
12849
13119
  id?: string;
12850
13120
  /** Visible text; the rail shows it as a tooltip. */
@@ -12857,7 +13127,7 @@ type SidebarItem = {
12857
13127
  badge?: ReactNode;
12858
13128
  };
12859
13129
 
12860
- type SidebarGroup = {
13130
+ export type SidebarGroup = {
12861
13131
  /** Heading above the items; fades out in the rail. */
12862
13132
  label?: string;
12863
13133
  /** The group's entries. */
@@ -12921,6 +13191,7 @@ export function Sidebar({
12921
13191
  const body = (
12922
13192
  <nav aria-label={label} className="flex min-h-0 flex-1 flex-col">
12923
13193
  {header && <div className="border-b p-2">{header}</div>}
13194
+
12924
13195
  <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-2">
12925
13196
  {groups.map((group, i) =>
12926
13197
  group.collapsible && group.label ? (
@@ -12929,6 +13200,7 @@ export function Sidebar({
12929
13200
  <CollapsibleTrigger className={cn("px-2.5 pb-1 text-muted-foreground text-xs", fade)}>
12930
13201
  {group.label}
12931
13202
  </CollapsibleTrigger>
13203
+
12932
13204
  <CollapsibleContent>{list(group.items)}</CollapsibleContent>
12933
13205
  </Collapsible>
12934
13206
  ) : (
@@ -12944,11 +13216,13 @@ export function Sidebar({
12944
13216
  {group.label}
12945
13217
  </p>
12946
13218
  )}
13219
+
12947
13220
  {list(group.items)}
12948
13221
  </div>
12949
13222
  ),
12950
13223
  )}
12951
13224
  </div>
13225
+
12952
13226
  {footer?.length ? <div className="border-t p-2">{list(footer)}</div> : null}
12953
13227
  </nav>
12954
13228
  );
@@ -12958,6 +13232,7 @@ export function Sidebar({
12958
13232
  <Drawer open={openMobile} onOpenChange={setOpenMobile}>
12959
13233
  <DrawerContent side="left" className={cn("bg-background pt-12 text-sm", className)}>
12960
13234
  <DrawerTitle className="sr-only">{label}</DrawerTitle>
13235
+
12961
13236
  {body}
12962
13237
  </DrawerContent>
12963
13238
  </Drawer>
@@ -12999,7 +13274,9 @@ function Entry({
12999
13274
  const body = (
13000
13275
  <>
13001
13276
  {item.icon}
13277
+
13002
13278
  <span className="min-w-0 flex-1 truncate">{item.label}</span>
13279
+
13003
13280
  {item.badge != null && (
13004
13281
  <span className="ml-auto text-muted-foreground text-xs tabular-nums">{item.badge}</span>
13005
13282
  )}
@@ -13096,8 +13373,8 @@ import {
13096
13373
  import { useDisclosure } from "@/lib/use-disclosure";
13097
13374
  import { cn } from "@/lib/utils";
13098
13375
 
13099
- type SiteLink = { label: string; href: string };
13100
- type SiteGroup = {
13376
+ export type SiteLink = { label: string; href: string };
13377
+ export type SiteGroup = {
13101
13378
  /** Trigger text for the panel. */
13102
13379
  label: string;
13103
13380
  /** Links in the panel, each with an optional description and icon. */
@@ -13162,6 +13439,7 @@ export function SiteHeader({
13162
13439
  return (
13163
13440
  <>
13164
13441
  <span ref={sentinel} aria-hidden="true" className="block h-px" />
13442
+
13165
13443
  <header
13166
13444
  ref={header}
13167
13445
  className={cn(
@@ -13178,6 +13456,7 @@ export function SiteHeader({
13178
13456
  isGroup(link) ? (
13179
13457
  <NavigationMenuItem key={link.label}>
13180
13458
  <NavigationMenuTrigger>{link.label}</NavigationMenuTrigger>
13459
+
13181
13460
  <NavigationMenuContent>
13182
13461
  <ul
13183
13462
  className={cn(
@@ -13219,6 +13498,7 @@ export function SiteHeader({
13219
13498
 
13220
13499
  <div className="ml-auto flex items-center gap-2">
13221
13500
  {actions}
13501
+
13222
13502
  <Button
13223
13503
  variant="ghost"
13224
13504
  size="icon"
@@ -13238,6 +13518,7 @@ export function SiteHeader({
13238
13518
  <DrawerHeader>
13239
13519
  <DrawerTitle>Menu</DrawerTitle>
13240
13520
  </DrawerHeader>
13521
+
13241
13522
  <DrawerBody>
13242
13523
  <nav aria-label={label} className="flex flex-col gap-5">
13243
13524
  {nav.map((link) =>
@@ -13246,6 +13527,7 @@ export function SiteHeader({
13246
13527
  <h2 className="px-2.5 font-medium text-muted-foreground text-xs">
13247
13528
  {link.label}
13248
13529
  </h2>
13530
+
13249
13531
  <ul className="flex flex-col">
13250
13532
  {link.items.map((i) => (
13251
13533
  <li key={i.href}>
@@ -13307,6 +13589,7 @@ function MobileLink({
13307
13589
  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"
13308
13590
  >
13309
13591
  {icon && <span aria-hidden="true">{icon}</span>}
13592
+
13310
13593
  {children}
13311
13594
  </a>
13312
13595
  );
@@ -13355,6 +13638,7 @@ export function SkeletonGroup({
13355
13638
  return (
13356
13639
  <div role="status" aria-busy="true" className={className} {...props}>
13357
13640
  <span className="sr-only">{label}</span>
13641
+
13358
13642
  {children}
13359
13643
  </div>
13360
13644
  );
@@ -13372,7 +13656,7 @@ export function PriceFilter() {
13372
13656
  <Slider value={price} onValueChange={setPrice} min={0} max={250} step={5} name="price" />
13373
13657
  </Field>
13374
13658
  );
13375
- }` }], 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
13659
+ }` }], 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
13376
13660
  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";
13377
13661
 
13378
13662
  import { motion } from "motion/react";
@@ -13396,7 +13680,7 @@ type SliderProps = {
13396
13680
  step?: number;
13397
13681
  /** Submitted per thumb: \`name\` (one value) or \`name\` twice (range). */
13398
13682
  name?: string;
13399
- /** Blocks dragging and keys. */
13683
+ /** Blocks dragging and keys; the values aren't submitted. */
13400
13684
  disabled?: boolean;
13401
13685
  /** Accessible names per thumb; defaults to "Minimum" / "Maximum" for a range. */
13402
13686
  labels?: string[];
@@ -13475,6 +13759,7 @@ export function Slider(allProps: SliderProps) {
13475
13759
  }}
13476
13760
  />
13477
13761
  </div>
13762
+
13478
13763
  {values.map((value, i) => (
13479
13764
  <motion.span
13480
13765
  // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
@@ -13489,6 +13774,7 @@ export function Slider(allProps: SliderProps) {
13489
13774
  )}
13490
13775
  />
13491
13776
  ))}
13777
+
13492
13778
  {values.map((thumbValue, i) => (
13493
13779
  <input
13494
13780
  // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional (min, max)
@@ -13571,6 +13857,7 @@ export function Spinner({
13571
13857
  {...props}
13572
13858
  >
13573
13859
  {label && <span className="sr-only">{label}</span>}
13860
+
13574
13861
  {spokes.map((i) => (
13575
13862
  <span
13576
13863
  key={i}
@@ -13671,10 +13958,12 @@ export function StatTile({
13671
13958
  <Card className={cn("gap-4 pb-4", className)}>
13672
13959
  <CardHeader>
13673
13960
  <CardDescription>{label}</CardDescription>
13961
+
13674
13962
  <div className="flex flex-wrap items-center gap-2">
13675
13963
  <CardTitle as="p" className="font-semibold text-3xl tabular-nums">
13676
13964
  {value}
13677
13965
  </CardTitle>
13966
+
13678
13967
  {delta !== undefined && delta !== 0 && (
13679
13968
  <Badge variant={good ? "success" : "destructive"} shape="pill">
13680
13969
  <Icon aria-hidden="true" />
@@ -13683,12 +13972,14 @@ export function StatTile({
13683
13972
  </Badge>
13684
13973
  )}
13685
13974
  </div>
13975
+
13686
13976
  {period && (
13687
13977
  <p aria-hidden="true" className="text-muted-foreground text-xs">
13688
13978
  {period}
13689
13979
  </p>
13690
13980
  )}
13691
13981
  </CardHeader>
13982
+
13692
13983
  {data && data.length > 1 && (
13693
13984
  <Chart
13694
13985
  type="area"
@@ -13716,14 +14007,14 @@ export function LandingStats({ users, uptime }: { users: number; uptime: number
13716
14007
  ]}
13717
14008
  />
13718
14009
  );
13719
- }` }], 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";
14010
+ }` }], 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";
13720
14011
  import { CountTo } from "@/components/ui/count-to";
13721
14012
  import { Reveal } from "@/components/ui/reveal";
13722
14013
  import { cn } from "@/lib/utils";
13723
14014
 
13724
14015
  // Server-safe: CountTo and Reveal are the client parts.
13725
14016
 
13726
- type Stat = {
14017
+ export type StatsStripItem = {
13727
14018
  /** What the number counts, e.g. "Active users". */
13728
14019
  label: string;
13729
14020
  /** Number to count up to. */
@@ -13744,7 +14035,7 @@ export function StatsStrip({
13744
14035
  className,
13745
14036
  }: {
13746
14037
  /** Numbers in display order. */
13747
- stats: Stat[];
14038
+ stats: StatsStripItem[];
13748
14039
  className?: string;
13749
14040
  }) {
13750
14041
  return (
@@ -13759,6 +14050,7 @@ export function StatsStrip({
13759
14050
  <Card className="h-full px-5 py-4">
13760
14051
  <dl className="flex flex-col gap-1">
13761
14052
  <dt className="text-muted-foreground text-sm">{stat.label}</dt>
14053
+
13762
14054
  <dd className="font-semibold text-3xl tabular-nums tracking-tight">
13763
14055
  <CountTo
13764
14056
  to={stat.value}
@@ -13767,6 +14059,7 @@ export function StatsStrip({
13767
14059
  decimals={stat.decimals}
13768
14060
  />
13769
14061
  </dd>
14062
+
13770
14063
  {stat.hint && <dd className="text-muted-foreground text-xs">{stat.hint}</dd>}
13771
14064
  </dl>
13772
14065
  </Card>
@@ -13778,7 +14071,7 @@ export function StatsStrip({
13778
14071
  ` }] }, 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";
13779
14072
  import { Input } from "@/components/ui/input";
13780
14073
  import { Stepper, StepperFooter, StepperList, StepperStep } from "@/components/ui/stepper";
13781
- import { createBot } from "./actions"; // "use server"
14074
+ import { createBot } from "./actions";
13782
14075
 
13783
14076
  const steps = [
13784
14077
  { value: "server", title: "Server", description: "Where the bot will live." },
@@ -13803,7 +14096,14 @@ export default function SetupPage() {
13803
14096
  <StepperFooter submitLabel="Create bot" />
13804
14097
  </Stepper>
13805
14098
  );
13806
- }` }], 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";
14099
+ }` }, { name: "actions.ts", code: `"use server";
14100
+
14101
+ export async function createBot(formData: FormData) {
14102
+ const serverId = String(formData.get("serverId"));
14103
+ const botName = String(formData.get("botName"));
14104
+
14105
+ console.log({ serverId, botName });
14106
+ }` }], 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/focus.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";
13807
14107
 
13808
14108
  import { Check } from "lucide-react";
13809
14109
  import { motion } from "motion/react";
@@ -13817,11 +14117,12 @@ import {
13817
14117
  useRef,
13818
14118
  } from "react";
13819
14119
  import { Button } from "@/components/ui/button";
14120
+ import { tabbables } from "@/lib/focus";
13820
14121
  import { useControllableState } from "@/lib/use-controllable-state";
13821
14122
  import { cn, spring } from "@/lib/utils";
13822
14123
 
13823
- type StepInfo = { value: string; title: string; description?: string };
13824
- type Guard = () => boolean | undefined | Promise<boolean | undefined>;
14124
+ export type StepInfo = { value: string; title: string; description?: string };
14125
+ export type StepGuard = () => boolean | undefined | Promise<boolean | undefined>;
13825
14126
 
13826
14127
  type StepperCtx = {
13827
14128
  steps: StepInfo[];
@@ -13830,7 +14131,7 @@ type StepperCtx = {
13830
14131
  next: () => Promise<void>;
13831
14132
  back: () => void;
13832
14133
  fieldsets: Map<string, HTMLFieldSetElement>;
13833
- guards: Map<string, Guard>;
14134
+ guards: Map<string, StepGuard>;
13834
14135
  };
13835
14136
 
13836
14137
  const StepperContext = createContext<StepperCtx | null>(null);
@@ -13844,8 +14145,6 @@ function useStepper() {
13844
14145
  }
13845
14146
 
13846
14147
  const FIELDS = "input:not([type='hidden']),select,textarea";
13847
- const FOCUSABLE =
13848
- "input:not([type='hidden']):not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex='-1'])";
13849
14148
 
13850
14149
  function check(fieldset: HTMLFieldSetElement | undefined) {
13851
14150
  const bad = [...(fieldset?.querySelectorAll<HTMLInputElement>(FIELDS) ?? [])].find(
@@ -13893,7 +14192,7 @@ export function Stepper({
13893
14192
  steps.findIndex((step) => step.value === value),
13894
14193
  );
13895
14194
  const fieldsets = useRef(new Map<string, HTMLFieldSetElement>()).current;
13896
- const guards = useRef(new Map<string, Guard>()).current;
14195
+ const guards = useRef(new Map<string, StepGuard>()).current;
13897
14196
  const moved = useRef(false);
13898
14197
 
13899
14198
  const go = (i: number) => {
@@ -13923,7 +14222,7 @@ export function Stepper({
13923
14222
  const step = steps[index];
13924
14223
  const fs = step && fieldsets.get(step.value);
13925
14224
 
13926
- (fs?.querySelector<HTMLElement>(FOCUSABLE) ?? fs)?.focus();
14225
+ (tabbables(fs ?? null)[0] ?? fs)?.focus();
13927
14226
  }, [index, steps, fieldsets]);
13928
14227
 
13929
14228
  const submit = (event: SubmitEvent<HTMLFormElement>) => {
@@ -14006,15 +14305,19 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
14006
14305
  className="flex cursor-pointer items-center gap-2 rounded-full pr-1 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
14007
14306
  >
14008
14307
  {mark}
14308
+
14009
14309
  {label}
14310
+
14010
14311
  <span className="sr-only"> (completed, go back)</span>
14011
14312
  </button>
14012
14313
  ) : (
14013
14314
  <span className="flex items-center gap-2 pr-1">
14014
14315
  {mark}
14316
+
14015
14317
  {label}
14016
14318
  </span>
14017
14319
  )}
14320
+
14018
14321
  {i < steps.length - 1 && (
14019
14322
  <span aria-hidden="true" className="relative mx-2 h-px flex-1 bg-border">
14020
14323
  <motion.span
@@ -14029,6 +14332,7 @@ export function StepperList({ className, ...props }: ComponentProps<"ol">) {
14029
14332
  );
14030
14333
  })}
14031
14334
  </ol>
14335
+
14032
14336
  {/* Small screens hide the titles; this keeps the current one (and announces moves). */}
14033
14337
  <p aria-live="polite" className="text-muted-foreground text-xs sm:sr-only">
14034
14338
  Step {index + 1} of {steps.length}
@@ -14051,7 +14355,7 @@ export function StepperStep({
14051
14355
  /** Matches a step in \`steps\`. */
14052
14356
  value: string;
14053
14357
  /** Runs on Continue after the native checks; return false to stay. */
14054
- onNext?: Guard;
14358
+ onNext?: StepGuard;
14055
14359
  className?: string;
14056
14360
  /** The step's fields. */
14057
14361
  children: ReactNode;
@@ -14087,11 +14391,13 @@ export function StepperStep({
14087
14391
  {step && (
14088
14392
  <legend className="mb-4 grid gap-1">
14089
14393
  <span className="font-semibold text-lg leading-none">{step.title}</span>
14394
+
14090
14395
  {step.description && (
14091
14396
  <span className="text-muted-foreground text-sm">{step.description}</span>
14092
14397
  )}
14093
14398
  </legend>
14094
14399
  )}
14400
+
14095
14401
  {children}
14096
14402
  </motion.fieldset>
14097
14403
  );
@@ -14120,6 +14426,7 @@ export function StepperFooter({
14120
14426
  <Button variant="ghost" onClick={back} className={cn(index === 0 && "invisible")}>
14121
14427
  {backLabel}
14122
14428
  </Button>
14429
+
14123
14430
  {last ? (
14124
14431
  <Button type="submit">{submitLabel}</Button>
14125
14432
  ) : (
@@ -14552,7 +14859,7 @@ export function StepperFooter({
14552
14859
  display: inline !important;
14553
14860
  }
14554
14861
  }
14555
- ` }] }, 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";
14862
+ ` }] }, 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";
14556
14863
  import { Label } from "@/components/ui/label";
14557
14864
  import { Switch } from "@/components/ui/switch";
14558
14865
  import { Textarea } from "@/components/ui/textarea";
@@ -14613,6 +14920,7 @@ export function Switch({
14613
14920
  className="peer absolute inset-x-0 -inset-y-1 z-10 cursor-pointer appearance-none rounded-full outline-none disabled:cursor-not-allowed"
14614
14921
  {...props}
14615
14922
  />
14923
+
14616
14924
  <span
14617
14925
  aria-hidden="true"
14618
14926
  className={cn(
@@ -14705,6 +15013,7 @@ export function Table({ striped = false, className, containerClassName, ...props
14705
15013
  )}
14706
15014
  {...props}
14707
15015
  />
15016
+
14708
15017
  <OverflowFocus />
14709
15018
  </div>
14710
15019
  );
@@ -14767,6 +15076,7 @@ export function TableHead({ sort, onSort, className, children, ...props }: Table
14767
15076
  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"
14768
15077
  >
14769
15078
  {children}
15079
+
14770
15080
  <ArrowUp
14771
15081
  aria-hidden="true"
14772
15082
  className={cn(
@@ -14933,6 +15243,7 @@ export function TabsList({ className, children, ...props }: ComponentProps<"div"
14933
15243
  )}
14934
15244
  />
14935
15245
  )}
15246
+
14936
15247
  {children}
14937
15248
  </div>
14938
15249
  );
@@ -15010,7 +15321,7 @@ export function TabsContent({
15010
15321
  </motion.div>
15011
15322
  );
15012
15323
  }
15013
- ` }] }, "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";
15324
+ ` }] }, "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";
15014
15325
 
15015
15326
  import { useState } from "react";
15016
15327
  import { Field } from "@/components/ui/field";
@@ -15030,12 +15341,13 @@ export function ClipTags() {
15030
15341
  />
15031
15342
  </Field>
15032
15343
  );
15033
- }` }], 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";
15344
+ }` }], 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";
15034
15345
 
15035
15346
  import { X } from "lucide-react";
15036
15347
  import { AnimatePresence, motion } from "motion/react";
15037
15348
  import { type ComponentProps, type ReactNode, useRef, useState } from "react";
15038
15349
  import { useFieldControl } from "@/components/ui/field";
15350
+ import { FormValue } from "@/lib/form-value";
15039
15351
  import { useControllableState } from "@/lib/use-controllable-state";
15040
15352
  import { cn, spring } from "@/lib/utils";
15041
15353
 
@@ -15072,6 +15384,7 @@ export function Tag({
15072
15384
  )}
15073
15385
  >
15074
15386
  <span className="truncate">{children}</span>
15387
+
15075
15388
  {onRemove && (
15076
15389
  <button
15077
15390
  type="button"
@@ -15111,7 +15424,7 @@ type TagInputProps = Omit<
15111
15424
 
15112
15425
  /**
15113
15426
  * Free-form tags: type and press Enter or a separator; paste a list to add many; Backspace in an
15114
- * empty input removes the last tag. \`name\` submits one hidden input per tag.
15427
+ * empty input removes the last tag. \`name\` submits one value per tag; \`required\` needs at least one.
15115
15428
  */
15116
15429
  export function TagInput({
15117
15430
  value: valueProp,
@@ -15128,7 +15441,8 @@ export function TagInput({
15128
15441
  onBlur,
15129
15442
  ...rest
15130
15443
  }: TagInputProps) {
15131
- const props = useFieldControl(rest);
15444
+ const { required, ...props } = useFieldControl(rest);
15445
+ const [invalid, setInvalid] = useState(false);
15132
15446
  const input = useRef<HTMLInputElement>(null);
15133
15447
  const [tags, set] = useControllableState(valueProp, defaultValue, onValueChange);
15134
15448
  const [draft, setDraft] = useState("");
@@ -15177,11 +15491,14 @@ export function TagInput({
15177
15491
  </Tag>
15178
15492
  ))}
15179
15493
  </AnimatePresence>
15494
+
15180
15495
  <input
15181
15496
  ref={input}
15182
15497
  type="text"
15183
15498
  data-slot="tag-input"
15184
15499
  {...props}
15500
+ aria-required={required}
15501
+ aria-invalid={props["aria-invalid"] || (invalid && !tags.length) || undefined}
15185
15502
  // readOnly, not disabled: Backspace still has to remove tags at the limit.
15186
15503
  readOnly={full || props.readOnly}
15187
15504
  placeholder={full ? undefined : placeholder}
@@ -15216,7 +15533,19 @@ export function TagInput({
15216
15533
  }}
15217
15534
  className="h-6 min-w-20 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
15218
15535
  />
15219
- {name && tags.map((tag) => <input key={tag} type="hidden" name={name} value={tag} />)}
15536
+
15537
+ {name &&
15538
+ tags.map((tag) => (
15539
+ <input key={tag} type="hidden" name={name} value={tag} disabled={props.disabled} />
15540
+ ))}
15541
+
15542
+ <FormValue
15543
+ value={tags.join(",")}
15544
+ required={required}
15545
+ disabled={props.disabled}
15546
+ anchor={input}
15547
+ onInvalid={() => setInvalid(true)}
15548
+ />
15220
15549
  </div>
15221
15550
  );
15222
15551
  }
@@ -15318,15 +15647,19 @@ function Line({
15318
15647
  {line.time && (
15319
15648
  <span className="mr-3 text-muted-foreground tabular-nums select-none">{line.time}</span>
15320
15649
  )}
15650
+
15321
15651
  {line.kind === "command" && (
15322
15652
  <span className="mr-2 text-muted-foreground select-none">{prompt}</span>
15323
15653
  )}
15654
+
15324
15655
  {kind.glyph && (
15325
15656
  <span aria-hidden="true" className={cn("mr-2", kind.glyphClass)}>
15326
15657
  {kind.glyph}
15327
15658
  </span>
15328
15659
  )}
15660
+
15329
15661
  {text}
15662
+
15330
15663
  {cursor && <Cursor />}
15331
15664
  </div>
15332
15665
  );
@@ -15458,6 +15791,7 @@ export function Terminal({
15458
15791
  <Line key={key(item, i)} line={item} prompt={prompt} />
15459
15792
  ))}
15460
15793
  </div>
15794
+
15461
15795
  <div aria-hidden="true" data-typewriter-animated="">
15462
15796
  {lines
15463
15797
  .slice(0, at.line + 1)
@@ -15474,8 +15808,10 @@ export function Terminal({
15474
15808
  />
15475
15809
  ) : null,
15476
15810
  )}
15811
+
15477
15812
  {done && <Line line={{ kind: "command", text: "" }} prompt={prompt} cursor />}
15478
15813
  </div>
15814
+
15479
15815
  <div aria-hidden="true" data-typewriter-fallback="" className="hidden">
15480
15816
  {lines.map((item, i) => (
15481
15817
  <Line key={key(item, i)} line={item} prompt={prompt} />
@@ -15497,6 +15833,7 @@ export function Terminal({
15497
15833
  lines.map((item, i) => <Line key={key(item, i)} line={item} prompt={prompt} />)
15498
15834
  )}
15499
15835
  </div>
15836
+
15500
15837
  <AnimatePresence>
15501
15838
  {log && behind && (
15502
15839
  <motion.button
@@ -15582,9 +15919,11 @@ export function Testimonial({
15582
15919
  >
15583
15920
  {children}
15584
15921
  </Quote>
15922
+
15585
15923
  {others && others.length > 0 && (
15586
15924
  <div className="flex flex-wrap items-center gap-3 border-t pt-5">
15587
15925
  <AvatarGroup users={others} max={5} size="sm" />
15926
+
15588
15927
  {othersLabel && <p className="text-muted-foreground text-sm">{othersLabel}</p>}
15589
15928
  </div>
15590
15929
  )}
@@ -15666,11 +16005,13 @@ export function TextSwap({
15666
16005
  aria-live={isControlled ? "polite" : undefined}
15667
16006
  >
15668
16007
  <span className="sr-only">{currentItem}</span>
16008
+
15669
16009
  {list.map((item) => (
15670
16010
  <span key={item} aria-hidden="true" className="invisible [grid-area:1/1]">
15671
16011
  {item}
15672
16012
  </span>
15673
16013
  ))}
16014
+
15674
16015
  <AnimatePresence mode="popLayout" initial={false}>
15675
16016
  <motion.span
15676
16017
  key={\`\${activeIndex}-\${currentItem}\`}
@@ -15695,7 +16036,7 @@ export function TextSwap({
15695
16036
  </span>
15696
16037
  );
15697
16038
  }
15698
- ` }] }, 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";
16039
+ ` }] }, 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";
15699
16040
  import { Label } from "@/components/ui/label";
15700
16041
  import { Switch } from "@/components/ui/switch";
15701
16042
  import { Textarea } from "@/components/ui/textarea";
@@ -15763,6 +16104,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
15763
16104
  return (
15764
16105
  <span className="relative block w-full min-w-0">
15765
16106
  {textarea}
16107
+
15766
16108
  <span
15767
16109
  id={countId}
15768
16110
  className={cn(
@@ -15780,7 +16122,7 @@ export function Textarea({ className, showCount, onChange, ...rest }: TextareaPr
15780
16122
  </span>
15781
16123
  );
15782
16124
  }
15783
- ` }] }, 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";
16125
+ ` }] }, 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";
15784
16126
 
15785
16127
  const KEY = "theme";
15786
16128
  const EVENT = "themechange";
@@ -15975,6 +16317,7 @@ export function TimelineItem({
15975
16317
  aria-hidden="true"
15976
16318
  className="absolute top-7 bottom-1 left-3 w-px -translate-x-1/2 bg-border group-last/item:hidden"
15977
16319
  />
16320
+
15978
16321
  <span
15979
16322
  aria-hidden="true"
15980
16323
  className={cn(
@@ -15986,6 +16329,7 @@ export function TimelineItem({
15986
16329
  >
15987
16330
  {icon}
15988
16331
  </span>
16332
+
15989
16333
  <div className="grid min-w-0 gap-1 pt-0.5">
15990
16334
  {time &&
15991
16335
  (dateTime ? (
@@ -15995,7 +16339,9 @@ export function TimelineItem({
15995
16339
  ) : (
15996
16340
  <span className="text-muted-foreground text-xs tabular-nums">{time}</span>
15997
16341
  ))}
16342
+
15998
16343
  <p className="font-medium text-sm leading-snug">{title}</p>
16344
+
15999
16345
  {children && (
16000
16346
  <div className="text-muted-foreground text-sm leading-relaxed">{children}</div>
16001
16347
  )}
@@ -16004,12 +16350,12 @@ export function TimelineItem({
16004
16350
  );
16005
16351
  }
16006
16352
  ` }] }, 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";
16007
- export function notifySaved() {
16353
+ export function notifySaved(undo: () => void) {
16008
16354
  toast.success("Changes saved", {
16009
16355
  description: "All changes synced",
16010
16356
  action: { label: "Undo", onClick: undo },
16011
16357
  });
16012
- }` }], 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";
16358
+ }` }], 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";
16013
16359
 
16014
16360
  import { CircleAlert, CircleCheck, Info, Loader2, X } from "lucide-react";
16015
16361
  import { AnimatePresence, animate, motion, type PanInfo, useMotionValue } from "motion/react";
@@ -16026,12 +16372,12 @@ import { cn, material, spring } from "@/lib/utils";
16026
16372
 
16027
16373
  /* ---------- store (module-level, no provider needed) ---------- */
16028
16374
 
16029
- type Variant = "default" | "success" | "error" | "info" | "loading";
16030
- type ToastData = {
16375
+ export type ToastVariant = "default" | "success" | "error" | "info" | "loading";
16376
+ export type ToastData = {
16031
16377
  id: number;
16032
16378
  title: ReactNode;
16033
16379
  description?: ReactNode;
16034
- variant: Variant;
16380
+ variant: ToastVariant;
16035
16381
  /** ms; Infinity = sticky. */
16036
16382
  duration: number;
16037
16383
  action?: { label: string; onClick: () => void };
@@ -16301,13 +16647,16 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16301
16647
  className="flex items-start gap-3 p-4"
16302
16648
  >
16303
16649
  {icons[data.variant] && <span className="mt-0.5">{icons[data.variant]}</span>}
16650
+
16304
16651
  <div
16305
16652
  className="flex min-w-0 flex-1 flex-col gap-0.5"
16306
16653
  role={data.variant === "error" ? "alert" : undefined}
16307
16654
  >
16308
16655
  <p className="font-medium text-sm">{data.title}</p>
16656
+
16309
16657
  {data.description && <p className="text-muted-foreground text-sm">{data.description}</p>}
16310
16658
  </div>
16659
+
16311
16660
  {data.action && (
16312
16661
  <button
16313
16662
  type="button"
@@ -16320,12 +16669,14 @@ function ToastItem({ toast: data, index, y, expanded, frontHeight, total, onHeig
16320
16669
  {data.action.label}
16321
16670
  </button>
16322
16671
  )}
16672
+
16323
16673
  <button
16324
16674
  type="button"
16325
16675
  onClick={() => dismiss(data.id)}
16326
16676
  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"
16327
16677
  >
16328
16678
  <X className="size-3.5" aria-hidden="true" />
16679
+
16329
16680
  <span className="sr-only">Dismiss</span>
16330
16681
  </button>
16331
16682
  </motion.div>
@@ -16471,6 +16822,7 @@ export function ToggleGroup(props: ToggleGroupProps) {
16471
16822
  className="-z-10 absolute inset-y-0.5 left-0 rounded-md bg-raised shadow-sm ring-[0.5px] ring-raised-ring"
16472
16823
  />
16473
16824
  )}
16825
+
16474
16826
  {children}
16475
16827
  </div>
16476
16828
  </GroupContext>
@@ -16675,9 +17027,11 @@ export function Tooltip({
16675
17027
  {...touchHandlers}
16676
17028
  >
16677
17029
  {children}
17030
+
16678
17031
  <span id={id} hidden>
16679
17032
  {content}
16680
17033
  </span>
17034
+
16681
17035
  {mounted &&
16682
17036
  createPortal(
16683
17037
  <AnimatePresence>
@@ -16850,12 +17204,15 @@ export function Typewriter({
16850
17204
  return (
16851
17205
  <span className={cn("inline-flex items-baseline", className)}>
16852
17206
  <span className="sr-only">{fullText}</span>
17207
+
16853
17208
  <span aria-hidden="true" data-typewriter-animated="">
16854
17209
  {displayed}
16855
17210
  </span>
17211
+
16856
17212
  <span aria-hidden="true" data-typewriter-fallback="" className="hidden">
16857
17213
  {phrases[0]}
16858
17214
  </span>
17215
+
16859
17216
  {cursor && (
16860
17217
  <span
16861
17218
  aria-hidden="true"
@@ -16874,24 +17231,24 @@ export function Typewriter({
16874
17231
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
16875
17232
  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 }?: {
16876
17233
  align?: "center" | "end" | "start" | undefined;
16877
- side?: Side | undefined;
17234
+ side?: AnchorSide | undefined;
16878
17235
  offset?: number | undefined;
16879
- }) => Placement`, description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
17236
+ }) => AnchorPlacement`, description: `Fixed-position coordinates for a floating panel next to its anchor: on the preferred side,
16880
17237
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
16881
- 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";
17238
+ 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";
16882
17239
 
16883
17240
  import { type RefObject, useLayoutEffect, useState } from "react";
16884
17241
  import { clamp } from "@/lib/utils";
16885
17242
 
16886
- type Side = "bottom" | "top";
17243
+ export type AnchorSide = "bottom" | "top";
16887
17244
 
16888
- type Placement = {
17245
+ export type AnchorPlacement = {
16889
17246
  top: number;
16890
17247
  left: number;
16891
17248
  /** Anchor width, for \`min-width\` matching (Select). */
16892
17249
  width: number;
16893
17250
  /** Side actually used: the preferred one, or the other for lack of room. */
16894
- side: Side;
17251
+ side: AnchorSide;
16895
17252
  };
16896
17253
 
16897
17254
  const MARGIN = 8;
@@ -16909,9 +17266,9 @@ export function useAnchor(
16909
17266
  align = "start",
16910
17267
  side: preferred = "bottom",
16911
17268
  offset = 6,
16912
- }: { align?: "start" | "center" | "end"; side?: Side; offset?: number } = {},
17269
+ }: { align?: "start" | "center" | "end"; side?: AnchorSide; offset?: number } = {},
16913
17270
  ) {
16914
- const [pos, setPos] = useState<Placement>({
17271
+ const [pos, setPos] = useState<AnchorPlacement>({
16915
17272
  top: -9999,
16916
17273
  left: -9999,
16917
17274
  width: 0,
@@ -16934,7 +17291,7 @@ export function useAnchor(
16934
17291
  const above = anchorRect.top - offset - height;
16935
17292
  const fitsBelow = below + height <= innerHeight - MARGIN;
16936
17293
  const fitsAbove = above >= MARGIN;
16937
- const side: Side =
17294
+ const side: AnchorSide =
16938
17295
  preferred === "bottom"
16939
17296
  ? fitsBelow || !fitsAbove
16940
17297
  ? "bottom"
@@ -17021,17 +17378,17 @@ export function useDisclosure(defaultOpen = false) {
17021
17378
  props: { open, onOpenChange: setOpen },
17022
17379
  };
17023
17380
  }
17024
- ` }] }, "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";
17381
+ ` }] }, "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";
17025
17382
 
17026
17383
  import { type RefObject, useEffect, useEffectEvent } from "react";
17027
17384
 
17028
- type Refs = readonly RefObject<HTMLElement | null>[];
17385
+ export type DismissRefs = readonly RefObject<HTMLElement | null>[];
17029
17386
 
17030
17387
  /**
17031
17388
  * Inside one of \`refs\` (panel first, then its trigger), or inside a floating child opened from
17032
17389
  * the panel: a Select list or menu portals out, but it is marked \`data-floating\`.
17033
17390
  */
17034
- export function isWithin(refs: Refs, target: Node | null) {
17391
+ export function isWithin(refs: DismissRefs, target: Node | null) {
17035
17392
  if (!target) return false;
17036
17393
  if (refs.some((ref) => ref.current?.contains(target))) return true;
17037
17394
 
@@ -17041,7 +17398,7 @@ export function isWithin(refs: Refs, target: Node | null) {
17041
17398
  }
17042
17399
 
17043
17400
  /** Closes a floating panel on a pointerdown outside it, its trigger and its floating children. */
17044
- export function useDismiss(open: boolean, refs: Refs, onDismiss: () => void) {
17401
+ export function useDismiss(open: boolean, refs: DismissRefs, onDismiss: () => void) {
17045
17402
  // Effect events, an inline refs array or callback can't re-subscribe every render.
17046
17403
  const outside = useEffectEvent((event: PointerEvent) => {
17047
17404
  if (!isWithin(refs, event.target as Node)) onDismiss();
@@ -17075,7 +17432,7 @@ export function matchesHotkey(
17075
17432
  return mod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;
17076
17433
  }
17077
17434
 
17078
- type HotkeyOptions = {
17435
+ export type HotkeyOptions = {
17079
17436
  /** Ignore the shortcut while typing in a field. Default: only for single keys. */
17080
17437
  skipInFields?: boolean;
17081
17438
  };
@@ -17248,9 +17605,9 @@ export const spring = { type: "spring", stiffness: 400, damping: 30 } as const;
17248
17605
  export const clamp = (value: number, min: number, max: number) =>
17249
17606
  Math.min(Math.max(value, min), max);
17250
17607
  ` }] } },
17251
- 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", "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 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", "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 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", "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", "empty", "field", "finds", "focus", "focuses", "form", "gets", "hidden", "input", "invalid", "message", "name", "native", "nothing", "oninvalid", "only", "part", "picked", "required", "shows", "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
17608
+ 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", "focus", "platform", "use-controllable-state", "utils"] }, { name: "drawer", type: "registry:ui", title: "Drawer", description: "Edge panel (left, right, top, bottom) that springs in and swipes away. Shares Dialog's focus trap, scroll lock and Esc handling. Sidebar uses it below md.", exports: ["Drawer", "DrawerTrigger", "DrawerClose", "DrawerTitle", "DrawerDescription", "DrawerContent", "DrawerHeader", "DrawerBody", "DrawerFooter"], keywords: ["action", "also", "and", "anywhere", "aria", "armed", "backdrop", "before", "blur", "body", "both", "bottom", "button", "called", "can", "check", "children", "click", "clicks", "close", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "corner", "default", "defaultopen", "defaults", "destructive", "dialogclose", "dialogcontent", "dialogtrigger", "dismiss", "done", "drag", "drawer", "drawers", "edge", "emphasis", "enter", "esc", "feedback", "fill", "first", "focusable", "footer", "for", "form", "from", "give", "handle", "header", "height", "icon", "ignore", "ignores", "irreversible", "its", "label", "left", "loading", "loadinglabel", "never", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "padding", "pair", "panel", "per", "place", "primary", "render", "reserved", "resets", "resize", "runs", "scroll", "scrolls", "second", "showclose", "shown", "shows", "side", "size", "slides", "spinner", "spins", "spring", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "swipe", "taller", "text", "than", "that", "the", "their", "this", "top", "toward", "trigger", "twice", "two", "uncontrolled", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["dialog", "platform", "utils"] }, { name: "dropdown-menu", type: "registry:ui", title: "Dropdown Menu", description: "Action menu anchored to a Button: items with icons and shortcuts, checkbox and radio items, labels, separators, destructive variant. Arrow keys, typeahead, focus follows the pointer, Esc returns focus.", exports: ["MenuContext", "DropdownMenu", "DropdownMenuTrigger", "DropdownMenuContent", "DropdownMenuItem", "DropdownMenuCheckboxItem", "DropdownMenuRadioGroup", "DropdownMenuRadioItem", "DropdownMenuLabel", "DropdownMenuSeparator"], keywords: ["action", "actions", "align", "aligned", "also", "anchored", "and", "aria", "armed", "before", "blur", "both", "but", "button", "called", "can", "check", "checked", "children", "choice", "click", "clicks", "closes", "closing", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "disabled", "done", "down", "dropdownmenuradioitem", "edge", "emphasis", "enter", "equals", "esc", "feedback", "fill", "first", "focusable", "for", "form", "give", "group", "groups", "height", "hint", "icon", "ignore", "ignores", "irreversible", "item", "items", "its", "keeps", "keyboard", "label", "labels", "last", "leading", "left", "lines", "loading", "loadinglabel", "mark", "menu", "never", "new", "not", "old", "oncheckedchange", "onclick", "one", "ones", "onopenchange", "onselect", "onvaluechange", "open", "opens", "optionally", "out", "outside", "padding", "pair", "per", "picked", "picking", "place", "primary", "radiogroup", "red", "render", "reserved", "resets", "resize", "right", "runs", "second", "selectable", "selection", "separators", "shortcut", "shown", "shows", "size", "slides", "space", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "text", "the", "then", "this", "toggles", "trigger", "twice", "two", "uncontrolled", "value", "variant", "view", "when", "where", "while", "width", "with", "without"], registryDependencies: ["button", "list", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "empty", type: "registry:ui", title: "Empty", description: "Server-safe empty state: icon tile, title, one helpful sentence and the next action. Bordered variant for empty lists and drop zones.", exports: ["Empty"], keywords: ["about", "action", "and", "app", "area", "bordered", "children", "clips", "dashed", "description", "drop", "empty", "for", "helpful", "icon", "inside", "like", "list", "next", "one", "outline", "page", "rounded", "sentence", "shown", "state", "the", "tile", "title", "what", "yet", "zone"], registryDependencies: ["utils"] }, { name: "field", type: "registry:ui", title: "Field", description: "Label, control, description and error as one unit. Controls inside (Input, Textarea, Select, Checkbox, Switch, RadioGroup) get id, aria-describedby, aria-invalid and required automatically; errors spring in and are announced.", exports: ["useFieldControl", "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: "focus", type: "registry:lib", description: "What Tab reaches inside `root`, in order. Skips disabled and hidden fields, tabIndex -1 (a\nFormValue helper) and all but one radio per group, so the first and last are real stops.", exports: ["tabbables"], keywords: ["all", "and", "are", "but", "disabled", "fields", "first", "formvalue", "group", "helper", "hidden", "inside", "last", "one", "order", "per", "radio", "reaches", "real", "root", "skips", "stops", "tab", "tabindex", "the", "what"], registryDependencies: [] }, { name: "form-value", type: "registry:lib", description: "Submits a custom control's value and takes part in native validation, which a hidden input\ncan't: an empty `required` value blocks submit, shows the browser's message and focuses `anchor`.", exports: ["FormValue"], 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: ["focus", "use-controllable-state", "use-dismiss", "use-indicator", "utils"] }, { name: "newsletter", type: "registry:block", title: "Newsletter", description: "Email signup: InputGroup with the submit Button inside, native email validation, a loading → success toast and an in-place success state on the button.", exports: ["Newsletter"], keywords: ["above", "address", "after", "and", "button", "check", "confirm", "confirms", "description", "email", "field", "for", "form", "heading", "inbox", "inputgroup", "inside", "into", "its", "line", "loading", "message", "native", "next", "onsubscribe", "place", "placeholder", "rejection", "resets", "resolves", "saved", "shows", "signup", "submit", "submitlabel", "success", "successmessage", "supporting", "text", "that", "the", "then", "title", "toast", "turns", "under", "validation", "when", "with", "your"], registryDependencies: ["button", "input", "input-group", "toast", "utils"] }, { name: "number-input", type: "registry:ui", title: "Number Input", description: "Native number field with −/+ buttons: arrow keys, min/max/step validation and form submit from the platform; clamps on blur, rounds to the step's precision.", exports: ["NumberInput"], keywords: ["amount", "and", "are", "arrows", "arrowup", "buttons", "called", "cart", "change", "controlled", "defaultvalue", "down", "field", "first", "form", "helpers", "highest", "keyboard", "lowest", "max", "min", "native", "new", "number", "onvaluechange", "pair", "pointer", "quantity", "render", "step", "submit", "tabindex", "the", "uncontrolled", "use", "users", "validation", "value", "with"], registryDependencies: ["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
17252
17609
  (tabIndex 0) so arrow keys can scroll it, and it leaves the tab order again once it fits.
17253
- 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", "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", "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", "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,
17610
+ Renders a hidden span, so server components (Table) can use it as a child.`, exports: ["OverflowFocus"], keywords: ["again", "and", "arrow", "becomes", "can", "child", "components", "container", "drop", "fits", "focusable", "hidden", "inside", "keyboard", "keys", "leaves", "once", "order", "overflows", "renders", "scroll", "server", "span", "tab", "tabindex", "table", "the", "use", "while"], registryDependencies: [] }, { name: "pagination", type: "registry:ui", title: "Pagination", description: "Server-safe page links from page and total: previous/next, ellipsis gaps and a sliding active pill, with numbers that swipe the way you moved. Links by default; buttons with onPageChange. The row keeps a fixed width so it never jumps.", exports: ["Pagination", "pageRange"], keywords: ["active", "always", "and", "arrows", "bar", "become", "between", "buttons", "called", "changes", "client", "controls", "current", "default", "each", "ellipsis", "first", "for", "from", "gaps", "href", "index", "instead", "items", "its", "keeps", "kept", "key", "labels", "last", "length", "links", "mounted", "moved", "never", "next", "number", "numbers", "one", "only", "onpagechange", "page", "pagerange", "pages", "parents", "phones", "picked", "pill", "previous", "remounting", "render", "returns", "row", "same", "show", "siblings", "side", "slides", "slot", "stays", "swipe", "swipes", "text", "that", "the", "total", "unused", "url", "way", "width", "with", "without", "work", "you"], registryDependencies: ["button-variants", "pagination-motion", "utils"] }, { name: "pagination-motion", type: "registry:ui", description: "Scopes the active pill's `layoutId` so several paginations on a page don't share it.", exports: ["PaginationMotion", "PaginationActivePill", "PaginationNumber"], keywords: ["active", "behind", "changes", "children", "direction", "don", "from", "items", "its", "layoutid", "link", "moved", "next", "number", "outline", "page", "pagination", "paginations", "pill", "scopes", "several", "share", "side", "slot", "springs", "swipes", "that", "the", "toward", "travel", "value", "when"], registryDependencies: ["utils"] }, { name: "platform", type: "registry:lib", description: "False on the server and during hydration, true after: gates portals and other client-only output.", exports: ["useMounted", "useIsMac", "useMediaQuery"], keywords: ["after", "and", "assumes", "client", "corrects", "ctrl", "during", "false", "gates", "hydration", "live", "match", "media", "not", "only", "other", "output", "portals", "query", "render", "server", "shortcuts", "ssr", "the", "then", "true", "use", "where"], registryDependencies: [] }, { name: "popover", type: "registry:ui", title: "Popover", description: "Non-modal panel anchored to a Button for filters, pickers and quick edits. Focus moves in, Esc returns it, outside clicks close; Selects and menus opened inside don't count as outside.", exports: ["Popover", "PopoverTrigger", "PopoverContent"], keywords: ["above", "action", "align", "alignment", "also", "anchored", "and", "any", "aria", "armed", "before", "blur", "both", "button", "called", "can", "check", "children", "click", "clicks", "closed", "closes", "confirm", "confirmlabel", "confirmtimeout", "content", "controlled", "default", "defaultopen", "defaults", "destructive", "done", "edits", "emphasis", "end", "enter", "esc", "feedback", "field", "fill", "filters", "first", "flips", "focus", "focusable", "for", "form", "from", "give", "height", "icon", "ignore", "ignores", "irreversible", "its", "keep", "label", "left", "like", "loading", "loadinglabel", "modal", "moves", "never", "non", "not", "old", "onclick", "one", "ones", "onopenchange", "open", "out", "outside", "padding", "pair", "panel", "past", "per", "pickers", "place", "popovercontent", "popovertrigger", "popup", "preferred", "primary", "quick", "render", "reserved", "resets", "resize", "returns", "room", "runs", "second", "shift", "shown", "shows", "side", "size", "slides", "spinner", "spins", "square", "state", "status", "stays", "step", "submit", "success", "successlabel", "swap", "swaps", "tab", "text", "the", "there", "this", "trap", "trigger", "twice", "two", "uncontrolled", "unmount", "values", "variant", "view", "when", "where", "while", "width", "with"], registryDependencies: ["button", "focus", "platform", "use-anchor", "use-controllable-state", "use-dismiss", "utils"] }, { name: "pricing", type: "registry:block", title: "Pricing", description: "Plan cards with a monthly / yearly segmented switch. Prices roll on a spring in the direction they moved; the featured plan is outlined in the accent. Card, Badge, ToggleGroup, Button.", exports: ["Pricing"], keywords: ["and", "bcp", "billing", "cards", "code", "currency", "defaultperiod", "display", "down", "drop", "eur", "first", "for", "formatting", "iso", "locale", "monthly", "next", "number", "order", "period", "plan", "plans", "price", "prices", "render", "rise", "rolling", "segmented", "selected", "spring", "swap", "switch", "the", "they", "togglegroup", "when", "with", "yearly", "yearlynote"], registryDependencies: ["badge", "card", "toggle-group", "utils"] }, { name: "product-card", type: "registry:block", title: "Product Card", description: "Shop tile: photos in an AspectRatio (a swipeable Carousel with dots when there are several), sale price, Badge and add-to-cart Button.", exports: ["ProductCard"], keywords: ["add", "addlabel", "are", "aspectratio", "badge", "bcp", "become", "button", "called", "card", "carousel", "code", "compareat", "currency", "current", "dots", "for", "formatting", "heading", "height", "href", "img", "iso", "label", "link", "locale", "major", "media", "name", "new", "number", "onaddtocart", "one", "original", "over", "page", "per", "photos", "pressed", "price", "product", "ratio", "sale", "several", "shop", "shows", "slide", "small", "square", "struck", "swipeable", "text", "the", "there", "through", "tile", "units", "when", "width", "with"], registryDependencies: ["aspect-ratio", "badge", "button", "carousel", "utils"] }, { name: "progress", type: "registry:ui", title: "Progress", description: "Accent bar that springs to each new value, with an optional label and percentage. Leave out value for an indeterminate sweep (static under reduced motion).", exports: ["Progress"], keywords: ["above", "accent", "accessible", "across", "allowed", "also", "aria", "bar", "colored", "duration", "each", "for", "frame", "indeterminate", "infinite", "its", "label", "layout", "max", "motion", "name", "new", "next", "omit", "one", "per", "percentage", "reduced", "scalex", "segment", "show", "showvalue", "size", "springs", "static", "sweeps", "that", "the", "there", "thickness", "tween", "under", "unknown", "value", "visible", "when", "work"], registryDependencies: ["utils"] }, { name: "quote", type: "registry:ui", title: "Quote", description: "Semantic figure, blockquote and figcaption. Plain, card and large variants, with author, subtitle, avatar, lang, typewriter and textSwap.", exports: ["Quote"], keywords: ["accent", "alt", "and", "array", "author", "avatar", "blockquote", "card", "children", "correct", "custom", "cycles", "delay", "display", "doe", "effect", "enables", "fades", "figcaption", "figure", "for", "into", "jane", "job", "lang", "language", "large", "line", "mark", "options", "pass", "plain", "pronunciation", "quote", "quoted", "quotes", "reader", "said", "screen", "scrolls", "secondary", "semantic", "seo", "size", "slot", "speed", "spring", "string", "strings", "subtitle", "supports", "surface", "swap", "text", "textswap", "the", "through", "title", "transitions", "typewriter", "under", "variant", "view", "when", "who", "with", "work", "wrote"], registryDependencies: ["text-swap", "typewriter", "utils"] }, { name: "radio-group", type: "registry:ui", title: "Radio Group", description: "Native radios in a fieldset with a legend: browser arrow keys and form submit, a dot that springs in, optional per-option descriptions, vertical or horizontal.", exports: ["RadioGroup", "RadioGroupItem"], keywords: ["adds", "and", "application", "aria", "arrow", "browser", "but", "called", "children", "conform", "controlled", "defaultvalue", "description", "disabled", "does", "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", "focus", "use-controllable-state", "utils"] }, { name: "styles", type: "registry:file", exports: [], keywords: [], registryDependencies: [] }, { name: "switch", type: "registry:ui", title: "Forms", description: "Input, Textarea, Label and Switch: smooth focus rings, native :user-invalid validation (red only after interaction), aria-invalid, auto-growing textarea with an optional character count, and a spring switch.", exports: ["Switch"], keywords: ["called", "checkbox", "checked", "controlled", "defaultchecked", "first", "knob", "native", "new", "oncheckedchange", "pair", "render", "role", "spring", "state", "switch", "the", "uncontrolled", "with"], registryDependencies: ["field", "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,
17254
17611
  flipped when it doesn't fit, clamped to the viewport, kept in place on scroll and resize.
17255
17612
  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,
17256
17613
  ToggleGroup). Re-measures when the item or its row resizes (font swap, wrap), or the pill
@@ -17350,7 +17707,7 @@ async function fetchAllItems(source) {
17350
17707
  // jsr.json
17351
17708
  var jsr_default = {
17352
17709
  name: "@kznlabs/labsui",
17353
- version: "0.1.6",
17710
+ version: "0.1.8",
17354
17711
  license: "MIT",
17355
17712
  exports: "./src/index.ts",
17356
17713
  publish: {