'use client';
import * as React from 'react';
import { cn } from '@/registry/default/lib/utils';
import { Label } from '@/registry/default/ui/label';
import { Slot } from '@radix-ui/react-slot';
import { Label as LabelPrimitive } from 'radix-ui';
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
function FormItem({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
const id = React.useId();
const { error } = useFormField();
return (
<FormItemContext.Provider value={{ id }}>
<div data-slot="form-item" className={cn('flex flex-col gap-2.5', className)} data-invalid={!!error} {...props} />
</FormItemContext.Provider>
);
}
function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { formItemId } = useFormField();
return (
<Label
data-slot="form-label"
className={cn('font-medium text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-50', className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
const { formDescriptionId, error } = useFormField();
if (error) {
return null; // Hide the description when there's an error
}
return (
<div
data-slot="form-description"
id={formDescriptionId}
className={cn('text-xs text-muted-foreground -mt-0.5', className)}
{...props}
/>
);
}
function FormMessage({ className, children, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<div
data-slot="form-message"
id={formMessageId}
className={cn('-mt-0.5 text-xs font-normal text-destructive', className)}
{...props}
>
{body}
</div>
);
}
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };