{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "schema-builder",
  "title": "Schema Builder",
  "description": "Build the shape you want extracted from a document, with nesting.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/schema-builder/schema-builder.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight, Plus, Trash2 } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type SchemaFieldType =\n  \"string\" | \"number\" | \"boolean\" | \"date\" | \"object\" | \"array\"\n\nexport type SchemaField = {\n  id: string\n  name: string\n  type: SchemaFieldType\n  description?: string\n  required?: boolean\n  fields?: SchemaField[]\n}\n\nexport type SchemaBuilderProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"children\" | \"onChange\"\n> & {\n  fields?: SchemaField[]\n  defaultFields?: SchemaField[]\n  onFieldsChange?: (fields: SchemaField[]) => void\n  types?: readonly SchemaFieldType[]\n  maxDepth?: number\n  label?: string\n  addLabel?: string\n  createId?: () => string\n}\n\nconst DEFAULT_TYPES = [\n  \"string\",\n  \"number\",\n  \"boolean\",\n  \"date\",\n  \"object\",\n  \"array\",\n] as const\n\nfunction nests(type: SchemaFieldType) {\n  return type === \"object\" || type === \"array\"\n}\n\nfunction updateAt(\n  fields: SchemaField[],\n  id: string,\n  update: (field: SchemaField) => SchemaField | null\n): SchemaField[] {\n  return fields.flatMap((field) => {\n    if (field.id === id) {\n      const next = update(field)\n      return next ? [next] : []\n    }\n\n    if (!field.fields) return [field]\n\n    return [{ ...field, fields: updateAt(field.fields, id, update) }]\n  })\n}\n\nfunction FieldRow({\n  field,\n  depth,\n  maxDepth,\n  types,\n  addLabel,\n  onChange,\n  onRemove,\n  onAddChild,\n}: {\n  field: SchemaField\n  depth: number\n  maxDepth: number\n  types: readonly SchemaFieldType[]\n  addLabel: string\n  onChange: (id: string, patch: Partial<SchemaField>) => void\n  onRemove: (id: string) => void\n  onAddChild: (parentId: string) => void\n}) {\n  const reactId = React.useId()\n  const canNest = nests(field.type) && depth < maxDepth\n  const [open, setOpen] = React.useState(true)\n\n  return (\n    <li data-slot=\"schema-field\" data-type={field.type}>\n      <div className=\"border-border bg-background grid gap-2 rounded-[calc(var(--radius)-0.25rem)] border p-2.5 sm:grid-cols-[minmax(0,1fr)_auto_auto]\">\n        <div className=\"grid gap-1.5\">\n          <label className=\"sr-only\" htmlFor={`${reactId}-name`}>\n            Field name\n          </label>\n          <input\n            id={`${reactId}-name`}\n            className=\"placeholder:text-muted-foreground min-h-9 bg-transparent font-[family-name:var(--font-mono),monospace] text-sm font-semibold outline-none\"\n            placeholder=\"field_name\"\n            value={field.name}\n            onChange={(event) =>\n              onChange(field.id, { name: event.target.value })\n            }\n          />\n\n          <label className=\"sr-only\" htmlFor={`${reactId}-description`}>\n            Description for {field.name || \"this field\"}\n          </label>\n          <input\n            id={`${reactId}-description`}\n            className=\"placeholder:text-muted-foreground text-muted-foreground min-h-8 bg-transparent text-xs outline-none\"\n            placeholder=\"What should the model extract here?\"\n            value={field.description ?? \"\"}\n            onChange={(event) =>\n              onChange(field.id, { description: event.target.value })\n            }\n          />\n        </div>\n\n        <div className=\"flex items-start gap-2\">\n          <label className=\"sr-only\" htmlFor={`${reactId}-type`}>\n            Type for {field.name || \"this field\"}\n          </label>\n          <select\n            id={`${reactId}-type`}\n            className=\"border-border bg-background min-h-9 rounded-full border px-2.5 text-xs font-semibold\"\n            value={field.type}\n            onChange={(event) =>\n              onChange(field.id, {\n                type: event.target.value as SchemaFieldType,\n              })\n            }\n          >\n            {types.map((type) => (\n              <option key={type} value={type}>\n                {type}\n              </option>\n            ))}\n          </select>\n\n          <label className=\"text-muted-foreground inline-flex min-h-9 items-center gap-1.5 text-xs font-semibold\">\n            <input\n              checked={field.required ?? false}\n              type=\"checkbox\"\n              onChange={(event) =>\n                onChange(field.id, { required: event.target.checked })\n              }\n            />\n            Required\n          </label>\n        </div>\n\n        <div className=\"flex items-start gap-1\">\n          {canNest ? (\n            <button\n              type=\"button\"\n              aria-expanded={open}\n              className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-9 items-center justify-center rounded-full transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n              onClick={() => setOpen(!open)}\n            >\n              <span className=\"sr-only\">\n                {open ? \"Hide\" : \"Show\"} fields inside{\" \"}\n                {field.name || \"this field\"}\n              </span>\n              <ChevronRight\n                aria-hidden=\"true\"\n                size={14}\n                className={cn(\n                  \"transition-transform duration-150 motion-reduce:transition-none\",\n                  open && \"rotate-90\"\n                )}\n              />\n            </button>\n          ) : null}\n\n          <button\n            type=\"button\"\n            className=\"text-muted-foreground hover:text-destructive focus-visible:ring-ring inline-flex size-9 items-center justify-center rounded-full transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n            onClick={() => onRemove(field.id)}\n          >\n            <span className=\"sr-only\">Remove {field.name || \"this field\"}</span>\n            <Trash2 aria-hidden=\"true\" size={14} />\n          </button>\n        </div>\n      </div>\n\n      {canNest && open ? (\n        <div className=\"border-border mt-2 ml-3 border-l pl-3\">\n          <ul className=\"grid gap-2\">\n            {(field.fields ?? []).map((child) => (\n              <FieldRow\n                key={child.id}\n                addLabel={addLabel}\n                depth={depth + 1}\n                field={child}\n                maxDepth={maxDepth}\n                types={types}\n                onAddChild={onAddChild}\n                onChange={onChange}\n                onRemove={onRemove}\n              />\n            ))}\n          </ul>\n\n          <button\n            type=\"button\"\n            className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring mt-2 inline-flex min-h-9 items-center gap-1.5 rounded-full text-xs font-semibold transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n            onClick={() => onAddChild(field.id)}\n          >\n            <Plus aria-hidden=\"true\" size={13} />\n            {addLabel} inside {field.name || \"this field\"}\n          </button>\n        </div>\n      ) : null}\n    </li>\n  )\n}\n\nexport function SchemaBuilder({\n  fields,\n  defaultFields = [],\n  onFieldsChange,\n  types = DEFAULT_TYPES,\n  maxDepth = 3,\n  label = \"Extraction schema\",\n  addLabel = \"Add field\",\n  createId,\n  className,\n  ...rootProps\n}: SchemaBuilderProps) {\n  const [uncontrolled, setUncontrolled] =\n    React.useState<SchemaField[]>(defaultFields)\n  const value = fields ?? uncontrolled\n  const counter = React.useRef(0)\n\n  const nextId = () => {\n    if (createId) return createId()\n    counter.current += 1\n    return `field-${counter.current}-${value.length}`\n  }\n\n  const commit = (next: SchemaField[]) => {\n    if (fields === undefined) setUncontrolled(next)\n    onFieldsChange?.(next)\n  }\n\n  const change = (id: string, patch: Partial<SchemaField>) => {\n    commit(updateAt(value, id, (field) => ({ ...field, ...patch })))\n  }\n\n  const remove = (id: string) => {\n    commit(updateAt(value, id, () => null))\n  }\n\n  const addChild = (parentId: string) => {\n    commit(\n      updateAt(value, parentId, (field) => ({\n        ...field,\n        fields: [\n          ...(field.fields ?? []),\n          { id: nextId(), name: \"\", type: \"string\" as const },\n        ],\n      }))\n    )\n  }\n\n  return (\n    <section\n      data-slot=\"schema-builder\"\n      aria-label={label}\n      className={cn(\n        \"border-border bg-card text-card-foreground rounded-[var(--radius)] border p-3\",\n        className\n      )}\n      {...rootProps}\n    >\n      <ul className=\"grid gap-2\">\n        {value.map((field) => (\n          <FieldRow\n            key={field.id}\n            addLabel={addLabel}\n            depth={1}\n            field={field}\n            maxDepth={maxDepth}\n            types={types}\n            onAddChild={addChild}\n            onChange={change}\n            onRemove={remove}\n          />\n        ))}\n      </ul>\n\n      <button\n        type=\"button\"\n        data-slot=\"schema-builder-add\"\n        className=\"border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 focus-visible:ring-ring mt-2 inline-flex min-h-11 w-full items-center justify-center gap-1.5 rounded-[calc(var(--radius)-0.25rem)] border border-dashed text-sm font-semibold transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        onClick={() =>\n          commit([...value, { id: nextId(), name: \"\", type: \"string\" }])\n        }\n      >\n        <Plus aria-hidden=\"true\" size={15} />\n        {addLabel}\n      </button>\n    </section>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "document",
    "schema",
    "form",
    "ai"
  ],
  "type": "registry:ui"
}