"use client"
import * as React from "react"
import { Field as FieldPrimitive } from "@base-ui/react/field"
import { cn } from "@/registry/lib/utils"
import { Label } from "@/registry/ui/label"
// Base UI shows an error from the control's own validation, but not from the
// `invalid` prop alone, so the prop is passed down to FieldError here.
const FieldInvalidContext = React.createContext<boolean | undefined>(undefined)
// @use-when a labelled form field stacked as one unit: a label over its text
// field, select or slider, with an optional help line and an error line under
// it. It links the label, help and error to the control and marks the control
// invalid, so no page writes an id or aria-describedby. It holds a control
// (Input, Textarea, Select), it is not one. Space one field from
// the next with Stack gap="field". Not for a checkbox or switch with its label
// beside it, which is a Label in a row.
function Field({
className,
invalid,
...props
}: Omit<React.ComponentProps<typeof FieldPrimitive.Root>, "className"> & {
className?: string
}) {
return (
<FieldInvalidContext.Provider value={invalid}>
<FieldPrimitive.Root
data-slot="field"
// The column owns every gap in the field: label to control on the
// `label` rung, control to help or error on the `help` rung. Its parts
// set no margin of their own. `--field-help-extra` is the help rung past
// the label rung the column already puts there: 0 while both ship at
// 8px, and right again the moment a consumer retunes either.
className={cn(
"flex min-w-0 flex-col gap-(--spacing-rhythm-label) [--field-help-extra:calc(var(--spacing-rhythm-help)_-_var(--spacing-rhythm-label))] *:data-[slot=field-description]:mt-(--field-help-extra) *:data-[slot=field-error]:mt-(--field-help-extra)",
className
)}
invalid={invalid}
{...props}
/>
</FieldInvalidContext.Provider>
)
}
// The kit's Label, so a field's label and a bare one are the same text.
function FieldLabel(props: React.ComponentProps<typeof FieldPrimitive.Label>) {
return <FieldPrimitive.Label data-slot="field-label" render={<Label />} {...props} />
}
// Muted: help text explains the control, which is the description job.
function FieldDescription({
className,
...props
}: Omit<React.ComponentProps<typeof FieldPrimitive.Description>, "className"> & {
className?: string
}) {
return (
<FieldPrimitive.Description
data-slot="field-description"
className={cn("text-xs text-muted-foreground", className)}
{...props}
/>
)
}
// Shown only while the field is invalid: pass `invalid` to Field, or let the
// control's own validation (`required`, `type="email"`) decide.
function FieldError({
className,
match,
...props
}: Omit<React.ComponentProps<typeof FieldPrimitive.Error>, "className"> & {
className?: string
}) {
const invalid = React.useContext(FieldInvalidContext)
return (
<FieldPrimitive.Error
data-slot="field-error"
match={invalid || match}
className={cn("text-xs text-destructive", className)}
{...props}
/>
)
}
export { Field, FieldLabel, FieldDescription, FieldError }