{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/form",
  "title": "Form",
  "description": "Form container with validation, Ctrl+S submit, and dirty tracking",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/form.tsx",
      "content": "import { useFocusManager, Box, Text } from \"ink\";\nimport {\n  useState,\n  useCallback,\n  useMemo,\n  createContext,\n  useContext,\n} from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\n\ninterface FormContextValue {\n  values: Record<string, unknown>;\n  errors: Record<string, string>;\n  isDirty: boolean;\n  setFieldValue: (name: string, value: unknown) => void;\n  setFieldError: (name: string, error: string) => void;\n}\n\nconst FormContext = createContext<FormContextValue>({\n  errors: {\n    /* noop */\n  },\n  isDirty: false,\n  setFieldError: () => {\n    /* noop */\n  },\n  setFieldValue: () => {\n    /* noop */\n  },\n  values: {\n    /* noop */\n  },\n});\n\nexport const useFormContext = () => useContext(FormContext);\n\nexport interface FormField {\n  name: string;\n  validate?: (value: unknown) => string | null;\n}\n\nexport interface FormProps extends InteractionProps {\n  onSubmit?: (values: Record<string, unknown>) => void;\n  initialValues?: Record<string, unknown>;\n  fields?: FormField[];\n  children: ReactNode;\n  onCancel?: () => void;\n  \"aria-label\"?: string;\n}\n\nexport const Form = ({\n  onSubmit,\n  initialValues = {\n    /* noop */\n  },\n  fields = [],\n  children,\n  onCancel,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"Form\",\n}: FormProps) => {\n  const theme = useTheme();\n  const { focus } = useFocusManager();\n  const [values, setValues] = useState<Record<string, unknown>>(initialValues);\n  const [errors, setErrors] = useState<Record<string, string>>({\n    /* noop */\n  });\n  const [isDirty, setIsDirty] = useState(false);\n\n  const setFieldValue = useCallback((name: string, value: unknown) => {\n    setValues((v) => ({ ...v, [name]: value }));\n    setIsDirty(true);\n  }, []);\n\n  const setFieldError = useCallback((name: string, error: string) => {\n    setErrors((e) => ({ ...e, [name]: error }));\n  }, []);\n\n  const validateAndSubmit = useCallback(() => {\n    const newErrors: Record<string, string> = {};\n    for (const field of fields) {\n      const error = field.validate?.(values[field.name]);\n      if (error) {\n        newErrors[field.name] = error;\n      }\n    }\n    setErrors(newErrors);\n    const firstInvalidField = fields.find((field) => newErrors[field.name]);\n    if (firstInvalidField) {\n      focus(firstInvalidField.name);\n      return;\n    }\n    onSubmit?.(values);\n  }, [fields, focus, onSubmit, values]);\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (key.ctrl && input === \"s\") {\n        validateAndSubmit();\n      } else if (key.escape) {\n        onCancel?.();\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const contextValue = useMemo(\n    () => ({ errors, isDirty, setFieldError, setFieldValue, values }),\n    [errors, isDirty, setFieldError, setFieldValue, values]\n  );\n\n  return (\n    <FormContext.Provider value={contextValue}>\n      <Box\n        flexDirection=\"column\"\n        gap={1}\n        aria-role=\"list\"\n        aria-state={{ disabled: disabled || undefined }}\n      >\n        <Text aria-label={`${ariaLabel}.${isFocused ? \" Focused.\" : \"\"}`}>\n          {\"\"}\n        </Text>\n        {Object.keys(errors).length > 0 && (\n          <Box aria-role=\"listitem\">\n            <Text\n              aria-label={`Form errors: ${Object.entries(errors)\n                .map(([name, error]) => `${name}: ${error}`)\n                .join(\". \")}`}\n            >\n              {`Errors: ${Object.keys(errors).length}`}\n            </Text>\n          </Box>\n        )}\n        {children}\n        <Text aria-hidden color={theme.colors.mutedForeground} dimColor>\n          Press Ctrl+S to submit\n        </Text>\n      </Box>\n    </FormContext.Provider>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}