attaform 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/abstract.d.mts +15 -21
  2. package/dist/abstract.mjs +3 -3
  3. package/dist/dev/abstract.mjs +4 -4
  4. package/dist/dev/chunks/devtools.mjs +4 -4
  5. package/dist/dev/directive.mjs +1 -1
  6. package/dist/dev/history.mjs +2 -2
  7. package/dist/dev/index.mjs +5 -5
  8. package/dist/dev/runtime/plugins/attaform.mjs +1 -1
  9. package/dist/dev/shared/{attaform-dev.DUZckyV8.mjs → attaform-dev.B1q-x9R9.mjs} +4 -4
  10. package/dist/dev/shared/{attaform-dev.BcYT6tFh.mjs → attaform-dev.CZz4GYZ6.mjs} +21 -9
  11. package/dist/dev/shared/{attaform-dev.BisFstxh.mjs → attaform-dev.Caibt7sb.mjs} +16 -15
  12. package/dist/dev/shared/{attaform-dev.DTrHonpc.mjs → attaform-dev.CtyqGia7.mjs} +176 -260
  13. package/dist/dev/shared/{attaform-dev.BK6Ge2qR.mjs → attaform-dev.DJ3nsWw6.mjs} +86 -122
  14. package/dist/dev/shared/{attaform-dev.BcNtyOmf.mjs → attaform-dev.DlaT4xGd.mjs} +4 -4
  15. package/dist/dev/shared/{attaform-dev.D6CLvaQT.mjs → attaform-dev.k5X47GMD.mjs} +520 -395
  16. package/dist/dev/shared/{attaform-dev.CQ2rJiAX.mjs → attaform-dev.y9x9Vp9F.mjs} +324 -234
  17. package/dist/dev/shared/{attaform-dev.BQc5QV3Y.mjs → attaform-dev.zlvDr0WB.mjs} +2 -2
  18. package/dist/dev/zod-v3.mjs +6 -6
  19. package/dist/dev/zod-v4.mjs +6 -6
  20. package/dist/dev/zod.mjs +5 -5
  21. package/dist/directive.d.mts +4 -4
  22. package/dist/directive.mjs +1 -1
  23. package/dist/esbuild.d.mts +11 -0
  24. package/dist/esbuild.mjs +1 -1
  25. package/dist/history.d.mts +3 -32
  26. package/dist/history.mjs +2 -2
  27. package/dist/index.d.mts +67 -70
  28. package/dist/index.mjs +4 -4
  29. package/dist/nuxt.d.mts +4 -3
  30. package/dist/nuxt.mjs +3 -6
  31. package/dist/rollup.d.mts +11 -0
  32. package/dist/rollup.mjs +1 -1
  33. package/dist/rspack.d.mts +19 -11
  34. package/dist/rspack.mjs +1 -1
  35. package/dist/runtime/components/AttaformDevtoolsPanel.vue +15 -15
  36. package/dist/runtime/components/DevtoolsValueTree.vue +2 -2
  37. package/dist/runtime/plugins/attaform.mjs +1 -1
  38. package/dist/shared/{attaform.BRDDQeHV.mjs → attaform.8mfKZCwH.mjs} +2 -2
  39. package/dist/shared/{attaform.DRUT703x.d.mts → attaform.B-Tb62qV.d.mts} +12 -12
  40. package/dist/shared/{attaform.v0fQtWyA.mjs → attaform.B1rH9ht6.mjs} +1 -1
  41. package/dist/shared/{attaform.DhnPb6nK.mjs → attaform.BAf3UNn4.mjs} +1 -1
  42. package/dist/shared/{attaform.BXneBxEj.mjs → attaform.BWUn8Kjy.mjs} +85 -121
  43. package/dist/shared/{attaform.WiGdIqgx.mjs → attaform.C4shmwyJ.mjs} +2 -2
  44. package/dist/shared/{attaform.Rc9u4Vfw.d.mts → attaform.CRVe8s82.d.mts} +83 -95
  45. package/dist/shared/attaform.D9RqPgGu.d.mts +42 -0
  46. package/dist/shared/attaform.DAo0QBmS.d.mts +2173 -0
  47. package/dist/shared/{attaform.BK0cgu2K.mjs → attaform.DBCbxlyk.mjs} +62 -4
  48. package/dist/shared/{attaform.CUfrMK7t.mjs → attaform.DL2nl_QR.mjs} +15 -14
  49. package/dist/shared/{attaform.DNJOWjQw.mjs → attaform.DNPJPOO7.mjs} +299 -218
  50. package/dist/shared/attaform.DZnrDrNb.d.mts +4657 -0
  51. package/dist/shared/{attaform.B3resMQ1.mjs → attaform.DxKQsqFZ.mjs} +496 -371
  52. package/dist/shared/{attaform.ByWP6kSx.mjs → attaform.L011tHgJ.mjs} +3 -3
  53. package/dist/shared/{attaform.CJ8DpRGj.mjs → attaform.LUPC-2Qg.mjs} +175 -257
  54. package/dist/transforms.d.mts +153 -26
  55. package/dist/transforms.mjs +1 -1
  56. package/dist/vite.d.mts +29 -52
  57. package/dist/vite.mjs +3 -3
  58. package/dist/webpack.d.mts +20 -12
  59. package/dist/webpack.mjs +1 -1
  60. package/dist/zod-v3.d.mts +90 -96
  61. package/dist/zod-v3.mjs +5 -5
  62. package/dist/zod-v4.d.mts +120 -118
  63. package/dist/zod-v4.mjs +5 -5
  64. package/dist/zod.d.mts +5 -5
  65. package/dist/zod.mjs +4 -4
  66. package/package.json +7 -6
  67. package/skills/attaform/SKILL.md +1 -1
  68. package/skills/attaform/references/custom-components.md +4 -0
  69. package/dist/shared/attaform.Clz0ArIi.d.mts +0 -44
  70. package/dist/shared/attaform.UGyOK83T.d.mts +0 -2366
  71. package/dist/shared/attaform.bKLzMU0o.d.mts +0 -4761
