{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "properties-form",
  "title": "Properties Form",
  "author": "ilinxa",
  "description": "Schema-driven read and edit form for typed records — six field types, per-field permissions, sync validation, and a custom renderer slot.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "switch",
    "select",
    "textarea",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/components/forms/properties-form/properties-form.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n} from \"react\";\nimport type {\n  PropertiesFormHandle,\n  PropertiesFormProps,\n  SubmitResult,\n} from \"./types\";\nimport { useFormReducer } from \"./hooks/use-form-reducer\";\nimport { useIdFactory } from \"./hooks/use-id-factory\";\nimport { useSubmitSpinner } from \"./hooks/use-submit-spinner\";\nimport { resolveFieldPermission } from \"./lib/resolve-permission\";\nimport {\n  safeRunFormValidate,\n  validateAllFields,\n} from \"./lib/validate\";\nimport { ErrorSummary } from \"./parts/error-summary\";\nimport { FieldRow, type FocusTarget } from \"./parts/field-row\";\nimport { SubmitActions } from \"./parts/submit-actions\";\nimport { cn } from \"@/lib/utils\";\n\nconst SCHEMA_INSTABILITY_THRESHOLD = 5;\n\nexport function PropertiesForm<T extends Record<string, unknown> = Record<string, unknown>>(\n  props: PropertiesFormProps<T>,\n) {\n  const {\n    schema,\n    values,\n    mode = \"read\",\n    onModeChange,\n    onChange,\n    onSubmit,\n    onCancel,\n    resolvePermission,\n    validate,\n    showSubmitActions = true,\n    submitLabel = \"Save\",\n    cancelLabel = \"Cancel\",\n    ariaLabel,\n    className,\n    ref,\n  } = props;\n\n  const reactFormId = useId();\n  const formElementId = `${reactFormId}-form`;\n\n  const [state, dispatch] = useFormReducer(values);\n  const getIds = useIdFactory();\n\n  const valuesRef = useRef(values);\n  const schemaRef = useRef(schema);\n  const onChangeRef = useRef(onChange);\n  const onSubmitRef = useRef(onSubmit);\n  const validateRef = useRef(validate);\n  const onCancelRef = useRef(onCancel);\n  const onModeChangeRef = useRef(onModeChange);\n\n  useEffect(() => {\n    valuesRef.current = values;\n    schemaRef.current = schema;\n    onChangeRef.current = onChange;\n    onSubmitRef.current = onSubmit;\n    validateRef.current = validate;\n    onCancelRef.current = onCancel;\n    onModeChangeRef.current = onModeChange;\n  });\n\n  const submitIdRef = useRef(0);\n  const focusTargetsRef = useRef<Map<string, FocusTarget>>(new Map());\n\n  const registerFocusTarget = useCallback(\n    (key: string, target: FocusTarget | null) => {\n      if (target) focusTargetsRef.current.set(key, target);\n      else focusTargetsRef.current.delete(key);\n    },\n    [],\n  );\n\n  const focusField = useCallback((key: string) => {\n    const target = focusTargetsRef.current.get(key);\n    const dev = process.env.NODE_ENV !== \"production\";\n    if (!target) {\n      if (!schemaRef.current.some((f) => f.key === key)) {\n        if (dev) {\n          console.error(\n            `[properties-form] focusField(\"${key}\") — unknown field key.`,\n          );\n        }\n        return;\n      }\n      const field = schemaRef.current.find((f) => f.key === key);\n      if (field) {\n        const perm = resolveFieldPermission(\n          field,\n          valuesRef.current,\n          resolvePermission as never,\n        );\n        if (perm === \"hidden\" && dev) {\n          console.warn(\n            `[properties-form] focusField(\"${key}\") — field is hidden; no-op.`,\n          );\n        }\n      }\n      return;\n    }\n    target.focus();\n  }, [resolvePermission]);\n\n  const handleFieldChange = useCallback(\n    (\n      _key: string,\n      nextValues: Record<string, unknown>,\n      error: string | undefined,\n    ) => {\n      dispatch({\n        type: \"field-changed\",\n        key: _key,\n        nextValues,\n        error,\n      });\n      onChangeRef.current?.(nextValues as T);\n    },\n    [dispatch],\n  );\n\n  const handleFieldBlur = useCallback(\n    (key: string, error: string | undefined) => {\n      dispatch({ type: \"field-blurred\", key, error });\n    },\n    [dispatch],\n  );\n\n  const handleSpinnerShow = useCallback(() => {\n    dispatch({ type: \"submit-spinner-show\" });\n  }, [dispatch]);\n\n  useSubmitSpinner(state.pending, handleSpinnerShow);\n\n  const focusFirstErrorKey = useCallback(\n    (errors: Record<string, string>) => {\n      const firstKey = schemaRef.current.find((f) => errors[f.key])?.key;\n      if (firstKey) focusField(firstKey);\n    },\n    [focusField],\n  );\n\n  const submit = useCallback(async (): Promise<SubmitResult> => {\n    const submitId = ++submitIdRef.current;\n    const currentValues = valuesRef.current;\n    const fieldErrors = validateAllFields(schemaRef.current, currentValues);\n    const formResult = safeRunFormValidate(validateRef.current, currentValues);\n    const merged: Record<string, string> = { ...formResult.errors };\n    for (const [k, v] of Object.entries(fieldErrors)) {\n      merged[k] = v;\n    }\n\n    dispatch({ type: \"submit-started\" });\n\n    if (Object.keys(merged).length > 0 || formResult.formError) {\n      dispatch({\n        type: \"submit-failed\",\n        errors: merged,\n        formError: formResult.formError,\n      });\n      focusFirstErrorKey(merged);\n      return { ok: false, errors: merged };\n    }\n\n    const handler = onSubmitRef.current;\n    if (!handler) {\n      dispatch({\n        type: \"submit-succeeded\",\n        cleanSnapshot: currentValues,\n      });\n      return { ok: true };\n    }\n\n    try {\n      const result = await handler(currentValues as T);\n      if (submitId !== submitIdRef.current) {\n        return result;\n      }\n      if (result.ok) {\n        dispatch({\n          type: \"submit-succeeded\",\n          cleanSnapshot: valuesRef.current,\n        });\n      } else {\n        const errors = result.errors ?? {};\n        dispatch({\n          type: \"submit-failed\",\n          errors,\n          formError: undefined,\n        });\n        focusFirstErrorKey(errors);\n      }\n      return result;\n    } catch (err) {\n      if (submitId !== submitIdRef.current) {\n        return { ok: false };\n      }\n      const message =\n        err instanceof Error ? err.message : \"Unknown error\";\n      if (process.env.NODE_ENV !== \"production\") {\n        console.error(\"[properties-form] onSubmit rejected:\", err);\n      }\n      dispatch({\n        type: \"submit-failed\",\n        errors: {},\n        formError: `Submit failed: ${message}`,\n      });\n      return { ok: false };\n    }\n  }, [dispatch, focusFirstErrorKey]);\n\n  const handleSubmitEvent = useCallback(\n    (e: React.FormEvent<HTMLFormElement>) => {\n      e.preventDefault();\n      void submit();\n    },\n    [submit],\n  );\n\n  const handleCancel = useCallback(() => {\n    submitIdRef.current++;\n    dispatch({ type: \"reset\", cleanSnapshot: state.cleanSnapshot });\n    onChangeRef.current?.(state.cleanSnapshot as T);\n    onCancelRef.current?.();\n    onModeChangeRef.current?.(\"read\");\n  }, [dispatch, state.cleanSnapshot]);\n\n  const reset = useCallback(() => {\n    submitIdRef.current++;\n    dispatch({ type: \"reset\", cleanSnapshot: state.cleanSnapshot });\n    onChangeRef.current?.(state.cleanSnapshot as T);\n  }, [dispatch, state.cleanSnapshot]);\n\n  const markClean = useCallback(() => {\n    dispatch({\n      type: \"mark-clean\",\n      cleanSnapshot: valuesRef.current,\n    });\n  }, [dispatch]);\n\n  const isDirty = useCallback(\n    () => state.version !== state.cleanVersion,\n    [state.version, state.cleanVersion],\n  );\n\n  useImperativeHandle(\n    ref,\n    (): PropertiesFormHandle => ({\n      isDirty,\n      markClean,\n      reset,\n      focusField,\n      submit,\n    }),\n    [isDirty, markClean, reset, focusField, submit],\n  );\n\n  const editWithoutOnChangeRef = useRef(false);\n  useEffect(() => {\n    if (mode === \"edit\" && !onChange) {\n      if (!editWithoutOnChangeRef.current) {\n        editWithoutOnChangeRef.current = true;\n        if (process.env.NODE_ENV !== \"production\") {\n          console.error(\n            \"[properties-form] mode=\\\"edit\\\" requires `onChange` — value commits will be no-ops.\",\n          );\n        }\n      }\n    } else {\n      editWithoutOnChangeRef.current = false;\n    }\n  }, [mode, onChange]);\n\n  const schemaInstabilityRef = useRef({ last: schema, count: 0, warned: false });\n  useEffect(() => {\n    const tracker = schemaInstabilityRef.current;\n    if (tracker.last === schema) {\n      tracker.count = 0;\n      return;\n    }\n    tracker.last = schema;\n    tracker.count++;\n    if (\n      tracker.count > SCHEMA_INSTABILITY_THRESHOLD &&\n      !tracker.warned &&\n      process.env.NODE_ENV !== \"production\"\n    ) {\n      tracker.warned = true;\n      console.warn(\n        \"[properties-form] `schema` prop is changing every render. \" +\n          \"Wrap with `useMemo` or hoist to module scope to avoid memo invalidation.\",\n      );\n    }\n  }, [schema]);\n\n  const visibleSchema = useMemo(\n    () =>\n      schema.filter((f) => {\n        const perm = resolveFieldPermission(\n          f,\n          values,\n          resolvePermission as never,\n        );\n        return perm !== \"hidden\";\n      }),\n    [schema, values, resolvePermission],\n  );\n\n  const errorVisibleFor = useCallback(\n    (key: string): boolean =>\n      state.submitAttempted || !!state.blurredWithError[key],\n    [state.submitAttempted, state.blurredWithError],\n  );\n\n  return (\n    <form\n      id={formElementId}\n      noValidate\n      aria-label={ariaLabel}\n      aria-busy={state.pending || undefined}\n      onSubmit={handleSubmitEvent}\n      className={cn(\"flex flex-col gap-4\", className)}\n    >\n      {state.submitAttempted ? (\n        <ErrorSummary\n          schema={visibleSchema}\n          errors={state.errors}\n          formError={state.formError}\n          onFocusField={focusField}\n        />\n      ) : null}\n      <div className=\"flex flex-col gap-4\">\n        {visibleSchema.map((field) => (\n          <FieldRow\n            key={field.key}\n            field={field}\n            values={values}\n            mode={mode}\n            error={state.errors[field.key]}\n            errorVisible={errorVisibleFor(field.key)}\n            pending={state.pending}\n            hostResolver={resolvePermission as never}\n            ids={getIds(field.key)}\n            onFieldChange={handleFieldChange}\n            onFieldBlur={handleFieldBlur}\n            registerFocusTarget={registerFocusTarget}\n          />\n        ))}\n      </div>\n      {showSubmitActions && mode === \"edit\" ? (\n        <SubmitActions\n          submitLabel={submitLabel}\n          cancelLabel={cancelLabel}\n          pending={state.pending}\n          showSpinner={state.showSpinner}\n          onCancel={handleCancel}\n          canCancel={!state.pending}\n        />\n      ) : null}\n    </form>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/properties-form.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/index.ts",
      "content": "export { PropertiesForm } from \"./properties-form\";\nexport { flatten, unflatten } from \"./lib/flatten\";\nexport type {\n  FieldOption,\n  FieldPermission,\n  FieldRendererProps,\n  FieldType,\n  FormMode,\n  PropertiesFormField,\n  PropertiesFormHandle,\n  PropertiesFormProps,\n  SubmitResult,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/properties-form/index.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/types.ts",
      "content": "import type { ComponentType, Ref } from \"react\";\n\nexport type FieldType =\n  | \"string\"\n  | \"number\"\n  | \"boolean\"\n  | \"date\"\n  | \"select\"\n  | \"textarea\";\n\nexport type FieldPermission = \"editable\" | \"read-only\" | \"hidden\";\n\nexport type FormMode = \"read\" | \"edit\";\n\nexport interface FieldOption {\n  value: string;\n  label: string;\n}\n\nexport interface PropertiesFormField {\n  key: string;\n  type: FieldType;\n  label: string;\n  description?: string;\n  required?: boolean;\n  options?: ReadonlyArray<FieldOption>;\n  placeholder?: string;\n  permission?: FieldPermission;\n  permissionReason?: string;\n  validate?: (\n    value: unknown,\n    allValues: Record<string, unknown>,\n  ) => string | undefined;\n  renderer?: ComponentType<FieldRendererProps>;\n}\n\nexport interface FieldRendererProps<V = unknown> {\n  value: V;\n  onChange: (value: V) => void;\n  field: PropertiesFormField;\n  allValues: Record<string, unknown>;\n  mode: FormMode;\n  error: string | undefined;\n  disabled: boolean;\n  fieldId: string;\n  errorId: string;\n}\n\nexport interface SubmitResult {\n  ok: boolean;\n  errors?: Record<string, string>;\n}\n\nexport interface PropertiesFormProps<\n  T extends Record<string, unknown> = Record<string, unknown>,\n> {\n  schema: ReadonlyArray<PropertiesFormField>;\n  values: T;\n\n  mode?: FormMode;\n  onModeChange?: (mode: FormMode) => void;\n\n  onChange?: (values: T) => void;\n  onSubmit?: (values: T) => Promise<SubmitResult>;\n  onCancel?: () => void;\n\n  resolvePermission?: (\n    field: PropertiesFormField,\n    values: T,\n  ) => FieldPermission | undefined;\n  validate?: (values: T) => Record<string, string> | undefined;\n\n  showSubmitActions?: boolean;\n  submitLabel?: string;\n  cancelLabel?: string;\n\n  ariaLabel?: string;\n  className?: string;\n\n  ref?: Ref<PropertiesFormHandle>;\n}\n\nexport interface PropertiesFormHandle {\n  isDirty(): boolean;\n  markClean(): void;\n  reset(): void;\n  focusField(key: string): void;\n  submit(): Promise<SubmitResult>;\n}\n\nexport interface FormState {\n  errors: Record<string, string>;\n  formError: string | undefined;\n  pending: boolean;\n  showSpinner: boolean;\n  version: number;\n  cleanVersion: number;\n  cleanSnapshot: Record<string, unknown>;\n  submitAttempted: boolean;\n  blurredWithError: Record<string, true>;\n}\n\nexport type FormAction =\n  | {\n      type: \"field-changed\";\n      key: string;\n      nextValues: Record<string, unknown>;\n      error: string | undefined;\n    }\n  | {\n      type: \"field-blurred\";\n      key: string;\n      error: string | undefined;\n    }\n  | { type: \"submit-started\" }\n  | { type: \"submit-spinner-show\" }\n  | {\n      type: \"submit-succeeded\";\n      cleanSnapshot: Record<string, unknown>;\n    }\n  | {\n      type: \"submit-failed\";\n      errors: Record<string, string>;\n      formError: string | undefined;\n    }\n  | { type: \"mark-clean\"; cleanSnapshot: Record<string, unknown> }\n  | { type: \"reset\"; cleanSnapshot: Record<string, unknown> };\n",
      "type": "registry:component",
      "target": "components/properties-form/types.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/error-summary.tsx",
      "content": "\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport type { PropertiesFormField } from \"../types\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ErrorSummaryProps {\n  schema: ReadonlyArray<PropertiesFormField>;\n  errors: Record<string, string>;\n  formError: string | undefined;\n  onFocusField: (key: string) => void;\n  className?: string;\n}\n\nexport function ErrorSummary({\n  schema,\n  errors,\n  formError,\n  onFocusField,\n  className,\n}: ErrorSummaryProps) {\n  const orderedKeys = schema\n    .map((f) => f.key)\n    .filter((k) => errors[k]);\n  if (!formError && orderedKeys.length === 0) return null;\n\n  return (\n    <div\n      role=\"alert\"\n      className={cn(\n        \"mb-4 rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start gap-2\">\n        <AlertCircle aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n        <div className=\"flex-1\">\n          <p className=\"font-medium\">\n            {formError ?? \"Please fix the errors below.\"}\n          </p>\n          {orderedKeys.length > 0 ? (\n            <ul className=\"mt-2 space-y-0.5\">\n              {orderedKeys.map((key) => {\n                const field = schema.find((f) => f.key === key);\n                const label = field?.label ?? key;\n                return (\n                  <li key={key}>\n                    <button\n                      type=\"button\"\n                      onClick={() => onFocusField(key)}\n                      className=\"text-left underline underline-offset-2 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/60 focus-visible:rounded-sm\"\n                    >\n                      {label}: {errors[key]}\n                    </button>\n                  </li>\n                );\n              })}\n            </ul>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/error-summary.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-boolean.tsx",
      "content": "\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { Switch } from \"@/components/ui/switch\";\nimport type { FieldRendererProps } from \"../types\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldBoolean({\n  value,\n  onChange,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  onBlur,\n  required,\n}: BuiltInFieldProps) {\n  const checked = value === true;\n  if (mode === \"read\") {\n    return (\n      <span className=\"inline-flex items-center gap-1.5 text-sm\">\n        {checked ? (\n          <Check\n            aria-label=\"Yes\"\n            className=\"size-4 text-primary\"\n          />\n        ) : (\n          <X\n            aria-label=\"No\"\n            className=\"size-4 text-muted-foreground\"\n          />\n        )}\n        <span className=\"sr-only\">{checked ? \"Yes\" : \"No\"}</span>\n      </span>\n    );\n  }\n  return (\n    <Switch\n      id={fieldId}\n      checked={checked}\n      disabled={disabled}\n      onCheckedChange={(next) => onChange(next)}\n      onBlur={onBlur}\n      aria-required={required || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-describedby={error ? errorId : undefined}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-boolean.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-date.tsx",
      "content": "\"use client\";\n\nimport type { FieldRendererProps } from \"../types\";\nimport { formatFieldValue } from \"../lib/format-value\";\nimport { cn } from \"@/lib/utils\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldDate({\n  value,\n  onChange,\n  field,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  onBlur,\n  required,\n}: BuiltInFieldProps) {\n  if (mode === \"read\") {\n    return (\n      <span className=\"block font-mono text-sm\">\n        {formatFieldValue(field, value)}\n      </span>\n    );\n  }\n  const inputValue =\n    typeof value === \"string\"\n      ? value\n      : value instanceof Date && !Number.isNaN(value.getTime())\n        ? value.toISOString().slice(0, 10)\n        : \"\";\n  return (\n    <input\n      id={fieldId}\n      type=\"date\"\n      value={inputValue}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      onChange={(e) => onChange(e.target.value)}\n      onBlur={onBlur}\n      aria-required={required || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-describedby={error ? errorId : undefined}\n      className={cn(\n        \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 font-mono text-sm transition-colors outline-none\",\n        \"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50\",\n        \"disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50\",\n        \"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20\",\n        \"dark:bg-input/30 dark:disabled:bg-input/80\",\n      )}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-date.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-error.tsx",
      "content": "import { AlertCircle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface FieldErrorProps {\n  id: string;\n  message: string;\n  className?: string;\n}\n\nexport function FieldError({ id, message, className }: FieldErrorProps) {\n  return (\n    <p\n      id={id}\n      role=\"alert\"\n      className={cn(\n        \"mt-1 flex items-start gap-1.5 text-xs text-destructive\",\n        className,\n      )}\n    >\n      <AlertCircle aria-hidden=\"true\" className=\"mt-0.5 size-3 shrink-0\" />\n      <span>{message}</span>\n    </p>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-error.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-number.tsx",
      "content": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport type { FieldRendererProps } from \"../types\";\nimport { formatFieldValue } from \"../lib/format-value\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldNumber({\n  value,\n  onChange,\n  field,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  onBlur,\n  required,\n}: BuiltInFieldProps) {\n  if (mode === \"read\") {\n    return (\n      <span className=\"block truncate text-right font-mono text-sm tabular-nums\">\n        {formatFieldValue(field, value)}\n      </span>\n    );\n  }\n  const inputValue =\n    value === undefined || value === null\n      ? \"\"\n      : typeof value === \"number\"\n        ? Number.isFinite(value)\n          ? String(value)\n          : \"\"\n        : String(value);\n  return (\n    <Input\n      id={fieldId}\n      type=\"number\"\n      inputMode=\"decimal\"\n      value={inputValue}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      onChange={(e) => {\n        const raw = e.target.value;\n        if (raw === \"\") {\n          onChange(undefined);\n          return;\n        }\n        const parsed = Number(raw);\n        onChange(Number.isNaN(parsed) ? raw : parsed);\n      }}\n      onBlur={onBlur}\n      aria-required={required || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-describedby={error ? errorId : undefined}\n      className=\"text-right font-mono tabular-nums\"\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-number.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-row.tsx",
      "content": "\"use client\";\n\nimport {\n  Component,\n  useCallback,\n  useMemo,\n  type ComponentType,\n  type ErrorInfo,\n  type ReactNode,\n} from \"react\";\nimport type {\n  FieldPermission,\n  FieldRendererProps,\n  FormMode,\n  PropertiesFormField,\n} from \"../types\";\nimport { resolveFieldPermission } from \"../lib/resolve-permission\";\nimport { runRequiredCheck, safeValidateField } from \"../lib/validate\";\nimport { formatFieldValue } from \"../lib/format-value\";\nimport type { FieldIds } from \"../hooks/use-id-factory\";\nimport { FieldError } from \"./field-error\";\nimport { PermissionTooltip } from \"./permission-tooltip\";\nimport { FieldString } from \"./field-string\";\nimport { FieldNumber } from \"./field-number\";\nimport { FieldBoolean } from \"./field-boolean\";\nimport { FieldDate } from \"./field-date\";\nimport { FieldSelect } from \"./field-select\";\nimport { FieldTextarea } from \"./field-textarea\";\nimport { cn } from \"@/lib/utils\";\n\ninterface FieldRowProps {\n  field: PropertiesFormField;\n  values: Record<string, unknown>;\n  mode: FormMode;\n  error: string | undefined;\n  errorVisible: boolean;\n  pending: boolean;\n  hostResolver?: (\n    field: PropertiesFormField,\n    values: Record<string, unknown>,\n  ) => FieldPermission | undefined;\n  ids: FieldIds;\n  onFieldChange: (\n    key: string,\n    nextValues: Record<string, unknown>,\n    error: string | undefined,\n  ) => void;\n  onFieldBlur: (key: string, error: string | undefined) => void;\n  registerFocusTarget: (key: string, target: FocusTarget | null) => void;\n}\n\nexport interface FocusTarget {\n  permission: FieldPermission;\n  focus: () => void;\n}\n\ninterface BuiltInComponentProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nconst BUILT_IN_RENDERERS: Record<\n  PropertiesFormField[\"type\"],\n  ComponentType<BuiltInComponentProps>\n> = {\n  string: FieldString,\n  number: FieldNumber,\n  boolean: FieldBoolean,\n  date: FieldDate,\n  select: FieldSelect,\n  textarea: FieldTextarea,\n};\n\nconst AUTO_COMMIT_TYPES: ReadonlySet<PropertiesFormField[\"type\"]> = new Set([\n  \"select\",\n  \"boolean\",\n]);\n\nclass RendererErrorBoundary extends Component<\n  { fieldKey: string; errorId: string; children: ReactNode },\n  { hasError: boolean }\n> {\n  override state = { hasError: false };\n  static getDerivedStateFromError() {\n    return { hasError: true };\n  }\n  override componentDidCatch(error: Error, info: ErrorInfo) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(\n        `[properties-form] renderer crashed for field \"${this.props.fieldKey}\":`,\n        error,\n        info,\n      );\n    }\n  }\n  override render() {\n    if (this.state.hasError) {\n      return (\n        <FieldError\n          id={this.props.errorId}\n          message=\"Custom renderer crashed — see console\"\n        />\n      );\n    }\n    return this.props.children;\n  }\n}\n\nfunction FieldRowImpl({\n  field,\n  values,\n  mode,\n  error,\n  errorVisible,\n  pending,\n  hostResolver,\n  ids,\n  onFieldChange,\n  onFieldBlur,\n  registerFocusTarget,\n}: FieldRowProps) {\n  const permission = useMemo(\n    () => resolveFieldPermission(field, values, hostResolver),\n    [field, values, hostResolver],\n  );\n\n  const value = values[field.key];\n  const computeError = useCallback(\n    (next: unknown, nextValues: Record<string, unknown>) => {\n      const requiredError = runRequiredCheck(field, next);\n      if (requiredError) return requiredError;\n      return safeValidateField(field, next, nextValues);\n    },\n    [field],\n  );\n\n  const handleChange = useCallback(\n    (nextValue: unknown) => {\n      const nextValues = { ...values, [field.key]: nextValue };\n      const nextError = computeError(nextValue, nextValues);\n      onFieldChange(field.key, nextValues, nextError);\n      if (AUTO_COMMIT_TYPES.has(field.type)) {\n        onFieldBlur(field.key, nextError);\n      }\n    },\n    [values, field.key, field.type, computeError, onFieldChange, onFieldBlur],\n  );\n\n  const handleBlur = useCallback(() => {\n    const blurError = computeError(values[field.key], values);\n    onFieldBlur(field.key, blurError);\n  }, [values, field.key, computeError, onFieldBlur]);\n\n  const dev = process.env.NODE_ENV !== \"production\";\n  if (field.type === \"select\" && (!field.options || field.options.length === 0)) {\n    if (dev) {\n      console.error(\n        `[properties-form] select field \"${field.key}\" is missing \\`options\\`.`,\n      );\n    }\n  }\n\n  if (permission === \"hidden\") return null;\n\n  const showError = errorVisible && !!error;\n  const ariaDescribedBy =\n    [\n      field.description ? ids.descriptionId : undefined,\n      showError ? ids.errorId : undefined,\n    ]\n      .filter(Boolean)\n      .join(\" \") || undefined;\n\n  const rendererProps: FieldRendererProps = {\n    value,\n    onChange: handleChange,\n    field,\n    allValues: values,\n    mode,\n    error: showError ? error : undefined,\n    disabled: permission === \"read-only\" || pending,\n    fieldId: ids.fieldId,\n    errorId: ids.errorId,\n  };\n\n  let content: ReactNode;\n\n  if (mode === \"edit\" && permission === \"read-only\") {\n    const display = formatFieldValue(field, value);\n    content = (\n      <PermissionTooltip\n        reason={field.permissionReason ?? \"This field is read-only.\"}\n        tooltipId={ids.permissionTooltipId}\n      >\n        <span\n          ref={(el) =>\n            registerFocusTarget(field.key, el ? { permission, focus: () => el.focus() } : null)\n          }\n          aria-readonly=\"true\"\n          aria-describedby={ariaDescribedBy}\n          className=\"block min-h-8 truncate rounded-md bg-muted/30 px-2 py-1 text-sm text-muted-foreground\"\n        >\n          {display}\n        </span>\n      </PermissionTooltip>\n    );\n  } else if (field.renderer) {\n    const CustomRenderer = field.renderer;\n    content = (\n      <RendererErrorBoundary fieldKey={field.key} errorId={ids.errorId}>\n        <div\n          ref={(el) =>\n            registerFocusTarget(\n              field.key,\n              el\n                ? {\n                    permission,\n                    focus: () => {\n                      const focusable = el.querySelector<HTMLElement>(\n                        \"input, textarea, select, button, [tabindex]:not([tabindex=\\\"-1\\\"])\",\n                      );\n                      (focusable ?? el).focus();\n                    },\n                  }\n                : null,\n            )\n          }\n        >\n          <CustomRenderer {...rendererProps} />\n        </div>\n      </RendererErrorBoundary>\n    );\n  } else if (mode === \"read\") {\n    const display = formatFieldValue(field, value);\n    content = <span className=\"block truncate text-sm\">{display}</span>;\n  } else {\n    const Renderer = BUILT_IN_RENDERERS[field.type];\n    content = (\n      <div\n        ref={(el) =>\n          registerFocusTarget(\n            field.key,\n            el\n              ? {\n                  permission,\n                  focus: () => {\n                    const focusable = el.querySelector<HTMLElement>(\n                      `#${CSS.escape(ids.fieldId)}, input, textarea, button, [role=\"combobox\"]`,\n                    );\n                    (focusable ?? el).focus();\n                  },\n                }\n              : null,\n          )\n        }\n      >\n        <Renderer\n          {...rendererProps}\n          onBlur={handleBlur}\n          required={!!field.required}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\")}>\n      <label\n        htmlFor={ids.fieldId}\n        className=\"flex items-center gap-1 text-sm font-medium text-foreground\"\n      >\n        <span>{field.label}</span>\n        {field.required ? (\n          <span aria-hidden=\"true\" className=\"text-destructive\">\n            *\n          </span>\n        ) : null}\n      </label>\n      {field.description ? (\n        <p\n          id={ids.descriptionId}\n          className=\"text-xs text-muted-foreground\"\n        >\n          {field.description}\n        </p>\n      ) : null}\n      {content}\n      {showError && error ? (\n        <FieldError id={ids.errorId} message={error} />\n      ) : null}\n    </div>\n  );\n}\n\nexport const FieldRow = FieldRowImpl;\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-row.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-select.tsx",
      "content": "\"use client\";\n\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport type { FieldRendererProps } from \"../types\";\nimport { formatFieldValue } from \"../lib/format-value\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldSelect({\n  value,\n  onChange,\n  field,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  required,\n}: BuiltInFieldProps) {\n  if (mode === \"read\") {\n    return (\n      <span className=\"block truncate text-sm\">\n        {formatFieldValue(field, value)}\n      </span>\n    );\n  }\n  const stringValue =\n    value === undefined || value === null ? \"\" : String(value);\n  return (\n    <Select\n      value={stringValue}\n      disabled={disabled}\n      onValueChange={(next) => onChange(next)}\n    >\n      <SelectTrigger\n        id={fieldId}\n        aria-required={required || undefined}\n        aria-invalid={error ? true : undefined}\n        aria-describedby={error ? errorId : undefined}\n        className=\"w-full\"\n      >\n        <SelectValue placeholder={field.placeholder ?? \"Select...\"} />\n      </SelectTrigger>\n      <SelectContent>\n        {(field.options ?? []).map((opt) => (\n          <SelectItem key={opt.value} value={opt.value}>\n            {opt.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-select.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-string.tsx",
      "content": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport type { FieldRendererProps } from \"../types\";\nimport { formatFieldValue } from \"../lib/format-value\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldString({\n  value,\n  onChange,\n  field,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  onBlur,\n  required,\n}: BuiltInFieldProps) {\n  if (mode === \"read\") {\n    return (\n      <span className=\"block truncate text-sm\">\n        {formatFieldValue(field, value)}\n      </span>\n    );\n  }\n  const stringValue = typeof value === \"string\" ? value : value == null ? \"\" : String(value);\n  return (\n    <Input\n      id={fieldId}\n      type=\"text\"\n      value={stringValue}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      onChange={(e) => onChange(e.target.value)}\n      onBlur={onBlur}\n      aria-required={required || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-describedby={error ? errorId : undefined}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-string.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/field-textarea.tsx",
      "content": "\"use client\";\n\nimport { Textarea } from \"@/components/ui/textarea\";\nimport type { FieldRendererProps } from \"../types\";\n\ninterface BuiltInFieldProps extends FieldRendererProps {\n  onBlur: () => void;\n  required: boolean;\n}\n\nexport function FieldTextarea({\n  value,\n  onChange,\n  field,\n  mode,\n  disabled,\n  error,\n  fieldId,\n  errorId,\n  onBlur,\n  required,\n}: BuiltInFieldProps) {\n  const stringValue =\n    value === undefined || value === null\n      ? \"\"\n      : typeof value === \"string\"\n        ? value\n        : String(value);\n  if (mode === \"read\") {\n    if (stringValue.length === 0) {\n      return <span className=\"block text-sm text-muted-foreground\">—</span>;\n    }\n    return (\n      <p className=\"block max-h-48 overflow-y-auto text-sm whitespace-pre-wrap\">\n        {stringValue}\n      </p>\n    );\n  }\n  return (\n    <Textarea\n      id={fieldId}\n      value={stringValue}\n      placeholder={field.placeholder}\n      disabled={disabled}\n      onChange={(e) => onChange(e.target.value)}\n      onBlur={onBlur}\n      aria-required={required || undefined}\n      aria-invalid={error ? true : undefined}\n      aria-describedby={error ? errorId : undefined}\n      rows={4}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/field-textarea.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/permission-tooltip.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Lock } from \"lucide-react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PermissionTooltipProps {\n  reason: string;\n  children: ReactNode;\n  tooltipId?: string;\n  className?: string;\n}\n\nexport function PermissionTooltip({\n  reason,\n  children,\n  tooltipId,\n  className,\n}: PermissionTooltipProps) {\n  // No delayDuration — Radix-only prop; Base UI's TooltipProvider rejects it (F-cross-13).\n  // No `asChild` either — the trigger IS the wrapper (a natively-focusable\n  // <button> in both backends; children must stay non-interactive display content).\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          aria-describedby={tooltipId}\n          className={cn(\n            \"inline-flex items-center gap-1.5 rounded-sm text-start outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            className,\n          )}\n        >\n          {children}\n          <Lock\n            aria-hidden=\"true\"\n            className=\"size-3 text-muted-foreground/70\"\n          />\n        </TooltipTrigger>\n        <TooltipContent id={tooltipId} side=\"top\" align=\"start\">\n          {reason}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/permission-tooltip.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/parts/submit-actions.tsx",
      "content": "\"use client\";\n\nimport { Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface SubmitActionsProps {\n  submitLabel: string;\n  cancelLabel: string;\n  pending: boolean;\n  showSpinner: boolean;\n  onCancel: () => void;\n  canCancel: boolean;\n  formId?: string;\n}\n\nexport function SubmitActions({\n  submitLabel,\n  cancelLabel,\n  pending,\n  showSpinner,\n  onCancel,\n  canCancel,\n  formId,\n}: SubmitActionsProps) {\n  return (\n    <div className=\"mt-4 flex items-center justify-end gap-2\">\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        onClick={onCancel}\n        disabled={!canCancel}\n      >\n        {cancelLabel}\n      </Button>\n      <Button\n        type=\"submit\"\n        form={formId}\n        aria-disabled={pending || undefined}\n        disabled={pending}\n      >\n        {showSpinner ? (\n          <Loader2\n            aria-hidden=\"true\"\n            className=\"size-4 animate-spin\"\n          />\n        ) : null}\n        {submitLabel}\n      </Button>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/parts/submit-actions.tsx"
    },
    {
      "path": "src/registry/components/forms/properties-form/hooks/use-form-reducer.ts",
      "content": "import { useReducer } from \"react\";\nimport type { FormAction, FormState } from \"../types\";\n\nfunction pruneToKeys<T>(\n  source: Record<string, T>,\n  keys: ReadonlySet<string>,\n): Record<string, T> {\n  let changed = false;\n  const next: Record<string, T> = {};\n  for (const k of Object.keys(source)) {\n    if (keys.has(k)) next[k] = source[k];\n    else changed = true;\n  }\n  return changed ? next : source;\n}\n\nexport function makeInitialState(\n  cleanSnapshot: Record<string, unknown>,\n): FormState {\n  return {\n    errors: {},\n    formError: undefined,\n    pending: false,\n    showSpinner: false,\n    version: 0,\n    cleanVersion: 0,\n    cleanSnapshot,\n    submitAttempted: false,\n    blurredWithError: {},\n  };\n}\n\nfunction reducer(state: FormState, action: FormAction): FormState {\n  switch (action.type) {\n    case \"field-changed\": {\n      const liveKeys = new Set(Object.keys(action.nextValues));\n      const prunedErrors = pruneToKeys(state.errors, liveKeys);\n      const prunedBlurred = pruneToKeys(state.blurredWithError, liveKeys);\n      const nextErrors = { ...prunedErrors };\n      if (action.error) nextErrors[action.key] = action.error;\n      else delete nextErrors[action.key];\n      return {\n        ...state,\n        errors: nextErrors,\n        blurredWithError: prunedBlurred,\n        version: state.version + 1,\n      };\n    }\n    case \"field-blurred\": {\n      if (!action.error) return state;\n      if (state.blurredWithError[action.key]) {\n        if (state.errors[action.key] === action.error) return state;\n        return {\n          ...state,\n          errors: { ...state.errors, [action.key]: action.error },\n        };\n      }\n      return {\n        ...state,\n        errors: { ...state.errors, [action.key]: action.error },\n        blurredWithError: { ...state.blurredWithError, [action.key]: true },\n      };\n    }\n    case \"submit-started\":\n      return {\n        ...state,\n        pending: true,\n        showSpinner: false,\n        formError: undefined,\n        submitAttempted: true,\n      };\n    case \"submit-spinner-show\":\n      if (!state.pending) return state;\n      return { ...state, showSpinner: true };\n    case \"submit-succeeded\":\n      return {\n        ...state,\n        pending: false,\n        showSpinner: false,\n        errors: {},\n        formError: undefined,\n        blurredWithError: {},\n        cleanSnapshot: action.cleanSnapshot,\n        cleanVersion: state.version,\n      };\n    case \"submit-failed\":\n      return {\n        ...state,\n        pending: false,\n        showSpinner: false,\n        errors: action.errors,\n        formError: action.formError,\n      };\n    case \"mark-clean\":\n      return {\n        ...state,\n        cleanSnapshot: action.cleanSnapshot,\n        cleanVersion: state.version,\n      };\n    case \"reset\":\n      return {\n        ...state,\n        pending: false,\n        showSpinner: false,\n        errors: {},\n        formError: undefined,\n        blurredWithError: {},\n        submitAttempted: false,\n        cleanSnapshot: action.cleanSnapshot,\n        cleanVersion: state.version + 1,\n        version: state.version + 1,\n      };\n  }\n}\n\nexport function useFormReducer(initialSnapshot: Record<string, unknown>) {\n  return useReducer(reducer, initialSnapshot, makeInitialState);\n}\n\nexport { reducer as formReducer };\n",
      "type": "registry:component",
      "target": "components/properties-form/hooks/use-form-reducer.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/hooks/use-id-factory.ts",
      "content": "import { useId } from \"react\";\n\nexport interface FieldIds {\n  fieldId: string;\n  errorId: string;\n  descriptionId: string;\n  permissionTooltipId: string;\n}\n\nexport function useIdFactory(): (key: string) => FieldIds {\n  const formId = useId();\n  return (key: string): FieldIds => {\n    const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n    return {\n      fieldId: `${formId}-field-${safeKey}`,\n      errorId: `${formId}-error-${safeKey}`,\n      descriptionId: `${formId}-desc-${safeKey}`,\n      permissionTooltipId: `${formId}-perm-${safeKey}`,\n    };\n  };\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/hooks/use-id-factory.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/hooks/use-submit-spinner.ts",
      "content": "import { useEffect, useRef } from \"react\";\n\nconst SPINNER_DELAY_MS = 200;\n\nexport function useSubmitSpinner(\n  pending: boolean,\n  onShow: () => void,\n): void {\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    if (!pending) {\n      if (timerRef.current !== null) {\n        clearTimeout(timerRef.current);\n        timerRef.current = null;\n      }\n      return;\n    }\n    timerRef.current = setTimeout(() => {\n      timerRef.current = null;\n      onShow();\n    }, SPINNER_DELAY_MS);\n    return () => {\n      if (timerRef.current !== null) {\n        clearTimeout(timerRef.current);\n        timerRef.current = null;\n      }\n    };\n  }, [pending, onShow]);\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/hooks/use-submit-spinner.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/lib/flatten.ts",
      "content": "type Flat = Record<string, unknown>;\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n  if (v === null || typeof v !== \"object\") return false;\n  const proto = Object.getPrototypeOf(v);\n  return proto === null || proto === Object.prototype;\n}\n\nexport function flatten(\n  obj: Record<string, unknown>,\n  prefix = \"\",\n): Flat {\n  const out: Flat = {};\n  for (const [key, value] of Object.entries(obj)) {\n    const next = prefix ? `${prefix}.${key}` : key;\n    if (isPlainObject(value)) {\n      Object.assign(out, flatten(value as Record<string, unknown>, next));\n    } else {\n      out[next] = value;\n    }\n  }\n  return out;\n}\n\nexport function unflatten(flat: Flat): Record<string, unknown> {\n  const out: Record<string, unknown> = {};\n  for (const [path, value] of Object.entries(flat)) {\n    const segments = path.split(\".\");\n    let cursor = out;\n    for (let i = 0; i < segments.length - 1; i++) {\n      const seg = segments[i];\n      const existing = cursor[seg];\n      if (!isPlainObject(existing)) {\n        cursor[seg] = {};\n      }\n      cursor = cursor[seg] as Record<string, unknown>;\n    }\n    cursor[segments[segments.length - 1]] = value;\n  }\n  return out;\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/lib/flatten.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/lib/format-value.ts",
      "content": "import type { PropertiesFormField } from \"../types\";\n\nconst EMPTY_DASH = \"—\";\n\nexport function formatFieldValue(\n  field: PropertiesFormField,\n  value: unknown,\n): string {\n  if (value === undefined || value === null) return EMPTY_DASH;\n  switch (field.type) {\n    case \"string\":\n    case \"textarea\":\n      return typeof value === \"string\" && value.length > 0 ? value : EMPTY_DASH;\n    case \"number\":\n      if (typeof value === \"number\" && Number.isFinite(value)) {\n        return String(value);\n      }\n      if (typeof value === \"string\" && value.length > 0) return value;\n      return EMPTY_DASH;\n    case \"boolean\":\n      return value ? \"Yes\" : \"No\";\n    case \"date\":\n      if (typeof value === \"string\" && value.length > 0) return value;\n      if (value instanceof Date && !Number.isNaN(value.getTime())) {\n        return value.toISOString().slice(0, 10);\n      }\n      return EMPTY_DASH;\n    case \"select\": {\n      const stringValue = typeof value === \"string\" ? value : String(value);\n      const match = field.options?.find((o) => o.value === stringValue);\n      return match?.label ?? stringValue;\n    }\n    default:\n      return String(value);\n  }\n}\n\nexport const EMPTY_DISPLAY = EMPTY_DASH;\n",
      "type": "registry:component",
      "target": "components/properties-form/lib/format-value.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/lib/resolve-permission.ts",
      "content": "import type {\n  FieldPermission,\n  PropertiesFormField,\n} from \"../types\";\n\nexport function resolveFieldPermission(\n  field: PropertiesFormField,\n  values: Record<string, unknown>,\n  hostResolver?: (\n    field: PropertiesFormField,\n    values: Record<string, unknown>,\n  ) => FieldPermission | undefined,\n): FieldPermission {\n  if (hostResolver) {\n    let hostResult: FieldPermission | undefined;\n    try {\n      hostResult = hostResolver(field, values);\n    } catch (err) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.error(\n          `[properties-form] resolvePermission threw for field \"${field.key}\":`,\n          err,\n        );\n      }\n      hostResult = undefined;\n    }\n    if (hostResult !== undefined) return hostResult;\n  }\n  if (field.permission !== undefined) return field.permission;\n  return \"editable\";\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/lib/resolve-permission.ts"
    },
    {
      "path": "src/registry/components/forms/properties-form/lib/validate.ts",
      "content": "import type { PropertiesFormField } from \"../types\";\n\nconst VALIDATOR_THROW_MESSAGE = \"Validation error — see console\";\n\nexport function safeValidateField(\n  field: PropertiesFormField,\n  value: unknown,\n  allValues: Record<string, unknown>,\n): string | undefined {\n  if (!field.validate) return undefined;\n  try {\n    return field.validate(value, allValues);\n  } catch (err) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(\n        `[properties-form] validate threw for field \"${field.key}\":`,\n        err,\n      );\n    }\n    return VALIDATOR_THROW_MESSAGE;\n  }\n}\n\nexport function runRequiredCheck(\n  field: PropertiesFormField,\n  value: unknown,\n): string | undefined {\n  if (!field.required) return undefined;\n  if (value === undefined || value === null) return \"Required\";\n  if (typeof value === \"string\" && value.trim().length === 0) return \"Required\";\n  return undefined;\n}\n\nexport function validateAllFields(\n  schema: ReadonlyArray<PropertiesFormField>,\n  values: Record<string, unknown>,\n): Record<string, string> {\n  const errors: Record<string, string> = {};\n  for (const field of schema) {\n    if (field.permission === \"hidden\") continue;\n    const value = values[field.key];\n    const requiredError = runRequiredCheck(field, value);\n    if (requiredError) {\n      errors[field.key] = requiredError;\n      continue;\n    }\n    const userError = safeValidateField(field, value, values);\n    if (userError) errors[field.key] = userError;\n  }\n  return errors;\n}\n\nexport function safeRunFormValidate<T extends Record<string, unknown>>(\n  hostValidate: ((values: T) => Record<string, string> | undefined) | undefined,\n  values: T,\n): { errors: Record<string, string>; formError: string | undefined } {\n  if (!hostValidate) return { errors: {}, formError: undefined };\n  try {\n    const result = hostValidate(values);\n    return { errors: result ?? {}, formError: undefined };\n  } catch (err) {\n    if (process.env.NODE_ENV !== \"production\") {\n      console.error(\"[properties-form] form-level validate threw:\", err);\n    }\n    return { errors: {}, formError: VALIDATOR_THROW_MESSAGE };\n  }\n}\n",
      "type": "registry:component",
      "target": "components/properties-form/lib/validate.ts"
    }
  ],
  "categories": [
    "forms",
    "graph-system"
  ],
  "type": "registry:block"
}