# ReFormer CDK - LLM Integration Guide
# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.

> Headless UI components for @reformer/core - form arrays, multi-step wizards, and more
> Package: @reformer/cdk  •  Version: 6.0.0

## Table of Contents
- 01-overview.md — Overview
- 02-form-array.md — FormArray
- 03-form-navigation.md — FormWizard
- 04-form-field.md — FormField
- 05-recipes.md — Advanced Recipes
- 06-troubleshooting.md — Troubleshooting / FAQ
- API Reference (auto-generated from JSDoc)

## 1. Key Concepts

- **Headless**: No default UI or styles - you build the interface
- **Compound Components**: Composable, declarative API
- **Render Props**: Children as function for full control
- **Context-based**: State shared via React Context

## 2. Components

| Component    | Purpose                                      |
| ------------ | -------------------------------------------- |
| `FormArray`  | Manage dynamic form arrays                   |
| `FormField`  | Accessible field anatomy (label/control/…)   |
| `FormWizard` | Multi-step form wizard                        |

## 3. Installation

```bash
npm install @reformer/cdk @reformer/core
```

## 4. Import Patterns

```typescript
// All components
import { FormArray, FormField, FormWizard } from '@reformer/cdk';

// Tree-shaking (recommended)
import { FormArray, useFormArray } from '@reformer/cdk/form-array';
import { FormField, useFormField } from '@reformer/cdk/form-field';
import { FormWizard, useFormWizard } from '@reformer/cdk/form-wizard';
```

## 5. Basic Usage

```tsx
import { FormArray } from '@reformer/cdk/form-array';

<FormArray.Root control={form.items}>
  <FormArray.Empty>
    <p>No items added</p>
  </FormArray.Empty>

  <FormArray.List>
    {({ control, index, remove }) => (
      <div key={control.id}>
        <h4>Item #{index + 1}</h4>
        <ItemForm control={control} />
        <button onClick={remove}>Remove</button>
      </div>
    )}
  </FormArray.List>

  <FormArray.AddButton>Add Item</FormArray.AddButton>
</FormArray.Root>;
```

## 6. Sub-components

| Component                | Props                           | Purpose                            |
| ------------------------ | ------------------------------- | ---------------------------------- |
| `FormArray.Root`         | `control: ArrayNode<T>`         | Context provider                   |
| `FormArray.List`         | `children: (item) => ReactNode` | Iterates items (render props)      |
| `FormArray.AddButton`    | `initialValue?: Partial<T>`     | Adds new item                      |
| `FormArray.RemoveButton` | -                               | Removes current item (inside List) |
| `FormArray.Empty`        | `children: ReactNode`           | Shows when array is empty          |
| `FormArray.Count`        | `render?: (count) => ReactNode` | Displays item count                |
| `FormArray.ItemIndex`    | `render?: (index) => ReactNode` | Displays current index             |

## 7. List Render Props

```typescript
interface FormArrayItemRenderProps<T> {
  control: FormProxy<T>; // Form control for item
  index: number; // Zero-based index
  id: string | number; // Unique key
  remove: () => void; // Remove this item
  moveUp: () => void; // Move one position up (no-op when first)
  moveDown: () => void; // Move one position down (no-op when last)
  canMoveUp: boolean; // index > 0
  canMoveDown: boolean; // index < length - 1
}
```

Reorder helpers (`moveUp` / `moveDown`) preserve item state — the underlying
`ArrayNode.move` reorders controls without recreating them.

## 8. Typed item access (avoid `FormProxy<object>`)

`FormArray.List` gets its `control` from `FormArrayContext`, which is typed
`any` (it has to accept any element type). The generic `T` you write on the
JSX tag is **not** inferred through React context, so a bare `<FormArray.List>`
defaults `T` to `object` and the render-prop `control` is `FormProxy<object>`.
Accessing item fields then fails to compile:

```tsx
<FormArray.List>
  {({ control: item }) => (
    // ❌ TS2339: Property 'type' does not exist on type 'FormProxy<object>'
    <FormField control={item.type} />
  )}
</FormArray.List>
```

The compound (`FormArray.Root` / `FormArray.List`) is **headless and
render-prop-untyped by design**. For typed access to item fields, use one of
the paths below. Prefer `FormArraySection` (canonical) or `useFormArray` — both
infer the element type, so you cannot forget the annotation.

### Canonical: ui-kit `FormArraySection` + typed `itemComponent`

The typed, styled array UI lives in `@reformer/ui-kit/form-array`, not in the
CDK compound. Its `itemComponent` FC receives a fully typed
`control: FormProxy<T>` (T inferred from `control`), so item fields resolve:

```tsx
import { FormArraySection } from '@reformer/ui-kit/form-array';
import type { FormProxy } from '@reformer/core';

const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
  <>
    <FormField control={control.type} /> {/* ✅ typed */}
    <FormField control={control.estimatedValue} />
  </>
);

<FormArraySection
  control={form.properties} // FormArrayProxy<Property>
  itemComponent={PropertyForm}
  title="Properties"
  initialValue={blankProperty()}
/>;
```

Full prop list, RenderSchema and JSON variants: `find_recipe(topic="form-array-section")`.

### CDK-native typed: `useFormArray<T>` hook

If you need the headless hook (custom layout, no ui-kit dependency),
`useFormArray(control)` infers `T` from the array control — every
`item.control` is `FormProxy<T>`, no explicit type argument required:

```tsx
function ExistingLoansList({ control }: { control: FormProxy<Application> }) {
  const { items, add } = useFormArray(control.existingLoans); // T = ExistingLoan inferred

  return (
    <>
      {items.map(({ control: item, id, remove }) => (
        <div key={id}>
          <FormField control={item.bank} /> {/* ✅ typed */}
          <FormField control={item.amount} />
          <button onClick={remove}>×</button>
        </div>
      ))}
      <button onClick={() => add(blankLoan())}>+ Add</button>
    </>
  );
}
```

### Staying on the compound: pin `FormArray.List<T>`

The JSX tag accepts an explicit type argument, which restores the item type:

```tsx
<FormArray.List<Property>>
  {({ control: item }) => <FormField control={item.type} />} {/* ✅ typed */}
</FormArray.List>
```

You must repeat `<T>` on **every** `FormArray.List` — miss one and it silently
falls back to `FormProxy<object>`. When you access item fields directly, reach
for `FormArraySection` or `useFormArray` instead.

## 9. External Control via Ref

```tsx
import { useRef } from 'react';
import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';

const arrayRef = useRef<FormArrayHandle<ItemType>>(null);

// Control from outside
arrayRef.current?.add({ name: 'New' });
arrayRef.current?.removeAt(0);
arrayRef.current?.move(2, 0);
arrayRef.current?.clear();

<FormArray.Root ref={arrayRef} control={form.items}>
  ...
</FormArray.Root>;
```

## 10. FormArrayHandle API

```typescript
interface FormArrayHandle<T> {
  add: (value?: Partial<T>) => void;
  clear: () => void;
  insert: (index: number, value?: Partial<T>) => void;
  removeAt: (index: number) => void;
  move: (from: number, to: number) => void; // reorder, state preserved
  swap: (a: number, b: number) => void; // swap two items, state preserved
  length: number;
  isEmpty: boolean;
  at: (index: number) => FormProxy<T> | undefined;
}
```

> `length` / `isEmpty` are a snapshot at render time. For a reactive length
> outside the array, subscribe via `useFormControl(form.items).length`.

## 11. useFormArray Hook

For full customization without compound components:

```tsx
import { useFormArray } from '@reformer/cdk/form-array';

function CustomList() {
  const { items, add, isEmpty, length } = useFormArray(form.items);

  return (
    <div>
      <span>Total: {length}</span>
      {items.map(({ control, id, remove }) => (
        <div key={id}>
          <ItemForm control={control} />
          <button onClick={remove}>X</button>
        </div>
      ))}
      {isEmpty && <p>Empty</p>}
      <button onClick={() => add()}>Add</button>
    </div>
  );
}
```

### UseFormArrayReturn

```typescript
interface UseFormArrayReturn<T> {
  items: FormArrayItem<T>[]; // { control, index, id, remove }
  length: number;
  isEmpty: boolean;
  add: (value?: Partial<T>) => void; // push to end
  clear: () => void; // remove all
  insert: (index: number, value?: Partial<T>) => void;
  move: (from: number, to: number) => void; // reorder, state preserved
  swap: (a: number, b: number) => void; // swap two items, state preserved
}
```

Note: `items[]` is memoized by array length AND order — changing a value inside
an item does not recreate the `items` array, but `move` / `swap` do (order ref changes).

## 12. Basic Usage

```tsx
import { FormWizard, type FormWizardConfig } from '@reformer/cdk/form-wizard';

// Config is a pair of validation callbacks — NOT schemas.
// Each returns boolean | Promise<boolean> (true = valid).
const config: FormWizardConfig = {
  validateStep: (step) => validateFormModel(model, stepSchema(step)),
  validateAll: () => validateFormModel(model, fullSchema),
};

<FormWizard form={form} config={config}>
  <FormWizard.Step component={Step1Form} control={form} />
  <FormWizard.Step component={Step2Form} control={form} />

  <FormWizard.Actions onSubmit={handleSubmit}>
    {({ prev, next, submit, isFirstStep, isLastStep }) => (
      <div>
        {!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}
        {!isLastStep ? (
          <button onClick={next.onClick} disabled={next.disabled}>Next</button>
        ) : (
          <button onClick={submit.onClick} disabled={submit.disabled}>Submit</button>
        )}
      </div>
    )}
  </FormWizard.Actions>
</FormWizard>;
```

> Note: `prev`/`next`/`submit` are `{ onClick, disabled }` objects (submit also has
> `isSubmitting`). They are not DOM-attribute bags — do not `{...prev}`-spread them
> onto `<button>` (that would set an invalid `disabled`/`onClick` mix). Read the
> members explicitly as shown, or use the compound `FormWizard.Prev/Next/Submit`.

## 13. Sub-components

| Component              | Purpose                                                |
| ---------------------- | ------------------------------------------------------ |
| `FormWizard`           | Root provider                                          |
| `FormWizard.Step`      | Renders component/children when step is current        |
| `FormWizard.Indicator` | Headless step indicator (render props)                 |
| `FormWizard.Actions`   | Navigation container: render props OR compound buttons |
| `FormWizard.Prev`      | Compound "Back" button (inside `Actions`)              |
| `FormWizard.Next`      | Compound "Next" button (inside `Actions`)              |
| `FormWizard.Submit`    | Compound "Submit" button (inside `Actions`)            |
| `FormWizard.Progress`  | Headless progress display (render props)               |

`FormWizard.Prev` / `Next` / `Submit` are compound children of `FormWizard.Actions`
(they read the Actions context) — they cannot be used standalone. They are not
top-level exports; access them as members: `FormWizard.Prev`, etc.

### Actions: compound mode

```tsx
<FormWizard.Actions onSubmit={handleSubmit} className="flex justify-between">
  <FormWizard.Prev>Back</FormWizard.Prev>
  <FormWizard.Next>Next</FormWizard.Next>
  <FormWizard.Submit loadingText="Submitting…">Submit</FormWizard.Submit>
</FormWizard.Actions>
```

## 14. FormWizard.Indicator

```tsx
<FormWizard.Indicator steps={STEPS}>
  {({ steps, goToStep, currentStep }) => (
    <nav>
      {steps.map((step) => (
        <button
          key={step.number}
          onClick={() => goToStep(step.number)}
          disabled={!step.canNavigate}
          aria-current={step.isCurrent ? 'step' : undefined}
        >
          {step.isCompleted ? '✓' : step.number} {step.title}
        </button>
      ))}
    </nav>
  )}
</FormWizard.Indicator>
```

### Step Definition

```typescript
interface FormWizardIndicatorStep {
  number: number; // 1-based step number
  title: string;
  icon?: string;
}
```

### Render Props

```typescript
interface FormWizardIndicatorRenderProps {
  steps: FormWizardIndicatorStepWithState[];
  goToStep: (step: number) => boolean;
  currentStep: number;
  totalSteps: number;
  completedSteps: number[];
}

interface FormWizardIndicatorStepWithState {
  number: number;
  title: string;
  icon?: string;
  isCurrent: boolean;
  isCompleted: boolean;
  canNavigate: boolean;
}
```

## 15. FormWizard.Actions

```tsx
<FormWizard.Actions onSubmit={handleSubmit}>
  {({ prev, next, submit, isFirstStep, isLastStep, isValidating }) => (
    <div>
      {!isFirstStep && (
        <button onClick={prev.onClick} disabled={prev.disabled}>
          Back
        </button>
      )}
      {!isLastStep ? (
        <button onClick={next.onClick} disabled={next.disabled}>
          {isValidating ? 'Validating...' : 'Next'}
        </button>
      ) : (
        <button onClick={submit.onClick} disabled={submit.disabled}>
          {submit.isSubmitting ? 'Submitting...' : 'Submit'}
        </button>
      )}
    </div>
  )}
</FormWizard.Actions>
```

### Render Props

```typescript
interface FormWizardActionsRenderProps {
  prev: { onClick: () => void; disabled: boolean };
  next: { onClick: () => void; disabled: boolean };
  submit: { onClick: () => void; disabled: boolean; isSubmitting: boolean };
  isFirstStep: boolean;
  isLastStep: boolean;
  isValidating: boolean;
  isSubmitting: boolean;
}
```

## 16. FormWizard.Progress

```tsx
<FormWizard.Progress>
  {({ current, total, percent }) => (
    <div>
      Step {current} of {total} ({percent}%)
      <div style={{ width: `${percent}%` }} />
    </div>
  )}
</FormWizard.Progress>
```

### Render Props

```typescript
interface FormWizardProgressRenderProps {
  current: number;
  total: number;
  percent: number;
  completedCount: number;
  isFirstStep: boolean;
  isLastStep: boolean;
}
```

