{"_id":"@akinurrahman/form","_rev":"2-1e4ffabe9e16a3ec51011ba67394d0d1","name":"@akinurrahman/form","dist-tags":{"latest":"0.0.2"},"versions":{"0.0.1":{"name":"@akinurrahman/form","version":"0.0.1","license":"MIT","_id":"@akinurrahman/form@0.0.1","maintainers":[{"name":"akinurrahman","email":"dev.akinurrahman@gmail.com"}],"dist":{"shasum":"8196c1863c5eca0d5f9ab520aaa2fff101644da2","tarball":"https://registry.npmjs.org/@akinurrahman/form/-/form-0.0.1.tgz","fileCount":92,"integrity":"sha512-SRJsD/AdM7MWW2B+OJ0hkpk+xSKQJIHGAsZGGPMrBMFl2HTRoLZNRQUFwBMfogM2I2EFOHjA77R+eEddSKooZA==","signatures":[{"sig":"MEYCIQC5ap+qMMYTjxOss65WU3Xaixtd0tdHOSfjIqyGE4Kp7AIhALF/6CITBj/fvBIB0tphpRrlvULJiYLGDktHhka2uh8h","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":202381},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4315d319d59ba46120e70fa79876a9843d37c14a","scripts":{"build":"pnpm build:js && pnpm build:types","build:js":"tsup src/index.ts --format esm,cjs","build:types":"tsc"},"_npmUser":{"name":"akinurrahman","email":"dev.akinurrahman@gmail.com"},"_npmVersion":"10.9.4","description":"A modern form toolkit for React.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","dependencies":{"zod":"^4.4.3","clsx":"^2.1.1","cmdk":"^1.1.1","lucide-react":"^1.25.0","tailwind-merge":"^3.6.0","@hookform/resolvers":"^5.4.0","@radix-ui/react-slot":"^1.3.0","@radix-ui/react-label":"^2.1.11","@radix-ui/react-dialog":"^1.1.19","@radix-ui/react-select":"^2.3.3","@radix-ui/react-popover":"^1.1.19","class-variance-authority":"^0.7.1"},"_hasShrinkwrap":false,"devDependencies":{"@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-hook-form":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.0.1_1784486946579_0.3568769776740486","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@akinurrahman/form","version":"0.0.2","type":"module","description":"A modern form toolkit for React — react-hook-form + zod with bundled shadcn-style fields, cascading selects, and server-paginated async selects.","license":"MIT","author":{"name":"Akinur Rahman","email":"dev.akinurrahman@gmail.com"},"homepage":"https://github.com/akinurrahman/akinur-toolkit/tree/main/packages/form#readme","repository":{"type":"git","url":"git+https://github.com/akinurrahman/akinur-toolkit.git","directory":"packages/form"},"bugs":{"url":"https://github.com/akinurrahman/akinur-toolkit/issues"},"keywords":["react","form","react-hook-form","zod","shadcn","tailwind","radix","async-select","multi-select","cascading-select"],"sideEffects":["**/*.css"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./theme.css":"./dist/theme.css"},"publishConfig":{"access":"public"},"scripts":{"build":"pnpm build:js && pnpm build:types","build:js":"tsup","build:types":"tsc","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"@hookform/resolvers":"^5.0.0","react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","react-hook-form":"^7.0.0","zod":"^4.0.0"},"devDependencies":{"@hookform/resolvers":"^5.4.0","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.3.2","@testing-library/user-event":"^14.6.1","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","jsdom":"^25.0.1","react":"^19.0.0","react-dom":"^19.0.0","react-hook-form":"^7.0.0","tsup":"^8.0.0","typescript":"^5.0.0","vitest":"^3.2.7","zod":"^4.4.3"},"dependencies":{"@radix-ui/react-dialog":"^1.1.19","@radix-ui/react-label":"^2.1.11","@radix-ui/react-popover":"^1.1.19","@radix-ui/react-select":"^2.3.3","@radix-ui/react-slot":"^1.3.0","class-variance-authority":"^0.7.1","clsx":"^2.1.1","cmdk":"^1.1.1","lucide-react":"^1.25.0","tailwind-merge":"^3.6.0"},"_id":"@akinurrahman/form@0.0.2","gitHead":"b6479759e464d97634229ae9c6441dc1b786ede4","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-06/RAWtM735aYgHcd6W0MvSLQqsMGBjqJduh0onPnQW6AC2UCErL8PWFMQ3qdKNXFo+WxejZ4e0T6eOFNLioTA==","shasum":"5f549a13da148d995c1b3b81e695ca7ed8540591","tarball":"https://registry.npmjs.org/@akinurrahman/form/-/form-0.0.2.tgz","fileCount":54,"unpackedSize":196630,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDiBRp/Pp5Da0bXvl0gYVpvhRwHOQGFfNQpNdF2nv1QOgIhAMIC+2gPypT/tP7jLuQOBx8pVAyNiHngzSRpNlyIT7Rl"}]},"_npmUser":{"name":"akinurrahman","email":"dev.akinurrahman@gmail.com"},"directories":{},"maintainers":[{"name":"akinurrahman","email":"dev.akinurrahman@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/form_0.0.2_1784575400672_0.3145962849637518"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-19T18:49:06.413Z","modified":"2026-07-20T19:23:20.995Z","0.0.1":"2026-07-19T18:49:06.710Z","0.0.2":"2026-07-20T19:23:20.843Z"},"license":"MIT","description":"A modern form toolkit for React — react-hook-form + zod with bundled shadcn-style fields, cascading selects, and server-paginated async selects.","maintainers":[{"name":"akinurrahman","email":"dev.akinurrahman@gmail.com"}],"readme":"﻿# @akinurrahman/form\n\nPart of the [`akinur-toolkit`](https://github.com/akinurrahman/akinur-toolkit) monorepo.\n\nA thin, opinionated layer over `react-hook-form` + `zod`, with bundled shadcn-style UI primitives, that removes form boilerplate without giving up flexibility. Write one line per field and get labels, validation errors, cascading fields, server-paginated async selects, and multi-select â€” all wired up, no separate component install required.\n\n```tsx\n<InputField name=\"email\" type=\"email\" label=\"Email\" required />\n```\n\ninstead of\n\n```tsx\n<FormField\n  control={form.control}\n  name=\"email\"\n  render={({ field }) => (\n    <FormItem>\n      <FormLabel>Email</FormLabel>\n      <FormControl>\n        <Input {...field} />\n      </FormControl>\n      <FormMessage />\n    </FormItem>\n  )}\n/>\n```\n\n---\n\n## Table of contents\n\n1. [Requirements](#requirements)\n2. [Theming without shadcn](#theming-without-shadcn)\n3. [No Tailwind at all](#no-tailwind-at-all)\n4. [Installation](#installation)\n5. [Quick start](#quick-start)\n6. [Core concepts](#core-concepts)\n7. [Field reference](#field-reference)\n   - [InputField](#inputfield)\n   - [TextareaField](#textareafield)\n   - [SelectField](#selectfield)\n   - [AsyncSelectField](#asyncselectfield)\n8. [Cascading fields (dependsOn)](#cascading-fields-dependson)\n9. [Async fields in depth](#async-fields-in-depth)\n10. [Create vs update forms](#create-vs-update-forms)\n11. [Common patterns](#common-patterns)\n12. [Building custom field types](#building-custom-field-types)\n13. [API reference](#api-reference)\n14. [Troubleshooting](#troubleshooting)\n15. [Cheat sheet](#cheat-sheet)\n\n---\n\n## Requirements\n\nThis package **bundles its own UI primitives** (`Button`, `Input`, `Select`, `Popover`, `Command`, etc. â€” built on Radix, styled with Tailwind) inside `src/components/primitives`. You do **not** need `shadcn/ui` installed, and you do **not** need to run the shadcn CLI â€” the primitives ship as part of this package, styled or unstyled depending on your setup below.\n\nWhat your project needs, always:\n\n- `react` 18+\n- `react-hook-form` 7+\n- `zod` (v4; see [Zod v3 compatibility](#zod-v3-compatibility))\n- `@hookform/resolvers`\n- Tailwind CSS v4 (the primitives are Tailwind utility classes â€” there's no version of this package that works without Tailwind in the build)\n\nBeyond that, what else you need depends on which of these three you're in:\n\n| Your setup                     | What you need to do                                                                                                                                                           |\n| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Already using shadcn/ui**    | Nothing extra. You already have the CSS variables our primitives read. Just do the [Tailwind content-scanning step](#tailwind-content-scanning) so Tailwind sees our classes. |\n| **Using Tailwind, not shadcn** | Same content-scanning step, **plus** import our default theme stylesheet (or define the variables yourself). See [Theming without shadcn](#theming-without-shadcn).           |\n| **Not using Tailwind at all**  | Tailwind has to enter your build somewhere â€” see [No Tailwind at all](#no-tailwind-at-all) for the two real options.                                                          |\n\n### Why theming still works even though primitives are bundled\n\nThe bundled primitives use CSS-variable-based Tailwind classes (`bg-primary`, `text-foreground`, `border-input`, etc.), not hardcoded colors. Those variables resolve from **your** project's theme â€” so a button rendered by `@akinurrahman/form` picks up your brand color automatically, not a fixed one baked into the package. Two apps using this package with different themes get differently-colored components from the same bundled code, correctly. This holds regardless of whether you use shadcn â€” shadcn just happens to be where this variable-naming convention comes from.\n\n### Tailwind content scanning\n\nEvery setup needs this one, regardless of theme approach.\n\n**Tailwind v4** (no `tailwind.config.js`, config lives in CSS):\n\nTailwind v4's automatic content detection does **not** scan `node_modules` by default (it follows `.gitignore`, which excludes `node_modules` in virtually every project). Since this package's compiled primitives live in `node_modules/@akinurrahman/form/dist`, point Tailwind at them explicitly with the `@source` directive in your global CSS file:\n\n```css\n/* app/globals.css (or wherever your Tailwind entry point is) */\n@import \"tailwindcss\";\n@source \"../node_modules/@akinurrahman/form/dist\";\n```\n\n**Tailwind v3** (`tailwind.config.js` with a `content` array):\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  content: [\n    \"./src/**/*.{ts,tsx}\",\n    \"./node_modules/@akinurrahman/form/dist/**/*.{js,ts,jsx,tsx}\", // <- add this line\n  ],\n  // ...\n};\n```\n\nSame underlying reason either way: Tailwind only generates CSS for classes it can see referenced somewhere in the scanned paths. Skip this step and components render structurally but completely unstyled â€” the classes exist in the shipped code, Tailwind just never generates CSS for them.\n\n### Dark mode: bind `dark:` to your class (Tailwind v4)\n\nThe bundled primitives use `dark:` utilities. Tailwind v4 defaults the `dark:`\nvariant to the OS `prefers-color-scheme` media query. If you drive dark mode\nwith a **class** (the shadcn convention â€” a `.dark` on `<html>`), you must tell\nTailwind so, or the primitives' `dark:` styles fire based on the visitor's OS\ntheme instead of your toggle (e.g. selected-item chips look washed out in light\nmode when the OS is dark). Add this once to your global CSS:\n\n```css\n@import \"tailwindcss\";\n@custom-variant dark (&:where(.dark, .dark *));\n```\n\nshadcn projects already include this, so there's nothing to do. Media-query\ndark mode (no `.dark` class) also needs nothing.\n\n---\n\n## Theming without shadcn\n\nIf your project uses Tailwind but not shadcn, our primitives will render **unstyled or broken** without this step â€” not \"differently themed,\" but genuinely broken, because the classes reference CSS variables (`--primary`, `--background`, etc.) that don't exist anywhere in your CSS yet. `bg-primary` resolves to nothing if `--primary` was never defined.\n\nYou have two options, pick based on how much you care about matching your own brand out of the box:\n\n### Option 1 â€” Import the default theme (fastest, zero design work)\n\n```ts\n// once, in your app's entry point (e.g. layout.tsx, main.tsx, _app.tsx)\nimport \"@akinurrahman/form/theme.css\";\n```\n\nThis defines every CSS variable our primitives read, with sensible neutral defaults. Everything renders correctly immediately â€” no shadcn knowledge required, no manual variable definitions.\n\nTo match your own brand color instead of the defaults, override just the tokens you care about **after** the import, in your own global CSS:\n\n```css\n@import \"tailwindcss\";\n@import \"@akinurrahman/form/theme.css\";\n\n:root {\n  --primary: 220 90% 56%; /* your brand color, HSL triplet */\n  --primary-foreground: 0 0% 100%;\n  --radius: 0.5rem; /* corner rounding, applies everywhere */\n}\n```\n\nAnything you don't override keeps the shipped default. This is the same mechanism a shadcn user gets automatically â€” you're just doing it explicitly instead of inheriting it from a CLI-generated file.\n\n### Option 2 â€” Define every token yourself (full control, no defaults)\n\nSkip the `theme.css` import and define the complete variable set your project needs. This is the full list our bundled primitives collectively reference â€” nothing shadcn-specific about the names themselves, they're just CSS custom properties:\n\n```css\n:root {\n  --background: 0 0% 100%;\n  --foreground: 222 47% 11%;\n\n  --card: 0 0% 100%;\n  --card-foreground: 222 47% 11%;\n\n  --popover: 0 0% 100%;\n  --popover-foreground: 222 47% 11%;\n\n  --primary: 222 47% 11%;\n  --primary-foreground: 210 40% 98%;\n\n  --secondary: 210 40% 96%;\n  --secondary-foreground: 222 47% 11%;\n\n  --muted: 210 40% 96%;\n  --muted-foreground: 215 16% 47%;\n\n  --accent: 210 40% 96%;\n  --accent-foreground: 222 47% 11%;\n\n  --destructive: 0 84% 60%;\n  --destructive-foreground: 210 40% 98%;\n\n  --border: 214 32% 91%;\n  --input: 214 32% 91%;\n  --ring: 222 47% 11%;\n\n  --radius: 0.5rem;\n}\n```\n\nEvery value is an HSL triplet (no `hsl()` wrapper, no commas â€” that's how Tailwind's `bg-primary` etc. expect to consume them). Dark mode, if you support it, typically redefines the same set under a `.dark` class or `[data-theme=\"dark\"]` selector with adjusted values.\n\n---\n\n## No Tailwind at all\n\nIf your build has zero Tailwind â€” not just no shadcn, but Tailwind isn't in the pipeline anywhere â€” nothing above helps, because the primitives are Tailwind utility classes. Two real paths:\n\n**Add Tailwind, scoped to this package.** You don't need to convert your whole app to Tailwind â€” just get it into the build so it can compile the classes this package ships, using the `@source` step above. This is usually the lower-effort option even for a non-Tailwind app, since Tailwind coexists fine alongside other styling approaches (CSS modules, styled-components, etc.) â€” it doesn't need to own your whole app's styling to compile one dependency's classes.\n\n**Skip the bundled UI, use only the logic.** `useCascade`, `useAsyncOptions`, `FieldWrapper`'s render-prop pattern, and the `BaseFieldProps` type are all exported independently of the styled primitives. You can build your own field components with your own styling system (CSS modules, vanilla CSS, styled-components, whatever) and still get cascading, async pagination, debouncing, and request cancellation for free. You lose the \"one line per field\" convenience of the built-in `InputField`/`SelectField`/etc. â€” you're writing the visual layer yourself â€” but the hard logic is still reusable. See [Building custom field types](#building-custom-field-types).\n\n---\n\n## Installation\n\n```bash\nnpm install @akinurrahman/form\n# or\npnpm add @akinurrahman/form\n# or\nyarn add @akinurrahman/form\n```\n\nNo CLI step, no component copying. Then:\n\n1. Do the [Tailwind content-scanning](#tailwind-content-scanning) step (always required).\n2. If you're not using shadcn, also do the [Theming without shadcn](#theming-without-shadcn) step.\n3. Import fields and go.\n\n### Developing inside this monorepo\n\nIf you're working on this package from within `akinur-toolkit` itself rather than consuming it from npm, install at the repo root â€” pnpm workspaces link `@akinurrahman/form` into any app that lists it as a dependency, no `npm link` needed:\n\n```bash\npnpm install\n```\n\nChanges to `packages/form/src` are picked up by any local app in the workspace on the next build/dev run. Run this package's own build with:\n\n```bash\npnpm --filter @akinurrahman/form build\n```\n\n---\n\n## Quick start\n\n```tsx\nimport { z } from \"zod\";\nimport {\n  Form,\n  InputField,\n  SelectField,\n  TextareaField,\n  Button,\n} from \"@akinurrahman/form\";\n\nconst schema = z.object({\n  name: z.string().min(2, \"At least 2 characters\"),\n  email: z.string().email(),\n  role: z.enum([\"admin\", \"user\"]),\n  bio: z.string().max(300).optional(),\n});\n\nexport function UserForm() {\n  return (\n    <Form\n      schema={schema}\n      defaultValues={{ role: \"user\" }}\n      onSubmit={(values) => console.log(values)}\n      className=\"space-y-4\"\n    >\n      <InputField name=\"name\" label=\"Name\" required />\n      <InputField name=\"email\" type=\"email\" label=\"Email\" required />\n      <SelectField\n        name=\"role\"\n        label=\"Role\"\n        options={[\n          { value: \"admin\", label: \"Admin\" },\n          { value: \"user\", label: \"User\" },\n        ]}\n        required\n      />\n      <TextareaField name=\"bio\" label=\"Bio\" />\n      <Button type=\"submit\">Save</Button>\n    </Form>\n  );\n}\n```\n\nValidation, error messages, submit handling, and accessibility (label association, `aria-invalid`, error announcements) are all wired up out of the box.\n\n---\n\n## Core concepts\n\n### The `<Form>` wrapper\n\n`<Form>` sets up `useForm` with your Zod schema via `zodResolver`, wraps children in `FormProvider`, and gives you a submit handler that only fires on valid input.\n\n| Prop            | Type                                                           | Default      | Notes                                                                        |\n| --------------- | -------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------- |\n| `schema`        | `z.ZodType<T>`                                                 | required     | Your Zod schema. `T` is inferred from it.                                    |\n| `defaultValues` | `DefaultValues<T>`                                             | â€”            | Merged over schema-derived defaults (see below); your values win.            |\n| `onSubmit`      | `(values: T, form: UseFormReturn<T>) => void \\| Promise<void>` | required     | Fires only when the schema passes.                                           |\n| `mode`          | `\"onSubmit\" \\| \"onBlur\" \\| \"onChange\" \\| \"onTouched\" \\| \"all\"` | `\"onSubmit\"` | react-hook-form validation mode.                                             |\n| `children`      | `ReactNode \\| (form: UseFormReturn<T>) => ReactNode`           | required     | Function form gives access to the form instance (for `watch`, `reset`, etc). |\n| `className`     | `string`                                                       | â€”            | Applied to the `<form>` element.                                             |\n\n**Automatic default values.** `<Form>` reads your schema and seeds each `string`, `enum`, and `array` field with its natural empty (`\"\"` / `[]`) before handing off to `useForm`. This is why you get your custom messages (`z.string().min(2, \"…\")`) instead of Zod's raw `\"expected string, received undefined\"`, and why there are no controlled/uncontrolled React warnings — even when you pass **no** `defaultValues` at all. Number/boolean fields are deliberately left `undefined` so \"required\" validation and numeric coercion still work. Anything you pass in `defaultValues` overrides the derived value, so edit forms and presets are unaffected. The derivation is also exported standalone as `getDefaults(schema)` if you want it outside `<Form>`.\n\n**Render-prop children**, useful when a field needs to react to another field's value in a way that isn't cascading (see [Conditional fields](#conditional-fields)):\n\n```tsx\n<Form schema={schema} defaultValues={defaults} onSubmit={handleSubmit}>\n  {(form) => (\n    <>\n      <SelectField name=\"type\" options={typeOptions} />\n      {form.watch(\"type\") === \"custom\" && (\n        <InputField name=\"customValue\" label=\"Custom value\" />\n      )}\n      <Button type=\"submit\">Save</Button>\n    </>\n  )}\n</Form>\n```\n\n### The FieldWrapper shell\n\nEvery field component is built on `<FieldWrapper>` internally, which owns label, description, and error message rendering. You don't touch this directly unless you're [building a custom field type](#building-custom-field-types) â€” it's exported for exactly that purpose.\n\n### Cascading model\n\nAny field can declare `dependsOn` â€” the name of one or more other fields. When a parent's value changes, the dependent field automatically resets, and is either hidden or disabled until every listed parent has a value. This works on any field type (input, textarea, select), not just selects, and chains naturally: `country â†’ state â†’ district â†’ city` is just four fields each pointing to their immediate parent. See [Cascading fields](#cascading-fields-dependson) for full details.\n\n---\n\n## Field reference\n\n### InputField\n\nHandles `text`, `email`, `url`, `tel`, `password` (with a show/hide eye toggle), and `number` (returns an actual `number`, not a string).\n\n```tsx\n<InputField name=\"email\" type=\"email\" label=\"Email\" required />\n<InputField name=\"password\" type=\"password\" label=\"Password\" />\n<InputField name=\"age\" type=\"number\" label=\"Age\" min={18} max={120} />\n```\n\n| Prop                 | Type                                                            | Default  | Notes                                                                    |\n| -------------------- | --------------------------------------------------------------- | -------- | ------------------------------------------------------------------------ |\n| `name`               | `string`                                                        | required | Must match a key in your schema.                                         |\n| `type`               | `\"text\" \\| \"email\" \\| \"url\" \\| \"tel\" \\| \"password\" \\| \"number\"` | `\"text\"` | Password gets an eye toggle; number returns a numeric value.             |\n| `label`              | `string`                                                        | â€”        |                                                                          |\n| `description`        | `string`                                                        | â€”        | Helper text below the input.                                             |\n| `placeholder`        | `string`                                                        | â€”        | Overridden by a cascading hint when the field is gated.                  |\n| `required`           | `boolean`                                                       | `false`  | Shows a visual asterisk. Actual validation still comes from your schema. |\n| `disabled`           | `boolean`                                                       | `false`  | Combined with the cascading gate if `dependsOn` is set.                  |\n| `min`, `max`, `step` | `number`                                                        | â€”        | Number-type constraints.                                                 |\n| `maxLength`          | `number`                                                        | â€”        |                                                                          |\n| `autoComplete`       | `string`                                                        | â€”        |                                                                          |\n| `dependsOn`          | `string \\| string[]`                                            | â€”        | See [Cascading fields](#cascading-fields-dependson).                     |\n| `alwaysVisible`      | `boolean`                                                       | `false`  | Stay visible (disabled) instead of hidden while gated.                   |\n| `className`          | `string`                                                        | â€”        | Applied to the underlying `<Input>`.                                     |\n\n**Number handling:** clearing the input sends `undefined`, not `\"\"` or `0` â€” so `z.number()` correctly reports \"required\" instead of a type error.\n\n### TextareaField\n\n```tsx\n<TextareaField name=\"bio\" label=\"About you\" rows={6} maxLength={500} />\n```\n\nSame base props as `InputField`, plus `rows` (default `4`) and `maxLength`.\n\n### SelectField\n\nStatic options select. Add `multi` to switch to a multi-select (popover + command palette + badges).\n\n**Single:**\n\n```tsx\n<SelectField\n  name=\"role\"\n  label=\"Role\"\n  options={[\n    { value: \"admin\", label: \"Admin\" },\n    { value: \"user\", label: \"User\" },\n  ]}\n  required\n/>\n```\n\n**Multi:**\n\n```tsx\n<SelectField\n  multi\n  showSelectAll\n  maxCount={3}\n  name=\"permissions\"\n  label=\"Permissions\"\n  options={PERMISSION_OPTIONS}\n  required\n/>\n```\n\n| Prop                                                                                                     | Type                                                | Notes                                                                                   |\n| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |\n| `name`                                                                                                   | `string`                                            | required.                                                                               |\n| `options`                                                                                                | `{ value: string; label: string }[]`                | Static list.                                                                            |\n| `optionsFn`                                                                                              | `(formValues: Record<string, unknown>) => Option[]` | Dynamic list, re-evaluated on any form value change. Overrides `options` when provided. |\n| `multi`                                                                                                  | `boolean`                                           | Switches value type to `string[]`.                                                      |\n| `showSelectAll`                                                                                          | `boolean`                                           | Multi only â€” shows a \"Select all\" row.                                                  |\n| `maxCount`                                                                                               | `number`                                            | Multi only â€” collapse extra badges into \"+N more\".                                      |\n| `label`, `description`, `placeholder`, `required`, `disabled`, `className`, `dependsOn`, `alwaysVisible` |                                                     | Same as `InputField`.                                                                   |\n\n**Dynamic options with `optionsFn`** â€” useful when a select's contents depend on another field's value but you don't want cascading hide/reset behavior:\n\n```tsx\n<SelectField\n  name=\"subCategory\"\n  label=\"Sub-category\"\n  optionsFn={(values) => {\n    if (values.category === \"vehicles\") return VEHICLE_TYPES;\n    if (values.category === \"electronics\") return ELECTRONIC_TYPES;\n    return [];\n  }}\n/>\n```\n\n### AsyncSelectField\n\nServer-paginated select with debounced search. Same `multi` prop as `SelectField`.\n\n```tsx\n<AsyncSelectField\n  name=\"managerId\"\n  label=\"Manager\"\n  required\n  fetchOptions={async ({ search, page, limit, signal }) => {\n    const res = await fetch(\n      `/api/users?q=${search}&page=${page}&limit=${limit}`,\n      { signal },\n    );\n    return res.json(); // { options: [...], hasMore: boolean }\n  }}\n/>\n```\n\n| Prop                                                                                                     | Type                                                  | Notes                                                                                                  |\n| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |\n| `name`                                                                                                   | `string`                                              | required.                                                                                              |\n| `fetchOptions`                                                                                           | `(args: AsyncFetchArgs) => Promise<AsyncFetchResult>` | See below.                                                                                             |\n| `multi`                                                                                                  | `boolean`                                             | Multi-select mode.                                                                                     |\n| `maxCount`                                                                                               | `number`                                              | Multi only.                                                                                            |\n| `pageSize`                                                                                               | `number`                                              | Default `20`.                                                                                          |\n| `debounceMs`                                                                                             | `number`                                              | Default `300`.                                                                                         |\n| `initialSelectedOptions`                                                                                 | `Option[]`                                            | Seeds the label cache so preselected values (edit forms) show a label before the first fetch resolves. |\n| `label`, `description`, `placeholder`, `required`, `disabled`, `className`, `dependsOn`, `alwaysVisible` |                                                       | Same as `InputField`.                                                                                  |\n\n**`AsyncFetchArgs`:**\n\n```ts\n{\n  search: string;              // debounced search input\n  page: number;                // 1-indexed\n  limit: number;                // = pageSize\n  parentValue?: string;        // first dependsOn field's value, for convenience\n  parentValues?: Record<string, string | undefined>;  // all dependsOn values\n  signal?: AbortSignal;         // aborted on stale requests â€” pass to fetch()\n}\n```\n\n**`AsyncFetchResult`:**\n\n```ts\n{\n  options: {\n    value: string;\n    label: string;\n  }\n  [];\n  hasMore: boolean; // controls infinite scroll\n}\n```\n\n---\n\n## Cascading fields (dependsOn)\n\n### Single parent\n\n```tsx\n<SelectField name=\"country\" label=\"Country\" options={countries} required />\n<SelectField\n  name=\"state\"\n  label=\"State\"\n  dependsOn=\"country\"\n  optionsFn={(v) => statesByCountry[v.country as string] ?? []}\n  required\n/>\n```\n\nWhen `country` changes, `state` resets to empty and hides (or disables, see below) until a country is picked.\n\n### Chained (multi-level)\n\nEach field only needs to know its immediate parent. Resets propagate automatically â€” a parent resetting is itself a value change that its own child is watching.\n\n```tsx\n<SelectField name=\"country\" label=\"Country\" options={countries} required />\n<SelectField name=\"state\" label=\"State\" dependsOn=\"country\" optionsFn={statesFn} required />\n<AsyncSelectField name=\"district\" label=\"District\" dependsOn=\"state\" fetchOptions={fetchDistricts} required />\n<AsyncSelectField name=\"city\" label=\"City\" dependsOn=\"district\" fetchOptions={fetchCities} required />\n```\n\nPicking a new country clears state, which clears district, which clears city â€” one user action, full chain resets.\n\n### Multiple parents (AND semantics)\n\nPass an array to `dependsOn`. The field is gated until **every** listed parent has a value; a change to **any** one of them triggers a reset.\n\n```tsx\n<AsyncSelectField\n  name=\"pricingTier\"\n  label=\"Pricing\"\n  dependsOn={[\"region\", \"productType\"]}\n  fetchOptions={async ({ parentValues, search, page, limit, signal }) => {\n    const res = await fetch(\n      `/api/pricing?region=${parentValues!.region}&type=${parentValues!.productType}&q=${search}&page=${page}&limit=${limit}`,\n      { signal },\n    );\n    return res.json();\n  }}\n/>\n```\n\n### Hide vs. disable\n\nBy default, a gated field is hidden entirely. Set `alwaysVisible` to keep it in the layout, shown but disabled with a \"Select X first\" hint placeholder â€” useful when hiding fields would cause your grid layout to jump around.\n\n```tsx\n<AsyncSelectField\n  name=\"district\"\n  label=\"District\"\n  dependsOn=\"state\"\n  alwaysVisible\n  fetchOptions={fetchDistricts}\n/>\n```\n\n### Edit-mode gotcha (read this before shipping an edit form)\n\nThe cascade logic skips its very first run so preselected values survive mount â€” but only if `defaultValues` are already populated when `<Form>` first renders. If you fetch a record after mount and call `form.reset(data)` later, that counts as a \"change,\" not the initial mount, and the whole dependent chain wipes itself back to empty.\n\n**Don't do this:**\n\n```tsx\nfunction EditPage({ id }) {\n  const { data } = useUser(id);\n  return (\n    <Form schema={schema} defaultValues={{}}>\n      {(form) => {\n        useEffect(() => {\n          if (data) form.reset(data); // wipes the cascade chain\n        }, [data]);\n        return null;\n      }}\n    </Form>\n  );\n}\n```\n\n**Do this instead** â€” gate the render on the data being ready, and pass it straight into `defaultValues`:\n\n```tsx\nfunction EditPage({ id }) {\n  const { data, isLoading } = useUser(id);\n  if (isLoading) return <FormSkeleton />;\n  return (\n    <Form schema={schema} defaultValues={data}>\n      {/* ... */}\n    </Form>\n  );\n}\n```\n\n---\n\n## Async fields in depth\n\n### The `fetchOptions` contract\n\nCalled whenever search, page, or a parent value changes. TypeScript can't verify your backend actually returns the shape you promise â€” `res.json()` is `any` â€” so validate or map explicitly at the boundary rather than returning the raw response. Three levels, pick based on how much you trust the endpoint:\n\n**Minimum â€” map inline:**\n\n```tsx\nfetchOptions={async ({ search, page, limit, signal }) => {\n  const res = await fetch(url, { signal });\n  const data: { items: { id: string; name: string }[]; total: number } = await res.json();\n  return {\n    options: data.items.map((u) => ({ value: u.id, label: u.name })),\n    hasMore: page * limit < data.total,\n  };\n}}\n```\n\n**Better â€” extract a typed mapper you can reuse:**\n\n```ts\ntype UserListResponse = {\n  items: { id: string; name: string; email: string }[];\n  total: number;\n};\n\nfunction toUserOptions(items: UserListResponse[\"items\"]) {\n  return items.map((u) => ({ value: u.id, label: `${u.name} (${u.email})` }));\n}\n```\n\n**Strict â€” validate with Zod, recommended for third-party or independently-shipped APIs:**\n\n```ts\nconst userListSchema = z.object({\n  items: z.array(z.object({ id: z.string(), name: z.string() })),\n  total: z.number(),\n});\n\nfetchOptions={async ({ search, page, limit, signal }) => {\n  const res = await fetch(url, { signal });\n  const data = userListSchema.parse(await res.json()); // throws with a clear message on drift\n  return {\n    options: data.items.map((u) => ({ value: u.id, label: u.name })),\n    hasMore: page * limit < data.total,\n  };\n}}\n```\n\n### Edit-mode labels for AsyncSelectField\n\nThe trigger looks up `field.value`'s label from an internal cache that's only populated as fetches resolve. In an edit form, the value is preselected before anything has been fetched, so the trigger would show the raw ID with nothing to display. Pass `initialSelectedOptions` to seed the cache:\n\n```tsx\n<AsyncSelectField\n  name=\"managerId\"\n  label=\"Manager\"\n  fetchOptions={fetchUsers}\n  initialSelectedOptions={\n    user.manager\n      ? [{ value: user.manager.id, label: user.manager.name }]\n      : undefined\n  }\n/>\n```\n\n### Cancelling stale requests\n\nAlways forward `signal` to your fetch call. Without it, a slow earlier request can resolve after a newer one and silently overwrite the options list with stale data.\n\n```tsx\nfetchOptions={async ({ search, signal }) => {\n  const res = await fetch(`/api/search?q=${search}`, { signal }); // <- important\n  return res.json();\n}}\n```\n\n---\n\n## Create vs update forms\n\nThe common pattern is one component handling both modes, switching on whether `initialData` was passed.\n\n```tsx\nfunction userFormSchema(mode: \"create\" | \"update\") {\n  const password =\n    mode === \"create\"\n      ? z.string().min(8, \"At least 8 characters\")\n      : z.string().min(8).optional().or(z.literal(\"\"));\n\n  return z.object({\n    name: z.string().min(2),\n    email: z.string().email(),\n    password,\n    role: z.enum([\"admin\", \"user\"]),\n    bio: z.string().max(300).optional(),\n  });\n}\n\nfunction UserForm({\n  initialData,\n  onSuccess,\n}: {\n  initialData?: User;\n  onSuccess?: (u: User) => void;\n}) {\n  const mode = initialData ? \"update\" : \"create\";\n  const schema = useMemo(() => userFormSchema(mode), [mode]);\n\n  const defaultValues = initialData\n    ? { ...initialData, password: \"\" }\n    : { role: \"user\" as const, bio: \"\" };\n\n  async function handleSubmit(values: z.infer<typeof schema>) {\n    const payload =\n      mode === \"update\" && !values.password\n        ? { ...values, password: undefined }\n        : values;\n\n    const url =\n      mode === \"create\" ? \"/api/users\" : `/api/users/${initialData!.id}`;\n    const method = mode === \"create\" ? \"POST\" : \"PATCH\";\n\n    const res = await fetch(url, {\n      method,\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(payload),\n    });\n    const user = await res.json();\n    onSuccess?.(user);\n  }\n\n  return (\n    <Form\n      schema={schema}\n      defaultValues={defaultValues}\n      onSubmit={handleSubmit}\n      className=\"space-y-4\"\n    >\n      <InputField name=\"name\" label=\"Name\" required />\n      <InputField name=\"email\" type=\"email\" label=\"Email\" required />\n      <InputField\n        name=\"password\"\n        type=\"password\"\n        label={mode === \"create\" ? \"Password\" : \"New password\"}\n        description={\n          mode === \"update\" ? \"Leave blank to keep current\" : undefined\n        }\n        required={mode === \"create\"}\n      />\n      <SelectField name=\"role\" label=\"Role\" options={roleOptions} required />\n      <TextareaField name=\"bio\" label=\"Bio\" />\n      <Button type=\"submit\">\n        {mode === \"create\" ? \"Create user\" : \"Save changes\"}\n      </Button>\n    </Form>\n  );\n}\n```\n\nThe key detail: password is required on create, optional on update, and dropped from the payload entirely if left blank on update so the backend keeps the existing one.\n\n---\n\n## Common patterns\n\n### Field grid layouts\n\nWrap fields in your own layout container â€” the system doesn't impose one:\n\n```tsx\n<div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n  <InputField name=\"firstName\" label=\"First name\" required />\n  <InputField name=\"lastName\" label=\"Last name\" required />\n</div>\n```\n\n### Conditional fields\n\nFor show/hide logic that isn't a parent-child cascade (e.g. showing a field based on a checkbox, not resetting anything), use render-prop children with `form.watch`:\n\n```tsx\n<Form schema={schema} defaultValues={defaults} onSubmit={onSubmit}>\n  {(form) => (\n    <>\n      <SelectField name=\"paymentType\" options={paymentOptions} />\n      {form.watch(\"paymentType\") === \"card\" && (\n        <InputField name=\"cardNumber\" label=\"Card number\" required />\n      )}\n    </>\n  )}\n</Form>\n```\n\nNote: a hidden field's value stays in form state. If your schema requires it, submission will fail on a field the user can't see. Either make it optional in the schema, or clear it explicitly when it hides (`form.setValue(\"cardNumber\", \"\")`).\n\n### Reset after submit\n\n```tsx\nasync function handleSubmit(values, form) {\n  await submitToApi(values);\n  form.reset(); // back to defaultValues\n}\n```\n\n### Async validation (e.g. uniqueness checks)\n\nHandled at the schema level with an async `.refine()`:\n\n```ts\nconst schema = z.object({\n  email: z\n    .string()\n    .email()\n    .refine(\n      async (email) => {\n        const res = await fetch(`/api/check-email?email=${email}`);\n        const { available } = await res.json();\n        return available;\n      },\n      { message: \"Email already in use\" },\n    ),\n});\n```\n\nPair this with `mode=\"onBlur\"` on `<Form>` so the check fires when the user leaves the field, not on every keystroke.\n\n---\n\n## Building custom field types\n\nFive building blocks are exported specifically so you can build field types this package doesn't ship:\n\n| Export                  | Purpose                                                                                                         |\n| ----------------------- | --------------------------------------------------------------------------------------------------------------- |\n| `FieldWrapper`          | Owns the label + description + error message shell.                                                             |\n| `FormControl`           | Bundled primitive â€” wraps your custom input inside `FieldWrapper`'s render prop.                                |\n| `useCascade`            | `dependsOn` reset-on-parent-change logic, with first-mount skip for edit-mode safety.                           |\n| `useAsyncOptions`       | Debounced search, pagination, request cancellation, and valueâ†’label caching.                                    |\n| `BaseFieldProps` (type) | The shared prop shape (`name`, `label`, `dependsOn`, etc.) so your field's props stay consistent with the rest. |\n\nEvery other bundled primitive (`Button`, `Input`, `Popover`, `Command`, `Badge`, etc.) is also exported from the package root â€” you're free to use them directly when building custom fields or your own surrounding UI, rather than pulling in a separate copy of shadcn primitives.\n\n### Example: a color picker\n\n```tsx\nimport { FieldWrapper, FormControl, useCascade } from \"@akinurrahman/form\";\nimport type { BaseFieldProps } from \"@akinurrahman/form\";\n\nexport function ColorPickerField({\n  name,\n  label,\n  description,\n  required,\n  disabled,\n  dependsOn,\n  alwaysVisible,\n}: BaseFieldProps) {\n  const { gated } = useCascade({ name, dependsOn, emptyValue: \"\" });\n\n  return (\n    <FieldWrapper\n      name={name}\n      label={label}\n      description={description}\n      required={required}\n      dependsOn={dependsOn}\n      alwaysVisible={alwaysVisible}\n    >\n      {(field) => (\n        <FormControl>\n          <input\n            type=\"color\"\n            value={field.value ?? \"#000000\"}\n            onChange={(e) => field.onChange(e.target.value)}\n            disabled={disabled || gated}\n            className=\"h-10 w-full rounded-md border\"\n          />\n        </FormControl>\n      )}\n    </FieldWrapper>\n  );\n}\n```\n\nThirty lines, and it inherits label rendering, error display, the required asterisk, and full cascading support.\n\n### Example: a custom async field with different UI\n\nIf you need async pagination but a different visual treatment than the built-in popover (inline chips, a full-page picker, etc.), build it on `useAsyncOptions` directly:\n\n```tsx\nimport { FieldWrapper, useAsyncOptions, useCascade } from \"@akinurrahman/form\";\nimport type { AsyncFetchFn, BaseFieldProps } from \"@akinurrahman/form\";\n\ntype Props = BaseFieldProps & { fetchOptions: AsyncFetchFn };\n\nexport function InlineChipsField({\n  name,\n  dependsOn,\n  fetchOptions,\n  ...base\n}: Props) {\n  const { parentValue, gated } = useCascade({\n    name,\n    dependsOn,\n    emptyValue: [],\n  });\n  const { options, search, setSearch, loading, loadMore, hasMore } =\n    useAsyncOptions({\n      fetchOptions,\n      parentValue,\n      enabled: !gated,\n    });\n\n  // render your own UI using field.value / field.onChange from FieldWrapper's render prop\n}\n```\n\nYou get the debounce, cancellation, and caching logic for free without inheriting the built-in popover markup.\n\n---\n\n## API reference\n\n### Exports\n\n```ts\nimport {\n  // Form fields\n  Form,\n  FieldWrapper,\n  InputField,\n  TextareaField,\n  SelectField,\n  AsyncSelectField,\n\n  // Hooks\n  useCascade,\n  useAsyncOptions,\n\n  // Utilities\n  getDefaults,\n\n  // Bundled UI primitives (shadcn-style, Radix-based â€” no separate install needed)\n  Button,\n  Input,\n  Textarea,\n  Select,\n  Popover,\n  Command,\n  Badge,\n  Dialog,\n  Label,\n  FormControl,\n  // ...and the rest of shadcn's `form` primitive set\n\n  // Types\n  Option,\n  BaseFieldProps,\n  AsyncFetchArgs,\n  AsyncFetchResult,\n  AsyncFetchFn,\n  OptionsFn,\n  SelectFieldProps,\n  AsyncSelectFieldProps,\n} from \"@akinurrahman/form\";\n```\n\nThere's also a CSS-only export, not a JS import â€” used only if you're not on shadcn (see [Theming without shadcn](#theming-without-shadcn)):\n\n```ts\nimport \"@akinurrahman/form/theme.css\";\n```\n\n### `BaseFieldProps`\n\nShared by every field component:\n\n```ts\ntype BaseFieldProps = {\n  name: string;\n  label?: string;\n  description?: string;\n  placeholder?: string;\n  required?: boolean;\n  disabled?: boolean;\n  className?: string;\n  dependsOn?: string | string[];\n  alwaysVisible?: boolean;\n};\n```\n\n### `Option`\n\n```ts\ntype Option = { value: string; label: string };\n```\n\n### `useCascade(options)`\n\n```ts\nfunction useCascade(options: {\n  name: string;\n  dependsOn?: string | string[];\n  emptyValue?: unknown; // value written on reset â€” \"\" for select, [] for multi, undefined for number\n}): {\n  parentValue: string | undefined; // first dependsOn field's value\n  parentValues: Record<string, string | undefined>; // all dependsOn values\n  dependsOnList: string[]; // normalized array form\n  gated: boolean; // true if any listed parent is empty\n};\n```\n\n### `useAsyncOptions(options)`\n\n```ts\nfunction useAsyncOptions(options: {\n  fetchOptions: AsyncFetchFn;\n  parentValue?: string;\n  parentValues?: Record<string, string | undefined>;\n  enabled?: boolean; // gate fetching, e.g. on popover open\n  pageSize?: number; // default 20\n  debounceMs?: number; // default 300\n  initialOptions?: Option[];\n}): {\n  options: Option[];\n  hasMore: boolean;\n  loading: boolean;\n  search: string;\n  setSearch: (value: string) => void;\n  loadMore: () => void;\n  getLabel: (value: string) => string;\n};\n```\n\n---\n\n## Troubleshooting\n\n### Components have zero styling â€” no border, no padding, no rounding at all\n\nThis is the Tailwind content-scanning issue, not a bug in the package. Tailwind only generates CSS for classes it can actually see referenced in a scanned file â€” and by default it never scans `node_modules`. See [Tailwind content scanning](#tailwind-content-scanning) and make sure you've added the `@source` directive (v4) or `content` glob entry (v3) pointing at `node_modules/@akinurrahman/form/dist`. This is the single most common setup issue â€” if a field renders as a raw, completely unstyled `<input>`-looking box, check this first.\n\n### Components have layout/spacing but wrong or missing colors (transparent, black-on-black, invisible focus rings)\n\nDifferent problem from the one above â€” this means Tailwind _is_ generating the CSS, but the CSS variables those classes point to (`--primary`, `--background`, etc.) were never defined anywhere in your project. This happens to non-shadcn users specifically. See [Theming without shadcn](#theming-without-shadcn) â€” either import `@akinurrahman/form/theme.css`, or define the token set yourself.\n\n### `Type '...' does not satisfy the constraint 'FieldValues'` (TypeScript)\n\nThis is a known type mismatch between Zod v4's internal `ZodType` shape and `@hookform/resolvers`' generic overloads. `<Form>` contains a documented `@ts-expect-error` suppression for it internally â€” this doesn't affect runtime behavior. If you're on an older `@hookform/resolvers` version, upgrading may resolve it entirely.\n\n### Zod v3 compatibility\n\nThis package targets Zod v4. If you're on Zod v3, `zodResolver` should work without the v4-specific workaround, but the internal type suppression may then report as an unused directive â€” that's expected and can be safely ignored or removed for your own build.\n\n### Field renders but no validation error shows\n\nCheck that:\n\n1. The field's `name` matches a key in your schema exactly.\n2. If you haven't set `mode` on `<Form>`, errors only appear after a submit attempt — set `mode=\"onBlur\"` or `\"onChange\"` for earlier feedback.\n\n### Cascading field wipes its value on mount (edit forms)\n\nYou're calling `form.reset(data)` in an effect after mount instead of passing `data` directly as `defaultValues`. See [Edit-mode gotcha](#edit-mode-gotcha-read-this-before-shipping-an-edit-form).\n\n### Number field submits `NaN` or an empty string\n\nMake sure you're using `<InputField type=\"number\">`, not a raw `<Input type=\"number\">`. If you built a custom numeric field, replicate the empty-to-`undefined` conversion:\n\n```tsx\nonChange={(e) => {\n  const v = e.target.value;\n  field.onChange(v === \"\" ? undefined : Number(v));\n}}\n```\n\n### `AsyncSelectField` shows a raw ID instead of a label in edit mode\n\nPass `initialSelectedOptions` â€” see [Edit-mode labels](#edit-mode-labels-for-asyncselectfield).\n\n### Stale async requests overwriting fresher results\n\nYou're not forwarding `signal` to your fetch call inside `fetchOptions`. See [Cancelling stale requests](#cancelling-stale-requests).\n\n### Multi-select value type mismatch\n\nIf `multi` is set on the field but the schema key is `z.string()` instead of `z.array(z.string())`, react-hook-form will treat the array as a single value and validation will fail unexpectedly. Match the schema to the field mode.\n\n### `optionsFn` re-runs on every keystroke, everywhere in the form\n\nExpected â€” `optionsFn` subscribes to the entire form state to support arbitrary cross-field logic. If this becomes a real performance concern on a large form, switch to `dependsOn` cascading instead, which only re-evaluates when the specific parent field changes.\n\n---\n\n## Cheat sheet\n\n```tsx\n// Text / email / password / number / tel / url\n<InputField name=\"x\" type=\"email\" label=\"Email\" required />\n\n// Textarea\n<TextareaField name=\"x\" label=\"Bio\" rows={6} maxLength={500} />\n\n// Static select\n<SelectField name=\"x\" label=\"Role\" options={roles} required />\n\n// Static multi-select\n<SelectField multi showSelectAll maxCount={3} name=\"x\" label=\"Tags\" options={tags} />\n\n// Async single select\n<AsyncSelectField name=\"x\" label=\"Manager\" fetchOptions={fetchUsers} />\n\n// Async multi-select\n<AsyncSelectField multi name=\"x\" label=\"Assignees\" fetchOptions={fetchUsers} />\n\n// Cascading â€” hidden until parent is picked\n<SelectField name=\"child\" dependsOn=\"parent\" optionsFn={childrenOf} />\n\n// Cascading â€” visible but disabled until parent is picked\n<SelectField name=\"child\" dependsOn=\"parent\" alwaysVisible optionsFn={childrenOf} />\n\n// Cascading â€” multiple parents (AND semantics)\n<AsyncSelectField name=\"x\" dependsOn={[\"region\", \"type\"]} fetchOptions={fetchPricing} />\n```\n\n---\n\n## Local development (monorepo)\n\nThis package lives at `packages/form` inside `akinur-toolkit`. To work on it locally:\n\n```bash\ngit clone https://github.com/akinurrahman/akinur-toolkit.git\ncd akinur-toolkit\npnpm install\npnpm --filter @akinurrahman/form build   # or dev, depending on your script names\n```\n\nTo use an unpublished local version in another package within the same monorepo, add it as a workspace dependency instead of installing from npm:\n\n```json\n{\n  \"dependencies\": {\n    \"@akinurrahman/form\": \"workspace:*\"\n  }\n}\n```\n\nTo test changes against a project outside the monorepo before publishing, `pnpm link` the package or use `pnpm pack` to generate a local tarball and install that.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","homepage":"https://github.com/akinurrahman/akinur-toolkit/tree/main/packages/form#readme","keywords":["react","form","react-hook-form","zod","shadcn","tailwind","radix","async-select","multi-select","cascading-select"],"repository":{"type":"git","url":"git+https://github.com/akinurrahman/akinur-toolkit.git","directory":"packages/form"},"author":{"name":"Akinur Rahman","email":"dev.akinurrahman@gmail.com"},"bugs":{"url":"https://github.com/akinurrahman/akinur-toolkit/issues"}}