{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "json-form",
  "title": "JSON Form",
  "author": "ilinxa",
  "description": "Schema-driven form engine — a field DSL compiled to Zod, 25 field types including rich text, conditional and computed fields, on React Hook Form.",
  "dependencies": [
    "react-hook-form@^7.75.0",
    "@hookform/resolvers@^5.2.2",
    "zod@^4.4.3",
    "lucide-react@^1.11.0"
  ],
  "registryDependencies": [
    "radio-group",
    "slider",
    "label",
    "input",
    "textarea",
    "select",
    "checkbox",
    "switch",
    "command",
    "popover",
    "calendar",
    "button",
    "separator",
    "badge",
    "@ilinxa/code-block",
    "@ilinxa/rich-text-editor"
  ],
  "files": [
    {
      "path": "src/registry/components/forms/json-form/json-form.tsx",
      "content": "\"use client\";\n\nimport {\n  Fragment,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { FormProvider, useFormState, useWatch } from \"react-hook-form\";\nimport { cn } from \"@/lib/utils\";\nimport { Separator } from \"@/components/ui/separator\";\n\nimport { useJsonForm } from \"./hooks/use-json-form\";\nimport { useDebouncedCallback } from \"./hooks/use-debounced-callback\";\nimport { mergeStrings } from \"./lib/strings\";\nimport { defaultJsonFormRegistry } from \"./lib/default-registry\";\nimport { flattenRhfErrorsToRecord } from \"./lib/flatten-errors\";\nimport { isValueField } from \"./lib/validate-schema\";\nimport { JsonFormProvider, useJsonFormContext } from \"./json-form-context\";\nimport { FieldWrapper } from \"./parts/field-wrapper\";\nimport { FieldFallback } from \"./parts/field-fallback\";\nimport { FormHeader } from \"./parts/form-header\";\nimport { JsonFormErrorSummary } from \"./parts/error-summary\";\nimport { JsonFormSubmitButton } from \"./parts/submit-button\";\nimport { JsonFormResetButton } from \"./parts/reset-button\";\nimport type {\n  FieldDefinition,\n  FieldRenderer,\n  JsonFormContextValue,\n  JsonFormProps,\n} from \"./types\";\n\n/**\n * Top-level component. Composes our `<JsonFormProvider>` + RHF's\n * `<FormProvider>` + a layout shell that walks `schema.fields` and renders\n * each via the resolver chain (`renderField` → `fieldRegistry` → default).\n */\nexport function JsonForm<\n  TValues extends Record<string, unknown> = Record<string, unknown>,\n>({\n  schema,\n  defaultValues,\n  values,\n  onSubmit,\n  onSubmitError,\n  onSubmitAttempt,\n  onChange,\n  onChangeDebounce = 100,\n  onValidationChange,\n  onReady,\n  columns = 1,\n  labelPosition = \"top\",\n  showSummary,\n  summaryStrategy = \"post-submit\",\n  showSchemaHeader = true,\n  submitButton,\n  resetButton,\n  fieldRegistry,\n  renderField,\n  validationMode = \"onTouched\",\n  strings: stringsOverride,\n  className,\n  style,\n  ref,\n}: JsonFormProps<TValues>) {\n  const formId = useId();\n  const strings = useMemo(() => mergeStrings(stringsOverride), [stringsOverride]);\n  const [hasSubmitted, setHasSubmitted] = useState(false);\n\n  const { form, zodSchema, handle } = useJsonForm<TValues>(schema, {\n    defaultValues,\n    values,\n    validationMode,\n    strings: stringsOverride,\n  });\n\n  // Merge consumer registry with the default.\n  const mergedRegistry = useMemo<Record<string, FieldRenderer>>(\n    () => ({ ...defaultJsonFormRegistry, ...(fieldRegistry ?? {}) }),\n    [fieldRegistry],\n  );\n\n  // `hasSubmitted` deliberately lives on a sibling context (provided to\n  // `<JsonFormProvider>` as a prop) — keeping it OUT of this memo means the\n  // first-submit flip doesn't invalidate the main context and re-render every\n  // field. The compat field on the value (set to `false` here) is overwritten\n  // by `useJsonFormContext` reads, which merge in the narrow context.\n  const ctx = useMemo<JsonFormContextValue<TValues>>(\n    () => ({\n      ...handle,\n      rhf: form,\n      schema,\n      zodSchema,\n      strings,\n      formId,\n      hasSubmitted: false,\n      fieldRegistry: mergedRegistry,\n    }),\n    [handle, form, schema, zodSchema, strings, formId, mergedRegistry],\n  );\n\n  useImperativeHandle(ref, () => handle, [handle]);\n\n  // T3.7 — fire `onReady` once after mount + defaults applied. Stored in a\n  // ref so a parent re-render that passes a fresh callback identity doesn't\n  // re-fire (the contract is \"once per form lifetime\"). Ref-write happens\n  // inside an effect (React Compiler bans mutation during render).\n  const onReadyRef = useRef(onReady);\n  useEffect(() => {\n    onReadyRef.current = onReady;\n  }, [onReady]);\n  const readyFiredRef = useRef(false);\n  useEffect(() => {\n    if (readyFiredRef.current) return;\n    readyFiredRef.current = true;\n    onReadyRef.current?.({ formApi: handle });\n  }, [handle]);\n\n  // Debounced onChange that subscribes to the values bag.\n  const debouncedChange = useDebouncedCallback((vals: Record<string, unknown>) => {\n    onChange?.({ values: vals as TValues, formApi: handle });\n  }, onChangeDebounce);\n\n  return (\n    <FormProvider {...form}>\n      <JsonFormProvider value={ctx as JsonFormContextValue} hasSubmitted={hasSubmitted}>\n        <form\n          noValidate\n          className={cn(\"flex flex-col gap-4\", className)}\n          style={style}\n          onSubmit={(e) => {\n            e.preventDefault();\n            setHasSubmitted(true);\n            // T3.6 — fires unconditionally before validation runs.\n            onSubmitAttempt?.({ formApi: handle });\n            void form.handleSubmit(\n              async (vals) => {\n                await onSubmit({\n                  values: vals as TValues,\n                  formApi: handle,\n                });\n              },\n              (errs) => {\n                const flat = flattenRhfErrorsToRecord(errs);\n                onSubmitError?.({\n                  errors: flat,\n                  formApi: handle,\n                });\n                onValidationChange?.({\n                  isValid: false,\n                  errors: flat,\n                });\n                // T2.2 — focus the first focusable invalid field in DOM\n                // order (schema.fields order). Skip non-focusable types\n                // (`computed` is `<output>`, `hidden`/`section`/`divider`\n                // render no control); falling through to the next errored\n                // field beats a silent no-op `setFocus` call.\n                const focusTarget = schema.fields.find(\n                  (f) =>\n                    isValueField(f) &&\n                    f.type !== \"computed\" &&\n                    f.type !== \"hidden\" &&\n                    flat[f.name] !== undefined,\n                );\n                if (focusTarget) {\n                  try {\n                    form.setFocus(focusTarget.name as never);\n                  } catch {\n                    // Fall through — non-registered name (rare; safer to\n                    // skip than to throw out of a submit handler).\n                  }\n                }\n              },\n            )(e);\n          }}\n        >\n          <ChangeBridge\n            onChange={onChange ? debouncedChange : undefined}\n            onValidationChange={onValidationChange}\n          />\n\n          {showSchemaHeader ? <FormHeader /> : null}\n\n          {showSummary ? (\n            <JsonFormErrorSummary strategy={summaryStrategy} />\n          ) : null}\n\n          <FieldList\n            fields={schema.fields}\n            registry={mergedRegistry}\n            renderField={renderField}\n            columns={columns}\n            labelPosition={labelPosition}\n          />\n\n          {(submitButton !== false || resetButton) ? (\n            <SubmitRow\n              submitButton={submitButton}\n              resetButton={resetButton}\n            />\n          ) : null}\n        </form>\n      </JsonFormProvider>\n    </FormProvider>\n  );\n}\n\n// ─── Change + validation bridge ──────────────────────────────────────────────\n\nfunction ChangeBridge({\n  onChange,\n  onValidationChange,\n}: {\n  onChange?: (vals: Record<string, unknown>) => void;\n  onValidationChange?: (args: {\n    isValid: boolean;\n    errors: Record<string, string>;\n  }) => void;\n}) {\n  const values = useWatch();\n  const { isValid, errors } = useFormState();\n  const rendered = useRef(0);\n  const lastValuesKey = useRef<string | null>(null);\n  const lastValidity = useRef<{ isValid: boolean; errorKey: string } | null>(null);\n\n  useEffect(() => {\n    // Skip first synthetic emit (initial mount) so we don't fire onChange\n    // before the user has touched anything.\n    if (rendered.current === 0) {\n      rendered.current++;\n      lastValuesKey.current = stableStringify(values);\n      return;\n    }\n    // Structural-equality guard breaks the controlled-mode echo loop: when\n    // a consumer pipes `onChange` back through the `values` prop, RHF\n    // re-runs `useWatch` and produces a new reference for the same payload.\n    // Without this guard the debounced fire-out would still leak, defeating\n    // defense #2 of the controlled-mode three-defenses pattern.\n    const next = stableStringify(values);\n    if (next === lastValuesKey.current) return;\n    lastValuesKey.current = next;\n    onChange?.((values ?? {}) as Record<string, unknown>);\n  }, [values, onChange]);\n\n  useEffect(() => {\n    if (!onValidationChange) return;\n    const flat = flattenRhfErrorsToRecord(errors as Record<string, unknown>);\n    const errorKey = `${Object.keys(flat).length}:${Object.keys(flat).join(\",\")}`;\n    const last = lastValidity.current;\n    if (last && last.isValid === isValid && last.errorKey === errorKey) return;\n    lastValidity.current = { isValid, errorKey };\n    onValidationChange({ isValid, errors: flat });\n  }, [isValid, errors, onValidationChange]);\n\n  return null;\n}\n\n// ─── Field-list layout (sections / dividers / rows) ──────────────────────────\n\ninterface FieldListProps {\n  fields: ReadonlyArray<FieldDefinition>;\n  registry: Record<string, FieldRenderer>;\n  renderField?: (args: {\n    field: FieldDefinition;\n    defaultRender: () => ReactNode;\n  }) => ReactNode;\n  columns: 1 | 2;\n  labelPosition: \"top\" | \"left\";\n}\n\nfunction FieldList({\n  fields,\n  registry,\n  renderField,\n  columns,\n  labelPosition,\n}: FieldListProps) {\n  const gridClass =\n    columns === 2 ? \"grid grid-cols-1 gap-4 md:grid-cols-2\" : \"flex flex-col gap-4\";\n\n  // Walk fields linearly. Section/divider are inline siblings — they break the\n  // grid rhythm (full-width) but don't introduce nested DOM groups, keeping\n  // the layout simple for v0.1.0.\n  return (\n    <div className={gridClass}>\n      {fields.map((field) => {\n        if (field.type === \"section\") {\n          return (\n            <section\n              key={field.name}\n              data-jsonform-section={field.name}\n              className={cn(\"col-span-full mt-2 flex flex-col gap-1\", columns === 2 && \"md:col-span-2\")}\n            >\n              {field.label ? (\n                <h3 className=\"text-sm font-semibold uppercase tracking-wide text-muted-foreground\">\n                  {field.label}\n                </h3>\n              ) : null}\n              {field.description ? (\n                <p className=\"text-xs text-muted-foreground\">\n                  {field.description}\n                </p>\n              ) : null}\n            </section>\n          );\n        }\n\n        if (field.type === \"divider\") {\n          return (\n            <div\n              key={field.name}\n              data-jsonform-divider={field.name}\n              className={cn(\"col-span-full\", columns === 2 && \"md:col-span-2\")}\n            >\n              <Separator />\n            </div>\n          );\n        }\n\n        const renderer = registry[field.type] ?? registry[\"__fallback__\"] ?? FieldFallback;\n        const defaultRender = () => (\n          <FieldWrapper\n            field={field}\n            renderer={renderer}\n            formLabelPosition={labelPosition}\n          />\n        );\n\n        return (\n          <Fragment key={field.name}>\n            {renderField\n              ? renderField({ field, defaultRender })\n              : defaultRender()}\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n}\n\n// ─── Submit row ──────────────────────────────────────────────────────────────\n\nfunction SubmitRow({\n  submitButton,\n  resetButton,\n}: {\n  submitButton: JsonFormProps[\"submitButton\"];\n  resetButton: JsonFormProps[\"resetButton\"];\n}) {\n  if (typeof submitButton === \"function\") {\n    return (\n      <SubmitFunctionRow render={submitButton} resetButton={resetButton} />\n    );\n  }\n\n  const cfg = submitButton ?? {};\n  const align = (cfg && typeof cfg === \"object\" ? cfg.align : \"right\") ?? \"right\";\n  const alignClass =\n    align === \"left\"\n      ? \"justify-start\"\n      : align === \"center\"\n        ? \"justify-center\"\n        : \"justify-end\";\n\n  return (\n    <div className={cn(\"flex items-center gap-2 pt-1\", alignClass)}>\n      {resetButton ? (\n        <JsonFormResetButton\n          label={resetButton.label}\n          variant={resetButton.variant}\n        />\n      ) : null}\n      {submitButton !== false ? (\n        <JsonFormSubmitButton\n          label={cfg && typeof cfg === \"object\" ? cfg.label : undefined}\n          variant={cfg && typeof cfg === \"object\" ? cfg.variant : undefined}\n          disableWhenInvalid={\n            cfg && typeof cfg === \"object\"\n              ? cfg.disableWhenInvalid\n              : undefined\n          }\n        />\n      ) : null}\n    </div>\n  );\n}\n\nfunction SubmitFunctionRow({\n  render,\n  resetButton,\n}: {\n  render: Extract<\n    JsonFormProps[\"submitButton\"],\n    (args: { isSubmitting: boolean; isValid: boolean }) => ReactNode\n  >;\n  resetButton: JsonFormProps[\"resetButton\"];\n}) {\n  // Read isSubmitting / isValid via context-aware atoms.\n  return (\n    <div className=\"flex items-center justify-end gap-2 pt-1\">\n      {resetButton ? (\n        <JsonFormResetButton\n          label={resetButton.label}\n          variant={resetButton.variant}\n        />\n      ) : null}\n      <SubmitRenderProxy render={render} />\n    </div>\n  );\n}\n\nfunction SubmitRenderProxy({\n  render,\n}: {\n  render: (args: { isSubmitting: boolean; isValid: boolean }) => ReactNode;\n}) {\n  const ctx = useJsonFormContext();\n  return (\n    <>\n      {render({\n        isSubmitting: ctx.rhf.formState.isSubmitting,\n        isValid: ctx.rhf.formState.isValid,\n      })}\n    </>\n  );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Key-sorted JSON stringify so `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` hash to\n * the same key. Used by `ChangeBridge` to detect echo-loop re-emissions of a\n * structurally-identical values bag.\n */\nfunction stableStringify(input: unknown): string {\n  if (input === null || typeof input !== \"object\") return JSON.stringify(input);\n  if (Array.isArray(input)) return `[${input.map(stableStringify).join(\",\")}]`;\n  const keys = Object.keys(input as Record<string, unknown>).sort();\n  return `{${keys\n    .map((k) => `${JSON.stringify(k)}:${stableStringify((input as Record<string, unknown>)[k])}`)\n    .join(\",\")}}`;\n}\n\n// ─── Tail type re-exports (cross-procomp consumers) ──────────────────────────\n// Procomps that compose json-form must import these types from THIS component-\n// file path, not `./types`: the shadcn path rewriter preserves the component-\n// file path but mangles `/types` cross-procomp imports (F-S1).\nexport type {\n  FormSchema,\n  Condition,\n  ConditionOrFn,\n  FieldDefinition,\n  FieldOptionsResolver,\n  FieldRenderer,\n  JsonFormHandle,\n  FieldConfig,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/json-form/json-form.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/json-form-context.tsx",
      "content": "\"use client\";\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport { FormProvider } from \"react-hook-form\";\nimport type { JsonFormContextValue } from \"./types\";\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- `Record<string, any>` is required to satisfy react-hook-form's `FieldValues` constraint; `unknown` won't type-check there. */\n\nconst JsonFormContext = createContext<JsonFormContextValue | null>(null);\n\n/**\n * Narrow context that ONLY carries `hasSubmitted`. Split from the main\n * `JsonFormContext` (v0.1.3) so that flipping `hasSubmitted` false→true on\n * the first submit doesn't invalidate the memoized main context — which\n * would re-render every field that consumes it. Only `<JsonFormErrorSummary>`\n * actually needs to react to this flag.\n */\nconst JsonFormSubmittedContext = createContext<boolean>(false);\n\nexport interface JsonFormProviderProps<TValues extends Record<string, any> = Record<string, any>> {\n  value: JsonFormContextValue<TValues>;\n  /**\n   * Has the form attempted a submit at least once? Drives the\n   * `post-submit` error-summary strategy. Optional — defaults to `false`.\n   * Provided as a sibling prop (not on `value`) so updating it doesn't\n   * invalidate the memoized main context.\n   */\n  hasSubmitted?: boolean;\n  children: ReactNode;\n}\n\nexport function JsonFormProvider<TValues extends Record<string, any> = Record<string, any>>({\n  value,\n  hasSubmitted = false,\n  children,\n}: JsonFormProviderProps<TValues>) {\n  // v0.2.4 — bridge RHF's <FormProvider> internally so the documented headless\n  // pattern (`<JsonFormProvider value={{ ...handle, rhf: form, ... }}>`)\n  // works end-to-end without consumers having to also remember to wrap\n  // children in `<FormProvider {...form}>`. `<JsonFormField>` /\n  // `<FieldWrapper>` and the narrow-deps hooks need RHF's context to call\n  // `useFormContext()` and `useWatch({ control })`; before v0.2.4 the\n  // headless example in `demo.tsx` (and any consumer copying it) silently\n  // mounted a tree where every FieldWrapper crashed with \"Cannot destructure\n  // 'control' of useFormContext() as it is null\". `<JsonForm>` already wraps\n  // with `<FormProvider {...form}>` at the top level — nesting a second copy\n  // here is a no-op since both share the same `form` instance (the inner\n  // wins, identical values).\n  return (\n    <FormProvider {...value.rhf}>\n      <JsonFormContext.Provider value={value as JsonFormContextValue}>\n        <JsonFormSubmittedContext.Provider value={hasSubmitted}>\n          {children}\n        </JsonFormSubmittedContext.Provider>\n      </JsonFormContext.Provider>\n    </FormProvider>\n  );\n}\n\n/**\n * Accessor hook for the JsonForm context. Use inside standalone parts\n * (`<JsonFormField>`, `<JsonFormErrorSummary>`, etc.) or to build a fully\n * headless layout while still using our context.\n *\n * v0.1.3 — the returned context's `hasSubmitted` is a read-time snapshot\n * from the narrow `JsonFormSubmittedContext` so consumers that depend on it\n * re-render only when it flips. Most consumers should not need this; reach\n * for `useJsonFormHasSubmitted()` if you only need the flag.\n */\nexport function useJsonFormContext<TValues extends Record<string, any> = Record<string, any>>(): JsonFormContextValue<TValues> {\n  const ctx = useContext(JsonFormContext);\n  const hasSubmitted = useContext(JsonFormSubmittedContext);\n  if (!ctx) {\n    throw new Error(\n      \"[json-form] useJsonFormContext must be used inside a <JsonForm> or <JsonFormProvider>.\",\n    );\n  }\n  // Reattach `hasSubmitted` so callers reading `ctx.hasSubmitted` still work,\n  // even though it lives on a separate context internally.\n  return { ...ctx, hasSubmitted } as JsonFormContextValue<TValues>;\n}\n\n/**\n * Narrow accessor — returns just the `hasSubmitted` flag. Use this in\n * standalone parts that ONLY care about post-submit state (e.g.,\n * `<JsonFormErrorSummary>`) so they don't re-render on every main-context\n * change.\n */\nexport function useJsonFormHasSubmitted(): boolean {\n  return useContext(JsonFormSubmittedContext);\n}\n\n/**\n * v0.2.1 — non-throwing variant of `useJsonFormContext`. Returns `null`\n * when no `<JsonFormProvider>` is in the tree. Internal-only — exported\n * for `<JsonFormDevtools>` to render a soft-warning fallback panel\n * instead of throwing when consumers forget the Provider wrap.\n */\nexport function useJsonFormContextOptional<TValues extends Record<string, any> = Record<string, any>>(): JsonFormContextValue<TValues> | null {\n  const ctx = useContext(JsonFormContext);\n  const hasSubmitted = useContext(JsonFormSubmittedContext);\n  if (!ctx) return null;\n  return { ...ctx, hasSubmitted } as JsonFormContextValue<TValues>;\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n",
      "type": "registry:component",
      "target": "components/json-form/json-form-context.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/index.ts",
      "content": "// Top-level component + standalone parts\nexport { JsonForm } from \"./json-form\";\nexport {\n  JsonFormProvider,\n  useJsonFormContext,\n  useJsonFormHasSubmitted,\n} from \"./json-form-context\";\nexport { JsonFormField } from \"./parts/json-form-field\";\nexport { JsonFormErrorSummary } from \"./parts/error-summary\";\nexport { JsonFormSubmitButton } from \"./parts/submit-button\";\nexport { JsonFormResetButton } from \"./parts/reset-button\";\nexport { FormHeader as JsonFormHeader } from \"./parts/form-header\";\nexport { FieldWrapper as JsonFormFieldWrapper } from \"./parts/field-wrapper\";\nexport {\n  JsonFormDevtools,\n  type JsonFormDevtoolsProps,\n} from \"./parts/json-form-devtools\";\n\n// Headless factory + default registry (consumer can spread + extend)\nexport { useJsonForm } from \"./hooks/use-json-form\";\nexport {\n  useJsonFormFieldValue,\n  useJsonFormFieldsValue,\n} from \"./hooks/use-json-form-field-value\";\nexport { defaultJsonFormRegistry } from \"./lib/default-registry\";\nexport { defineFieldRenderer } from \"./lib/define-field-renderer\";\n\n// String dictionary + helpers\nexport { defaultStrings as defaultJsonFormStrings, mergeStrings as mergeJsonFormStrings } from \"./lib/strings\";\n\n// Note: `RICH_TEXT_EMPTY_VALUE` (the canonical Plate \"empty paragraph\"\n// default) is intentionally NOT re-exported from this barrel. Earlier\n// attempts (v0.1.3 + v0.1.4) hit a shadcn path-rewriter inconsistency\n// where cross-slug imports from `index.ts` (and `lib/*.ts`) didn't get\n// the same flattening as imports from `parts/*.tsx`. Consumers using\n// `richtext` fields should import the empty default directly from\n// `@ilinxa/rich-text-editor`:\n//\n//   import { RICH_TEXT_EMPTY_VALUE } from \"@/components/rich-text-editor\";\n//\n// (or whatever the consumer-side path is for their setup). Documented\n// in usage.tsx.\n\n// Public types\nexport type {\n  ButtonVariant,\n  Condition,\n  ConditionOrFn,\n  FieldConfig,\n  FieldAriaProps,\n  FieldDefinition,\n  FieldOption,\n  FieldOptionsResolver,\n  FieldRenderer,\n  FieldRendererArgs,\n  FieldType,\n  FieldValidators,\n  FormSchema,\n  JsonFormContextValue,\n  JsonFormHandle,\n  JsonFormProps,\n  JsonFormStrings,\n  JsonFormSubmitErrorArgs,\n  JsonFormValidationChangeArgs,\n  JsonFormValuesArgs,\n  UseJsonFormOptions,\n  UseJsonFormReturn,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/json-form/index.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/types.ts",
      "content": "import type { CSSProperties, ReactNode, Ref } from \"react\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport type { ZodObject, ZodTypeAny } from \"zod\";\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- `Record<string, any>` is required to satisfy react-hook-form's `FieldValues` constraint; `unknown` won't type-check there. */\n\n// ─── Field DSL ───────────────────────────────────────────────────────────────\n\n/** All built-in v0.1.x field types, plus string for consumer-registered ones. */\nexport type FieldType =\n  // text family\n  | \"text\" | \"email\" | \"password\" | \"url\" | \"tel\" | \"textarea\" | \"number\"\n  // choice family\n  | \"select\" | \"multi-select\" | \"radio-group\" | \"checkbox\" | \"checkbox-group\" | \"switch\"\n  // date/time family\n  | \"date\" | \"date-range\" | \"time\" | \"datetime\"\n  // rich / composite (v0.1.0 + v0.1.1 `richtext`)\n  | \"code\" | \"slider\" | \"rating\" | \"richtext\"\n  // special\n  | \"computed\" | \"hidden\" | \"section\" | \"divider\"\n  // consumer-extended (registry lookup); also covers v0.1.x types ('file', 'color', 'array') before they ship\n  | (string & {});\n\n/** Static-array form of options. */\nexport interface FieldOption {\n  value: unknown;\n  label: string;\n  description?: string;\n  disabled?: boolean;\n}\n\n/** Async resolver form of options. */\nexport type FieldOptionsResolver = (args: {\n  query?: string;\n  allValues: Record<string, unknown>;\n  /**\n   * v0.1.6 — abort signal. Fires when the resolver run is superseded by a\n   * newer query/dep change or the field unmounts. Fetch-based resolvers\n   * should pass this to `fetch(url, { signal })`; ignoring it is harmless\n   * (the result is still discarded by the generation guard).\n   */\n  signal: AbortSignal;\n}) => Promise<FieldOption[]>;\n\n/** Per-field type-specific config (hybrid shape — A1 resolution). */\nexport interface FieldConfig {\n  code?: { editorExtensions?: unknown[]; readOnly?: boolean };\n  date?: { firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6; minYear?: number; maxYear?: number };\n  rating?: { stars?: number };\n  /** v0.1.1 — Plate-based richtext field config. `hideToolbar` collapses both top and floating toolbars. */\n  richText?: { hideToolbar?: boolean; autoFocus?: boolean };\n  // v0.1.x additions land here non-breakingly: file?, color?\n}\n\n/** Declarative validators block. Compiled to Zod chain at schema-compile time. */\nexport interface FieldValidators {\n  required?: boolean | string;\n  min?: number | string | { value: number | string; message: string };\n  max?: number | string | { value: number | string; message: string };\n  minLength?: number | { value: number; message: string };\n  maxLength?: number | { value: number; message: string };\n  pattern?: string | { value: string; message: string };\n  email?: boolean | { message: string };\n  url?: boolean | { message: string };\n}\n\n/** Condition DSL — 11 operators + function escape hatch. */\nexport type Condition =\n  | { field: string; equals: unknown }\n  | { field: string; notEquals: unknown }\n  | { field: string; in: ReadonlyArray<unknown> }\n  | { field: string; notIn: ReadonlyArray<unknown> }\n  | { field: string; matches: string }\n  | { field: string; truthy: boolean }\n  | { field: string; greaterThan: number }\n  | { field: string; lessThan: number }\n  | { all: ReadonlyArray<Condition> }\n  | { any: ReadonlyArray<Condition> }\n  | { not: Condition };\n\nexport type ConditionOrFn =\n  | Condition\n  | ((args: { values: Record<string, unknown> }) => boolean);\n\n/**\n * A single field definition in the JSON schema.\n *\n * `name` is REQUIRED for all field types — including `section` and `divider`,\n * which produce no value but use `name` as a stable React key. Consumer\n * convention: prefix non-value-carrying names with `_` (e.g., `'_personal'`,\n * `'_divider1'`). Compiler ignores `name` for `section`/`divider` when\n * building the ZodObject.\n */\nexport interface FieldDefinition {\n  name: string;\n  type: FieldType;\n  label?: string;\n  description?: string;\n  placeholder?: string;\n  defaultValue?: unknown;\n\n  validators?: FieldValidators;\n  validate?: (args: { value: unknown; allValues: Record<string, unknown> }) => string | undefined;\n  validateAsync?: (args: { value: unknown; allValues: Record<string, unknown> }) => Promise<string | undefined>;\n  validateAsyncDebounce?: number;\n\n  visibleWhen?: ConditionOrFn;\n  enabledWhen?: ConditionOrFn;\n  requiredWhen?: ConditionOrFn;\n  keepValueWhenHidden?: boolean;\n\n  options?: ReadonlyArray<FieldOption> | FieldOptionsResolver;\n  searchable?: boolean;\n  optionsDebounce?: number;\n  min?: number; max?: number; step?: number;\n  rows?: number;\n  lang?: string;\n\n  config?: FieldConfig;\n\n  expression?: string;\n  compute?: (args: { values: Record<string, unknown> }) => unknown;\n  editable?: boolean;\n\n  row?: string;\n  colSpan?: \"full\" | 1 | 2;\n  width?: \"full\" | \"half\" | \"third\" | \"quarter\" | number;\n  labelPosition?: \"top\" | \"left\";\n\n  disabled?: boolean;\n  readOnly?: boolean;\n  autoComplete?: string;\n  autoFocus?: boolean;\n\n  /**\n   * v0.1.6 — typed metadata for downstream per-locale renderers (T3.8).\n   * Upstream `JsonForm` itself does NOT change behavior based on this flag;\n   * the contract is purely \"this field's value is locale-aware.\" A\n   * consumer-side HOC composed into the registry (e.g., `withPerLocale`)\n   * reads this flag and renders a locale-tab strip around the underlying\n   * renderer. See the docs for the canonical HOC pattern.\n   */\n  translatable?: boolean;\n\n  /**\n   * Declares which field paths this renderer's `allValues` access depends\n   * on. The FieldWrapper resolves a per-field subscription mode against\n   * this flag (active as of v0.2.0):\n   *\n   * - `undefined` (default) → full-bag subscription; preserves v0.1.x\n   *   behavior for unmigrated custom renderers.\n   * - `[]` → no subscription; renderer receives a `getValues()` snapshot\n   *   under `allValues`. Use this when the renderer doesn't read\n   *   `allValues` at all (the most common case for custom renderers).\n   * - `['a', 'b']` → narrow subscription; `allValues` is rebuilt from the\n   *   watched paths only via `setByPath`.\n   *\n   * Built-in default-registry renderers (`text`, `radio-group`, `checkbox`,\n   * etc.) auto-skip the subscription per an internal whitelist — declare\n   * `dependsOn` only on custom renderers, or to override the whitelist for\n   * a specific field. The whitelist check resolves on the FINAL renderer\n   * identity (not on `field.type` alone), so a consumer-registered renderer\n   * replacing `text` correctly opts back into full-bag unless it sets\n   * `dependsOn`.\n   *\n   * **Stable identity:** the array reference should be stable across renders\n   * (module-scope const or `useMemo`-cached). RHF de-dupes subscription\n   * targets by content, so an unstable reference doesn't break correctness,\n   * but stable refs avoid wasted reconciles.\n   *\n   * `validateSchemaDev` warns when `dependsOn` references a field name that\n   * doesn't exist in `schema.fields[].name`.\n   */\n  dependsOn?: ReadonlyArray<string>;\n}\n\n/** Top-level form schema. */\nexport interface FormSchema {\n  fields: ReadonlyArray<FieldDefinition>;\n  validate?: (args: { values: Record<string, unknown> }) => Record<string, string> | undefined;\n  zodSchema?: ZodObject<Record<string, ZodTypeAny>>;\n  meta?: { id?: string; version?: string; title?: string; description?: string };\n}\n\n// ─── Callback arg shapes (object-shape per F-cross-12) ───────────────────────\n\nexport interface JsonFormValuesArgs<TValues> {\n  values: TValues;\n  formApi: JsonFormHandle<TValues>;\n}\n\nexport interface JsonFormSubmitErrorArgs<TValues> {\n  errors: Record<string, string>;\n  formApi: JsonFormHandle<TValues>;\n}\n\nexport interface JsonFormValidationChangeArgs {\n  isValid: boolean;\n  errors: Record<string, string>;\n}\n\n// ─── FieldRenderer (T1 lock — single canonical name) ─────────────────────────\n\n/**\n * ARIA attributes the field-wrapper computes once per render and passes to\n * the renderer. Renderers MUST attach these to the appropriate interactive\n * element:\n *\n * - **Native form controls** (`<input>`, `<textarea>`, `<button>`, etc.):\n *   spread `id` + `aria-required` + `aria-invalid` + `aria-disabled` +\n *   `aria-describedby` onto the element. The wrapper's `<label htmlFor>`\n *   targets the same `id`.\n * - **Group-style controls** (`role=\"radiogroup\"`, `role=\"group\"`,\n *   custom widgets): attach `aria-labelledby={labelledBy}` to the group root\n *   instead of `id` (HTML `label[for]` doesn't bind to non-form controls).\n *   Still spread the remaining `aria-*` attributes.\n *\n * v0.1.2 — `ariaProps` replaces the v0.1.0 `Slot.Root` forwarding strategy.\n * That approach silently failed for Popover-wrapped controls and ARIA groups\n * because Radix `Popover.Root` is a context-only component (no DOM node).\n */\nexport interface FieldAriaProps {\n  /** Use on native form controls so `label[htmlFor]` binds correctly. */\n  id: string;\n  /** Use on group-style controls via `aria-labelledby`. */\n  labelledBy: string;\n  \"aria-required\"?: true;\n  \"aria-invalid\"?: true;\n  \"aria-disabled\"?: true;\n  \"aria-describedby\"?: string;\n}\n\nexport interface FieldRendererArgs {\n  field: FieldDefinition;\n  value: unknown;\n  onChange: (next: unknown) => void;\n  onBlur: () => void;\n  error: string | undefined;\n  disabled: boolean;\n  readOnly: boolean;\n  /**\n   * The form's current values bag. **Freshness depends on the field's\n   * subscription mode** (resolved per-field as of v0.2.0):\n   *\n   * - **Snapshot mode** (built-in default renderers OR `dependsOn: []`):\n   *   captured via `getValues()` at render time. Stays fresh on the field's\n   *   own value changes (Controller re-renders), but DOES NOT reflect\n   *   changes to other fields between renders. If a custom renderer in\n   *   snapshot mode needs live values, subscribe via\n   *   `useJsonFormFieldValue` / `useJsonFormFieldsValue` instead of reading\n   *   from `allValues`.\n   * - **Narrow mode** (`dependsOn: ['a','b']`): live for the declared paths;\n   *   `setByPath`-rebuilt so dot-paths read correctly.\n   * - **Full-bag mode** (`dependsOn` omitted on a custom renderer): live\n   *   across the whole form. Preserves v0.1.x behavior.\n   */\n  allValues: Record<string, unknown>;\n  formApi: JsonFormHandle<Record<string, unknown>>;\n  /** v0.1.2 — ARIA bridge. See `FieldAriaProps` for usage rules. */\n  ariaProps: FieldAriaProps;\n}\n\nexport type FieldRenderer = (args: FieldRendererArgs) => ReactNode;\n\n// ─── Imperative handle ────────────────────────────────────────────────────────\n\nexport interface JsonFormHandle<TValues = Record<string, unknown>> {\n  submit: () => Promise<{ ok: boolean; values?: TValues; errors?: Record<string, string> }>;\n  reset: (values?: Partial<TValues>) => void;\n  setValue: (name: string, value: unknown) => void;\n  getValue: (name: string) => unknown;\n  getValues: () => TValues;\n  setError: (name: string, message: string) => void;\n  clearErrors: (name?: string) => void;\n  trigger: (name?: string | string[]) => Promise<boolean>;\n  focus: (name: string) => void;\n  isDirty: () => boolean;\n  isValid: () => boolean;\n  isSubmitting: () => boolean;\n}\n\n// ─── Strings dictionary ───────────────────────────────────────────────────────\n\nexport interface JsonFormStrings {\n  submit: string;\n  reset: string;\n  requiredIndicator: string;\n  optionalIndicator: string;\n  loadingOptions: string;\n  optionsError: string;\n  optionsRetry: string;\n  noOptions: string;\n  summaryHeading: string;\n  submitFailed: string;\n  errorTemplates: {\n    required: string;\n    minLength: string;\n    maxLength: string;\n    min: string;\n    max: string;\n    pattern: string;\n    email: string;\n    url: string;\n  };\n}\n\n// ─── Component props ──────────────────────────────────────────────────────────\n\nexport type ButtonVariant = \"default\" | \"outline\" | \"ghost\" | \"secondary\" | \"destructive\";\n\nexport interface JsonFormProps<TValues extends Record<string, any> = Record<string, unknown>> {\n  schema: FormSchema;\n  defaultValues?: Partial<TValues>;\n  values?: Partial<TValues>;\n\n  onSubmit: (args: JsonFormValuesArgs<TValues>) => void | Promise<void>;\n  onSubmitError?: (args: JsonFormSubmitErrorArgs<TValues>) => void;\n  /**\n   * v0.1.6 (T3.6) — fires on every submit press, BEFORE validation runs.\n   * Use for \"user attempted submit\" telemetry / submit-counter UX hints\n   * that need to fire even when validation rejects the attempt. `onSubmit`\n   * still gates on validation passing.\n   */\n  onSubmitAttempt?: (args: { formApi: JsonFormHandle<TValues> }) => void;\n  onChange?: (args: JsonFormValuesArgs<TValues>) => void;\n  onChangeDebounce?: number;\n  onValidationChange?: (args: JsonFormValidationChangeArgs) => void;\n  /**\n   * v0.1.6 (T3.7) — fires once after mount + defaults applied. Async-schema\n   * consumers (load schema → mount → wire callbacks) can ditch the manual\n   * `useEffect(...)` they wrote to capture the imperative handle.\n   */\n  onReady?: (args: { formApi: JsonFormHandle<TValues> }) => void;\n\n  columns?: 1 | 2;\n  labelPosition?: \"top\" | \"left\";\n  showSummary?: boolean;\n  summaryStrategy?: \"always\" | \"post-submit\";\n  showSchemaHeader?: boolean;\n\n  submitButton?:\n    | {\n        label?: string;\n        variant?: ButtonVariant;\n        align?: \"left\" | \"right\" | \"center\";\n        disableWhenInvalid?: boolean;\n      }\n    | ((args: { isSubmitting: boolean; isValid: boolean }) => ReactNode)\n    | false;\n  resetButton?: { label?: string; variant?: ButtonVariant };\n\n  fieldRegistry?: Record<string, FieldRenderer>;\n  renderField?: (args: { field: FieldDefinition; defaultRender: () => ReactNode }) => ReactNode;\n\n  validationMode?: \"onChange\" | \"onBlur\" | \"onTouched\" | \"onSubmit\" | \"all\";\n\n  strings?: Partial<JsonFormStrings>;\n  className?: string;\n  style?: CSSProperties;\n  ref?: Ref<JsonFormHandle<TValues>>;\n}\n\n// ─── Standalone-parts contexts ────────────────────────────────────────────────\n\nexport type JsonFormContextValue<TValues extends Record<string, any> = Record<string, any>> = JsonFormHandle<TValues> & {\n  /**\n   * Escape hatch — the raw react-hook-form return. Standalone parts read\n   * `formState`, register custom controllers, etc. through this. NOT part of\n   * the json-form stability contract: if RHF renames or relocates API\n   * surface, this type follows. Prefer the typed handle methods\n   * (`setValue` / `getValue` / `trigger` / …) for anything reachable that\n   * way.\n   */\n  rhf: UseFormReturn<TValues>;\n  schema: FormSchema;\n  zodSchema: ZodObject<Record<string, ZodTypeAny>>;\n  strings: JsonFormStrings;\n  formId: string;\n  hasSubmitted: boolean;\n  /** Merged field registry (default ⨯ consumer overrides). Exposed for standalone parts that resolve renderers. */\n  fieldRegistry: Record<string, FieldRenderer>;\n};\n\nexport interface UseJsonFormReturn<TValues extends Record<string, any>> {\n  form: UseFormReturn<TValues>;\n  zodSchema: ZodObject<Record<string, ZodTypeAny>>;\n  fieldList: ReadonlyArray<FieldDefinition>;\n  isValid: boolean;\n  isSubmitting: boolean;\n  handle: JsonFormHandle<TValues>;\n}\n\nexport interface UseJsonFormOptions<TValues extends Record<string, any>> {\n  defaultValues?: Partial<TValues>;\n  values?: Partial<TValues>;\n  validationMode?: \"onChange\" | \"onBlur\" | \"onTouched\" | \"onSubmit\" | \"all\";\n  strings?: Partial<JsonFormStrings>;\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n",
      "type": "registry:component",
      "target": "components/json-form/types.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-wrapper.tsx",
      "content": "\"use client\";\n\nimport { Suspense, useEffect, useRef, type ReactNode } from \"react\";\nimport { Controller, useFormContext, useWatch } from \"react-hook-form\";\nimport type { FieldValues, Path } from \"react-hook-form\";\nimport { cn } from \"@/lib/utils\";\nimport type { FieldAriaProps, FieldDefinition, FieldRenderer } from \"../types\";\nimport { useConditional } from \"../hooks/use-conditional\";\nimport { useJsonFormContext } from \"../json-form-context\";\nimport {\n  BUILTIN_RENDERER_TYPES_SKIPPING_ALL_VALUES,\n  defaultJsonFormRegistry,\n} from \"../lib/default-registry\";\nimport { setByPath } from \"../lib/path\";\n\n/**\n * Stable, SSR-friendly id slug for a field. Dots are escaped (HTML id can't\n * contain `.` reliably across browsers + CSS queries).\n */\nexport function fieldIdSlug(formId: string, name: string): string {\n  return `${formId}-${name.replace(/\\./g, \"_\")}`;\n}\n\nfunction widthToClass(w: FieldDefinition[\"width\"]): string | undefined {\n  if (w == null || w === \"full\") return undefined;\n  if (w === \"half\" || w === \"third\" || w === \"quarter\") return \"md:col-span-1\";\n  return undefined;\n}\n\nexport interface FieldWrapperProps {\n  field: FieldDefinition;\n  renderer: FieldRenderer;\n  formLabelPosition?: \"top\" | \"left\";\n  loadingFallback?: ReactNode;\n}\n\n/**\n * The single layout shell every field renderer flows through.\n * Owns: label, helper text, error message, ARIA wiring (via `ariaProps`),\n * visibility unmount, disabled merging, RHF Controller binding.\n *\n * v0.1.2 — ARIA is now passed through `FieldRendererArgs.ariaProps` instead\n * of via `Slot.Root` (which silently failed for Popover-wrapped controls and\n * non-form group roots).\n */\nexport function FieldWrapper({\n  field,\n  renderer,\n  formLabelPosition,\n  loadingFallback,\n}: FieldWrapperProps) {\n  const { control, register, unregister, trigger, getFieldState, getValues } =\n    useFormContext();\n  const ctx = useJsonFormContext();\n\n  // v0.2.0 (A2) — three-mode subscription gate. The default v0.1.x behavior\n  // was a full-bag `useWatch({ control })` on every field, re-rendering the\n  // whole form on every keystroke. Now resolved per-field:\n  //\n  //   • snapshot — built-in default renderer (audited not to read\n  //     `allValues`) OR explicit opt-out via `dependsOn: []`. Skip the\n  //     watch; `allValues` is a `getValues()` snapshot at render.\n  //   • narrow — `dependsOn: ['a','b']`. Subscribe only to those paths;\n  //     rebuild a path-keyed bag via `setByPath`.\n  //   • fullbag — fallback. Preserves v0.1.x behavior for custom renderers\n  //     that didn't opt in.\n  //\n  // The whitelist check resolves on the FINAL renderer identity (not just\n  // `field.type`) so a consumer-registered renderer replacing `text`\n  // correctly opts back into full-bag unless it sets `dependsOn`.\n  const dependsOn = field.dependsOn;\n  const hasDependsOn = Array.isArray(dependsOn);\n  const isBuiltinDefault =\n    BUILTIN_RENDERER_TYPES_SKIPPING_ALL_VALUES.has(field.type) &&\n    renderer === defaultJsonFormRegistry[field.type];\n  const subscriptionMode: \"snapshot\" | \"narrow\" | \"fullbag\" =\n    isBuiltinDefault || (hasDependsOn && dependsOn.length === 0)\n      ? \"snapshot\"\n      : hasDependsOn && dependsOn.length > 0\n        ? \"narrow\"\n        : \"fullbag\";\n\n  // Always call `useWatch` once with mode-conditional inputs to keep hook\n  // order stable. `disabled: true` skips the subscription in snapshot mode.\n  const watched = useWatch(\n    (subscriptionMode === \"snapshot\"\n      ? { control, disabled: true }\n      : subscriptionMode === \"narrow\"\n        ? { control, name: dependsOn as ReadonlyArray<string> }\n        : { control }) as never,\n  );\n\n  let allValues: Record<string, unknown>;\n  if (subscriptionMode === \"snapshot\") {\n    allValues = (getValues() ?? {}) as Record<string, unknown>;\n  } else if (subscriptionMode === \"narrow\") {\n    const arr = Array.isArray(watched) ? watched : [];\n    const bag: Record<string, unknown> = {};\n    const names = dependsOn as ReadonlyArray<string>;\n    for (let i = 0; i < names.length; i++) {\n      setByPath(bag, names[i], arr[i]);\n    }\n    allValues = bag;\n  } else {\n    allValues = (watched ?? {}) as Record<string, unknown>;\n  }\n\n  const { visible, enabled, required } = useConditional(field);\n\n  // Unmount-clean: when `visibleWhen` flips a field invisible and the consumer\n  // hasn't set `keepValueWhenHidden`, drop the registered key so submission\n  // strips it (C1 lock).\n  useEffect(() => {\n    if (field.type === \"hidden\") return;\n    if (!visible && !field.keepValueWhenHidden) {\n      unregister(field.name as Path<FieldValues>, { keepDefaultValue: true });\n    }\n  }, [visible, field.name, field.type, field.keepValueWhenHidden, unregister]);\n\n  // T2.6 — when `requiredWhen` flips a field's required state false→true on\n  // a field the user has already touched, trigger validation so the\n  // \"required\" error surfaces immediately instead of waiting for the next\n  // blur / submit. The first-render skip prevents pre-interaction errors.\n  const prevRequiredRef = useRef(required);\n  useEffect(() => {\n    if (!field.requiredWhen) return;\n    const prev = prevRequiredRef.current;\n    prevRequiredRef.current = required;\n    if (prev === required) return;\n    if (!required) return;\n    const state = getFieldState(field.name as Path<FieldValues>);\n    if (state.isTouched || state.isDirty) {\n      void trigger(field.name as Path<FieldValues>);\n    }\n  }, [required, field.requiredWhen, field.name, trigger, getFieldState]);\n\n  // `hidden` fields are tracked but render nothing — register imperatively so\n  // they always submit.\n  useEffect(() => {\n    if (field.type !== \"hidden\") return;\n    register(field.name as Path<FieldValues>);\n  }, [field.type, field.name, register]);\n\n  if (!visible) return null;\n  if (field.type === \"hidden\") return null;\n\n  const labelPosition = field.labelPosition ?? formLabelPosition ?? \"top\";\n  const controlId = fieldIdSlug(ctx.formId, field.name);\n  const labelId = `${controlId}-label`;\n  const descriptionId = `${controlId}-description`;\n  const errorId = `${controlId}-error`;\n  const isDisabled = !enabled || !!field.disabled;\n  const isReadOnly = !!field.readOnly;\n\n  const wrapperLayout =\n    labelPosition === \"left\"\n      ? \"grid grid-cols-[10rem_1fr] items-start gap-x-3 gap-y-1\"\n      : \"flex flex-col gap-1.5\";\n\n  return (\n    <Controller\n      control={control}\n      name={field.name as Path<FieldValues>}\n      render={({ field: controller, fieldState }) => {\n        const errorMessage = fieldState.error?.message\n          ? String(fieldState.error.message)\n          : undefined;\n\n        const describedBy = buildDescribedBy(\n          field.description ? descriptionId : undefined,\n          errorMessage ? errorId : undefined,\n        );\n\n        const ariaProps: FieldAriaProps = {\n          id: controlId,\n          labelledBy: labelId,\n          \"aria-required\": required ? true : undefined,\n          \"aria-invalid\": errorMessage ? true : undefined,\n          \"aria-disabled\": isDisabled ? true : undefined,\n          \"aria-describedby\": describedBy,\n        };\n\n        const rendered = renderer({\n          field,\n          value: controller.value,\n          onChange: (next: unknown) => controller.onChange(next),\n          onBlur: () => controller.onBlur(),\n          error: errorMessage,\n          disabled: isDisabled,\n          readOnly: isReadOnly,\n          allValues,\n          formApi: ctx,\n          ariaProps,\n        });\n\n        return (\n          <div\n            data-jsonform-field={field.name}\n            data-jsonform-type={field.type}\n            data-disabled={isDisabled ? \"true\" : undefined}\n            className={cn(\n              \"group/field\",\n              wrapperLayout,\n              widthToClass(field.width),\n            )}\n          >\n            {field.label ? (\n              <label\n                id={labelId}\n                htmlFor={controlId}\n                className={cn(\n                  \"text-sm font-medium text-foreground\",\n                  labelPosition === \"left\" ? \"pt-1.5\" : undefined,\n                  isDisabled ? \"opacity-60\" : undefined,\n                )}\n              >\n                {field.label}\n                {required ? (\n                  <span aria-hidden=\"true\" className=\"ml-0.5 text-destructive\">\n                    {ctx.strings.requiredIndicator}\n                  </span>\n                ) : null}\n              </label>\n            ) : null}\n\n            <div\n              className={cn(\n                \"flex flex-col gap-1\",\n                labelPosition === \"left\" ? \"min-w-0\" : undefined,\n              )}\n            >\n              <Suspense\n                fallback={\n                  loadingFallback ?? (\n                    <div className=\"h-9 w-full animate-pulse rounded-md bg-muted/40\" />\n                  )\n                }\n              >\n                {rendered}\n              </Suspense>\n\n              {field.description ? (\n                <p id={descriptionId} className=\"text-xs text-muted-foreground\">\n                  {field.description}\n                </p>\n              ) : null}\n\n              {errorMessage ? (\n                <p\n                  id={errorId}\n                  role=\"alert\"\n                  className=\"text-xs text-destructive\"\n                >\n                  {errorMessage}\n                </p>\n              ) : null}\n            </div>\n          </div>\n        );\n      }}\n    />\n  );\n}\n\nfunction buildDescribedBy(\n  descriptionId: string | undefined,\n  errorId: string | undefined,\n): string | undefined {\n  const ids: string[] = [];\n  if (descriptionId) ids.push(descriptionId);\n  if (errorId) ids.push(errorId);\n  return ids.length ? ids.join(\" \") : undefined;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-wrapper.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-text.tsx",
      "content": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport type { FieldRenderer } from \"../types\";\n\nconst TYPE_TO_INPUT_TYPE: Record<string, string> = {\n  text: \"text\",\n  email: \"email\",\n  password: \"password\",\n  url: \"url\",\n  tel: \"tel\",\n  number: \"number\",\n};\n\nexport const FieldText: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  readOnly,\n  ariaProps,\n}) => {\n  const inputType = TYPE_TO_INPUT_TYPE[field.type] ?? \"text\";\n  const isNumberField = field.type === \"number\";\n  return (\n    <Input\n      id={ariaProps.id}\n      type={inputType}\n      value={value == null ? \"\" : String(value)}\n      onChange={(e) => {\n        const raw = e.target.value;\n        // T2.5 — for `number` fields, surface an empty input as `undefined`\n        // rather than `\"\"`. `z.coerce.number()` turns `\"\"` into `0`, which\n        // produces surprising \"value is 0 but the box is empty\" UX. With\n        // `undefined`, optional fields validate cleanly and required\n        // fields surface the right \"this field is required\" error.\n        if (isNumberField && raw === \"\") {\n          onChange(undefined);\n          return;\n        }\n        onChange(raw);\n      }}\n      onBlur={onBlur}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      readOnly={readOnly}\n      autoComplete={field.autoComplete}\n      autoFocus={field.autoFocus}\n      min={field.min}\n      max={field.max}\n      step={field.step}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n    />\n  );\n};\n\nexport default FieldText;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-text.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-textarea.tsx",
      "content": "\"use client\";\n\nimport { Textarea } from \"@/components/ui/textarea\";\nimport type { FieldRenderer } from \"../types\";\n\nexport const FieldTextarea: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  readOnly,\n  ariaProps,\n}) => {\n  return (\n    <Textarea\n      id={ariaProps.id}\n      value={value == null ? \"\" : String(value)}\n      onChange={(e) => onChange(e.target.value)}\n      onBlur={onBlur}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      readOnly={readOnly}\n      rows={field.rows ?? 4}\n      autoComplete={field.autoComplete}\n      autoFocus={field.autoFocus}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n    />\n  );\n};\n\nexport default FieldTextarea;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-textarea.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-select.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronDownIcon, XIcon } from \"lucide-react\";\nimport type { FieldAriaProps, FieldOption, FieldRenderer } from \"../types\";\nimport { useAsyncOptions } from \"../hooks/use-async-options\";\nimport { useJsonFormContext } from \"../json-form-context\";\n\n/**\n * Combined renderer for `select` + `multi-select`. Static-option `select`\n * uses shadcn `Select`; `searchable: true` OR `multi-select` switches to a\n * `Command + Popover` combobox.\n */\nexport const FieldSelect: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const isMulti = field.type === \"multi-select\";\n  const useCombobox = isMulti || !!field.searchable;\n  const { options, loading, error, retry, setQuery } = useAsyncOptions(field);\n  const ctx = useJsonFormContext();\n\n  if (!useCombobox) {\n    return (\n      <Select\n        value={value == null ? \"\" : String(value)}\n        onValueChange={(v) => onChange(v == null ? null : coerceOptionValue(v, options))}\n        disabled={disabled}\n      >\n        <SelectTrigger\n          id={ariaProps.id}\n          className=\"w-full\"\n          onBlur={onBlur}\n          aria-required={ariaProps[\"aria-required\"]}\n          aria-invalid={ariaProps[\"aria-invalid\"]}\n          aria-disabled={ariaProps[\"aria-disabled\"]}\n          aria-describedby={ariaProps[\"aria-describedby\"]}\n        >\n          <SelectValue placeholder={field.placeholder ?? \"Select…\"} />\n        </SelectTrigger>\n        <SelectContent>\n          {loading ? (\n            <div className=\"px-2 py-1 text-xs text-muted-foreground\">\n              {ctx.strings.loadingOptions}\n            </div>\n          ) : null}\n          {error ? (\n            <div className=\"flex items-center justify-between px-2 py-1 text-xs text-destructive\">\n              <span>{ctx.strings.optionsError}</span>\n              <Button size=\"xs\" variant=\"ghost\" onClick={retry}>\n                {ctx.strings.optionsRetry}\n              </Button>\n            </div>\n          ) : null}\n          {!loading && options.length === 0 && !error ? (\n            <div className=\"px-2 py-1 text-xs text-muted-foreground\">\n              {ctx.strings.noOptions}\n            </div>\n          ) : null}\n          {options.map((opt) => (\n            <SelectItem\n              key={String(opt.value)}\n              value={String(opt.value)}\n              disabled={opt.disabled}\n            >\n              {opt.label}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    );\n  }\n\n  return (\n    <ComboboxSelect\n      isMulti={isMulti}\n      value={value}\n      onChange={onChange}\n      onBlur={onBlur}\n      disabled={disabled}\n      options={options}\n      loading={loading}\n      error={error}\n      retry={retry}\n      setQuery={setQuery}\n      placeholder={field.placeholder}\n      ariaProps={ariaProps}\n    />\n  );\n};\n\ninterface ComboboxProps {\n  isMulti: boolean;\n  value: unknown;\n  onChange: (next: unknown) => void;\n  onBlur: () => void;\n  disabled: boolean;\n  options: FieldOption[];\n  loading: boolean;\n  error: string | null;\n  retry: () => void;\n  setQuery: (q: string) => void;\n  placeholder?: string;\n  ariaProps: FieldAriaProps;\n}\n\nfunction ComboboxSelect({\n  isMulti,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  options,\n  loading,\n  error,\n  retry,\n  setQuery,\n  placeholder,\n  ariaProps,\n}: ComboboxProps) {\n  const ctx = useJsonFormContext();\n  const [open, setOpen] = useState(false);\n  const selected = normalizeSelected(value, isMulti);\n\n  function toggle(opt: FieldOption) {\n    if (isMulti) {\n      const next = selected.includes(opt.value)\n        ? selected.filter((v) => v !== opt.value)\n        : [...selected, opt.value];\n      onChange(next);\n      return;\n    }\n    onChange(opt.value);\n    setOpen(false);\n  }\n\n  function removeChip(v: unknown, e: React.MouseEvent) {\n    e.stopPropagation();\n    if (!isMulti) return;\n    onChange(selected.filter((x) => x !== v));\n  }\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (!o) onBlur();\n      }}\n    >\n      {/* F-cross-13: no `asChild` — render the trigger as the combobox button\n          directly (native props only; buttonVariants supplies the styling). */}\n      <PopoverTrigger\n        id={ariaProps.id}\n        type=\"button\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-required={ariaProps[\"aria-required\"]}\n        aria-invalid={ariaProps[\"aria-invalid\"]}\n        aria-disabled={ariaProps[\"aria-disabled\"]}\n        aria-describedby={ariaProps[\"aria-describedby\"]}\n        disabled={disabled}\n        className={cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"h-auto min-h-9 w-full justify-between gap-1.5 px-2.5 py-1.5 font-normal\",\n        )}\n      >\n        <span className=\"flex flex-1 flex-wrap items-center gap-1\">\n          {selected.length === 0 ? (\n            <span className=\"text-muted-foreground\">\n              {placeholder ?? \"Select…\"}\n            </span>\n          ) : isMulti ? (\n            selected.map((v) => {\n              const opt = options.find((o) => o.value === v);\n              return (\n                <Badge\n                  key={String(v)}\n                  variant=\"secondary\"\n                  className={cn(\"gap-1 pr-1\")}\n                >\n                  <span>{opt?.label ?? String(v)}</span>\n                  <span\n                    role=\"button\"\n                    aria-label={`Remove ${opt?.label ?? String(v)}`}\n                    tabIndex={0}\n                    onClick={(e) => removeChip(v, e)}\n                    onKeyDown={(e) => {\n                      if (e.key === \"Enter\" || e.key === \" \") {\n                        e.preventDefault();\n                        e.stopPropagation();\n                        onChange(selected.filter((x) => x !== v));\n                      }\n                    }}\n                    className=\"inline-flex size-3.5 cursor-pointer items-center justify-center rounded hover:bg-foreground/10\"\n                  >\n                    <XIcon className=\"size-3\" />\n                  </span>\n                </Badge>\n              );\n            })\n          ) : (\n            <span>\n              {options.find((o) => o.value === selected[0])?.label ??\n                String(selected[0])}\n            </span>\n          )}\n        </span>\n        <ChevronDownIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n      </PopoverTrigger>\n      {/* Radix: --radix-popover-trigger-width · Base UI: --anchor-width (F-cross-13) */}\n      <PopoverContent className=\"w-(--radix-popover-trigger-width,var(--anchor-width)) p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search…\"\n            onValueChange={setQuery}\n          />\n          <CommandList>\n            {loading ? (\n              <div className=\"px-3 py-2 text-xs text-muted-foreground\">\n                {ctx.strings.loadingOptions}\n              </div>\n            ) : null}\n            {error ? (\n              <div className=\"flex items-center justify-between px-3 py-2 text-xs text-destructive\">\n                <span>{ctx.strings.optionsError}</span>\n                <Button size=\"xs\" variant=\"ghost\" onClick={retry}>\n                  {ctx.strings.optionsRetry}\n                </Button>\n              </div>\n            ) : null}\n            {!loading && options.length === 0 && !error ? (\n              <CommandEmpty>{ctx.strings.noOptions}</CommandEmpty>\n            ) : null}\n            <CommandGroup>\n              {options.map((opt) => {\n                const isSel = selected.includes(opt.value);\n                return (\n                  <CommandItem\n                    key={String(opt.value)}\n                    value={String(opt.label)}\n                    disabled={opt.disabled}\n                    data-checked={isSel || undefined}\n                    onSelect={() => toggle(opt)}\n                  >\n                    <span>{opt.label}</span>\n                    {opt.description ? (\n                      <span className=\"ml-auto text-xs text-muted-foreground\">\n                        {opt.description}\n                      </span>\n                    ) : null}\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction normalizeSelected(value: unknown, isMulti: boolean): unknown[] {\n  if (isMulti) {\n    if (Array.isArray(value)) return value;\n    return value == null ? [] : [value];\n  }\n  if (value == null || value === \"\") return [];\n  return [value];\n}\n\n/**\n * Preserve non-string option.value types through the shadcn Select string\n * round-trip. shadcn Select only accepts string values; if the consumer's\n * option had `{ value: 42, label: \"...\" }`, we'd otherwise submit `\"42\"`\n * instead of `42`. Look up the original `option.value` by stringified key.\n */\nfunction coerceOptionValue(raw: string, options: FieldOption[]): unknown {\n  const match = options.find((o) => String(o.value) === raw);\n  return match ? match.value : raw;\n}\n\nexport default FieldSelect;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-select.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-radio-group.tsx",
      "content": "\"use client\";\n\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Label } from \"@/components/ui/label\";\nimport type { FieldOption, FieldRenderer } from \"../types\";\nimport { useAsyncOptions } from \"../hooks/use-async-options\";\nimport { fieldIdSlug } from \"./field-wrapper\";\nimport { useJsonFormContext } from \"../json-form-context\";\n\nexport const FieldRadioGroup: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const ctx = useJsonFormContext();\n  const { options } = useAsyncOptions(field);\n  const baseId = fieldIdSlug(ctx.formId, field.name);\n\n  return (\n    <RadioGroup\n      value={value == null ? \"\" : String(value)}\n      onValueChange={(v) => onChange(coerceOptionValue(v, options))}\n      disabled={disabled}\n      onBlur={onBlur}\n      aria-labelledby={ariaProps.labelledBy}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      className=\"gap-1.5\"\n    >\n      {options.map((opt) => {\n        const optId = `${baseId}-opt-${String(opt.value).replace(/[^a-z0-9_-]/gi, \"_\")}`;\n        return (\n          <div key={String(opt.value)} className=\"flex items-start gap-2\">\n            <RadioGroupItem\n              id={optId}\n              value={String(opt.value)}\n              disabled={opt.disabled}\n            />\n            <div className=\"flex flex-col\">\n              <Label htmlFor={optId} className=\"font-normal\">\n                {opt.label}\n              </Label>\n              {opt.description ? (\n                <span className=\"text-xs text-muted-foreground\">\n                  {opt.description}\n                </span>\n              ) : null}\n            </div>\n          </div>\n        );\n      })}\n    </RadioGroup>\n  );\n};\n\nfunction coerceOptionValue(raw: string, options: FieldOption[]): unknown {\n  // Preserve non-string option.value types by looking up the original.\n  const match = options.find((o) => String(o.value) === raw);\n  return match ? match.value : raw;\n}\n\nexport default FieldRadioGroup;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-radio-group.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-checkbox.tsx",
      "content": "\"use client\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport type { FieldRenderer } from \"../types\";\n\nexport const FieldCheckbox: FieldRenderer = ({\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  return (\n    <Checkbox\n      id={ariaProps.id}\n      checked={value === true}\n      onCheckedChange={(c) => onChange(c === true)}\n      onBlur={onBlur}\n      disabled={disabled}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n    />\n  );\n};\n\nexport default FieldCheckbox;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-checkbox.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-checkbox-group.tsx",
      "content": "\"use client\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\nimport type { FieldRenderer } from \"../types\";\nimport { useAsyncOptions } from \"../hooks/use-async-options\";\nimport { fieldIdSlug } from \"./field-wrapper\";\nimport { useJsonFormContext } from \"../json-form-context\";\n\nexport const FieldCheckboxGroup: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const ctx = useJsonFormContext();\n  const { options } = useAsyncOptions(field);\n  const selected = Array.isArray(value) ? value : [];\n  const baseId = fieldIdSlug(ctx.formId, field.name);\n\n  function toggle(optValue: unknown, checked: boolean) {\n    if (checked) {\n      if (!selected.includes(optValue)) onChange([...selected, optValue]);\n    } else {\n      onChange(selected.filter((v) => v !== optValue));\n    }\n  }\n\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={ariaProps.labelledBy}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      className=\"flex flex-col gap-1.5\"\n      onBlur={onBlur}\n    >\n      {options.map((opt) => {\n        const optId = `${baseId}-opt-${String(opt.value).replace(/[^a-z0-9_-]/gi, \"_\")}`;\n        const isChecked = selected.includes(opt.value);\n        return (\n          <div key={String(opt.value)} className=\"flex items-start gap-2\">\n            <Checkbox\n              id={optId}\n              checked={isChecked}\n              onCheckedChange={(c) => toggle(opt.value, c === true)}\n              disabled={disabled || opt.disabled}\n            />\n            <div className=\"flex flex-col\">\n              <Label htmlFor={optId} className=\"font-normal\">\n                {opt.label}\n              </Label>\n              {opt.description ? (\n                <span className=\"text-xs text-muted-foreground\">\n                  {opt.description}\n                </span>\n              ) : null}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default FieldCheckboxGroup;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-checkbox-group.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-switch.tsx",
      "content": "\"use client\";\n\nimport { Switch } from \"@/components/ui/switch\";\nimport type { FieldRenderer } from \"../types\";\n\nexport const FieldSwitch: FieldRenderer = ({\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  return (\n    <Switch\n      id={ariaProps.id}\n      checked={value === true}\n      onCheckedChange={(c) => onChange(c === true)}\n      onBlur={onBlur}\n      disabled={disabled}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n    />\n  );\n};\n\nexport default FieldSwitch;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-switch.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-date.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Input } from \"@/components/ui/input\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { CalendarIcon } from \"lucide-react\";\nimport type { FieldAriaProps, FieldRenderer } from \"../types\";\n\n/**\n * Combined renderer for date / date-range / time / datetime.\n * Values are stored as ISO 8601 strings (YYYY-MM-DD for date, HH:mm for time,\n * full ISO for datetime). date-range stores `{ start, end }`.\n */\nexport const FieldDate: FieldRenderer = (args) => {\n  if (args.field.type === \"time\") return <FieldTime {...args} />;\n  if (args.field.type === \"date-range\") return <FieldDateRange {...args} />;\n  if (args.field.type === \"datetime\") return <FieldDateTime {...args} />;\n  return <FieldDateSingle {...args} />;\n};\n\nfunction formatDate(d: Date): string {\n  const y = d.getFullYear();\n  const m = String(d.getMonth() + 1).padStart(2, \"0\");\n  const day = String(d.getDate()).padStart(2, \"0\");\n  return `${y}-${m}-${day}`;\n}\n\nfunction parseDate(s: unknown): Date | undefined {\n  if (typeof s !== \"string\" || !s) return undefined;\n  const d = new Date(s);\n  return Number.isNaN(d.getTime()) ? undefined : d;\n}\n\nfunction ariaAttrs(p: FieldAriaProps, withId: boolean) {\n  return {\n    ...(withId ? { id: p.id } : {}),\n    \"aria-required\": p[\"aria-required\"],\n    \"aria-invalid\": p[\"aria-invalid\"],\n    \"aria-disabled\": p[\"aria-disabled\"],\n    \"aria-describedby\": p[\"aria-describedby\"],\n  };\n}\n\nconst FieldDateSingle: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const [open, setOpen] = useState(false);\n  const parsed = parseDate(value);\n  const display = parsed ? formatDate(parsed) : \"\";\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (!o) onBlur();\n      }}\n    >\n      {/* F-cross-13: no `asChild` — render the trigger as the button directly\n          (both Radix + Base UI pass native props through to a <button>). */}\n      <PopoverTrigger\n        type=\"button\"\n        disabled={disabled}\n        className={cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"w-full justify-start gap-2 font-normal\",\n          !display && \"text-muted-foreground\",\n        )}\n        {...ariaAttrs(ariaProps, true)}\n      >\n        <CalendarIcon className=\"size-4\" />\n        {display || field.placeholder || \"Pick a date\"}\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\">\n        <Calendar\n          mode=\"single\"\n          selected={parsed}\n          onSelect={(d) => {\n            onChange(d ? formatDate(d) : \"\");\n            setOpen(false);\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst FieldTime: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  readOnly,\n  ariaProps,\n}) => {\n  return (\n    <Input\n      type=\"time\"\n      value={typeof value === \"string\" ? value : \"\"}\n      onChange={(e) => onChange(e.target.value)}\n      onBlur={onBlur}\n      disabled={disabled}\n      readOnly={readOnly}\n      step={field.step}\n      {...ariaAttrs(ariaProps, true)}\n    />\n  );\n};\n\nconst FieldDateTime: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const [open, setOpen] = useState(false);\n  const iso = typeof value === \"string\" ? value : \"\";\n  const datePart = iso.slice(0, 10);\n  const timePart = iso.length >= 16 ? iso.slice(11, 16) : \"00:00\";\n  const parsed = parseDate(datePart);\n  const display = iso ? `${datePart} ${timePart}` : \"\";\n\n  function update(nextDate: Date | undefined, nextTime: string) {\n    if (!nextDate) {\n      onChange(\"\");\n      return;\n    }\n    const d = formatDate(nextDate);\n    onChange(`${d}T${nextTime}`);\n  }\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (!o) onBlur();\n      }}\n    >\n      <PopoverTrigger\n        type=\"button\"\n        disabled={disabled}\n        className={cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"w-full justify-start gap-2 font-normal\",\n          !display && \"text-muted-foreground\",\n        )}\n        {...ariaAttrs(ariaProps, true)}\n      >\n        <CalendarIcon className=\"size-4\" />\n        {display || field.placeholder || \"Pick a date & time\"}\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto space-y-2 p-2\">\n        <Calendar\n          mode=\"single\"\n          selected={parsed}\n          onSelect={(d) => update(d, timePart || \"00:00\")}\n        />\n        <Input\n          type=\"time\"\n          value={timePart}\n          onChange={(e) => update(parsed, e.target.value)}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst FieldDateRange: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const [open, setOpen] = useState(false);\n  const range = isRange(value)\n    ? { from: parseDate(value.start), to: parseDate(value.end) }\n    : { from: undefined as Date | undefined, to: undefined as Date | undefined };\n\n  const display =\n    range.from && range.to\n      ? `${formatDate(range.from)} → ${formatDate(range.to)}`\n      : range.from\n        ? `${formatDate(range.from)} → …`\n        : \"\";\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(o) => {\n        setOpen(o);\n        if (!o) onBlur();\n      }}\n    >\n      <PopoverTrigger\n        type=\"button\"\n        disabled={disabled}\n        className={cn(\n          buttonVariants({ variant: \"outline\" }),\n          \"w-full justify-start gap-2 font-normal\",\n          !display && \"text-muted-foreground\",\n        )}\n        {...ariaAttrs(ariaProps, true)}\n      >\n        <CalendarIcon className=\"size-4\" />\n        {display || field.placeholder || \"Pick a date range\"}\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\">\n        <Calendar\n          mode=\"range\"\n          selected={range}\n          onSelect={(r) => {\n            onChange({\n              start: r?.from ? formatDate(r.from) : \"\",\n              end: r?.to ? formatDate(r.to) : \"\",\n            });\n            if (r?.from && r?.to) setOpen(false);\n          }}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nfunction isRange(v: unknown): v is { start: string; end: string } {\n  return (\n    !!v &&\n    typeof v === \"object\" &&\n    \"start\" in v &&\n    \"end\" in v\n  );\n}\n\nexport default FieldDate;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-date.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-code.tsx",
      "content": "\"use client\";\n\n// Import from the specific file (not the barrel) so shadcn's path rewriter\n// produces a consumer-side path that resolves. The barrel index.ts re-export\n// works here by accident — code-block.tsx happens to export `CodeBlock` —\n// but the explicit file path is the safe convention. See field-richtext.tsx.\nimport { CodeBlock } from \"@/registry/components/code/code-block/code-block\";\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * `code` field renderer — wraps `@ilinxa/code-block` in `mode='edit'`.\n *\n * Loaded lazily by `default-registry.ts`\n * (`React.lazy(() => import('../parts/field-code'))`) so the CodeMirror\n * chunk only ships when a form actually contains a `code` field.\n *\n * Therefore this MUST `export default`.\n *\n * ARIA: `code-block` doesn't expose `id` (CodeMirror takes over the\n * contenteditable element). Wrap in a `<div role=\"group\">` with\n * `aria-labelledby` so the wrapper's label still announces correctly.\n */\nconst FieldCode: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  disabled,\n  readOnly,\n  ariaProps,\n}) => {\n  const cfg = field.config?.code;\n  const codeReadOnly = readOnly || disabled || cfg?.readOnly;\n\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={ariaProps.labelledBy}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      data-aria-required={ariaProps[\"aria-required\"] ? \"true\" : undefined}\n      data-aria-invalid={ariaProps[\"aria-invalid\"] ? \"true\" : undefined}\n    >\n      <CodeBlock\n        mode=\"edit\"\n        value={typeof value === \"string\" ? value : \"\"}\n        onChange={(args) => onChange(args.value)}\n        readOnly={codeReadOnly}\n        lang={field.lang}\n        emptyMessage={field.placeholder}\n        showLineNumbers\n        showCopy\n        ariaLabel={field.label ?? field.name}\n      />\n    </div>\n  );\n};\n\nexport default FieldCode;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-code.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-richtext.tsx",
      "content": "\"use client\";\n\n// Import everything from `rich-text-editor.tsx` (not `./types`, not the\n// barrel `./index`). shadcn 4.6.0's path rewriter handles cross-procomp\n// component-file paths correctly but mangles `/types` and `/index` to the\n// CURRENT slug (F-S1 cross-procomp `/types` bug), which then breaks\n// consumer-tsc. `rich-text-editor.tsx` re-exports the symbols we need at\n// its tail, so all imports land on a path the rewriter preserves.\nimport {\n  RichTextEditor,\n  RICH_TEXT_EMPTY_VALUE,\n  type RichTextValue,\n} from \"@/registry/components/data/rich-text-editor/rich-text-editor\";\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * `richtext` field renderer — wraps `@ilinxa/rich-text-editor`'s\n * `<RichTextEditor>` in a Plate-based WYSIWYG.\n *\n * Loaded lazily by `default-registry.ts` so the Plate bundle (~165KB gzip)\n * only ships when a form actually contains a `richtext` field. Therefore\n * this MUST `export default`.\n *\n * Submitted value: `RichTextValue` (Plate JSON — `{ type, children }[]`).\n * Serialize with `serializeRichTextToHtml(value)` from\n * `@ilinxa/rich-text-editor` if you need HTML at an export boundary\n * (RSS / email / OG tags).\n *\n * ARIA: `RichTextEditor` doesn't expose `id` (Plate manages a\n * contenteditable internally). Wrap in a `role=\"group\"` with\n * `aria-labelledby` so the wrapper's label binds.\n */\nconst FieldRichtext: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  disabled,\n  readOnly,\n  ariaProps,\n}) => {\n  const cfg = field.config?.richText;\n  const safe = isRichtextValue(value) ? value : RICH_TEXT_EMPTY_VALUE;\n\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={ariaProps.labelledBy}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      data-aria-required={ariaProps[\"aria-required\"] ? \"true\" : undefined}\n      data-aria-invalid={ariaProps[\"aria-invalid\"] ? \"true\" : undefined}\n    >\n      <RichTextEditor\n        value={safe}\n        onChange={(next) => onChange(next)}\n        readOnly={readOnly || disabled}\n        placeholder={field.placeholder}\n        hideToolbar={cfg?.hideToolbar}\n        autoFocus={cfg?.autoFocus ?? field.autoFocus}\n      />\n    </div>\n  );\n};\n\nfunction isRichtextValue(v: unknown): v is RichTextValue {\n  return Array.isArray(v);\n}\n\nexport default FieldRichtext;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-richtext.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-slider.tsx",
      "content": "\"use client\";\n\nimport { Slider } from \"@/components/ui/slider\";\nimport type { FieldRenderer } from \"../types\";\n\nexport const FieldSlider: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const min = field.min ?? 0;\n  const max = field.max ?? 100;\n  const step = field.step ?? 1;\n  const numeric = typeof value === \"number\" ? value : Number(value);\n  const safe = Number.isFinite(numeric) ? numeric : min;\n\n  return (\n    <div className=\"flex w-full items-center gap-3\" onBlur={onBlur}>\n      <Slider\n        id={ariaProps.id}\n        value={[safe]}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        onValueChange={(v) => onChange(firstNumber(v, min))}\n        aria-labelledby={ariaProps.labelledBy}\n        aria-required={ariaProps[\"aria-required\"]}\n        aria-invalid={ariaProps[\"aria-invalid\"]}\n        aria-disabled={ariaProps[\"aria-disabled\"]}\n        aria-describedby={ariaProps[\"aria-describedby\"]}\n        className=\"flex-1\"\n      />\n      <span className=\"w-10 text-right text-xs tabular-nums text-muted-foreground\">\n        {safe}\n      </span>\n    </div>\n  );\n};\n\n/** Slider's onValueChange emits `number | readonly number[]` depending on\n *  the installed shadcn version. Normalize to a single number. */\nfunction firstNumber(v: number | readonly number[], fallback: number): number {\n  if (typeof v === \"number\") return v;\n  if (Array.isArray(v) && v.length > 0) return v[0];\n  return fallback;\n}\n\nexport default FieldSlider;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-slider.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-rating.tsx",
      "content": "\"use client\";\n\nimport { useRef, type KeyboardEvent } from \"react\";\nimport { StarIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { FieldRenderer } from \"../types\";\n\nexport const FieldRating: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const stars = field.config?.rating?.stars ?? 5;\n  const current = typeof value === \"number\" ? value : Number(value) || 0;\n  const groupRef = useRef<HTMLDivElement | null>(null);\n\n  function setValue(n: number) {\n    onChange(Math.max(0, Math.min(stars, n)));\n  }\n\n  function handleKey(e: KeyboardEvent<HTMLDivElement>) {\n    if (disabled) return;\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setValue(current + 1);\n      return;\n    }\n    if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setValue(current - 1);\n      return;\n    }\n    if (e.key === \"Home\") {\n      e.preventDefault();\n      setValue(1);\n      return;\n    }\n    if (e.key === \"End\") {\n      e.preventDefault();\n      setValue(stars);\n      return;\n    }\n    const n = Number(e.key);\n    if (Number.isFinite(n) && n >= 1 && n <= stars) {\n      e.preventDefault();\n      setValue(n);\n    }\n  }\n\n  return (\n    <div\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-labelledby={ariaProps.labelledBy}\n      aria-required={ariaProps[\"aria-required\"]}\n      aria-invalid={ariaProps[\"aria-invalid\"]}\n      aria-disabled={ariaProps[\"aria-disabled\"]}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      tabIndex={disabled ? -1 : 0}\n      onKeyDown={handleKey}\n      onBlur={onBlur}\n      className={cn(\n        \"inline-flex w-fit items-center gap-1 rounded-md p-0.5 outline-none ring-ring/50 focus-visible:ring-3\",\n        disabled && \"opacity-60\",\n      )}\n    >\n      {Array.from({ length: stars }, (_, i) => {\n        const n = i + 1;\n        const filled = n <= current;\n        return (\n          <button\n            key={n}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={n === current}\n            aria-label={`${n} star${n === 1 ? \"\" : \"s\"}`}\n            disabled={disabled}\n            onClick={() => setValue(n)}\n            tabIndex={-1}\n            className={cn(\n              \"p-0.5 transition-colors\",\n              filled ? \"text-primary\" : \"text-muted-foreground/40\",\n              !disabled && \"hover:text-primary\",\n            )}\n          >\n            <StarIcon\n              className=\"size-5\"\n              fill={filled ? \"currentColor\" : \"none\"}\n            />\n          </button>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default FieldRating;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-rating.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-computed.tsx",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport type { FieldRenderer } from \"../types\";\nimport { useComputed } from \"../hooks/use-computed\";\n\nexport const FieldComputed: FieldRenderer = ({\n  field,\n  value,\n  onChange,\n  onBlur,\n  disabled,\n  ariaProps,\n}) => {\n  const computed = useComputed(field);\n  const display = computed == null ? \"\" : String(computed);\n\n  // Push computed value into RHF state so consumers can read it via\n  // `formApi.getValues()`. Skipped in editable mode (consumer types over).\n  useEffect(() => {\n    if (field.editable) return;\n    if (value !== computed) onChange(computed);\n  }, [computed, field.editable, value, onChange]);\n\n  if (field.editable) {\n    return (\n      <Input\n        id={ariaProps.id}\n        type=\"text\"\n        value={typeof value === \"string\" ? value : display}\n        onChange={(e) => onChange(e.target.value)}\n        onBlur={onBlur}\n        disabled={disabled}\n        placeholder={field.placeholder}\n        aria-required={ariaProps[\"aria-required\"]}\n        aria-invalid={ariaProps[\"aria-invalid\"]}\n        aria-disabled={ariaProps[\"aria-disabled\"]}\n        aria-describedby={ariaProps[\"aria-describedby\"]}\n      />\n    );\n  }\n\n  return (\n    <output\n      id={ariaProps.id}\n      aria-labelledby={ariaProps.labelledBy}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      data-jsonform-computed\n      className=\"inline-flex h-8 w-full items-center rounded-lg border border-dashed border-input bg-muted/30 px-2.5 text-sm text-muted-foreground\"\n    >\n      {display || (\n        <span className=\"text-muted-foreground/60\">—</span>\n      )}\n    </output>\n  );\n};\n\nexport default FieldComputed;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-computed.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-hidden.tsx",
      "content": "\"use client\";\n\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * Hidden field — renders `null`. Registration + value tracking happens in\n * `FieldWrapper`'s effect; FieldWrapper returns null for `type: 'hidden'`\n * before this renderer is reached. This renderer exists for completeness\n * (and consumer registry lookups) but is effectively unused at runtime.\n */\nexport const FieldHidden: FieldRenderer = () => null;\n\nexport default FieldHidden;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-hidden.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-section.tsx",
      "content": "\"use client\";\n\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * `section` — renders `<fieldset>` chrome via the wrapper. Returns null here\n * because section is handled at the form layout level (it groups subsequent\n * fields visually, it doesn't render an input). The resolver in `<JsonForm>`\n * routes `section`/`divider` types through dedicated layout primitives,\n * bypassing this renderer. Kept in the registry for completeness so the\n * fallback path doesn't trip.\n */\nexport const FieldSection: FieldRenderer = () => null;\n\nexport default FieldSection;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-section.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-divider.tsx",
      "content": "\"use client\";\n\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * `divider` — see `field-section.tsx`. Routed through the layout primitive\n * in `<JsonForm>`. This renderer is kept for symmetry; never invoked at\n * runtime under normal flow.\n */\nexport const FieldDivider: FieldRenderer = () => null;\n\nexport default FieldDivider;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-divider.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/field-fallback.tsx",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { FieldRenderer } from \"../types\";\n\n/**\n * Fallback renderer for unknown field types. Renders an inline error\n * placeholder and warns once per mount in dev.\n */\nexport const FieldFallback: FieldRenderer = ({ field, ariaProps }) => {\n  useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return;\n    console.warn(\n      `[json-form] No renderer registered for field type \"${field.type}\" ` +\n        `(field \"${field.name}\"). Register one via the \\`fieldRegistry\\` prop ` +\n        `or use one of the built-in types.`,\n    );\n  }, [field.type, field.name]);\n\n  return (\n    <div\n      id={ariaProps.id}\n      role=\"alert\"\n      aria-labelledby={ariaProps.labelledBy}\n      aria-describedby={ariaProps[\"aria-describedby\"]}\n      className=\"inline-flex w-full items-center gap-2 rounded-md border border-dashed border-destructive/40 bg-destructive/5 px-2.5 py-1.5 text-xs text-destructive\"\n    >\n      <span aria-hidden>⚠</span>\n      <span>\n        Unknown field type <code className=\"font-mono\">{field.type}</code>\n      </span>\n    </div>\n  );\n};\n\nexport default FieldFallback;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/field-fallback.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/form-header.tsx",
      "content": "\"use client\";\n\nimport { useJsonFormContext } from \"../json-form-context\";\n\n/**\n * Renders the form's `meta.title` + `meta.description` chrome at the top of\n * the form. Hidden by default if `meta` is absent or `showSchemaHeader` is\n * `false` (handled by `<JsonForm>`). Exported as a standalone atom so\n * consumers building fully-headless layouts can place it themselves.\n */\nexport function FormHeader() {\n  const ctx = useJsonFormContext();\n  const meta = ctx.schema.meta;\n  if (!meta?.title && !meta?.description) return null;\n\n  return (\n    <header className=\"flex flex-col gap-1 pb-2\">\n      {meta.title ? (\n        <h2 className=\"text-base font-semibold text-foreground\">\n          {meta.title}\n        </h2>\n      ) : null}\n      {meta.description ? (\n        <p className=\"text-sm text-muted-foreground\">{meta.description}</p>\n      ) : null}\n    </header>\n  );\n}\n\nexport default FormHeader;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/form-header.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/error-summary.tsx",
      "content": "\"use client\";\n\nimport { useFormState } from \"react-hook-form\";\nimport {\n  useJsonFormContext,\n  useJsonFormHasSubmitted,\n} from \"../json-form-context\";\nimport { flattenRhfErrorsToList } from \"../lib/flatten-errors\";\nimport { fieldIdSlug } from \"./field-wrapper\";\n\nexport interface JsonFormErrorSummaryProps {\n  /** Override the form-level `summaryStrategy`. */\n  strategy?: \"always\" | \"post-submit\";\n  className?: string;\n}\n\n/**\n * Standalone error-summary atom. `<div role=\"alert\" aria-live=\"polite\">`\n * with anchor-linked errors. Per default, only renders post-submit.\n * Doubles as the form's live region for validation state changes.\n *\n * v0.1.3 — reads `hasSubmitted` via the narrow `useJsonFormHasSubmitted`\n * accessor so the main context's identity doesn't churn when the flag\n * flips (F-R4 fix).\n */\nexport function JsonFormErrorSummary({\n  strategy,\n  className,\n}: JsonFormErrorSummaryProps) {\n  const ctx = useJsonFormContext();\n  const hasSubmitted = useJsonFormHasSubmitted();\n  const { errors } = useFormState({ control: ctx.rhf.control });\n  const effective = strategy ?? \"post-submit\";\n\n  const flat = flattenRhfErrorsToList(errors as Record<string, unknown>);\n  if (flat.length === 0) return null;\n  if (effective === \"post-submit\" && !hasSubmitted) return null;\n\n  return (\n    <div\n      role=\"alert\"\n      aria-live=\"polite\"\n      className={\n        \"rounded-md border border-destructive/30 bg-destructive/5 p-3 \" +\n        (className ?? \"\")\n      }\n    >\n      <h3 className=\"mb-1.5 text-sm font-medium text-destructive\">\n        {ctx.strings.summaryHeading}\n      </h3>\n      <ul className=\"ml-4 list-disc space-y-0.5 text-xs text-destructive\">\n        {flat.map(({ name, message }) => (\n          <li key={name}>\n            <a\n              href={`#${fieldIdSlug(ctx.formId, name)}`}\n              className=\"underline-offset-2 hover:underline\"\n              onClick={(e) => {\n                // Browser default scrolls to `id`, but does NOT focus the\n                // control (only focusable elements get focus on `href`-jump\n                // and group-style controls have id on the wrapper). Force\n                // focus via RHF so keyboard users land on the offending\n                // input, not just near it.\n                e.preventDefault();\n                const slug = fieldIdSlug(ctx.formId, name);\n                const target = typeof document !== \"undefined\"\n                  ? document.getElementById(slug)\n                  : null;\n                target?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n                try {\n                  ctx.rhf.setFocus(name as never);\n                } catch {\n                  // setFocus throws on non-registered names; fall through to\n                  // letting the scroll-into-view land where it can.\n                }\n              }}\n            >\n              <span className=\"font-medium\">{name}</span>\n              <span className=\"px-1\">—</span>\n              <span>{message}</span>\n            </a>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nexport default JsonFormErrorSummary;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/error-summary.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/submit-button.tsx",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useJsonFormContext } from \"../json-form-context\";\nimport type { ButtonVariant } from \"../types\";\n\nexport interface JsonFormSubmitButtonProps {\n  label?: string;\n  variant?: ButtonVariant;\n  disableWhenInvalid?: boolean;\n  className?: string;\n}\n\n/**\n * Standalone submit button. Reads `isSubmitting` + `isValid` from context.\n * Opt-in `disableWhenInvalid` (the description Q19 / Q40 anti-pattern is\n * disabled by default).\n */\nexport function JsonFormSubmitButton({\n  label,\n  variant = \"default\",\n  disableWhenInvalid = false,\n  className,\n}: JsonFormSubmitButtonProps) {\n  const ctx = useJsonFormContext();\n  const isSubmitting = ctx.rhf.formState.isSubmitting;\n  const isValid = ctx.rhf.formState.isValid;\n  const disabled = isSubmitting || (disableWhenInvalid && !isValid);\n\n  return (\n    <Button\n      type=\"submit\"\n      variant={variant}\n      disabled={disabled}\n      data-jsonform-submit\n      className={className}\n    >\n      {label ?? ctx.strings.submit}\n    </Button>\n  );\n}\n\nexport default JsonFormSubmitButton;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/submit-button.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/reset-button.tsx",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useJsonFormContext } from \"../json-form-context\";\nimport type { ButtonVariant } from \"../types\";\n\nexport interface JsonFormResetButtonProps {\n  label?: string;\n  variant?: ButtonVariant;\n  className?: string;\n}\n\n/**\n * Standalone reset button. Calls `formApi.reset()` — does NOT fire the\n * native form reset (which would clear values outside RHF's tracking).\n */\nexport function JsonFormResetButton({\n  label,\n  variant = \"outline\",\n  className,\n}: JsonFormResetButtonProps) {\n  const ctx = useJsonFormContext();\n  return (\n    <Button\n      type=\"button\"\n      variant={variant}\n      onClick={(e) => {\n        e.preventDefault();\n        ctx.reset();\n      }}\n      data-jsonform-reset\n      className={className}\n    >\n      {label ?? ctx.strings.reset}\n    </Button>\n  );\n}\n\nexport default JsonFormResetButton;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/reset-button.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/json-form-field.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport type { FieldRenderer } from \"../types\";\nimport { useJsonFormContext } from \"../json-form-context\";\nimport { FieldWrapper } from \"./field-wrapper\";\nimport { FieldFallback } from \"./field-fallback\";\n\nexport interface JsonFormFieldProps {\n  /** Field name to look up in the active schema. */\n  name: string;\n  /**\n   * Optional override registry for this single field. Falls back to the\n   * form-level merged registry (built from `<JsonForm fieldRegistry>`\n   * spread over the default registry).\n   */\n  registry?: Record<string, FieldRenderer>;\n}\n\n/**\n * Standalone field renderer for fully-headless layouts. Looks up the field\n * by `name` in the current schema, resolves a renderer from the registry,\n * wraps it in `<FieldWrapper>`. Throws (dev) if the field isn't found.\n *\n * The form-level merged registry is exposed via `ctx.fieldRegistry`.\n */\nexport function JsonFormField({ name, registry }: JsonFormFieldProps) {\n  const ctx = useJsonFormContext();\n  const field = useMemo(\n    () => ctx.schema.fields.find((f) => f.name === name),\n    [ctx.schema.fields, name],\n  );\n\n  if (!field) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.warn(\n        `[json-form] <JsonFormField name=\"${name}\"> — no field with this name in the current schema.`,\n      );\n    }\n    return null;\n  }\n\n  const merged = registry ?? ctx.fieldRegistry;\n  const renderer = merged[field.type] ?? FieldFallback;\n  return <FieldWrapper field={field} renderer={renderer} />;\n}\n\nexport default JsonFormField;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/json-form-field.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-json-form.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { useForm, type UseFormReturn } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport type {\n  FormSchema,\n  JsonFormHandle,\n  JsonFormStrings,\n  UseJsonFormOptions,\n  UseJsonFormReturn,\n} from \"../types\";\nimport { flattenRhfErrorsToRecord } from \"../lib/flatten-errors\";\nimport { compileStructural, injectStrings } from \"../lib/schema-compiler\";\nimport { mergeStrings } from \"../lib/strings\";\nimport { validateSchemaDev } from \"../lib/validate-schema\";\n\n/**\n * Headless factory hook. Builds the Zod schema, instantiates `useForm`\n * with the resolver, returns everything needed to render the form\n * yourself (or for `<JsonForm>` to render it for you).\n */\nexport function useJsonForm<TValues extends Record<string, unknown> = Record<string, unknown>>(\n  schema: FormSchema,\n  options: UseJsonFormOptions<TValues> = {},\n): UseJsonFormReturn<TValues> {\n  const strings: JsonFormStrings = useMemo(() => mergeStrings(options.strings), [options.strings]);\n\n  // Run dev-only schema sanity checks once per schema identity\n  useMemo(() => {\n    validateSchemaDev(schema);\n  }, [schema]);\n\n  // v0.1.7 (C1) — two-memo compile pipeline. The structural step (field-list\n  // extraction + schema reference) caches on schema identity; the strings\n  // injection (Zod chain construction + outer refinements) caches on the\n  // structural + strings pair. Strings-only changes (locale switch, error\n  // template overrides) skip the schema re-walk.\n  const structural = useMemo(() => compileStructural(schema), [schema]);\n  const zodSchema = useMemo(\n    () => injectStrings(structural, strings),\n    [structural, strings],\n  );\n\n  const defaultValues = useMemo(() => {\n    return mergeDefaultValues<TValues>(schema, options.defaultValues);\n  }, [schema, options.defaultValues]);\n\n  const form = useForm<TValues>({\n    // `zodSchema as never` bypasses zodResolver's overload matching.\n    // The resolver has 4 overloads (v3/v4 × raw/non-raw); consumer-side\n    // overload-resolution behavior depends on which sub-zod the bundler\n    // picks for the resolver's `zod/v4/core` import, and that can vary\n    // between producer and consumer trees even with identical pinned\n    // versions (path-b smoke 2026-05-13 surfaced this). Casting the\n    // argument removes the choice; runtime behavior is identical\n    // (resolver dispatches at runtime via instanceof checks).\n    resolver: zodResolver(zodSchema as never) as never,\n    mode: options.validationMode ?? \"onTouched\",\n    defaultValues: defaultValues as never,\n    values: options.values as never,\n  });\n\n  const handle = useFormHandle(form);\n\n  return {\n    form,\n    zodSchema,\n    fieldList: schema.fields,\n    isValid: form.formState.isValid,\n    isSubmitting: form.formState.isSubmitting,\n    handle,\n  };\n}\n\n/**\n * Merge per-field `defaultValue` with form-level `defaultValues`. Form-level\n * wins on conflict (P-09).\n *\n * v0.2.0 (B1): form-level `defaultValues` deep-merges per leaf via\n * `setByPath`/`walkLeaves` instead of shallow per-top-level-key replacement.\n * `{ address: { city: 'X' } }` at form-level no longer drops sibling per-field\n * defaults at `address.country`, `address.zip`, etc.\n */\nfunction mergeDefaultValues<TValues extends Record<string, unknown>>(\n  schema: FormSchema,\n  formLevel: Partial<TValues> | undefined,\n): Partial<TValues> {\n  const out: Record<string, unknown> = {};\n  for (const field of schema.fields) {\n    if (field.defaultValue !== undefined) {\n      setByPath(out, field.name.split(\".\"), field.defaultValue);\n    }\n  }\n  if (formLevel) {\n    walkLeaves(formLevel as Record<string, unknown>, [], (path, value) => {\n      setByPath(out, path, value);\n    });\n  }\n  return out as Partial<TValues>;\n}\n\n/** True for `{}` / `Object.create(null)` shapes. Arrays / Dates / class\n * instances return false so they're treated as terminal leaves. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  if (value === null || typeof value !== \"object\") return false;\n  if (Array.isArray(value)) return false;\n  const proto = Object.getPrototypeOf(value);\n  return proto === Object.prototype || proto === null;\n}\n\n/** Write `value` at dot-segments `path` into `obj`, lazily creating\n * intermediate plain-object containers when missing or non-object. */\nfunction setByPath(\n  obj: Record<string, unknown>,\n  path: ReadonlyArray<string>,\n  value: unknown,\n): void {\n  let cur = obj;\n  for (let i = 0; i < path.length - 1; i++) {\n    const seg = path[i];\n    if (!isPlainObject(cur[seg])) {\n      cur[seg] = {};\n    }\n    cur = cur[seg] as Record<string, unknown>;\n  }\n  cur[path[path.length - 1]] = value;\n}\n\n/** Depth-first walk: invokes `fn(path, leafValue)` for every non-plain-object\n * value reachable from `obj`. Empty objects emit no callback. */\nfunction walkLeaves(\n  obj: Record<string, unknown>,\n  prefix: ReadonlyArray<string>,\n  fn: (path: ReadonlyArray<string>, value: unknown) => void,\n): void {\n  for (const key of Object.keys(obj)) {\n    const value = obj[key];\n    const nextPath = [...prefix, key];\n    if (isPlainObject(value)) {\n      walkLeaves(value, nextPath, fn);\n    } else {\n      fn(nextPath, value);\n    }\n  }\n}\n\n/** Stable imperative-handle factory; methods read fresh RHF state per call. */\nfunction useFormHandle<TValues extends Record<string, unknown>>(\n  form: UseFormReturn<TValues>,\n): JsonFormHandle<TValues> {\n  const formRef = useRef(form);\n  useEffect(() => {\n    formRef.current = form;\n  }, [form]);\n\n  const submit = useCallback(async () => {\n    let result: { ok: boolean; values?: TValues; errors?: Record<string, string> } = { ok: false };\n    await formRef.current.handleSubmit(\n      (values) => {\n        result = { ok: true, values: values as TValues };\n      },\n      (errors) => {\n        // v0.2.6 (review §6 medium) — use the shared recursive flattener: the\n        // previous inline loop only read TOP-LEVEL `message` fields, so a\n        // nested error tree (e.g. `address.city`) came back as `errors: {}`.\n        result = {\n          ok: false,\n          errors: flattenRhfErrorsToRecord(errors as Record<string, unknown>),\n        };\n      },\n    )();\n    return result;\n  }, []);\n\n  const reset = useCallback((values?: Partial<TValues>) => {\n    formRef.current.reset(values as never);\n  }, []);\n  const setValue = useCallback((name: string, value: unknown) => {\n    formRef.current.setValue(name as never, value as never);\n  }, []);\n  const getValue = useCallback((name: string) => {\n    return formRef.current.getValues(name as never);\n  }, []);\n  const getValues = useCallback(() => formRef.current.getValues() as TValues, []);\n  const setError = useCallback((name: string, message: string) => {\n    formRef.current.setError(name as never, { message });\n  }, []);\n  const clearErrors = useCallback((name?: string) => {\n    formRef.current.clearErrors(name as never);\n  }, []);\n  const trigger = useCallback((name?: string | string[]) => {\n    return formRef.current.trigger(name as never);\n  }, []);\n  const focus = useCallback((name: string) => {\n    formRef.current.setFocus(name as never);\n  }, []);\n  const isDirty = useCallback(() => formRef.current.formState.isDirty, []);\n  const isValid = useCallback(() => formRef.current.formState.isValid, []);\n  const isSubmitting = useCallback(() => formRef.current.formState.isSubmitting, []);\n\n  return useMemo<JsonFormHandle<TValues>>(\n    () => ({\n      submit, reset, setValue, getValue, getValues,\n      setError, clearErrors, trigger, focus,\n      isDirty, isValid, isSubmitting,\n    }),\n    [submit, reset, setValue, getValue, getValues, setError, clearErrors, trigger, focus, isDirty, isValid, isSubmitting],\n  );\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-json-form.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-conditional.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useFormContext, useWatch } from \"react-hook-form\";\nimport type { FieldDefinition } from \"../types\";\nimport {\n  extractConditionOrFnDeps,\n  resolveConditionOrFn,\n} from \"../lib/condition-evaluator\";\nimport { setByPath } from \"../lib/path\";\n\nexport interface ConditionalState {\n  visible: boolean;\n  enabled: boolean;\n  required: boolean;\n}\n\n/**\n * Evaluates `visibleWhen` / `enabledWhen` / `requiredWhen` for a single\n * field.\n *\n * v0.1.6 — narrow-deps subscription (T1.2). The union of static deps across\n * all three conditions drives `useWatch({ name })`, so a conditional that\n * only depends on `country` only re-renders when `country` changes — not on\n * every keystroke anywhere in the form. Function-form conditions can't be\n * statically analyzed (`extractConditionOrFnDeps` returns `null`); we fall\n * back to the full-bag subscription for those.\n *\n * v0.1.3 — memoization on the boolean shape (F-R5). The returned object has\n * stable identity when nothing changed.\n */\nexport function useConditional(field: FieldDefinition): ConditionalState {\n  const { control } = useFormContext();\n\n  const watchTarget = useMemo(() => {\n    if (!field.visibleWhen && !field.enabledWhen && !field.requiredWhen) {\n      return { mode: \"none\" as const };\n    }\n    const allDeps = new Set<string>();\n    let anyFnForm = false;\n    for (const key of [\"visibleWhen\", \"enabledWhen\", \"requiredWhen\"] as const) {\n      const c = field[key];\n      if (!c) continue;\n      const deps = extractConditionOrFnDeps(c);\n      if (deps == null) {\n        anyFnForm = true;\n        break;\n      }\n      deps.forEach((d) => allDeps.add(d));\n    }\n    if (anyFnForm) return { mode: \"full\" as const };\n    return { mode: \"narrow\" as const, names: Array.from(allDeps) };\n  }, [field.visibleWhen, field.enabledWhen, field.requiredWhen]);\n\n  // `useWatch` is called UNCONDITIONALLY (hook rules); the `name` arg drives\n  // narrow vs full subscription. RHF returns `[]` for `name: []`, which is\n  // fine — we never read it in that case. The `as never` cast matches the\n  // codebase's existing RHF-overload-disambiguation pattern (see\n  // `use-json-form.ts` resolver cast comment for the precedent rationale);\n  // here the union of `{ control, name }` vs `{ control }` ambiguates v7.76\n  // overload resolution against the new `compute` overload.\n  const watched = useWatch(\n    (watchTarget.mode === \"narrow\"\n      ? { control, name: watchTarget.names }\n      : { control }) as never,\n  );\n\n  const values = useMemo<Record<string, unknown>>(() => {\n    if (watchTarget.mode === \"narrow\") {\n      // RHF returns an array (positional) when `name` is an array. Rebuild\n      // a nested bag via `setByPath` so the condition evaluator's\n      // `getByPath('address.city')` walks into the right shape.\n      const arr = Array.isArray(watched) ? watched : [];\n      const bag: Record<string, unknown> = {};\n      watchTarget.names.forEach((n, i) => {\n        setByPath(bag, n, arr[i]);\n      });\n      return bag;\n    }\n    return (watched ?? {}) as Record<string, unknown>;\n  }, [watched, watchTarget]);\n\n  const visible = field.visibleWhen\n    ? resolveConditionOrFn(field.visibleWhen, values)\n    : true;\n  const enabled = field.enabledWhen\n    ? resolveConditionOrFn(field.enabledWhen, values)\n    : true;\n  const required = field.requiredWhen\n    ? resolveConditionOrFn(field.requiredWhen, values)\n    : false;\n\n  return useMemo<ConditionalState>(\n    () => ({ visible, enabled, required }),\n    [visible, enabled, required],\n  );\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-conditional.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-computed.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useFormContext, useWatch } from \"react-hook-form\";\nimport type { FieldDefinition } from \"../types\";\nimport { interpolate, parseExpression } from \"../lib/expression-parser\";\nimport { setByPath } from \"../lib/path\";\n\n/**\n * Evaluate a `computed` field's value. Two forms:\n *   - `expression: '{firstName} {lastName}'` — pure interpolation\n *   - `compute: (args) => unknown` — function escape hatch\n *\n * v0.1.6 — narrow-deps subscription (T1.2). For the `expression` form we\n * statically extract the referenced field paths from the parsed template and\n * scope the `useWatch` to those names; the `compute` form is opaque and\n * falls back to a full-bag subscription.\n *\n * v0.1.3 — inline evaluation (F-R5). The body is cheap (string interpolation\n * OR a single function call) and `field-computed.tsx` has an outer\n * value-equality guard before propagating to RHF, so referential instability\n * of the return value is not a correctness concern.\n */\nexport function useComputed(field: FieldDefinition): unknown {\n  const { control } = useFormContext();\n\n  const parsed = useMemo(() => {\n    if (!field.expression) return null;\n    return parseExpression(field.expression);\n  }, [field.expression]);\n\n  // Static deps when we have an `expression`; full-bag fallback for the\n  // `compute` function form (or when neither is set — short-circuit later).\n  const watchTarget = useMemo(() => {\n    if (parsed) {\n      return { mode: \"narrow\" as const, names: Array.from(parsed.deps) };\n    }\n    return { mode: \"full\" as const };\n  }, [parsed]);\n\n  // `as never` matches the codebase's RHF-overload pattern; v7.76 added a\n  // `compute` overload that ambiguates union-arg call sites here.\n  const watched = useWatch(\n    (watchTarget.mode === \"narrow\"\n      ? { control, name: watchTarget.names }\n      : { control }) as never,\n  );\n\n  const values = useMemo<Record<string, unknown>>(() => {\n    if (watchTarget.mode === \"narrow\") {\n      const arr = Array.isArray(watched) ? watched : [];\n      const bag: Record<string, unknown> = {};\n      watchTarget.names.forEach((n, i) => {\n        setByPath(bag, n, arr[i]);\n      });\n      return bag;\n    }\n    return (watched ?? {}) as Record<string, unknown>;\n  }, [watched, watchTarget]);\n\n  if (field.expression && parsed) {\n    return interpolate(parsed, values);\n  }\n  if (field.compute) {\n    return field.compute({ values });\n  }\n  return undefined;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-computed.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-debounced-callback.ts",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\n\n/**\n * Returns a debounced version of `fn` that delays invocation by `delay` ms.\n * Hand-rolled — avoiding `use-debounce` npm dep for ~15 lines of logic.\n */\nexport function useDebouncedCallback<TArgs extends unknown[]>(\n  fn: (...args: TArgs) => void,\n  delay: number,\n): (...args: TArgs) => void {\n  const fnRef = useRef(fn);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // Keep the latest fn reference without re-creating the debounced wrapper.\n  useEffect(() => {\n    fnRef.current = fn;\n  }, [fn]);\n\n  // Clear on unmount.\n  useEffect(() => {\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, []);\n\n  return useMemo(() => {\n    return (...args: TArgs) => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n      if (delay <= 0) {\n        fnRef.current(...args);\n        return;\n      }\n      timerRef.current = setTimeout(() => {\n        fnRef.current(...args);\n      }, delay);\n    };\n  }, [delay]);\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-debounced-callback.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-async-options.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useFormContext } from \"react-hook-form\";\nimport type { FieldDefinition, FieldOption, FieldOptionsResolver } from \"../types\";\n\nexport interface AsyncOptionsState {\n  options: FieldOption[];\n  loading: boolean;\n  error: string | null;\n  /** Triggers a re-fetch with the same query (used by the Retry button). */\n  retry: () => void;\n  /** Update the search query (combobox typing). */\n  setQuery: (q: string) => void;\n}\n\nconst DEFAULT_DEBOUNCE = 200;\n\n/**\n * Manages an async options resolver: debounce, loading state, error+retry.\n * Static options pass through directly (no state — derived on render).\n */\nexport function useAsyncOptions(field: FieldDefinition): AsyncOptionsState {\n  const { getValues } = useFormContext();\n  const [asyncOptions, setAsyncOptions] = useState<FieldOption[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [query, setQueryState] = useState(\"\");\n  const [tick, setTick] = useState(0);\n  const generation = useRef(0);\n\n  const isAsync = typeof field.options === \"function\";\n  const staticOptions = useMemo<FieldOption[]>(\n    () => (Array.isArray(field.options) ? (field.options as FieldOption[]) : []),\n    [field.options],\n  );\n  const debounce = field.optionsDebounce ?? DEFAULT_DEBOUNCE;\n\n  useEffect(() => {\n    if (!isAsync) return;\n    const resolver = field.options as FieldOptionsResolver;\n    // Loading + error reset before the network call is intentional —\n    // ergonomic \"spinner before fetch\" UX. Suppress the React Compiler\n    // cascading-render lint; the subsequent then/catch are stable.\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    setLoading(true);\n    setError(null);\n    const myGen = ++generation.current;\n    const controller = new AbortController();\n\n    const handle = setTimeout(() => {\n      resolver({\n        query,\n        allValues: getValues() as Record<string, unknown>,\n        signal: controller.signal,\n      })\n        .then((next) => {\n          if (myGen !== generation.current) return;\n          setAsyncOptions(next);\n          setLoading(false);\n        })\n        .catch((err: unknown) => {\n          if (myGen !== generation.current) return;\n          // AbortError is expected on supersede/unmount — don't surface it.\n          if (err instanceof DOMException && err.name === \"AbortError\") return;\n          setAsyncOptions([]);\n          setLoading(false);\n          setError(err instanceof Error ? err.message : String(err));\n        });\n    }, debounce);\n\n    return () => {\n      clearTimeout(handle);\n      controller.abort();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isAsync, query, debounce, tick]);\n\n  const retry = useCallback(() => setTick((t) => t + 1), []);\n  const setQuery = useCallback((q: string) => setQueryState(q), []);\n\n  return {\n    options: isAsync ? asyncOptions : staticOptions,\n    loading: isAsync ? loading : false,\n    error: isAsync ? error : null,\n    retry,\n    setQuery,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-async-options.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/schema-compiler.ts",
      "content": "import { z } from \"zod\";\nimport type { ZodObject, ZodTypeAny } from \"zod\";\nimport type { FieldDefinition, FieldValidators, FormSchema, JsonFormStrings } from \"../types\";\nimport { resolveConditionOrFn } from \"./condition-evaluator\";\nimport { formatErrorTemplate } from \"./strings\";\nimport { isValueField } from \"./validate-schema\";\nimport { getByPath } from \"./path\";\n\n/**\n * v0.1.7 (C1) — compile pipeline split into two stages for cache granularity:\n *\n *   compileStructural(schema)        → intermediate representation, schema-keyed\n *   injectStrings(structural, strings) → final ZodObject, strings-keyed\n *   compileSchema(schema, strings)   → composition wrapper (internal-only — not\n *                                       exported from index.ts; kept for code\n *                                       readability and any direct internal call)\n *\n * Why the split: in `useJsonForm`, `strings` identity changes (locale switch,\n * error-message overrides) shouldn't re-walk the schema's field list. The\n * two-memo pattern caches the structural step on schema identity, and only\n * the string-injection step runs on strings change. For typical schemas the\n * absolute saving is microseconds, but the cleanly-separated stages make the\n * cache reasoning obvious and free us from RHF re-initialization on\n * strings-only updates (the resolver wraps the new Zod object — RHF detects\n * the resolver identity change but reuses prior form state).\n *\n * Pipeline (per the v0.1.0 plan):\n *   1. Walk value-carrying fields, build a per-field Zod chain from\n *      `field.validators` + base type.\n *   2. Group fields by dot-path roots, build a nested ZodObject via path-trie.\n *   3. Apply `field.validate` (sync) + `field.validateAsync` via per-field\n *      `.superRefine()` (resolver evaluates after structural validators).\n *   4. Apply form-level `schema.validate` via an outer `.superRefine()`.\n *   5. Apply `requiredWhen` via another outer `.superRefine()` that checks\n *      visibility-aware required-ness against the runtime values bag.\n *   6. Merge `schema.zodSchema` LAST — consumer-provided keys overwrite\n *      DSL-generated ones (M-03 / T8 lock).\n *\n * Steps 1 + 2 + 5 + 6 reference `strings` (via `errorTemplates.*` messages\n * baked into the constraint chains); step 1 also runs `buildFieldZod` per\n * field which is the bulk of the per-field work. So `injectStrings` does\n * most of the heavy lifting; `compileStructural` is the cheap \"field list\n * collection\" memo whose output is reused across strings changes.\n */\n\n/**\n * Schema-keyed intermediate. Internal-only; not exported from index.ts.\n *\n * Holds the value-carrying field list pre-extracted (so `injectStrings`\n * doesn't re-filter on every strings change) plus a reference to the source\n * `FormSchema` for the outer-refinement steps (custom validators / form-level\n * validate / requiredWhen / consumer-zodSchema merge).\n */\nexport interface CompiledStructural {\n  readonly leafFields: ReadonlyArray<FieldDefinition>;\n  readonly schema: FormSchema;\n}\n\n/** Schema-keyed compile stage. Cheap; safe to re-run on any schema change. */\nexport function compileStructural(schema: FormSchema): CompiledStructural {\n  const leafFields: FieldDefinition[] = [];\n  for (const field of schema.fields) {\n    if (!isValueField(field)) continue;\n    leafFields.push(field);\n  }\n  return { leafFields, schema };\n}\n\n/** Strings-keyed compile stage. Builds the final Zod chains + outer refinements. */\nexport function injectStrings(\n  structural: CompiledStructural,\n  strings: JsonFormStrings,\n): ZodObject<Record<string, ZodTypeAny>> {\n  // 1. Build flat leaf map: 'path' → ZodType (strings baked in)\n  const leaves = new Map<string, ZodTypeAny>();\n  for (const field of structural.leafFields) {\n    leaves.set(field.name, buildFieldZod(field, strings));\n  }\n\n  // 2. Nested ZodObject via path-trie\n  let zod = buildNestedZodObject(leaves);\n\n  // 3. Per-field custom sync + async validators via outer .superRefine\n  zod = applyCustomValidators(zod, structural.schema.fields);\n\n  // 4. Form-level validate\n  if (structural.schema.validate) {\n    const formValidate = structural.schema.validate;\n    zod = zod.superRefine((values, ctx) => {\n      const errors = formValidate({ values: values as Record<string, unknown> });\n      if (!errors) return;\n      for (const [path, message] of Object.entries(errors)) {\n        ctx.addIssue({\n          code: \"custom\",\n          path: path.split(\".\"),\n          message,\n        });\n      }\n    }) as ZodObject<Record<string, ZodTypeAny>>;\n  }\n\n  // 5. requiredWhen — conditional required\n  zod = applyRequiredWhen(zod, structural.schema.fields, strings);\n\n  // 6. Merge consumer's zodSchema — consumer wins per-key\n  if (structural.schema.zodSchema) {\n    zod = mergeZodSchemas(zod, structural.schema.zodSchema);\n  }\n\n  return zod;\n}\n\n/**\n * Composition wrapper. Same signature as v0.1.x for any internal callsite\n * that wants the one-shot compile. Equivalent to\n * `injectStrings(compileStructural(schema), strings)`.\n */\nexport function compileSchema(\n  schema: FormSchema,\n  strings: JsonFormStrings,\n): ZodObject<Record<string, ZodTypeAny>> {\n  return injectStrings(compileStructural(schema), strings);\n}\n\n// ─── Per-field base Zod construction ─────────────────────────────────────────\n\nfunction buildFieldZod(field: FieldDefinition, strings: JsonFormStrings): ZodTypeAny {\n  const v = field.validators ?? {};\n  let chain: ZodTypeAny;\n\n  switch (field.type) {\n    case \"text\":\n    case \"email\":\n    case \"password\":\n    case \"url\":\n    case \"tel\":\n    case \"textarea\":\n    case \"code\": {\n      let s = z.string();\n      // built-in format checks derived from type\n      if (field.type === \"email\") {\n        s = s.email(extractMessage(v.email, strings.errorTemplates.email));\n      }\n      if (field.type === \"url\") {\n        s = s.url(extractMessage(v.url, strings.errorTemplates.url));\n      }\n      // declared validators\n      if (v.minLength != null) {\n        const { value, message } = normalizeNumeric(v.minLength, strings.errorTemplates.minLength);\n        s = s.min(value, formatErrorTemplate(message, { n: value }));\n      }\n      if (v.maxLength != null) {\n        const { value, message } = normalizeNumeric(v.maxLength, strings.errorTemplates.maxLength);\n        s = s.max(value, formatErrorTemplate(message, { n: value }));\n      }\n      if (v.pattern != null) {\n        const { value, message } = normalizePattern(v.pattern, strings.errorTemplates.pattern);\n        try {\n          const re = new RegExp(value);\n          s = s.regex(re, message);\n        } catch {\n          // invalid regex — leave the field non-strictly-validated\n        }\n      }\n      if (v.email && field.type !== \"email\") {\n        s = s.email(extractMessage(v.email, strings.errorTemplates.email));\n      }\n      if (v.url && field.type !== \"url\") {\n        s = s.url(extractMessage(v.url, strings.errorTemplates.url));\n      }\n      chain = applyRequired(s, v, strings);\n      break;\n    }\n\n    case \"number\":\n    case \"slider\":\n    case \"rating\": {\n      // z.coerce.number handles the <input> string→number conversion\n      let n = z.coerce.number();\n      if (v.min != null) {\n        const { value, message } = normalizeNumeric(v.min, strings.errorTemplates.min);\n        n = n.gte(Number(value), formatErrorTemplate(message, { n: value }));\n      }\n      if (v.max != null) {\n        const { value, message } = normalizeNumeric(v.max, strings.errorTemplates.max);\n        n = n.lte(Number(value), formatErrorTemplate(message, { n: value }));\n      }\n      chain = applyRequired(n, v, strings);\n      break;\n    }\n\n    case \"checkbox\":\n    case \"switch\":\n      chain = applyRequiredBoolean(z.boolean(), v, strings);\n      break;\n\n    case \"select\":\n    case \"radio-group\":\n      chain = applyRequiredUnknown(z.unknown(), v, strings);\n      break;\n\n    case \"multi-select\":\n    case \"checkbox-group\":\n      chain = applyRequiredArray(z.array(z.unknown()), v, strings);\n      break;\n\n    case \"date\":\n    case \"time\":\n    case \"datetime\":\n      chain = applyRequired(z.string(), v, strings);\n      break;\n\n    case \"richtext\":\n      // Plate `Value` = `Array<TElement | TText>`. Required = at least one\n      // node that contains non-empty text (we can't strict-check the inner\n      // shape without pulling platejs into the compiler, so a permissive\n      // array check + a custom-validate hook covers it).\n      chain = applyRequiredRichtext(v, strings);\n      break;\n\n    case \"date-range\":\n      chain = applyRequiredUnknown(\n        z.object({ start: z.string(), end: z.string() }),\n        v,\n        strings,\n      );\n      break;\n\n    case \"computed\":\n    case \"hidden\":\n      // Carry value through; only `required` matters (e.g., CSRF token).\n      chain = applyRequiredUnknown(z.unknown(), v, strings);\n      break;\n\n    default:\n      // unknown/consumer-registered type → permissive default\n      chain = applyRequiredUnknown(z.unknown(), v, strings);\n  }\n\n  return chain;\n}\n\n// ─── Required-handling per base type ─────────────────────────────────────────\n\nfunction applyRequired(s: ZodTypeAny, v: FieldValidators, strings: JsonFormStrings): ZodTypeAny {\n  if (!v.required) {\n    if (s instanceof z.ZodString) return s.optional().or(z.literal(\"\"));\n    return s.optional();\n  }\n  const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n  if (s instanceof z.ZodString) {\n    return (s as z.ZodString).min(1, message);\n  }\n  // For numbers (incl. ZodCoercedNumber) and other types, the resolver\n  // already rejects undefined values when not made optional.\n  return s.refine((val) => val !== undefined && val !== null && val !== \"\", { message });\n}\n\nfunction applyRequiredBoolean(s: z.ZodBoolean, v: FieldValidators, strings: JsonFormStrings): ZodTypeAny {\n  if (!v.required) return s.optional();\n  const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n  return s.refine((val) => val === true, { message });\n}\n\nfunction applyRequiredUnknown<T extends ZodTypeAny>(s: T, v: FieldValidators, strings: JsonFormStrings): ZodTypeAny {\n  if (!v.required) return s.optional();\n  const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n  return s.refine((val) => val !== undefined && val !== null && val !== \"\", { message });\n}\n\nfunction applyRequiredArray(s: z.ZodArray<ZodTypeAny>, v: FieldValidators, strings: JsonFormStrings): ZodTypeAny {\n  if (!v.required) return s.optional();\n  const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n  return s.min(1, message);\n}\n\nfunction applyRequiredRichtext(v: FieldValidators, strings: JsonFormStrings): ZodTypeAny {\n  const base = z.array(z.unknown());\n  if (!v.required) return base.optional();\n  const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n  // Required = non-empty after stripping the \"single empty paragraph\" default.\n  return base.refine((nodes) => richtextHasContent(nodes as unknown[]), { message });\n}\n\nfunction richtextHasContent(nodes: unknown[]): boolean {\n  if (!Array.isArray(nodes) || nodes.length === 0) return false;\n  // Walk leaf `text` props; consider empty if every text node is whitespace-only.\n  function visit(n: unknown): boolean {\n    if (n == null || typeof n !== \"object\") return false;\n    const obj = n as Record<string, unknown>;\n    if (typeof obj.text === \"string\") return obj.text.trim().length > 0;\n    const children = obj.children;\n    if (Array.isArray(children)) return children.some(visit);\n    return false;\n  }\n  return nodes.some(visit);\n}\n\n// ─── Validator normalization helpers ─────────────────────────────────────────\n\ntype NumericValidator = number | string | { value: number | string; message: string };\n\nfunction normalizeNumeric(\n  raw: NumericValidator,\n  defaultMessage: string,\n): { value: number; message: string } {\n  if (typeof raw === \"object\") {\n    return { value: Number(raw.value), message: raw.message };\n  }\n  return { value: Number(raw), message: defaultMessage };\n}\n\ntype PatternValidator = string | { value: string; message: string };\n\nfunction normalizePattern(raw: PatternValidator, defaultMessage: string): { value: string; message: string } {\n  if (typeof raw === \"object\") return raw;\n  return { value: raw, message: defaultMessage };\n}\n\nfunction extractMessage(raw: boolean | { message: string } | undefined, fallback: string): string {\n  if (raw && typeof raw === \"object\") return raw.message;\n  return fallback;\n}\n\n// ─── Path-trie → nested ZodObject ────────────────────────────────────────────\n\ntype TrieNode = Map<string, TrieNode | ZodTypeAny>;\n\nfunction isTrieNode(v: TrieNode | ZodTypeAny): v is TrieNode {\n  return v instanceof Map;\n}\n\nfunction buildNestedZodObject(leaves: Map<string, ZodTypeAny>): ZodObject<Record<string, ZodTypeAny>> {\n  const root: TrieNode = new Map();\n  for (const [path, ztype] of leaves) {\n    const segments = path.split(\".\");\n    let cur: TrieNode = root;\n    for (let i = 0; i < segments.length - 1; i++) {\n      const seg = segments[i];\n      const existing = cur.get(seg);\n      if (existing && isTrieNode(existing)) {\n        cur = existing;\n      } else {\n        const next: TrieNode = new Map();\n        cur.set(seg, next);\n        cur = next;\n      }\n    }\n    cur.set(segments[segments.length - 1], ztype);\n  }\n\n  function trieToZod(trie: TrieNode): ZodObject<Record<string, ZodTypeAny>> {\n    const shape: Record<string, ZodTypeAny> = {};\n    for (const [key, val] of trie) {\n      shape[key] = isTrieNode(val) ? trieToZod(val) : val;\n    }\n    return z.object(shape);\n  }\n\n  return trieToZod(root);\n}\n\n// ─── Per-field custom validate + validateAsync ───────────────────────────────\n\nfunction applyCustomValidators(\n  zod: ZodObject<Record<string, ZodTypeAny>>,\n  fields: ReadonlyArray<FieldDefinition>,\n): ZodObject<Record<string, ZodTypeAny>> {\n  const withCustom = fields.filter((f) => isValueField(f) && (f.validate || f.validateAsync));\n  if (withCustom.length === 0) return zod;\n\n  return zod.superRefine(async (values, ctx) => {\n    for (const f of withCustom) {\n      const value = getByPath(values, f.name);\n      const allValues = values as Record<string, unknown>;\n\n      // Sync first — short-circuit on error\n      if (f.validate) {\n        const err = f.validate({ value, allValues });\n        if (err) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: f.name.split(\".\"),\n            message: err,\n          });\n          continue;\n        }\n      }\n      // Async runs only if sync passed\n      if (f.validateAsync) {\n        const err = await f.validateAsync({ value, allValues });\n        if (err) {\n          ctx.addIssue({\n            code: \"custom\",\n            path: f.name.split(\".\"),\n            message: err,\n          });\n        }\n      }\n    }\n  }) as ZodObject<Record<string, ZodTypeAny>>;\n}\n\n// ─── requiredWhen — conditional required ─────────────────────────────────────\n\nfunction applyRequiredWhen(\n  zod: ZodObject<Record<string, ZodTypeAny>>,\n  fields: ReadonlyArray<FieldDefinition>,\n  strings: JsonFormStrings,\n): ZodObject<Record<string, ZodTypeAny>> {\n  const conditional = fields.filter((f) => isValueField(f) && f.requiredWhen);\n  if (conditional.length === 0) return zod;\n\n  return zod.superRefine((values, ctx) => {\n    const allValues = values as Record<string, unknown>;\n    for (const f of conditional) {\n      const required = resolveConditionOrFn(f.requiredWhen!, allValues);\n      if (!required) continue;\n      const value = getByPath(values, f.name);\n      const isEmpty = value === undefined || value === null || value === \"\" ||\n        (Array.isArray(value) && value.length === 0);\n      if (isEmpty) {\n        const v = f.validators ?? {};\n        const message = typeof v.required === \"string\" ? v.required : strings.errorTemplates.required;\n        ctx.addIssue({\n          code: \"custom\",\n          path: f.name.split(\".\"),\n          message,\n        });\n      }\n    }\n  }) as ZodObject<Record<string, ZodTypeAny>>;\n}\n\n// ─── Merge consumer-provided ZodSchema (consumer wins per key) ───────────────\n\nfunction mergeZodSchemas(\n  base: ZodObject<Record<string, ZodTypeAny>>,\n  override: ZodObject<Record<string, ZodTypeAny>>,\n): ZodObject<Record<string, ZodTypeAny>> {\n  const baseShape = base.shape;\n  const overrideShape = override.shape;\n  const merged: Record<string, ZodTypeAny> = { ...baseShape };\n  for (const key of Object.keys(overrideShape)) {\n    merged[key] = overrideShape[key];\n  }\n  return z.object(merged);\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/schema-compiler.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/condition-evaluator.ts",
      "content": "import type { Condition, ConditionOrFn } from \"../types\";\nimport { getByPath } from \"./path\";\n\n/**\n * Evaluate a Condition against a values bag. Returns the boolean result.\n * Unknown shape (defensive — should never happen if types hold) → true\n * (the \"always visible / enabled / required\" safe default) + a dev warn\n * at the call site.\n */\nexport function evaluateCondition(\n  cond: Condition,\n  values: Record<string, unknown>,\n): boolean {\n  if (\"all\" in cond) return cond.all.every((c) => evaluateCondition(c, values));\n  if (\"any\" in cond) return cond.any.some((c) => evaluateCondition(c, values));\n  if (\"not\" in cond) return !evaluateCondition(cond.not, values);\n\n  if (\"field\" in cond) {\n    const v = getByPath(values, cond.field);\n\n    if (\"equals\" in cond) return Object.is(v, cond.equals);\n    if (\"notEquals\" in cond) return !Object.is(v, cond.notEquals);\n    if (\"in\" in cond) return cond.in.includes(v);\n    if (\"notIn\" in cond) return !cond.notIn.includes(v);\n    if (\"matches\" in cond) {\n      if (typeof v !== \"string\") return false;\n      try {\n        return new RegExp(cond.matches).test(v);\n      } catch {\n        return false;\n      }\n    }\n    if (\"truthy\" in cond) return cond.truthy ? !!v : !v;\n    if (\"greaterThan\" in cond) return typeof v === \"number\" && v > cond.greaterThan;\n    if (\"lessThan\" in cond) return typeof v === \"number\" && v < cond.lessThan;\n  }\n\n  return true;\n}\n\n/**\n * Helper: resolve a `Condition` or function form. The fn form receives the\n * full values bag.\n */\nexport function resolveConditionOrFn(\n  cond: ConditionOrFn,\n  values: Record<string, unknown>,\n): boolean {\n  if (typeof cond === \"function\") return cond({ values });\n  return evaluateCondition(cond, values);\n}\n\n/**\n * Walk a Condition tree and collect every field path it references.\n * Used to subscribe to only the relevant slice of form state via RHF\n * `watch(deps)` — keeps the per-conditional cost bounded.\n */\nexport function extractConditionDeps(cond: Condition): Set<string> {\n  const deps = new Set<string>();\n  function walk(c: Condition): void {\n    if (\"all\" in c) { c.all.forEach(walk); return; }\n    if (\"any\" in c) { c.any.forEach(walk); return; }\n    if (\"not\" in c) { walk(c.not); return; }\n    if (\"field\" in c) { deps.add(c.field); }\n  }\n  walk(cond);\n  return deps;\n}\n\n/**\n * For function-form conditions, we can't statically extract deps. Return\n * `null` → caller subscribes to the whole values bag.\n */\nexport function extractConditionOrFnDeps(cond: ConditionOrFn): Set<string> | null {\n  if (typeof cond === \"function\") return null;\n  return extractConditionDeps(cond);\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/condition-evaluator.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/expression-parser.ts",
      "content": "import { getByPath } from \"./path\";\n\n/**\n * Computed-field expression parser. Pure interpolation only:\n *   '{firstName} {lastName}'              → \"Hessam Hezaveh\"\n *   '{address.city}, {address.country}'   → \"Tehran, Iran\"\n *\n * NO operators, NO conditionals, NO function calls (per A3 lock). Consumers\n * needing anything more use `compute: (values) => ...` instead.\n */\n\nexport interface ParsedExpression {\n  /** Ordered parts: alternating literal strings and field references. */\n  parts: Array<{ kind: \"literal\"; text: string } | { kind: \"field\"; path: string }>;\n  /** Set of all field paths referenced. */\n  deps: Set<string>;\n}\n\nconst FIELD_RE = /\\{([^{}]+)\\}/g;\n\nexport function parseExpression(expr: string): ParsedExpression {\n  const parts: ParsedExpression[\"parts\"] = [];\n  const deps = new Set<string>();\n  let lastIndex = 0;\n  FIELD_RE.lastIndex = 0;\n\n  let m: RegExpExecArray | null;\n  while ((m = FIELD_RE.exec(expr)) !== null) {\n    if (m.index > lastIndex) {\n      parts.push({ kind: \"literal\", text: expr.slice(lastIndex, m.index) });\n    }\n    const path = m[1].trim();\n    parts.push({ kind: \"field\", path });\n    deps.add(path);\n    lastIndex = m.index + m[0].length;\n  }\n  if (lastIndex < expr.length) {\n    parts.push({ kind: \"literal\", text: expr.slice(lastIndex) });\n  }\n\n  return { parts, deps };\n}\n\n/**\n * Render an expression against a values bag. Unknown fields → empty string\n * (plus dev warn from the caller if desired).\n */\nexport function interpolate(\n  parsed: ParsedExpression,\n  values: Record<string, unknown>,\n): string {\n  let out = \"\";\n  for (const part of parsed.parts) {\n    if (part.kind === \"literal\") {\n      out += part.text;\n    } else {\n      const v = getByPath(values, part.path);\n      out += v == null ? \"\" : String(v);\n    }\n  }\n  return out;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/expression-parser.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/default-registry.ts",
      "content": "import { createElement, lazy } from \"react\";\nimport type { FieldRenderer, FieldRendererArgs } from \"../types\";\n\nimport { FieldText } from \"../parts/field-text\";\nimport { FieldTextarea } from \"../parts/field-textarea\";\nimport { FieldSelect } from \"../parts/field-select\";\nimport { FieldRadioGroup } from \"../parts/field-radio-group\";\nimport { FieldCheckbox } from \"../parts/field-checkbox\";\nimport { FieldCheckboxGroup } from \"../parts/field-checkbox-group\";\nimport { FieldSwitch } from \"../parts/field-switch\";\nimport { FieldDate } from \"../parts/field-date\";\nimport { FieldSlider } from \"../parts/field-slider\";\nimport { FieldRating } from \"../parts/field-rating\";\nimport { FieldComputed } from \"../parts/field-computed\";\nimport { FieldHidden } from \"../parts/field-hidden\";\nimport { FieldSection } from \"../parts/field-section\";\nimport { FieldDivider } from \"../parts/field-divider\";\nimport { FieldFallback } from \"../parts/field-fallback\";\n\n// The `code` and `richtext` fields are lazy-loaded so the CodeMirror and\n// Plate bundles only ship when a form actually contains one of those\n// fields. The Suspense boundary lives in `FieldWrapper`.\nconst LazyFieldCode = lazy(() => import(\"../parts/field-code\"));\nconst FieldCode: FieldRenderer = (args: FieldRendererArgs) =>\n  createElement(LazyFieldCode, args);\n\nconst LazyFieldRichtext = lazy(() => import(\"../parts/field-richtext\"));\nconst FieldRichtext: FieldRenderer = (args: FieldRendererArgs) =>\n  createElement(LazyFieldRichtext, args);\n\n/**\n * Built-in field renderer registry. Spread + extend in consumer code:\n *\n *   const myRegistry = { ...defaultJsonFormRegistry, \"rich-text\": MyRenderer };\n */\n/**\n * v0.1.7 — internal whitelist of built-in field types whose renderers do NOT\n * read `args.allValues` reactively. Defined in v0.1.7 (defined-but-unused);\n * `field-wrapper.tsx` in v0.2.0 consults this set to skip the FieldWrapper-\n * level `useWatch({ control })` for whitelisted types — `allValues` then\n * comes from a `rhf.getValues()` snapshot taken at render time.\n *\n * Audit basis: every built-in renderer's source was checked for\n * `args.allValues` access on 2026-05-21. `computed`'s renderer destructures\n * only `field`, `value`, `onChange`, `disabled`, `ariaProps`; the values-\n * reading happens inside `useComputed()` via its own narrow `useWatch`\n * (independent of the FieldWrapper-level watch), so `computed` is in the\n * whitelist. `section` + `divider` are layout-only and never hit\n * FieldWrapper, but are included for completeness so the set covers every\n * registry slot.\n *\n * Consumer-registered renderers OVERRIDING built-in types are NOT in this\n * whitelist (the v0.2.0 check resolves on the FINAL renderer identity, not\n * on `field.type` alone). A custom renderer replacing `text` correctly\n * opts out of the watch drop unless the field also sets `dependsOn`.\n *\n * Drift mitigation: any future built-in renderer that adds `args.allValues`\n * access MUST be removed from this set in the same commit. Enforced by\n * `scripts/validate-default-registry-whitelist.mjs` (F-03), which is\n * chained into `pnpm registry:build` and `pnpm vercel-build` so producer\n * deploys fail fast if drift lands. Run standalone via\n * `pnpm validate:default-registry-whitelist`.\n */\nexport const BUILTIN_RENDERER_TYPES_SKIPPING_ALL_VALUES: ReadonlySet<string> =\n  new Set([\n    // text family\n    \"text\",\n    \"email\",\n    \"password\",\n    \"url\",\n    \"tel\",\n    \"number\",\n    \"textarea\",\n    // choice family\n    \"select\",\n    \"multi-select\",\n    \"radio-group\",\n    \"checkbox\",\n    \"checkbox-group\",\n    \"switch\",\n    // date/time family\n    \"date\",\n    \"date-range\",\n    \"time\",\n    \"datetime\",\n    // rich / composite\n    \"code\",\n    \"slider\",\n    \"rating\",\n    \"richtext\",\n    // special\n    \"computed\",\n    \"hidden\",\n    \"section\",\n    \"divider\",\n  ]);\n\nexport const defaultJsonFormRegistry: Record<string, FieldRenderer> = {\n  // text family\n  text: FieldText,\n  email: FieldText,\n  password: FieldText,\n  url: FieldText,\n  tel: FieldText,\n  number: FieldText,\n  textarea: FieldTextarea,\n  // choice family\n  select: FieldSelect,\n  \"multi-select\": FieldSelect,\n  \"radio-group\": FieldRadioGroup,\n  checkbox: FieldCheckbox,\n  \"checkbox-group\": FieldCheckboxGroup,\n  switch: FieldSwitch,\n  // date/time family\n  date: FieldDate,\n  \"date-range\": FieldDate,\n  time: FieldDate,\n  datetime: FieldDate,\n  // rich / composite\n  code: FieldCode,\n  slider: FieldSlider,\n  rating: FieldRating,\n  richtext: FieldRichtext,\n  // special\n  computed: FieldComputed,\n  hidden: FieldHidden,\n  section: FieldSection,\n  divider: FieldDivider,\n  // fallback\n  __fallback__: FieldFallback,\n};\n",
      "type": "registry:component",
      "target": "components/json-form/lib/default-registry.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/strings.ts",
      "content": "import type { JsonFormStrings } from \"../types\";\n\nexport const defaultStrings: JsonFormStrings = {\n  submit: \"Submit\",\n  reset: \"Reset\",\n  requiredIndicator: \"*\",\n  optionalIndicator: \"(optional)\",\n  loadingOptions: \"Loading…\",\n  optionsError: \"Failed to load options\",\n  optionsRetry: \"Retry\",\n  noOptions: \"No options\",\n  summaryHeading: \"Please fix the following errors:\",\n  submitFailed: \"Submission failed. Please try again.\",\n  errorTemplates: {\n    required: \"This field is required\",\n    minLength: \"Must be at least {n} characters\",\n    maxLength: \"Must be at most {n} characters\",\n    min: \"Must be at least {n}\",\n    max: \"Must be at most {n}\",\n    pattern: \"Value does not match the required pattern\",\n    email: \"Must be a valid email address\",\n    url: \"Must be a valid URL\",\n  },\n};\n\n/** Substitute `{n}`, `{value}`, etc. in a template. */\nexport function formatErrorTemplate(\n  template: string,\n  params: Record<string, string | number>,\n): string {\n  return template.replace(/\\{(\\w+)\\}/g, (_, key) =>\n    key in params ? String(params[key]) : `{${key}}`,\n  );\n}\n\n/** Shallow-merge consumer overrides into the default strings dictionary. */\nexport function mergeStrings(\n  overrides: Partial<JsonFormStrings> | undefined,\n): JsonFormStrings {\n  if (!overrides) return defaultStrings;\n  return {\n    ...defaultStrings,\n    ...overrides,\n    errorTemplates: {\n      ...defaultStrings.errorTemplates,\n      ...(overrides.errorTemplates ?? {}),\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/strings.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/path.ts",
      "content": "/**\n * Dot-path helpers used by the expression interpolator + nested ZodObject\n * construction. RHF handles deep-set/get internally for `register('a.b.c')`,\n * but we need read-side dot-path access for `expression: '{address.city}'`.\n */\n\nexport function getByPath(obj: unknown, path: string): unknown {\n  if (obj == null) return undefined;\n  const segments = path.split(\".\");\n  let cur: unknown = obj;\n  for (const seg of segments) {\n    if (cur == null || typeof cur !== \"object\") return undefined;\n    cur = (cur as Record<string, unknown>)[seg];\n  }\n  return cur;\n}\n\nexport function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n  const segments = path.split(\".\");\n  const last = segments.pop();\n  if (!last) return;\n  let cur: Record<string, unknown> = obj;\n  for (const seg of segments) {\n    if (!(seg in cur) || typeof cur[seg] !== \"object\" || cur[seg] === null) {\n      cur[seg] = {};\n    }\n    cur = cur[seg] as Record<string, unknown>;\n  }\n  cur[last] = value;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/path.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/flatten-errors.ts",
      "content": "/**\n * Shared error-flattener for RHF's nested error tree.\n *\n * The error tree can be either:\n *   { fieldName: { message: '...', type: '...' } }\n * or deeper:\n *   { address: { city: { message: '...' } } }\n *\n * Both flatten to dot-path keys. Two shapes are exposed because callers want\n * different output: `flattenRhfErrorsToRecord` for callback payloads (record),\n * `flattenRhfErrorsToList` for the summary UI (ordered list).\n */\n\ninterface FlatEntry {\n  path: string;\n  message: string;\n}\n\nfunction* walkRhfErrors(\n  errors: Record<string, unknown>,\n  prefix = \"\",\n): Generator<FlatEntry> {\n  for (const [key, val] of Object.entries(errors)) {\n    if (!val) continue;\n    const path = prefix ? `${prefix}.${key}` : key;\n    if (\n      typeof val === \"object\" &&\n      val !== null &&\n      \"message\" in val &&\n      typeof (val as { message?: unknown }).message === \"string\"\n    ) {\n      yield { path, message: String((val as { message?: unknown }).message) };\n      continue;\n    }\n    if (typeof val === \"object\" && val !== null) {\n      yield* walkRhfErrors(val as Record<string, unknown>, path);\n    }\n  }\n}\n\n/** Flatten errors to `{ 'path.to.field': 'message' }`. Callback-payload shape. */\nexport function flattenRhfErrorsToRecord(\n  errors: Record<string, unknown>,\n): Record<string, string> {\n  const out: Record<string, string> = {};\n  for (const { path, message } of walkRhfErrors(errors)) {\n    out[path] = message;\n  }\n  return out;\n}\n\n/** Flatten errors to an ordered list. Summary-UI shape. */\nexport function flattenRhfErrorsToList(\n  errors: Record<string, unknown>,\n): Array<{ name: string; message: string }> {\n  const out: Array<{ name: string; message: string }> = [];\n  for (const { path, message } of walkRhfErrors(errors)) {\n    out.push({ name: path, message });\n  }\n  return out;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/flatten-errors.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/define-field-renderer.ts",
      "content": "import type { ReactNode } from \"react\";\nimport type {\n  FieldDefinition,\n  FieldRenderer,\n  FieldRendererArgs,\n} from \"../types\";\n\n/**\n * v0.1.7 — narrowed argument shape passed to the typed factory's `impl`\n * callback. Inherits all `FieldRendererArgs` keys except `value` (narrowed\n * to `TValue`) and `field` (narrowed so `field.config[k]` reads as `TConfig`).\n *\n * Internal. The factory unwraps it back to a plain `FieldRenderer` for\n * registry storage — registry consumers see the standard `FieldRendererArgs`\n * shape.\n */\nexport interface NarrowedRendererArgs<TValue, TConfig>\n  extends Omit<FieldRendererArgs, \"value\" | \"field\"> {\n  value: TValue;\n  field: FieldDefinition & {\n    config?: { [key: string]: TConfig };\n  };\n}\n\nexport interface DefineFieldRendererConfig<TValue, TConfig> {\n  /**\n   * Display name surfaced in `<JsonFormDevtools>` and dev-tools panels.\n   * Defaults to `\"AnonymousRenderer\"` when omitted. Attached as a\n   * non-enumerable `displayName` property on the returned `FieldRenderer`\n   * so registry-walker code can pick it up.\n   */\n  displayName?: string;\n  /**\n   * The renderer body. Receives the narrowed args; returns the rendered\n   * React node.\n   */\n  impl: (args: NarrowedRendererArgs<TValue, TConfig>) => ReactNode;\n}\n\n/**\n * v0.1.7 — typed factory for custom field renderers.\n *\n * Returns a plain `FieldRenderer` (the same shape consumers use today via\n * `Record<string, FieldRenderer>`) — so the result drops directly into a\n * registry without any other plumbing. The factory is **type-narrowing\n * only**: there's no runtime narrowing of `value` or `field.config`,\n * because RHF values aren't statically known. The generics are\n * consumer-asserted convenience.\n *\n * @example\n * ```tsx\n * import { defineFieldRenderer } from \"@ilinxa/json-form\";\n *\n * interface ColorConfig {\n *   palette?: string[];\n * }\n *\n * const ColorSwatch = defineFieldRenderer<string, ColorConfig>({\n *   displayName: \"ColorSwatch\",\n *   impl: ({ value, onChange, field, disabled }) => {\n *     const palette = field.config?.color?.palette ?? [\"#FF595E\", \"#FFCA3A\"];\n *     return palette.map((c) => (\n *       <button key={c} onClick={() => onChange(c)} disabled={disabled}>\n *         {value === c ? \"✓\" : \"\"}\n *       </button>\n *     ));\n *   },\n * });\n *\n * const registry = { ...defaultJsonFormRegistry, color: ColorSwatch };\n * ```\n */\nexport function defineFieldRenderer<TValue = unknown, TConfig = unknown>(\n  config: DefineFieldRendererConfig<TValue, TConfig>,\n): FieldRenderer {\n  const renderer: FieldRenderer = (args) =>\n    config.impl(args as unknown as NarrowedRendererArgs<TValue, TConfig>);\n\n  // Attach displayName as a non-enumerable property so registry-walker code\n  // (e.g., `<JsonFormDevtools>`) can read it without it leaking into\n  // `Object.keys` / JSON.stringify of registry maps.\n  Object.defineProperty(renderer, \"displayName\", {\n    value: config.displayName ?? \"AnonymousRenderer\",\n    enumerable: false,\n    writable: false,\n    configurable: false,\n  });\n\n  return renderer;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/define-field-renderer.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/hooks/use-json-form-field-value.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useWatch } from \"react-hook-form\";\nimport { useJsonFormContext } from \"../json-form-context\";\nimport { setByPath } from \"../lib/path\";\n\n/**\n * v0.1.7 — narrow-deps single-field watch. Pure ergonomic wrapper around\n * RHF's `useWatch({ control, name })` scoped to one path, resolved against\n * the active `<JsonFormProvider>` context.\n *\n * **Typing caveat:** the generic `<T>` is consumer-asserted. RHF values\n * aren't statically known — the runtime returns whatever the form currently\n * holds at `name`. Treat the generic as a typed-assertion convenience, not a\n * runtime guarantee.\n *\n * Use this hook to build fully-custom form layouts that subscribe to one\n * field's value without re-rendering on every other field's change. Mirrors\n * the narrow-deps pattern shipped in `useConditional` + `useComputed`\n * (v0.1.6) and exposes it to consumer code.\n *\n * @example\n * ```tsx\n * const country = useJsonFormFieldValue<string>(\"country\");\n * return <p>You selected {country}</p>;\n * ```\n */\nexport function useJsonFormFieldValue<T = unknown>(name: string): T {\n  const ctx = useJsonFormContext();\n  // `as never` matches the codebase's RHF-overload-disambiguation pattern\n  // (see `use-conditional.ts` for the precedent rationale). The v7.76\n  // `compute` overload ambiguates single-string-name calls when the\n  // resolved Control's value type is unknown.\n  return useWatch({ control: ctx.rhf.control, name } as never) as T;\n}\n\n/**\n * v0.1.7 — narrow-deps multi-field watch. Returns a path-keyed bag with\n * only the named fields rehydrated via `setByPath` — so consumers can read\n * `bag[\"address.city\"]` via `getByPath` or destructure nested paths\n * naturally.\n *\n * **Typing caveat:** same as `useJsonFormFieldValue<T>` — the generic is\n * consumer-asserted. The returned shape is structurally a `Record<string,\n * unknown>` enriched with whatever `setByPath` walks produce.\n *\n * @example\n * ```tsx\n * const { firstName, lastName } = useJsonFormFieldsValue<{\n *   firstName: string;\n *   lastName: string;\n * }>([\"firstName\", \"lastName\"]);\n * ```\n */\nexport function useJsonFormFieldsValue<\n  T extends Record<string, unknown> = Record<string, unknown>,\n>(names: ReadonlyArray<string>): T {\n  const ctx = useJsonFormContext();\n  // `as never` per the same overload-disambiguation rationale above.\n  const watched = useWatch({ control: ctx.rhf.control, name: names } as never);\n\n  return useMemo<T>(() => {\n    const arr = Array.isArray(watched) ? watched : [];\n    const bag: Record<string, unknown> = {};\n    names.forEach((n, i) => {\n      setByPath(bag, n, arr[i]);\n    });\n    return bag as T;\n  }, [watched, names]);\n}\n",
      "type": "registry:component",
      "target": "components/json-form/hooks/use-json-form-field-value.ts"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/json-form-devtools.tsx",
      "content": "\"use client\";\n\nimport { Suspense, lazy } from \"react\";\n\n/**\n * v0.1.7 — JsonFormDevtools loader stub. The ~250 LOC panel body lives in\n * `./json-form-devtools-body.tsx` and is fetched via `React.lazy()` only\n * when the component actually renders. In prod (or when the consumer-side\n * import is itself wrapped behind `process.env.NODE_ENV !== \"production\"`),\n * the body chunk is never fetched.\n *\n * **This is NOT a bundler tree-shake** — the loader stub itself still\n * ships in prod chunks unless the consumer-side import is conditional. For\n * true chunk-level elimination, wrap the usage:\n *\n * ```tsx\n * {process.env.NODE_ENV !== \"production\" && <JsonFormDevtools />}\n * ```\n *\n * Override the auto-disable via the `force` prop for prod-debug builds.\n */\n\nconst LazyBody = lazy(() => import(\"./json-form-devtools-body\"));\n\nexport interface JsonFormDevtoolsProps {\n  /**\n   * Render inline (as a block child) instead of as a floating panel\n   * anchored to the bottom-right viewport corner. Inline mode is always\n   * visible; floating mode shows a pill-shaped toggle until opened.\n   */\n  inline?: boolean;\n  /**\n   * Force-enable in production builds. By default the component returns\n   * `null` (no-op) when `process.env.NODE_ENV === \"production\"`.\n   */\n  force?: boolean;\n  /**\n   * Keyboard shortcut to toggle the floating panel open/closed. Ignored in\n   * inline mode. Format: `\"Ctrl+Shift+J\"` (case-insensitive modifiers).\n   * Default: `\"Ctrl+Shift+J\"`.\n   */\n  shortcut?: string;\n  className?: string;\n}\n\nexport function JsonFormDevtools({\n  inline = false,\n  force = false,\n  shortcut = \"Ctrl+Shift+J\",\n  className,\n}: JsonFormDevtoolsProps) {\n  if (process.env.NODE_ENV === \"production\" && !force) return null;\n\n  return (\n    <Suspense fallback={null}>\n      <LazyBody inline={inline} shortcut={shortcut} className={className} />\n    </Suspense>\n  );\n}\n\nexport default JsonFormDevtools;\n",
      "type": "registry:component",
      "target": "components/json-form/parts/json-form-devtools.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/parts/json-form-devtools-body.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useFormState, useWatch } from \"react-hook-form\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  XIcon,\n  ChevronDownIcon,\n  ChevronRightIcon,\n} from \"lucide-react\";\nimport { useJsonFormContext, useJsonFormContextOptional } from \"../json-form-context\";\nimport { useConditional } from \"../hooks/use-conditional\";\nimport { flattenRhfErrorsToList } from \"../lib/flatten-errors\";\nimport { isValueField } from \"../lib/validate-schema\";\nimport type { FieldDefinition } from \"../types\";\n\n/**\n * v0.1.7 — body of the JsonFormDevtools panel. Lives in its own module so\n * the parent `<JsonFormDevtools>` can lazy-load it via `React.lazy()` — the\n * ~250 LOC body chunk only loads when the panel actually renders, while the\n * tiny loader stub ships in the parent module.\n *\n * This module is rendered exclusively from the dynamic-import boundary and\n * is NOT exported from `index.ts`. Consumers reach it only via\n * `<JsonFormDevtools>`.\n */\n\nexport interface JsonFormDevtoolsBodyProps {\n  inline: boolean;\n  shortcut: string;\n  className?: string;\n}\n\ntype Tab = \"schema\" | \"values\" | \"conditionals\" | \"errors\";\n\nexport default function JsonFormDevtoolsBody({\n  inline,\n  shortcut,\n  className,\n}: JsonFormDevtoolsBodyProps) {\n  const maybeCtx = useJsonFormContextOptional();\n  const [open, setOpen] = useState(inline);\n  const [tab, setTab] = useState<Tab>(\"schema\");\n\n  // Keyboard shortcut listener (skipped in inline mode — the panel is\n  // always rendered then).\n  useEffect(() => {\n    if (inline) return;\n    const handler = (e: KeyboardEvent) => {\n      if (matchesShortcut(e, shortcut)) {\n        e.preventDefault();\n        setOpen((o) => !o);\n      }\n    };\n    window.addEventListener(\"keydown\", handler);\n    return () => window.removeEventListener(\"keydown\", handler);\n  }, [inline, shortcut]);\n\n  const wrapperClass = inline\n    ? cn(\n        \"rounded-lg border border-border bg-card text-card-foreground shadow-sm\",\n        className,\n      )\n    : cn(\n        \"fixed bottom-4 right-4 z-9999 flex w-96 max-w-[calc(100vw-2rem)] flex-col rounded-lg border border-border bg-card text-card-foreground shadow-xl\",\n        className,\n      );\n\n  if (!inline && !open) {\n    return (\n      <button\n        type=\"button\"\n        onClick={() => setOpen(true)}\n        className=\"fixed bottom-4 right-4 z-9999 flex h-10 items-center gap-2 rounded-full border border-border bg-card px-4 text-xs font-medium text-foreground shadow-md hover:bg-muted\"\n        aria-label=\"Open JsonForm devtools panel\"\n      >\n        <span className=\"size-2 rounded-full bg-primary\" />\n        json-form devtools\n      </button>\n    );\n  }\n\n  // v0.2.1 (F-06) — soft-warning fallback when no <JsonFormProvider> is in\n  // the tree. Renders an explanatory panel instead of throwing, so the\n  // consumer mistake is visible-but-recoverable rather than a hard crash.\n  if (!maybeCtx) {\n    return (\n      <div className={wrapperClass} data-jsonform-devtools data-state=\"no-provider\">\n        {!inline ? (\n          <header className=\"flex items-center justify-between border-b border-border px-3 py-2\">\n            <span className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n              json-form devtools\n            </span>\n            <button\n              type=\"button\"\n              onClick={() => setOpen(false)}\n              aria-label=\"Close devtools panel\"\n              className=\"rounded p-1 hover:bg-muted\"\n            >\n              <XIcon className=\"size-3.5\" />\n            </button>\n          </header>\n        ) : null}\n        <div className=\"p-3 text-xs text-muted-foreground\">\n          <p className=\"mb-1 font-medium text-foreground\">No JsonForm context found.</p>\n          <p>\n            Place <code className=\"rounded bg-muted px-1\">&lt;JsonFormDevtools /&gt;</code>{\" \"}\n            inside a <code className=\"rounded bg-muted px-1\">&lt;JsonForm&gt;</code> or{\" \"}\n            <code className=\"rounded bg-muted px-1\">&lt;JsonFormProvider&gt;</code>{\" \"}\n            tree so the panel can read schema, values, conditionals, and errors.\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  const ctx = maybeCtx;\n\n  return (\n    <div className={wrapperClass} data-jsonform-devtools>\n      {!inline ? (\n        <header className=\"flex items-center justify-between border-b border-border px-3 py-2\">\n          <span className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n            json-form devtools\n          </span>\n          <div className=\"flex items-center gap-1\">\n            <span className=\"hidden text-[10px] text-muted-foreground/70 sm:inline\">\n              {shortcut}\n            </span>\n            <button\n              type=\"button\"\n              onClick={() => setOpen(false)}\n              aria-label=\"Close devtools panel\"\n              className=\"rounded p-1 hover:bg-muted\"\n            >\n              <XIcon className=\"size-3.5\" />\n            </button>\n          </div>\n        </header>\n      ) : null}\n\n      <nav className=\"flex items-center gap-1 border-b border-border px-2 py-1 text-xs\">\n        {([\"schema\", \"values\", \"conditionals\", \"errors\"] as const).map((t) => (\n          <button\n            key={t}\n            type=\"button\"\n            onClick={() => setTab(t)}\n            className={cn(\n              \"rounded px-2 py-0.5 transition-colors\",\n              tab === t\n                ? \"bg-primary text-primary-foreground\"\n                : \"text-muted-foreground hover:bg-muted\",\n            )}\n          >\n            {t}\n          </button>\n        ))}\n      </nav>\n\n      <div className=\"max-h-100 min-h-40 flex-1 overflow-y-auto p-3 text-xs\">\n        {tab === \"schema\" ? <SchemaTab /> : null}\n        {tab === \"values\" ? <ValuesTab /> : null}\n        {tab === \"conditionals\" ? <ConditionalsTab /> : null}\n        {tab === \"errors\" ? <ErrorsTab /> : null}\n      </div>\n\n      <footer className=\"flex items-center justify-between border-t border-border px-3 py-1.5 text-[10px] text-muted-foreground\">\n        <span data-jsonform-devtools-form-id>{ctx.formId}</span>\n        <span>v0.2.1</span>\n      </footer>\n    </div>\n  );\n}\n\n// ─── Tabs ────────────────────────────────────────────────────────────────────\n\nfunction SchemaTab() {\n  const ctx = useJsonFormContext();\n  const [collapsed, setCollapsed] = useState(false);\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <button\n        type=\"button\"\n        onClick={() => setCollapsed((c) => !c)}\n        className=\"flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground hover:text-foreground\"\n      >\n        {collapsed ? (\n          <ChevronRightIcon className=\"size-3\" />\n        ) : (\n          <ChevronDownIcon className=\"size-3\" />\n        )}\n        FormSchema ({ctx.schema.fields.length} fields)\n      </button>\n      {!collapsed ? (\n        <pre className=\"overflow-x-auto rounded-md border border-border bg-muted/30 p-2 font-mono text-[11px] leading-relaxed text-foreground\">\n          {JSON.stringify(ctx.schema, prettyReplacer, 2)}\n        </pre>\n      ) : null}\n    </div>\n  );\n}\n\nfunction ValuesTab() {\n  const ctx = useJsonFormContext();\n  // Devtools-only subscription — paying the full-bag watch cost is OK here\n  // because devtools panels are excluded from normal render paths.\n  const values = useWatch({ control: ctx.rhf.control }) ?? {};\n\n  return (\n    <pre className=\"overflow-x-auto rounded-md border border-border bg-muted/30 p-2 font-mono text-[11px] leading-relaxed text-foreground\">\n      {JSON.stringify(values, prettyReplacer, 2)}\n    </pre>\n  );\n}\n\nfunction ConditionalsTab() {\n  const ctx = useJsonFormContext();\n\n  const conditionalFields = useMemo(\n    () =>\n      ctx.schema.fields.filter(\n        (f) =>\n          isValueField(f) && (f.visibleWhen || f.enabledWhen || f.requiredWhen),\n      ),\n    [ctx.schema.fields],\n  );\n\n  if (conditionalFields.length === 0) {\n    return (\n      <p className=\"italic text-muted-foreground\">\n        No conditional fields in this schema.\n      </p>\n    );\n  }\n\n  return (\n    <table className=\"w-full border-collapse text-[11px]\">\n      <thead>\n        <tr className=\"text-left text-muted-foreground\">\n          <th className=\"py-1 pr-2 font-medium\">Field</th>\n          <th className=\"py-1 pr-2 font-medium\">visible</th>\n          <th className=\"py-1 pr-2 font-medium\">enabled</th>\n          <th className=\"py-1 pr-2 font-medium\">required</th>\n        </tr>\n      </thead>\n      <tbody className=\"divide-y divide-border\">\n        {conditionalFields.map((f) => (\n          <ConditionalRow key={f.name} field={f} />\n        ))}\n      </tbody>\n    </table>\n  );\n}\n\nfunction ConditionalRow({ field }: { field: FieldDefinition }) {\n  // useConditional handles the narrow-deps subscription internally; the\n  // devtools row re-renders only when this field's evaluated state flips.\n  const { visible, enabled, required } = useConditional(field);\n\n  return (\n    <tr>\n      <td className=\"py-1 pr-2 font-mono text-foreground\">{field.name}</td>\n      <td className=\"py-1 pr-2\">{visible ? \"✓\" : \"✗\"}</td>\n      <td className=\"py-1 pr-2\">{enabled ? \"✓\" : \"✗\"}</td>\n      <td className=\"py-1 pr-2\">{required ? \"✓\" : \"✗\"}</td>\n    </tr>\n  );\n}\n\nfunction ErrorsTab() {\n  const ctx = useJsonFormContext();\n  const { errors } = useFormState({ control: ctx.rhf.control });\n  const flat = flattenRhfErrorsToList(errors as Record<string, unknown>);\n\n  if (flat.length === 0) {\n    return (\n      <p className=\"italic text-muted-foreground\">No errors. Form is valid.</p>\n    );\n  }\n\n  return (\n    <ul className=\"flex flex-col gap-1\">\n      {flat.map(({ name, message }) => (\n        <li\n          key={name}\n          className=\"flex items-baseline gap-2 rounded border border-destructive/30 bg-destructive/5 p-1.5\"\n        >\n          <span className=\"font-mono text-foreground\">{name}</span>\n          <span className=\"text-destructive\">{message}</span>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * JSON.stringify replacer that turns function values into a placeholder\n * (raw schemas often hold validate / compute / visibleWhen callbacks). Also\n * serializes Map / Set / BigInt — `JSON.stringify` itself throws on bigint\n * (v0.2.1 F-08), so the panel would crash if a consumer's form held a\n * bigint value.\n */\nfunction prettyReplacer(_key: string, value: unknown): unknown {\n  if (typeof value === \"function\") return \"[Function]\";\n  if (typeof value === \"bigint\") return `${value.toString()}n`;\n  if (value instanceof Map) return Object.fromEntries(value);\n  if (value instanceof Set) return Array.from(value);\n  return value;\n}\n\n/**\n * Matches a KeyboardEvent against a shortcut string like \"Ctrl+Shift+J\".\n * Modifiers are case-insensitive; the final key compares against\n * `event.key` (case-insensitive too).\n */\nfunction matchesShortcut(e: KeyboardEvent, shortcut: string): boolean {\n  const parts = shortcut.split(\"+\").map((p) => p.trim().toLowerCase());\n  const target = parts.pop();\n  if (!target) return false;\n  if (parts.includes(\"ctrl\") !== e.ctrlKey) return false;\n  if (parts.includes(\"shift\") !== e.shiftKey) return false;\n  if (parts.includes(\"alt\") !== e.altKey) return false;\n  if (parts.includes(\"meta\") !== e.metaKey) return false;\n  return e.key.toLowerCase() === target;\n}\n",
      "type": "registry:component",
      "target": "components/json-form/parts/json-form-devtools-body.tsx"
    },
    {
      "path": "src/registry/components/forms/json-form/lib/validate-schema.ts",
      "content": "import type { FieldDefinition, FormSchema } from \"../types\";\nimport { extractConditionOrFnDeps } from \"./condition-evaluator\";\n\n/**\n * Dev-only sanity checks. Emits warnings via `console.warn`. Does NOT throw —\n * a broken schema should still render something (the offending field falls\n * back to \"always visible / enabled / always-true\" defaults).\n *\n * Skipped in production builds (no-op).\n */\nexport function validateSchemaDev(schema: FormSchema): void {\n  if (process.env.NODE_ENV === \"production\") return;\n\n  // (1) Duplicate field names\n  const seen = new Set<string>();\n  for (const field of schema.fields) {\n    if (seen.has(field.name)) {\n      console.warn(\n        `[json-form] Duplicate field name \"${field.name}\". First occurrence renders; others are ignored.`,\n      );\n    }\n    seen.add(field.name);\n  }\n\n  // (2) Value-carrying types without a name (shouldn't be possible per TS,\n  //     but defensive in case consumers pass loose JSON)\n  for (const field of schema.fields) {\n    if (!field.name) {\n      console.warn(\n        `[json-form] Field of type \"${field.type}\" has no name. All field types require a stable name.`,\n      );\n    }\n  }\n\n  // (3) Active-conditional count — perf ceiling (O1 lock; threshold bumped\n  // from 50 → 200 in v0.2.0 since per-keystroke render count no longer scales\n  // with conditional count under the default-registry watch drop).\n  let conditionalCount = 0;\n  for (const field of schema.fields) {\n    if (field.visibleWhen) conditionalCount++;\n    if (field.enabledWhen) conditionalCount++;\n    if (field.requiredWhen) conditionalCount++;\n  }\n  if (conditionalCount > 200) {\n    console.warn(\n      `[json-form] Schema has ${conditionalCount} active conditionals (visibleWhen / enabledWhen / requiredWhen). ` +\n        `Performance may degrade past ~200; consider hoisting conditionals into a derived schema or splitting the form.`,\n    );\n  }\n\n  // (4) Conditional dependency cycles — A.visibleWhen depends on B, B.visibleWhen depends on A, etc.\n  const depGraph = new Map<string, Set<string>>();\n  for (const field of schema.fields) {\n    const deps = new Set<string>();\n    for (const key of [\"visibleWhen\", \"enabledWhen\", \"requiredWhen\"] as const) {\n      const c = field[key];\n      if (!c) continue;\n      const fieldDeps = extractConditionOrFnDeps(c);\n      if (fieldDeps) fieldDeps.forEach((d) => deps.add(d));\n    }\n    if (deps.size) depGraph.set(field.name, deps);\n  }\n\n  function hasCycle(start: string, visited: Set<string>, stack: Set<string>): string[] | null {\n    if (stack.has(start)) return [start];\n    if (visited.has(start)) return null;\n    visited.add(start);\n    stack.add(start);\n    const deps = depGraph.get(start);\n    if (deps) {\n      for (const d of deps) {\n        const result = hasCycle(d, visited, stack);\n        if (result) return [start, ...result];\n      }\n    }\n    stack.delete(start);\n    return null;\n  }\n\n  const cycleVisited = new Set<string>();\n  for (const name of depGraph.keys()) {\n    if (cycleVisited.has(name)) continue;\n    const cycle = hasCycle(name, cycleVisited, new Set<string>());\n    if (cycle) {\n      console.warn(\n        `[json-form] Conditional dependency cycle detected: ${cycle.join(\" → \")}. ` +\n          `Falling back to \"always true\" for the cyclic fields.`,\n      );\n      break; // one warn is enough; consumer can fix and re-run\n    }\n  }\n\n  // (5) v0.1.7 — dangling `dependsOn` references. Match is dot-path-as-flat\n  // -string against `schema.fields[i].name` exactly. Fields declared via\n  // nested `defaultValue` shapes (e.g., `{ name: 'address', defaultValue:\n  // { city: '…' } }`) WON'T satisfy this lint for `'address.city'` —\n  // consumers using nested-default patterns need individual leaf entries\n  // to declare them lint-visible. Runtime watch-gating that honors\n  // `dependsOn` ships in v0.2.0; the lint runs in v0.1.7 so consumers can\n  // author forward-compatible schemas now.\n  const knownNames = new Set(schema.fields.map((f) => f.name));\n  for (const field of schema.fields) {\n    if (!field.dependsOn) continue;\n    for (const dep of field.dependsOn) {\n      if (knownNames.has(dep)) continue;\n      console.warn(\n        `[json-form] Field \"${field.name}\" declares dependsOn: \"${dep}\", ` +\n          `but no field with that exact name exists in the schema. ` +\n          `Lint matches against schema.fields[].name as a flat dot-path string.`,\n      );\n    }\n  }\n}\n\n/** Convenience: skip `section` / `divider` types when walking value-carrying fields. */\nexport function isValueField(field: FieldDefinition): boolean {\n  return field.type !== \"section\" && field.type !== \"divider\";\n}\n",
      "type": "registry:component",
      "target": "components/json-form/lib/validate-schema.ts"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:block"
}