## 17. External Control via Ref

```tsx
const navRef = useRef<FormWizardHandle<FormType>>(null);

// Programmatic navigation
navRef.current?.goToStep(2);
navRef.current?.goToNextStep();
navRef.current?.goToPreviousStep();

// Submit with validation
const result = await navRef.current?.submit(async (values) => {
  return api.submit(values);
});

<FormWizard ref={navRef} form={form} config={config}>
  ...
</FormWizard>;
```

## 18. Configuration

`FormWizardConfig` is a pair of optional validation callbacks (not generic, not
schema-based). Each returns `boolean | Promise<boolean>` — `true` means valid.

```typescript
interface FormWizardConfig {
  /** Validate step N (1-based). Missing → step treated as valid (warns in console). */
  validateStep?: (step: number) => boolean | Promise<boolean>;
  /** Validate the whole form before submit. Missing → submit not blocked. */
  validateAll?: () => boolean | Promise<boolean>;
}
```

Typically both wrap `validateFormModel(model, schema)` from `@reformer/core` and
close over the current step's schema:

```typescript
const config: FormWizardConfig = {
  validateStep: async (step) => {
    const res = await validateFormModel(model, stepSchemas[step - 1]);
    return Object.keys(res.errors).length === 0;
  },
  validateAll: async () => {
    const res = await validateFormModel(model, fullSchema);
    return Object.keys(res.errors).length === 0;
  },
};
```

Validation happens automatically:

- On `next.onClick` / `FormWizard.Next`: runs `validateStep(currentStep)`; on failure calls `form.markAsTouched()` and stays on the step.
- On `submit` (ref `submit()` or `FormWizard.Actions` `onSubmit`): runs `validateAll()`; on success delegates to `form.submit(onSubmit, { skipValidation: true })`.

> The higher-level `@reformer/ui-kit` `FormWizard` accepts the same `config`.

## 19. Purpose

- Дать минимальный «скелет» поля: label + control + description + error.
- Не навязывать стилей — каждый sub-компонент рендерит обычные HTML-элементы (или `Slot` через `asChild`).
- Подписаться на `FieldNode` ровно один раз в `Root` и раздать состояние детям через React Context.
- Гарантировать корректные ARIA-атрибуты в нетривиальных сценариях (multi-error, отсутствие label, async pending).

В отличие от `FormField` из `@reformer/ui-kit`, который рендерит готовый layout (label сверху, ошибка снизу), `FormField` из `@reformer/cdk` ничего не рендерит сверх минимально необходимого и нужен, когда требуется собственный layout.

## 20. Components

| Component                    | Purpose                                                                                                                                                               | Notes                                                                                                                         |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `FormField.Root`             | Context provider; принимает `control: FieldNode<T>` и опциональный `id`/`hasDescription`.                                                                             | Подписывается на `useFormControl(control)` один раз. Без `Root` дети бросают исключение.                                      |
| `FormField.Label`            | `<label>` с автоматическим `htmlFor`. Текст по умолчанию из `componentProps.label`. Required-индикатор `*` добавляется при `required`.                                | Возвращает `null`, если нет ни `componentProps.label`, ни `children`. Используйте `forceRender` чтобы рендерить пустой label. |
| `FormField.Control`          | Auto-renders `control.component` со всеми пропсами и a11y-атрибутами. С `asChild`/`children` — вмёрживает a11y-атрибуты в произвольный дочерний элемент через `Slot`. | Auto-mode прокидывает `componentProps`, `value`, `disabled`, `onChange`, `onBlur`.                                            |
| `FormField.Error`            | `<p role="alert">` с `errors[0].message`. Поддерживает `multi`, `render`, кастомные `children`.                                                                       | Не рендерится, пока `shouldShowError === false` (поле не touched / нет ошибок).                                               |
| `FormField.Description`      | `<p>` с стабильным `id={ids.descriptionId}` для `aria-describedby`.                                                                                                   | Чтобы `Control` автоматически прописал `aria-describedby`, передайте `hasDescription` в `Root`.                               |
| `useFormFieldContext<T>()`   | Хук для произвольных дочерних компонентов, которым нужен `control`, `value`, `errors`, `ids`, `componentProps`.                                                       | Бросает `Error`, если вызван вне `FormField.Root`.                                                                            |
| `useFormField(control, id?)` | Standalone hook без compound API. Возвращает `labelProps`, `controlProps`, `errorProps`, `descriptionProps`, `state`, `actions`, `ids`.                               | Удобен, когда нужен полный контроль над DOM-структурой и пропсами.                                                            |

## 21. Examples

### Базовый сценарий — auto-render всех частей

Минимум кода: `Label` и `Control` сами берут текст / компонент из конфига поля, `Error` прячется до touch.

```tsx
import { FormField } from '@reformer/cdk/form-field';

function EmailField({ control }: { control: typeof form.email }) {
  return (
    <FormField.Root control={control}>
      <FormField.Label />
      <FormField.Control />
      <FormField.Error />
    </FormField.Root>
  );
}
```

`Label` рендерит текст из `componentProps.label`, `Control` — компонент, заданный через `component:` в схеме формы (`Input`, `InputPassword`, `Select`...).

### Custom layout — обёртки и стилизация

Когда требуется горизонтальный layout, иконка слева и helper-текст:

```tsx
<FormField.Root control={form.email} hasDescription>
  <div className="grid grid-cols-[120px_1fr] items-start gap-3">
    <FormField.Label className="pt-2 text-sm font-medium text-gray-700" />

    <div className="space-y-1">
      <FormField.Control asChild>
        <Input type="email" leftIcon={<MailIcon />} className="w-full" />
      </FormField.Control>
      <FormField.Description className="text-xs text-gray-500">
        Мы не передаём email третьим сторонам.
      </FormField.Description>
      <FormField.Error className="text-xs text-red-600" />
    </div>
  </div>
</FormField.Root>
```

Передача `hasDescription` обязательна для того, чтобы `Control` прописал `aria-describedby={descriptionId}`.

### Async-валидация с pending-индикатором

Состояние асинхронной валидации (`pending`) доступно через `useFormFieldContext()`. Удобно показать спиннер или disabled у submit-кнопки.

```tsx
import { FormField, useFormFieldContext } from '@reformer/cdk/form-field';

function PendingDot() {
  const { pending } = useFormFieldContext();
  if (!pending) return null;
  return <Spinner size="sm" aria-label="Проверяем..." />;
}

<FormField.Root control={form.username}>
  <div className="flex items-center gap-2">
    <FormField.Label />
    <PendingDot />
  </div>
  <FormField.Control />
  <FormField.Error multi className="text-xs text-red-600" />
</FormField.Root>;
```

`multi` рендерит все ошибки из `errors[]` (например, async-валидатор может вернуть и «слишком короткое имя», и «уже занято»). Первая ошибка получит `id={errorId}` для `aria-errormessage`.

### Интеграция с готовым `FormField` из `@reformer/ui-kit`

`@reformer/ui-kit` экспортирует свой `FormField`, который собран на этих compound-блоках. В большинстве форм его достаточно — без необходимости опускаться на уровень CDK:

```tsx
import { FormField } from '@reformer/ui-kit';

<form>
  <FormField control={form.username} className="mb-4" />
  <FormField control={form.email} className="mb-4" />
</form>;
```

Если нужен один кастомный кейс среди типовых — комбинируйте: `FormField` из ui-kit для большинства полей и `FormField.Root` из cdk для нестандартного:

```tsx
import { FormField } from '@reformer/ui-kit';
import { FormField as FieldRoot } from '@reformer/cdk/form-field';

<>
  <FormField control={form.email} />
  <FieldRoot.Root control={form.captcha}>
    <FieldRoot.Label />
    <div className="flex items-center gap-2">
      <FieldRoot.Control asChild>
        <Input className="flex-1" />
      </FieldRoot.Control>
      <CaptchaImage />
    </div>
    <FieldRoot.Error />
  </FieldRoot.Root>
</>;
```

## 22. Anti-patterns

- **Использовать `FormField.Label` / `Error` / `Control` без `FormField.Root`.** Каждый дочерний компонент вызывает `useFormFieldContext()` и бросает: `FormField.* components must be used within <FormField.Root>`.
- **Подписываться на `useFormControl(control)` рядом с `FormField.Root`.** `Root` уже подписан — лишняя подписка приведёт к двойному ререндеру. Используйте `useFormFieldContext()` для доступа к состоянию.
- **Передавать `id` руками в `Control` / `Label`.** ID назначаются автоматически из `useId()`. Если нужен предсказуемый ID для тестов, передайте `id="my-field"` в `Root` — все потомки получат `control-my-field`, `label-my-field`, …
- **Забывать `hasDescription` при наличии `FormField.Description`.** Без флага `Control` не пропишет `aria-describedby={descriptionId}`, и screen reader не зачитает helper-текст.
- **Двойное рендерание ошибки (`Error` + ручной `<p>`).** `FormField.Error` уже подписан на `errors`/`shouldShowError`. Если нужен кастомный layout — используйте `render` prop, а не дублируйте.
- **Применять `asChild` к компоненту, который не пробрасывает `ref`/`...props`.** `Slot` объединяет пропсы и ref в дочерний элемент; если потомок их не принимает, `aria-*`-атрибуты потеряются.

## 23. Troubleshooting

- **`Error: FormField.* components must be used within <FormField.Root>`.** Проверьте, что вызов `FormField.Label` / `Control` / `Error` обёрнут в `FormField.Root` и компонент не рендерится в портале выше провайдера.
- **`Label` ничего не показывает.** В схеме поля нет `componentProps.label`. Вариант: задайте `label` в схеме, или передайте `children` в `FormField.Label`, или поставьте `forceRender`.
- **`Control` рендерит «голый» `<input>` без стилей.** Auto-mode рендерит `control.component` — убедитесь, что в схеме указан компонент (`component: Input`). Иначе используйте `asChild` + свой компонент.
- **`aria-describedby` пустой при наличии `Description`.** Не передан `hasDescription` в `Root`. Это не «магический» флаг — без него `Control` не знает, что description есть в дереве.
- **`FormField.Error` не появляется при наличии ошибки.** Поле не помечено как touched. Используйте `form.markAsTouched()` или `control.markAsTouched()` перед сабмитом, либо настройте `revalidateWhen` чтобы помечать touched по `change`.
- **При async-валидации индикатор моргает.** `pending` переключается на каждый `setValue`. Дебаунсьте источник или добавьте задержку перед показом спиннера (например, `useDeferredValue`).
- **Дубликаты `id` в DOM.** Несколько `FormField.Root` с одинаковым явным `id`. Опустите `id` (тогда работает `useId()`) или дайте уникальные значения.

## 24. See also

