{
  "name": "shuip-forms",
  "type": "registry:item",
  "files": [
    {
      "path": "./skills/.generated/shuip-forms/SKILL.md",
      "type": "registry:file",
      "target": ".claude/skills/shuip-forms/SKILL.md",
      "content": "---\nname: shuip-forms\ndescription: Use when building or editing a form in a project that uses shuip — covers shuip's react-hook-form (rhf-) and tanstack-form (tsf-) field components, their install commands, validation, submission, and accessible error/loading state. Use before hand-writing shadcn FormField boilerplate.\n---\n\n# Building forms with shuip\n\n## Overview\n\nshuip ships pre-wired form **field components** for two libraries: **react-hook-form** (registry prefix `rhf-`) and **tanstack-form** (prefix `tsf-`). Each field collapses the label / control / error-message / accessibility wiring into one component.\n\n**Core principle:** compose shuip field components — do NOT hand-write shadcn's `<FormField render={...}>` boilerplate. If you're reaching for `FormField`/`FormItem`/`FormControl`/`FormMessage`, you're rebuilding what these items already give you.\n\nThere is **no composite \"form\" item** in the registry — you assemble a form from individual field items plus your own schema and submit handler.\n\n## Choose the library first\n\nMatch whatever the project already uses. If it's greenfield:\n\n- **react-hook-form** (`rhf-*`) — mature ecosystem, uncontrolled/minimal re-renders, validates with a zod resolver. Fields bind through a typed **lens** (`@hookform/lenses`). Pick this unless you have a reason not to.\n- **tanstack-form** (`tsf-*`) — end-to-end type-safe field names, validators co-located on the field, good async validation. Natural fit if the project is already on the TanStack stack (Query/Router). Requires the `tsf-form-context` item as its foundation.\n\nDo not mix the two families in one form.\n\n## Install\n\n```bash\n# react-hook-form fields (install the ones you need)\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/rhf-input-field\"\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/rhf-password-field\"\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/submit-button\"\n\n# tanstack-form: install the form context FIRST, then fields\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/tsf-form-context\"\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/tsf-input-field\"\nnpx shadcn@latest add \"https://shuip.plvo.dev/r/tsf-submit-button\"\n```\n\nField naming follows the input type: `input-field`, `password-field`, `number-field`, `select-field`, `checkbox-field`, `radio-field`, `textarea-field`, `date-field`, `date-range-field`, `datetime-field`, `time-field`, `month-field`, `autocomplete-field`, plus `address-field` (rhf only). For the full catalog use the **shuip-components** skill. The shadcn CLI pulls each item's shadcn primitives and npm deps (react-hook-form / zod / @hookform/lenses, or @tanstack/react-form) automatically.\n\n**Exports are unprefixed.** The item `rhf-input-field` exports `InputField`. The `rhf-`/`tsf-` prefix is only the registry name, never the import symbol.\n\n## react-hook-form pattern\n\nCreate one lens per form with `useLens({ control })`, then give each field `lens.focus('fieldName')`. Wrap everything in `<Form {...form}>`.\n\n```tsx\n'use client';\n\nimport { useLens } from '@hookform/lenses';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { useForm } from 'react-hook-form';\nimport { z } from 'zod';\nimport { Form } from '@/components/ui/form';\nimport { InputField } from '@/components/ui/shuip/react-hook-form/input-field';\nimport { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field';\nimport { SubmitButton } from '@/components/ui/shuip/submit-button';\n\nconst schema = z\n  .object({\n    email: z.string().email('Enter a valid email'),\n    password: z.string().min(8, 'At least 8 characters'),\n    confirmPassword: z.string(),\n  })\n  .refine((v) => v.password === v.confirmPassword, {\n    message: 'Passwords do not match',\n    path: ['confirmPassword'],\n  });\n\ntype Values = z.infer<typeof schema>;\n\nexport function SignupForm() {\n  const form = useForm<Values>({\n    resolver: zodResolver(schema),\n    defaultValues: { email: '', password: '', confirmPassword: '' },\n  });\n  const lens = useLens({ control: form.control });\n\n  async function onSubmit(values: Values) {\n    const result = await signup(values); // a server action\n    if (result?.fieldErrors) {\n      for (const [name, message] of Object.entries(result.fieldErrors)) {\n        form.setError(name as keyof Values, { message });\n      }\n    }\n  }\n\n  return (\n    <Form {...form}>\n      <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n        <InputField lens={lens.focus('email')} label=\"Email\" type=\"email\" placeholder=\"you@example.com\" />\n        <PasswordField lens={lens.focus('password')} label=\"Password\" />\n        <PasswordField lens={lens.focus('confirmPassword')} label=\"Confirm password\" />\n        <SubmitButton loading={form.formState.isSubmitting}>Create account</SubmitButton>\n      </form>\n    </Form>\n  );\n}\n```\n\n- Field-specific props (`type`, `placeholder`, …) pass straight through to the underlying input.\n- `SubmitButton` does **not** auto-disable — bind `loading={form.formState.isSubmitting}` (it disables and shows a spinner while loading).\n- Server-side validation failures map back onto fields with `form.setError`.\n\n## tanstack-form pattern\n\nBuild the typed form hook once with `createFormHook`, passing the contexts from `tsf-form-context` and your field/form components. Bind fields with `<form.AppField>`; validators live on the field.\n\n```tsx\n'use client';\n\nimport { createFormHook } from '@tanstack/react-form';\nimport { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';\nimport { InputField } from '@/components/ui/shuip/tanstack-form/input-field';\nimport { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field';\nimport { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';\n\nconst { useAppForm } = createFormHook({\n  fieldContext,\n  formContext,\n  fieldComponents: { InputField, PasswordField },\n  formComponents: { SubmitButton },\n});\n\nexport function SignupForm() {\n  const form = useAppForm({\n    defaultValues: { email: '', password: '' },\n    onSubmit: async ({ value }) => {\n      await signup(value); // a server action\n    },\n  });\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        form.handleSubmit();\n      }}\n      className=\"space-y-4\"\n    >\n      <form.AppField\n        name=\"email\"\n        validators={{ onChange: ({ value }) => (!value.includes('@') ? 'Invalid email' : undefined) }}\n        children={(field) => <field.InputField label=\"Email\" props={{ type: 'email' }} />}\n      />\n      <form.AppField\n        name=\"password\"\n        validators={{ onChange: ({ value }) => (value.length < 8 ? 'At least 8 characters' : undefined) }}\n        children={(field) => <field.PasswordField label=\"Password\" />}\n      />\n      <form.AppForm>\n        <form.SubmitButton>Create account</form.SubmitButton>\n      </form.AppForm>\n    </form>\n  );\n}\n```\n\n- tsf fields take split props: `props` for the native input, `fieldProps` for the field wrapper. There is no lens and no `<Form>` wrapper.\n- The tsf `SubmitButton` auto-disables via `form.Subscribe` — render it inside `<form.AppForm>`, no `loading` prop needed.\n\n## Accessibility & states (both libraries)\n\nThe field components already render `aria-invalid`, associate the error message, and show validation errors — you get accessible errors for free. For a form-level server error, render a `role=\"alert\"` region yourself. Loading/disabled on submit is handled by `SubmitButton` as shown above.\n\n## Common mistakes\n\n| Mistake | Reality |\n|---------|---------|\n| `<InputField control={form.control} name=\"email\" />` | Wrong API. rhf fields bind via a lens: `lens={lens.focus('email')}` after `const lens = useLens({ control: form.control })`. |\n| Importing `RhfInputField` / `TsfInputField` | Export is `InputField`. The `rhf-`/`tsf-` prefix is the registry name only. |\n| `@/components/ui/shuip/rhf-input-field` (flat) | Real path is `@/components/ui/shuip/react-hook-form/input-field` (category sub-folder). |\n| Installing a `rhf-form` / `tsf-form` item | No composite form item exists. Assemble from field items. |\n| Hand-writing `<FormField render={...}>` | That's the boilerplate shuip fields replace. Use the field component. |\n| Raw `<Button disabled={isSubmitting}>` | Use `SubmitButton`. rhf: `loading={form.formState.isSubmitting}`; tsf: auto via `form.Subscribe`. |\n| Using tsf fields without `tsf-form-context` | Every tsf field reads `useFieldContext`; install and wire `tsf-form-context` first via `createFormHook`. |\n| Mixing rhf and tsf fields in one form | Pick one family per form. |\n\n## Red flags — STOP\n\n- About to type `control={...}` or `name=\"...\"` on an rhf shuip field → use the lens.\n- About to import a `Rhf*`/`Tsf*`-prefixed symbol → the export is unprefixed.\n- About to write `FormField`/`FormItem`/`FormControl` by hand → a shuip field already does this.\n- Looking for an `rhf-form` install command → it doesn't exist.\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
