{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auto-form",
  "title": "Auto Form",
  "description": "Render a complete, accessible form from a JSON Schema (or a zod schema through z.toJSONSchema): strings, numbers, booleans, enums, nested objects and arrays, with built-in validation and per-field overrides.",
  "registryDependencies": [
    "field",
    "input",
    "textarea",
    "native-select",
    "checkbox",
    "switch",
    "radio-group",
    "label",
    "button",
    "spinner",
    "@tealmac/form-store",
    "@tealmac/field-array"
  ],
  "files": [
    {
      "path": "registry/tealmac/auto-form.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n  FieldLegend,\n  FieldSet,\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { NativeSelect, NativeSelectOption } from \"@/components/ui/native-select\"\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport {\n  FieldArray,\n  FieldArrayRowActions,\n} from \"@/registry/tealmac/field-array\"\nimport {\n  getIn,\n  useFormSelector,\n  useFormStore,\n  type FormErrors,\n  type FormStore,\n  type SchemaLike,\n} from \"@/registry/tealmac/form-store\"\n\n/** The subset of JSON Schema AutoForm understands. `z.toJSONSchema(schema)` produces it. */\nexport interface JsonSchema {\n  type?: string | string[]\n  title?: string\n  description?: string\n  properties?: Record<string, JsonSchema>\n  required?: string[]\n  enum?: unknown[]\n  const?: unknown\n  items?: JsonSchema\n  format?: string\n  minLength?: number\n  maxLength?: number\n  minimum?: number\n  maximum?: number\n  exclusiveMinimum?: number\n  exclusiveMaximum?: number\n  multipleOf?: number\n  pattern?: string\n  minItems?: number\n  maxItems?: number\n  default?: unknown\n  anyOf?: JsonSchema[]\n  oneOf?: JsonSchema[]\n  readOnly?: boolean\n  [key: string]: unknown\n}\n\nexport type AutoFormWidget =\n  \"input\" | \"textarea\" | \"password\" | \"select\" | \"radio\" | \"switch\" | \"checkbox\"\n\nexport interface AutoFormFieldConfig {\n  label?: React.ReactNode\n  description?: React.ReactNode\n  placeholder?: string\n  widget?: AutoFormWidget\n  hidden?: boolean\n  disabled?: boolean\n  /** Labels for enum values: `{ pro: \"Pro plan\" }`. */\n  optionLabels?: Record<string, string>\n}\n\n/** What `onSubmit` may return to put errors on fields. Paths use dots: `address.city`. */\nexport interface AutoFormResult {\n  fieldErrors: Record<string, string[]>\n}\n\nexport interface AutoFormProps {\n  /** An object schema. Typed loosely so the output of `z.toJSONSchema()` fits. */\n  schema: JsonSchema | object\n  /** Config per field path without indexes: `\"address.city\"`, `\"phones.number\"`. */\n  fieldConfig?: Record<string, AutoFormFieldConfig>\n  defaultValues?: Record<string, unknown>\n  /** Use this instead of the built-in JSON Schema checks, for example a zod schema. */\n  validator?: SchemaLike\n  /**\n   * Called with cleaned values after validation passes. Return\n   * `{ fieldErrors: { \"email\": [\"Already registered.\"] } }` to show server\n   * errors on fields; throw to show a form-level message.\n   */\n  onSubmit: (\n    values: Record<string, unknown>\n  ) => void | AutoFormResult | Promise<void | AutoFormResult>\n  submitLabel?: string\n  pendingLabel?: string\n  /** Called with the cleaned values after every edit (not on first render). */\n  onValuesChange?: (values: Record<string, unknown>) => void\n  /** Render values as read-only controls and hide the submit button. */\n  readOnly?: boolean\n  className?: string\n  /** Extra content rendered above the submit button. */\n  children?: React.ReactNode\n}\n\n// ---------------------------------------------------------------- schema\n\nfunction isNull(s: JsonSchema) {\n  return s.type === \"null\"\n}\n\n/** Flatten nullable unions, type arrays and `oneOf` of constants into one schema. */\nexport function resolveSchema(schema: JsonSchema): JsonSchema {\n  const union = schema.anyOf ?? schema.oneOf\n  if (union) {\n    const rest = union.filter((s) => !isNull(s))\n    if (rest.length > 0 && rest.every((s) => s.const !== undefined)) {\n      return {\n        ...schema,\n        type: typeof rest[0].const === \"number\" ? \"number\" : \"string\",\n        enum: rest.map((s) => s.const),\n        \"x-labels\": Object.fromEntries(\n          rest.map((s) => [String(s.const), s.title ?? String(s.const)])\n        ),\n        anyOf: undefined,\n        oneOf: undefined,\n      }\n    }\n    if (rest.length === 1) return resolveSchema({ ...rest[0], ...pick(schema) })\n  }\n  if (Array.isArray(schema.type)) {\n    const type = schema.type.find((t) => t !== \"null\") ?? \"string\"\n    return { ...schema, type }\n  }\n  return schema\n}\n\nfunction pick(s: JsonSchema) {\n  const out: JsonSchema = {}\n  if (s.title) out.title = s.title\n  if (s.description) out.description = s.description\n  if (s.default !== undefined) out.default = s.default\n  return out\n}\n\nexport function humanize(key: string) {\n  const spaced = key\n    .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n    .replace(/[_-]+/g, \" \")\n    .trim()\n    .toLowerCase()\n  return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\nexport function defaultsFor(raw: JsonSchema): unknown {\n  const schema = resolveSchema(raw)\n  if (schema.default !== undefined) return schema.default\n  switch (schema.type) {\n    case \"object\": {\n      const out: Record<string, unknown> = {}\n      for (const [key, child] of Object.entries(schema.properties ?? {}))\n        out[key] = defaultsFor(child)\n      return out\n    }\n    case \"array\":\n      return []\n    case \"boolean\":\n      return false\n    case \"number\":\n    case \"integer\":\n      return undefined\n    default:\n      return \"\"\n  }\n}\n\nconst FORMATS: Record<string, [RegExp, string]> = {\n  email: [/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, \"Enter a valid email address.\"],\n  uri: [/^[a-z][a-z0-9+.-]*:\\/\\/\\S+$/i, \"Enter a valid URL.\"],\n  date: [/^\\d{4}-\\d{2}-\\d{2}$/, \"Enter a valid date.\"],\n}\n\n/** Validate a value against the supported keywords. Returns messages by path. */\nexport function validateJsonSchema(\n  raw: JsonSchema,\n  value: unknown,\n  path = \"\",\n  required = true,\n  out: FormErrors = {}\n): FormErrors {\n  const schema = resolveSchema(raw)\n  const add = (message: string) => {\n    ;(out[path] ??= []).push(message)\n  }\n  const empty = value === undefined || value === null || value === \"\"\n  switch (schema.type) {\n    case \"object\": {\n      const requiredKeys = new Set(schema.required ?? [])\n      for (const [key, child] of Object.entries(schema.properties ?? {}))\n        validateJsonSchema(\n          child,\n          (value as Record<string, unknown> | undefined)?.[key],\n          path ? `${path}.${key}` : key,\n          requiredKeys.has(key),\n          out\n        )\n      return out\n    }\n    case \"array\": {\n      const list = Array.isArray(value) ? value : []\n      if (schema.minItems !== undefined && list.length < schema.minItems)\n        add(`Add at least ${schema.minItems}.`)\n      if (schema.maxItems !== undefined && list.length > schema.maxItems)\n        add(`Add at most ${schema.maxItems}.`)\n      if (schema.items)\n        list.forEach((item, i) =>\n          validateJsonSchema(schema.items!, item, `${path}.${i}`, true, out)\n        )\n      return out\n    }\n    case \"boolean\":\n      if (required && value !== true && schema[\"x-must-be-true\"] === true)\n        add(\"This must be checked.\")\n      return out\n  }\n  if (empty) {\n    if (required && schema.type !== \"boolean\") add(\"This field is required.\")\n    return out\n  }\n  if (schema.type === \"number\" || schema.type === \"integer\") {\n    const n = Number(value)\n    if (!Number.isFinite(n)) return (add(\"Enter a number.\"), out)\n    if (schema.type === \"integer\" && !Number.isInteger(n))\n      add(\"Enter a whole number.\")\n    if (schema.minimum !== undefined && n < schema.minimum)\n      add(`Must be at least ${schema.minimum}.`)\n    if (schema.maximum !== undefined && n > schema.maximum)\n      add(`Must be at most ${schema.maximum}.`)\n    if (schema.exclusiveMinimum !== undefined && n <= schema.exclusiveMinimum)\n      add(`Must be greater than ${schema.exclusiveMinimum}.`)\n    if (schema.exclusiveMaximum !== undefined && n >= schema.exclusiveMaximum)\n      add(`Must be less than ${schema.exclusiveMaximum}.`)\n    if (schema.multipleOf && n % schema.multipleOf !== 0)\n      add(`Must be a multiple of ${schema.multipleOf}.`)\n  } else {\n    const text = String(value)\n    if (schema.minLength !== undefined && text.length < schema.minLength)\n      add(`Use at least ${schema.minLength} characters.`)\n    if (schema.maxLength !== undefined && text.length > schema.maxLength)\n      add(`Use at most ${schema.maxLength} characters.`)\n    const format = schema.format ? FORMATS[schema.format] : undefined\n    if (format && !format[0].test(text)) add(format[1])\n    else if (schema.pattern) {\n      try {\n        if (!new RegExp(schema.pattern).test(text))\n          add(\n            typeof schema[\"x-pattern-message\"] === \"string\"\n              ? schema[\"x-pattern-message\"]\n              : \"Use the requested format.\"\n          )\n      } catch {\n        /* an invalid pattern is ignored */\n      }\n    }\n  }\n  if (\n    schema.enum &&\n    !schema.enum.includes(value) &&\n    !schema.enum.map(String).includes(String(value))\n  )\n    add(\"Choose one of the options.\")\n  return out\n}\n\n/** Empty optional strings and missing numbers are dropped from the submitted object. */\nexport function cleanValues(\n  raw: JsonSchema,\n  value: unknown,\n  required = true\n): unknown {\n  const schema = resolveSchema(raw)\n  if (schema.type === \"object\") {\n    const requiredKeys = new Set(schema.required ?? [])\n    const out: Record<string, unknown> = {}\n    for (const [key, child] of Object.entries(schema.properties ?? {})) {\n      const cleaned = cleanValues(\n        child,\n        (value as Record<string, unknown> | undefined)?.[key],\n        requiredKeys.has(key)\n      )\n      if (cleaned !== undefined) out[key] = cleaned\n    }\n    return out\n  }\n  if (schema.type === \"array\")\n    return (Array.isArray(value) ? value : []).map((item) =>\n      cleanValues(schema.items ?? {}, item, true)\n    )\n  if (value === \"\" && !required) return undefined\n  if (\n    (schema.type === \"number\" || schema.type === \"integer\") &&\n    value !== undefined\n  )\n    return Number(value)\n  return value\n}\n\n// -------------------------------------------------------------------- UI\n\ninterface Ctx {\n  form: FormStore<Record<string, unknown>>\n  formId: string\n  config: Record<string, AutoFormFieldConfig>\n  readOnly: boolean\n}\nconst AutoCtx = React.createContext<Ctx | null>(null)\n\nfunction configFor(config: Ctx[\"config\"], path: string) {\n  return config[path.replace(/\\.\\d+(?=\\.|$)/g, \"\")] ?? {}\n}\n\nfunction widgetFor(\n  schema: JsonSchema,\n  cfg: AutoFormFieldConfig\n): AutoFormWidget {\n  if (cfg.widget) return cfg.widget\n  if (schema.enum) return \"select\"\n  if (schema.type === \"boolean\") return \"checkbox\"\n  if (schema.format === \"password\") return \"password\"\n  if (schema.type === \"string\" && (schema.maxLength ?? 0) > 160)\n    return \"textarea\"\n  return \"input\"\n}\n\nconst INPUT_TYPES: Record<string, string> = {\n  email: \"email\",\n  uri: \"url\",\n  date: \"date\",\n  \"date-time\": \"datetime-local\",\n  time: \"time\",\n}\n\nfunction AutoNode({\n  name,\n  path,\n  schema: raw,\n  required,\n}: {\n  name: string\n  path: string\n  schema: JsonSchema\n  required: boolean\n}) {\n  const ctx = React.useContext(AutoCtx)!\n  const schema = resolveSchema(raw)\n  const cfg = configFor(ctx.config, path)\n  if (cfg.hidden) return null\n  const label = cfg.label ?? schema.title ?? humanize(name)\n  const description = cfg.description ?? schema.description\n\n  if (schema.type === \"object\") {\n    return (\n      <FieldSet>\n        <FieldLegend variant=\"label\">{label}</FieldLegend>\n        {description ? (\n          <FieldDescription>{description}</FieldDescription>\n        ) : null}\n        <AutoProperties schema={schema} path={path} />\n      </FieldSet>\n    )\n  }\n  if (schema.type === \"array\") {\n    return (\n      <AutoArray\n        path={path}\n        schema={schema}\n        label={label}\n        description={description}\n      />\n    )\n  }\n  return (\n    <ctx.form.Field<unknown> name={path}>\n      {(field) => (\n        <AutoControl\n          id={`${ctx.formId}-${path}`}\n          schema={schema}\n          cfg={cfg}\n          label={label}\n          description={description}\n          required={required}\n          field={field}\n          readOnly={ctx.readOnly}\n        />\n      )}\n    </ctx.form.Field>\n  )\n}\n\nfunction AutoControl({\n  id,\n  schema,\n  cfg,\n  label,\n  description,\n  required,\n  field,\n  readOnly,\n}: {\n  id: string\n  schema: JsonSchema\n  cfg: AutoFormFieldConfig\n  label: React.ReactNode\n  description?: React.ReactNode\n  required: boolean\n  readOnly: boolean\n  field: {\n    value: unknown\n    errors: { message: string }[]\n    invalid: boolean\n    name: string\n    setValue: (v: unknown) => void\n    onBlur: () => void\n  }\n}) {\n  const widget = widgetFor(schema, cfg)\n  const disabled = cfg.disabled || schema.readOnly\n  const describedBy =\n    [\n      description ? `${id}-description` : null,\n      field.invalid ? `${id}-error` : null,\n    ]\n      .filter(Boolean)\n      .join(\" \") || undefined\n  const common = {\n    id,\n    name: field.name,\n    \"aria-invalid\": field.invalid || undefined,\n    \"aria-describedby\": describedBy,\n    \"aria-required\": required || undefined,\n    disabled,\n    onBlur: field.onBlur,\n  }\n  const asText = field.value == null ? \"\" : String(field.value)\n  const labels =\n    (schema[\"x-labels\"] as Record<string, string> | undefined) ?? {}\n  const labelOf = (v: unknown) =>\n    cfg.optionLabels?.[String(v)] ?? labels[String(v)] ?? humanize(String(v))\n  const isNumber = schema.type === \"number\" || schema.type === \"integer\"\n\n  const mark = required ? (\n    <span aria-hidden=\"true\" className=\"text-destructive\">\n      *\n    </span>\n  ) : null\n\n  let control: React.ReactNode\n  if (widget === \"checkbox\" || widget === \"switch\") {\n    const Box = widget === \"switch\" ? Switch : Checkbox\n    return (\n      <Field orientation=\"horizontal\" data-invalid={field.invalid || undefined}>\n        <Box\n          {...common}\n          checked={field.value === true}\n          onCheckedChange={(v: boolean | \"indeterminate\") =>\n            field.setValue(v === true)\n          }\n          disabled={disabled || readOnly}\n        />\n        <div className=\"flex flex-col gap-1\">\n          <FieldLabel htmlFor={id}>\n            {label}\n            {mark}\n          </FieldLabel>\n          {description ? (\n            <FieldDescription id={`${id}-description`}>\n              {description}\n            </FieldDescription>\n          ) : null}\n          {field.invalid ? (\n            <FieldError id={`${id}-error`} errors={field.errors} />\n          ) : null}\n        </div>\n      </Field>\n    )\n  }\n  if (widget === \"radio\" && schema.enum) {\n    return (\n      <FieldSet data-invalid={field.invalid || undefined}>\n        <FieldLegend variant=\"label\">\n          {label}\n          {mark}\n        </FieldLegend>\n        {description ? (\n          <FieldDescription id={`${id}-description`}>\n            {description}\n          </FieldDescription>\n        ) : null}\n        <RadioGroup\n          value={asText}\n          onValueChange={field.setValue}\n          disabled={disabled || readOnly}\n          aria-describedby={describedBy}\n          aria-invalid={field.invalid || undefined}\n          onBlur={field.onBlur}\n        >\n          {schema.enum.map((option) => (\n            <div key={String(option)} className=\"flex items-center gap-2\">\n              <RadioGroupItem\n                id={`${id}-${String(option)}`}\n                value={String(option)}\n              />\n              <Label htmlFor={`${id}-${String(option)}`}>\n                {labelOf(option)}\n              </Label>\n            </div>\n          ))}\n        </RadioGroup>\n        {field.invalid ? (\n          <FieldError id={`${id}-error`} errors={field.errors} />\n        ) : null}\n      </FieldSet>\n    )\n  }\n  if (widget === \"select\" && schema.enum) {\n    control = (\n      <NativeSelect\n        {...common}\n        className=\"w-full\"\n        value={asText}\n        disabled={disabled || readOnly}\n        onChange={(e) =>\n          field.setValue(\n            isNumber && e.target.value !== \"\"\n              ? Number(e.target.value)\n              : e.target.value\n          )\n        }\n      >\n        <NativeSelectOption value=\"\">\n          {cfg.placeholder ?? \"Select...\"}\n        </NativeSelectOption>\n        {schema.enum.map((option) => (\n          <NativeSelectOption key={String(option)} value={String(option)}>\n            {labelOf(option)}\n          </NativeSelectOption>\n        ))}\n      </NativeSelect>\n    )\n  } else if (widget === \"textarea\") {\n    control = (\n      <Textarea\n        {...common}\n        value={asText}\n        readOnly={readOnly}\n        placeholder={cfg.placeholder}\n        onChange={(e) => field.setValue(e.target.value)}\n      />\n    )\n  } else {\n    const type = isNumber\n      ? \"number\"\n      : widget === \"password\"\n        ? \"password\"\n        : (schema.format && INPUT_TYPES[schema.format]) || \"text\"\n    control = (\n      <Input\n        {...common}\n        type={type}\n        value={asText}\n        readOnly={readOnly}\n        placeholder={cfg.placeholder}\n        min={isNumber ? schema.minimum : undefined}\n        max={isNumber ? schema.maximum : undefined}\n        step={\n          isNumber\n            ? (schema.multipleOf ?? (schema.type === \"integer\" ? 1 : \"any\"))\n            : undefined\n        }\n        onChange={(e) =>\n          field.setValue(\n            isNumber\n              ? e.target.value === \"\"\n                ? undefined\n                : Number(e.target.value)\n              : e.target.value\n          )\n        }\n      />\n    )\n  }\n  return (\n    <Field data-invalid={field.invalid || undefined}>\n      <FieldLabel htmlFor={id}>\n        {label}\n        {mark}\n      </FieldLabel>\n      {control}\n      {description ? (\n        <FieldDescription id={`${id}-description`}>\n          {description}\n        </FieldDescription>\n      ) : null}\n      {field.invalid ? (\n        <FieldError id={`${id}-error`} errors={field.errors} />\n      ) : null}\n    </Field>\n  )\n}\n\nfunction AutoArray({\n  path,\n  schema,\n  label,\n  description,\n}: {\n  path: string\n  schema: JsonSchema\n  label: React.ReactNode\n  description?: React.ReactNode\n}) {\n  const ctx = React.useContext(AutoCtx)!\n  const value = useFormSelector(ctx.form, (s) => getIn(s.values, path)) as\n    unknown[] | undefined\n  const items = resolveSchema(schema.items ?? { type: \"string\" })\n  const singular = (schema.items?.title ?? \"item\").toLowerCase()\n  const shown = useFormSelector(ctx.form, (s) =>\n    s.submitCount > 0 ? ctx.form.errors[path] : undefined\n  )\n  return (\n    <>\n      <FieldArray\n        label={label}\n        description={description}\n        itemName={singular}\n        value={value ?? []}\n        onChange={(next) => ctx.form.setValue(path, next)}\n        createItem={() => defaultsFor(items)}\n        min={schema.minItems}\n        max={schema.maxItems}\n        addLabel={`Add ${singular}`}\n      >\n        {(row) => (\n          <>\n            <div className=\"min-w-0 flex-1\">\n              {items.type === \"object\" ? (\n                <AutoProperties schema={items} path={`${path}.${row.index}`} />\n              ) : (\n                <AutoNode\n                  name={String(singular)}\n                  path={`${path}.${row.index}`}\n                  schema={{\n                    ...items,\n                    title: `${humanize(singular)} ${row.index + 1}`,\n                  }}\n                  required\n                />\n              )}\n            </div>\n            {ctx.readOnly ? null : <FieldArrayRowActions />}\n          </>\n        )}\n      </FieldArray>\n      {shown?.length ? (\n        <FieldError errors={shown.map((message) => ({ message }))} />\n      ) : null}\n    </>\n  )\n}\n\nfunction AutoProperties({\n  schema,\n  path,\n}: {\n  schema: JsonSchema\n  path: string\n}) {\n  const required = new Set(schema.required ?? [])\n  const keys = Object.keys(schema.properties ?? {})\n  const order = (schema[\"x-order\"] as string[] | undefined) ?? keys\n  const ordered = [\n    ...order.filter((k) => keys.includes(k)),\n    ...keys.filter((k) => !order.includes(k)),\n  ]\n  return (\n    <>\n      {ordered.map((key) => (\n        <AutoNode\n          key={key}\n          name={key}\n          path={path ? `${path}.${key}` : key}\n          schema={schema.properties![key]}\n          required={required.has(key)}\n        />\n      ))}\n    </>\n  )\n}\n\n/**\n * A form generated from a JSON Schema. Values live in a form store, so the\n * behaviour is the same as a hand-written form: errors show after blur or\n * submit, the first invalid control gets focus, and `onSubmit` receives clean\n * values (empty optional strings dropped, numbers as numbers).\n */\nexport function AutoForm({\n  schema,\n  fieldConfig = {},\n  defaultValues,\n  validator,\n  onSubmit,\n  submitLabel = \"Submit\",\n  pendingLabel = \"Submitting...\",\n  readOnly = false,\n  onValuesChange,\n  className,\n  children,\n}: AutoFormProps) {\n  const formId = React.useId()\n  const root = React.useMemo(\n    () => resolveSchema(schema as JsonSchema),\n    [schema]\n  )\n  const initial = React.useMemo(\n    () => ({\n      ...(defaultsFor(root) as Record<string, unknown>),\n      ...defaultValues,\n    }),\n    [root, defaultValues]\n  )\n  const validate = React.useCallback(\n    (values: Record<string, unknown>): FormErrors => {\n      if (validator) {\n        const result = validator.safeParse(cleanValues(root, values))\n        if (result.success) return {}\n        const errors: FormErrors = {}\n        for (const issue of result.error.issues)\n          (errors[issue.path.map(String).join(\".\")] ??= []).push(issue.message)\n        return errors\n      }\n      return validateJsonSchema(root, values)\n    },\n    [root, validator]\n  )\n  const form = useFormStore<Record<string, unknown>>({\n    defaultValues: initial,\n    validators: { change: validate, blur: validate, submit: validate },\n    onSubmit: async ({ value }) => {\n      const result = await onSubmit(\n        cleanValues(root, value) as Record<string, unknown>\n      )\n      if (result && \"fieldErrors\" in result) form.setErrors(result.fieldErrors)\n    },\n  })\n  const values = useFormSelector(form, (s) => s.values)\n  const first = React.useRef(true)\n  const changeRef = React.useRef(onValuesChange)\n  changeRef.current = onValuesChange\n  React.useEffect(() => {\n    if (first.current) {\n      first.current = false\n      return\n    }\n    changeRef.current?.(cleanValues(root, values) as Record<string, unknown>)\n  }, [values, root])\n  const submitting = useFormSelector(form, (s) => s.submitting)\n  const submitError = useFormSelector(form, (s) => s.submitError)\n  const ctx = React.useMemo<Ctx>(\n    () => ({ form, formId, config: fieldConfig, readOnly }),\n    [form, formId, fieldConfig, readOnly]\n  )\n  return (\n    <AutoCtx.Provider value={ctx}>\n      <form\n        noValidate\n        onSubmit={form.handleSubmit}\n        className={className ?? \"flex flex-col gap-5\"}\n        aria-busy={submitting || undefined}\n      >\n        {root.title ? (\n          <h2 className=\"text-lg font-semibold\">{root.title}</h2>\n        ) : null}\n        <AutoProperties schema={root} path=\"\" />\n        {children}\n        {submitError ? (\n          <p role=\"alert\" className=\"text-destructive text-sm\">\n            {submitError}\n          </p>\n        ) : null}\n        {readOnly ? null : (\n          <Button type=\"submit\" disabled={submitting} className=\"self-start\">\n            {submitting ? <Spinner /> : null}\n            {submitting ? pendingLabel : submitLabel}\n          </Button>\n        )}\n      </form>\n    </AutoCtx.Provider>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}