@@ -0,0 +1,4657 @@
1
+ import { ObjectDirective, Ref, ComputedRef, MaybeRefOrGetter } from 'vue';
2
+
3
+ /**
4
+ * Schema-attached field metadata: the shared types used by both Zod
5
+ * adapters and the unified `attaform/zod` entry so a consumer's data
6
+ * flow reads the same shape regardless of which path runs at lookup.
7
+ *
8
+ * Storage lives in the cross-adapter `field-meta-store` core: a pair
9
+ * of WeakMaps (single-payload for last-write-wins reads, list-of-
10
+ * payloads for shared-schema disambiguation). Every entry's
11
+ * `fieldMeta` re-exports the same registry-shaped object, so
12
+ * `withMeta`/`fieldMeta.add` writes from one entry surface at lookup
13
+ * through any other.
14
+ *
15
+ * `withMeta(schema, payload)` clones the schema before registering,
16
+ * so each call gets fresh identity (the WeakMap keys on reference).
17
+ * The cloning strategy depends on the major: Zod 4 schemas use the
18
+ * native `.clone()`, Zod 3 schemas reconstruct via constructor +
19
+ * `_def`. The unified entry's `withMeta` runtime-branches on which
20
+ * one is in play.
21
+ *
22
+ * Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
23
+ * which returns a fully-resolved `ResolvedFieldMeta` (label /
24
+ * description / placeholder / meta) so the per-leaf and per-container
25
+ * `FieldState` producers in core never see the version split.
26
+ */
27
+ /**
28
+ * The metadata a consumer attaches to a schema node: a short label
29
+ * (presentational), longer description (helper text), placeholder
30
+ * (input affordance). Declared as `interface` (not `type`) so
31
+ * downstream apps can extend the shape via TypeScript declaration
32
+ * merging when they want to register richer payloads (tooltips,
33
+ * icons, badge counts, etc.):
34
+ *
35
+ * declare module 'attaform/zod' {
36
+ * interface FieldMetaPayload {
37
+ * tooltip?: string
38
+ * }
39
+ * }
40
+ *
41
+ * After augmentation, `withMeta(schema, { tooltip: '…' })` is typed
42
+ * and `state.meta.tooltip` reads back as `string | undefined`.
43
+ *
44
+ * Every key is optional. Empty payloads (no keys registered) are
45
+ * indistinguishable from "not registered at all", both surfacing as
46
+ * fallbacks (humanize for label, undefined for the rest).
47
+ */
48
+ interface FieldMetaPayload {
49
+ label?: string;
50
+ description?: string;
51
+ placeholder?: string;
52
+ }
53
+ /**
54
+ * The fully-resolved metadata returned by
55
+ * `AbstractSchema.getFieldMetaAtPath(path)`. Adapters apply the
56
+ * precedence rules:
57
+ *
58
+ * - `label`: registry payload → `humanize(lastSegment)`
59
+ * - `description`: registry payload → schema's `.describe()` value → `undefined`
60
+ * - `placeholder`: registry payload → `undefined`
61
+ * - `meta`: the full registered payload, frozen; `{}` if nothing registered
62
+ *
63
+ * `label` is always a non-empty string at leaves (humanize fallback
64
+ * guarantees this for any non-numeric segment). For containers it
65
+ * may collapse to the empty string when the path is empty (root) or
66
+ * the segment is a numeric index, so callers display "" or substitute
67
+ * a context-appropriate fallback.
68
+ */
69
+ type ResolvedFieldMeta = {
70
+ readonly label: string;
71
+ readonly description: string | undefined;
72
+ readonly placeholder: string | undefined;
73
+ readonly meta: Readonly<FieldMetaPayload>;
74
+ };
75
+
76
+ declare const pathKeyBrand: unique symbol;
77
+ /**
78
+ * Branded string identifier for a canonicalised path, and a useful `Map`
79
+ * key: two paths resolving to the same canonical form produce the same
80
+ * `PathKey`. Treat it as opaque and do not parse it.
81
+ */
82
+ type PathKey = string & {
83
+ readonly [pathKeyBrand]: 'PathKey';
84
+ };
85
+ /** A single path segment: a property name, or an array index. */
86
+ type Segment = string | number;
87
+ /** A structured path as a read-only sequence of segments. */
88
+ type Path = readonly Segment[];
89
+ /**
90
+ * Parse a dotted-string path into structured segments.
91
+ *
92
+ * ```ts
93
+ * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
94
+ * parseDottedPath('items.0.name') // ['items', 0, 'name']
95
+ * parseDottedPath('') // [''] (the empty-string key)
96
+ * ```
97
+ *
98
+ * The empty-string input `''` is the **literal empty-key path** `['']`,
99
+ * an ordinary (if rare) field address, not the root. Use the array
100
+ * form `[]` for the root. Form-level errors (root `.refine()`,
101
+ * `setErrors` with no path) live at the root path `[]` and are read via
102
+ * `errors([])`, never `errors('')`.
103
+ *
104
+ * Throws `InvalidPathError` for paths with empty INTERNAL segments
105
+ * (`'a..b'`, leading or trailing dots). For keys containing literal
106
+ * dots, pass an array form (`['user.name']`) instead.
107
+ */
108
+ declare function parseDottedPath(path: string): Segment[];
109
+ /**
110
+ * Canonicalise a path into structured segments plus a stable string
111
+ * key. Accepts either dotted-string or array form; integer-looking
112
+ * segments normalise to numbers.
113
+ *
114
+ * ```ts
115
+ * canonicalizePath('items.0.name')
116
+ * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
117
+ *
118
+ * canonicalizePath(['items', 0, 'name'])
119
+ * // → same result
120
+ * ```
121
+ *
122
+ * The returned `key` works as a `Map` or `Set` key: equal paths produce
123
+ * equal keys whichever input form they arrived in.
124
+ */
125
+ declare function canonicalizePath(input: string | Path): {
126
+ segments: readonly Segment[];
127
+ key: PathKey;
128
+ };
129
+ /**
130
+ * The root path, an empty segment tuple. Pass it to any API taking a
131
+ * `Path` to address the form value whole. It is also where form-level
132
+ * errors live: a root `.refine()` message, a hydration failure, and a
133
+ * `setErrors` entry with no path all land at `[]`. The aggregate reads
134
+ * (`errors()`, `errors([])`, `meta.errors`) surface them alongside field
135
+ * errors, and `meta.ownErrors` returns the root bucket alone.
136
+ *
137
+ * The empty SEGMENT tuple `[]` is structurally unconstructible as a field
138
+ * path, so it can never collide with a schema key. The empty STRING key
139
+ * `''` (path `['']`, key `'[""]'`) is an ordinary field address.
140
+ */
141
+ declare const ROOT_PATH: Path;
142
+ /** Stable string key for the root path. */
143
+ declare const ROOT_PATH_KEY: PathKey;
144
+ /**
145
+ * `true` when `path` starts with every segment of `prefix`, in order. The
146
+ * empty `prefix` matches every path, the ROOT prefix being universal.
147
+ *
148
+ * Walks segments rather than `PathKey` strings because the data it
149
+ * operates on (`meta.errors[].path`, for one) carries segment arrays
150
+ * directly.
151
+ *
152
+ * ```ts
153
+ * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
154
+ * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
155
+ * isPathPrefix([], ['anything']) // true (root prefix)
156
+ * ```
157
+ */
158
+ declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
159
+
160
+ /**
161
+ * Per-path field status. Replaced wholesale (not mutated in place) on
162
+ * every change. Five semantic groups:
163
+ *
164
+ * - `connected`: is a DOM element registered for this path?
165
+ * - `focused` / `blurred`: DOM-state flags. `null` while no element
166
+ * is connected (no DOM means the concepts don't apply); plain
167
+ * booleans once connected, with the invariant `blurred === !focused`
168
+ * enforced by `markFocused`.
169
+ * - `touched`: focus/blur history, not DOM state. Always a plain
170
+ * boolean: `false` at registration, sticky `true` after first blur,
171
+ * cleared only by `form.reset()` / `form.resetField(path)`. Persists
172
+ * across disconnects so v-if'd-away fields don't lose their touched
173
+ * state on rehide (wizard "show review of touched fields" patterns
174
+ * rely on this).
175
+ * - `interacted`: value-mutation history, not DOM state. Plain
176
+ * boolean: `false` at registration, sticky `true` once the user
177
+ * issues a value edit through the directive's input listeners
178
+ * (never on hydration, default seeding, or programmatic setValue);
179
+ * cleared with `touched` by `form.reset()` / `form.resetField(path)`.
180
+ * - `blurredAfterInteraction`: the first blur that follows a value
181
+ * edit (the field has been edited and then left). Plain boolean,
182
+ * sticky `true`. A tab-through blur with no prior edit does NOT set
183
+ * it (`interacted` is still false at that blur). Composes
184
+ * `interacted` with the departure; drives the default display gate.
185
+ */
186
+ type FieldRecord = {
187
+ readonly path: Path;
188
+ readonly updatedAt: string | null;
189
+ readonly connected: boolean;
190
+ readonly focused: boolean | null;
191
+ readonly blurred: boolean | null;
192
+ readonly touched: boolean;
193
+ readonly interacted: boolean;
194
+ readonly blurredAfterInteraction: boolean;
195
+ };
196
+ /** Per-path DOM element tracking. Client-only. */
197
+ type ElementRecord = {
198
+ /**
199
+ * Original Path captured at first registration. Stored alongside the
200
+ * elements Set so the DOM-order sort cache can recover the structured
201
+ * Path without round-tripping through `JSON.parse(pathKey)`.
202
+ */
203
+ readonly path: Path;
204
+ readonly elements: Set<HTMLElement>;
205
+ };
206
+ /**
207
+ * Per-path record stored in `originals`. Pairing `segments` with the tracked
208
+ * value means `dirty` and `resetField`'s container loop don't have to
209
+ * `JSON.parse(pathKey)` on every iteration, the canonical Path already
210
+ * sitting next to the value it belongs to. PathKey still keys the Map (the
211
+ * stable string is the only collision-free identifier), but downstream
212
+ * iteration reads `segments` directly.
213
+ */
214
+ type OriginalsRecord = {
215
+ readonly segments: Path;
216
+ readonly value: unknown;
217
+ };
218
+
219
+ /** Internal brand for the `Unset` type. Never exposed at runtime. */
220
+ declare const _unsetBrand: unique symbol;
221
+ /**
222
+ * Brand-typed sentinel admitted at every primitive leaf of
223
+ * `DefaultValuesShape<T>`, `setValue`, and `reset`. The runtime value
224
+ * is exported as {@link unset} under the same name.
225
+ */
226
+ type Unset = typeof _unsetBrand;
227
+ /**
228
+ * The `unset` sentinel: pass it as a primitive leaf's value to mark
229
+ * the field **displayed-empty** while storage holds the schema's slim
230
+ * default (`0` / `''` / `false` / `0n`).
231
+ *
232
+ * Use it wherever a primitive leaf value is expected:
233
+ *
234
+ * ```ts
235
+ * const form = useForm({
236
+ * schema: z.object({ income: z.number() }),
237
+ * defaultValues: { income: unset }, // UI starts blank, storage holds 0
238
+ * key: 'housing',
239
+ * })
240
+ *
241
+ * form.setValue('income', unset) // re-blank a field after a write
242
+ * form.reset({ income: unset }) // reset to the blank state
243
+ * ```
244
+ *
245
+ * Accepted at any `string` / `number` / `boolean` / `bigint` leaf in
246
+ * `defaultValues`, `setValue(path, unset)`, and `form.reset({ … })`.
247
+ *
248
+ * The path joins the form's `blankPaths` set as long as it stays
249
+ * unset. Required schemas (no `.optional()` / `.nullable()` /
250
+ * `.default(N)`) raise `"No value supplied"` on submit while a leaf
251
+ * is in `blankPaths`; optional / nullable / has-default schemas
252
+ * accept the empty case as their wrapper allows.
253
+ *
254
+ * Storage never holds the symbol; the runtime translates it at the
255
+ * API boundary, so reads through `form.values` always see the slim
256
+ * default. Cross-bundle / SSR-safe: backed by `Symbol.for(...)` so
257
+ * every realm gets the same sentinel.
258
+ *
259
+ * @see {@link isUnset}, the type guard that narrows a value back to {@link Unset}.
260
+ * @see `docs/validation/blank.md` for the model behind blank-aware fields.
261
+ */
262
+ declare const unset: Unset;
263
+ /**
264
+ * Type guard: `true` when `value` is the `unset` sentinel.
265
+ *
266
+ * ```ts
267
+ * if (isUnset(payload.income)) {
268
+ * // payload.income is the sentinel; the field will display empty
269
+ * }
270
+ * ```
271
+ */
272
+ declare function isUnset(value: unknown): value is Unset;
273
+
274
+ /**
275
+ * The minimum shape any form value satisfies, a plain record. Use it as
276
+ * a constraint for composables that work generically across forms, such
277
+ * as a custom hook taking any form's `useForm` return.
278
+ */
279
+ type GenericForm = Record<string, unknown>;
280
+ /** `true` when `T` is an object or array. */
281
+ type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
282
+ /**
283
+ * Shared recursion body backing `PartialFlatPath` and
284
+ * `RegisterFlatPath`. One walk over `Form`, with `Mode` deciding
285
+ * whether container paths are emitted alongside their reachable leaves.
286
+ *
287
+ * - `'partial'`: emit every container path (object containers,
288
+ * nested-object array roots, array-of-object element containers).
289
+ * Used by `setValue`, `form.values.<path>`, and every read-side API
290
+ * that can address a container.
291
+ * - `'register'`: skip container paths, since `v-register` binds onto a
292
+ * leaf-backing native element. Primitive arrays still admit the
293
+ * array-root path under both modes, because multi-select and
294
+ * grouped-checkbox bindings register onto the array itself.
295
+ */
296
+ type FlatPathBuilder<Form, Mode extends 'partial' | 'register', Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? Mode extends 'partial' ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}` | `${Key}.${number}` : Value extends GenericForm ? Mode extends 'partial' ? `${Key}` | `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${FlatPathBuilder<Form[Key], Mode>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? Mode extends 'partial' ? `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}` : never) : never : never;
297
+ /**
298
+ * Backs `FlatPath` in its default partial-path mode. Reach for
299
+ * `FlatPath`; this alias is not part of the stable surface.
300
+ *
301
+ * It is exported so `rollup-plugin-dts` keeps it as a named alias in
302
+ * the bundled `.d.ts` rather than inlining the full template-literal
303
+ * recursion at every reference site. Inlined, it compounds into TS2589
304
+ * territory once several complex forms share a scope.
305
+ *
306
+ * The `Form extends unknown` wrapper distributes over a top-level
307
+ * union so each member contributes its OWN `keyof`. That is what makes
308
+ * a `z.discriminatedUnion` root expose every variant's keys as
309
+ * addressable paths, rather than the intersection that naked
310
+ * `keyof (A | B)` collapses to. Interior unions already distribute
311
+ * inside `FlatPathBuilder` at its `infer Value` step.
312
+ */
313
+ type PartialFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'partial'> : never;
314
+ /**
315
+ * Union of dotted-string paths reachable inside `Form`. For
316
+ * `{ user: { email: string }, items: string[] }`:
317
+ *
318
+ * `'user' | 'user.email' | 'items' | 'items.0' | 'items.1' | ...`
319
+ *
320
+ * Every path-addressed API (`setValue(path, value)`, `register(path)`,
321
+ * `toRef(path)`) takes one, so paths autocomplete and typos are
322
+ * compile errors.
323
+ */
324
+ type FlatPath<Form> = PartialFlatPath<Form>;
325
+ /**
326
+ * Convert a tuple of path segments to its dotted-string equivalent.
327
+ *
328
+ * `JoinSegments<['cargo', 'items', 0, 'sku']>` is `'cargo.items.0.sku'`
329
+ *
330
+ * Depth is bounded by the tuple length, typically 3 or 4, so the cost
331
+ * does not scale with `FlatPath<Form>`. Template literal types
332
+ * distribute over unions, so a segment union propagates into the joined
333
+ * path: `JoinSegments<['pickup' | 'delivery', 'line1']>` is
334
+ * `'pickup.line1' | 'delivery.line1'`. That is what makes tuple-form
335
+ * path APIs work inside a `v-for` over a prefix variable, since the
336
+ * joined result is checked against the existing `FlatPath<Form>` /
337
+ * `RegisterFlatPath<Form>` unions instead of a separate enumeration.
338
+ */
339
+ type JoinSegments<S extends ReadonlyArray<string | number>, Acc extends string = ''> = S extends readonly [
340
+ infer Head extends string | number,
341
+ ...infer Rest extends ReadonlyArray<string | number>
342
+ ] ? Acc extends '' ? JoinSegments<Rest, `${Head}`> : JoinSegments<Rest, `${Acc}.${Head}`> : Acc;
343
+ /**
344
+ * The advice half of a segment-array rejection, used when `S` never
345
+ * inferred a tuple at all.
346
+ *
347
+ * A caller passing a plain `string` lands here: inference falls back to
348
+ * the constraint, and `JoinSegments` of a non-tuple array is `''`.
349
+ * Naming a path would be a lie, so the message names the remedies
350
+ * instead. This is the shape behind #568, where a row component
351
+ * received its path prefix as an untyped `string` prop and every
352
+ * concatenation widened to `string`.
353
+ */
354
+ type PlainStringAdvice = 'attaform: a plain string cannot be checked against the schema. Pass a literal path, type the dynamic prefix, or use the segment-array form.';
355
+ /**
356
+ * Why a segment array was rejected, as a type the compiler prints.
357
+ *
358
+ * Every segment-array overload brands its parameter
359
+ * `S & (<joined path is valid> ? unknown : <this>)`. The brand must
360
+ * stay a string literal: `S & never` is `never`, which makes TypeScript
361
+ * report the parameter as `never` and say nothing about what was wrong,
362
+ * while a string literal intersects without collapsing, so the sentence
363
+ * survives into the diagnostic and the argument keeps its inferred
364
+ * tuple type:
365
+ *
366
+ * ```
367
+ * Argument of type '["boxes", 3, "nope"]' is not assignable to parameter
368
+ * of type 'readonly ["boxes", 3, "nope"] &
369
+ * "attaform: 'boxes.3.nope' is not a path in this form's schema"'.
370
+ * ```
371
+ *
372
+ * Nothing can satisfy the brand, so a wrong path stays a compile error;
373
+ * only the message changes.
374
+ */
375
+ type SegmentPathRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a path in this form's schema`;
376
+ /**
377
+ * `SegmentPathRejection` for `register`, whose accepted set is
378
+ * `RegisterFlatPath` rather than `FlatPath`.
379
+ *
380
+ * Worth its own sentence: a container path is in the schema and still
381
+ * not registrable, so "not a path in this form's schema" would be false
382
+ * exactly where it is most likely to be read.
383
+ */
384
+ type SegmentRegisterRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a registrable path. v-register binds a leaf input, so container paths are excluded.`;
385
+ /**
386
+ * `true` when `T` is a union, `false` for a single type. Gates
387
+ * non-homomorphic mapped-type forms so a single object type keeps its
388
+ * homomorphic `[K in keyof T]` lookup, preserving literal keys instead
389
+ * of widening to an index signature.
390
+ */
391
+ type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
392
+ /**
393
+ * Union of all keys across all members of `T`. For a single object type
394
+ * this equals `keyof T`; for `A | B` it produces `keyof A | keyof B`,
395
+ * where naked `keyof (A | B)` would intersect to common keys only.
396
+ *
397
+ * Paired with `ValueOfUnion` to merge variant key sets in the chained
398
+ * metadata proxies (`form.fields`, `form.errors`), so per-variant
399
+ * leaves stay addressable through one shape whichever discriminant is
400
+ * active.
401
+ */
402
+ type KeyofUnion<T> = T extends unknown ? keyof T : never;
403
+ /**
404
+ * Value at key `K` across union members of `T`. Members holding `K`
405
+ * contribute `T[K]`; members lacking it contribute `undefined`.
406
+ *
407
+ * That mirrors the metadata proxies at runtime: chained access works at
408
+ * every union member, and the leaf carries `T | undefined` because the
409
+ * key is absent in some variants, where the runtime returns a stable
410
+ * stub.
411
+ */
412
+ type ValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : undefined : never;
413
+ /**
414
+ * Value at key `K` across union members of `T`, dropping members that
415
+ * LACK `K` entirely: they contribute `never`, not `undefined`. The
416
+ * counterpart to `ValueOfUnion`, which injects a synthetic `undefined`
417
+ * for absent-variant keys so chained reads stay safe.
418
+ *
419
+ * `form.fields` uses it at discriminated-union keys so a variant-only
420
+ * field types as node-optional `FieldState<X> | undefined`, the node
421
+ * being absent when its variant is not active, rather than
422
+ * value-optional `FieldState<X | undefined>`, which would falsely
423
+ * promise a readable node. A genuine `undefined` from an OPTIONAL
424
+ * declaration survives; only the synthetic one is stripped.
425
+ */
426
+ type PresentValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : never : never;
427
+ /**
428
+ * Apply the discriminated-union key merge to a shape carrying values
429
+ * rather than metadata leaves. Single object types map homomorphically;
430
+ * unions of objects merge keys via `KeyofUnion` / `ValueOfUnion` so
431
+ * per-variant fields are reachable through one chained-access shape.
432
+ *
433
+ * `ValuesSurface` uses it to make an oversized-only
434
+ * `form.values.cargo.permitNumber` typecheck whichever variant is
435
+ * active, matching the runtime, where plain JS access on a missing key
436
+ * returns `undefined` rather than throwing.
437
+ *
438
+ * Date / Map / Set / RegExp / function leaves stay opaque, since a
439
+ * value read of those should preserve the platform shape.
440
+ */
441
+ type LiftedValueShape<T> = [T] extends [
442
+ string | number | boolean | bigint | symbol | null | undefined
443
+ ] ? T : [T] extends [
444
+ Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown)
445
+ ] ? T : [T] extends [ReadonlyArray<unknown>] ? T : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
446
+ [K in KeyofUnion<T>]: LiftedValueShape<ValueOfUnion<T, K>>;
447
+ } : {
448
+ [K in keyof T]: LiftedValueShape<T[K]>;
449
+ } : T;
450
+ /**
451
+ * Recursive `Partial`: every property at every depth is optional, so a
452
+ * partial override at any nesting level is valid.
453
+ */
454
+ type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem> ? DeepPartial<ArrayItem>[] : T extends object ? {
455
+ [Key in keyof T]?: DeepPartial<T[Key]>;
456
+ } : T;
457
+ /**
458
+ * Shared descent body backing `NestedType` and `NestedReadType`. Both
459
+ * walk segment by segment, distributing over union members via
460
+ * `KeyofUnion` / `ValueOfUnion`, and diverge only at the leaf:
461
+ *
462
+ * - `TaintArrayCrossings extends false` (`NestedType`): leaves are
463
+ * returned untouched, which is what the strict write-side APIs need
464
+ * (`setValue`'s value parameter, `form.fields.<path>`).
465
+ * - `TaintArrayCrossings extends true` (`NestedReadType`): leaves widen
466
+ * with `| undefined` once any segment in the walk was an array index,
467
+ * reflecting an out-of-bounds read at runtime.
468
+ *
469
+ * `_Tainted` carries the array crossing under taint mode and stays
470
+ * `false` through every arm under strict mode. Both strip nullishness
471
+ * at the root.
472
+ *
473
+ * Reach for `NestedType` or `NestedReadType`; this is not part of the
474
+ * stable surface.
475
+ */
476
+ type NestedTypeBuilder<RootValue, FlattenedPath extends string, TaintArrayCrossings extends boolean, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : Key extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, NumericKey>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : never : never : Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, NumericKey> | undefined : ValueOfUnion<_RootValue, NumericKey> : never : never : FlattenedPath extends KeyofUnion<_RootValue> ? _Tainted extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : never;
477
+ /**
478
+ * Resolve the type at a dotted-string path inside `RootValue`, for the
479
+ * strict write-side APIs:
480
+ *
481
+ * `NestedType<{ user: { email: string } }, 'user.email'>` is `string`
482
+ *
483
+ * On a discriminated-union descent it uses `KeyofUnion` /
484
+ * `ValueOfUnion`, so a per-variant key resolves to `T | undefined`
485
+ * rather than `never`. That keeps `NestedType` in lockstep with
486
+ * `FlatPath`: every path `FlatPath` says is reachable resolves to a
487
+ * useful value type, instead of collapsing to the intersection of all
488
+ * variants' keys.
489
+ *
490
+ * TypeScript caps conditional-type recursion near 50 levels, so paths
491
+ * deeper than that resolve to `never`. Real schemas do not reach it.
492
+ */
493
+ type NestedType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, false>;
494
+ /**
495
+ * Primitive-leaf marker used by `DeepPartial` and the sibling
496
+ * structural walkers. Reach for `DeepPartial`; this is not part of the
497
+ * stable surface, and is exported only so the bundled `.d.ts`
498
+ * references one alias instead of re-emitting the union at every
499
+ * recursion branch of every walker.
500
+ */
501
+ type Primitive = string | number | boolean | symbol | bigint | null | undefined;
502
+ /**
503
+ * Distinguish a tuple from a regular array, for write-side helpers that
504
+ * must preserve tuple positions instead of widening to
505
+ * `Array<element>`.
506
+ *
507
+ * `IsTuple<[string, number]>` is `true`
508
+ * `IsTuple<string[]>` is `false`
509
+ */
510
+ type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
511
+ /**
512
+ * Path-resolved type for the read-side APIs (`form.values.<path>`,
513
+ * `form.toRef(path)`, `register(path).innerRef`). Like `NestedType`,
514
+ * except that once the walk crosses an array index the result is tagged
515
+ * `| undefined`, honouring the out-of-bounds read the runtime allows.
516
+ * Discriminated-union descents follow the same rule as `NestedType`.
517
+ */
518
+ type NestedReadType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, true>;
519
+ /**
520
+ * The array a path actually addresses, nullish stripped, or `never`
521
+ * when the path does not address an array. Shared by `ArrayPath`
522
+ * (membership) and `ArrayItem` (element type) so the filter and the
523
+ * extractor cannot disagree about what counts as one.
524
+ *
525
+ * The strip is load-bearing. `Form` is the schema's INPUT shape, so
526
+ * `z.array(row).default([])` resolves to `row[] | undefined`, and
527
+ * `NestedType` tags a discriminated union's per-variant keys the same
528
+ * way on purpose. A bare `extends readonly unknown[]` predicate reads
529
+ * that tag as "not an array", which is how every optional, defaulted,
530
+ * nullable and DU-variant array fell out of the field-array helpers
531
+ * while the runtime went on accepting all of them (#541).
532
+ *
533
+ * `extends infer Leaf` binds the stripped leaf once so the guards below
534
+ * do not re-instantiate `NestedType`, and the tuple wraps suppress
535
+ * distribution so a union leaf is judged whole: `A[] | B[]` is an
536
+ * array, `A[] | string` is not.
537
+ *
538
+ * Reach for `ArrayPath` / `ArrayItem`; this alias exists so
539
+ * `rollup-plugin-dts` keeps one named reference in the bundled `.d.ts`.
540
+ */
541
+ type ArrayLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? Leaf : never : never;
542
+ /**
543
+ * `FlatPath<Form>` narrowed to the paths whose leaf is an array, so the
544
+ * typed field-array helpers (append, remove, swap) accept only paths
545
+ * that address one: `append('email', ...)` on a `{ email: string }` is
546
+ * a compile error. Optionality is not a disqualifier; see `ArrayLeafOf`
547
+ * for what counts as an array here.
548
+ *
549
+ * `P extends string` re-triggers distribution over the `FlatPath<Form>`
550
+ * union so the conditional evaluates per member. Without it the branch
551
+ * reduces against the union as a whole and collapses to `never` as soon
552
+ * as one member fails the predicate.
553
+ */
554
+ type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [ArrayLeafOf<Form, P>] extends [never] ? never : P : never;
555
+ /**
556
+ * Element type of the array addressed by `Path`. Callers constrain
557
+ * `Path extends ArrayPath<Form>`, so this is always well-defined. It
558
+ * rides on `ArrayLeafOf`, the same alias `ArrayPath` admits the path
559
+ * with, so an accepted path never hands its helper a `never` value
560
+ * slot.
561
+ */
562
+ type ArrayItem<Form, Path extends ArrayPath<Form>> = ArrayLeafOf<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
563
+ /**
564
+ * The record a path actually addresses, nullish stripped, or `never`
565
+ * when the path does not address one. Shared by `RecordPath`
566
+ * (membership) and `RecordValue` (value type).
567
+ *
568
+ * `string extends keyof Leaf` is the index-signature probe: it holds
569
+ * for `Record<string, V>`, whose `keyof` is `string`, and fails for a
570
+ * fixed object, whose `keyof` is a literal key union. The leading array
571
+ * guard keeps arrays, which also satisfy the object check, out of the
572
+ * record set.
573
+ *
574
+ * Nullish is stripped first for the reason `ArrayLeafOf` gives, with an
575
+ * extra bite here: `keyof (Record<string, V> | undefined)` is `never`,
576
+ * so on a `.default({})` or `.optional()` record the probe could not
577
+ * fire at all and `form.record(path)` rejected the path (#541).
578
+ *
579
+ * Reach for `RecordPath` / `RecordValue`; exported for the same `.d.ts`
580
+ * reason as `ArrayLeafOf`.
581
+ */
582
+ type RecordLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? never : [Leaf] extends [Record<string, unknown>] ? string extends keyof Leaf ? Leaf : never : never : never;
583
+ /**
584
+ * Companion to `ArrayPath`: `FlatPath<Form>` narrowed to the paths
585
+ * whose leaf is a record, meaning an object with an open string-keyed
586
+ * index signature such as `z.record(z.string(), V)`. A fixed-shape
587
+ * `z.object({ ... })` is excluded, since its keys are statically known
588
+ * and it has no `string` index signature.
589
+ */
590
+ type RecordPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [RecordLeafOf<Form, P>] extends [never] ? never : P : never;
591
+ /**
592
+ * Value type of the record addressed by `Path`, the `V` in a
593
+ * `Record<string, V>`. Callers constrain `Path extends
594
+ * RecordPath<Form>`, so the leaf is always an open string-keyed record.
595
+ * It rides on `RecordLeafOf`, the same alias `RecordPath` admits the
596
+ * path with, so the two cannot disagree.
597
+ */
598
+ type RecordValue<Form, Path extends RecordPath<Form>> = RecordLeafOf<Form, Path> extends Record<string, infer Value> ? Value : never;
599
+ /**
600
+ * Widens primitive-literal leaves to their primitive supertype, to
601
+ * match the runtime's slim-primitive write contract.
602
+ *
603
+ * WriteShape<{ color: 'red' | 'green' }>
604
+ * // { color: string }
605
+ * WriteShape<{ count: 42 }>
606
+ * // { count: number }
607
+ *
608
+ * The runtime write gate accepts any value whose primitive type matches
609
+ * the schema's slim primitive set at that path. Refinement-level
610
+ * constraints (enum membership, literal equality, format checks, length
611
+ * and range bounds, regex, custom predicates) are not enforced at write
612
+ * time; they surface through field-level validation. So
613
+ * `setValue('color', 'magenta')` and `defaultValues: { color: 'teal' }`
614
+ * are not type errors despite being out-of-enum at the validation
615
+ * layer.
616
+ *
617
+ * Tuple positions keep their literal types through the homomorphic
618
+ * mapped form, so `[string, number]` stays a 2-tuple of widened
619
+ * primitives instead of collapsing to `Array<string | number>`, and
620
+ * tuple detection runs before the array branch so positionally-typed
621
+ * literals survive. Date / RegExp / Map / Set / function instances pass
622
+ * through unchanged, since the runtime accepts them as their own slim
623
+ * kinds.
624
+ *
625
+ * This stays STRICT, with no `| Unset` widening, because it types the
626
+ * prev-value argument of `setValue(path, prev => ...)`, which always
627
+ * receives a real value, plus read-side types and internals like
628
+ * `FieldStateMap<T>`. The consumer-facing write-value type is
629
+ * `DefaultValuesShape<T>`, which adds `| Unset` at every recursable
630
+ * position.
631
+ */
632
+ type WriteShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends bigint ? bigint : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
633
+ -readonly [K in keyof T]: WriteShape<T[K]>;
634
+ } : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
635
+ -readonly [K in keyof T]: WriteShape<T[K]>;
636
+ } : Array<WriteShape<U>> : T extends object ? {
637
+ [K in keyof T]: WriteShape<T[K]>;
638
+ } : T;
639
+ /**
640
+ * Walk `T` and add `| Unset` at every primitive leaf except symbol,
641
+ * null and undefined, at every opaque leaf (`Date`, `RegExp`, `Map`,
642
+ * `Set`, functions), and at every container position. The recursion
643
+ * topology mirrors `WriteShape<T>`, which is what lets
644
+ * `DefaultValuesShape<T>` be a one-line composition.
645
+ *
646
+ * Symbol / null / undefined leaves pass through untouched, so the
647
+ * runtime sentinel does not pollute leaf semantics it has no business
648
+ * carrying. Container positions widen so a single `unset` at any level
649
+ * recursively marks every descendant primitive blank.
650
+ *
651
+ * Reach for `DefaultValuesShape`; this is not part of the stable
652
+ * surface.
653
+ */
654
+ type AugmentWithUnset<T> = T extends string | number | boolean | bigint ? T | Unset : T extends symbol | null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
655
+ -readonly [K in keyof T]: AugmentWithUnset<T[K]>;
656
+ } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
657
+ -readonly [K in keyof T]: AugmentWithUnset<T[K]>;
658
+ } | Unset : Array<AugmentWithUnset<U>> | Unset : T extends object ? {
659
+ [K in keyof T]: AugmentWithUnset<T[K]>;
660
+ } | Unset : T;
661
+ /**
662
+ * `WriteShape<T>` plus `Unset`, the brand-typed sentinel that marks a
663
+ * leaf as starting displayed-empty in `defaultValues`, `setValue` and
664
+ * `reset`.
665
+ *
666
+ * `Unset` is admitted at every position: primitive leaves, opaque
667
+ * leaves (`Date`, `RegExp`, `Map`, `Set`, functions), and containers
668
+ * (objects, arrays, tuples, records, discriminated unions, optional and
669
+ * nullable wrappers). A container `unset` recursively marks every
670
+ * primitive descendant blank, so `defaultValues: { profile: unset }`
671
+ * and `setValue('cargo', unset)` typecheck cleanly.
672
+ *
673
+ * DefaultValuesShape<{ income: number; name: string; age: 21 }>
674
+ * // { income: number | Unset; name: string | Unset; age: number | Unset } | Unset
675
+ */
676
+ type DefaultValuesShape<T> = AugmentWithUnset<WriteShape<T>>;
677
+ /**
678
+ * The type accepted at `defaultValues` and at `reset()`'s parameter:
679
+ * `DeepPartial` and `DefaultValuesShape` fused into a single walk, so
680
+ * every level is optional and every position, primitive leaf, opaque
681
+ * leaf or container, admits `| Unset`.
682
+ *
683
+ * The fusion is deliberate and should not be re-split into
684
+ * `DeepPartial<DefaultValuesShape<F>>`. Both passes have identical
685
+ * topology (object to mapped, tuple to positional, array to recurse,
686
+ * primitive to terminal), and walking twice exhausts the depth budget
687
+ * at call sites wiring several complex forms into one scope. One walk
688
+ * also keeps opaque leaves (`Date`, `Map`, `Set`, `RegExp`, functions)
689
+ * intact when their containing property is optional, where
690
+ * `DeepPartial`'s pass would structurally destructure them.
691
+ *
692
+ * ```ts
693
+ * type T = DefaultValuesInput<{
694
+ * email: string
695
+ * joinedAt: Date
696
+ * profile: { name: string; age: number }
697
+ * }>
698
+ * // → {
699
+ * // email?: string | Unset
700
+ * // joinedAt?: Date | Unset
701
+ * // profile?: { name?: string | Unset; age?: number | Unset } | Unset
702
+ * // } | Unset
703
+ * ```
704
+ */
705
+ type DefaultValuesInput<T> = T extends string ? string | Unset : T extends number ? number | Unset : T extends boolean ? boolean | Unset : T extends bigint ? bigint | Unset : T extends symbol ? symbol : T extends null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
706
+ -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
707
+ } | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
708
+ -readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
709
+ } | Unset : Array<DefaultValuesInput<U>> | Unset : T extends object ? {
710
+ [K in keyof T]?: DefaultValuesInput<T[K]>;
711
+ } | Unset : T;
712
+ /**
713
+ * The type accepted at a `useForm` overload's `defaultValues` slot.
714
+ * `Form` is the schema's input projection; `SchemaInput` is the
715
+ * schema's own `z.input<Schema>`. A value is one of:
716
+ *
717
+ * - `DefaultValuesInput<Form>`, the partial, `Unset`-widened
718
+ * in-progress shape. `defaultValues` deliberately accepts a form
719
+ * mid-completion: a leaf may be blank or hold a not-yet-valid value,
720
+ * so a `z.email()` field accepts any `string`. Sharp, fully
721
+ * validated types are produced only at `handleSubmit`.
722
+ * - `SchemaInput`, the schema's full input shape.
723
+ * - a sync or async factory returning either.
724
+ *
725
+ * The `SchemaInput` arm is redundant at a concrete call site, since a
726
+ * schema's input is always assignable to its own `DefaultValuesInput`,
727
+ * and it does not weaken per-field checking (`number` is still rejected
728
+ * where `string` is expected). It earns its place in the generic
729
+ * form-wrapper case (#422): when the schema is a free type parameter
730
+ * `S`, a forwarded `z.input<S>` is REFLEXIVELY assignable to that arm,
731
+ * a relation TypeScript can decide even under a generic, whereas
732
+ * `DefaultValuesInput<Form>` alone stays a deferred conditional cascade
733
+ * that is not provably assignable and trips TS2589 / TS2769. No runtime
734
+ * effect; the slot only governs what the type checker accepts.
735
+ *
736
+ * `DefaultValuesInput<Form>` is computed ONCE, as the `DVI` argument to
737
+ * `AcceptableDefaultsOf`, rather than inlined in each arm, which would
738
+ * re-instantiate the deep cascade three times per overload and, across
739
+ * the unified entry's two overloads, tip the bundled `.d.ts` into
740
+ * TS2589. The arms stay a DIRECT union rather than a conditional over
741
+ * the cascade, so the `SchemaInput` arm remains reflexively matchable
742
+ * under a generic.
743
+ */
744
+ type AcceptableDefaults<Form, SchemaInput> = AcceptableDefaultsOf<DefaultValuesInput<Form>, SchemaInput>;
745
+ type AcceptableDefaultsOf<DVI, SchemaInput> = DVI | SchemaInput | (() => DVI | SchemaInput) | (() => Promise<DVI | SchemaInput>);
746
+
747
+ /**
748
+ * Identifier for a form: the string passed to `useForm({ key })`, used
749
+ * to look a form up by name from a distant component and to label
750
+ * errors and DevTools entries. An anonymous `useForm` allocates one
751
+ * automatically, so pick one only when the form needs stable identity.
752
+ */
753
+ type FormKey = string;
754
+ /** Per-form options threaded from `useForm` into the adapter factory. */
755
+ interface SchemaFactoryOptions {
756
+ /** Recursion ceiling for walks through recursive schemas. */
757
+ maxRecursionDepth: number;
758
+ }
759
+ /**
760
+ * A JSON-serialisable value: the recursive shape of anything that
761
+ * survives a `JSON.stringify` / `JSON.parse` round-trip unchanged. The
762
+ * type of `ValidationError.data`.
763
+ *
764
+ * The object arm is a named interface rather than an inline index
765
+ * signature because TypeScript expands an anonymous recursive alias
766
+ * eagerly and hits TS2589 once `Json` is checked inside a large
767
+ * structural type such as the form store. A named reference defers
768
+ * that expansion.
769
+ */
770
+ type Json = string | number | boolean | null | JsonArray | JsonObject;
771
+ interface JsonArray extends Array<Json> {
772
+ }
773
+ interface JsonObject {
774
+ [key: string]: Json;
775
+ }
776
+ /**
777
+ * One validation failure, as returned by `validate()`, `parse()` and
778
+ * `handleSubmit`'s `onError`, and accepted leniently as `ErrorInput` by
779
+ * `form.setErrors`.
780
+ *
781
+ * Which form produced an error is envelope-level identity: the
782
+ * `ValidationResponse` or submit result carrying the list stamps
783
+ * `formKey` once, and an aggregator merging lists across forms (the
784
+ * wizard) stamps its own envelope.
785
+ */
786
+ type ValidationError = {
787
+ /** Human-readable message describing the failure. */
788
+ message: string;
789
+ /**
790
+ * Structured path of the offending field, such as
791
+ * `['user', 'address', 0, 'line1']`.
792
+ *
793
+ * The root path `[]` is the form-level bucket, the home for errors
794
+ * belonging to no specific field: root `.refine()` messages,
795
+ * `setErrors` entries with no path, hydration failures,
796
+ * server-emitted banners. The empty-STRING path `['']` is unrelated
797
+ * and means an ordinary field whose key is the empty string.
798
+ */
799
+ path: (string | number)[];
800
+ /**
801
+ * Stable machine identifier for the failure, scoped by prefix:
802
+ *
803
+ * - `atta:` for Attaform's own codes, see `AttaformErrorCode`.
804
+ * - an adapter prefix such as `zod:`, forwarded from the underlying
805
+ * schema library's issue code where it has one.
806
+ * - anything else for consumer-defined codes (`api:duplicate-email`,
807
+ * `auth:expired-token`). Pick a prefix and stay consistent, so
808
+ * error renderers and tests branch on `code` rather than matching
809
+ * exact message strings.
810
+ */
811
+ code: string;
812
+ /**
813
+ * Optional structured payload. Attaform never sets or reads this; it
814
+ * is a passthrough for whatever a server sends alongside the message
815
+ * (a captcha challenge, a lockout `unlocks_at`, an MFA step-up
816
+ * descriptor) so the UI can act on it. Survives serialise, hydrate,
817
+ * undo and redo unchanged.
818
+ */
819
+ data?: Json | null;
820
+ };
821
+ /**
822
+ * One path's slot in the form's tagged error store. The two sources
823
+ * that can put an error at a path stay segregated: `schema` holds the
824
+ * validation pipeline's verdicts, `user` holds `setErrors` entries and
825
+ * their history and SSR replays. Reads merge schema, then derived
826
+ * blank, then user, and each writer replaces exactly its own side.
827
+ *
828
+ * Cells are immutable. Every write replaces the whole cell, or deletes
829
+ * the key when both sides are empty, so Vue's per-key Map tracking
830
+ * fires for a change on either side. An empty side is the shared frozen
831
+ * `NO_ERRORS` array, never a fresh allocation.
832
+ *
833
+ * @internal
834
+ */
835
+ type ErrorCell = {
836
+ readonly schema: readonly ValidationError[];
837
+ readonly user: readonly ValidationError[];
838
+ };
839
+ /**
840
+ * The lenient input `form.setErrors` accepts: a real `Error`, or a
841
+ * partial `ValidationError` where every field is optional.
842
+ *
843
+ * - `message`: omitted or empty coerces to `"Unknown error"`.
844
+ * - `path`: defaults to `[]`, a form-level error. Ignored by the
845
+ * path-scoped `setErrors(path, ...)` form, which stamps its own.
846
+ * - `code`: defaults to `atta:user-error`.
847
+ * - `data`: forwarded verbatim onto the produced `ValidationError`.
848
+ *
849
+ * Because every field is optional, `ValidationError` is a subtype of
850
+ * `ErrorInput`: a `ValidationError[]` read back out, or a server
851
+ * response already emitting the shape, pipes straight into
852
+ * `form.setErrors` with no adapter.
853
+ */
854
+ type ErrorInput = Error | {
855
+ message?: string;
856
+ path?: (string | number)[];
857
+ code?: string;
858
+ data?: Json | null;
859
+ };
860
+ /**
861
+ * A parse verdict as an `AbstractSchema` produces it. Adapter authors
862
+ * implement THESE.
863
+ *
864
+ * A schema knows nothing about which form asked, being a pure function
865
+ * of the schema it wraps, which is what lets one `AbstractSchema` serve
866
+ * every form built on the same schema. The owning store stamps its own
867
+ * `formKey` on the way out, producing the `ValidationResponse` arms
868
+ * below.
869
+ */
870
+ type SchemaParseSuccess<TData> = {
871
+ /** The parsed value at the validated subtree, or the whole form when `validate()` took no path. */
872
+ data: TData;
873
+ errors: undefined;
874
+ success: true;
875
+ };
876
+ /** Schema-level verdict when no data could be produced, such as a top-level type mismatch. */
877
+ type SchemaParseErrorWithoutData = {
878
+ data: undefined;
879
+ /** Non-empty list of failures. */
880
+ errors: ValidationError[];
881
+ success: false;
882
+ };
883
+ /** Schema-level verdict when the parser produced partial data alongside failures. */
884
+ type SchemaParseErrorWithData<TData> = {
885
+ data: TData;
886
+ errors: ValidationError[];
887
+ success: false;
888
+ };
889
+ /** Settled schema-level verdict. Discriminate on `success`. */
890
+ type SchemaParseResult<TData> = SchemaParseSuccess<TData> | SchemaParseErrorWithData<TData> | SchemaParseErrorWithoutData;
891
+ /** Settled validation result when the form or subtree parsed successfully. */
892
+ type ValidationResponseSuccess<TData> = SchemaParseSuccess<TData> & {
893
+ /** The form this verdict belongs to. Stamped by the store, not the schema. */
894
+ formKey: FormKey;
895
+ };
896
+ /** Settled validation result when no data could be produced. */
897
+ type ValidationResponseErrorWithoutData = SchemaParseErrorWithoutData & {
898
+ formKey: FormKey;
899
+ };
900
+ /** Settled validation result when the parser produced partial data alongside failures. */
901
+ type ValidationResponseErrorWithData<TData> = SchemaParseErrorWithData<TData> & {
902
+ formKey: FormKey;
903
+ };
904
+ /**
905
+ * Settled validation result. Discriminate on `success`:
906
+ *
907
+ * ```ts
908
+ * if (result.success) {
909
+ * // result.data is the parsed value, errors is undefined
910
+ * } else {
911
+ * // result.errors is non-empty, data may or may not be set
912
+ * }
913
+ * ```
914
+ */
915
+ type ValidationResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData> | ValidationResponseErrorWithoutData;
916
+ /**
917
+ * `ValidationResponse` without the `data` payload, for the reactive
918
+ * `validate()` status, whose consumers need only the success flag and
919
+ * the error list.
920
+ */
921
+ type ValidationResponseWithoutValue<Form> = Omit<ValidationResponse<Form>, 'data'>;
922
+ /** Options bag for `form.parse`. */
923
+ type ParseOptions = {
924
+ /**
925
+ * Turn the pure read into an authoritative run: the verdict is
926
+ * committed to the error store at the parsed scope, and in-flight
927
+ * per-field validation is cancelled first. Default `false`.
928
+ */
929
+ readonly commit?: boolean;
930
+ };
931
+ /**
932
+ * Sync-or-async return shape for `AbstractSchema.validateAtPath`. The
933
+ * adapter returns the response inline when the schema and the caller's
934
+ * options permit synchronous validation, and a `Promise<T>` otherwise.
935
+ * Callers that do not care simply `await`, which works for both; the
936
+ * reshape pre-pass, which prevents flicker, branches on
937
+ * `instanceof Promise`.
938
+ */
939
+ type MaybePromise<T> = T | Promise<T>;
940
+ /** Options accepted by `AbstractSchema.validateAtPath`. */
941
+ type ValidateOptions = {
942
+ /**
943
+ * Ask the adapter to return the response inline where the schema
944
+ * permits synchronous validation. A preference, not a guarantee: a
945
+ * structurally async schema, meaning any verdict that resolves only
946
+ * through a Promise (async refinements, async transforms or pipes)
947
+ * in whichever library the adapter wraps, still falls back to a
948
+ * `Promise<T>`.
949
+ *
950
+ * Omitted or `false`, the adapter is free to use its async path.
951
+ * Every call site outside the reshape pre-pass leaves it so.
952
+ */
953
+ sync?: boolean;
954
+ };
955
+ /**
956
+ * Configuration passed to `AbstractSchema.getDefaultValues`. Adapters
957
+ * receive `useDefaultSchemaValues`, which chooses between honoring
958
+ * `.default(x)` wrappers and using empty or falsy fallbacks, plus an
959
+ * optional `constraints` overlay merged into the derived defaults so
960
+ * the runtime can stamp user-supplied defaults at construction.
961
+ * Exported so adapter authors can co-implement the service contract.
962
+ */
963
+ type GetDefaultValuesConfig<Form> = {
964
+ useDefaultSchemaValues: boolean;
965
+ constraints?: DeepPartial<WriteShape<Form>> | undefined;
966
+ };
967
+ /**
968
+ * The contract a schema adapter implements so the form runtime can
969
+ * read defaults, validate, and walk paths against any underlying
970
+ * schema library.
971
+ *
972
+ * Most consumers never touch this type directly, since the typed entry
973
+ * points (`attaform/zod`, `attaform/zod-v3`) wire an adapter
974
+ * automatically. Implement it only to add support for a new schema
975
+ * library.
976
+ */
977
+ type AbstractSchema<Form, GetValueFormType> = {
978
+ /**
979
+ * The form's starting value: the schema's defaults, with
980
+ * `config.constraints` merged over them and any shape the merge broke
981
+ * repaired. Derivation only. Nothing is validated here, so no
982
+ * refinement, check or transform runs; `validateSync` is the verdict.
983
+ */
984
+ getDefaultValues(config: GetDefaultValuesConfig<Form>): Form;
985
+ /**
986
+ * The schema's synchronous verdict on `data` as a whole form, as a
987
+ * list of failures: empty when the data passes.
988
+ *
989
+ * The runtime seeds the schema error channel with it whenever it
990
+ * establishes a baseline: on the starting value, the first time
991
+ * anything reads or writes the form's errors, and on `reset()`. A form
992
+ * starting from invalid defaults therefore reads invalid from its
993
+ * first render.
994
+ * A hydrating client never calls it, since the server's verdict
995
+ * arrives with the payload.
996
+ *
997
+ * Adapters MUST NOT throw: a refinement that throws, or a schema whose
998
+ * verdict resolves only through a Promise, returns `[]` and leaves the
999
+ * verdict to the async pass that `needsAsyncValidation` queues. An
1000
+ * adapter with no synchronous parse returns `[]` unconditionally.
1001
+ */
1002
+ validateSync(data: unknown): ValidationError[];
1003
+ /**
1004
+ * The schema-prescribed default at `path`. The runtime uses it to
1005
+ * fill structural gaps, so every `setValue` write leaves the form
1006
+ * satisfying the slim schema, meaning objects, arrays and primitives
1007
+ * without refinement-level constraints.
1008
+ *
1009
+ * - Object property: the property's schema default.
1010
+ * - Array element: the element default. Positions past the array's
1011
+ * current length still resolve, since every position has the same
1012
+ * element type.
1013
+ * - Tuple position: the position's default, and `undefined` out of
1014
+ * range.
1015
+ * - Optional / Default / Nullable / Readonly / Catch / Pipe: the
1016
+ * inner default.
1017
+ * - Discriminated union: the first variant's default, matching
1018
+ * `validateAtPath`'s first-success rule.
1019
+ * - Leaf: the primitive default (`''`, `0`, `false`), or the
1020
+ * wrapper's `.default(x)` value when present.
1021
+ * - A path the schema does not declare: `undefined`.
1022
+ *
1023
+ * An adapter may return `undefined` for any path it cannot resolve;
1024
+ * callers read that as "do not fill" and keep the existing data.
1025
+ */
1026
+ getDefaultAtPath(path: Path): unknown;
1027
+ /**
1028
+ * The schema's appropriate nullish value at `path`: the underlying
1029
+ * type's empty concrete, with `.default(x)` wrappers explicitly NOT
1030
+ * honoured. This is what makes `form.clear(path)` differ from
1031
+ * `reset`: it ignores declared defaults and produces `false`, `0`,
1032
+ * `''`, `[]` or a recursively empty object instead.
1033
+ *
1034
+ * Sub-path resolution mirrors `getDefaultAtPath`; only leaves differ.
1035
+ *
1036
+ * - Primitive leaf: the falsy concrete (`''`, `0`, `false`, `0n`,
1037
+ * `new Date(0)`).
1038
+ * - Array / Set / Record: empty.
1039
+ * - `Optional<T>`: `undefined`, the wrapper's absent marker.
1040
+ * - `Nullable<T>`: `null`, the wrapper's explicit empty.
1041
+ * - `Default<T>` / `Prefault<T>` / `Catch<T>`: the inner schema's
1042
+ * empty. The declared default is INTENTIONALLY skipped.
1043
+ * - `Readonly<T>` / `preprocess(fn, T)`: the inner schema's empty.
1044
+ * - Object: recursive, every property gets its own empty.
1045
+ * - Discriminated union: the first variant's recursive empty.
1046
+ * - A path the schema does not declare: `undefined`.
1047
+ *
1048
+ * An adapter may return `undefined` for any path it cannot resolve;
1049
+ * callers read that as "do not write" and leave storage unchanged.
1050
+ */
1051
+ getEmptyValueAtPath(path: Path): unknown;
1052
+ /**
1053
+ * Whether `path` resolves to, or descends through, a schema-side
1054
+ * normalizer that runs at parse rather than at the write boundary.
1055
+ * In Zod v4 that is `z.preprocess(fn, inner)` and `z.coerce.X()`,
1056
+ * which both desugar to `ZodPipe<ZodTransform, inner>`; in Zod v3 it
1057
+ * is `ZodEffects` with `_def.effect.type === 'preprocess'`.
1058
+ *
1059
+ * The slim-primitive write gate consults it. True at a path means the
1060
+ * gate takes the consumer's raw value verbatim and stops walking
1061
+ * children: storage holds the user's input, and the normalizer fires
1062
+ * during `safeParse`, not at `setValue` time.
1063
+ *
1064
+ * Prefix semantic: true if ANY ancestor of `path` resolves to such a
1065
+ * wrapper, so descendants of a preprocess-wrapped container
1066
+ * short-circuit the gate too. Adapters cache by canonical path key.
1067
+ */
1068
+ isPreprocessOrCoerceLeaf(path: Path): boolean;
1069
+ /**
1070
+ * Whether `path` resolves to an opaque leaf: a schema that declares a
1071
+ * value without describing its shape. In Zod v4 that is `z.any()`,
1072
+ * `z.unknown()` and `z.custom(...)`, which is what both
1073
+ * `z.instanceof(X)` and `z.custom<T>()` compile to; in Zod v3 it is
1074
+ * `z.any()` and `z.unknown()`, with `z.instanceof(X)` peeling through
1075
+ * `ZodEffects` to `ZodAny`.
1076
+ *
1077
+ * The slim-primitive write gate consults it. An opaque leaf admits
1078
+ * every kind, containers included, and declares no sub-paths. Without
1079
+ * this the gate accepts an array at the leaf and then walks into it,
1080
+ * checking `files.0` against a schema that never declared that path;
1081
+ * the empty accept set there reads as "not in your schema" and no-ops
1082
+ * the whole write (#542).
1083
+ *
1084
+ * Exact-path semantic, unlike `isPreprocessOrCoerceLeaf`: it answers
1085
+ * for the node at `path` only. Descendants stay unwritable on their
1086
+ * own, because the schema genuinely does not declare them and
1087
+ * fabricating them would put phantom nodes on `form.fields`. Writing
1088
+ * the whole value at the opaque leaf is the supported move.
1089
+ */
1090
+ isOpaqueLeafAtPath(path: Path): boolean;
1091
+ /**
1092
+ * Distinguish a tuple, fixed-length and position-typed, from an
1093
+ * unbounded array at `path`. The runtime calls this on every write
1094
+ * that descends into an array branch and treats the answer as
1095
+ * definitive.
1096
+ *
1097
+ * - `number`: a tuple of this structural length. The runtime pads the
1098
+ * consumer's value to that length and recurses position by
1099
+ * position.
1100
+ * - `null`: not a tuple. Covers the unbounded array, where the
1101
+ * runtime uses the consumer's length and reuses one element default
1102
+ * for every position, and also every path that does not resolve to
1103
+ * an array at all, where `getDefaultAtPath` yields no element
1104
+ * default either and the consumer's array passes through unchanged.
1105
+ *
1106
+ * Wrappers (optional, nullable, default, readonly, catch, pipe, lazy)
1107
+ * are peeled before the type check, so `optional(z.tuple([...]))`
1108
+ * reports its tuple length.
1109
+ */
1110
+ arrayShapeAtPath(path: Path): number | null;
1111
+ /**
1112
+ * Whether the schema at `path` is a FIXED object, meaning a closed
1113
+ * set of declared keys (`z.object`), as opposed to an open or union
1114
+ * container (array, record, map, set, union, discriminated union)
1115
+ * whose element schema matches any segment.
1116
+ *
1117
+ * `form.fields` and `form.errors` use this to resolve a collision. A
1118
+ * fixed object's declared keys are known ahead of any data, so a key
1119
+ * the schema owns descends to a real terminal even before the live
1120
+ * value is populated, which keeps a declared-but-absent `optional`
1121
+ * field registrable. An open container cannot promise that: its
1122
+ * element schema accepts ANY segment, so the proxy falls back to the
1123
+ * keys the data currently holds and reads a genuinely absent key (an
1124
+ * out-of-bounds index, a missing record key, an inactive variant key)
1125
+ * as `undefined` rather than a phantom node.
1126
+ *
1127
+ * The empty path, the root form, is always a fixed object. Wrappers
1128
+ * are peeled before the kind check, so `z.object({...}).optional()`
1129
+ * still reports `true`. An undeclared path reports `false`.
1130
+ */
1131
+ isFixedObjectAtPath(path: Path): boolean;
1132
+ /**
1133
+ * How the container at `path` spells the keys of its own entries:
1134
+ * `'number'` for an array or tuple, `'string'` for an object or
1135
+ * record, and for a `z.map` the kind its declared key type accepts.
1136
+ * `undefined` for anything else, a leaf and an undeclared path
1137
+ * included.
1138
+ *
1139
+ * The write walkers consult it at a `Map`, where a new entry must be
1140
+ * filed under a key of the declared type and the path segment alone
1141
+ * cannot say which: an integer-looking segment canonicalises to a
1142
+ * number, so `scores.42` against a `z.map(z.string(), V)` would
1143
+ * otherwise take the number `42` as its key and fail the map's own
1144
+ * parse. An entry the map already holds needs no ruling, since its
1145
+ * existing key wins.
1146
+ *
1147
+ * A map whose key type is neither string-ish nor number-ish, or which
1148
+ * admits both, reports `undefined`: no segment can spell such a key,
1149
+ * so the map stays a whole value with no addressable entries.
1150
+ *
1151
+ * Adapters MAY cache per path; the runtime calls this on the write
1152
+ * path.
1153
+ */
1154
+ entryKeyKindAtPath(path: Path): 'string' | 'number' | undefined;
1155
+ /**
1156
+ * Every sub-schema that could resolve at `path`. Multiple results are
1157
+ * expected only for union branches, where the adapter cannot pick a
1158
+ * single winner until the data lands.
1159
+ *
1160
+ * `path` is the canonical `Segment[]`, walked segment by segment, so
1161
+ * a literal-dot key (`['user.name']`) never collides with the
1162
+ * sibling-pair form (`['user', 'name']`).
1163
+ */
1164
+ getSchemasAtPath(path: Path): AbstractSchema<unknown, GetValueFormType>[];
1165
+ /**
1166
+ * Validate the subtree at `path`, or the whole form when `path` is
1167
+ * `undefined`. As with `getSchemasAtPath`, `path` is the canonical
1168
+ * `Segment[]` rather than a dotted string, so two schemas with
1169
+ * colliding dotted forms stay distinct at the adapter boundary.
1170
+ *
1171
+ * Adapters MUST NOT throw: an error comes back as a `success: false`
1172
+ * response with a populated `errors` array.
1173
+ *
1174
+ * The `MaybePromise` return has three cases:
1175
+ * - `options.sync === true` and a sync-capable schema: the adapter
1176
+ * SHOULD return the response inline. That lets the runtime batch
1177
+ * error writes with a coincident value mutation into one Vue
1178
+ * reactive flush, which is what prevents the `{}` flicker
1179
+ * observable during a discriminated-union variant reshape.
1180
+ * - `options.sync === true` and an async-only schema, meaning any
1181
+ * verdict that resolves only through a Promise: the adapter MUST
1182
+ * fall back to `Promise<T>`. The flag is a preference; sync is not
1183
+ * always achievable.
1184
+ * - `options.sync` omitted or `false`: the adapter SHOULD return
1185
+ * `Promise<T>`.
1186
+ *
1187
+ * Callers that do not care simply `await`, which works for both arms.
1188
+ * A caller that must tell them apart branches on `instanceof Promise`.
1189
+ */
1190
+ validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<SchemaParseResult<GetValueFormType>>;
1191
+ /**
1192
+ * The set of primitive `typeof`-style kinds the leaf schema at `path`
1193
+ * accepts at write time. The sync sister to `getSchemasAtPath` and
1194
+ * `validateAtPath`, used by `setValueAtPath` to gate writes without
1195
+ * round-tripping through async validation.
1196
+ *
1197
+ * Wrappers (optional, nullable, default, refinement, transform, pipe,
1198
+ * readonly, catch, lazy) are peeled. Refinement-level constraints,
1199
+ * meaning format checks such as email or uuid, min and max length,
1200
+ * enum membership, literal equality and regex, are IGNORED here:
1201
+ * those are a validation-time concern.
1202
+ *
1203
+ * The returned set unions across union branches and intersects across
1204
+ * intersection sides. Conventions:
1205
+ *
1206
+ * - Empty set: no kind admitted, so the gate rejects every write to
1207
+ * the path. Surfaces for `never`-typed schemas and for paths that
1208
+ * do not resolve in the schema at all, such as a typo.
1209
+ * - Permissive set, every kind: unknown or unconstrained, so the gate
1210
+ * accepts any value. Surfaces for `any`, `unknown` and `void`, and
1211
+ * where a lazy peel failed and the adapter cannot introspect.
1212
+ * - String-valued enums return `{'string'}`; numeric enums
1213
+ * `{'number'}`; literal types `{primitiveKindOf(literalValue)}`.
1214
+ * - Object and array containers return `{'object'}` / `{'array'}`;
1215
+ * the runtime walker recurses into entries at write time.
1216
+ * - Nullable and optional wrappers add `'null'` / `'undefined'` to
1217
+ * the inner set.
1218
+ */
1219
+ getSlimPrimitiveTypesAtPath(path: Path): ReadonlySet<SlimPrimitiveKind>;
1220
+ /**
1221
+ * Whether `path` resolves to a LEAF, meaning a path the schema
1222
+ * declares no sub-paths under. `form.values`, `form.errors` and
1223
+ * `form.fields` query this at every step to choose between
1224
+ * descending into a sub-proxy and terminating with a leaf value.
1225
+ *
1226
+ * That branching is what keeps reserved leaf-prop names (`dirty`,
1227
+ * `errors`, `valid`) from shadowing schema keys: they inject only at
1228
+ * the FieldState terminal, not at every depth, so a schema field
1229
+ * literally named `dirty` at depth 2 or deeper stays reachable in its
1230
+ * own right.
1231
+ *
1232
+ * - Object / Array / Map / Set, under any wrapper: `false`, a
1233
+ * container, so descend further.
1234
+ * - Primitive (string, number, boolean, bigint, symbol, null,
1235
+ * undefined, date, function): `true`. `date` and `function` are
1236
+ * leaves because their values are opaque; do not drill into them.
1237
+ * - Opaque leaf (`z.any()`, `z.unknown()`, `z.custom()`, and the
1238
+ * `z.instanceof(X)` that compiles to it): `true`, and this answers
1239
+ * AHEAD of the kind test. An opaque leaf admits every kind
1240
+ * including the container ones, so classifying it off the slim
1241
+ * primitive set alone would call it a container and hang phantom
1242
+ * nodes under it on `form.fields`. See `isOpaqueLeafAtPath`.
1243
+ * Leafness asks whether the schema declares sub-paths here, and an
1244
+ * opaque leaf declares none.
1245
+ * - Optional / Nullable / Default / Catch: transparent. They add
1246
+ * `'null'` / `'undefined'` to the inner kind set without changing
1247
+ * the classification.
1248
+ * - Discriminated union root: `false`, since the variants are
1249
+ * objects and the kind set holds `'object'`.
1250
+ * - A union's discriminator key: `true`, since the literal type
1251
+ * resolves to `{'string'}` or `{'number'}`.
1252
+ * - A variant-only key: `true` if it resolves to a primitive in any
1253
+ * variant. Schema-static, so it does NOT read live storage to
1254
+ * decide which variant is active.
1255
+ * - Empty path, the root: `false`, the form-as-object.
1256
+ * - A path the schema does not declare: `false`. The proxy descends
1257
+ * permissively and leaf-prop reads at the unknown path return
1258
+ * `undefined` from the store. Treating unknown paths as containers
1259
+ * preserves the schema's authority and keeps a typo from
1260
+ * re-introducing shadowing.
1261
+ *
1262
+ * Adapters MAY cache per path: this is called on every proxy `get`
1263
+ * trap hit. The reference implementation memoises a
1264
+ * `Map<PathKey, boolean>` keyed by `canonicalizePath(path).key`, with
1265
+ * a lifetime tied to the adapter, one per `useForm()` call.
1266
+ */
1267
+ isLeafAtPath(path: Path): boolean;
1268
+ /**
1269
+ * Whether the leaf at `path` is required, meaning the schema does NOT
1270
+ * admit empty through `.optional()`, `.nullable()`, `.default(N)` or
1271
+ * `.catch(N)` at the leaf or any wrapper.
1272
+ *
1273
+ * The submit and validate path uses it to raise "No value supplied"
1274
+ * when a field is in the form's `blankPaths` set, because the user
1275
+ * cleared it or never answered, AND the schema treats it as required.
1276
+ * Without it a strict numeric leaf silently accepts its slim default
1277
+ * of `0` for an unanswered field, so an unanswered income reads as
1278
+ * zero and passes validation.
1279
+ *
1280
+ * - Optional / Nullable / Default / Catch at any wrapper layer, root
1281
+ * or nested: `false`. The schema author opted into accepting empty.
1282
+ * - Readonly / Pipe / Lazy: transparent, peel and re-check the inner
1283
+ * schema.
1284
+ * - Union or discriminated union: `false` if ANY branch admits empty,
1285
+ * since a union accepts what its most permissive branch accepts.
1286
+ * This matches `validateAtPath`'s first-success rule.
1287
+ * - Intersection: `true` if EITHER side requires the path, since an
1288
+ * intersection needs both sides to accept, so one side rejecting
1289
+ * empty makes the whole rejection.
1290
+ * - A path the schema does not declare: `false`.
1291
+ * - Empty path, the root: `true`, always required as an object.
1292
+ *
1293
+ * Refinement-level constraints (length, format, custom predicates)
1294
+ * are not consulted. Those run at parse time inside `validateAtPath`
1295
+ * and surface as schema errors regardless. This answers only whether
1296
+ * the leaf is required at all; refinements layer on top.
1297
+ */
1298
+ isRequiredAtPath(path: Path): boolean;
1299
+ /**
1300
+ * If the schema at `path` is, or wraps, a discriminated union, its
1301
+ * discriminator key plus a `getVariantDefault(value)` lookup;
1302
+ * otherwise `undefined`. Wrappers are peeled transparently.
1303
+ *
1304
+ * Two reshapes share this lookup:
1305
+ *
1306
+ * 1. Discriminator-key write. The runtime calls it with the PARENT
1307
+ * path. If `discriminatorKey` matches the path's last segment,
1308
+ * the write changes which variant is active, so parent storage
1309
+ * is replaced with the matching variant's slim default and the
1310
+ * old variant's keys (an `address` left over after switching to
1311
+ * `sms`) cannot leak.
1312
+ * 2. Whole-union write. The runtime calls it with the path itself.
1313
+ * If the info exists and the consumer's value carries the
1314
+ * discriminator key, the merge uses that variant's default
1315
+ * instead of the first-variant fallback `getDefaultAtPath`
1316
+ * returns for a union.
1317
+ *
1318
+ * An adapter that does not model discriminated unions can return
1319
+ * `undefined` unconditionally; the reshape is then a no-op.
1320
+ */
1321
+ getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
1322
+ /**
1323
+ * Resolved field metadata for the schema node at `path`: label,
1324
+ * description and placeholder, plus the full registered payload as
1325
+ * `meta` for consumer-augmented keys. Reads through the shared
1326
+ * cross-adapter field-meta store, with one-way fallbacks:
1327
+ *
1328
+ * - `label`: registry payload, else `humanize(lastSegment)`
1329
+ * - `description`: registry payload, else `schema.description`
1330
+ * (the `.describe()` value), else `undefined`
1331
+ * - `placeholder`: registry payload, else `undefined`
1332
+ * - `meta`: registry payload, frozen; an empty object when
1333
+ * nothing was registered
1334
+ *
1335
+ * `path` is the canonical `Segment[]`, and the empty path resolves to
1336
+ * the root schema's metadata. Multiple candidates from union branches
1337
+ * resolve against the first match, following the same first-success
1338
+ * precedent as `getDefaultAtPath` and `validateAtPath`: register on
1339
+ * the union root for shared metadata, on individual branches for
1340
+ * variant-specific metadata.
1341
+ *
1342
+ * Optional. A missing implementation is treated as a stub returning
1343
+ * `EMPTY_RESOLVED_FIELD_META`, so consumers see the humanized `label`
1344
+ * fallback and `undefined` elsewhere.
1345
+ */
1346
+ getFieldMetaAtPath?(path: Path): ResolvedFieldMeta;
1347
+ /**
1348
+ * Whether `validateAtPath` MAY have to run asynchronously to surface
1349
+ * every error this schema can produce. The runtime asks at
1350
+ * construction: `false` means the synchronous verdict on the starting
1351
+ * value is authoritative and no extra microtask is spent, `true` queues a
1352
+ * one-shot full-form async pass so async-only verdicts (refinements,
1353
+ * transforms or pipes that resolve only through a Promise) surface
1354
+ * without waiting for a user mutation.
1355
+ *
1356
+ * Optional and best-effort. A missing implementation is treated as
1357
+ * `() => false`, and async-only errors then fire on first user
1358
+ * mutation instead.
1359
+ *
1360
+ * For a per-path query, compose with `getSchemasAtPath(path)`: each
1361
+ * candidate sub-schema exposes its own `needsAsyncValidation`, so
1362
+ * "does the cargo subtree contain async work?" is the union of the
1363
+ * per-candidate answers, with no separate top-level overload. A root
1364
+ * that answers `false` answers for every path, and the runtime then
1365
+ * asks no sub-schema, so never answer `false` at the root of a tree
1366
+ * where a sub-schema would answer `true`.
1367
+ */
1368
+ needsAsyncValidation?(): boolean;
1369
+ /**
1370
+ * Whether the schema carries a refine, check or transform at any
1371
+ * NON-LEAF position: a container node (object, array, tuple, union,
1372
+ * intersection, record, map, set) or the root itself. `false` means
1373
+ * every check is leaf-local, so a per-keystroke
1374
+ * `validateAtPath(form, leafPath)` catches the same verdicts as a
1375
+ * whole-form pass, because no ancestor refine reads the form's wider
1376
+ * state.
1377
+ *
1378
+ * The runtime uses it at the per-keystroke schedule to scope
1379
+ * field-level validation to the changed subtree where it can, and
1380
+ * falls back to a whole-form pass when an ancestor refine, such as a
1381
+ * cross-field equality or a sum constraint, could be moved by a leaf
1382
+ * write.
1383
+ *
1384
+ * Optional, and a missing implementation is treated as `() => true`,
1385
+ * the conservative whole-form answer. Detection is best-effort and
1386
+ * the two error directions are not symmetric: returning `true` where
1387
+ * no container refine exists only loses a perf win, while returning
1388
+ * `false` where one exists lets an ancestor verdict go stale. Bias
1389
+ * toward `true` when in doubt.
1390
+ */
1391
+ hasContainerOrRootRefine?(): boolean;
1392
+ /**
1393
+ * Whether the schema tree holds at least one discriminated union at
1394
+ * any depth, inside arrays, tuples, records and lazy schemas
1395
+ * included. The store computes its per-form union capability flag
1396
+ * from this once at construction: `false` lets every write skip the
1397
+ * cross-variant ancestor guard and the variant-reshape dispatch, and
1398
+ * stops stub correction from running at all.
1399
+ *
1400
+ * Optional, and an adapter that omits it is treated as containing
1401
+ * unions, which keeps the conservative per-write probes on. Never
1402
+ * return `false` for a schema that DOES hold a discriminated union:
1403
+ * that disables variant reshape and stub correction for the form.
1404
+ */
1405
+ hasDiscriminatedUnions?(): boolean;
1406
+ /**
1407
+ * Whether `isPreprocessOrCoerceLeaf` can answer `true` at any path:
1408
+ * the schema holds a preprocess-style normalizer or a coerce
1409
+ * primitive somewhere. The store reads it once at construction.
1410
+ * `false` lets it skip recording which paths the consumer authored,
1411
+ * a set that is only read at such leaves.
1412
+ *
1413
+ * Optional, and an adapter that omits it is treated as holding one,
1414
+ * which keeps that bookkeeping on. Never return `false` for a schema
1415
+ * where `isPreprocessOrCoerceLeaf` answers `true` anywhere: verdicts
1416
+ * at a path the consumer authored with `undefined` would then stay
1417
+ * hidden.
1418
+ */
1419
+ hasPreprocessOrCoerce?(): boolean;
1420
+ };
1421
+ /**
1422
+ * A discriminated union as the adapter reports it: the discriminator
1423
+ * key plus a lookup from a discriminator literal to the matching
1424
+ * variant's slim default. Returned by
1425
+ * `AbstractSchema.getUnionDiscriminatorAtPath`.
1426
+ */
1427
+ type UnionDiscriminatorContext = {
1428
+ /**
1429
+ * The property name whose literal value selects the variant, such as
1430
+ * `'channel'` for a union split on `{ channel: 'sms' | 'email' }`.
1431
+ */
1432
+ readonly discriminatorKey: string;
1433
+ /**
1434
+ * Slim default for the variant whose discriminator literal equals
1435
+ * `value`, or `undefined` when no variant matches, in which case the
1436
+ * runtime skips the reshape and falls back to a plain write.
1437
+ */
1438
+ getVariantDefault(value: unknown): unknown;
1439
+ /**
1440
+ * Whether `value` is a literal one of the discriminator's variants
1441
+ * recognises. Reshape uses it to choose between seeking a variant
1442
+ * default and emitting a stub state. NOT used at the write gate,
1443
+ * where consumer-side value validity is a validation-time concern.
1444
+ */
1445
+ isVariantSelected(value: unknown): boolean;
1446
+ };
1447
+ /**
1448
+ * The primitive kinds the slim-primitive write contract recognises:
1449
+ * `typeof` plus a few well-known reference shapes (`Date`, `Array`,
1450
+ * `Map`, `Set`, plain `object`, `null`).
1451
+ *
1452
+ * The runtime gate's `slimKindOf(value)` returns one of these for a
1453
+ * value, and the adapter's `getSlimPrimitiveTypesAtPath(path)` returns
1454
+ * the set a leaf accepts. A write is gated by
1455
+ * `accepted.has(slimKindOf(value))`.
1456
+ */
1457
+ type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set' | 'file';
1458
+ /**
1459
+ * The "no result yet" status the reactive `validate()` ref holds while
1460
+ * a run is in flight. Narrow against `pending` to reach the settled
1461
+ * fields:
1462
+ *
1463
+ * ```ts
1464
+ * const status = form.validate()
1465
+ * watchEffect(() => {
1466
+ * if (status.value.pending) return
1467
+ * // status.value.success / status.value.errors are now safe to read
1468
+ * })
1469
+ * ```
1470
+ */
1471
+ type PendingValidationStatus = {
1472
+ readonly pending: true;
1473
+ readonly errors: undefined;
1474
+ readonly success: false;
1475
+ readonly formKey: FormKey;
1476
+ };
1477
+ /** Settled status of a reactive `validate()` call. Mirrors the latest result. */
1478
+ type SettledValidationStatus<Form> = {
1479
+ readonly pending: false;
1480
+ } & ValidationResponseWithoutValue<Form>;
1481
+ /**
1482
+ * The value type of the ref `validate()` returns. Discriminate on
1483
+ * `pending` to switch between the in-flight and settled states.
1484
+ */
1485
+ type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
1486
+ /**
1487
+ * When per-field validation runs.
1488
+ *
1489
+ * - `'change'` (default): every committed write schedules a validation
1490
+ * for the affected path. At `debounceMs: 0`, also the default, the
1491
+ * run is synchronous in the write handler; a positive `debounceMs`
1492
+ * coalesces rapid bursts.
1493
+ * - `'blur'`: validate when the user tabs away from a registered
1494
+ * field. No debounce; `debounceMs` is a type error.
1495
+ * - `'submit'`: no live validation. `handleSubmit` and explicit
1496
+ * `validate()` / `parse()` calls are the only validation surfaces.
1497
+ * `debounceMs` is a type error.
1498
+ *
1499
+ * Only validation timing varies by mode. How often a value commits to
1500
+ * storage is the directive's concern: `<input v-register>` commits per
1501
+ * keystroke, `<input v-register.lazy>` defers to blur.
1502
+ */
1503
+ type ValidateOn = 'change' | 'blur' | 'submit';
1504
+ /**
1505
+ * Validation timing: `validateOn` is the trigger and `debounceMs` the
1506
+ * wait after the last committed write. The union is what makes pairing
1507
+ * `debounceMs` with `'blur'` or `'submit'` a type error rather than a
1508
+ * silent runtime drop.
1509
+ */
1510
+ type ValidateOnConfig = {
1511
+ /** Validation trigger. Default `'change'`. */
1512
+ validateOn?: 'change';
1513
+ /**
1514
+ * Milliseconds to wait after the last committed write before
1515
+ * running validation, to coalesce rapid bursts into one pass.
1516
+ * Useful for slow async adapters, or to smooth inline feedback
1517
+ * under heavy typing.
1518
+ *
1519
+ * Default `0`: validation runs synchronously after the write,
1520
+ * with no `setTimeout`. Schema work still rides
1521
+ * `Promise.resolve().then(validateAtPath)`, so even at `0` errors
1522
+ * land a microtask later rather than in the same statement.
1523
+ *
1524
+ * This debounces validation only. A value commits to
1525
+ * `form.values` immediately either way.
1526
+ */
1527
+ debounceMs?: number;
1528
+ } | {
1529
+ /** Validation trigger. */
1530
+ validateOn: 'blur' | 'submit';
1531
+ /** `debounceMs` is not allowed with `'blur'` or `'submit'`. */
1532
+ debounceMs?: never;
1533
+ };
1534
+ /**
1535
+ * Per-write metadata, tagging a write so listeners and the write funnel
1536
+ * can treat it specially. Every field is internal; do not set any of
1537
+ * them from consumer code.
1538
+ */
1539
+ type WriteMeta = {
1540
+ /**
1541
+ * Add the written path to the store's `blankPaths` set: storage holds
1542
+ * a real, schema-conformant value, the slim default, but the UI shows
1543
+ * the field as empty. The next write to that path WITHOUT this flag
1544
+ * removes it from the set, the user having typed something real.
1545
+ *
1546
+ * Set by `markBlank()` on the register binding, and by the `unset`
1547
+ * translation in `setValue`, `reset` and `useAbstractForm`
1548
+ * construction.
1549
+ */
1550
+ readonly blank?: boolean;
1551
+ /**
1552
+ * Skip the discriminator-aware variant reshape inside
1553
+ * `setValueAtPath` for this write. Set by the reshape itself when it
1554
+ * re-enters with the new variant default, so the literal
1555
+ * discriminator inside that default cannot loop forever.
1556
+ */
1557
+ readonly skipDiscriminatorReshape?: boolean;
1558
+ /**
1559
+ * Record an array structural mutation precisely enough to replay the
1560
+ * index permutation it produced. Set by the typed array helpers in
1561
+ * `array-engine.ts`, and used by `setValueAtPath` to surgically clear
1562
+ * variant memory for just the indices the operation invalidated.
1563
+ *
1564
+ * Without the hint, a raw whole-array `setValue(arrayPath, [...])`
1565
+ * clears all memory under the array, because the runtime cannot tell
1566
+ * which indices stayed put.
1567
+ */
1568
+ readonly arrayOp?: {
1569
+ readonly kind: 'insert';
1570
+ readonly index: number;
1571
+ } | {
1572
+ readonly kind: 'remove';
1573
+ readonly index: number;
1574
+ } | {
1575
+ readonly kind: 'move';
1576
+ readonly from: number;
1577
+ readonly to: number;
1578
+ } | {
1579
+ readonly kind: 'swap';
1580
+ readonly a: number;
1581
+ readonly b: number;
1582
+ } | {
1583
+ readonly kind: 'replace-at';
1584
+ readonly index: number;
1585
+ };
1586
+ /**
1587
+ * Per-instance config threaded through writes so each
1588
+ * `useForm({ key })` call site honors its own `validateOn`,
1589
+ * `debounceMs` and `rememberVariants` even while sharing a store with
1590
+ * sibling calls, as a modal and a main form rendering the same
1591
+ * logical form do.
1592
+ *
1593
+ * Set by `buildFormApi` from the per-instance options bag; the store
1594
+ * reads each field with a fallback to its construction-time defaults.
1595
+ */
1596
+ readonly instance?: {
1597
+ readonly validateOn?: ValidateOn;
1598
+ readonly debounceMs?: number;
1599
+ readonly rememberVariants?: boolean;
1600
+ };
1601
+ /**
1602
+ * Mark this `applyFormReplacement` call as a hydration step, meaning
1603
+ * the async-`defaultValues`, `activate()` or `rehydrate()` path.
1604
+ * Modules that snapshot form state, the history module in particular,
1605
+ * treat hydration as the baseline: stacks reset to a single seed of
1606
+ * the post-hydration value, so a later `undo()` cannot recover the
1607
+ * transient pre-hydration default.
1608
+ *
1609
+ * Set by the activate path in `create-form-store.ts`.
1610
+ */
1611
+ readonly hydration?: boolean;
1612
+ };
1613
+ /**
1614
+ * The store slice the history runtime binds to. Structural on purpose:
1615
+ * `historyPlugin()` ships from the separate `attaform/history` entry,
1616
+ * so the runtime receives the store through this seam instead of
1617
+ * importing the store module, which would pull history internals onto
1618
+ * every form's eager path. A FormStore satisfies it as-is.
1619
+ *
1620
+ * @internal
1621
+ */
1622
+ type HistoryKernel = {
1623
+ readonly form: Ref<unknown>;
1624
+ readonly blankPaths: Set<PathKey>;
1625
+ readonly errorCells: () => Map<PathKey, ErrorCell>;
1626
+ onFormChange(listener: (next: unknown, meta?: WriteMeta) => void): () => void;
1627
+ applyFormReplacement(next: unknown): void;
1628
+ restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
1629
+ };
1630
+ /**
1631
+ * The live undo/redo runtime a {@link HistoryPlugin} attaches to one
1632
+ * form. Cached on the store so every `useForm` and `injectForm`
1633
+ * consumer of the same key shares one chain; `buildFormApi` adapts it
1634
+ * into the public `form.history` namespace.
1635
+ *
1636
+ * @internal
1637
+ */
1638
+ type HistoryModule = {
1639
+ undo(): boolean;
1640
+ redo(): boolean;
1641
+ clear(): void;
1642
+ canUndo: Readonly<ComputedRef<boolean>>;
1643
+ canRedo: Readonly<ComputedRef<boolean>>;
1644
+ historySize: Readonly<ComputedRef<number>>;
1645
+ dispose(): void;
1646
+ };
1647
+ /**
1648
+ * Opt-in undo/redo, created by `historyPlugin()` from
1649
+ * `attaform/history` and passed to `useForm({ history })`:
1650
+ *
1651
+ * ```ts
1652
+ * import { historyPlugin } from 'attaform/history'
1653
+ *
1654
+ * const form = useForm({ schema, history: historyPlugin({ max: 200 }) })
1655
+ * ```
1656
+ *
1657
+ * Every mutation records a position, and `form.history.undo()` /
1658
+ * `form.history.redo()` walk the chain. `reset()` is itself a mutation,
1659
+ * so the pre-reset state stays one undo away. Hydration is the floor:
1660
+ * once it applies, the chain reseeds with the hydrated value and
1661
+ * `undo()` cannot reach the transient pre-hydration default.
1662
+ *
1663
+ * A plugin instance is a reusable configuration, not per-form state, so
1664
+ * passing one instance to several forms gives each its own independent
1665
+ * chain.
1666
+ */
1667
+ type HistoryPlugin = {
1668
+ /**
1669
+ * Bind a fresh history runtime to one form's store. Called by
1670
+ * `useForm` when the store is first created.
1671
+ *
1672
+ * @internal
1673
+ */
1674
+ readonly attach: (kernel: HistoryKernel) => HistoryModule;
1675
+ };
1676
+ /**
1677
+ * The undo/redo namespace at `form.history`, holding every
1678
+ * history-related method and reactive flag, so there is one address to
1679
+ * read from.
1680
+ *
1681
+ * Always present on a `useForm()` return whether or not `history` was
1682
+ * configured. Unconfigured, the methods are no-ops returning `false` or
1683
+ * `void`, `canUndo` and `canRedo` read `false`, and `size` reads `0`,
1684
+ * so a template needs no conditional.
1685
+ *
1686
+ * `canUndo`, `canRedo` and `size` read as plain values rather than
1687
+ * refs; no `.value`.
1688
+ */
1689
+ type FormHistoryNamespace = {
1690
+ /**
1691
+ * Step back one position. `true` when a step was taken, `false` at
1692
+ * the oldest reachable position or when history is not configured.
1693
+ */
1694
+ readonly undo: () => boolean;
1695
+ /**
1696
+ * Replay the next step forward. `true` on success, `false` when
1697
+ * nothing is queued or history is not configured. The forward branch
1698
+ * is dropped as soon as a new mutation lands.
1699
+ */
1700
+ readonly redo: () => boolean;
1701
+ /**
1702
+ * Wipe the undo and redo branches and reseed the chain with the
1703
+ * current form state as the new baseline. Values, errors and
1704
+ * blankPaths all stay put; only past and future reset. Afterwards
1705
+ * `canUndo` and `canRedo` are `false` and `size` is `1`. A no-op when
1706
+ * history is not configured.
1707
+ */
1708
+ readonly clear: () => void;
1709
+ /** `true` when at least one undo step is available. */
1710
+ readonly canUndo: boolean;
1711
+ /** `true` when `undo()` has been called and a `redo()` would replay. */
1712
+ readonly canRedo: boolean;
1713
+ /**
1714
+ * Total reachable positions: the current one plus everything
1715
+ * reachable by `undo()` and `redo()`. Useful for a debug overlay; UI
1716
+ * driving undo and redo buttons should gate on `canUndo` and
1717
+ * `canRedo`. Reads `0` when history is not configured.
1718
+ */
1719
+ readonly size: number;
1720
+ };
1721
+ /**
1722
+ * Configuration passed to `useForm`. Only `schema` is required.
1723
+ *
1724
+ * ```ts
1725
+ * const form = useForm({
1726
+ * schema: signupSchema,
1727
+ * defaultValues: { email: '' },
1728
+ * validateOn: 'change',
1729
+ * debounceMs: 200,
1730
+ * })
1731
+ * ```
1732
+ */
1733
+ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DefaultValuesInput<Form>, K extends FormKey = FormKey> = {
1734
+ /**
1735
+ * The schema describing the form's shape and validation rules. A
1736
+ * typed entry point such as `attaform/zod` takes the underlying
1737
+ * library's schema directly and wraps an adapter; the abstract entry
1738
+ * point takes any object implementing `AbstractSchema`.
1739
+ *
1740
+ * For a schema that depends on the form's identity or per-form
1741
+ * options, pass a factory `(key, options) => schema` instead, called
1742
+ * once per form. Most adapters ignore the options argument; the typed
1743
+ * Zod entry points use it to thread the recursion cap into the
1744
+ * adapter closure.
1745
+ */
1746
+ schema: Schema | ((key: FormKey, options: SchemaFactoryOptions) => Schema);
1747
+ /**
1748
+ * Optional identifier for this form. Omit it for a one-off form and
1749
+ * Attaform allocates a unique key automatically, SSR-safe and stable
1750
+ * across hydration.
1751
+ *
1752
+ * Pass a string when the form needs identity:
1753
+ * - to look it up from a distant component via `injectForm(key)`;
1754
+ * - to share state across components, since several `useForm({ key })`
1755
+ * calls with the same key resolve to the same form;
1756
+ * - to give DevTools and validation errors a recognisable label.
1757
+ *
1758
+ * Keys starting with `__atta:` are reserved and throw
1759
+ * `ReservedFormKeyError`.
1760
+ *
1761
+ * A string literal is preserved on `form.key`, so `useWizard` and
1762
+ * other consumers can discriminate against the union of known keys at
1763
+ * compile time.
1764
+ */
1765
+ key?: K;
1766
+ /**
1767
+ * Initial values applied over the schema's defaults. A field not
1768
+ * named here falls back to the schema default, or to the primitive
1769
+ * default for the slot's type.
1770
+ *
1771
+ * Values must satisfy the slim primitive type at each path (string,
1772
+ * number, boolean, Date) but do NOT have to satisfy refinement-level
1773
+ * constraints such as format checks, enum membership or length and
1774
+ * range bounds. A refinement-invalid default passes through and
1775
+ * surfaces as a field error, which is what lets you rehydrate stale
1776
+ * saved data without discarding the user's input.
1777
+ *
1778
+ * Accepts a plain value, a sync function, or an async function:
1779
+ *
1780
+ * ```ts
1781
+ * // Plain value: applies at construction.
1782
+ * defaultValues: { email: '' }
1783
+ *
1784
+ * // Sync function: invoked on a microtask after construction.
1785
+ * defaultValues: () => buildDraft()
1786
+ *
1787
+ * // Async function: the form starts with the schema's slim defaults
1788
+ * // and `form.hydrating` is true while the promise is in flight; on
1789
+ * // resolve the values apply and `hydrating` flips false. Under SSR
1790
+ * // the factory fires via `onServerPrefetch`, so the resolved payload
1791
+ * // bakes into hydration transfer state and the client never
1792
+ * // re-fetches.
1793
+ * defaultValues: async () => api.fetchDraft(userId)
1794
+ * ```
1795
+ *
1796
+ * An error thrown by a function-form factory surfaces on
1797
+ * `form.hydrateError` and leaves the form usable with slim defaults.
1798
+ * Call `form.rehydrate()` to re-fire the factory.
1799
+ */
1800
+ defaultValues?: DefaultValues | (() => DefaultValues) | (() => Promise<DefaultValues>);
1801
+ /**
1802
+ * Move keyboard and screen-reader focus to the first errored field
1803
+ * when a submit attempt fails validation, after errors are populated
1804
+ * and before your `onError` callback runs. Default `true`: focusing
1805
+ * the broken field is an accessibility baseline, and browsers scroll
1806
+ * the focused element into view as part of the move.
1807
+ *
1808
+ * Pass `false` to drive the nudge yourself from `onError` with
1809
+ * `form.focusFirstError()` or `form.scrollToFirstError()`, both of
1810
+ * which stay available either way.
1811
+ *
1812
+ * The focus requests `focusVisible: true` so the browser paints a
1813
+ * focus ring despite the move being programmatic. On a UA that does
1814
+ * not support that hint, a non-text control (radio, checkbox, custom
1815
+ * widget) can end up focused with no visible ring after a
1816
+ * pointer-driven submit; pair this with your own indicator, such as a
1817
+ * `:focus-within` ring on the option wrapper, if you target those
1818
+ * browsers.
1819
+ *
1820
+ * If no errored field has a mounted, visible element, the focus
1821
+ * silently no-ops.
1822
+ */
1823
+ focusOnInvalidSubmit?: boolean;
1824
+ /**
1825
+ * Freeze the form's data. While this resolves truthy every value
1826
+ * write no-ops at the store's write chokepoint, programmatic
1827
+ * `setValue`, the `v-register` directive and host-model events alike;
1828
+ * native registered inputs render the HTML `disabled` attribute;
1829
+ * component hosts receive a `disabled` flag on their register value;
1830
+ * and every field's `displayState` drops to `'idle'`, so error,
1831
+ * pending and success signals stand down. The first blocked write
1832
+ * logs a one-time dev warning. Writes never throw.
1833
+ *
1834
+ * `reset()` and `defaultValues` hydration still apply while frozen,
1835
+ * so a disabled form can be populated or cleared programmatically.
1836
+ * This is a data freeze only: the form stays navigable, which is what
1837
+ * a read-only review page wants.
1838
+ *
1839
+ * Accepts a boolean, ref, computed or getter, read live so the freeze
1840
+ * tracks a reactive source. `undefined` resolves to `false`.
1841
+ *
1842
+ * A shared store resolves `disabled` from its first
1843
+ * `useForm({ key })` call; a later caller passing a different value
1844
+ * is ignored.
1845
+ */
1846
+ disabled?: MaybeRefOrGetter<boolean | undefined>;
1847
+ /**
1848
+ * When per-field validation runs. Default `'change'`. See
1849
+ * `ValidateOn` for mode semantics.
1850
+ *
1851
+ * This sets validation timing only. How often a value commits to
1852
+ * storage is the directive's concern: per keystroke for
1853
+ * `<input v-register>`, per blur for `<input v-register.lazy>`.
1854
+ */
1855
+ validateOn?: ValidateOn;
1856
+ /**
1857
+ * Milliseconds to wait after the last committed write before running
1858
+ * validation, to coalesce rapid bursts. Default `0`: validation runs
1859
+ * synchronously after the write, with no `setTimeout`.
1860
+ *
1861
+ * Only accepted under `validateOn: 'change'`. Passing it with
1862
+ * `'blur'` or `'submit'` is a type error.
1863
+ */
1864
+ debounceMs?: number;
1865
+ /**
1866
+ * Opt-in undo/redo, off by default. Pass `historyPlugin()` from
1867
+ * `attaform/history`, which caps at 128 positions, or
1868
+ * `historyPlugin({ max: N })` to tune it. See {@link HistoryPlugin}
1869
+ * for how the chain behaves, and `form.history` for the surface it
1870
+ * adds.
1871
+ */
1872
+ history?: HistoryPlugin;
1873
+ /**
1874
+ * Whether to remember each discriminated-union variant's typed state
1875
+ * across switches. Default `true`.
1876
+ *
1877
+ * At `true`, switching `notify.channel` from `email`, holding
1878
+ * `address: 'foo@bar.com'`, to `sms` and back lands on
1879
+ * `address: 'foo@bar.com'` again: the runtime snapshots the outgoing
1880
+ * variant's subtree on switch-out and restores the incoming variant's
1881
+ * prior subtree on switch-in. Every union at every nesting depth is
1882
+ * memorized independently.
1883
+ *
1884
+ * At `false`, the outgoing variant's typed state is dropped on every
1885
+ * switch and the incoming variant initializes from its slim default.
1886
+ *
1887
+ * Memory is in-memory only and does not survive a fresh mount, so a
1888
+ * page reload starts every discriminator empty. `reset()` clears it;
1889
+ * `resetField(path)` clears any entry whose union path equals or sits
1890
+ * under `path`.
1891
+ */
1892
+ rememberVariants?: boolean;
1893
+ /**
1894
+ * Schema-driven coercion of user-typed DOM values at the v-register
1895
+ * layer. Two rules ship, string to number and string to boolean, each
1896
+ * firing only where the schema declares that single type at the path.
1897
+ * Defaults to on; `false` disables coercion form-wide and the slim
1898
+ * gate rejects mismatches instead.
1899
+ *
1900
+ * Coercion applies ONLY to user-typed DOM values. A programmatic
1901
+ * write is never coerced.
1902
+ */
1903
+ coerce?: boolean;
1904
+ /**
1905
+ * @internal
1906
+ * SSR prefetch mark, set by the `attaform/vite` compile-time
1907
+ * transform on a `useForm` call whose surrounding SFC template, or a
1908
+ * computed feeding it, reads the form's reactive state. The flag
1909
+ * enqueues the form on the registry's SSR prefetch queue, so an async
1910
+ * `defaultValues` factory runs inside `onServerPrefetch` and the
1911
+ * resolved payload bakes into the hydration transfer state.
1912
+ *
1913
+ * Consumers do not write this; `form.activate()` is the documented
1914
+ * escape hatch for where the transform's static analysis cannot see a
1915
+ * reference, such as cross-module sharing, dynamic property access or
1916
+ * a headless context.
1917
+ */
1918
+ __ssrAccessed?: boolean;
1919
+ };
1920
+ /**
1921
+ * Callback invoked by `handleSubmit` after the form parses successfully.
1922
+ * It receives the strictly-typed parsed value, with refinements already
1923
+ * run, so enum, literal and format constraints are honoured.
1924
+ */
1925
+ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
1926
+ /**
1927
+ * Callback invoked by `handleSubmit` on a failed submit. Fires when
1928
+ * client validation fails AND when the submit callback leaves errors in
1929
+ * the user-error layer (the `setErrors(...); return` server-rejection
1930
+ * pattern). Receives the full list of errors. Bind this when you want to
1931
+ * react to submit failures explicitly (alongside or instead of the
1932
+ * automatic `focusOnInvalidSubmit` nudge).
1933
+ */
1934
+ type OnError = (error: ValidationError[]) => void | Promise<void>;
1935
+ /**
1936
+ * The display-state verdict at a path: the one signal a UI needs to
1937
+ * decide what, if anything, to surface about validation right now.
1938
+ * Rolled up at containers and at the form root
1939
+ * (`form.meta.displayState`).
1940
+ *
1941
+ * - `'idle'`: nothing to surface, either because the timing gate has
1942
+ * not opened yet or because it has and no verdict is worth showing.
1943
+ * - `'pending'`: a run is in flight at this path and the prior verdict
1944
+ * is stale. Drive a spinner or a "Checking..." affordance.
1945
+ * - `'error'`: a blocking error the timing gate has cleared for
1946
+ * display.
1947
+ * - `'success'`: validation passed and the gate has cleared a positive
1948
+ * confirmation, the green-check pattern.
1949
+ *
1950
+ * The four `show*` booleans on `FieldState` are sugar over this enum
1951
+ * (`showErrors === (displayState === 'error')`, and so on), so they can
1952
+ * never contradict it.
1953
+ */
1954
+ type DisplayState = 'idle' | 'pending' | 'error' | 'success';
1955
+ /**
1956
+ * The `FieldState` keys layered on FROM the display-state predicate,
1957
+ * plus `firstError` and `firstOwnError`, computed alongside them.
1958
+ *
1959
+ * They are `Omit`'d from the predicate's own arguments so it cannot
1960
+ * read its own output and form a cycle. Enforced at the type level and
1961
+ * at runtime both: the base objects passed in literally lack these
1962
+ * keys, so neither an `as` cast nor a vanilla-JS caller can reach them.
1963
+ * `FieldStateBase` and `FormMetaBase` in `field-state-api.ts` omit the
1964
+ * same set in lockstep.
1965
+ */
1966
+ type FieldStateDerivedKey = 'displayState' | 'showErrors' | 'showPending' | 'showSuccess' | 'showIdle' | 'firstError' | 'firstOwnError';
1967
+ /**
1968
+ * One step of the display state machine: the verdict the field should
1969
+ * render now, as `display`, projected to `displayState` and the `show*`
1970
+ * booleans, plus two optional timing cells the engine reads.
1971
+ */
1972
+ type DisplayMachine = {
1973
+ readonly display: DisplayState;
1974
+ /**
1975
+ * An absolute `Date.now()` stamp meaning "re-evaluate this field no
1976
+ * later than here". The engine keeps one timer per form aimed at the
1977
+ * nearest `reviewAt` across all active fields; when it fires, the
1978
+ * dependent field computeds re-run and call the reducer again.
1979
+ *
1980
+ * A machine with no `reviewAt` and a non-pending `display` is
1981
+ * terminal, and the engine evicts it.
1982
+ */
1983
+ readonly reviewAt?: number;
1984
+ /**
1985
+ * The stamp at which `'pending'` was first shown: the memory the
1986
+ * min-visible hold needs, so a spinner that has only just appeared is
1987
+ * not yanked away the instant validation resolves. Opaque to the
1988
+ * engine, and a custom reducer may carry extra memory fields of its
1989
+ * own alongside it.
1990
+ */
1991
+ readonly pendingShownAt?: number;
1992
+ };
1993
+ /**
1994
+ * Inputs to the display reducer. `field` and `formMeta` are the
1995
+ * reactive snapshots it resolves against, minus the derived
1996
+ * `displayState`, `show*` and `firstError` keys, so the reducer can
1997
+ * never read its own output and form a cycle. See
1998
+ * `FieldStateDerivedKey`.
1999
+ */
2000
+ type DisplayCtx = {
2001
+ readonly field: Omit<FieldState, FieldStateDerivedKey>;
2002
+ readonly formMeta: Omit<FormMeta, FieldStateDerivedKey>;
2003
+ /**
2004
+ * `Date.now()` at which the field's current validation streak opened,
2005
+ * or `null` when nothing is in flight. This, not `field.validating`,
2006
+ * is the timing anchor: the elapsed wait is `now - validatingSince`.
2007
+ * Pinned to the start of the streak, so overlapping sub-runs do not
2008
+ * reset it.
2009
+ */
2010
+ readonly validatingSince: number | null;
2011
+ /**
2012
+ * The same anchor for an in-flight async `register` transform, or
2013
+ * `null` when none is running, including for a sync-only chain, which
2014
+ * never defers. It folds into the one in-flight clock the reducer
2015
+ * already runs for validation, so a deferred transform rides the
2016
+ * anti-flash spinner timing identically.
2017
+ */
2018
+ readonly transformingSince: number | null;
2019
+ /**
2020
+ * The engine's clock, injected so the reducer stays pure and
2021
+ * deterministic. Frozen to `0` under SSR, where there is no clock.
2022
+ */
2023
+ readonly now: number;
2024
+ };
2025
+ /**
2026
+ * Pure transition reducer resolving a path's `displayState`. Given the
2027
+ * field's previous `DisplayMachine` and the current `DisplayCtx` it
2028
+ * returns the next machine: the engine owns the clock and the timers,
2029
+ * the reducer owns the timing policy.
2030
+ *
2031
+ * It runs on every field-state read, and again whenever a `reviewAt`
2032
+ * deadline fires, so the whole app's validation-display behavior flows
2033
+ * from this one function in `core/display-state.ts`.
2034
+ *
2035
+ * @internal
2036
+ */
2037
+ type GetDisplayState = (prev: DisplayMachine, ctx: DisplayCtx) => DisplayMachine;
2038
+ /**
2039
+ * The submit handler `handleSubmit(onSubmit, onError)` returns. Bind it
2040
+ * to a `<form>`:
2041
+ *
2042
+ * ```vue
2043
+ * <form @submit="onSubmit">...</form>
2044
+ * ```
2045
+ *
2046
+ * It takes the originating `Event` and calls `event.preventDefault()`
2047
+ * itself, so bind with `@submit`, not `@submit.prevent`, which would
2048
+ * only prevent the default a second time. Called imperatively with no
2049
+ * event, the `preventDefault` step is skipped.
2050
+ */
2051
+ type SubmitHandler = (event?: Event) => Promise<void>;
2052
+ /**
2053
+ * The type of `form.handleSubmit`. Pass an `onSubmit` for the happy
2054
+ * path, and optionally an `onError` that receives the validation errors
2055
+ * when parsing fails.
2056
+ *
2057
+ * ```ts
2058
+ * const onSubmit = form.handleSubmit(
2059
+ * (data) => api.signup(data),
2060
+ * (errors) => console.log(errors),
2061
+ * )
2062
+ * ```
2063
+ */
2064
+ type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
2065
+ /**
2066
+ * Per-leaf internal tracker record, distinct from `FieldState.meta`,
2067
+ * which surfaces the registry-attached label, description and
2068
+ * placeholder payload as `Readonly<FieldMetaPayload>`.
2069
+ *
2070
+ * Surfaced for custom-adapter authors threading metadata through their
2071
+ * own pipelines. Most consumers do not reach for it, since the matching
2072
+ * fields appear in friendlier shape on `FieldState`.
2073
+ */
2074
+ type MetaTrackerValue = {
2075
+ /** ISO timestamp of the most recent write at this path. `null` if never written. */
2076
+ updatedAt: string | null;
2077
+ /** Value as it arrived, before any transforms. Distinguishes a parse-coerced read from raw user input. */
2078
+ rawValue: unknown;
2079
+ /** `true` while at least one binding to this path is currently mounted. */
2080
+ connected: boolean;
2081
+ /** Form this metadata belongs to. */
2082
+ formKey: FormKey;
2083
+ /** Dotted-string path to this leaf, or `null` when not applicable. */
2084
+ path: string | null;
2085
+ /**
2086
+ * `true` when storage and the visible display diverge at this path.
2087
+ * Reserved for the case the schema cannot see on its own: storage
2088
+ * forces a value, `0` for a numeric leaf or `0n` for a bigint leaf,
2089
+ * while the DOM input shows `''`, so the runtime needs a side channel
2090
+ * to tell "user typed 0" from "user supplied nothing".
2091
+ *
2092
+ * Set automatically for numeric leaves, by the directive's input
2093
+ * listener on clear and by the construction-time pass when the
2094
+ * consumer supplied no value. Set explicitly for any primitive leaf
2095
+ * through `setValue(path, unset)`, `defaultValues: { x: unset }` or
2096
+ * `reset({ x: unset })`, which is the documented opt-in for strings,
2097
+ * booleans and other types that do not otherwise diverge. Cleared on
2098
+ * the first non-`unset` write.
2099
+ *
2100
+ * Errors are reactive end to end, so any required path with
2101
+ * `blank: true` puts a "No value supplied" entry in `form.errors`
2102
+ * immediately, with no `validate()` or `handleSubmit` call. Most
2103
+ * consumers gate UI on `errors[path]` and `touched` instead; read
2104
+ * `blank` when you want pre-error introspection, such as a "the user
2105
+ * has not decided yet" indicator or a "review unanswered fields"
2106
+ * hint.
2107
+ *
2108
+ * See `docs/validation/blank.md` for the full conceptual model.
2109
+ */
2110
+ blank: boolean;
2111
+ };
2112
+ type RegisterFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'register'> : never;
2113
+ /**
2114
+ * A transformation applied to a field's value as user input flows from
2115
+ * the DOM through the directive's assigner. Composes left to right via
2116
+ * the `transforms: [...]` array on `register()`.
2117
+ *
2118
+ * The shape is deliberately generic-erased rather than per-path-typed,
2119
+ * so a personal library of transforms (`trim`, `lowercase`, `slugify`,
2120
+ * `clamp`) plugs into any `register()` slot whatever the path's value
2121
+ * type. Write defensive bodies that no-op on a type mismatch:
2122
+ *
2123
+ * ```ts
2124
+ * export const trim: RegisterTransform = (v) =>
2125
+ * typeof v === 'string' ? v.trim() : v
2126
+ * ```
2127
+ *
2128
+ * Call-site type safety is delegated to the slim-primitive gate: a
2129
+ * transform producing a value the path's storage does not accept is
2130
+ * rejected at write time with a standard diagnostic.
2131
+ *
2132
+ * Transforms may be sync or async. The chain stays fully synchronous,
2133
+ * with the value reaching form state in the same tick, until a
2134
+ * transform returns a thenable; from there the write defers, the field
2135
+ * reads `busy` and `transforming` while the chain settles, and the
2136
+ * resolved value commits once it lands. Rapid edits discard all but the
2137
+ * latest, and a rejection surfaces on `field.transformError` rather
2138
+ * than throwing or logging:
2139
+ *
2140
+ * ```ts
2141
+ * export const normalize: RegisterTransform = async (v, ctx) => {
2142
+ * const res = await fetch(`/normalize?q=${v}`, { signal: ctx?.signal })
2143
+ * return res.text()
2144
+ * }
2145
+ * ```
2146
+ *
2147
+ * `ctx.signal` aborts when the run is superseded by a newer edit, or is
2148
+ * torn down by `reset()` or unmount. Thread it into cancellable I/O so
2149
+ * a stale request is dropped. A sync chain never touches it and
2150
+ * allocates no controller.
2151
+ *
2152
+ * A synchronous throw is caught and aborts the pipeline: later
2153
+ * transforms do not run, nothing is written, and the assigner returns
2154
+ * `false`, so a buggy or defensively-throwing transform never crashes
2155
+ * the host app. An async rejection is the `transformError` channel
2156
+ * above, not a throw into the host app.
2157
+ */
2158
+ type RegisterTransform = (value: unknown, ctx?: TransformContext) => unknown;
2159
+ /** Options for `register(path, options)`, applied at the binding's own call site. */
2160
+ type RegisterOptions = {
2161
+ /**
2162
+ * Transformation pipeline applied to user-typed values before they
2163
+ * reach form state, composing left to right: each transform receives
2164
+ * the previous one's output, and the first receives the
2165
+ * directive-extracted DOM value.
2166
+ *
2167
+ * The full order is
2168
+ * `DOM event -> modifier cast (.lazy/.trim/.number) -> transforms[0] -> ... -> assigner`.
2169
+ *
2170
+ * User input only. `form.setValue(...)`,
2171
+ * `rv.setValueWithInternalPath(...)`, `form.reset()`, hydration, SSR
2172
+ * replay and `markBlank()` all bypass transforms, because they write
2173
+ * canonical state rather than normalized user input. To apply the
2174
+ * same normalization to a programmatic write, compose it yourself:
2175
+ *
2176
+ * ```ts
2177
+ * form.setValue('email', slugify(lowercase(rawValue)))
2178
+ * ```
2179
+ *
2180
+ * See `RegisterTransform` for the sync and async contract. For a
2181
+ * pattern that needs to inspect the `RegisterValue` itself, such as
2182
+ * rejection with a side effect, redirection to other fields or custom
2183
+ * DOM mutation, use `@update:registerValue` on the bound element
2184
+ * instead; see "Custom assigners" in the API docs.
2185
+ */
2186
+ transforms?: ReadonlyArray<RegisterTransform>;
2187
+ };
2188
+ /**
2189
+ * The narrow kernel surface the DOM binding drives, structurally a
2190
+ * subset of the form store: the field-record connect and disconnect
2191
+ * transitions, focus marking, which owns blur-validation, the record
2192
+ * and merged-error reads the focus walk needs, and the async-transform
2193
+ * abort for a fully detached path.
2194
+ *
2195
+ * @internal
2196
+ */
2197
+ type DomBindingKernel = {
2198
+ readonly noteDomConnected: (path: Path) => void;
2199
+ readonly noteDomDisconnected: (path: Path) => void;
2200
+ readonly markFocused: (path: Path, focused: boolean, meta?: {
2201
+ readonly instance?: WriteMeta['instance'];
2202
+ }) => void;
2203
+ readonly getFieldRecord: (path: Path) => FieldRecord | undefined;
2204
+ readonly getErrorsForPath: (path: Path) => ValidationError[];
2205
+ readonly cancelTransformsUnder: (prefix: Path) => void;
2206
+ };
2207
+ /**
2208
+ * The form store's DOM slice: the element registry behind
2209
+ * `field.element` and `field.elements`, no-latch host focus anchors,
2210
+ * and first-error focus resolution. Implemented in the directive
2211
+ * cluster's lazy graph and armed into the store's `domBinding` slot on
2212
+ * first use, see `RegisterValue.ensureDomBinding`. A `null` slot means
2213
+ * nothing in the app ever registered an element.
2214
+ *
2215
+ * @internal
2216
+ */
2217
+ type AttaformDomBinding = {
2218
+ readonly elements: Map<PathKey, ElementRecord>;
2219
+ readonly attach: (segments: Path, element: HTMLElement, formInstanceId: string, instanceMeta: WriteMeta['instance'] | undefined) => void;
2220
+ readonly detach: (segments: Path, element: HTMLElement) => void;
2221
+ readonly markHostConnected: (segments: Path, connected: boolean, hostEl: HTMLElement, formInstanceId: string) => void;
2222
+ readonly getFirstErrorElement: (formInstanceId: string) => {
2223
+ path: Path;
2224
+ element: HTMLElement;
2225
+ } | null;
2226
+ readonly hostChannel: HostChannel;
2227
+ };
2228
+ /**
2229
+ * The write policy for a `v-register` component host, held on the DOM
2230
+ * binding so it stays in the directive cluster's lazy graph. Applies the
2231
+ * binding site's modifiers, runs register `transforms` and schema
2232
+ * coercion, and buffers `.lazy` writes while focus is inside the host.
2233
+ *
2234
+ * - `write` commits a value the host emitted through
2235
+ * `update:modelValue`.
2236
+ * - `writeInput` takes an `input` emit and commits it only when the
2237
+ * payload carries the live value as an own `value` property.
2238
+ * - `markEditing` records focus entering or leaving the host. Leaving
2239
+ * commits a buffered `.lazy` value, or the trimmed form under `.trim`.
2240
+ * - `holdsDraft` reports a buffered `.lazy` value that storage has not
2241
+ * moved past, the edit a re-render must leave on screen.
2242
+ * - `release` drops any buffered value when the host unmounts.
2243
+ *
2244
+ * @internal
2245
+ */
2246
+ type HostChannel = {
2247
+ readonly write: (rv: RegisterValue, value: unknown, modifiers: HostModifiers) => boolean;
2248
+ readonly writeInput: (rv: RegisterValue, payload: unknown, modifiers: HostModifiers) => boolean;
2249
+ readonly markEditing: (rv: RegisterValue, editing: boolean, modifiers: HostModifiers) => void;
2250
+ readonly holdsDraft: (rv: RegisterValue) => boolean;
2251
+ readonly release: (rv: RegisterValue) => void;
2252
+ };
2253
+ /**
2254
+ * Factory the directive and `useRegister` inject through
2255
+ * `RegisterValue.ensureDomBinding` to arm a store's `domBinding` slot.
2256
+ *
2257
+ * @internal
2258
+ */
2259
+ type DomBindingFactory = (kernel: DomBindingKernel) => AttaformDomBinding;
2260
+ /**
2261
+ * What `form.register(path)` returns. Pass it to a native input via
2262
+ * `v-register`:
2263
+ *
2264
+ * ```vue
2265
+ * <input v-register="form.register('email')" />
2266
+ * ```
2267
+ *
2268
+ * Or read `innerRef` directly when integrating a custom component.
2269
+ *
2270
+ * It is a `shallowReadonly` reactive proxy: top-level reads track in
2271
+ * reactive scopes, mutations are blocked, and inner refs (`innerRef`,
2272
+ * `displayValue`) keep their `Ref` shape.
2273
+ *
2274
+ * `path`, `formKey` and `formInstanceId` are the wrapper-component
2275
+ * primitives: a generic component using `useRegister()` derives field
2276
+ * state and form identity from them without re-threading props from
2277
+ * its parent.
2278
+ */
2279
+ type RegisterValue<Value = unknown> = Readonly<{
2280
+ /**
2281
+ * Live, read-only value at this path. Watch it to drive UI that
2282
+ * depends on the field's current value.
2283
+ */
2284
+ innerRef: Readonly<Ref<Value>>;
2285
+ /**
2286
+ * Attach an HTML element to this binding. `v-register` calls it
2287
+ * automatically; expose it to a custom integration that registers an
2288
+ * element by hand.
2289
+ *
2290
+ * Recording the element drives the form's element map, behind
2291
+ * `field.meta.connected`, `focusFirstError` and `scrollToFirstError`.
2292
+ */
2293
+ registerElement: (el: HTMLElement) => void;
2294
+ /**
2295
+ * Detach an HTML element, dropping it from the form's element map.
2296
+ * Pair with `registerElement` for custom integrations.
2297
+ */
2298
+ deregisterElement: (el: HTMLElement) => void;
2299
+ /**
2300
+ * Write the field's value programmatically, returning `true` when
2301
+ * accepted and `false` when rejected, as for a wrong primitive type
2302
+ * at the path.
2303
+ *
2304
+ * This is the write path for custom directives and consumer
2305
+ * assigners: it routes through the same funnel, and the same
2306
+ * per-instance meta, as the directive's default assigner.
2307
+ * Caller-supplied `meta` passes through unchanged.
2308
+ */
2309
+ setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
2310
+ /**
2311
+ * Commit a value emitted by a third-party component bound through
2312
+ * `v-register`'s compile-time v-model desugar, and mark the field
2313
+ * interacted, since a v-model host has no DOM input listener to flip
2314
+ * the sticky `interacted` bit. The write takes the same path a native
2315
+ * input's does: the binding site's `.number` / `.trim` / `.lazy`
2316
+ * modifiers, then register `transforms`, then schema coercion. The
2317
+ * injected `onUpdate:modelValue` handler is the caller, passing the
2318
+ * modifiers as a static literal. `true` when the write was accepted
2319
+ * or buffered.
2320
+ *
2321
+ * @internal
2322
+ */
2323
+ setValueFromHost: (value: unknown, modifiers: HostModifiers) => boolean;
2324
+ /**
2325
+ * Commit the live value from a component host's `input` emit. A
2326
+ * payload that is a non-Event object owning a `value` property (the
2327
+ * shape PrimeVue's InputNumber reports per keystroke) writes that
2328
+ * value through `setValueFromHost`; anything else, a native `Event`
2329
+ * reaching a fallthrough listener included, is ignored and returns
2330
+ * `false`. The injected `onInput` handler is the only caller.
2331
+ *
2332
+ * @internal
2333
+ */
2334
+ setValueFromHostInput: (payload: unknown, modifiers: HostModifiers) => boolean;
2335
+ /**
2336
+ * The final step of a component-host write, after modifiers,
2337
+ * transforms and coercion: an empty signal (`''`, `null` or
2338
+ * `undefined`) the field's schema does not admit marks the field
2339
+ * blank, and anything else writes through. Called by the host write
2340
+ * channel.
2341
+ *
2342
+ * @internal
2343
+ */
2344
+ commitFromHost: (value: unknown) => boolean;
2345
+ /**
2346
+ * Mark this field DOM-connected during SSR, so a server-rendered
2347
+ * template reading `form.fields.<path>.connected` does not flicker on
2348
+ * hydration. `v-register` calls it for you; a no-op on the client.
2349
+ *
2350
+ * @internal
2351
+ */
2352
+ markConnectedOptimistically: () => void;
2353
+ /**
2354
+ * Mark this field DOM-connected or disconnected for a `v-register`
2355
+ * component host that binds value through the v-model desugar but
2356
+ * exposes no single inner control to register: a composite widget, or
2357
+ * one where none was found. The directive calls it on mount and
2358
+ * unmount. Distinct from the SSR-only
2359
+ * `markConnectedOptimistically`.
2360
+ *
2361
+ * `hostEl` is the host root, recorded on connect as the field's
2362
+ * focus-first-error anchor. A no-latch host registers no control, so
2363
+ * it would otherwise be invisible to `focusFirstError` and
2364
+ * `scrollToFirstError`, which walk the registered-element set. On an
2365
+ * invalid submit the error walk resolves the host root to its first
2366
+ * focusable descendant. The directive passes the same element on both
2367
+ * edges.
2368
+ *
2369
+ * @internal
2370
+ */
2371
+ markHostConnected: (connected: boolean, hostEl: HTMLElement) => void;
2372
+ /**
2373
+ * Mark this field focused or blurred for a `v-register` component
2374
+ * host with no single latched control: a composite widget whose focus
2375
+ * moves between inner segments, or a control-less one. The directive
2376
+ * tracks focusin and focusout on the widget root and forwards here,
2377
+ * so focus state and blur-validation still arm without an
2378
+ * element-level focus listener. Carries this binding's instance meta,
2379
+ * including its `validateOn`.
2380
+ *
2381
+ * @internal
2382
+ */
2383
+ markFocused: (focused: boolean) => void;
2384
+ /**
2385
+ * `true` when an element already registered for this binding's path
2386
+ * is inside, or is, `hostElement`. The directive's component-host
2387
+ * branch reads it to tell a `useRegister` wrapper, whose inner
2388
+ * control self-registered, from a third-party component, which
2389
+ * registered nothing, so the latch runs only for the latter.
2390
+ *
2391
+ * @internal
2392
+ */
2393
+ hasRegisteredDescendant: (hostElement: HTMLElement) => boolean;
2394
+ /**
2395
+ * Arm this binding's form store with the DOM-binding implementation:
2396
+ * element registry, focus listeners, first-error focus resolution.
2397
+ * That implementation lives in the directive cluster's lazy graph, so
2398
+ * the directive and `useRegister` inject its factory here before any
2399
+ * element call, and once armed the store's `domBinding` slot serves
2400
+ * every later registration.
2401
+ *
2402
+ * Optional, so a hand-rolled RegisterValue owning its own element
2403
+ * handling need not declare it. Returns the armed binding, which is how
2404
+ * the directive reaches the host write channel.
2405
+ *
2406
+ * @internal
2407
+ */
2408
+ ensureDomBinding?: (factory: DomBindingFactory) => AttaformDomBinding;
2409
+ /**
2410
+ * Canonical JSON-encoded path key for this binding, such as
2411
+ * `'["items",0,"name"]'`. Useful for stable Map and Set keys, log
2412
+ * messages, and equality against another `RegisterValue`'s path.
2413
+ * Treat it as opaque: for `form.fields(...)` and `form.values(...)`
2414
+ * lookups inside a wrapper component, use `segments`.
2415
+ */
2416
+ path: PathKey;
2417
+ /**
2418
+ * Structured path segments for this binding, such as
2419
+ * `['items', 0, 'name']`. The consumer-friendly form for
2420
+ * `form.fields(...)` and `form.values(...)` lookups in a generic
2421
+ * wrapper:
2422
+ *
2423
+ * ```ts
2424
+ * const rv = useRegister()
2425
+ * const form = injectForm()
2426
+ * const field = computed(() => form.fields(rv.value?.segments ?? []))
2427
+ * ```
2428
+ *
2429
+ * Frozen at runtime, so a wrapper can read it without defensive
2430
+ * copying.
2431
+ */
2432
+ segments: Path;
2433
+ /**
2434
+ * The form's `key`, supplied or auto-allocated, mirroring `form.key`.
2435
+ * Useful in a wrapper that targets a specific form by key without
2436
+ * prop-drilling.
2437
+ */
2438
+ formKey: string;
2439
+ /**
2440
+ * Per-mount identifier for the form instance, stable across the
2441
+ * form's lifetime. The directive uses it to scope element
2442
+ * registrations to a single mount; it is exposed here for wrappers
2443
+ * that must disambiguate sibling forms sharing a `key`.
2444
+ */
2445
+ formInstanceId: string;
2446
+ /**
2447
+ * The readonly handle the directive iterates for this binding's
2448
+ * transform pipeline. See `RegisterOptions.transforms` for the public
2449
+ * contract. Optional, so a hand-rolled mock need not declare an empty
2450
+ * array; the directive falls back to a no-op pipeline.
2451
+ *
2452
+ * @internal
2453
+ */
2454
+ transforms?: ReadonlyArray<RegisterTransform>;
2455
+ /**
2456
+ * Schema-driven coercion closure baked at register time, capturing
2457
+ * the path's slim accept set and the resolved coercion index so the
2458
+ * per-event hot path is one function call. The identity function when
2459
+ * coercion is disabled or the path admits no coercion target.
2460
+ * Optional, so a hand-rolled mock need not declare it; the directive
2461
+ * falls back to identity.
2462
+ *
2463
+ * @internal
2464
+ */
2465
+ coerce?: (value: unknown) => unknown;
2466
+ /**
2467
+ * Element-level coercion closure for container paths (`z.array(...)`,
2468
+ * `z.set(...)`), coercing a scalar DOM-side value such as an option's
2469
+ * `value` attribute or a checkbox's value against the container's
2470
+ * element type. `undefined` when the path is not a container, where
2471
+ * `coerce` is used exclusively.
2472
+ *
2473
+ * The directive's read-side comparisons in `setChecked` and
2474
+ * `setSelected` use it to keep parity with the change handler's
2475
+ * write-side path-level coerce.
2476
+ *
2477
+ * @internal
2478
+ */
2479
+ coerceElement?: (value: unknown) => unknown;
2480
+ /**
2481
+ * Read-only string view of the field's current value: what the
2482
+ * compile-time `:value` injection reads on every input, textarea and
2483
+ * select bound by `v-register`.
2484
+ *
2485
+ * `''` when the path is in the form's `blankPaths` set, or storage is
2486
+ * `null` or `undefined`; otherwise `String(storage)`. The blank branch
2487
+ * is what lets a user clear a numeric field without the next render
2488
+ * patching `el.value` back to the slim default `'0'`.
2489
+ */
2490
+ displayValue: Readonly<Ref<string>>;
2491
+ /**
2492
+ * Blank-aware presentation for a component host's `:modelValue`, the
2493
+ * typed-model analog of `displayValue`'s `''`: `undefined` when the
2494
+ * path is in `blankPaths`, otherwise the raw typed storage. Lets a
2495
+ * cleared numeric field read empty in a v-model-bound component as it
2496
+ * does in a native input. Read only by the compile-time
2497
+ * component-bridge transform.
2498
+ *
2499
+ * @internal
2500
+ */
2501
+ hostModelValue: Readonly<Ref<Value | undefined>>;
2502
+ /**
2503
+ * Live `true` when this binding's form is frozen via
2504
+ * `useForm({ disabled })`. The compile-time transforms bind it to the
2505
+ * host's `:disabled`, so a native `<input v-register>` renders the
2506
+ * HTML `disabled` attribute on both server and client, and a
2507
+ * component host receives a `disabled` prop. Read it from a custom
2508
+ * `useRegister` integration to drive your own disabled affordance.
2509
+ */
2510
+ disabled: Readonly<Ref<boolean>>;
2511
+ /**
2512
+ * Add this field's path to the form's `blankPaths` set, writing the
2513
+ * slim default to storage. Returns the `setValueAtPath` verdict:
2514
+ * `true` accepted, `false` rejected by the slim-primitive gate.
2515
+ *
2516
+ * Called by the directive's input listener on a numeric clear, and by
2517
+ * the `setValue(path, unset)` translation.
2518
+ *
2519
+ * @internal
2520
+ */
2521
+ markBlank: () => boolean;
2522
+ /**
2523
+ * Flip this field's sticky `interacted` flag, the signal that the
2524
+ * user has made at least one value edit here, an insert or a delete.
2525
+ * Called by the directive's input and change listeners on genuine
2526
+ * user input, never by hydration or a programmatic write. Idempotent,
2527
+ * since the store skips the write once set.
2528
+ *
2529
+ * @internal
2530
+ */
2531
+ markInteracted: () => void;
2532
+ /**
2533
+ * `true` when the schema's slim primitive set at this path includes
2534
+ * `'undefined'`, meaning the leaf was declared `.optional()`, or sits
2535
+ * in a union admitting `undefined`. Cached at register time.
2536
+ *
2537
+ * The directive's text-input listener reads it to map a DOM clear
2538
+ * onto `undefined` storage rather than `''`, so the `.optional()`
2539
+ * absent semantic survives user interaction. Without it, a user who
2540
+ * typed an invalid value into an optional field and then cleared it
2541
+ * would be stuck with a permanent validation error, storage holding
2542
+ * `''`, which is neither `undefined` nor a valid inner value.
2543
+ *
2544
+ * @internal
2545
+ */
2546
+ acceptsUndefined: boolean;
2547
+ /**
2548
+ * `true` when the schema's slim primitive set at this path includes
2549
+ * `'string'`. Cached at register time alongside `acceptsUndefined`.
2550
+ *
2551
+ * The directive's text-input listener reads it so a DOM clear on a
2552
+ * numeric-only, boolean-only or bigint-only leaf takes the
2553
+ * `markBlank` path instead of writing `''` through the assigner. The
2554
+ * slim gate would reject the empty string anyway, and the directive's
2555
+ * post-write force-sync would then snap the DOM back to the last
2556
+ * accepted value, making the final character undeletable. Through
2557
+ * `markBlank`, storage holds the slim default with the blank meta and
2558
+ * the DOM stays empty.
2559
+ *
2560
+ * @internal
2561
+ */
2562
+ acceptsString: boolean;
2563
+ /**
2564
+ * The field's aria satellite ids, mirroring `FieldState.aria`. The
2565
+ * directive points `aria-describedby` at `errorId` while the field is
2566
+ * in its error state. Optional for mock tolerance; the directive
2567
+ * skips aria wiring when absent.
2568
+ *
2569
+ * @internal
2570
+ */
2571
+ aria?: {
2572
+ readonly errorId: string;
2573
+ readonly descriptionId: string;
2574
+ };
2575
+ /**
2576
+ * Whether the schema marks this path required, from
2577
+ * `schema.isRequiredAtPath(segments)`. Drives `aria-required`.
2578
+ * Optional for the same mock tolerance as `aria`.
2579
+ *
2580
+ * @internal
2581
+ */
2582
+ isRequired?: boolean;
2583
+ /**
2584
+ * The gated display-state verdict for this path, reusing the same
2585
+ * field-state identity as `form.fields`. The directive watches it to
2586
+ * keep `aria-invalid`, `aria-busy` and `aria-describedby` in lockstep
2587
+ * with the visible error state, even on an async tick with no parent
2588
+ * re-render. Optional; the directive skips aria wiring when absent.
2589
+ *
2590
+ * @internal
2591
+ */
2592
+ ariaDisplayState?: Readonly<Ref<DisplayState>>;
2593
+ }>;
2594
+ /**
2595
+ * Mutable holder for an async-transform run's `AbortController`, shared
2596
+ * by the directive, which creates the controller the first time a
2597
+ * transform reaches for `ctx.signal`, and the store, which aborts it
2598
+ * when the run is superseded, cancelled or reset. `controller` stays
2599
+ * `null` until `ctx.signal` is touched, so a purely sync chain never
2600
+ * allocates one. `aborted` latches `true` the moment the store tears the
2601
+ * run down, so a signal accessed AFTER teardown still resolves to an
2602
+ * already-aborted signal rather than a live one.
2603
+ */
2604
+ type TransformAbortHolder = {
2605
+ controller: AbortController | null;
2606
+ aborted: boolean;
2607
+ };
2608
+ /**
2609
+ * The second argument handed to every transform in a `transforms: [...]`
2610
+ * chain. `signal` is an `AbortSignal` that aborts when the run is
2611
+ * superseded by newer input or torn down by a reset or cancel, so a
2612
+ * transform doing cancellable I/O (a `fetch`, a worker round-trip) can
2613
+ * pass `ctx.signal` through and bail the moment its result stops
2614
+ * mattering.
2615
+ *
2616
+ * The signal is lazy: its backing `AbortController` is allocated on
2617
+ * first access, so a purely synchronous chain that never reaches for
2618
+ * `ctx.signal` allocates nothing, and having no in-flight I/O to cancel,
2619
+ * never aborts.
2620
+ */
2621
+ type TransformContext = {
2622
+ readonly signal: AbortSignal;
2623
+ };
2624
+ /**
2625
+ * Custom assigner installed on an element via the directive's
2626
+ * `[assignKey]` slot OR an `@update:registerValue` listener. Called
2627
+ * by the directive when a DOM event (input / change / etc.) fires
2628
+ * on the bound element.
2629
+ *
2630
+ * The directive passes the extracted value plus the `RegisterValue`
2631
+ * the directive is currently bound to, regardless of install path.
2632
+ * The second arg lets a top-level handler write back to form state
2633
+ * without having to capture the RV via closure:
2634
+ *
2635
+ * ```ts
2636
+ * function upperCaseAssigner(value: unknown, rv: RegisterValue): void {
2637
+ * rv.setValueWithInternalPath(String(value ?? '').toUpperCase())
2638
+ * }
2639
+ * ```
2640
+ *
2641
+ * The `registerValue` parameter is typed optional only to keep
2642
+ * standalone invocations from outside the directive (rare; manual
2643
+ * dispatch in tests, for example) type-checkable; the directive
2644
+ * itself always supplies it at fire time.
2645
+ *
2646
+ * Return `true` when the write was accepted, `false` when it was
2647
+ * rejected (e.g. the value didn't match the path's expected type).
2648
+ * `undefined` is treated as "succeeded" so simple assigners can
2649
+ * just return `void`.
2650
+ */
2651
+ type CustomDirectiveRegisterAssignerFn = (value: unknown, registerValue?: RegisterValue) => boolean | undefined;
2652
+ /**
2653
+ * Generic shape of a v-register directive variant. Used by the
2654
+ * library's text / checkbox / radio / select directive types and
2655
+ * available for custom integrations that need to drop in their own
2656
+ * variant.
2657
+ *
2658
+ * The value generic admits `undefined` because `useRegister()` may
2659
+ * return `undefined` (a wrapper component rendered without a parent
2660
+ * `registerValue`); binding that value to `v-register` is supported
2661
+ * and installs a no-op assigner at runtime.
2662
+ */
2663
+ type CustomRegisterDirective<T, Modifiers extends string = string> = ObjectDirective<T & {
2664
+ _assigning?: boolean;
2665
+ /**
2666
+ * Snapshot of the last `value.innerRef.value` reference the
2667
+ * directive's DOM-sync (setSelected / setChecked / radio
2668
+ * `el.checked = …` / text `el.value = …`) was applied for. Used by
2669
+ * every input directive's `updated` / `beforeUpdate` to skip the
2670
+ * per-render DOM sync when the model is identity-unchanged (for
2671
+ * text, a focused `.lazy` edit is held through it), so a parent
2672
+ * re-render (a typed character in a sibling, an async-validation
2673
+ * tick, any reactive read) cannot clobber an in-progress user
2674
+ * interaction. Identity comparison is sound because every form
2675
+ * write produces a fresh value at the path: scalars are new
2676
+ * primitives, and arrays and Sets get fresh references along the
2677
+ * spine through diff-apply, so reference equality on
2678
+ * `innerRef.value` tracks "did the model move" exactly. The
2679
+ * `_assigning` gate stays alongside it, short-circuiting the
2680
+ * immediate post-write render where the user's own input has
2681
+ * already put the DOM in sync.
2682
+ */
2683
+ _lastAppliedModel?: unknown;
2684
+ /**
2685
+ * Variant-specific "repaint the DOM from current storage" closure,
2686
+ * stashed by each input directive's `created` hook (it mirrors that
2687
+ * variant's post-write force-sync block). The deferred async-transform
2688
+ * orchestrator calls it once the resolved value has committed, so a
2689
+ * bare `<input v-register>` with no other reactive reader still paints
2690
+ * the normalized result without depending on a parent re-render.
2691
+ */
2692
+ _syncFromStorage?: () => void;
2693
+ [S: symbol]: CustomDirectiveRegisterAssignerFn;
2694
+ }, RegisterValue | undefined, Modifiers, string>;
2695
+ /**
2696
+ * Modifier names supported by `v-register` on `<input type="text">`,
2697
+ * `<input type="number">`, `<textarea>`, and component hosts. Mirrors
2698
+ * Vue's `v-model` modifier semantics on the same elements; combine
2699
+ * freely (`<input v-register.lazy.trim.number="..." />`).
2700
+ */
2701
+ type RegisterTextModifier =
2702
+ /**
2703
+ * Write on `change` (blur) instead of `input`. The reactive
2704
+ * model only updates after the user tabs/clicks out of the
2705
+ * field. IME composition handlers are skipped under `.lazy`, since
2706
+ * composition events do not gate writes. On a component host the
2707
+ * last value emitted while focus is inside the host commits when
2708
+ * focus leaves it.
2709
+ */
2710
+ 'lazy'
2711
+ /**
2712
+ * Strip leading and trailing whitespace on blur. The form holds
2713
+ * the user's raw input (whitespace included) while they're
2714
+ * typing; on `change` (blur / commit) the value is trimmed
2715
+ * once and written back to both the model and the visible DOM.
2716
+ * Combine with `.lazy` to skip the mid-typing writes entirely. On a
2717
+ * component host the trimmed value commits when focus leaves the
2718
+ * host.
2719
+ */
2720
+ | 'trim'
2721
+ /**
2722
+ * Cast the value with `parseFloat` before writing. An empty field,
2723
+ * or text that does not parse as a number (`'abc'`), marks the field
2724
+ * blank instead of writing a string. On a component host the cast
2725
+ * applies to string emits, and a value the component already typed
2726
+ * passes through. Auto-applied for `<input type="number">`, where an
2727
+ * explicit `.number` is redundant.
2728
+ */
2729
+ | 'number';
2730
+ /**
2731
+ * The `v-register` modifiers a component host honours, carried from the
2732
+ * binding site to the host write channel as a static literal that the
2733
+ * compile-time bridge transform emits. Shaped like Vue's directive
2734
+ * modifiers, so the directive passes a binding's modifiers to the channel
2735
+ * unchanged.
2736
+ *
2737
+ * @internal
2738
+ */
2739
+ type HostModifiers = Readonly<Partial<Record<RegisterTextModifier, boolean>>>;
2740
+ /**
2741
+ * v-register directive variant for `<input type="text">`,
2742
+ * `<input type="number">` and `<textarea>`. Supports the `.lazy`,
2743
+ * `.trim` and `.number` modifiers; `RegisterTextModifier` carries
2744
+ * the per-modifier semantics.
2745
+ */
2746
+ type RegisterTextCustomDirective = CustomRegisterDirective<HTMLInputElement | HTMLTextAreaElement, RegisterTextModifier>;
2747
+ /** v-register directive variant for checkboxes. No modifiers. */
2748
+ type RegisterCheckboxCustomDirective = CustomRegisterDirective<HTMLInputElement>;
2749
+ /** v-register directive variant for radio inputs. No modifiers. */
2750
+ type RegisterRadioCustomDirective = CustomRegisterDirective<HTMLInputElement>;
2751
+ /**
2752
+ * Modifier name supported by `v-register` on `<select>`. Mirrors
2753
+ * Vue's `v-model` `.number` on the same element.
2754
+ */
2755
+ type RegisterSelectModifier =
2756
+ /**
2757
+ * Cast each selected option's `value` with `parseFloat` before
2758
+ * writing, so form state holds numbers rather than numeric
2759
+ * strings. The case for it is markup whose option values are
2760
+ * written as strings against a schema expecting numbers.
2761
+ */
2762
+ 'number';
2763
+ /**
2764
+ * v-register directive variant for `<select>`. Supports `.number`;
2765
+ * `RegisterSelectModifier` carries the semantics.
2766
+ */
2767
+ type RegisterSelectCustomDirective = CustomRegisterDirective<HTMLSelectElement, RegisterSelectModifier>;
2768
+ /** v-register directive variant for the dynamic input/select/textarea bridge. */
2769
+ type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, RegisterValue | undefined, string>;
2770
+ /**
2771
+ * The `v-register` directive. Binds a form field to a native
2772
+ * input, select, textarea, checkbox, or radio:
2773
+ *
2774
+ * ```vue
2775
+ * <input v-register="form.register('email')" />
2776
+ * <select v-register="form.register('country')">
2777
+ * <option value="us">US</option>
2778
+ * <option value="uk">UK</option>
2779
+ * </select>
2780
+ * ```
2781
+ *
2782
+ * It also works on a custom component whose root is NOT a native
2783
+ * input: call `useRegister()` in the child's setup to read the
2784
+ * parent's binding, then re-bind `v-register` onto an inner native
2785
+ * element. When the wrapper's root IS the input, attribute
2786
+ * fallthrough handles it and `useRegister` is unnecessary.
2787
+ *
2788
+ * ```vue
2789
+ * <!-- Parent -->
2790
+ * <MyField label="Email" v-register="form.register('email')" />
2791
+ *
2792
+ * <!-- MyField.vue (root is <label>, not <input>) -->
2793
+ * <script setup>
2794
+ * import { useRegister } from 'attaform'
2795
+ * defineProps<{ label: string }>()
2796
+ * const register = useRegister()
2797
+ * </script>
2798
+ * <template>
2799
+ * <label>
2800
+ * <span>{{ label }}</span>
2801
+ * <input v-register="register" />
2802
+ * </label>
2803
+ * </template>
2804
+ * ```
2805
+ *
2806
+ * Modifier support varies by element:
2807
+ * - text / number / textarea: `.lazy`, `.trim`, `.number`
2808
+ * - select: `.number`
2809
+ * - checkbox / radio: none
2810
+ *
2811
+ * See `RegisterTextModifier` / `RegisterSelectModifier` for
2812
+ * per-modifier semantics.
2813
+ *
2814
+ * Delivered by the Vite or Nuxt plugin's compile-time binding, and by
2815
+ * `installVRegister(app)` from `attaform/directive` everywhere else.
2816
+ * Most consumers never import the directive itself; it is exposed for
2817
+ * integrations that install directives manually.
2818
+ */
2819
+ type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
2820
+ /**
2821
+ * Module augmentation: register `v-register` with Vue's template
2822
+ * type system. Lives in `types-api` because every public entry
2823
+ * (`attaform`, `attaform/zod`, `attaform/zod-v3`, `attaform/zod-v4`)
2824
+ * transitively reaches this file via the `useForm` return type, so
2825
+ * the augmentation propagates to consumer SFCs regardless of which
2826
+ * entry they import from, and whether or not they install
2827
+ * `attaform/nuxt` or the Vite plugin.
2828
+ *
2829
+ * Augmentation targets `vue` rather than `@vue/runtime-core`:
2830
+ * `GlobalDirectives` is originally declared in `@vue/runtime-core`,
2831
+ * but consumers and Volar's strict-template codegen both resolve
2832
+ * the interface through `vue`'s `export * from '@vue/runtime-dom'`
2833
+ * to `export * from '@vue/runtime-core'` chain. TypeScript merges
2834
+ * interfaces across re-exports, so augmenting `'vue'` reaches Volar
2835
+ * without needing `@vue/runtime-core` to be hoisted into the
2836
+ * library's own `node_modules` for its own typecheck.
2837
+ */
2838
+ declare module 'vue' {
2839
+ interface GlobalDirectives {
2840
+ /**
2841
+ * The `v-register` directive. Binds a form field to a native
2842
+ * input, select, textarea, checkbox, or radio:
2843
+ *
2844
+ * ```vue
2845
+ * <input v-register="form.register('email')" />
2846
+ * ```
2847
+ *
2848
+ * It also works on a custom component whose root is NOT a native
2849
+ * input: call `useRegister()` in the child's setup to read the
2850
+ * parent's binding, then re-bind `v-register` onto an inner
2851
+ * native element. When the wrapper's root IS the input,
2852
+ * attribute fallthrough handles it and `useRegister` is
2853
+ * unnecessary.
2854
+ *
2855
+ * Modifier support varies by element:
2856
+ * - text / number / textarea: `.lazy`, `.trim`, `.number`
2857
+ * - select: `.number`
2858
+ * - checkbox / radio: none
2859
+ */
2860
+ vRegister: RegisterDirective;
2861
+ }
2862
+ }
2863
+ /**
2864
+ * Callback form of `setValue`'s value argument. Receives the previous
2865
+ * value at the path and returns the next value:
2866
+ *
2867
+ * ```ts
2868
+ * form.setValue('count', (prev) => prev + 1)
2869
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
2870
+ * ```
2871
+ *
2872
+ * A whole-form callback replaces the whole form. Any key the return
2873
+ * omits is refilled from the SCHEMA's declared defaults, or the slim
2874
+ * value where none is declared, so an omitted key is never carried over
2875
+ * from the previous value or from `defaultValues`. Spread `prev` to keep
2876
+ * the rest of the form.
2877
+ */
2878
+ type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
2879
+ /**
2880
+ * The value argument of `form.setValue`. Either the next value
2881
+ * directly, or a callback that derives it from the previous value.
2882
+ *
2883
+ * Type parameters:
2884
+ * - `Write`: what the direct value form accepts, the storable shape at
2885
+ * the path.
2886
+ * - `Read`: what the callback's `prev` argument exposes, defaulting to
2887
+ * `Write`. For a whole-form callback the read shape tags array
2888
+ * elements as possibly-undefined, to match runtime reality.
2889
+ */
2890
+ type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
2891
+ /**
2892
+ * Detect `any` distinctly from `unknown`. The trick: `1 & any` is `any`
2893
+ * and `0 extends any` is `true`; `1 & unknown` is `1` and `0 extends 1`
2894
+ * is `false`. Used to fork `PathSetValuePayload` so `z.any()` paths
2895
+ * resolve to `any` (matching the read-side surface) and `z.unknown()` /
2896
+ * preprocess paths resolve to `unknown` (matching Zod's input typing).
2897
+ */
2898
+ type IsAny<T> = 0 extends 1 & T ? true : false;
2899
+ /**
2900
+ * Resolves `setValue`'s `value` argument type at a single `Path` leaf.
2901
+ *
2902
+ * Three branches, one per Zod input-typing case:
2903
+ *
2904
+ * 1. **`any` leaf (`z.any()`)**: the schema input type is `any`, and
2905
+ * so is the whole form API surface at this path (read, register,
2906
+ * fields). This branch returns raw `any` to stay consistent with
2907
+ * them. A call site passing an unannotated `(prev) => ...` may hit
2908
+ * `noImplicitAny` under the consumer's tsconfig; annotating
2909
+ * `(prev: any) => ...` opts into the looser shape explicitly.
2910
+ *
2911
+ * 2. **`unknown` leaf (`z.unknown()`, `z.preprocess()` input)**: the
2912
+ * schema input is unconstrained and consumers narrow before use.
2913
+ * The branch returns
2914
+ * `({} | null | undefined) | ((prev: unknown) => unknown)` rather
2915
+ * than a `SetValuePayload<unknown, ...>`-style union, for three
2916
+ * reasons:
2917
+ *
2918
+ * a. **Union absorption.** `unknown | X` collapses to `unknown`,
2919
+ * erasing the callback union member. With the callback shape
2920
+ * gone, TS has no contextual type for `prev` and decays it to
2921
+ * implicit `any` under `noImplicitAny`. The triple
2922
+ * `{} | null | undefined` covers the same value space as
2923
+ * `unknown` but is NOT subject to absorption, so the callback
2924
+ * branch survives and `prev` infers cleanly to `unknown`.
2925
+ *
2926
+ * b. **`NonNullable<unknown> = {}`.** Applying `NonNullable` to
2927
+ * the read slot for an unknown leaf narrows `prev` to `{}`,
2928
+ * which is looser than `unknown` and allows ad-hoc property
2929
+ * access. This branch keeps the read slot as `unknown` so the
2930
+ * consumer has to narrow.
2931
+ *
2932
+ * c. **`Unset`-widening does not apply.** `DefaultValuesShape`
2933
+ * widens a primitive leaf to admit `unset`, and an unknown leaf
2934
+ * has no primitive to widen. The open-form triple covers the
2935
+ * same value space the runtime accepts, `unset` included, since
2936
+ * a symbol is a `{}`.
2937
+ *
2938
+ * 3. **Every other leaf** flows through unchanged via
2939
+ * `SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>`.
2940
+ */
2941
+ type PathSetValuePayload<Leaf> = IsAny<Leaf> extends true ? any : unknown extends Leaf ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
2942
+ ({} | null | undefined) | ((prev: unknown) => unknown) : SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>;
2943
+ /**
2944
+ * Per-field reactive shape returned by `form.fields.<leaf-path>` and
2945
+ * `form.fields(path)`. Slim, readonly across the board. One type lives at
2946
+ * every path, with aggregations rolled up at containers.
2947
+ *
2948
+ * Leaf-aware: dot-access injects these keys at LEAF paths only. At a
2949
+ * container path the proxy descends without injecting, so a schema field
2950
+ * literally named `dirty` at depth 2 or deeper stays reachable as a
2951
+ * descent target and nothing is shadowed. The container call form
2952
+ * (`form.fields('address')`) returns a `FieldState` whose keys aggregate
2953
+ * the descendant leaves.
2954
+ */
2955
+ type FieldState<Value = unknown> = {
2956
+ readonly value: Value;
2957
+ readonly original: Value;
2958
+ readonly pristine: boolean;
2959
+ readonly dirty: boolean;
2960
+ readonly focused: boolean | null;
2961
+ readonly blurred: boolean | null;
2962
+ readonly touched: boolean;
2963
+ /**
2964
+ * `true` once the user has issued at least one value edit on this
2965
+ * field through `v-register` (an insert or a delete), sticky
2966
+ * thereafter and preserved across disconnects. Distinct from
2967
+ * `dirty`: typing `"a"` then deleting it back to empty leaves the
2968
+ * field net-unchanged (`dirty: false`) yet `interacted: true`.
2969
+ * Distinct from `touched`: tabbing through a field without editing
2970
+ * flips `touched` but never `interacted`. Set only by user input,
2971
+ * never by hydration or programmatic `setValue`; cleared by
2972
+ * `form.reset()` / `form.resetField(path)`. Containers roll it up as
2973
+ * a disjunction (any descendant interacted).
2974
+ */
2975
+ readonly interacted: boolean;
2976
+ /**
2977
+ * `true` once the user has blurred this field after editing it: the
2978
+ * first time they edit a value and then leave. Sticky thereafter and
2979
+ * preserved across disconnects; a tab-through with no edit never sets
2980
+ * it (`interacted` is still false at that blur). It composes
2981
+ * `interacted` with the departure and drives the default display gate,
2982
+ * so errors reveal once the user finishes a pass and leaves, then stay
2983
+ * visible through a re-focus to be fixed live. Set only by user
2984
+ * input/blur, never by hydration or programmatic writes; cleared by
2985
+ * `form.reset()` / `form.resetField(path)`. Containers roll it up as a
2986
+ * disjunction.
2987
+ */
2988
+ readonly blurredAfterInteraction: boolean;
2989
+ readonly connected: boolean;
2990
+ /**
2991
+ * The first DOM element bound to this path via `v-register`, or
2992
+ * `null` when none is registered (initial mount, post-unmount,
2993
+ * SSR). "First" means first by registration order. Reach for it to
2994
+ * call a native DOM method on a field's input (`focus()`,
2995
+ * `scrollIntoView()`, `select()`, `setSelectionRange()`) without
2996
+ * Attaform having to verb every imperative:
2997
+ *
2998
+ * ```ts
2999
+ * form.fields.email.element?.focus()
3000
+ * form.fields.email.element?.scrollIntoView({ block: 'center' })
3001
+ * ```
3002
+ *
3003
+ * On a path with multiple bindings (input syncing, mirrored shadow
3004
+ * inputs), prefer `elements` and pick the target yourself. Reactive:
3005
+ * a register or deregister triggers re-evaluation.
3006
+ */
3007
+ readonly element: HTMLElement | null;
3008
+ /**
3009
+ * Every DOM element currently bound to this path via `v-register`,
3010
+ * in registration order, or an empty array when none is. Two
3011
+ * bindings on one path is a supported shape, for input syncing or
3012
+ * mirrored shadow inputs:
3013
+ *
3014
+ * ```ts
3015
+ * for (const el of form.fields.email.elements) el.blur()
3016
+ * ```
3017
+ *
3018
+ * For the common single-binding case, `element` is sugar over
3019
+ * `elements[0] ?? null`.
3020
+ */
3021
+ readonly elements: readonly HTMLElement[];
3022
+ readonly updatedAt: string | null;
3023
+ /**
3024
+ * Every `ValidationError` in this path's SUBTREE: the node's own
3025
+ * bucket plus every descendant's, sorted by schema-declaration order.
3026
+ * On a leaf (no descendants) this equals `ownErrors`. On a container
3027
+ * it rolls up the whole fieldset, so a parent's `errors` is non-empty
3028
+ * whenever any child is invalid.
3029
+ *
3030
+ * For the errors pinned at THIS path alone (a container's own
3031
+ * cross-field `.refine()`, or `setErrors` at this path), read
3032
+ * `ownErrors`.
3033
+ */
3034
+ readonly errors: readonly ValidationError[];
3035
+ /**
3036
+ * Every `ValidationError` at THIS exact path's own bucket, excluding
3037
+ * descendants: schema errors, blank-required errors, and `setErrors`
3038
+ * pinned here, in `schema`, `blank`, `user` order. The exact-path
3039
+ * counterpart to the subtree-scoped `errors`.
3040
+ *
3041
+ * On a leaf this is the same array as `errors` (a leaf has no
3042
+ * descendants). On a container it holds only the container's OWN
3043
+ * errors, a cross-field `.refine()` on the fieldset or a path-less
3044
+ * `setErrors` at this node, without dragging in any child error:
3045
+ *
3046
+ * ```vue
3047
+ * <p v-if="form.fields.address.firstOwnError">
3048
+ * {{ form.fields.address.firstOwnError.message }}
3049
+ * </p>
3050
+ * ```
3051
+ *
3052
+ * On `form.meta` this is the root `[]` bucket: form-level errors from
3053
+ * a root `.refine()` or a path-less `setErrors`. `form.meta.ownErrors`
3054
+ * (with `firstOwnError`) is the banner accessor for those.
3055
+ */
3056
+ readonly ownErrors: readonly ValidationError[];
3057
+ /**
3058
+ * `true` while a per-field validation run is in flight at this path.
3059
+ * Reflects field-level debounced runs (`validate-on-change`) and
3060
+ * cross-field re-validations targeting this path. A whole-form
3061
+ * `validate()` or `parse()` drives `form.meta.validating` alone and
3062
+ * never flips a per-field flag.
3063
+ *
3064
+ * Per-field analogue of `form.meta.validating`. Use for a tight
3065
+ * "Checking…" indicator next to a single async-validated input
3066
+ * without commandeering the whole-form spinner.
3067
+ */
3068
+ readonly validating: boolean;
3069
+ /**
3070
+ * `true` when this field has no errors AND no per-field validation
3071
+ * is in flight (`errors.length === 0 && !validating`). Confidence
3072
+ * that "we've checked, and we have no problems right now." Use for
3073
+ * green-checkmark / `aria-invalid` UX.
3074
+ *
3075
+ * Validation-only: an in-flight async transform does NOT clamp
3076
+ * `valid` to `false`. `valid` is the verdict on the last committed
3077
+ * value; `busy` is the union "work in flight" signal.
3078
+ */
3079
+ readonly valid: boolean;
3080
+ /**
3081
+ * `true` while an async `register` transform is in flight at this
3082
+ * path: a transform returned a thenable and the resolved value has
3083
+ * not yet committed to form state. Always `false` for a sync-only
3084
+ * chain, which reaches form state in the same tick with no deferral.
3085
+ * Containers roll it up as a disjunction (any descendant transforming).
3086
+ */
3087
+ readonly transforming: boolean;
3088
+ /**
3089
+ * `transforming || validating`: the "work is in flight at this path"
3090
+ * signal. Drives `aria-busy` through `displayState` on a
3091
+ * revealed field, and is the surface to bind for a busy indicator on a
3092
+ * field not yet revealed (where `displayState` stays idle by the
3093
+ * reveal gate). Containers roll it up as a disjunction.
3094
+ */
3095
+ readonly busy: boolean;
3096
+ /**
3097
+ * The `Error` from the most recent async transform that rejected at
3098
+ * this path, else `null`. A per-field normalization-failure channel
3099
+ * separate from validation `errors`: a transform that rejects (a
3100
+ * failed fetch, a parse error) surfaces here instead of crashing the
3101
+ * host app or logging. Cleared when a fresh transform starts or a
3102
+ * write supersedes it. Leaf-only: a container never rolls it up, and
3103
+ * reads `null`.
3104
+ */
3105
+ readonly transformError: Error | null;
3106
+ /**
3107
+ * The single display-state verdict at this path: `'idle'`,
3108
+ * `'pending'`, `'error'`, or `'success'`. The source of truth the
3109
+ * four `show*` booleans below derive from. Bind it directly when one
3110
+ * branch over the set reads cleaner than four flags:
3111
+ *
3112
+ * ```vue
3113
+ * <FieldStatusIcon :state="form.fields.email.displayState" />
3114
+ * ```
3115
+ *
3116
+ * Resolved by the display heuristic: gate closed → `'idle'`; a run
3117
+ * in flight → a delayed `'pending'`; an own-path error → `'error'`;
3118
+ * otherwise earned `valid` → `'success'`, else `'idle'`. The gate
3119
+ * opens after the first submit attempt OR once the field is edited
3120
+ * and left.
3121
+ *
3122
+ * Available on container paths too: `form.fields.users[0].displayState`
3123
+ * rolls up over the row's descendants.
3124
+ */
3125
+ readonly displayState: DisplayState;
3126
+ /**
3127
+ * `displayState === 'error'`. The centralised "render this field's
3128
+ * errors right now?" gate, so templates avoid re-spelling the
3129
+ * heuristic at every error site:
3130
+ *
3131
+ * ```vue
3132
+ * <span v-if="form.fields.email.showErrors">
3133
+ * {{ form.fields.email.firstError?.message }}
3134
+ * </span>
3135
+ * ```
3136
+ *
3137
+ * Kept plural to match `errors` / `firstError`. On container paths it
3138
+ * rolls up over descendants (any descendant resolving to `'error'`
3139
+ * flips the container on).
3140
+ */
3141
+ readonly showErrors: boolean;
3142
+ /**
3143
+ * `displayState === 'pending'`. A per-field validation run is in
3144
+ * flight at this path and the prior verdict is stale; drive a spinner
3145
+ * or a "Checking…" affordance.
3146
+ */
3147
+ readonly showPending: boolean;
3148
+ /**
3149
+ * `displayState === 'success'`. Validation has passed and the timing
3150
+ * gate has cleared a positive confirmation; drive the green-check
3151
+ * pattern.
3152
+ */
3153
+ readonly showSuccess: boolean;
3154
+ /**
3155
+ * `displayState === 'idle'`. Nothing to surface yet, either
3156
+ * pre-interaction or gate-open with no verdict worth showing. Read it
3157
+ * to suppress helper text the moment another signal takes over.
3158
+ */
3159
+ readonly showIdle: boolean;
3160
+ /**
3161
+ * The first `ValidationError` at this path in the deterministic
3162
+ * schema-declaration order, equivalent to `errors[0]` and exposed as
3163
+ * sugar for "show the highest-priority error message and ignore the
3164
+ * rest":
3165
+ *
3166
+ * ```vue
3167
+ * <span v-if="form.fields.email.showErrors">
3168
+ * {{ form.fields.email.firstError?.message }}
3169
+ * </span>
3170
+ * ```
3171
+ *
3172
+ * `undefined` when no errors exist, and independent of `showErrors`:
3173
+ * the data primitive is always available, and the heuristic decides
3174
+ * only when to render it.
3175
+ *
3176
+ * On container paths, the first error in the aggregated subtree
3177
+ * (descendants sorted by `pathOrdinals`). For the container's OWN
3178
+ * first error only, read `firstOwnError`.
3179
+ */
3180
+ readonly firstError: ValidationError | undefined;
3181
+ /**
3182
+ * The first `ValidationError` in `ownErrors` (this exact path's own
3183
+ * bucket), or `undefined` when that bucket is empty. Equivalent to
3184
+ * `ownErrors[0]`, the exact-path counterpart to `firstError`.
3185
+ *
3186
+ * On a leaf this equals `firstError`. On a container it is the
3187
+ * container's own cross-field error (a fieldset `.refine()`), never a
3188
+ * child's. `form.meta.firstOwnError` is the form-level banner: the
3189
+ * first root `[]` error from a root `.refine()` or a path-less
3190
+ * `setErrors`:
3191
+ *
3192
+ * ```vue
3193
+ * <p v-if="form.meta.firstOwnError" role="alert">
3194
+ * {{ form.meta.firstOwnError.message }}
3195
+ * </p>
3196
+ * ```
3197
+ */
3198
+ readonly firstOwnError: ValidationError | undefined;
3199
+ readonly path: ReadonlyArray<string | number>;
3200
+ /**
3201
+ * Stable, SSR-safe DOM id for this field, unique across every mount
3202
+ * on the page. Derived from the form's key and this path, folded with
3203
+ * the form's per-mount `instanceId` so two simultaneous mounts of the
3204
+ * same keyed form never collide. Bind it to wire a label and its
3205
+ * input without inventing your own id:
3206
+ *
3207
+ * ```vue
3208
+ * <label :for="form.fields.email.id">Email</label>
3209
+ * <input :id="form.fields.email.id" v-register="form.register('email')" />
3210
+ * ```
3211
+ *
3212
+ * Treat as identity, not state: stable for the path across the form's
3213
+ * lifetime, opaque, not meant to be parsed.
3214
+ */
3215
+ readonly id: string;
3216
+ /**
3217
+ * Satellite ids derived from {@link id} for the elements that
3218
+ * describe this field. Wire them to an error node and a description
3219
+ * node so assistive tech announces them with the input. The
3220
+ * `v-register` directive points `aria-describedby` at `errorId`
3221
+ * automatically while the field is in its error state; you render the
3222
+ * matching element and id it:
3223
+ *
3224
+ * ```vue
3225
+ * <input v-register="form.register('email')" />
3226
+ * <span :id="form.fields.email.aria.errorId" v-if="form.fields.email.showErrors">
3227
+ * {{ form.fields.email.firstError?.message }}
3228
+ * </span>
3229
+ * ```
3230
+ *
3231
+ * `descriptionId` is for opt-in help text; chain it into your own
3232
+ * `aria-describedby` when you render a persistent description element.
3233
+ */
3234
+ readonly aria: {
3235
+ readonly errorId: string;
3236
+ readonly descriptionId: string;
3237
+ };
3238
+ /**
3239
+ * Stable identity for this field as an element of its parent array,
3240
+ * suitable as a Vue `:key` when iterating array elements. An allocated
3241
+ * token (not derived from the element's value) that follows the
3242
+ * element across inserts, removals, moves, and swaps, so a row keeps
3243
+ * its component instance across a reorder. Empty for fields that are
3244
+ * not array elements. Treat as opaque identity, not state.
3245
+ */
3246
+ readonly key: string;
3247
+ readonly blank: boolean;
3248
+ /**
3249
+ * `true` when this field's form is frozen via `useForm({ disabled })`.
3250
+ * While disabled, value writes no-op and this field's `displayState`
3251
+ * is forced to `'idle'` so error, pending, and success signals stand
3252
+ * down. A form-level flag: every field of a disabled form reads
3253
+ * `true`, and the form root exposes it as `form.meta.disabled`. Bind
3254
+ * read-only or styling affordances off this.
3255
+ */
3256
+ readonly disabled: boolean;
3257
+ /**
3258
+ * Presentational label for this field. It resolves through the
3259
+ * shared cross-adapter field-meta store, written with
3260
+ * `schema.register(fieldMeta, {...})` on Zod 4 or the `withMeta()`
3261
+ * helper on either major, and falls back to a humanized form of the
3262
+ * path's last segment when nothing is registered. Always a string.
3263
+ *
3264
+ * ```ts
3265
+ * z.string().register(fieldMeta, { label: 'Reference' })
3266
+ * // template: <label>{{ form.fields.reference.label }}</label>
3267
+ * ```
3268
+ *
3269
+ * A numeric segment (an array index) collapses to the empty string,
3270
+ * so a consumer wanting "Item 3" substitutes their own format.
3271
+ */
3272
+ readonly label: string;
3273
+ /**
3274
+ * Helper-text description for this field. It reads the registered
3275
+ * `description` first, falls back to the schema's own
3276
+ * `.describe('...')` value (both Zod majors expose that as
3277
+ * `schema.description`), and is `undefined` when neither is set.
3278
+ *
3279
+ * Useful for `aria-describedby`-linked help text, and distinct from
3280
+ * `label`: a description is longer prose, a label a short
3281
+ * presentational noun.
3282
+ */
3283
+ readonly description: string | undefined;
3284
+ /**
3285
+ * Placeholder hint for input affordance. Reads from the
3286
+ * registered `placeholder`; `undefined` otherwise.
3287
+ */
3288
+ readonly placeholder: string | undefined;
3289
+ /**
3290
+ * The full registered metadata payload, frozen, or an empty object
3291
+ * when nothing is registered. It is the escape hatch for
3292
+ * consumer-augmented keys, declared through module augmentation on
3293
+ * `FieldMetaPayload`:
3294
+ *
3295
+ * ```ts
3296
+ * declare module 'attaform/zod' {
3297
+ * interface FieldMetaPayload { tooltip?: string }
3298
+ * }
3299
+ * // template: {{ form.fields.email.meta.tooltip }}
3300
+ * ```
3301
+ */
3302
+ readonly meta: Readonly<FieldMetaPayload>;
3303
+ };
3304
+ /** `true` only for bare `unknown`, the storage shape of a preprocess or
3305
+ * coerce leaf. `any` is excluded, since it absorbs every test. */
3306
+ type IsUnknown<T> = IsAny<T> extends true ? false : unknown extends T ? true : false;
3307
+ /**
3308
+ * Leaf-shape dispatch table for `LeafWalker`. Each entry maps a walker
3309
+ * kind to the leaf type that walker produces at a primitive, Date or
3310
+ * non-recursable position. The lookup `LeafSchemeFor<T>[Kind]` threads
3311
+ * `T` through the leaf type when the kind needs it, as `field` does for
3312
+ * `FieldState<T>`; a kind that does not depend on `T` ignores it, as
3313
+ * `errors` does in always producing
3314
+ * `readonly ValidationError[] | undefined`.
3315
+ *
3316
+ * Adding a walker is one entry here plus a one-line wrapper alias
3317
+ * (`type FooShape<T> = LeafWalker<T, 'foo'>`), because the topology is
3318
+ * shared and only the leaf changes.
3319
+ *
3320
+ * The `errors` entry threads `T` to preserve `| undefined` when the
3321
+ * value type itself includes undefined (DU variant-only fields whose
3322
+ * lifted shape resolves to `X | undefined`). A statically-known leaf
3323
+ * collapses to `readonly ValidationError[]`
3324
+ * with no undefined, while a dynamic-key boundary (an array index, a
3325
+ * record key) re-introduces `| undefined` through the structural
3326
+ * index-signature channels.
3327
+ *
3328
+ * A preprocess or coerce leaf (StorageShape `unknown`) is statically
3329
+ * known too, and the IsUnknown filter keeps it on the non-optional
3330
+ * branch rather than letting `undefined extends unknown` sweep it into
3331
+ * the dynamic arm.
3332
+ *
3333
+ * An implementation-detail surface: consumers reach for `FieldStateMap`
3334
+ * or `FormErrorsSurface` instead.
3335
+ */
3336
+ interface LeafSchemeFor<T> {
3337
+ field: FieldState<T>;
3338
+ errors: IsUnknown<T> extends true ? readonly ValidationError[] : undefined extends T ? readonly ValidationError[] | undefined : readonly ValidationError[];
3339
+ }
3340
+ /**
3341
+ * Generic walk that produces a proxy shape over `T` with leaves
3342
+ * dispatched through `LeafSchemeFor<T>[Kind]`. The walk topology is
3343
+ * identical across walker kinds and only the leaf type differs: an
3344
+ * object maps homomorphically, an object-union merges by KeyofUnion, an
3345
+ * array indexes, and a primitive or Date terminates.
3346
+ *
3347
+ * `FieldStateMapEntry` and `ErrorsProxyShape` share it rather than
3348
+ * walking the same shape twice per useForm return type, once for the
3349
+ * fields proxy and once for the errors proxy. Sharing lets the bundled
3350
+ * `.d.ts` carry one walker body plus per-kind one-line wrappers,
3351
+ * halving what these two proxies contribute to recursive depth at a
3352
+ * consumer call site.
3353
+ *
3354
+ * `StripOptional` controls whether optional modifiers on input
3355
+ * properties are stripped at every recursion level. `true`, the
3356
+ * default, matches the fields proxy, where every known leaf carries a
3357
+ * `FieldState` wrapper whatever the source `?` says. `false` matches
3358
+ * the errors proxy, whose shape stays structurally identical to the
3359
+ * input form, optional keys included.
3360
+ *
3361
+ * An implementation-detail surface: consumers reach for `FieldStateMap`
3362
+ * or `FormErrorsSurface` instead.
3363
+ */
3364
+ type LeafWalker<T, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean = true> = [T] extends [string | number | boolean | bigint | symbol | null | undefined | Date | File] ? LeafSchemeFor<T>[Kind] : [T] extends [ReadonlyArray<infer U>] ? {
3365
+ readonly [K: number]: LeafWalker<U, Kind, StripOptional>;
3366
+ } & ContainerSelfErrorsSlot<T, Kind> : [T] extends [object] ? string extends keyof T ? {
3367
+ readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
3368
+ } & ContainerSelfErrorsSlot<T, Kind> : [IsUnion<T>] extends [true] ? StripOptional extends true ? {
3369
+ readonly [K in KeyofUnion<T>]-?: DiscriminatedLeaf<T, K, Kind, StripOptional>;
3370
+ } & ContainerSelfErrorsSlot<T, Kind> : {
3371
+ readonly [K in KeyofUnion<T>]: DiscriminatedLeaf<T, K, Kind, StripOptional>;
3372
+ } & ContainerSelfErrorsSlot<T, Kind> : StripOptional extends true ? {
3373
+ readonly [K in keyof T]-?: LeafWalker<T[K], Kind, StripOptional>;
3374
+ } & ContainerSelfErrorsSlot<T, Kind> : {
3375
+ readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
3376
+ } & ContainerSelfErrorsSlot<T, Kind> : LeafSchemeFor<T>[Kind];
3377
+ /**
3378
+ * One key of a discriminated-union container in `LeafWalker`. A key
3379
+ * present and required in EVERY variant is universal, and its node is
3380
+ * always reachable. A variant-only or optional-in-some key is a dynamic
3381
+ * hop: its node is `undefined` while its variant is inactive, so it
3382
+ * carries NODE-optionality (`LeafWalker<...> | undefined`), not
3383
+ * VALUE-optionality (`LeafWalker<... | undefined>`).
3384
+ * `PresentValueOfUnion` strips the synthetic absent-variant `undefined`
3385
+ * so the present node resolves to the precise value type, while a
3386
+ * genuine `undefined` from an `optional` declaration survives.
3387
+ *
3388
+ * Universality is `[T] extends [Record<K, unknown>]`, true exactly when
3389
+ * the whole union satisfies "has K, required", which holds only when
3390
+ * every variant declares K as a required property.
3391
+ */
3392
+ type DiscriminatedLeaf<T, K extends PropertyKey, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean> = [T] extends [Record<K, unknown>] ? LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> : LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> | undefined;
3393
+ /**
3394
+ * Intersection augmenting every container in the `form.errors` walker
3395
+ * with a `''` slot. At a container of depth 1 or deeper it is the
3396
+ * container-self sentinel, home to cross-field refine errors and
3397
+ * server-side container marks; at the root it addresses the literal
3398
+ * empty-key field. Gated on `Kind extends 'errors'` so `form.values`
3399
+ * and `form.fields` surfaces stay untouched. Carve-out for schemas that
3400
+ * legitimately declare a `''` field: the declared field type wins; at
3401
+ * runtime the two collide harmlessly (errors at the literal leaf and
3402
+ * any container-self errors share the slot via array concat).
3403
+ *
3404
+ * Global (root) errors are NOT this slot: they live at the root `[]`
3405
+ * and are read via the `errors([])` call-form, never `errors['']`.
3406
+ */
3407
+ type ContainerSelfErrorsSlot<T, Kind> = Kind extends 'errors' ? '' extends keyof T ? unknown : {
3408
+ readonly ['']: readonly ValidationError[];
3409
+ } : unknown;
3410
+ /**
3411
+ * Recursive type behind `form.fields`. Leaf-aware branching: at
3412
+ * a primitive path (string, number, boolean, bigint, Date) the proxy
3413
+ * returns a `FieldState`; at a container path (object, array) it
3414
+ * descends without injecting leaf keys.
3415
+ *
3416
+ * Field-name collisions at depth 2+ resolve unambiguously: a schema
3417
+ * field literally named `dirty` at depth 2 is reachable as a
3418
+ * descent target (`form.fields.address.dirty` returns the
3419
+ * FieldState for `address.dirty`). Reading `dirty` AT the
3420
+ * leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's own
3421
+ * dirty boolean, because a path segment and a leaf prop occupy
3422
+ * different proxy depths.
3423
+ *
3424
+ * The runtime implementation queries `schema.isLeafAtPath(segments)`
3425
+ * at every step, and this type approximates that with
3426
+ * "T extends primitive". The two agree for typical schemas; an exotic
3427
+ * adapter-defined leaf kind, a custom `Date`-like, may need a runtime
3428
+ * check, and the runtime is authoritative.
3429
+ *
3430
+ * The mapped type strips optional flags (`-?:`) because the field-
3431
+ * state surface always exposes a record per known leaf, regardless
3432
+ * of whether the schema field is declared `.optional()`. An optional
3433
+ * schema means the VALUE can be undefined, which
3434
+ * `FieldState<string | undefined>` carries, while the wrapper itself
3435
+ * always exists. Without the strip, `form.fields.notes` on a
3436
+ * `notes?: string` would type as `FieldState<...> | undefined` and
3437
+ * force consumers to optional-chain through every reactive read.
3438
+ *
3439
+ * For discriminated-union containers the object branch uses
3440
+ * `[T] extends [object]` (non-distributive) plus
3441
+ * `KeyofUnion` / `ValueOfUnion` to merge variant key sets, so a
3442
+ * refrigerated-only `form.fields.cargo.tempMinC` is reachable whatever
3443
+ * the active variant, typed as `FieldState<number | undefined>`. That
3444
+ * matches the runtime's stub `FieldState` for an inactive-variant
3445
+ * path.
3446
+ */
3447
+ type FieldStateMapEntry<T> = LeafWalker<T, 'field'>;
3448
+ /**
3449
+ * Result of the `form.fields(path)` string call form. A path the schema
3450
+ * declares resolves to its `FieldState`: a leaf's value type, or a
3451
+ * container's rolled-up aggregate, where every FieldState property
3452
+ * exists whatever the value type. A path the schema lacks resolves to
3453
+ * `undefined`, because a typo is not a field and the runtime hands back
3454
+ * `undefined` rather than a phantom stub. A non-literal `string` could
3455
+ * be either, so it widens to `FieldState<unknown> | undefined`.
3456
+ *
3457
+ * Named rather than inlined into the `FieldStateMap` call signature so
3458
+ * the conditional is one cached type. That keeps two
3459
+ * structurally-identical `FieldStateMap` instantiations, such as the
3460
+ * unified `attaform/zod` return and `UseFormReturnV4`, relatable rather
3461
+ * than collapsing into a nominal "two different types with this name"
3462
+ * mismatch.
3463
+ */
3464
+ type FieldCallResult<Form, P extends string> = [P] extends [FlatPath<Form>] ? FieldState<NestedType<Form, P>> : string extends P ? FieldState<unknown> | undefined : undefined;
3465
+ /**
3466
+ * Type of `form.fields`, a leaf-aware drillable callable Proxy. At a
3467
+ * leaf path it resolves to a `FieldState<Value>`; at a container path
3468
+ * it returns a sub-proxy you can keep drilling.
3469
+ *
3470
+ * Augmented with the callable signatures so dot-access and function-
3471
+ * call coexist on the same identifier:
3472
+ *
3473
+ * ```ts
3474
+ * form.fields.email.value // string (leaf-prop on FieldState)
3475
+ * form.fields('email').value // function-call (dynamic / programmatic)
3476
+ * form.fields(['users', 0, 'name']) // path-array form
3477
+ * form.fields() // root proxy
3478
+ * ```
3479
+ *
3480
+ * Single-bracket dotted access (`form.fields['address.city']`) is
3481
+ * deliberately NOT supported, because JS object semantics read the
3482
+ * dotted string as a single key. Use chained dot or bracket access, or
3483
+ * the callable form.
3484
+ */
3485
+ type FieldStateMap<Form extends GenericForm> = LeafWalker<Form, 'field'> & {
3486
+ /**
3487
+ * String-path form (dynamic / programmatic). See {@link FieldCallResult}:
3488
+ * a path the schema declares resolves to its precise `FieldState`, a
3489
+ * path it lacks to `undefined`, and a non-literal `string` to
3490
+ * `FieldState<unknown> | undefined`.
3491
+ */
3492
+ <P extends string>(path: P): FieldCallResult<Form, P>;
3493
+ /**
3494
+ * Tuple-segment form. The segment-array spelling of the string
3495
+ * call form above, for a path built from variables. The two resolve
3496
+ * to the same proxy, so `form.fields(['users', i, 'email'])` and
3497
+ * `form.fields(\`users.${i}.email\`)` are interchangeable.
3498
+ *
3499
+ * Resolves to that path's `FieldState`, which at a container is its
3500
+ * rolled-up aggregate (`displayState`, `valid`, `dirty`, ... all
3501
+ * exist regardless of the value type), exactly as the string form
3502
+ * does. Note this is the CALL form: it hands back the path's state,
3503
+ * not a drillable sub-proxy. To walk into a container's children use
3504
+ * dot/bracket access (`form.fields.users[i].email`), which is what
3505
+ * descends.
3506
+ */
3507
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): FieldState<NestedType<Form, JoinSegments<S>>>;
3508
+ /**
3509
+ * Dynamic-array fallback for a caller passing `Path`-typed runtime
3510
+ * segments, as forwarding `RegisterValue.segments` to resolve a field
3511
+ * view does. The path may not resolve, so the result widens with
3512
+ * `| undefined`; cast when the value type is known.
3513
+ */
3514
+ (segments: ReadonlyArray<string | number>): FieldState<unknown> | undefined;
3515
+ /**
3516
+ * The no-arg call returns the root FieldState, the same as
3517
+ * `form.fields([])`, aggregating over the whole form as one
3518
+ * conjunction across every active-variant leaf.
3519
+ */
3520
+ (): FieldState<Form>;
3521
+ };
3522
+ /**
3523
+ * Untyped error map keyed by dotted-string path: the same data
3524
+ * `form.errors` exposes, as a plain record. The case for it is routing
3525
+ * API errors that may land on paths the form's TypeScript type does not
3526
+ * know about.
3527
+ */
3528
+ type FormErrorRecord = Record<string, ValidationError[]>;
3529
+ /**
3530
+ * Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
3531
+ * statically-known leaf the proxy resolves to `readonly ValidationError[]`
3532
+ * (empty array when no errors land); at dynamic boundaries (array
3533
+ * indices, record keys, DU variant-only fields) it resolves to
3534
+ * `readonly ValidationError[] | undefined`. At a container path it
3535
+ * returns a sub-proxy you can keep drilling.
3536
+ *
3537
+ * Dot/bracket access mirrors the schema shape:
3538
+ *
3539
+ * ```ts
3540
+ * form.errors.email // readonly ValidationError[] (static leaf)
3541
+ * form.errors.user.profile.email // readonly ValidationError[] (chained static leaves)
3542
+ * form.errors.posts[3]?.title // readonly ValidationError[] | undefined (past array boundary)
3543
+ * form.errors.address // sub-proxy (a container, so descend)
3544
+ * ```
3545
+ *
3546
+ * Callable form for dynamic / programmatic paths:
3547
+ *
3548
+ * ```ts
3549
+ * form.errors('user.profile.email') // dotted-string
3550
+ * form.errors(['user', 'profile', 'email']) // path-array
3551
+ * form.errors() // root proxy
3552
+ * ```
3553
+ *
3554
+ * Single-bracket dotted access (`form.errors['user.profile.email']`) is
3555
+ * intentionally NOT supported: JS object semantics read the dotted string
3556
+ * as one key, which lands on a path that does not exist. Use chained
3557
+ * dot/bracket access, or the callable form.
3558
+ *
3559
+ * The underlying shape mirrors the schema. A statically-known primitive
3560
+ * leaf exposes `readonly ValidationError[]`, always an array and empty
3561
+ * when no errors land; a leaf whose value type itself includes `undefined`
3562
+ * (a DU variant-only field) keeps the `| undefined` branch. A container
3563
+ * exposes a sub-shape to keep drilling, and an array exposes
3564
+ * numeric-indexed sub-shapes where reading an index introduces
3565
+ * `| undefined` through noUncheckedIndexedAccess. The callable signatures
3566
+ * are intersected in, so dot access and a function call coexist on one
3567
+ * identifier.
3568
+ */
3569
+ type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
3570
+ (path: string): readonly ValidationError[];
3571
+ /**
3572
+ * Tuple-segment form. Validated against `FlatPath<Form>` so literal
3573
+ * tuples that don't resolve to a known path fail at the call site.
3574
+ * Dynamic `Path`-typed inputs hit the untyped fallback overload below.
3575
+ */
3576
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): readonly ValidationError[];
3577
+ (segments: ReadonlyArray<string | number>): readonly ValidationError[];
3578
+ /**
3579
+ * No-arg call returns the whole-form error aggregate, the same as
3580
+ * `form.meta.errors` and `form.errors([])`: every field error plus
3581
+ * the root bucket. `form.errors(path)` is uniform at every depth,
3582
+ * including `[]`, so the root is no exception. For the root bucket
3583
+ * alone (a root `.refine()`, a path-less `setErrors`), read
3584
+ * `form.meta.ownErrors`. Always a readonly array; empty when the form
3585
+ * has no errors.
3586
+ */
3587
+ (): readonly ValidationError[];
3588
+ };
3589
+ /**
3590
+ * Implementation-detail walker backing `form.errors` typed proxy.
3591
+ * A thin alias over `LeafWalker<T, 'errors', false>`, whose topology is
3592
+ * defined once at `LeafWalker` and parameterized through
3593
+ * `LeafSchemeFor`. The `false` preserves optional-key modifiers, since
3594
+ * the errors proxy mirrors the input shape including `?`, where the
3595
+ * fields proxy alias strips them under the default `true`.
3596
+ *
3597
+ * Exported so the bundled `.d.ts` references a single alias body
3598
+ * rather than re-emitting the full union-aware recursion at every
3599
+ * consumer call site that types `form.errors`. Multiple useForm
3600
+ * instances in one scope otherwise compound this into TS2589
3601
+ * territory. Consumers should reach for `FormErrorsSurface` instead.
3602
+ */
3603
+ type ErrorsProxyShape<T> = LeafWalker<T, 'errors', false>;
3604
+ /**
3605
+ * Type of `form.values`: a drillable readonly callable proxy. Unlike
3606
+ * `form.errors` and `form.fields`, containers here are USEFUL
3607
+ * terminals, because every container in `values` carries real data
3608
+ * where in errors and fields it carries a derivation:
3609
+ * `form.values.address` returns the actual `{ city, ... }` subtree and
3610
+ * keeps drilling.
3611
+ *
3612
+ * ```ts
3613
+ * form.values.email // string (the value)
3614
+ * form.values.address // { city, ... } an object, drillable
3615
+ * form.values.address.city // string (chained descent)
3616
+ * form.values('address.city') // function-call (dynamic / programmatic)
3617
+ * form.values(['address', 'city']) // path-array form
3618
+ * form.values() // the whole form value (root)
3619
+ * ```
3620
+ *
3621
+ * The two shapes answer different questions. Dot access is the
3622
+ * REACTIVE view: per-key tracking, no copying, always the live value.
3623
+ * The call form returns a SNAPSHOT: a detached plain object that keeps
3624
+ * what it held at capture time, so it is the shape to hand an async
3625
+ * call, a serialiser, or a diff.
3626
+ *
3627
+ * ```ts
3628
+ * const before = form.values() // detached; survives later writes
3629
+ * await api.save(before) // cannot mutate underneath the call
3630
+ * form.values.email // reactive read; use this in computed
3631
+ * ```
3632
+ *
3633
+ * Prefer dot access inside a `computed`, `watchEffect`, or template:
3634
+ * it tracks the one key you read, where the call form depends on the
3635
+ * whole form and re-runs the consumer on any change.
3636
+ *
3637
+ * The copy is deep across plain objects and arrays. Non-plain
3638
+ * instances (Map, Set, File, Date) are shared by reference, matching
3639
+ * `JSON.stringify` behaviour; `structuredClone(form.values())` gives a
3640
+ * fully detached copy when one is needed.
3641
+ *
3642
+ * Single-bracket dotted access (`form.values['address.city']`) is
3643
+ * deliberately NOT supported, because JS object semantics read the
3644
+ * dotted string as a single key. Use chained dot or bracket access, or
3645
+ * the callable form.
3646
+ *
3647
+ * The chained shape applies the discriminated-union lift via
3648
+ * `LiftedValueShape<F>` so per-variant keys are reachable without
3649
+ * narrowing first: `form.values.cargo.permitNumber` types as
3650
+ * `string | undefined` whatever the active cargo variant, matching the
3651
+ * runtime, where plain JS object access on a missing variant key
3652
+ * returns `undefined`. The strict-variant shape is still required on
3653
+ * the WRITE side, where `setValue` and `defaultValues` use the unlifted
3654
+ * `WriteShape` so a partial or cross-variant object cannot reach the
3655
+ * form.
3656
+ */
3657
+ type ValuesSurface<F> = Readonly<LiftedValueShape<F>> & {
3658
+ (path: string): unknown;
3659
+ (path: ReadonlyArray<string | number>): unknown;
3660
+ (): Readonly<F>;
3661
+ };
3662
+ /**
3663
+ * Reactive form-level flags, counters, and aggregates returned as
3664
+ * `form.meta`. "Meta" because every other surface (`form.values`,
3665
+ * `form.errors`, `form.fields`) is data-shaped, where `form.meta`
3666
+ * holds facts derived ABOUT the form.
3667
+ *
3668
+ * Read its fields directly, with no `.value`: they auto-unwrap inside
3669
+ * the reactive object:
3670
+ *
3671
+ * ```vue
3672
+ * <button :disabled="form.meta.submitting">Save</button>
3673
+ * ```
3674
+ *
3675
+ * Watch a single field via the getter form:
3676
+ *
3677
+ * ```ts
3678
+ * watch(() => form.meta.submitting, (value) => …)
3679
+ * ```
3680
+ *
3681
+ * Per-field state (touched, dirty, errors) lives behind
3682
+ * `form.fields.<path>`; this is the aggregate view across the
3683
+ * whole form.
3684
+ *
3685
+ * Read-only at runtime: an assignment is refused with a dev-console
3686
+ * warning and never lands, because Attaform does not throw from a read
3687
+ * surface. Destructuring snapshots the current values, so reach for
3688
+ * `toRefs()` when you need reactive handles to individual fields.
3689
+ */
3690
+ type FormMeta<F = unknown> = FieldState<F> & {
3691
+ /**
3692
+ * `true` while a `handleSubmit`-produced submit handler is running.
3693
+ * Covers both the validation phase and your async submit callback.
3694
+ * Useful for disabling the submit button.
3695
+ */
3696
+ readonly submitting: boolean;
3697
+ /**
3698
+ * How many times the submit handler has been invoked, regardless of
3699
+ * outcome (validation failure, callback success, callback throw).
3700
+ * Useful for "show errors after first submit attempt" UX.
3701
+ */
3702
+ readonly submissionAttempts: number;
3703
+ /**
3704
+ * How many times wizard navigation (`wizard.next`, `wizard.back`,
3705
+ * `wizard.goTo`) has actually departed this form. Bumped on real
3706
+ * departures only: no-ops like `back()` from the first step, a
3707
+ * same-key `goTo`, or a `next()` blocked by failed activation leave
3708
+ * the counter at its prior value.
3709
+ *
3710
+ * A pure introspection counter: it serves "this form has been visited
3711
+ * and left" UX (analytics, prior-step badges) and drives no part of
3712
+ * the display heuristic. Reveal-on-submit runs entirely through
3713
+ * `submissionAttempts`, which `wizard.handleSubmit` bumps on every
3714
+ * form, since it always validates the whole step list.
3715
+ *
3716
+ * `submissionAttempts` counts `handleSubmit` passes alone, so a wizard
3717
+ * departure and a form submission stay separately introspectable.
3718
+ * `form.validate()` is a read-only inspection primitive and bumps no
3719
+ * counter at all.
3720
+ *
3721
+ * Cleared by `form.reset()`.
3722
+ */
3723
+ readonly departAttempts: number;
3724
+ /**
3725
+ * The error thrown or rejected by the most recent submit callback (or
3726
+ * its `onError` handler), coerced to a real `Error` (a non-`Error`
3727
+ * throw keeps its origin on `.cause`). Cleared to `null` at the start
3728
+ * of each new submission attempt; stays `null` on success.
3729
+ *
3730
+ * The submit handler does NOT re-throw: its returned promise always
3731
+ * resolves, so binding it to `@submit` never manufactures a window
3732
+ * unhandledrejection. This is the channel for an UNEXPECTED submit
3733
+ * failure, a thrown exception or a rejected promise, read the same way
3734
+ * in a template and after an imperative `await submit()`. An EXPECTED
3735
+ * rejection handled the documented way (`setErrors(...)` then
3736
+ * `return`, no throw) surfaces through the error store and `onError`
3737
+ * instead, leaving `submitError` null. Like `hydrateError` it stays
3738
+ * distinct from the curated user-error store, so render it where you
3739
+ * choose:
3740
+ *
3741
+ * ```vue
3742
+ * <p v-if="form.meta.submitError">{{ form.meta.submitError.message }}</p>
3743
+ * ```
3744
+ */
3745
+ readonly submitError: Error | null;
3746
+ /**
3747
+ * Scalar mirror of `meta.errors.length`. Read it from templates and
3748
+ * `watch()` without indexing the underlying array.
3749
+ *
3750
+ * It tracks `errors.length` exactly, through the same computed graph,
3751
+ * so a `watch(form.meta.errorCount, ...)` fires exactly when the
3752
+ * aggregate error count changes.
3753
+ */
3754
+ readonly errorCount: number;
3755
+ /**
3756
+ * `true` once a `handleSubmit` callback has resolved without throwing
3757
+ * AND left no errors behind. It is independent of
3758
+ * `submissionAttempts`: a failed submit (a validation failure, a
3759
+ * callback rejection, or a callback calling `setErrors` and returning)
3760
+ * increments attempts and leaves `submitted` false. A template reads
3761
+ * it as "the form has been
3762
+ * submitted successfully at least once."
3763
+ *
3764
+ * The error check is scoped to the user-error layer (`setErrors` /
3765
+ * `clearErrors`): the documented server-rejection pattern
3766
+ * (`setErrors(response.errors); return`) is a failed submit, so it does
3767
+ * not flip `submitted`. A background async refinement that writes a
3768
+ * schema error mid-submit does not retroactively fail it.
3769
+ *
3770
+ * Cleared by `form.reset()` alongside `submissionAttempts` and
3771
+ * `submitError`. For "the user has attempted a submit," read
3772
+ * `submissionAttempts > 0` directly.
3773
+ */
3774
+ readonly submitted: boolean;
3775
+ /**
3776
+ * Per-`useForm()`-call identity. Stable for the lifetime of one
3777
+ * `useForm()` call; new on every fresh mount. Orthogonal to
3778
+ * `form.key`: the key identifies a SHARED FormStore (so two
3779
+ * `useForm({ key: 'signup' })` calls return the same store and the
3780
+ * same key), while `instanceId` identifies THIS call site, which is
3781
+ * what disambiguates two forms sharing a key, a sidebar and a main
3782
+ * view rendering the same form.
3783
+ *
3784
+ * The format is opaque, derived from Vue 3.5+ `useId()`. Treat it as
3785
+ * identity rather than state: do not parse it, compare it ordinally,
3786
+ * or persist it.
3787
+ *
3788
+ * Common patterns:
3789
+ *
3790
+ * - **Devtools panels** disambiguating shared-key form mounts.
3791
+ * - **Telemetry / logging hooks** tagging events with which mount
3792
+ * triggered them.
3793
+ * - **E2E test selectors** stamping `data-form-id={form.meta.instanceId}`
3794
+ * onto a wrapper to assert which form was focused.
3795
+ * - **Vue `:key`** for a keyed list of dynamically-rendered forms, a
3796
+ * drag-reorder say, since identity is stable per useForm() call.
3797
+ */
3798
+ readonly instanceId: string;
3799
+ };
3800
+ /**
3801
+ * Read-only view returned by `form.blankPaths.value`. Exposes lookup
3802
+ * (`.has`), aggregate (`.size`), and iteration over the form's
3803
+ * blank-marked paths.
3804
+ *
3805
+ * `.has(input)` and the iterator yield consistent results across both
3806
+ * input conventions the library accepts everywhere a path is named:
3807
+ *
3808
+ * - **Dotted string**: `'user.email'`, matching what `register('user.email')`
3809
+ * or `setValue('items.0.sku', …)` accept. Convenient when no segment
3810
+ * contains a literal dot.
3811
+ * - **Array form**: `['user', 'email']`, mirroring `register(['user', 'email'])`.
3812
+ * Required when a single segment contains literal dots, so
3813
+ * `['address.primary']` for a top-level key named `address.primary`,
3814
+ * which the dotted form would parse as two segments.
3815
+ *
3816
+ * Iteration yields `Path` arrays, so a consumer building debug UI or
3817
+ * persisting the set never has to guess whether a dot in a segment is a
3818
+ * separator or part of the name.
3819
+ *
3820
+ * Mutating the view does nothing. Writes go through
3821
+ * `setValue(path, unset)`, `markBlank()` on a register binding, or the
3822
+ * directive's input listener on a numeric clear.
3823
+ */
3824
+ interface BlankPathsView {
3825
+ /** Number of blank-marked paths. */
3826
+ readonly size: number;
3827
+ /**
3828
+ * `true` when the path is in the blank set. Accepts dotted-string
3829
+ * form (parsed by [[parseDottedPath]]) or the array form.
3830
+ */
3831
+ has(input: string | Path): boolean;
3832
+ /** Snapshot of all blank-marked paths as segment arrays. */
3833
+ values(): readonly Path[];
3834
+ /** Iterates the blank-marked paths as segment arrays. */
3835
+ [Symbol.iterator](): IterableIterator<Path>;
3836
+ }
3837
+ /**
3838
+ * The no-arg `form.record()` call form, present only when the form root
3839
+ * is itself an open record (`z.record(K, V)`). `string extends keyof
3840
+ * Form` is the open-keyset probe: true for `Record<string, V>`, false
3841
+ * for a fixed `z.object` shape. On a fixed object this resolves to
3842
+ * `unknown`, which contributes nothing to the intersection in `record`
3843
+ * below, so the no-arg call form simply does not exist there (and
3844
+ * `form.record()` stays a compile error, as it should when the root has
3845
+ * a closed key set). On a record root it mirrors the path-addressed
3846
+ * `record(path)` overload: one `FieldState` per entry, keyed by the
3847
+ * record's own runtime keys.
3848
+ */
3849
+ type RootRecordView<Form> = string extends keyof Form ? Form extends Record<string, infer RootValue> ? () => Readonly<Record<string, FieldState<RootValue>>> : unknown : unknown;
3850
+ /**
3851
+ * The object returned by `useForm`. Holds every reactive ref, write
3852
+ * helper, and lifecycle method bound to one form.
3853
+ *
3854
+ * ```ts
3855
+ * const form = useForm({ schema })
3856
+ * form.register('email') // bind to <input v-register>
3857
+ * form.values.email // current value (proxy, no .value)
3858
+ * form.fields.email.dirty // per-field flags
3859
+ * form.errors.email // readonly ValidationError[]
3860
+ * form.setValue('email', 'a@b.c')
3861
+ * form.handleSubmit(onSubmit) // returns a submit handler
3862
+ * form.meta.submitting // form-level reactive flag
3863
+ * ```
3864
+ *
3865
+ * Three generic slots split the write view, parse view, and read view:
3866
+ *
3867
+ * - `Form` is the **input / write shape** (`z.input<Schema>`), used by
3868
+ * `setValue`, `defaultValues` and `register`'s write side. Loose: a
3869
+ * preprocess path accepts `unknown` at the write boundary, and a
3870
+ * defaulted field accepts its inner type optionally.
3871
+ *
3872
+ * - `GetValueFormType` is the **output / parsed shape**
3873
+ * (`z.output<Schema>`), used by `handleSubmit`'s `onSubmit` callback
3874
+ * and `form.parse()`'s success payload: the shape after refinements
3875
+ * have fired and transforms have run.
3876
+ *
3877
+ * - `ReadForm` is the **read / storage shape**, used by `values`,
3878
+ * `fields`, `register`'s read side and `toRef`. Precise per key: at a
3879
+ * write-boundary wrapper (`default`, `prefault`, `catch`, `readonly`,
3880
+ * `preprocess`) the value is `z.output<Inner>`, because the default
3881
+ * has fired and preprocess has normalized; at a transform or pipe it
3882
+ * stays `z.input<Inner>`, because transforms are deferred until
3883
+ * parse. A schema-agnostic call site defaults it to `Form`.
3884
+ *
3885
+ * For schemas without write-boundary wrappers or transforms the three
3886
+ * shapes coincide.
3887
+ */
3888
+ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form, K extends FormKey = FormKey> = {
3889
+ /**
3890
+ * Wraps your submit logic with validation and error routing.
3891
+ *
3892
+ * ```ts
3893
+ * const onSubmit = form.handleSubmit(
3894
+ * (data) => api.signup(data),
3895
+ * (errors) => console.log(errors),
3896
+ * )
3897
+ * // Bind the returned handler: <form @submit="onSubmit">
3898
+ * ```
3899
+ *
3900
+ * `data` is the strictly-typed parsed value: refinements have fired
3901
+ * and `.transform()`s have run, so the payload matches
3902
+ * `z.output<Schema>`. Where the input type differs from the output
3903
+ * type, as in `z.string().transform(v => v.length > 10)`, `data` is
3904
+ * the output shape while `form.values` stays the input shape.
3905
+ */
3906
+ handleSubmit: HandleSubmit<GetValueFormType>;
3907
+ /**
3908
+ * Reactive readonly proxy over the form's storage value, read
3909
+ * identically in script and template: no `.value`, no auto-unwrap
3910
+ * rules, the Pinia setup-store pattern.
3911
+ *
3912
+ * ```vue
3913
+ * <script setup>
3914
+ * const form = useForm({ schema, key: 'login' })
3915
+ * </script>
3916
+ *
3917
+ * <template>
3918
+ * <p>{{ form.values.email }}</p>
3919
+ * <p>{{ form.values.address.city }}</p>
3920
+ * </template>
3921
+ * ```
3922
+ *
3923
+ * Writes are blocked at the proxy boundary: go through `setValue`,
3924
+ * the directive, or a field-array helper. The slim-primitive write
3925
+ * gate is the only path into storage.
3926
+ *
3927
+ * Reads reflect what's storable: enum-typed slots widen to their
3928
+ * primitive supertype (`string`), so refinement-invalid but
3929
+ * structurally-valid values are visible. Storage holds the
3930
+ * `z.input<Schema>` shape and `.transform()`s have NOT run, so on a
3931
+ * `z.string().transform(v => v.length > 10)` the value reads as
3932
+ * `string`, not `boolean`. Reach for `handleSubmit` or `form.parse()`
3933
+ * when you need the post-transform output shape.
3934
+ */
3935
+ values: ValuesSurface<WriteShape<ReadForm>>;
3936
+ /**
3937
+ * Reactive per-field state proxy, a Pinia-style nested object: read
3938
+ * leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
3939
+ * `focused`, `blank`) directly off the field's path:
3940
+ *
3941
+ * ```vue
3942
+ * <p v-if="form.fields.email.touched && form.fields.email.errors.length">
3943
+ * {{ form.fields.email.errors[0].message }}
3944
+ * </p>
3945
+ * <p>City dirty? {{ form.fields.address.city.dirty }}</p>
3946
+ * ```
3947
+ *
3948
+ * The same proxy descends at every level: `address` reads the
3949
+ * FieldState for the address object, and `address.city` descends into
3950
+ * the nested leaf.
3951
+ *
3952
+ * Leaf values follow the slim WriteShape contract: enum-typed leaves
3953
+ * widen to their primitive supertype, and the leaf value reflects
3954
+ * the `z.input<Schema>` shape (transforms deferred until parse).
3955
+ * The errors array, dirty flag, focus state, etc. are unaffected.
3956
+ *
3957
+ * Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
3958
+ * `errors`, `blank`, `focused`, `blurred`, `value`,
3959
+ * `original`, `pristine`, `connected`, `updatedAt`, `path`) win
3960
+ * over schema field names. Top-level fields are NOT shadowed.
3961
+ * Document edge case; rename the offending schema field if the
3962
+ * collision matters.
3963
+ */
3964
+ fields: FieldStateMap<WriteShape<ReadForm>>;
3965
+ /**
3966
+ * Write to the form programmatically. Two forms:
3967
+ *
3968
+ * - `setValue(value)` replaces the whole form.
3969
+ * - `setValue(path, value)` writes at one path.
3970
+ *
3971
+ * Either takes a callback in place of `value` to derive the next
3972
+ * value from the previous one:
3973
+ *
3974
+ * ```ts
3975
+ * form.setValue('count', (prev) => prev + 1)
3976
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
3977
+ * ```
3978
+ *
3979
+ * Returns `true` when the write is accepted. A `false` means the
3980
+ * value did not match the slot's expected type, as writing a number
3981
+ * to a string field does not, and form state is unchanged. A
3982
+ * refinement-level mismatch (an out-of-enum value, a failing format
3983
+ * check) DOES succeed and surfaces as a field error instead.
3984
+ */
3985
+ setValue: {
3986
+ /**
3987
+ * Replace the whole form. Pass a value or a callback receiving
3988
+ * the previous form.
3989
+ *
3990
+ * ```ts
3991
+ * form.setValue({ name: 'Ada', email: 'a@b.c' })
3992
+ * form.setValue((prev) => ({ ...prev, name: 'Ada' }))
3993
+ * ```
3994
+ *
3995
+ * Returns `true` when the write was accepted, `false` when the
3996
+ * value didn't match the expected shape (e.g. wrong primitive
3997
+ * type at a leaf). Refinement-level mismatches (out-of-enum
3998
+ * values, failing format checks, etc.) succeed and surface as
3999
+ * field errors instead.
4000
+ */
4001
+ <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
4002
+ /**
4003
+ * Write at a specific path. Pass a value or a callback receiving
4004
+ * the previous value at that path.
4005
+ *
4006
+ * ```ts
4007
+ * form.setValue('email', 'a@b.c')
4008
+ * form.setValue('count', (prev) => prev + 1)
4009
+ * form.setValue('income', unset) // numeric leaf marked displayed-empty
4010
+ * ```
4011
+ *
4012
+ * Returns `true` when the write was accepted, `false` when the
4013
+ * value didn't match the slot's expected primitive type.
4014
+ * Refinement-level mismatches succeed and surface as field
4015
+ * errors. Pass the `unset` symbol at any primitive leaf to mark
4016
+ * it blank (storage holds the slim default; UI displays
4017
+ * empty; submit raises "No value supplied" for required schemas).
4018
+ */
4019
+ <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
4020
+ /**
4021
+ * Tuple-segment form, equivalent to the dotted-string overload and
4022
+ * the one to reach for when a path is built from variables or
4023
+ * arrays: `form.setValue([prefix, 'line1'], 'value')`. The resolved
4024
+ * leaf type is exact, matching the dotted-string form.
4025
+ */
4026
+ <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>), value: Value): boolean;
4027
+ };
4028
+ /**
4029
+ * Reactive validation status. Re-runs whenever the form (or the
4030
+ * subtree at `path`) mutates. The returned ref carries a `pending`
4031
+ * flag, so gate on `!status.value.pending` before reading `success`
4032
+ * or `errors`.
4033
+ *
4034
+ * ```ts
4035
+ * const status = form.validate()
4036
+ * watchEffect(() => {
4037
+ * if (status.value.pending) return
4038
+ * if (!status.value.success) console.log(status.value.errors)
4039
+ * })
4040
+ * ```
4041
+ *
4042
+ * A stale in-flight run is dropped automatically: the ref settles
4043
+ * only to results from the most recent call.
4044
+ */
4045
+ validate: (path?: FlatPath<Form>) => Readonly<Ref<ReactiveValidationStatus<Form>>>;
4046
+ /**
4047
+ * Resolve once every in-flight async `register({ transforms })` run
4048
+ * has settled, either globally or, with `path`, at or under it.
4049
+ * Resolve-never-reject: a transform that throws still settles the
4050
+ * field (its failure lands on `field.transformError`), so the returned
4051
+ * promise always resolves.
4052
+ *
4053
+ * `handleSubmit` awaits this internally before parsing, so a submit
4054
+ * fired the instant after an async transform still validates the
4055
+ * resolved value. Reach for it directly when you need the same
4056
+ * guarantee outside submit, before reading `form.values` in an
4057
+ * imperative flow or a test:
4058
+ *
4059
+ * ```ts
4060
+ * input.value = ' a@b.com '
4061
+ * await form.settleTransforms('email')
4062
+ * // form.values.email is now the normalized value
4063
+ * ```
4064
+ */
4065
+ settleTransforms: (path?: FlatPath<Form>) => Promise<void>;
4066
+ /**
4067
+ * Imperative one-shot parse. It runs the full pipeline, refinements,
4068
+ * `.transform()`s and blank-required composition, against the current
4069
+ * form snapshot and RETAINS the parsed data.
4070
+ *
4071
+ * Storage holds the honest input view: the values you wrote, with
4072
+ * preprocess normalization applied and `.transform()` deferred. Where
4073
+ * the input type differs from the output type, as on a
4074
+ * `z.string().transform(v => v.length > 10)`, `form.values.X` is the
4075
+ * input shape and `(await form.parse()).data?.X` the output shape.
4076
+ *
4077
+ * ```ts
4078
+ * const result = await form.parse()
4079
+ * if (result.success) {
4080
+ * // result.data matches z.output<typeof schema>
4081
+ * } else {
4082
+ * // result.errors is the validation failure list
4083
+ * }
4084
+ * ```
4085
+ *
4086
+ * By default the call is a PURE read: nothing is written to
4087
+ * `form.errors`, and in-flight per-field validation runs are left
4088
+ * alone. Pass `{ commit: true }` to make the run authoritative: the
4089
+ * verdict is committed to the error store at the parsed scope and any
4090
+ * in-flight per-field runs are cancelled first, mirroring
4091
+ * `handleSubmit`, so `await form.parse('email', { commit: true })`
4092
+ * lands a deterministic view of `form.errors.email`:
4093
+ *
4094
+ * ```ts
4095
+ * const result = await form.parse({ commit: true })
4096
+ * if (!result.success) showErrors(result.errors)
4097
+ * ```
4098
+ *
4099
+ * Always async, and there is no synchronous variant by design: a
4100
+ * schema can carry async refinements or transforms, so a sync parse
4101
+ * would silently miss them the moment one is added. One always-
4102
+ * awaited `parse` closes that category of bug entirely. The returned
4103
+ * promise never rejects, since a thrown adapter lands as a
4104
+ * `success: false` response. Pass a path to parse a subtree only;
4105
+ * `meta.validating` flips `true` while the promise is in flight.
4106
+ */
4107
+ parse: {
4108
+ (path?: FlatPath<Form>, options?: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
4109
+ (options: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
4110
+ };
4111
+ /**
4112
+ * Bind a path to a native input via `v-register`. Returns a
4113
+ * `RegisterValue` carrying the live ref and event handlers the
4114
+ * directive needs.
4115
+ *
4116
+ * ```vue
4117
+ * <input v-register="form.register('email')" />
4118
+ * <input v-register="form.register('username', { transforms: [trim] })" />
4119
+ * ```
4120
+ *
4121
+ * It also accepts a segment-array form for a caller building paths
4122
+ * dynamically, above all inside a `v-for` over a prefix variable,
4123
+ * where dotted-string concatenation widens the prefix's literal union
4124
+ * to plain `string`:
4125
+ *
4126
+ * ```vue
4127
+ * <fieldset v-for="block in [{ prefix: 'pickup' }, { prefix: 'delivery' }] as const">
4128
+ * <input v-register="form.register([block.prefix, 'line1'])" />
4129
+ * </fieldset>
4130
+ * ```
4131
+ *
4132
+ * Pass `options.transforms` to run a sync normalisation pipeline over
4133
+ * user-typed values before they reach form state.
4134
+ */
4135
+ register: {
4136
+ <Path extends RegisterFlatPath<Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
4137
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form>] ? unknown : SegmentRegisterRejection<JoinSegments<S>>), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>;
4138
+ };
4139
+ /**
4140
+ * The form's identifier: the explicit `key` passed to `useForm`, or
4141
+ * an auto-generated unique id when `key` was omitted. Every
4142
+ * `ValidationError` this form produces carries it as `formKey`, so a
4143
+ * shared error list routes back to the right form.
4144
+ *
4145
+ * Typed as the literal `K` when an explicit `key` was passed, and as
4146
+ * `FormKey` when it was omitted.
4147
+ */
4148
+ key: K;
4149
+ /**
4150
+ * `true` while a function-form `defaultValues` factory is in flight,
4151
+ * between `useForm` construction and the moment the factory resolves:
4152
+ * the next microtask for a sync function, the promise settling for an
4153
+ * async one. `false` otherwise, a plain `defaultValues` included.
4154
+ *
4155
+ * The form is fully usable while `hydrating` is `true`, holding the
4156
+ * schema's slim defaults. The flag exists so a template can show a
4157
+ * spinner or dim the form while real data loads:
4158
+ *
4159
+ * ```vue
4160
+ * <div :aria-busy="form.hydrating">…</div>
4161
+ * ```
4162
+ *
4163
+ * Exposed as an auto-unwrapping `boolean` with no `.value`.
4164
+ * Reactivity survives through a getter that tracks the underlying ref
4165
+ * at the access site, so `watch(() => form.hydrating, ...)` and a
4166
+ * template read both fire on change. Reading it activates the form's
4167
+ * factory under the lazy-by-default rule.
4168
+ */
4169
+ readonly hydrating: boolean;
4170
+ /**
4171
+ * The error from the most recent function-form `defaultValues` factory,
4172
+ * normalized to a `ValidationError` (code `atta:hydration-failed`) so the
4173
+ * shape matches every other surface in `form.errors` / `form.meta.errors`.
4174
+ * `null` on construction, on successful resolution, and whenever no
4175
+ * factory has fired. Updates with each `form.rehydrate()` call.
4176
+ *
4177
+ * Distinct from `meta.submitError` so retry buttons and recovery UX can
4178
+ * stay focused on the load-time failure without entangling the submit
4179
+ * pipeline. Read directly in templates and script (no `.value`);
4180
+ * reactivity is preserved via a getter:
4181
+ *
4182
+ * ```vue
4183
+ * <p v-if="form.hydrateError">{{ form.hydrateError.message }}</p>
4184
+ * ```
4185
+ */
4186
+ readonly hydrateError: ValidationError | null;
4187
+ /**
4188
+ * `true` once the form's defaults have been applied, whether from a
4189
+ * plain `defaultValues` at construction or an async factory that
4190
+ * settled successfully. It stays `false` for a dormant lazy form
4191
+ * whose factory has not activated, and for a failed activation, which
4192
+ * sets `hydrateError`. Once `true` it stays `true` through refetches
4193
+ * so stale-while-revalidate UIs can keep rendering the prior values
4194
+ * while a `rehydrate()` is in flight.
4195
+ *
4196
+ * Composes with `hydrating` and `hydrateError`:
4197
+ *
4198
+ * ```vue
4199
+ * <Spinner v-if="!form.ready && form.hydrating" />
4200
+ * <ErrorBanner v-if="!form.ready && form.hydrateError" :error="form.hydrateError" />
4201
+ * <form v-if="form.ready">…</form>
4202
+ * ```
4203
+ *
4204
+ * Exposed as a reactive `boolean` with no `.value`. Reading it
4205
+ * activates the factory under the lazy-by-default rule, since
4206
+ * observing readiness implies use.
4207
+ */
4208
+ readonly ready: boolean;
4209
+ /**
4210
+ * Re-fire the captured `defaultValues` factory and re-apply its
4211
+ * payload over the current form values. Useful when the upstream
4212
+ * source changes (the user picks a different draft, a background
4213
+ * sync indicates fresh server data, etc.).
4214
+ *
4215
+ * Resolves after `hydrating` flips back to `false`. Throws
4216
+ * synchronously when the form was constructed with a plain-value
4217
+ * `defaultValues` (nothing to re-fire).
4218
+ *
4219
+ * The payload becomes the form's defaults, the same way a
4220
+ * `reset(next)` argument does, so `form.reset()` afterwards lands on
4221
+ * the values just fetched. Does NOT clear touched / submit state;
4222
+ * chain `form.reset()` for that.
4223
+ */
4224
+ rehydrate(): Promise<void>;
4225
+ /**
4226
+ * Idempotent activation. Forms are lazy-by-default: a function-form
4227
+ * `defaultValues` factory fires on the first reactive interaction
4228
+ * (reading `form.values`, calling `form.setValue`, etc.). Call
4229
+ * `form.activate()` to kick the factory explicitly, usually from
4230
+ * `setup`, so SSR's `onServerPrefetch` hook awaits the resolution
4231
+ * before the page renders. A later call returns the in-flight promise
4232
+ * until the factory settles, and resolves immediately after. A
4233
+ * plain-value form, having captured no factory, always returns a
4234
+ * resolved promise.
4235
+ */
4236
+ activate(): Promise<void>;
4237
+ /**
4238
+ * Reactive map of field errors, keyed by dotted path. Populated
4239
+ * automatically by `handleSubmit` and per-field validation; cleared
4240
+ * on validation success.
4241
+ *
4242
+ * Read in templates with no `.value`:
4243
+ *
4244
+ * ```vue
4245
+ * <p v-if="form.errors.email">{{ form.errors.email[0].message }}</p>
4246
+ * ```
4247
+ *
4248
+ * Watch from script via the getter form:
4249
+ *
4250
+ * ```ts
4251
+ * watch(() => form.errors.email, (errors) => …)
4252
+ * ```
4253
+ *
4254
+ * Use bracket access for a nested dotted key
4255
+ * (`form.errors['user.profile.email']`), since JS dot notation splits
4256
+ * on a literal dot.
4257
+ *
4258
+ * Read-only: populate it through `setErrors` and `clearErrors`. A
4259
+ * server response already emitting `ValidationError[]` pipes straight
4260
+ * into `setErrors` with no adapter.
4261
+ */
4262
+ errors: FormErrorsSurface<Form>;
4263
+ /**
4264
+ * The escape hatch for the rare case a consumer needs a `Ref<T>`:
4265
+ * handing the value to an external composable that expects a Vue ref,
4266
+ * or watching a single path with `watch(formRef, ...)`.
4267
+ *
4268
+ * ```ts
4269
+ * const emailRef = form.toRef('email') // Readonly<Ref<string>>
4270
+ * watch(emailRef, (next) => console.log(next))
4271
+ * ```
4272
+ *
4273
+ * Returns `Readonly<Ref<...>>`: writes go through `setValue`,
4274
+ * `register()` or a field-array helper, never the ref. Prefer
4275
+ * `form.values.email` for a direct read in a template or script, and
4276
+ * keep `toRef` for ref-shaped interop.
4277
+ */
4278
+ toRef: {
4279
+ <Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, Path>>>;
4280
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>>;
4281
+ };
4282
+ /**
4283
+ * Set the form's manual error layer. One surface for server-side
4284
+ * errors, optimistic-UI errors, and form-level banners: a field error
4285
+ * and a global (form-level) error are the same thing at different
4286
+ * paths, so there is no separate field/form split.
4287
+ *
4288
+ * Input is lenient ({@link ErrorInput}): a real `Error`, a partial
4289
+ * `{ message?, path?, code?, data? }`, or an array of either. The form
4290
+ * stamps its own `formKey`, defaults a missing `code` to
4291
+ * `atta:user-error`, and coerces a missing or empty `message` to
4292
+ * `"Unknown error"` instead of throwing. A server that already emits
4293
+ * `ValidationError[]` satisfies the input shape directly, so
4294
+ * `form.setErrors(response.errors)` needs no adapter.
4295
+ *
4296
+ * Three call forms, mirroring `setValue`:
4297
+ *
4298
+ * ```ts
4299
+ * // Whole-layer replace. An entry with no `path` is a global,
4300
+ * // form-level error (path `[]`); add a `path` to target a field.
4301
+ * form.setErrors([{ path: ['email'], message: 'Already taken' }])
4302
+ * form.setErrors({ message: 'Capacity exceeded' }) // global banner
4303
+ * form.setErrors(new Error('Network unreachable')) // message coerced
4304
+ *
4305
+ * // Functional update. `prev` is the current manual layer, flat.
4306
+ * form.setErrors((prev) => [...prev, { message: 'And one more' }])
4307
+ *
4308
+ * // Path-scoped. The path is stamped onto every entry, and only that
4309
+ * // path's bucket is replaced. `prev` is that path's manual errors.
4310
+ * form.setErrors('email', [{ message: 'Already taken' }])
4311
+ * form.setErrors(['profile', 'handle'], { message: 'Reserved' })
4312
+ * form.setErrors('email', (prev) => prev.slice(0, 1))
4313
+ * ```
4314
+ *
4315
+ * Replaces only the manual layer; schema/validation errors live in a
4316
+ * separate store and merge on read. Pass `[]` to clear the manual
4317
+ * layer (or use `clearErrors`, which also clears the schema layer).
4318
+ */
4319
+ setErrors: {
4320
+ (update: (prev: ValidationError[]) => ErrorInput | ErrorInput[]): void;
4321
+ (errors: ErrorInput | ErrorInput[]): void;
4322
+ (path: string | (string | number)[], errors: ErrorInput | ErrorInput[] | ((prev: ValidationError[]) => ErrorInput | ErrorInput[])): void;
4323
+ };
4324
+ /**
4325
+ * Clear errors at one path, or everywhere. Clears BOTH the manual
4326
+ * layer (set through `setErrors`) and the schema/validation layer at
4327
+ * the target. With always-on validation the schema half re-populates
4328
+ * on the next mutation if the value is still invalid, so the cleared
4329
+ * state is short-lived for a field that is still wrong.
4330
+ *
4331
+ * ```ts
4332
+ * form.clearErrors('email') // one field
4333
+ * form.clearErrors([]) // the global, form-level bucket
4334
+ * form.clearErrors() // every error on the form
4335
+ * ```
4336
+ */
4337
+ clearErrors: (path?: string | (string | number)[]) => void;
4338
+ /**
4339
+ * Form-level reactive flags, counters, and aggregates (`dirty`,
4340
+ * `valid`, `submitting`, `submissionAttempts`, and the flat `errors`
4341
+ * array). See `FormMeta` for the full shape. Read leaves directly
4342
+ * with no `.value`.
4343
+ *
4344
+ * For per-field state (touched, focused, blurred, errors at one
4345
+ * path), use `form.fields.<path>` instead. Undo/redo state lives at
4346
+ * `form.history` (see `FormHistoryNamespace`).
4347
+ */
4348
+ meta: FormMeta<Form>;
4349
+ /**
4350
+ * Restore the form to its defaults. Without arguments, re-applies
4351
+ * whatever the defaults currently are: `useForm({ defaultValues })`
4352
+ * on a fresh form, or the last values a `reset(next)` or an async
4353
+ * `defaultValues` factory settled on.
4354
+ *
4355
+ * Resets:
4356
+ * - the form value back to defaults;
4357
+ * - the dirty baseline (so the next edit flips `dirty` correctly);
4358
+ * - field errors;
4359
+ * - touched / focused / blurred per-field flags;
4360
+ * - submission state (`submitting` / `submissionAttempts` /
4361
+ * `submitted` / `submitError`).
4362
+ *
4363
+ * `nextDefaultValues` RE-SEATS the defaults rather than applying for
4364
+ * one call, so the form has a single set of defaults that `reset()`,
4365
+ * `resetField(path)`, and `dirty` all read. That is what makes the
4366
+ * save-then-discard shape work: reset from the resource the server
4367
+ * returned, and a later Discard returns to the save instead of
4368
+ * rolling back across it.
4369
+ *
4370
+ * ```ts
4371
+ * const saved = await api.save(form.values())
4372
+ * form.reset(toValues(saved)) // the saved resource is now the baseline
4373
+ * // …later…
4374
+ * form.reset() // back to the saved resource
4375
+ * ```
4376
+ *
4377
+ * The argument is sparse: it folds over the defaults already in
4378
+ * force, so paths it does not name keep the value they had, and
4379
+ * `reset({})` changes nothing. Arrays are replaced wholesale rather
4380
+ * than merged element-wise. Pass `unset` at a path to withdraw its
4381
+ * value and mark it blank.
4382
+ */
4383
+ reset: (nextDefaultValues?: DefaultValuesInput<Form>) => void;
4384
+ /**
4385
+ * Restore a single field (or a sub-tree like `'user'`) to its
4386
+ * initial value. Clears errors and touched flags for the field
4387
+ * and its descendants; leaves siblings and submission state alone.
4388
+ *
4389
+ * No-op when the path doesn't exist on the form (e.g. a typo'd
4390
+ * dynamic key).
4391
+ */
4392
+ resetField: (path: FlatPath<Form>) => void;
4393
+ /**
4394
+ * Wipe a field, or the whole form, to the appropriate nullish value
4395
+ * for its declared type: the underlying type's empty or falsy
4396
+ * concrete, with any `.default(x)` wrapper DELIBERATELY skipped.
4397
+ * Orthogonal to `reset` and `resetField` by design.
4398
+ *
4399
+ * ```ts
4400
+ * const schema = z.object({
4401
+ * notify: z.boolean().default(true),
4402
+ * count: z.number().default(5),
4403
+ * })
4404
+ * const form = useForm({ schema })
4405
+ *
4406
+ * form.reset() // notify → true, count → 5 (defaults)
4407
+ * form.clear() // notify → false, count → 0 (falsy-for-type)
4408
+ * form.clear('notify') // → false (NOT the declared default true)
4409
+ * ```
4410
+ *
4411
+ * Per-wrapper semantics:
4412
+ *
4413
+ * - `.default(x)`, `.prefault(x)` and `.catch(x)` give the inner
4414
+ * schema's empty; the default is DELIBERATELY skipped.
4415
+ * - `.optional()` gives `undefined`, the wrapper's absent marker.
4416
+ * - `.nullable()` gives `null`, its explicit empty.
4417
+ * - An object recurses, so every property gets its own empty.
4418
+ * - An array, Set or Record gives the empty one.
4419
+ *
4420
+ * Returns `true` when the write was accepted and `false` when the
4421
+ * adapter could not resolve an empty value at the path, as happens
4422
+ * when the path is not in the schema. Form state is unchanged on a
4423
+ * `false`.
4424
+ *
4425
+ * Sugar over `setValue(path, schema.getEmptyValueAtPath(path))` with
4426
+ * no separate bookkeeping, so variant memory, history and listeners
4427
+ * all see a regular write at the path.
4428
+ *
4429
+ * `clear()` with no argument targets the whole form, while
4430
+ * `clear('')` targets the empty-string path slot SPECIFICALLY. The
4431
+ * two are not interchangeable, matching `touch()` and `touch('')`
4432
+ * from #184.
4433
+ */
4434
+ clear: {
4435
+ (): boolean;
4436
+ <Path extends FlatPath<Form> | ''>(path: Path): boolean;
4437
+ <const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form> | ''] ? unknown : SegmentPathRejection<JoinSegments<S>>)): boolean;
4438
+ };
4439
+ /**
4440
+ * The undo/redo namespace: `form.history.{undo, redo, clear, canUndo,
4441
+ * canRedo, size}`. Always present, and inert when `useForm({ history })`
4442
+ * was not configured. `FormHistoryNamespace` carries the
4443
+ * field-by-field semantics.
4444
+ */
4445
+ history: FormHistoryNamespace;
4446
+ /**
4447
+ * Focus the first errored field's first visible element. Returns
4448
+ * `true` when an element was focused, `false` when no candidate
4449
+ * element exists (no errors, or every errored field is unmounted
4450
+ * or hidden).
4451
+ *
4452
+ * Pass `{ preventScroll: true }` if you're scrolling separately
4453
+ * (e.g. via `scrollToFirstError`) and don't want the browser to
4454
+ * fight the explicit scroll.
4455
+ *
4456
+ * Requests `focusVisible: true` so the UA paints a focus ring even
4457
+ * though the move is programmatic. Honored where supported; on
4458
+ * browsers without it, non-text controls focused after a pointer
4459
+ * submit may show no ring, so you may still want an app-side
4460
+ * indicator.
4461
+ */
4462
+ focusFirstError: (options?: {
4463
+ preventScroll?: boolean;
4464
+ }) => boolean;
4465
+ /**
4466
+ * Scroll the first errored field's first visible element into
4467
+ * view. Returns `true` when the call ran, `false` when no
4468
+ * candidate element exists.
4469
+ *
4470
+ * `options` is forwarded to `Element.scrollIntoView` unchanged.
4471
+ */
4472
+ scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
4473
+ /**
4474
+ * Run the form's own invalid-submit nudge imperatively: exactly what
4475
+ * `handleSubmit` does after a failed submit, but available standalone
4476
+ * and honoring this form's `useForm({ focusOnInvalidSubmit })` choice.
4477
+ *
4478
+ * Used by `useWizard` after navigating to the first failing form
4479
+ * during `wizard.handleSubmit`, so the failing form's own configured
4480
+ * behavior fires once its DOM is in view.
4481
+ *
4482
+ * No-op when this form opted out, or when no errored field is
4483
+ * currently registered. Use `focusFirstError()` to focus regardless
4484
+ * of the configured choice.
4485
+ */
4486
+ applyInvalidSubmitPolicy: () => void;
4487
+ /**
4488
+ * Programmatically mark fields as `touched`, the descriptive "this
4489
+ * field was visited" flag.
4490
+ *
4491
+ * ```ts
4492
+ * form.touch('email') // one leaf
4493
+ * form.touch('profile') // every leaf under profile
4494
+ * form.touch(['profile', 'name']) // segment-array form
4495
+ * form.touch() // every leaf in the form
4496
+ * ```
4497
+ *
4498
+ * **This does not reveal errors under the default display
4499
+ * heuristic.** `touched` also flips on a bare focus and blur with no
4500
+ * edit, so the library-default gate ignores it and reads
4501
+ * `blurredAfterInteraction` instead, the stricter bit that only a
4502
+ * blur *following* an edit sets. Reach for `touch()` when your own
4503
+ * analytics reads `touched`, and for
4504
+ * {@link UseFormReturnType.interact} to make a seeded or imported
4505
+ * value surface its errors.
4506
+ *
4507
+ * A pure flag write: it mutates no value, focused or blurred state
4508
+ * and triggers no validation. Idempotent, so re-calling on an
4509
+ * already-touched field is a no-op. Touched is sticky-true; pair it
4510
+ * with `form.reset()` or `form.resetField()` to clear.
4511
+ */
4512
+ touch: (path?: FlatPath<Form> | (string | number)[]) => void;
4513
+ /**
4514
+ * Simulate a complete focus, edit and blur over every leaf under
4515
+ * `path`, or the whole form when it is omitted, so a value that
4516
+ * arrived without a DOM gesture reveals its errors under the default
4517
+ * display heuristic.
4518
+ *
4519
+ * ```ts
4520
+ * form.interact('profile') // arm one subtree
4521
+ * await form.interact(['members', 2]) // errors committed on resolve
4522
+ * form.interact() // every leaf in the form
4523
+ * ```
4524
+ *
4525
+ * Flips the full interaction ladder (`touched`, `interacted`, and
4526
+ * `blurredAfterInteraction`) and runs the subtree's validation, so
4527
+ * the default gate (`submissionAttempts > 0 ||
4528
+ * blurredAfterInteraction`) opens through its front door. Use it to
4529
+ * arm one region's errors without a form-wide submit, which would
4530
+ * light up every other field on the page: a field-array row edited in
4531
+ * a modal, a server-seeded section, a pasted import.
4532
+ *
4533
+ * Walks schema leaves, so it reaches fields that are currently
4534
+ * `v-if`'d away or were never mounted; the flags are sticky, so
4535
+ * such a subtree stays revealed when it remounts.
4536
+ *
4537
+ * Flags land synchronously, and the returned promise resolves once
4538
+ * the subtree's validation has committed, so an awaiting caller reads
4539
+ * `showErrors` and `errors` immediately after. It never rejects, and
4540
+ * ignoring it is fine.
4541
+ *
4542
+ * No-op on a disabled form, matching every other interaction-
4543
+ * lifecycle write. Does not mutate value, `focused`, or `blurred`:
4544
+ * those are DOM-owned, and the display gate reads none of them.
4545
+ * Sticky like `touched`; pair with `form.reset()` /
4546
+ * `form.resetField()` to clear.
4547
+ */
4548
+ interact: (path?: FlatPath<Form> | (string | number)[]) => Promise<void>;
4549
+ /**
4550
+ * Append `value` to the array at `path`.
4551
+ *
4552
+ * ```ts
4553
+ * form.append('items', { name: 'New' })
4554
+ * ```
4555
+ */
4556
+ append: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
4557
+ /** Prepend `value` to the array at `path`. */
4558
+ prepend: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
4559
+ /**
4560
+ * Insert `value` into the array at `path` at the given `index`.
4561
+ * Behaves like `Array.prototype.splice`: `index` is clamped into
4562
+ * `[0, length]`, and negative indices count from the end.
4563
+ */
4564
+ insert: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
4565
+ /** Remove the element at `index` from the array at `path`. No-op when out of range. */
4566
+ remove: <Path extends ArrayPath<Form>>(path: Path, index: number) => void;
4567
+ /** Swap the elements at indices `a` and `b`. No-op when either is out of range. */
4568
+ swap: <Path extends ArrayPath<Form>>(path: Path, a: number, b: number) => void;
4569
+ /**
4570
+ * Move the element at `from` to `to`. Useful for drag-and-drop
4571
+ * reordering. No-op when either index is out of range.
4572
+ */
4573
+ move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
4574
+ /** Replace the element at `index` with `value`. No-op when out of range. */
4575
+ replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
4576
+ /**
4577
+ * Read-only, reactive view of the array at `path` as one `FieldState`
4578
+ * per element, in index order. Each entry carries its element `key`,
4579
+ * an allocated identity token, so a `v-for` keyed by it keeps a row's
4580
+ * component instance across an insert, removal, move, or swap:
4581
+ *
4582
+ * ```vue
4583
+ * <div v-for="(row, i) in form.list('contacts')" :key="row.key">
4584
+ * <input v-register="form.register(`contacts.${i}.name`)" />
4585
+ * <p v-if="row.showErrors">{{ row.firstError?.message }}</p>
4586
+ * </div>
4587
+ * ```
4588
+ *
4589
+ * Entries are the same field states `form.fields` exposes, so reads
4590
+ * stay live. `form.fields(path)` remains the single aggregated
4591
+ * container for the whole array; `list` is the per-element view.
4592
+ * For a record, reach for `record`, which keys each entry by its own
4593
+ * key.
4594
+ */
4595
+ list: <Path extends ArrayPath<Form>>(path: Path) => readonly FieldState<ArrayItem<Form, Path>>[];
4596
+ /**
4597
+ * Read-only, reactive view of the record at `path` as one `FieldState`
4598
+ * per entry, keyed by the entry's own key. Where `list` hands back an
4599
+ * ordered array for an array path, `record` hands back a keyed object
4600
+ * for a record path, so you iterate it by key:
4601
+ *
4602
+ * ```vue
4603
+ * <div v-for="(field, key) in form.record('scoresByTeam')" :key="key">
4604
+ * <label>{{ key }}</label>
4605
+ * <input v-register="form.register(`scoresByTeam.${key}`)" />
4606
+ * <p v-if="field.showErrors">{{ field.firstError?.message }}</p>
4607
+ * </div>
4608
+ * ```
4609
+ *
4610
+ * Entries are the same field states `form.fields` exposes, so reads
4611
+ * stay live, and the keyed shape mirrors the record's own keys: an
4612
+ * entry appears once you write its key (`form.setValue`) and drops
4613
+ * when the key leaves. `form.fields(path)` remains the single
4614
+ * aggregated container for the whole record; `record` is the
4615
+ * per-entry view.
4616
+ *
4617
+ * When the form root is itself a record (`useForm({ schema:
4618
+ * z.record(K, V) })`, a dictionary form), call `form.record()` with
4619
+ * no argument for the root entry view:
4620
+ *
4621
+ * ```vue
4622
+ * <div v-for="(member, id) in form.record()" :key="id">
4623
+ * <input v-register="form.register(id)" />
4624
+ * <p v-if="member.showErrors">{{ member.firstError?.message }}</p>
4625
+ * </div>
4626
+ * ```
4627
+ */
4628
+ record: RootRecordView<Form> & (<Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>);
4629
+ /**
4630
+ * Read-only view of the form's blank path set, and reactive: Vue 3.5
4631
+ * tracks `.has()`, `for..of` and size accesses, so a consumer drives
4632
+ * conditional UI off it directly:
4633
+ *
4634
+ * ```ts
4635
+ * watchEffect(() => {
4636
+ * if (form.blankPaths.value.size > 0) {
4637
+ * const paths = [...form.blankPaths.value] // Path[][], segments per entry
4638
+ * console.warn('unanswered fields:', paths.map((p) => p.join('.')))
4639
+ * }
4640
+ * })
4641
+ * ```
4642
+ *
4643
+ * `.has(input)` accepts the dotted-string form (`'user.email'`) or
4644
+ * the array form (`['user', 'email']`), which disambiguates a key
4645
+ * carrying literal dots such as `['address.primary']`.
4646
+ * [[BlankPathsView]] carries the full surface.
4647
+ *
4648
+ * For per-path access, use `form.fields.<path>.blank`.
4649
+ * Writes happen through `setValue(path, unset)`,
4650
+ * `markBlank()` on a register binding, and the directive's
4651
+ * input listener on numeric clear.
4652
+ */
4653
+ blankPaths: ComputedRef<BlankPathsView>;
4654
+ };
4655
+
4656
+ export { ROOT_PATH as R, canonicalizePath as ai, isPathPrefix as aj, isUnset as ak, parseDottedPath as al, unset as am, ROOT_PATH_KEY as z };
4657
+ export type { SchemaParseErrorWithData as $, AbstractSchema as A, ReactiveValidationStatus as B, CustomDirectiveRegisterAssignerFn as C, DefaultValuesInput as D, ErrorInput as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, ParseOptions as P, RecordPath as Q, RecordValue as S, RegisterDirective as T, UseFormConfiguration as U, RegisterFlatPath as V, RegisterOptions as W, RegisterSelectModifier as X, RegisterTextModifier as Y, RegisterTransform as Z, RegisterValue as _, UseFormReturnType as a, SchemaParseErrorWithoutData as a0, SchemaParseResult as a1, SchemaParseSuccess as a2, Segment as a3, SetValueCallback as a4, SetValuePayload as a5, SettledValidationStatus as a6, SlimPrimitiveKind as a7, SubmitHandler as a8, Unset as a9, ValidateOn as aa, ValidateOnConfig as ab, ValidationError as ac, ValidationResponse as ad, ValidationResponseWithoutValue as ae, ValueOfUnion as af, WriteMeta as ag, WriteShape as ah, RegisterModelDynamicCustomDirective as an, SchemaFactoryOptions as ao, AcceptableDefaults as ap, DisplayCtx as aq, GetDisplayState as ar, DisplayMachine as as, FieldRecord as at, ErrorCell as au, OriginalsRecord as av, AttaformDomBinding as aw, TransformAbortHolder as ax, ArrayItem as b, ArrayPath as c, DeepPartial as d, DefaultValuesShape as e, DisplayState as f, ErrorsProxyShape as g, FieldMetaPayload as h, FieldState as i, FieldStateMap as j, FieldStateMapEntry as k, FlatPath as l, FormErrorRecord as m, FormErrorsSurface as n, FormMeta as o, HistoryPlugin as p, IsUnion as q, Json as r, NestedType as s, OnSubmit as t, PartialFlatPath as u, Path as v, PathKey as w, PendingValidationStatus as x, Primitive as y };