{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form",
  "type": "registry:ui",
  "title": "Form",
  "description": "react-hook-form integration layer — field context, labels, descriptions, and validation messages.",
  "dependencies": [
    "@radix-ui/react-label",
    "@radix-ui/react-slot",
    "@saasflare/ui",
    "react-hook-form"
  ],
  "files": [
    {
      "path": "components/ui/form.tsx",
      "type": "registry:ui",
      "content": "// @toreview\n\"use client\"\n\n/**\n * @fileoverview Form primitive — form field wrappers integrating React Hook Form with accessible labels,\n * descriptions, and error messages. Uses Radix UI Slot for composable form controls.\n * Part of the Saasflare base component layer.\n * @module packages/ui/components/ui/form\n * @layer core\n *\n * @requires react-hook-form — peer dependency.\n * @requires @hookform/resolvers — peer dependency (for zod/yup/etc resolver glue).\n * @requires zod — peer dependency (or substitute schema lib via @hookform/resolvers).\n *\n * @component\n * @example\n * import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@saasflare/ui';\n * <Form {...form}>\n *   <FormField control={form.control} name=\"email\" render={({ field }) => (\n *     <FormItem>\n *       <FormLabel>Email</FormLabel>\n *       <FormControl><input {...field} /></FormControl>\n *       <FormMessage />\n *     </FormItem>\n *   )} />\n * </Form>\n */\n\nimport * as React from \"react\"\nimport type * as LabelPrimitive from \"@radix-ui/react-label\"\nimport * as Slot from \"@radix-ui/react-slot\"\nimport {\n  Controller,\n  FormProvider,\n  useFormContext,\n  useFormState,\n  type ControllerProps,\n  type FieldPath,\n  type FieldValues,\n} from \"react-hook-form\"\n\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { Label } from \"@saasflare/ui\"\n\n/**\n * Form root — re-export of react-hook-form's `FormProvider`. Spread the object\n * returned by `useForm()` into it to make form state available to nested\n * {@link FormField} compositions.\n *\n * @component\n * @layer core\n */\nconst Form = FormProvider\n\ninterface FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> {\n  name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue | null>(null)\n\n/**\n * Controlled field binding — wraps react-hook-form's `Controller` and exposes\n * the field name via context so {@link useFormField} can resolve ids and\n * validation state for the label, control, description, and message.\n *\n * @component\n * @layer core\n */\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  )\n}\n\n/**\n * Resolves the current field's accessibility wiring and validation state.\n * Returns the field `id`/`name`, the derived `formItemId`, `formDescriptionId`,\n * and `formMessageId`, plus the react-hook-form field state (`error`,\n * `invalid`, `isDirty`, `isTouched`). Must be called inside both\n * `<FormField>` and `<FormItem>`, under a `<Form>` provider — throws otherwise.\n */\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext)\n  const itemContext = React.useContext(FormItemContext)\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\")\n  }\n  if (!itemContext) {\n    throw new Error(\"useFormField should be used within <FormItem>\")\n  }\n\n  const formContext = useFormContext()\n  if (!formContext) {\n    throw new Error(\"useFormField should be used within <Form>\")\n  }\n\n  const { getFieldState } = formContext\n  const formState = useFormState({ name: fieldContext.name })\n  const fieldState = getFieldState(fieldContext.name, formState)\n\n  const { id } = itemContext\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  }\n}\n\ninterface FormItemContextValue {\n  id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue | null>(null)\n\n/**\n * Field wrapper — generates the unique id shared by label, control,\n * description, and message, and stacks them in a vertical grid.\n *\n * @component\n * @layer core\n */\nfunction FormItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const id = React.useId()\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div\n        data-slot=\"form-item\"\n        className={cn(\"grid gap-2\", className)}\n        {...props}\n      />\n    </FormItemContext.Provider>\n  )\n}\n\n/**\n * Props for {@link FormLabel}. Extends {@link SaasflareComponentProps} so\n * `surface`, `radius`, `animated`, and `iconWeight` can override the\n * <SaasflareProvider> context per instance.\n */\ninterface FormLabelProps\n  extends Omit<\n      React.ComponentProps<typeof LabelPrimitive.Root>,\n      keyof SaasflareComponentProps\n    >,\n    SaasflareComponentProps {}\n\n/**\n * Field label — wired to the control via `htmlFor` and tinted destructive when\n * the field has a validation error.\n *\n * @component\n * @layer core\n */\nfunction FormLabel({\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: FormLabelProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const { error, formItemId } = useFormField()\n\n  return (\n    <Label\n      data-slot=\"form-label\"\n      data-error={!!error}\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\"data-[error=true]:text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  )\n}\n\n/**\n * Slot that wires the wrapped input to its label, description, and message via\n * `id`, `aria-describedby`, and `aria-invalid`. Place the actual form control\n * as its single child.\n *\n * @component\n * @layer core\n */\nfunction FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n  return (\n    <Slot.Root\n      data-slot=\"form-control\"\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  )\n}\n\n/**\n * Props for {@link FormDescription}. Extends {@link SaasflareComponentProps}\n * for per-instance `surface` / `radius` / `animated` / `iconWeight` overrides.\n */\ninterface FormDescriptionProps\n  extends Omit<React.ComponentProps<\"p\">, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {}\n\n/**\n * Muted helper text below the control, referenced by the control's\n * `aria-describedby`.\n *\n * @component\n * @layer core\n */\nfunction FormDescription({\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: FormDescriptionProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const { formDescriptionId } = useFormField()\n\n  return (\n    <p\n      data-slot=\"form-description\"\n      id={formDescriptionId}\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\n/**\n * Props for {@link FormMessage}. Extends {@link SaasflareComponentProps}\n * for per-instance `surface` / `radius` / `animated` / `iconWeight` overrides.\n */\ninterface FormMessageProps\n  extends Omit<React.ComponentProps<\"p\">, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {}\n\n/**\n * Validation message — shows the field's error message when present, otherwise\n * its children; renders nothing when both are empty.\n *\n * @component\n * @layer core\n */\nfunction FormMessage({\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: FormMessageProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const { error, formMessageId } = useFormField()\n  const body = error ? String(error?.message ?? \"\") : props.children\n\n  if (!body) {\n    return null\n  }\n\n  return (\n    <p\n      data-slot=\"form-message\"\n      id={formMessageId}\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      className={cn(\"text-sm text-destructive\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  )\n}\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n  type FormLabelProps,\n  type FormDescriptionProps,\n  type FormMessageProps,\n}\n",
      "target": "components/ui/form.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/form.json"
  }
}
