{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-form",
  "type": "registry:component",
  "title": "Form",
  "description": "Schema validation and a submit lifecycle around fields",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "@tanstack/form-core@1.33.5",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/form.tsx",
      "target": "@ui/gpuixui/components/form.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { cn, FORM_ERROR, FORM_ERRORS, FORM_ROOT } from \"../tw/index.js\";\nimport { FormApi } from \"@tanstack/form-core\";\nimport { Effect } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  useCallback,\n  useState,\n  useSyncExternalStore,\n  type ReactNode,\n} from \"react\";\nimport {\n  decode,\n  schemaValidator,\n  type SchemaValidator,\n} from \"./form/validator.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { statusTone, useTheme } from \"./internal/theme.js\";\nimport { validateAll } from \"./validation/logic.js\";\n\nexport type SchemaFormApi = FormApi<\n  unknown,\n  undefined,\n  undefined,\n  undefined,\n  undefined,\n  undefined,\n  SchemaValidator,\n  undefined,\n  undefined,\n  undefined,\n  undefined\n>;\n\nexport interface FormState {\n  readonly errors: ReadonlyArray<string>;\n\n  readonly pending: boolean;\n\n  readonly submit: () => void;\n\n  readonly form: SchemaFormApi;\n}\n\nexport interface FormProps<T> extends Omit<Props, \"children\" | \"onSubmit\"> {\n  readonly schema: Schema.ConstraintDecoder<T>;\n\n  readonly value: unknown;\n\n  readonly onSubmit: (value: T) => void;\n\n  readonly pending?: boolean;\n\n  readonly label?: string;\n\n  readonly className?: string;\n  readonly children: (state: FormState) => ReactNode;\n}\n\nconst syncValues = (form: SchemaFormApi, value: unknown): void => {\n  if (!P.isObject(value) || Array.isArray(value)) return;\n  Arr.forEach(Object.entries(value), ([key, next]) =>\n    form.setFieldValue(key, () => next, {\n      dontUpdateMeta: true,\n      dontValidate: true,\n    }),\n  );\n};\n\nconst clearSubmitErrors = (form: SchemaFormApi): void => {\n  Arr.forEach(R.keys(form.state.fieldMeta), (name) => {\n    if (form.state.fieldMeta[name]?.errorSourceMap?.onSubmit !== \"form\") return;\n    form.setFieldMeta(name, (meta) => ({\n      ...meta,\n      errorMap: { ...meta.errorMap, onSubmit: undefined },\n      errorSourceMap: { ...meta.errorSourceMap, onSubmit: undefined },\n    }));\n  });\n};\n\nexport const Form = <T,>({\n  schema,\n  value,\n  onSubmit,\n  pending = false,\n  label,\n  className,\n  children,\n  style,\n  ...rest\n}: FormProps<T>) => {\n  const theme = useTheme();\n\n  const [form] = useState<SchemaFormApi>(\n    () =>\n      new FormApi({\n        defaultValues: value,\n        validators: { onSubmit: schemaValidator(schema) },\n        onSubmit: ({ value: rawValue }) =>\n          Option.match(decode(schema)(rawValue), {\n            onNone: () => undefined,\n            onSome: onSubmit,\n          }),\n      }),\n  );\n\n  const state = useSyncExternalStore(\n    useCallback(\n      (onChange: () => void) => form.store.subscribe(onChange).unsubscribe,\n      [form],\n    ),\n    () => form.state,\n  );\n\n  const submit = useCallback((): void => {\n    if (pending) return;\n    syncValues(form, value);\n    clearSubmitErrors(form);\n    void form.handleSubmit();\n  }, [form, pending, value]);\n\n  const errors =\n    state.submissionAttempts > 0\n      ? Effect.runSync(validateAll(schema)(value))\n      : [];\n\n  return (\n    <div\n      {...(label === undefined ? {} : { role: \"form\", \"aria-label\": label })}\n      {...rest}\n      style={mergeStyles(recipeStyle(cn(FORM_ROOT, className), theme), style)}\n    >\n      {Arr.match(errors, {\n        onEmpty: () => null,\n        onNonEmpty: (messages) => (\n          <div role=\"alert\" style={recipeStyle(FORM_ERRORS, theme)}>\n            {Arr.map(messages, (message) => (\n              <text\n                key={message}\n                style={{\n                  ...recipeStyle(FORM_ERROR, theme),\n                  color: statusTone(theme, \"destructive\").foreground,\n                }}\n              >\n                {message}\n              </text>\n            ))}\n          </div>\n        ),\n      })}\n      {children({ errors, pending, submit, form })}\n    </div>\n  );\n};\n\nexport const formMeta = {\n  name: \"Form\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"owned-value: the caller passes the value and receives the decoded one; there is no runtime form element\",\n    \"a render-prop child is the only way to reach submit, because the contract fixes Form as one part\",\n    \"the schema and the submit handler are read when the form mounts, matching the core's defaultValues lifetime\",\n  ],\n  docs: \"docs/form.mdx\",\n  example: \"examples/form.example.tsx\",\n  testFile: \"src/behaviour/form.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/form/validator.ts",
      "target": "@ui/gpuixui/components/form/validator.ts",
      "type": "registry:lib",
      "content": "import { Effect, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as R from \"effect/Record\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport { locatedMessages, type LocatedMessage } from \"../validation/logic.js\";\n\nexport interface SchemaFormError {\n  readonly form: string | undefined;\n  readonly fields: Readonly<Record<string, ReadonlyArray<string>>>;\n}\n\nexport type SchemaValidator = (props: { readonly value: unknown }) => unknown;\n\nconst fieldName = (path: ReadonlyArray<PropertyKey>): string =>\n  pipe(\n    Arr.map(path, (segment, index) =>\n      P.isNumber(segment)\n        ? `[${String(segment)}]`\n        : index === 0\n          ? String(segment)\n          : `.${String(segment)}`,\n    ),\n    Arr.join(\"\"),\n  );\n\nconst appendFieldError = (\n  fields: Readonly<Record<string, ReadonlyArray<string>>>,\n  name: string,\n  message: string,\n): Readonly<Record<string, ReadonlyArray<string>>> =>\n  R.set(\n    fields,\n    name,\n    Arr.append(\n      Option.getOrElse(R.get(fields, name), () => []),\n      message,\n    ),\n  );\n\nconst toFormError = (\n  messages: ReadonlyArray<LocatedMessage>,\n): SchemaFormError => {\n  const empty: {\n    readonly form: Option.Option<string>;\n    readonly fields: Readonly<Record<string, ReadonlyArray<string>>>;\n  } = { form: Option.none(), fields: {} };\n  const grouped = Arr.reduce(messages, empty, (acc, { path, message }) =>\n    Arr.match(path, {\n      onEmpty: () => ({\n        form: Option.orElse(acc.form, () => Option.some(message)),\n        fields: acc.fields,\n      }),\n      onNonEmpty: () => ({\n        form: acc.form,\n        fields: appendFieldError(acc.fields, fieldName(path), message),\n      }),\n    }),\n  );\n  return { form: Option.getOrUndefined(grouped.form), fields: grouped.fields };\n};\n\nexport const decode =\n  <T>(schema: Schema.ConstraintDecoder<T>) =>\n  (value: unknown): Option.Option<T> => {\n    const result = Effect.runSync(\n      Schema.decodeUnknownEffect(schema)(value).pipe(Effect.result),\n    );\n    return Result.isFailure(result)\n      ? Option.none()\n      : Option.some(result.success);\n  };\n\nconst firstMessage =\n  <T>(schema: Schema.ConstraintDecoder<T>) =>\n  (value: unknown): Option.Option<string> => {\n    const result = Effect.runSync(\n      Schema.decodeUnknownEffect(schema)(value).pipe(Effect.result),\n    );\n    return Result.isFailure(result)\n      ? Arr.head(\n          Arr.map(\n            locatedMessages(result.failure.issue),\n            ({ message }) => message,\n          ),\n        )\n      : Option.none<string>();\n  };\n\nexport const schemaValidator =\n  <T>(schema: Schema.ConstraintDecoder<T>): SchemaValidator =>\n  ({ value }) => {\n    const result = Effect.runSync(\n      Schema.decodeUnknownEffect(schema, { errors: \"all\" })(value).pipe(\n        Effect.result,\n      ),\n    );\n    return Result.isFailure(result)\n      ? toFormError(locatedMessages(result.failure.issue))\n      : undefined;\n  };\n\nexport const fieldSchemaValidator =\n  <T>(schema: Schema.ConstraintDecoder<T>): SchemaValidator =>\n  ({ value }) =>\n    Option.getOrUndefined(firstMessage(schema)(value));\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/validation/logic.ts",
      "target": "@ui/gpuixui/components/validation/logic.ts",
      "type": "registry:lib",
      "content": "import { Effect, Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport * as SchemaIssue from \"effect/SchemaIssue\";\n\nconst format = SchemaIssue.makeFormatterDefault();\n\nconst isTag =\n  <T extends SchemaIssue.Issue[\"_tag\"]>(tag: T) =>\n  (\n    issue: SchemaIssue.Issue,\n  ): issue is Extract<SchemaIssue.Issue, { _tag: T }> =>\n    issue._tag === tag;\n\nexport interface LocatedMessage {\n  readonly path: ReadonlyArray<PropertyKey>;\n\n  readonly message: string;\n}\n\nexport const locatedMessages = (\n  issue: SchemaIssue.Issue,\n  prefix: ReadonlyArray<PropertyKey> = [],\n): ReadonlyArray<LocatedMessage> =>\n  Match.value(issue).pipe(\n    Match.when(isTag(\"Composite\"), (composite) =>\n      Arr.flatMap(composite.issues, (child) => locatedMessages(child, prefix)),\n    ),\n    Match.when(isTag(\"Pointer\"), (pointer) =>\n      locatedMessages(pointer.issue, Arr.appendAll(prefix, pointer.path)),\n    ),\n    Match.when(isTag(\"Encoding\"), (encoding) =>\n      locatedMessages(encoding.issue, prefix),\n    ),\n    Match.orElse((leaf) => [{ path: prefix, message: format(leaf) }]),\n  );\n\nconst messagesOf = (issue: SchemaIssue.Issue): ReadonlyArray<string> =>\n  Arr.map(locatedMessages(issue), ({ message }) => message);\n\nexport const validate =\n  <T>(schema: Schema.ConstraintDecoder<T>) =>\n  (value: unknown): Effect.Effect<Option.Option<string>> => {\n    const result = Schema.decodeUnknownResult(schema)(value);\n    return Effect.succeed(\n      Result.isFailure(result)\n        ? pipe(messagesOf(result.failure.issue), Arr.head)\n        : Option.none<string>(),\n    );\n  };\n\nexport const validateAll =\n  <T>(schema: Schema.ConstraintDecoder<T>) =>\n  (value: unknown): Effect.Effect<ReadonlyArray<string>> => {\n    const result = Schema.decodeUnknownResult(schema, { errors: \"all\" })(value);\n    return Effect.succeed(\n      Result.isFailure(result)\n        ? pipe(messagesOf(result.failure.issue), Arr.dedupe)\n        : [],\n    );\n  };\n"
    }
  ],
  "docs": "[Form](https://gpuixui.sudokw.com/components/form) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/form",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}