- [01-overview.md](01-overview.md) — общее введение в `@reformer/cdk`.
- [02-form-array.md](02-form-array.md), [03-form-navigation.md](03-form-navigation.md) — соседние compound-компоненты.
- [05-recipes.md](05-recipes.md) — продвинутые паттерны (включая собственный wrapper над FormField).
- [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки FormField/FormArray/FormWizard.
- [src/components/form-field/](../../src/components/form-field/) — исходники compound-блоков и `useFormField`.

## 25. Nested FormArray

### Problem

В одной форме нужно несколько уровней вложенности массивов: например, `properties[]` (имущество клиента) и внутри каждого — `coOwners[]` (совладельцы). Контролы вложенных массивов — это `ArrayNode<T>` внутри `FormProxy<Property>`, и стандартного `FormArray.Root + List` достаточно, чтобы рендерить любую глубину.

### Solution

```tsx
import { FormArray } from '@reformer/cdk/form-array';
import { FormField } from '@reformer/ui-kit';

<FormArray.Root control={form.properties}>
  <FormArray.List className="space-y-4">
    {({ control: property, index, remove }) => (
      <fieldset className="border rounded p-4">
        <legend className="flex justify-between w-full">
          <span>Имущество #{index + 1}</span>
          <button type="button" onClick={remove}>
            ×
          </button>
        </legend>

        <FormField control={property.address} />
        <FormField control={property.estimatedValue} />

        <h4 className="mt-4 mb-2">Совладельцы</h4>
        <FormArray.Root control={property.coOwners}>
          <FormArray.List className="space-y-2">
            {({ control: owner, index: ownerIdx, remove: removeOwner }) => (
              <div className="flex gap-2">
                <FormField control={owner.fullName} className="flex-1" />
                <FormField control={owner.share} className="w-24" />
                <button type="button" onClick={removeOwner}>
                  ×
                </button>
              </div>
            )}
          </FormArray.List>
          <FormArray.AddButton className="btn-secondary mt-2">
            + Добавить совладельца
          </FormArray.AddButton>
        </FormArray.Root>
      </fieldset>
    )}
  </FormArray.List>
  <FormArray.AddButton className="btn-primary">+ Добавить имущество</FormArray.AddButton>
</FormArray.Root>;
```

### Notes

- Каждый `FormArray.Root` создаёт собственный `FormArrayContext`. Внутренний `useFormArrayContext()` (например, в `AddButton`) видит ближайший провайдер — в примере выше `coOwners`, не `properties`.
- `FormArray.List` мемоизирует элементы по `length` массива (см. `useFormArray`); смена порядка/количества внутреннего массива не дёргает внешний `List`.
- `useFormArrayItemContext()` внутри вложенного `List` вернёт **внутренний** item (`coOwner`), не внешний. Если нужен индекс внешнего элемента, забирайте его в замыкании render-функции (`index`, `property`).
- Рендер-проп `FormArray.List` стабилен по identity: вынесите тяжёлые шаги в отдельный компонент, чтобы получить React.memo-эффект.

## 26. Custom AddButton

### Problem

`FormArray.AddButton` рендерит обычный `<button>` (или Slot через `asChild`). Иногда нужно совсем другое UI: dropdown с выбором типа, drag-drop файлов, шаблоны заготовок. Самый чистый путь — обойти compound-компонент и вызвать `useFormArrayContext()` напрямую.

### Solution

```tsx
import { useFormArrayContext } from '@reformer/cdk/form-array';
import { Menu } from '@/ui';

function AddPropertyMenu() {
  const { add } = useFormArrayContext<Property>();

  return (
    <Menu>
      <Menu.Trigger className="btn-primary">+ Добавить имущество ▾</Menu.Trigger>
      <Menu.Content>
        <Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
        <Menu.Item onSelect={() => add({ type: 'house', estimatedValue: 0 })}>Дом</Menu.Item>
        <Menu.Item onSelect={() => add({ type: 'commercial' })}>Коммерческое</Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

<FormArray.Root control={form.properties}>
  <FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
  <AddPropertyMenu />
</FormArray.Root>;
```

### Notes

- Хук работает только внутри `FormArray.Root` — снаружи бросает `Error: FormArray.* components must be used within FormArray.Root`.
- `add(value?: Partial<T>)` пропускает значение в `control.push(value)` — не задавать поля можно, ReFormer возьмёт значения по умолчанию из схемы.
- Если кастомный триггер живёт **снаружи** `Root` (например, в шапке страницы), используйте ref-handle: `useRef<FormArrayHandle<T>>` + `arrayRef.current?.add(...)`.
- Для batch-добавления нескольких элементов вызывайте `add` в цикле — каждый push триггерит ререндер `List` (через `length`-зависимость в `useFormArray`).

## 27. Conditional / dynamic step count in FormWizard

### Problem

`FormWizard` считает количество шагов через `Children.forEach` по `FormWizard.Step` детям (см. `FormWizard.tsx`). Если шаг условный (например, «верификация документов» нужна только для суммы > 1 000 000), достаточно условного рендера: `{showVerification && <FormWizard.Step ... />}`. `totalSteps` пересчитается, индикатор сожмётся.

### Solution

```tsx
import { useMemo } from 'react';
import { useFormControl } from '@reformer/core';
import { FormWizard, type FormWizardConfig } from '@reformer/cdk/form-wizard';

function CreditWizard({ form, stepSchemas, fullSchema }: Props) {
  const { value: amount } = useFormControl(form.amount);
  const needsVerification = amount > 1_000_000;

  // config — пара колбэков validateStep / validateAll (не схемы, не generic).
  // step (1-based) роутится в нужную схему; при выключенной верификации
  // нумерация сдвигается — учитываем это в самом колбэке.
  const config = useMemo<FormWizardConfig>(
    () => ({
      validateStep: (step) => {
        const schema = needsVerification
          ? stepSchemas.withVerification[step - 1]
          : stepSchemas.withoutVerification[step - 1];
        return validateFormModel(form.model, schema ?? { children: [] }).then(
          (r) => Object.keys(r.errors).length === 0
        );
      },
      validateAll: () =>
        validateFormModel(form.model, fullSchema).then(
          (r) => Object.keys(r.errors).length === 0
        ),
    }),
    [needsVerification, stepSchemas, fullSchema, form]
  );

  return (
    <FormWizard form={form} config={config}>
      <FormWizard.Step component={AmountForm} control={form} />
      <FormWizard.Step component={PersonalForm} control={form} />
      <FormWizard.Step component={ContactForm} control={form} />
      {needsVerification && <FormWizard.Step component={VerificationForm} control={form} />}
      <FormWizard.Step component={ConfirmationForm} control={form} />

      <FormWizard.Actions onSubmit={handleSubmit}>
        {({ prev, next, submit, isLastStep }) => (
          <div>
            <button onClick={prev.onClick} disabled={prev.disabled}>Назад</button>
            {isLastStep ? (
              <button onClick={submit.onClick} disabled={submit.disabled}>Подтвердить</button>
            ) : (
              <button onClick={next.onClick} disabled={next.disabled}>Далее</button>
            )}
          </div>
        )}
      </FormWizard.Actions>
    </FormWizard>
  );
}
```

### Notes

- **Номера шагов сдвигаются** при включении/выключении: `validateStep(4)` должен разрешаться либо в схему верификации, либо в схему подтверждения — в зависимости от `needsVerification`. Держите колбэк `validateStep`/`validateAll` в `useMemo` от того же флага, иначе он замкнётся на устаревшую нумерацию.
- `currentStep` в state `FormWizard` хранится как число. Если флаг изменился пока пользователь стоит на шаге 4 (где раньше была верификация, а теперь подтверждение), он останется на том же номере — но рендер увидит уже другой `Step`. В критичных кейсах вызывайте `navRef.current?.goToStep(1)` после переключения.
- `completedSteps` — массив номеров; при изменении общей нумерации логика «можно ли перейти на шаг N» (`step === 1 || completedSteps.includes(step - 1)`) может отметить шаг как доступный без валидации. Скиньте `completedSteps` через перемонтирование `FormWizard` (key={needsVerification}) если важна строгость.
- Children-формы рендерятся условно `_stepIndex === currentStep` (см. `FormWizardStep.tsx`); неактивные `Step` возвращают `null`, состояние их полей живёт в `form` независимо от рендера.

## 28. Externally-controlled wizard via `useRef<FormWizardHandle>`

### Problem

Кнопка «Сохранить и выйти» лежит вне `FormWizard` (в шапке страницы), нужен программный submit. Аналогично — переход на шаг по клику в стороннем breadcrumb или после ответа из API.

### Solution

```tsx
import { useRef } from 'react';
import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';

function Page({ form, config }: Props) {
  const navRef = useRef<FormWizardHandle<CreditApplication>>(null);

  const handleSaveAndExit = async () => {
    // submit с полной валидацией; null если форма невалидна
    const result = await navRef.current?.submit(async (values) => {
      return api.saveDraft(values);
    });
    if (result) router.push('/dashboard');
  };

  const jumpToContacts = () => {
    const ok = navRef.current?.goToStep(3);
    if (!ok) toast('Сначала заполните предыдущие шаги');
  };

  return (
    <>
      <header className="flex justify-between p-4 border-b">
        <button onClick={jumpToContacts}>Перейти к контактам</button>
        <button onClick={handleSaveAndExit}>Сохранить и выйти</button>
      </header>

      <FormWizard ref={navRef} form={form} config={config}>
        <FormWizard.Step component={Step1} control={form} />
        <FormWizard.Step component={Step2} control={form} />
        <FormWizard.Step component={Step3} control={form} />
      </FormWizard>
    </>
  );
}
```

### Notes

- `FormWizardHandle` exposes: `currentStep`, `completedSteps`, `goToNextStep` (с валидацией), `goToPreviousStep`, `goToStep` (boolean — true если переход разрешён), `submit`, `validateCurrentStep`, `isFirstStep`, `isLastStep`, `isValidating`, `form`.
- `submit(onSubmit)` сначала прогоняет `config.validateAll?.()` (если колбэк задан), затем `form.markAsTouched()` если invalid, иначе делегирует в `form.submit(onSubmit, { skipValidation: true })`. Возвращает `R | null`. `null` — форма не прошла валидацию.
- `goToStep(n)` возвращает `false`, если `n > totalSteps`, `n < 1`, или предыдущий шаг (`n - 1`) не в `completedSteps` (исключение — сам шаг 1). Используйте `await goToNextStep()` чтобы пройти вперёд с валидацией.
- Ref становится `null` пока компонент не смонтировался. Все вызовы — `navRef.current?.method()` с optional chaining, либо проверка `if (!navRef.current) return`.

## 29. See also

- [02-form-array.md](02-form-array.md) — основы FormArray (compound API, ref-handle).
- [03-form-navigation.md](03-form-navigation.md) — основы FormWizard (Indicator, Actions, Progress).
- [04-form-field.md](04-form-field.md) — компоновка одного поля.
- [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки и пути их обхода.

## 30. `FormArray.AddButton` не появляется на странице

**Причина.** `FormArray.AddButton` не самостоятельный элемент — он рендерит кнопку только внутри `FormArray.Root`. Если он вынесен наружу или `Root` не отрендерился (например, под условным `if`), кнопки не будет.

**Решение.** Поместите `AddButton` в дерево `Root` либо вызывайте `useFormArrayContext().add()` из своей кнопки (см. [05-recipes.md → Custom AddButton](05-recipes.md)).

```tsx
<FormArray.Root control={form.items}>
  <FormArray.List>{renderItem}</FormArray.List>
  <FormArray.AddButton>+ Добавить</FormArray.AddButton>
</FormArray.Root>
```

## 31. `FormArray.List` ререндерит весь массив при изменении одного элемента

**Причина.** Render-функция массива возвращает inline JSX, который не мемоизирован. Изменение поля у элемента триггерит ререндер `FormArray.Root` (через `useFormControl(arrayNode)`), а List-children получают новые ссылки.

**Решение.** Вынесите рендер элемента в отдельный компонент, обёрнутый в `React.memo`. Передавайте `control` (стабильный по identity на протяжении жизни элемента) и `id` для key.

```tsx
const Item = React.memo(({ control }: { control: FormProxy<Property> }) => (
  <PropertyForm control={control} />
));

<FormArray.List>{({ control, id }) => <Item key={id} control={control} />}</FormArray.List>;
```

`useFormArray` мемоизирует `items` по длине массива (см. `useFormArray.ts`), поэтому смена значения внутри элемента не пересоздаёт `items`-массив.

## 32. `FormWizardHandle.goToStep` возвращает `false`

**Причина.** `goToStep(n)` пускает только если `n === 1` или `n - 1` уже в `completedSteps` (см. `FormWizard.tsx:goToStep`). Это защита от пропуска валидации. Также `false` возвращается при `n < 1` или `n > totalSteps`.

**Решение.**

- Для последовательного перехода вперёд используйте `await navRef.current?.goToNextStep()` — он сам валидирует и помечает шаг completed.
- Для произвольного «прыжка» предварительно отметьте предыдущий шаг как completed через `goToNextStep()` или вручную (через свой `useState` поверх).
- Проверьте `n` в диапазоне `[1; totalSteps]` (totalSteps = количество `FormWizard.Step` детей).

## 33. Шаг без валидации пропускается без проверки

**Причина.** `validateCurrentStep` вызывает `config.validateStep(currentStep)`. Если колбэк `validateStep` вообще не задан в `config` — выводит `console.warn` и возвращает `true` (шаг считается валидным). Актуальный `FormWizardConfig` — это пара колбэков, а не словарь схем:

```typescript
interface FormWizardConfig {
  validateStep?: (step: number) => boolean | Promise<boolean>;
  validateAll?: () => boolean | Promise<boolean>;
}
```

**Решение.** Задайте `validateStep`, который сам роутит по номеру шага в нужную схему. Для чисто информационного шага (например, «Подтверждение») верните `true`.

```typescript
const stepSchemas = [amountSchema, personalSchema, confirmationSchema];

const config: FormWizardConfig = {
  validateStep: async (step) => {
    const schema = stepSchemas[step - 1];
    if (!schema) return true; // нет схемы для шага — считаем валидным
    const res = await validateFormModel(model, schema);
    return Object.keys(res.errors).length === 0;
  },
  validateAll: async () => {
    const res = await validateFormModel(model, fullSchema);
    return Object.keys(res.errors).length === 0;
  },
};
```

## 34. `useFormArrayContext` бросает исключение

**Сообщение.** `Error: FormArray.* components must be used within FormArray.Root or RenderSchema FormArray`.

**Причина.** Хук вызван вне дерева `FormArray.Root`. Часто — в компоненте, который рендерится через портал (Modal, Tooltip), либо рядом с `Root`, а не внутри.

**Решение.** Поместите потребителя внутрь `FormArray.Root`. Для портала — оберните потребителя ещё одним `Root` с тем же `control`, либо используйте ref-handle `FormArrayHandle` снаружи.

## 35. `useFormFieldContext` бросает исключение

**Сообщение.** `Error: FormField.* components must be used within <FormField.Root>`.

**Причина.** Тот же сценарий: `FormField.Label` / `Control` / `Error` без обёрнутого `FormField.Root`. Также возникает, если `Root` рендерит `null` (например, когда поле скрыто `enableWhen`) — Label/Error всё равно бросят, если рендерятся параллельно.

**Решение.** Перепроверьте дерево; если поле условное — выносите всю секцию в `if (!visible) return null;` снаружи `FormField.Root`.

## 36. `FormField.Error` не показывает ошибку, хотя `errors.length > 0`

**Причина.** Поле не помечено как touched, `shouldShowError === false`. По умолчанию ReFormer показывает ошибки только после blur/submit.

**Решение.**

- На сабмите формы вызовите `form.markAsTouched()` перед `await form.validate()`.
- Для немедленной валидации поля используйте `revalidateWhen({ when: 'change' })` behavior, либо вручную `control.markAsTouched()` в `onChange`.

```tsx
const handleSubmit = async () => {
  form.markAsTouched();
  await form.validate();
  if (form.valid.value) {
    /* submit */
  }
};
```

## 37. `FormField.Description` есть в DOM, но `aria-describedby` пустой

**Причина.** В `FormField.Root` не передан проп `hasDescription`. Без него `Control` не вписывает `descriptionId` в `aria-describedby` (это сделано осознанно — чтобы избежать двойного рендера от динамической регистрации).

**Решение.** Установите `hasDescription` в `Root` явно.

```tsx
<FormField.Root control={form.email} hasDescription>
  <FormField.Label />
  <FormField.Control />
  <FormField.Description>Hint</FormField.Description>
</FormField.Root>
```

## 38. Multi-step submit срабатывает раньше валидации последнего шага

**Причина.** `FormWizard.Actions` render-prop `submit.onClick` вызывает переданный в `Actions` `onSubmit` сразу при клике. Если в этом обработчике идёт `navRef.current?.submit(...)`, он запустит `config.validateAll?.()` поверх всей формы (а не только последнего шага). Однако touched-флаги ставятся только на полях с ошибками, и пользователю может быть неочевидно, где именно проблема.

**Решение.** В кастомном submit-обработчике сначала вызывайте `validateCurrentStep`, затем `submit`. Кнопка `submit` в render-props отключается (`submit.disabled === true`) только на время `isValidating`/`isSubmitting`, поэтому явная проверка шага полезна.

```tsx
const handleSubmit = async () => {
  const stepOk = await navRef.current?.validateCurrentStep();
  if (!stepOk) return;
  await navRef.current?.submit(api.submit);
};
```

## 39. `FormArray.List` теряет focus или ререндерит inputs при `add`/`removeAt`

**Причина.** Используется `index` в качестве React-key. После `removeAt(0)` индексы сдвигаются, React переиспользует DOM-узлы для других данных — фокус «переезжает».

**Решение.** Используйте `id` из item-render-props, который ReFormer присваивает каждому элементу при `push`/`insert`:

```tsx
<FormArray.List>
  {({ control, id, remove }) => (
    <div key={id}>
      {' '}
      {/* ✓ стабильный id */}
      <ItemForm control={control} />
    </div>
  )}
</FormArray.List>
```

Сам `FormArray.List` уже использует `item.id` для ключа `<FormArrayItemContext.Provider key={item.id}>` — но если внутренний контейнер тоже задаёт key, поставьте `id`, а не `index`.

## 40. `FormWizard` показывает «No validateStep callback configured for step N» в консоли

**Причина.** `validateCurrentStep` warn'ит, если в `config` не задан колбэк `validateStep` (тогда шаг считается валидным без проверки).

**Решение.** Передайте `validateStep(step)` в `config` (см. раздел «Шаг без валидации…» выше). Если конкретный шаг намеренно без валидации, внутри колбэка верните `true` для его номера.

## 41. Ref `FormArrayHandle.length` / `isEmpty` не обновляется

**Причина.** Эти поля snapshot'ятся в `useImperativeHandle` от `arrayState` и пересоздаются при каждом ререндере. Если вы храните `arrayRef.current` в замыкании или в `useEffect` без зависимостей, можете прочитать устаревшее значение.

**Решение.** Читайте `arrayRef.current.length` непосредственно в обработчике, не кешируйте. Для реактивной длины снаружи используйте `useFormControl(form.items).length` — это даст подписку.

## 42. See also

- [01-overview.md](01-overview.md), [04-form-field.md](04-form-field.md), [05-recipes.md](05-recipes.md).
- [@reformer/core troubleshooting](../../../reformer/docs/llms/) — общие проблемы с валидацией и behaviors.

## 43. API Reference

_Auto-generated from JSDoc on public exports._

### createMessageResolver

**Kind:** `function`

Создаёт резолвер из таблицы кодов сообщений (точка i18n). Текст берётся по `error.code`
с подстановкой `error.params`; если кода нет в таблице — fallback на `error.message`, затем
на сам `error.code`. Позволяет валидаторам нести только `code`+`params`, а тексты (RU/EN/…)
держать в таблице и менять без правки схемы валидации.

**Signature:**
```typescript
export function createMessageResolver(table: ValidationMessageTable): ValidationErrorResolver
```

**Parameters:**
- `table` — - Таблица `code → (params) => message`.

**Returns:** Резолвер {@link ValidationErrorResolver} для {@link ValidationMessagesProvider}.

**Examples:**

Таблица сообщений с подстановкой параметров
```ts
import { createMessageResolver } from '@reformer/cdk';

const resolve = createMessageResolver({
required: () => 'Обязательное поле',
minLength: (p) => `Минимум ${p?.minLength} символов`,
});

resolve({ code: 'required', message: 'старое' });                        // → 'Обязательное поле'
resolve({ code: 'minLength', message: '', params: { minLength: 3 } });   // → 'Минимум 3 символов'
resolve({ code: 'pattern', message: 'Неверный формат' });               // → 'Неверный формат' (fallback)
```

**See also:**
- {@link ValidationMessagesProvider} — как подключить резолвер к поддереву формы.

_Source: src/validation/error-resolver.tsx_

### defaultErrorResolver

**Kind:** `const`

Дефолтный резолвер: отдаёт `error.message`, а если оно пустое — `error.code`. Применяется, когда
форма не обёрнута в {@link ValidationMessagesProvider}. Обеспечивает обратную совместимость:
пока валидаторы несут готовые тексты в `message`, отображение не меняется.

**Signature:**
```typescript
export const defaultErrorResolver: ValidationErrorResolver
```

**Examples:**

Прямое применение к ошибке
```ts
import { defaultErrorResolver } from '@reformer/cdk';

defaultErrorResolver({ code: 'required', message: 'Обязательно' }); // → 'Обязательно'
defaultErrorResolver({ code: 'required', message: '' });            // → 'required'
```

_Source: src/validation/error-resolver.tsx_

### FormArray

**Kind:** `const`

FormArray - Headless compound component for managing form arrays

Provides complete flexibility for building array UI while handling
all the form array state and actions internally.

#### Features
- **Headless** - complete freedom in building UI
- **Compound Components** - declarative API via nested components
- **External Control** - control from outside via ref (useImperativeHandle)
- **Type Safe** - full TypeScript support

#### Sub-components
- `FormArray.Root` - context provider, accepts ref for external control
- `FormArray.List` - iterates over array items
- `FormArray.AddButton` - button to add item
- `FormArray.RemoveButton` - button to remove item (inside List)
- `FormArray.Empty` - content for empty state
- `FormArray.Count` - display item count
- `FormArray.ItemIndex` - display item index (inside List)

#### FormArrayHandle API (ref)
- `add(value?)` - add item to the end
- `insert(index, value?)` - insert item at position
- `removeAt(index)` - remove item by index
- `move(from, to)` - reorder item (state preserved)
- `swap(a, b)` - swap two items (state preserved)
- `clear()` - clear array
- `at(index)` - get item control by index
- `length` - current item count
- `isEmpty` - empty array flag

**Signature:**
```typescript
export const FormArray
```

**Examples:**

Basic usage
```tsx
<FormArray.Root control={form.properties}>
<h3>Properties (<FormArray.Count />)</h3>

<FormArray.Empty>
<p className="text-gray-500">No properties added</p>
</FormArray.Empty>

<FormArray.List className="space-y-4">
{({ control }) => (
<div className="p-4 border rounded">
<div className="flex justify-between mb-2">
 <h4>Property #<FormArray.ItemIndex /></h4>
 <FormArray.RemoveButton className="text-red-500">
   Remove
 </FormArray.RemoveButton>
</div>
<PropertyForm control={control} />
</div>
)}
</FormArray.List>

<FormArray.AddButton className="mt-4 btn-primary">
+ Add Property
</FormArray.AddButton>
</FormArray.Root>
```

External control via ref
```tsx
import { useRef } from 'react';
import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';

function PropertiesManager() {
const arrayRef = useRef<FormArrayHandle<Property>>(null);

// Programmatic control from outside
const handleAddApartment = () => {
arrayRef.current?.add({ type: 'apartment', estimatedValue: 0 });
};

const handleClearAll = () => {
if (confirm('Delete all items?')) {
arrayRef.current?.clear();
}
};

const handleRemoveFirst = () => {
if (arrayRef.current && arrayRef.current.length > 0) {
arrayRef.current.removeAt(0);
}
};

const handleInsertAtStart = () => {
arrayRef.current?.insert(0, { type: 'house' });
};

return (
<div>
<div className="toolbar">
<button onClick={handleAddApartment}>+ Apartment</button>
<button onClick={handleInsertAtStart}>Insert at start</button>
<button onClick={handleRemoveFirst}>Remove first</button>
<button onClick={handleClearAll}>Clear all</button>
</div>

<FormArray.Root ref={arrayRef} control={form.properties}>
<FormArray.List>
 {({ control }) => <PropertyForm control={control} />}
</FormArray.List>
</FormArray.Root>
</div>
);
}
```

Using useFormArray hook for full customization
```tsx
import { useFormArray } from '@reformer/cdk/form-array';

function CustomArrayUI() {
const { items, add, isEmpty, length } = useFormArray(form.properties);

return (
<div>
<span>Total: {length}</span>
{items.map(({ control, id, remove }) => (
<CustomCard key={id} onDelete={remove}>
 <PropertyForm control={control} />
</CustomCard>
))}
{isEmpty && <EmptyState />}
<button onClick={() => add()}>Add</button>
</div>
);
}
```

_Source: src/components/form-array/FormArray.tsx_

### FormArrayAddButton

**Kind:** `const`

`FormArray.AddButton` — кнопка добавления нового элемента в массив.

Рендерит `<button type="button">` (или произвольный элемент через `asChild`),
по клику вызывает `add(initialValue)` из контекста {@link FormArrayContext},
то есть `ArrayNode.push`. Должна находиться внутри `FormArray.Root` (или
эквивалентного провайдера), иначе `useFormArrayContext` бросит исключение.

Generic `T` — тип элемента массива. По умолчанию `Record<string, unknown>` (широкий).
Передавайте его явно, если нужна type-safe проверка `initialValue`:
`<FormArray.AddButton<PropertyItem> initialValue={...}>`.

**Signature:**
```typescript
export const FormArrayAddButton
```

**Examples:**

Внутри compound-разметки FormArray
```tsx
<FormArray.Root control={form.properties}>
<FormArray.List>
{({ control }) => <PropertyForm control={control} />}
</FormArray.List>

<FormArray.AddButton className="btn-primary">
+ Добавить объект
</FormArray.AddButton>
</FormArray.Root>
```

С типизированным initialValue
```tsx
<FormArray.AddButton<Property> initialValue={{ type: 'apartment', estimatedValue: 0 }}>
+ Квартира
</FormArray.AddButton>
```

Своя кнопка через asChild (props мержатся в дочерний элемент)
```tsx
<FormArray.AddButton asChild initialValue={{ name: '' }}>
<MyButton variant="primary">+ Добавить</MyButton>
</FormArray.AddButton>
```

_Source: src/components/form-array/FormArrayAddButton.tsx_

### FormArrayAddButtonProps

**Kind:** `interface`

Props for FormArray.AddButton component

Generic `T` — тип элемента массива. По умолчанию `Record<string, unknown>` (широкий) —
для совместимости. Для type-safe initialValue передавайте generic явно
(`<FormArray.AddButton<PropertyItem> ...>`) либо проксируйте через
`FormArraySection<T>` из `@reformer/ui-kit`.

**Signature:**
```typescript
export interface FormArrayAddButtonProps<T extends object = Record<string, unknown>> extends Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  'onClick'
> {
  /** Начальное значение нового элемента (передаётся в `add()` → `ArrayNode.push`) */
  initialValue?: Partial<T>;
  /** Рендерить как дочерний элемент через Slot (props мержатся в children вместо `<button>`) */
  asChild?: boolean;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayContext

**Kind:** `const`

React context, который снабжает дочерние компоненты `FormArray` (List, AddButton, …)
текущим `ArrayNode` и хелперами. Создаётся `FormArray.Root`. Читать через {@link useFormArrayContext}.

**Signature:**
```typescript
export const FormArrayContext
```

**Examples:**

```tsx
import { FormArrayContext } from '@reformer/cdk/form-array';

function MyConsumer() {
  const ctx = useContext(FormArrayContext);
  return <span>items: {ctx?.items.length}</span>;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### FormArrayContextValue

**Kind:** `interface`

Контекст уровня массива

**Signature:**
```typescript
export interface FormArrayContextValue<T extends object = Record<string, unknown>> {
  /** Массив элементов с контролами и действиями */
  items: FormArrayItem<T>[];
  /** Текущая длина массива */
  length: number;
  /** Пустой ли массив */
  isEmpty: boolean;
  /** Добавить новый элемент в конец */
  add: (value?: Partial<T>) => void;
  /** Удалить все элементы */
  clear: () => void;
  /** Вставить элемент на указанную позицию */
  insert: (index: number, value?: Partial<T>) => void;
  /** Переместить элемент (реордер, состояние сохраняется) */
  move: (from: number, to: number) => void;
  /** Поменять местами два элемента (реордер, состояние сохраняется) */
  swap: (a: number, b: number) => void;
  /** Оригинальный ArrayNode */
  control: ArrayNode<T>;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### FormArrayCount

**Kind:** `function`

FormArray.Count - Displays the number of items in the array

**Signature:**
```typescript
export function FormArrayCount({ render }: FormArrayCountProps)
```

**Examples:**

Basic usage
```tsx
<h3>Items (<FormArray.Count />)</h3>
```

With custom render
```tsx
<FormArray.Count render={(count) => (
count === 0 ? 'No items' : `${count} item${count > 1 ? 's' : ''}`
)} />
```

_Source: src/components/form-array/FormArrayCount.tsx_

### FormArrayCountProps

**Kind:** `interface`

Props for FormArray.Count component

**Signature:**
```typescript
export interface FormArrayCountProps {
  /** Custom render function for the count */
  render?: (count: number) => ReactNode;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayEmpty

**Kind:** `function`

FormArray.Empty - Renders children only when array is empty

**Signature:**
```typescript
export function FormArrayEmpty({ children }: FormArrayEmptyProps)
```

**Examples:**

Basic usage
```tsx
<FormArray.Empty>
<p className="text-gray-500">No items added yet</p>
</FormArray.Empty>
```

With call to action
```tsx
<FormArray.Empty>
<div className="text-center p-8">
<p>No properties</p>
<FormArray.AddButton>Add your first property</FormArray.AddButton>
</div>
</FormArray.Empty>
```

_Source: src/components/form-array/FormArrayEmpty.tsx_

### FormArrayEmptyProps

**Kind:** `interface`

Props for FormArray.Empty component

**Signature:**
```typescript
export interface FormArrayEmptyProps {
  /** Content to show when array is empty */
  children: ReactNode;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayHandle

**Kind:** `interface`

Handle exposed via ref for external control of {@link FormArray}.

Имперо-API для случаев, когда триггер находится вне дерева `FormArray.Root`
(тулбар страницы, диалог подтверждения, async-эффект). Получают через
`useRef<FormArrayHandle<T>>(null)` и передают в `<FormArray.Root ref={...}>`.

Свойства `length` / `isEmpty` — снимок на момент рендера. Реактивную длину
для условного UI снаружи берите через `useFormControl(arrayNode).length`.

**Signature:**
```typescript
export interface FormArrayHandle<T extends object> {
  /** Add a new item to the end of the array */
  add: (value?: Partial<T>) => void;
  /** Remove all items from the array */
  clear: () => void;
  /** Insert a new item at a specific index */
  insert: (index: number, value?: Partial<T>) => void;
  /** Remove item at specific index */
  removeAt: (index: number) => void;
  /** Move an item from one index to another (reorder, state preserved) */
  move: (from: number, to: number) => void;
  /** Swap two items by index (reorder, state preserved) */
  swap: (a: number, b: number) => void;
  /** Current number of items */
  length: number;
  /** Whether the array is empty */
  isEmpty: boolean;
  /** Get item control at specific index */
  at: (index: number) => FormProxy<T> | undefined;
}
```

**Examples:**

Тулбар «Добавить / Очистить» поверх массива
```tsx
import { useRef } from 'react';
import { FormArray, type FormArrayHandle } from '@reformer/cdk/form-array';

function PropertiesEditor({ form }: Props) {
const arrayRef = useRef<FormArrayHandle<Property>>(null);

return (
<>
<div className="toolbar">
<button onClick={() => arrayRef.current?.add({ type: 'apartment' })}>
 + Квартира
</button>
<button onClick={() => arrayRef.current?.add({ type: 'house' })}>
 + Дом
</button>
<button onClick={() => confirm('Удалить всё?') && arrayRef.current?.clear()}>
 Очистить
</button>
</div>
<FormArray.Root ref={arrayRef} control={form.properties}>
<FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
</FormArray.Root>
</>
);
}
```

Импорт массива из API: insert + at для проверки дублей
```tsx
const arrayRef = useRef<FormArrayHandle<Contact>>(null);

async function importFromCSV(rows: Contact[]) {
for (const row of rows) {
// skip duplicates by email
const existing = Array.from({ length: arrayRef.current?.length ?? 0 })
.map((_, i) => arrayRef.current?.at(i)?.getValue());
if (existing.some((c) => c?.email === row.email)) continue;
arrayRef.current?.insert(0, row); // добавляем в начало
}
}
```

_Source: src/components/form-array/FormArray.tsx_

### FormArrayItem

**Kind:** `interface`

Представляет элемент массива с контролом, индексом и действиями

**Signature:**
```typescript
export interface FormArrayItem<T extends object> {
  /** Контрол для данного элемента */
  control: FormProxy<T>;
  /** Индекс элемента (0-based) */
  index: number;
  /** Уникальный идентификатор для React key */
  id: string | number;
  /** Удалить этот элемент из массива */
  remove: () => void;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### FormArrayItemContext

**Kind:** `const`

React context, видимый внутри `FormArray.List` для одного элемента массива.
Содержит `control`, `index`, `id`, `remove()` и хелперы реордера
(`moveUp`/`moveDown`, `canMoveUp`/`canMoveDown`). Читать через {@link useFormArrayItemContext}.

**Signature:**
```typescript
export const FormArrayItemContext
```

**Examples:**

```tsx
import { FormArrayItemContext } from '@reformer/cdk/form-array';

function CurrentIndex() {
  const item = useContext(FormArrayItemContext);
  return <small>#{item?.index}</small>;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### FormArrayItemContextValue

**Kind:** `interface`

Контекст уровня элемента массива

**Signature:**
```typescript
export interface FormArrayItemContextValue<T extends object = Record<string, unknown>> {
  /** Контрол для данного элемента */
  control: FormProxy<T>;
  /** Индекс элемента (0-based) */
  index: number;
  /** Уникальный идентификатор для React key */
  id: string | number;
  /** Удалить этот элемент из массива */
  remove: () => void;
  /** Переместить элемент на одну позицию вверх (no-op если он первый) */
  moveUp: () => void;
  /** Переместить элемент на одну позицию вниз (no-op если он последний) */
  moveDown: () => void;
  /** Можно ли переместить вверх (index > 0) */
  canMoveUp: boolean;
  /** Можно ли переместить вниз (index < length - 1) */
  canMoveDown: boolean;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### FormArrayItemIndex

**Kind:** `function`

`FormArray.ItemIndex` — выводит номер текущего элемента (должен находиться
внутри `FormArray.List` / item-шаблона; читает индекс из
{@link FormArrayItemContext}).

По умолчанию показывает 1-based номер (`index + 1`) — так удобнее для UI.
Проп `render` получает исходный 0-based `index`, поэтому позволяет вывести
любую форму (в т.ч. 0-based).

**Signature:**
```typescript
export function FormArrayItemIndex({ render }: FormArrayItemIndexProps)
```

**Examples:**

По умолчанию — 1-based (рендерит 1, 2, 3, …)
```tsx
<FormArray.List>
{() => (
<h4>Объект #<FormArray.ItemIndex /></h4>
)}
</FormArray.List>
```

0-based индекс через render
```tsx
<FormArray.ItemIndex render={(index) => index} /> // рендерит 0, 1, 2, …
```

Кастомный вывод
```tsx
<FormArray.ItemIndex render={(index) => `Позиция: ${index + 1}`} />
```

_Source: src/components/form-array/FormArrayItemIndex.tsx_

### FormArrayItemIndexProps

**Kind:** `interface`

Props for FormArray.ItemIndex component

**Signature:**
```typescript
export interface FormArrayItemIndexProps {
  /** Custom render function for the index (receives 0-based index) */
  render?: (index: number) => ReactNode;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayItemRenderProps

**Kind:** `interface`

Props passed to the render function in FormArray.List

**Signature:**
```typescript
export interface FormArrayItemRenderProps<T extends object> {
  /** The form control for this item */
  control: FormProxy<T>;
  /** Zero-based index of the item */
  index: number;
  /** Unique identifier for React key */
  id: string | number;
  /** Remove this item from the array */
  remove: () => void;
  /** Move this item one position up (no-op when first) */
  moveUp: () => void;
  /** Move this item one position down (no-op when last) */
  moveDown: () => void;
  /** Whether this item can move up (index > 0) */
  canMoveUp: boolean;
  /** Whether this item can move down (index < length - 1) */
  canMoveDown: boolean;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayList

**Kind:** `function`

FormArray.List - Iterates over array items and provides item context

**Signature:**
```typescript
export function FormArrayList<T extends object>({
  children,
  className,
  as = 'div',
}: FormArrayListProps<T>)
```

**Examples:**

Basic usage
```tsx
<FormArray.List>
{({ control, index, remove }) => (
<div>
<span>Item #{index + 1}</span>
<button onClick={remove}>Remove</button>
<ItemForm control={control} />
</div>
)}
</FormArray.List>
```

With custom container
```tsx
<FormArray.List className="space-y-4" as="ul">
{(item) => <li><ItemForm control={item.control} /></li>}
</FormArray.List>
```

_Source: src/components/form-array/FormArrayList.tsx_

### FormArrayListProps

**Kind:** `interface`

Props for FormArray.List component

**Signature:**
```typescript
export interface FormArrayListProps<T extends object> {
  /** Render function for each item */
  children: (item: FormArrayItemRenderProps<T>) => ReactNode;
  /** Optional className for the list container */
  className?: string;
  /** Optional element type for the container (default: 'div') */
  as?: ElementType;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayRemoveButton

**Kind:** `const`

FormArray.RemoveButton - Button to remove current item (must be inside FormArray.List)

**Signature:**
```typescript
export const FormArrayRemoveButton
```

**Examples:**

Basic usage
```tsx
<FormArray.List>
{({ control }) => (
<div>
<ItemForm control={control} />
<FormArray.RemoveButton className="text-red-500">
Remove
</FormArray.RemoveButton>
</div>
)}
</FormArray.List>
```

With custom button (asChild)
```tsx
<FormArray.RemoveButton asChild>
<IconButton icon="trash" aria-label="Remove" />
</FormArray.RemoveButton>
```

_Source: src/components/form-array/FormArrayRemoveButton.tsx_

### FormArrayRemoveButtonProps

**Kind:** `interface`

Props for FormArray.RemoveButton component

**Signature:**
```typescript
export interface FormArrayRemoveButtonProps extends Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  'onClick'
> {
  /** Рендерить как дочерний элемент через Slot (props мержатся в children вместо `<button>`) */
  asChild?: boolean;
}
```

_Source: src/components/form-array/types.ts_

### FormArrayRootProps

**Kind:** `interface`

Props for FormArray.Root component

**Signature:**
```typescript
export interface FormArrayRootProps<T extends object> {
  /** The ArrayNode control from the form */
  control: ArrayNode<T>;
  /** Child components */
  children: ReactNode;
}
```

_Source: src/components/form-array/types.ts_

### FormField

**Kind:** `const`

FormField - Headless compound component for accessible form field anatomy.

Provides complete freedom in building field UI while handling all accessible
ID wiring (htmlFor, aria-labelledby, aria-describedby, aria-errormessage)
and field state management internally.

#### Features
- **Headless** — complete freedom in building UI, no styles imposed
- **Compound Components** — declarative API via nested components
- **Accessible by default** — all ARIA relationships wired automatically
- **Type Safe** — full TypeScript support with generics

#### Sub-components
- `FormField.Root` — context provider, accepts `control` and optional `id`
- `FormField.Label` — `<label>` with automatic `htmlFor` and required indicator
- `FormField.Control` — auto-renders `control.component` or wraps custom children
- `FormField.Error` — error message with `role="alert"`, supports multi-error
- `FormField.Description` — helper text with stable `id` for `aria-describedby`

**Signature:**
```typescript
export const FormField
```

**Examples:**

Minimal (auto-renders everything from field config)
```tsx
<FormField.Root control={control.email}>
<FormField.Label />
<FormField.Control />
<FormField.Error />
</FormField.Root>
```

Full control with custom styling
```tsx
<FormField.Root control={control.email} hasDescription>
<div className="space-y-1">
<FormField.Label className="text-sm font-medium text-gray-700" />

<FormField.Control asChild>
<Input type="email" className="border rounded-md px-3 py-2 w-full" />
</FormField.Control>

<FormField.Description className="text-xs text-gray-500">
We'll never share your email.
</FormField.Description>

<FormField.Error className="text-xs text-red-600" />
</div>
</FormField.Root>
```

Multiple errors with custom rendering
```tsx
<FormField.Root control={control.password}>
<FormField.Label />
<FormField.Control />
<FormField.Error
multi
render={(err) => (
<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
{err.message}
</span>
)}
/>
</FormField.Root>
```

Using useFormField hook for full customization
```tsx
import { useFormField } from '@reformer/cdk/form-field';

function EmailField({ control }: { control: FieldNode<string> }) {
const { labelProps, controlProps, errorProps, state, ids } = useFormField(control);

return (
<div>
<label {...labelProps}>{state.label}</label>
<Input
{...controlProps}
aria-describedby={ids.descriptionId}
type="email"
/>
<p id={ids.descriptionId} className="text-xs text-gray-500">
Helper text
</p>
{state.shouldShowError && (
<p {...errorProps} className="text-xs text-red-600">{state.error}</p>
)}
</div>
);
}
```

_Source: src/components/form-field/FormField.tsx_

### FormFieldContext

**Kind:** `const`

React context, который снабжает дочерние компоненты `FormField` (Label, Error,
Hint, Control) текущим контролом. Создаётся `FormField.Root`. Читать через
{@link useFormFieldContext}.

**Signature:**
```typescript
export const FormFieldContext
```

**Examples:**

```tsx
import { FormFieldContext } from '@reformer/cdk/form-field';

function CurrentValue() {
  const ctx = useContext(FormFieldContext);
  return <pre>{JSON.stringify(ctx?.control.value)}</pre>;
}
```

_Source: src/components/form-field/FormFieldContext.tsx_

### FormFieldContextValue

**Kind:** `interface`

Context value provided by FormField.Root

**Signature:**
```typescript
export interface FormFieldContextValue<T extends FormValue = FormValue> {
  // ─── Field state ──────────────────────────────────────────────────────────
  value: T;
  errors: ValidationError[];
  pending: boolean;
  disabled: boolean;
  valid: boolean;
  invalid: boolean;
  touched: boolean;
  shouldShowError: boolean;
  /** First error message, only set when shouldShowError is true */
  error: string | undefined;
  // ─── Derived from componentProps ──────────────────────────────────────────
  label: string | undefined;
  required: boolean;
  /** Full componentProps bag */
  componentProps: Record<string, unknown>;
  // ─── The control itself ───────────────────────────────────────────────────
  control: FieldNode<T>;
  // ─── Accessible IDs ───────────────────────────────────────────────────────
  ids: FormFieldIds;
  /** Whether a FormField.Description is present (drives aria-describedby on Control) */
  hasDescription: boolean;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldControl

**Kind:** `const`

FormField.Control - Renders the interactive form control.

**Auto-render mode** (default): renders `control.component` with all necessary
props pre-wired: `value`, `onChange`, `onBlur`, `disabled`, `aria-*` attributes,
and all `componentProps` from the field config.

**Custom children mode** (`asChild` or `children`): merges accessible props
into the provided child element via Slot, letting you use any custom component.

**Signature:**
```typescript
export const FormFieldControl
```

**Examples:**

Auto-render (renders control.component)
```tsx
<FormField.Root control={control.email}>
<FormField.Label />
<FormField.Control />
</FormField.Root>
```

Custom input with asChild (merges aria-* into your element)
```tsx
<FormField.Control asChild>
<MyInput type="email" className="custom-input" />
</FormField.Control>
```

Custom children (same as asChild)
```tsx
<FormField.Control>
<MyInput type="email" />
</FormField.Control>
```

_Source: src/components/form-field/FormFieldControl.tsx_

### FormFieldControlProps

**Kind:** `interface`

Props for FormField.Control

**Signature:**
```typescript
export interface FormFieldControlProps extends Omit<
  HTMLAttributes<HTMLElement>,
  'id' | 'onChange' | 'onBlur'
> {
  /**
   * Merge accessible props into a custom child element instead of
   * auto-rendering control.component.
   */
  asChild?: boolean;
  /**
   * Custom children. When provided, control.component is NOT auto-rendered.
   * Accessible props are merged into the child via Slot.
   */
  children?: ReactNode;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldDescription

**Kind:** `const`

FormField.Description - Helper text for the form field.

Renders with a stable `id` (descriptionId) that can be wired to
`aria-describedby` on the control. To enable automatic wiring, pass
`hasDescription` to the parent `FormField.Root`.

**Signature:**
```typescript
export const FormFieldDescription
```

**Examples:**

```tsx
<FormField.Root control={control.email} hasDescription>
  <FormField.Label />
  <FormField.Control />
  <FormField.Description className="text-xs text-gray-500">
    We'll never share your email with anyone.
  </FormField.Description>
  <FormField.Error />
</FormField.Root>
```

With custom element (asChild)
```tsx
<FormField.Description asChild>
<Tooltip content="More info">
<InfoIcon />
</Tooltip>
</FormField.Description>
```

_Source: src/components/form-field/FormFieldDescription.tsx_

### FormFieldDescriptionProps

**Kind:** `interface`

Props for FormField.Description

**Signature:**
```typescript
export interface FormFieldDescriptionProps extends Omit<
  HTMLAttributes<HTMLParagraphElement>,
  'id'
> {
  asChild?: boolean;
  children: ReactNode;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldError

**Kind:** `const`

FormField.Error - Displays validation error message(s).

Renders nothing when `shouldShowError` is false (field not touched or no errors).
The first error paragraph receives `id={ids.errorId}` for `aria-errormessage` wiring.

**Signature:**
```typescript
export const FormFieldError
```

**Examples:**

Single error (default)
```tsx
<FormField.Error className="text-xs text-red-600" />
```

All errors
```tsx
<FormField.Error multi className="text-xs text-red-600" />
```

Custom render per error
```tsx
<FormField.Error
render={(err) => (
<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
{err.message}
</span>
)}
/>
```

Custom error content
```tsx
<FormField.Error className="text-xs">
This field is required
</FormField.Error>
```

_Source: src/components/form-field/FormFieldError.tsx_

### FormFieldErrorProps

**Kind:** `interface`

Props for FormField.Error

**Signature:**
```typescript
export interface FormFieldErrorProps extends Omit<
  HTMLAttributes<HTMLParagraphElement>,
  'id' | 'role'
> {
  asChild?: boolean;
  /**
   * When true, renders all errors instead of only the first.
   * @default false
   */
  multi?: boolean;
  /**
   * Custom render function per error. When provided, multi is implied true.
   */
  render?: (error: ValidationError, index: number) => ReactNode;
  /**
   * Override error content. Defaults to errors[0].message.
   */
  children?: ReactNode;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldIds

**Kind:** `interface`

Stable IDs for all accessible elements of a form field

**Signature:**
```typescript
export interface FormFieldIds {
  /** ID placed on the interactive control element (<input>, etc.) */
  controlId: string;
  /** ID placed on the <label> element */
  labelId: string;
  /** ID placed on the description paragraph */
  descriptionId: string;
  /** ID placed on the first error paragraph */
  errorId: string;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldLabel

**Kind:** `const`

FormField.Label - Accessible label for the form field.

Automatically wires `htmlFor` to the control ID and `id` to the label ID
so that `aria-labelledby` on FormField.Control works correctly.

The label text defaults to `componentProps.label` from the field config.
Pass `children` to override or enrich the label content.

A required indicator `*` is appended automatically when `componentProps.required` is set.

**Signature:**
```typescript
export const FormFieldLabel
```

**Examples:**

Auto label from field config
```tsx
<FormField.Root control={control.email}>
<FormField.Label />  {/* renders componentProps.label *\/}
<FormField.Control />
</FormField.Root>
```

Custom label content
```tsx
<FormField.Label className="font-semibold">
Email Address <span className="text-gray-400">(optional)</span>
</FormField.Label>
```

With custom element (asChild)
```tsx
<FormField.Label asChild>
<Typography variant="label">{label}</Typography>
</FormField.Label>
```

_Source: src/components/form-field/FormFieldLabel.tsx_

### FormFieldLabelProps

**Kind:** `interface`

Props for FormField.Label

**Signature:**
```typescript
export interface FormFieldLabelProps extends Omit<
  LabelHTMLAttributes<HTMLLabelElement>,
  'htmlFor'
> {
  /** Render as child element via Slot (merges accessible props into the child) */
  asChild?: boolean;
  /**
   * Override label text. Defaults to componentProps.label.
   * Pass children explicitly when you need custom content inside the label.
   */
  children?: ReactNode;
  /**
   * If true, always renders even when no label text is available.
   * Useful when the consumer provides children.
   * @default false
   */
  forceRender?: boolean;
}
```

_Source: src/components/form-field/types.ts_

### FormFieldRoot

**Kind:** `function`

FormField.Root - Context provider for form field compound component.

Computes stable accessible IDs (controlId, labelId, descriptionId, errorId)
and provides all field state to child FormField.* components.

**Signature:**
```typescript
function FormFieldRoot<T extends FormValue>({
  control,
  children,
  id,
  hasDescription = false,
}: FormFieldRootProps<T>)
```

**Examples:**

Minimal usage
```tsx
<FormField.Root control={control.email}>
<FormField.Label />
<FormField.Control />
<FormField.Error />
</FormField.Root>
```

With description (pass hasDescription to auto-wire aria-describedby)
```tsx
<FormField.Root control={control.email} hasDescription>
<FormField.Label />
<FormField.Control />
<FormField.Description>Helper text</FormField.Description>
<FormField.Error />
</FormField.Root>
```

_Source: src/components/form-field/FormFieldRoot.tsx_

### FormFieldRootProps

**Kind:** `interface`

Props for FormField.Root

**Signature:**
```typescript
export interface FormFieldRootProps<T extends FormValue = FormValue> {
  /** The FieldNode control from the form */
  control: FieldNode<T>;
  children: ReactNode;
  /** Explicit id prefix; if omitted, useId() is used */
  id?: string;
  /**
   * Set to true when the field has a description element so that
   * FormField.Control automatically wires aria-describedby.
   * Avoids the double-render caused by dynamic description registration.
   * @default false
   */
  hasDescription?: boolean;
}
```

_Source: src/components/form-field/types.ts_

### FormWizard

**Kind:** `const`

Headless-мастер многошаговой формы. Compound-компонент: сам `FormWizard` держит состояние
шага/навигации/валидации, а под-компоненты подключаются к нему через контекст:
`FormWizard.Step` (рендер текущего шага), `FormWizard.Actions` (кнопки навигации),
`FormWizard.Indicator` и `FormWizard.Progress` (headless render-props), а также готовые
кнопки `FormWizard.Prev` / `FormWizard.Next` / `FormWizard.Submit`.

Число шагов (`totalSteps`) выводится автоматически из количества `<FormWizard.Step>` в детях
(в т.ч. вложенных в обёрточные `div`). Валидация шага и всей формы — через колбэки в
{@link FormWizardConfig} (`validateStep` / `validateAll`), поэтому компонент не привязан к
конкретному движку валидации. Для внешнего управления (submit/переход из шапки страницы)
пробросьте `ref` типа {@link FormWizardHandle}.

**Signature:**
```typescript
export const FormWizard
```

**Examples:**

Полный мастер с индикатором, действиями и прогрессом
```tsx
import { useMemo, useRef } from 'react';
import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';

const STEPS = [
{ number: 1, title: 'Личные данные' },
{ number: 2, title: 'Проверка' },
];

function Page({ form, model }: Props) {
const navRef = useRef<FormWizardHandle<Profile>>(null);
// config: { validateStep, validateAll } — например, из makeValidationConfig(model)
const config = useMemo(() => makeValidationConfig(model), [model]);

return (
<FormWizard ref={navRef} form={form} config={config}>
<FormWizard.Indicator steps={STEPS}>
{({ steps, goToStep }) => (
 <nav>
   {steps.map((s) => (
     <button key={s.number} onClick={() => goToStep(s.number)} disabled={!s.canNavigate}>
       {s.title}
     </button>
   ))}
 </nav>
)}
</FormWizard.Indicator>

<FormWizard.Step component={PersonalStep} control={form} />
<FormWizard.Step component={ReviewStep} control={form} />

<FormWizard.Actions onSubmit={() => navRef.current?.submit(api.save)}>
<FormWizard.Prev>Назад</FormWizard.Prev>
<FormWizard.Next>Далее</FormWizard.Next>
<FormWizard.Submit loadingText="Отправка…">Отправить</FormWizard.Submit>
</FormWizard.Actions>

<FormWizard.Progress>
{({ current, total, percent }) => <span>Шаг {current}/{total} ({percent}%)</span>}
</FormWizard.Progress>
</FormWizard>
);
}
```

**See also:**
- {@link FormWizardHandle} — методы для внешнего управления через `ref`.
- {@link FormWizardConfig} — колбэки валидации `validateStep` / `validateAll`.
- [docs/llms/03-form-navigation.md](../../../docs/llms/03-form-navigation.md)

_Source: src/components/form-wizard/FormWizard.tsx_

### FormWizardActions

**Kind:** `function`

`FormWizard.Actions` — контейнер кнопок навигации мастера. Поддерживает два режима:
compound-children (`FormWizard.Prev`/`Next`/`Submit`) и render-props.

Render-props получают: `prev`, `next`, `submit` (все с `onClick`/`disabled`),
`isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`.

**Signature:**
```typescript
export function FormWizardActions({
  onSubmit,
  children,
  className,
  style,
}: FormWizardActionsProps)
```

**Examples:**

Compound mode
```tsx
<FormWizard.Actions onSubmit={handleSubmit}>
<FormWizard.Prev>Back</FormWizard.Prev>
<FormWizard.Next>Next</FormWizard.Next>
<FormWizard.Submit loadingText="Submitting...">Submit</FormWizard.Submit>
</FormWizard.Actions>
```

Render-props mode
```tsx
<FormWizard.Actions onSubmit={handleSubmit}>
{({ prev, next, submit, isFirstStep, isLastStep }) => (
<div className="flex justify-between">
{!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}
{isLastStep
? <button onClick={submit.onClick} disabled={submit.disabled}>{submit.isSubmitting ? '…' : 'Submit'}</button>
: <button onClick={next.onClick} disabled={next.disabled}>Next</button>}
</div>
)}
</FormWizard.Actions>
```

_Source: src/components/form-wizard/FormWizardActions.tsx_

### FormWizardActionsProps

**Kind:** `interface`

Props for FormWizard.Actions component

**Signature:**
```typescript
export interface FormWizardActionsProps {
  /** Submit handler (called on last step) */
  onSubmit?: () => void | Promise<void>;
  /** Children: render function (headless) or ReactNode (compound components) */
  children: ReactNode | RenderFunction;
  /** Optional className for wrapper (compound mode only) */
  className?: string;
  /** Optional style for wrapper (compound mode only) */
  style?: CSSProperties;
}
```

_Source: src/components/form-wizard/FormWizardActions.tsx_

### FormWizardActionsRenderProps

**Kind:** `interface`

Render props passed to children function

**Signature:**
```typescript
export interface FormWizardActionsRenderProps {
  /** Props for the "Previous" button */
  prev: FormWizardButtonProps;
  /** Props for the "Next" button */
  next: FormWizardButtonProps;
  /** Props for the "Submit" button */
  submit: FormWizardSubmitRenderProps;
  /** Whether current step is the first step */
  isFirstStep: boolean;
  /** Whether current step is the last step */
  isLastStep: boolean;
  /** Whether validation is in progress */
  isValidating: boolean;
  /** Whether form is submitting */
  isSubmitting: boolean;
}
```

_Source: src/components/form-wizard/FormWizardActions.tsx_

### FormWizardButtonProps

**Kind:** `interface`

Props for a navigation button (prev/next)

**Signature:**
```typescript
export interface FormWizardButtonProps {
  /** Click handler */
  onClick: () => void;
  /** Whether the button is disabled */
  disabled: boolean;
}
```

_Source: src/components/form-wizard/FormWizardActions.tsx_

### FormWizardConfig

**Kind:** `interface`

Configuration for multi-step form navigation.
Note: totalSteps is inferred from children count. Валидация под M1 — через колбэки,
не зависит от типа формы, поэтому конфиг не дженерик.

**Signature:**
```typescript
export interface FormWizardConfig {
  /**
   * M1: валидация шага (1-based) через колбэк (например, `validateFormModel`).
   * Возвращает `true`, если шаг валиден. Нет колбэка → шаг считается валидным.
   */
  validateStep?: (step: number) => boolean | Promise<boolean>;

  /**
   * M1: валидация всей формы перед submit. Нет колбэка → submit без блокировки.
   */
  validateAll?: () => boolean | Promise<boolean>;
}
```

_Source: src/components/form-wizard/types.ts_

### FormWizardContext

**Kind:** `const`

React context, который снабжает дочерние компоненты `FormWizard` (Step,
Actions, Indicator, Progress) текущим состоянием мастера. Создаётся
`FormWizard`. Читать через `useFormWizard()`.

**Signature:**
```typescript
export const FormWizardContext
```

**Examples:**

```tsx
import { FormWizardContext } from '@reformer/cdk/form-wizard';

function CurrentStep() {
  const ctx = useContext(FormWizardContext);
  return <span>step {ctx?.currentStep}</span>;
}
```

_Source: src/components/form-wizard/FormWizardContext.tsx_

### FormWizardContextValue

**Kind:** `interface`

Context value for FormWizard
Shares navigation state and methods with child components

**Signature:**
```typescript
export interface FormWizardContextValue<T extends Record<string, any>> {
  // ============================================================================
  // State
  // ============================================================================

  /** Current step (1-based) */
  currentStep: number;

  /** Total number of steps */
  totalSteps: number;

  /** Completed steps */
  completedSteps: number[];

  /** Is this the first step */
  isFirstStep: boolean;

  /** Is this the last step */
  isLastStep: boolean;

  /** Is validation in progress */
  isValidating: boolean;

  /** Is form submitting */
  isSubmitting: boolean;

  /** Form instance */
  form: FormProxy<T>;

  // ============================================================================
  // Navigation Methods
  // ============================================================================

  /** Go to next step (with validation) */
  goToNextStep: () => Promise<boolean>;

  /** Go to previous step */
  goToPreviousStep: () => void;

  /** Go to specific step */
  goToStep: (step: number) => boolean;
}
```

_Source: src/components/form-wizard/FormWizardContext.tsx_

### FormWizardHandle

**Kind:** `interface`

Handle для внешнего управления {@link FormWizard} через `useRef`.

Используется, когда submit/навигация инициируется снаружи дерева Wizard:
шапка страницы, breadcrumbs, side-effect от API. Получают через
`useRef<FormWizardHandle<T>>(null)` и передают в `<FormWizard ref={...}>`.

- `goToNextStep()` / `submit()` запускают валидацию текущего шага / всей
  формы соответственно.
- `goToStep(n)` возвращает `false`, если предыдущий шаг не в `completedSteps`
  (защита от пропуска валидации) либо `n` вне диапазона `[1; totalSteps]`.
- `submit()` возвращает `R | null`. `null` — форма не прошла `validateAll`.

**Signature:**
```typescript
export interface FormWizardHandle<T extends Record<string, any>> {
  /** Form instance — используется в RenderBehaviorFn для доступа к форме через ref */
  form: FormProxy<T>;

  /** Current step (1-based) */
  currentStep: number;

  /** Completed steps */
  completedSteps: number[];

  /** Validate current step */
  validateCurrentStep: () => Promise<boolean>;

  /** Go to next step (with validation) */
  goToNextStep: () => Promise<boolean>;

  /** Go to previous step */
  goToPreviousStep: () => void;

  /** Go to specific step */
  goToStep: (step: number) => boolean;

  /** Submit form (with full validation) */
  submit: <R>(onSubmit: (values: T) => Promise<R> | R) => Promise<R | null>;

  /** Is this the first step */
  isFirstStep: boolean;

  /** Is this the last step */
  isLastStep: boolean;

  /** Is validation in progress */
  isValidating: boolean;
}
```

**Examples:**

«Сохранить и выйти» поверх wizard
```tsx
import { useRef } from 'react';
import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';

function Page({ form, config }: Props) {
const navRef = useRef<FormWizardHandle<CreditApplication>>(null);

const handleSaveAndExit = async () => {
const saved = await navRef.current?.submit((values) => api.saveDraft(values));
if (saved) router.push('/dashboard');
};

return (
<>
<header>
<button onClick={handleSaveAndExit}>Сохранить и выйти</button>
</header>
<FormWizard ref={navRef} form={form} config={config}>
<FormWizard.Step component={Step1} control={form} />
<FormWizard.Step component={Step2} control={form} />
</FormWizard>
</>
);
}
```

Программный переход на шаг с проверкой доступности
```tsx
const handleClickContacts = () => {
const ok = navRef.current?.goToStep(3);
if (!ok) toast('Сначала заполните предыдущие шаги');
};

// Или с явной валидацией текущего шага:
const moveOn = async () => {
const valid = await navRef.current?.validateCurrentStep();
if (!valid) return;
await navRef.current?.goToNextStep();
};
```

_Source: src/components/form-wizard/types.ts_

### FormWizardIndicator

**Kind:** `function`

FormWizard.Indicator - Headless component for step indicator

Provides step data with state for building custom step indicators.
No default UI - you build exactly what you need.

#### Render Props
- `steps` - array of steps with state (`isCurrent`, `isCompleted`, `canNavigate`)
- `goToStep` - function to navigate to a step
- `currentStep` - current step number
- `totalSteps` - total number of steps
- `completedSteps` - array of completed step numbers

**Signature:**
```typescript
export function FormWizardIndicator({ steps, children }: FormWizardIndicatorProps)
```

**Examples:**

Basic stepper
```tsx
<FormWizard.Indicator steps={STEPS}>
{({ steps, goToStep }) => (
<nav className="flex gap-2">
{steps.map((step) => (
<button
 key={step.number}
 onClick={() => goToStep(step.number)}
 disabled={!step.canNavigate}
 className={cn(
   'px-4 py-2 rounded',
   step.isCurrent && 'bg-blue-500 text-white',
   step.isCompleted && 'bg-green-100',
   !step.canNavigate && 'opacity-50 cursor-not-allowed'
 )}
>
 {step.icon} {step.title}
</button>
))}
</nav>
)}
</FormWizard.Indicator>
```

With progress line
```tsx
<FormWizard.Indicator steps={STEPS}>
{({ steps, goToStep }) => (
<div className="flex items-center">
{steps.map((step, index) => (
<Fragment key={step.number}>
 <StepCircle
   active={step.isCurrent}
   completed={step.isCompleted}
   onClick={() => step.canNavigate && goToStep(step.number)}
 >
   {step.isCompleted ? '✓' : step.number}
 </StepCircle>
 {index < steps.length - 1 && (
   <StepLine completed={step.isCompleted} />
 )}
</Fragment>
))}
</div>
)}
</FormWizard.Indicator>
```

_Source: src/components/form-wizard/FormWizardIndicator.tsx_

### FormWizardIndicatorProps

**Kind:** `interface`

Props for FormWizard.Indicator component

**Signature:**
```typescript
export interface FormWizardIndicatorProps {
  /** Step definitions */
  steps: FormWizardIndicatorStep[];
  /** Render function for custom UI */
  children: (props: FormWizardIndicatorRenderProps) => ReactNode;
}
```

_Source: src/components/form-wizard/FormWizardIndicator.tsx_

### FormWizardIndicatorRenderProps

**Kind:** `interface`

Render props passed to children function

**Signature:**
```typescript
export interface FormWizardIndicatorRenderProps {
  /** Steps with their current state */
  steps: FormWizardIndicatorStepWithState[];
  /** Navigate to a specific step */
  goToStep: (step: number) => boolean;
  /** Current step number */
  currentStep: number;
  /** Total number of steps */
  totalSteps: number;
  /** Completed step numbers */
  completedSteps: number[];
}
```

_Source: src/components/form-wizard/FormWizardIndicator.tsx_

### FormWizardIndicatorStep

**Kind:** `interface`

Step definition for the indicator

**Signature:**
```typescript
export interface FormWizardIndicatorStep {
  /** Step number (1-based) */
  number: number;
  /** Step title/label */
  title: string;
  /** Optional icon */
  icon?: string;
  /** Component to render for this step, or a ReactNode (pre-rendered element) */
  component?:
    | ComponentType<
        {
          control: FormProxy<unknown>;
        } & Record<string, unknown>
      >
    | ReactNode;
}
```

_Source: src/components/form-wizard/FormWizardIndicator.tsx_

### FormWizardIndicatorStepWithState

**Kind:** `interface`

Enriched step with state information

**Signature:**
```typescript
export interface FormWizardIndicatorStepWithState extends FormWizardIndicatorStep {
  /** Whether this is the current step */
  isCurrent: boolean;
  /** Whether this step is completed */
  isCompleted: boolean;
  /** Whether user can navigate to this step */
  canNavigate: boolean;
}
```

_Source: src/components/form-wizard/FormWizardIndicator.tsx_

### FormWizardProgress

**Kind:** `function`

FormWizard.Progress - Headless component for progress display

Provides progress data for building custom progress indicators.
No default UI - you build exactly what you need.

#### Render Props
- `current` - current step number
- `total` - total number of steps
- `percent` - completion percentage (0-100)
- `completedCount` - number of completed steps
- `isFirstStep` - whether on first step
- `isLastStep` - whether on last step

**Signature:**
```typescript
export function FormWizardProgress({ children }: FormWizardProgressProps)
```

**Examples:**

Simple text progress
```tsx
<FormWizard.Progress>
{({ current, total, percent }) => (
<div className="text-sm text-gray-600">
Step {current} of {total} ({percent}% complete)
</div>
)}
</FormWizard.Progress>
```

Progress bar
```tsx
<FormWizard.Progress>
{({ percent, current, total }) => (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Step {current}/{total}</span>
<span>{percent}%</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
 className="h-full bg-blue-500 transition-all"
 style={{ width: `${percent}%` }}
/>
</div>
</div>
)}
</FormWizard.Progress>
```

Circular progress
```tsx
<FormWizard.Progress>
{({ percent }) => (
<CircularProgress value={percent} />
)}
</FormWizard.Progress>
```

_Source: src/components/form-wizard/FormWizardProgress.tsx_

### FormWizardProgressProps

**Kind:** `interface`

Props for FormWizard.Progress component

**Signature:**
```typescript
export interface FormWizardProgressProps {
  /** Render function for custom UI */
  children: (props: FormWizardProgressRenderProps) => ReactNode;
}
```

_Source: src/components/form-wizard/FormWizardProgress.tsx_

### FormWizardProgressRenderProps

**Kind:** `interface`

Render props passed to children function

**Signature:**
```typescript
export interface FormWizardProgressRenderProps {
  /** Current step number (1-based) */
  current: number;
  /** Total number of steps */
  total: number;
  /** Completion percentage (0-100) */
  percent: number;
  /** Number of completed steps */
  completedCount: number;
  /** Whether on first step */
  isFirstStep: boolean;
  /** Whether on last step */
  isLastStep: boolean;
}
```

_Source: src/components/form-wizard/FormWizardProgress.tsx_

### FormWizardProps

**Kind:** `interface`

Props for FormWizard component

**Signature:**
```typescript
export interface FormWizardProps<T extends Record<string, any>> {
  /** Form instance */
  form: FormProxy<T>;

  /** Step configuration (validation callbacks) */
  config: FormWizardConfig;

  /** Children (Step components, Indicator, Actions, Progress, or any ReactNode) */
  children?: ReactNode;

  /** Callback when step changes */
  onStepChange?: (step: number) => void;

  /** Scroll to top on step change */
  scrollToTop?: boolean;
}
```

_Source: src/components/form-wizard/types.ts_

### FormWizardStep

**Kind:** `function`

FormWizard.Step - renders a step component when it's the current step

**Signature:**
```typescript
export function FormWizardStep<T extends Record<string, any>>({
  component: Component,
  control,
  children,
  _stepIndex,
  ...restProps
}: FormWizardStepInternalProps<T>)
```

**Examples:**

Component-based (legacy)
```tsx
<FormWizard ref={navRef} form={form} config={config}>
<FormWizard.Step component={Step1} control={form} />
<FormWizard.Step component={Step2} control={form} extraProp="value" />
</FormWizard>
```

Children-based (new)
```tsx
<FormWizard form={form} config={config}>
<FormWizard.Step>
<RenderNodeComponent node={step1Content} ... />
</FormWizard.Step>
<FormWizard.Step>
<RenderNodeComponent node={step2Content} ... />
</FormWizard.Step>
</FormWizard>
```

_Source: src/components/form-wizard/FormWizardStep.tsx_

### FormWizardStepProps

**Kind:** `interface`

Props for FormWizard.Step component

Supports two usage patterns:
1. Component-based: `<FormWizard.Step component={Step1} control={form} />`
2. Children-based: `<FormWizard.Step>{children}</FormWizard.Step>`

**Signature:**
```typescript
export interface FormWizardStepProps<T extends Record<string, any>> {
  /** Component to render for this step (legacy API) */
  component?: ComponentType<{ control: FormProxy<T> } & Record<string, unknown>>;

  /** Form control to pass to the component (legacy API) */
  control?: FormProxy<T>;

  /** Children to render (new API - for use with selector-based wizard) */
  children?: ReactNode;

  /** Any additional props to pass to the component */
  [key: string]: unknown;
}
```

_Source: src/components/form-wizard/FormWizardStep.tsx_

### FormWizardSubmitProps

**Kind:** `interface`

Props for FormWizard.Submit component

**Signature:**
```typescript
export interface FormWizardSubmitProps extends Omit<
  ButtonHTMLAttributes<HTMLButtonElement>,
  'onClick'
> {
  /** Button content */
  children: ReactNode;
  /** Render as child element (merge props into child) */
  asChild?: boolean;
  /** Additional disabled state (merged with automatic via OR) */
  disabled?: boolean;
  /** Content to show during submission (replaces children) */
  loadingText?: ReactNode;
}
```

_Source: src/components/form-wizard/FormWizardSubmit.tsx_

### useFormArray

**Kind:** `function`

Headless hook for managing form arrays

Provides reactive state and actions for form array manipulation
without any UI - perfect for building custom array interfaces.

**Signature:**
```typescript
export function useFormArray<T extends object>(control: ArrayNode<T>): UseFormArrayReturn<T>
```

**Examples:**

Basic usage
```tsx
function PropertyList() {
const { items, add, isEmpty } = useFormArray(form.properties);

return (
<div>
{items.map(({ control, index, remove, id }) => (
<div key={id}>
 <span>Property #{index + 1}</span>
 <button onClick={remove}>Remove</button>
 <PropertyForm control={control} />
</div>
))}
{isEmpty && <p>No properties added</p>}
<button onClick={() => add()}>Add Property</button>
</div>
);
}
```

With initial values + clear / insert
```tsx
function PropertyToolbar() {
const { add, clear, insert, length } = useFormArray(form.properties);

return (
<div className="flex gap-2">
<button onClick={() => add({ type: 'apartment', estimatedValue: 0 })}>
+ Квартира
</button>
<button onClick={() => insert(0, { type: 'house' })}>
+ Дом (в начало)
</button>
<button onClick={clear} disabled={length === 0}>Очистить</button>
<span>{length} шт.</span>
</div>
);
}
```

Кастомный AddButton снаружи compound API (drop-down)
```tsx
import { useFormArrayContext } from '@reformer/cdk/form-array';

function AddPropertyMenu() {
const { add } = useFormArrayContext<Property>();
return (
<Menu>
<Menu.Trigger>+ Добавить ▾</Menu.Trigger>
<Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
<Menu.Item onSelect={() => add({ type: 'house' })}>Дом</Menu.Item>
</Menu>
);
}
```

_Source: src/components/form-array/useFormArray.ts_

### useFormArrayContext

**Kind:** `function`

Хук для доступа к контексту `FormArray`. Бросает исключение, если вызван вне
`FormArray.Root` или эквивалентного провайдера.

**Signature:**
```typescript
export function useFormArrayContext<
  T extends object = Record<string, unknown>,
>(): FormArrayContextValue<T>
```

**Returns:** Текущий {@link FormArrayContextValue}.

**Examples:**

Кастомный AddButton с predefined значением
```tsx
import { useFormArrayContext } from '@reformer/cdk/form-array';

function AddDraftButton() {
const { add } = useFormArrayContext<Item>();
return (
<button onClick={() => add({ status: 'draft', createdAt: Date.now() })}>
+ Add Draft
</button>
);
}
```

Счётчик и условный empty-state из произвольного места дерева
```tsx
function ItemsBadge() {
const { length, isEmpty } = useFormArrayContext();
if (isEmpty) return <span className="text-gray-400">Нет элементов</span>;
return <span className="badge">{length}</span>;
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### useFormArrayItemContext

**Kind:** `function`

Хук для доступа к контексту текущего элемента внутри `FormArray.List`.

**Signature:**
```typescript
export function useFormArrayItemContext<
  T extends object = Record<string, unknown>,
>(): FormArrayItemContextValue<T>
```

**Returns:** Текущий {@link FormArrayItemContextValue} (`control`, `index`, `id`, `remove`,
`moveUp`/`moveDown`, `canMoveUp`/`canMoveDown`).

**Examples:**

Кнопка удаления текущего элемента
```tsx
import { useFormArrayItemContext } from '@reformer/cdk/form-array';

function ItemRemoveButton() {
const { remove } = useFormArrayItemContext();
return <button onClick={remove}>×</button>;
}
```

Доступ к control + index для условной валидации
```tsx
function ItemHeader() {
const { control, index } = useFormArrayItemContext<Property>();
const { value: type } = useFormControl(control.type);
return (
<h4>
#{index + 1} — {type === 'house' ? 'Дом' : 'Квартира'}
</h4>
);
}
```

_Source: src/components/form-array/FormArrayContext.tsx_

### UseFormArrayReturn

**Kind:** `interface`

Return type for useFormArray hook

**Signature:**
```typescript
export interface UseFormArrayReturn<T extends object> {
  /** Array of items with their controls and actions */
  items: FormArrayItem<T>[];
  /** Current number of items in the array */
  length: number;
  /** Whether the array is empty */
  isEmpty: boolean;
  /** Add a new item to the end of the array */
  add: (value?: Partial<T>) => void;
  /** Remove all items from the array */
  clear: () => void;
  /** Insert a new item at a specific index */
  insert: (index: number, value?: Partial<T>) => void;
  /** Move an item from one index to another (reorder, state preserved) */
  move: (from: number, to: number) => void;
  /** Swap two items by index (reorder, state preserved) */
  swap: (a: number, b: number) => void;
}
```

_Source: src/components/form-array/useFormArray.ts_

### useFormField

**Kind:** `function`

Primary hook for building accessible form fields.

Returns partitioned prop collections and structured state that you can spread
directly onto your own elements. No prescribed DOM structure.

**Signature:**
```typescript
export function useFormField<T extends FormValue>(
  control: FieldNode<T>,
  id?: string
): UseFormFieldReturn<T>
```

**Examples:**

Basic usage
```tsx
function EmailField({ control }: { control: FieldNode<string> }) {
const { labelProps, controlProps, errorProps, state } = useFormField(control);

return (
<div>
<label {...labelProps}>{state.label}</label>
<input {...controlProps} type="email" />
{state.shouldShowError && (
<p {...errorProps}>{state.error}</p>
)}
</div>
);
}
```

With description (manual aria-describedby wiring)
```tsx
const { labelProps, controlProps, errorProps, descriptionProps, state, ids } =
useFormField(control);

const enrichedControlProps = {
...controlProps,
'aria-describedby': [
ids.descriptionId,
state.shouldShowError ? ids.errorId : null,
].filter(Boolean).join(' ') || undefined,
};
```

_Source: src/components/form-field/useFormField.ts_

### useFormFieldContext

**Kind:** `function`

Хук для доступа к контексту `FormField`. Бросает исключение, если вызван
вне `FormField.Root`.

**Signature:**
```typescript
export function useFormFieldContext<T extends FormValue = FormValue>(): FormFieldContextValue<T>
```

**Returns:** Текущий {@link FormFieldContextValue}.

**Examples:**

Счётчик символов рядом с label
```tsx
import { useFormFieldContext } from '@reformer/cdk/form-field';

function CharCount() {
const { control } = useFormFieldContext<string>();
return <small>{control.value.length} chars</small>;
}
```

Async pending-индикатор и required-астериск
```tsx
function PendingBadge() {
const { pending, required, error } = useFormFieldContext();
if (pending) return <Spinner size="xs" aria-label="Проверяем..." />;
if (error) return <span className="text-red-600 text-xs">!</span>;
if (required) return <span className="text-gray-400">*</span>;
return null;
}

<FormField.Root control={form.username}>
<div className="flex items-center gap-2">
<FormField.Label />
<PendingBadge />
</div>
<FormField.Control />
<FormField.Error />
</FormField.Root>
```

_Source: src/components/form-field/FormFieldContext.tsx_

### UseFormFieldControlProps

**Kind:** `interface`

Props to spread onto the interactive control element

**Signature:**
```typescript
export interface UseFormFieldControlProps {
  id: string;
  disabled: boolean;
  'aria-labelledby': string;
  'aria-invalid': true | undefined;
  'aria-errormessage': string | undefined;
  'aria-required': true | undefined;
  /** Direct-value onChange compatible with ReFormer field components */
  onChange: (value: unknown) => void;
  onBlur: () => void;
}
```

_Source: src/components/form-field/useFormField.ts_

### UseFormFieldDescriptionProps

**Kind:** `interface`

Props to spread onto a description paragraph

**Signature:**
```typescript
export interface UseFormFieldDescriptionProps {
  id: string;
}
```

_Source: src/components/form-field/useFormField.ts_

### UseFormFieldErrorProps

**Kind:** `interface`

Props to spread onto an error paragraph

**Signature:**
```typescript
export interface UseFormFieldErrorProps {
  id: string;
  role: 'alert';
}
```

_Source: src/components/form-field/useFormField.ts_

### UseFormFieldLabelProps

**Kind:** `interface`

Props to spread onto a <label> element

**Signature:**
```typescript
export interface UseFormFieldLabelProps {
  id: string;
  htmlFor: string;
}
```

_Source: src/components/form-field/useFormField.ts_

### UseFormFieldReturn

**Kind:** `interface`

Return type of useFormField hook

**Signature:**
```typescript
export interface UseFormFieldReturn<T extends FormValue = FormValue> {
  /** Spread onto <label> */
  labelProps: UseFormFieldLabelProps;
  /** Spread onto the interactive control; includes value */
  controlProps: UseFormFieldControlProps & { value: T };
  /** Spread onto the first error paragraph */
  errorProps: UseFormFieldErrorProps;
  /** Spread onto the description paragraph */
  descriptionProps: UseFormFieldDescriptionProps;
  /** Structured field state */
  state: UseFormFieldState<T>;
  /** Field actions */
  actions: {
    setValue: (value: T) => void;
    markAsTouched: () => void;
    markAsUntouched: () => void;
    reset: (value?: T) => void;
  };
  /** Raw IDs for manual wiring (e.g. aria-describedby) */
  ids: FormFieldIds;
}
```

_Source: src/components/form-field/useFormField.ts_

### UseFormFieldState

**Kind:** `interface`

Field state returned by useFormField

**Signature:**
```typescript
export interface UseFormFieldState<T extends FormValue = FormValue> {
  value: T;
  errors: ValidationError[];
  /** First error message, only set when shouldShowError is true */
  error: string | undefined;
  isPending: boolean;
  isDisabled: boolean;
  isValid: boolean;
  isInvalid: boolean;
  isTouched: boolean;
  shouldShowError: boolean;
  label: string | undefined;
  required: boolean;
  /** Full componentProps bag from FieldNode config */
  componentProps: Record<string, unknown>;
}
```

_Source: src/components/form-field/useFormField.ts_

### useFormWizard

**Kind:** `function`

Хук для доступа к контексту {@link FormWizard} из любого потомка.

Возвращает текущее состояние мастера (`currentStep`, `totalSteps`,
`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`,
`form`) и методы навигации (`goToNextStep`, `goToPreviousStep`, `goToStep`).
Бросает исключение, если вызван вне `<FormWizard>`.

Для внешнего управления (вне дерева Wizard) используйте
{@link FormWizardHandle} через `useRef`.

**Signature:**
```typescript
export function useFormWizard<T extends Record<string, any>>(): FormWizardContextValue<T>
```

**Returns:** Текущий {@link FormWizardContextValue}.

**Examples:**

Минимальное использование внутри custom-step
```tsx
function MyStepComponent() {
const { currentStep, isLastStep } = useFormWizard();
return <p>Шаг {currentStep}{isLastStep && ' — последний'}</p>;
}
```

Условный рендер кнопки на основе isValidating + completedSteps
```tsx
function ProgressBadge() {
const { currentStep, totalSteps, completedSteps, isValidating } =
useFormWizard<CreditApplication>();

if (isValidating) return <span>Проверяем шаг {currentStep}...</span>;
return (
<span>
Завершено {completedSteps.length} из {totalSteps}
</span>
);
}
```

_Source: src/components/form-wizard/FormWizardContext.tsx_

### useValidationErrorResolver

**Kind:** `function`

Возвращает текущий резолвер сообщений из контекста. Если форма не обёрнута в
{@link ValidationMessagesProvider}, вернётся {@link defaultErrorResolver}. Используется
внутри `useFormField` для преобразования {@link ValidationError} в отображаемую строку;
вызывайте напрямую, если строите собственный рендер ошибок.

**Signature:**
```typescript
export function useValidationErrorResolver(): ValidationErrorResolver
```

**Returns:** Активный {@link ValidationErrorResolver} (или дефолтный, если провайдера нет).

**Examples:**

Кастомный рендер ошибок с активным резолвером
```tsx
import { useValidationErrorResolver } from '@reformer/cdk';
import { useFormControl } from '@reformer/core';

function FieldErrors({ control }: { control: FieldNode<string> }) {
const resolve = useValidationErrorResolver();
const { errors } = useFormControl(control);
return <>{errors.map((e) => <span key={e.code}>{resolve(e)}</span>)}</>;
}
```

_Source: src/validation/error-resolver.tsx_

### ValidationErrorResolver

**Kind:** `type`

Преобразует ошибку валидации в отображаемую строку.

**Signature:**
```typescript
export type ValidationErrorResolver = (error: ValidationError) => string;
```

_Source: src/validation/error-resolver.tsx_

### ValidationMessagesProvider

**Kind:** `function`

Провайдер резолвера сообщений валидации (i18n) для поддерева формы. Все `useFormField` внутри
начинают отображать не `error.message`, а результат переданного `resolver`. Оберните форму
резолвером из {@link createMessageResolver}, чтобы включить локализацию по кодам ошибок.

**Signature:**
```typescript
export function ValidationMessagesProvider(props: {
  resolver: ValidationErrorResolver;
  children: ReactNode;
}): ReactNode
```

**Examples:**

Локализация всей формы через таблицу сообщений
```tsx
import { ValidationMessagesProvider, createMessageResolver } from '@reformer/cdk';

const ru = createMessageResolver({
required: () => 'Обязательное поле',
email: () => 'Введите корректный email',
minLength: (p) => `Минимум ${p?.minLength} символов`,
});

<ValidationMessagesProvider resolver={ru}>
<MyForm />
</ValidationMessagesProvider>
```

**See also:**
- {@link createMessageResolver} — построение резолвера из таблицы кодов.
- {@link useValidationErrorResolver} — чтение текущего резолвера из контекста.

_Source: src/validation/error-resolver.tsx_

### ValidationMessageTable

**Kind:** `type`

Таблица `code → (params) => message`.

**Signature:**
```typescript
export type ValidationMessageTable = Record<string, (params?: Record<string, unknown>) => string>;
```

_Source: src/validation/error-resolver.tsx_
