{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "field", "title": "Integrated Field", "description": "Flat form-field wrapper for label, control, description, required marker, and validation message patterns.", "dependencies": [ "@radix-ui/react-slot" ], "registryDependencies": [ "field" ], "files": [ { "path": "registry/ui/field.tsx", "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport type { ClassValue } from \"clsx\";\nimport type React from \"react\";\nimport type { ReactNode } from \"react\";\nimport { isValidElement, useId } from \"react\";\nimport {\n Field as BaseField,\n FieldContent as BaseFieldContent,\n FieldDescription as BaseFieldDescription,\n FieldError as BaseFieldError,\n FieldLabel as BaseFieldLabel,\n FieldTitle as BaseFieldTitle,\n} from \"@/components/ui/field\";\nimport { cn } from \"@/lib/utils\";\n\nexport type FieldErrorItem = { message?: string } | undefined;\n\nexport interface FieldProps\n extends Omit, \"className\"> {\n className?: ClassValue;\n contentClassName?: ClassValue;\n description?: ReactNode;\n descriptionClassName?: ClassValue;\n disabled?: boolean;\n error?: ReactNode | FieldErrorItem[];\n errorClassName?: ClassValue;\n htmlFor?: string;\n invalid?: boolean;\n label?: ReactNode;\n labelClassName?: ClassValue;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n required?: boolean;\n}\n\n// RHF/zod issue arrays are plain objects, never renderable nodes — they get\n// routed through the primitive's deduplicating errors list.\nconst toMessageArray = (\n error: FieldProps[\"error\"]\n): FieldErrorItem[] | null => {\n if (!Array.isArray(error)) {\n return null;\n }\n const isPlainItem = (item: unknown) =>\n item === null ||\n item === undefined ||\n (typeof item === \"object\" && !isValidElement(item));\n return error.every(isPlainItem) ? (error as FieldErrorItem[]) : null;\n};\n\nconst RequiredMark = () => (\n <>\n \n *\n \n required\n \n);\n\ntype FlatFieldBodyProps = Pick<\n FieldProps,\n | \"children\"\n | \"contentClassName\"\n | \"description\"\n | \"descriptionClassName\"\n | \"errorClassName\"\n | \"htmlFor\"\n | \"label\"\n | \"labelClassName\"\n | \"orientation\"\n | \"required\"\n> & {\n error: ReactNode;\n hasError: boolean;\n isInvalid: boolean;\n messageArray: FieldErrorItem[] | null;\n};\n\nconst FlatFieldBody = ({\n children,\n contentClassName,\n description,\n descriptionClassName,\n error,\n errorClassName,\n hasError,\n htmlFor,\n isInvalid,\n label,\n labelClassName,\n messageArray,\n orientation,\n required,\n}: FlatFieldBodyProps) => {\n const autoId = useId();\n\n const childElement = isValidElement(children) ? children : null;\n const childId = childElement\n ? (childElement.props as { id?: string }).id\n : undefined;\n const controlId =\n htmlFor ?? childId ?? (childElement ? `${autoId}-control` : undefined);\n const descriptionId = description ? `${autoId}-description` : undefined;\n const errorId = hasError ? `${autoId}-error` : undefined;\n const describedBy =\n [descriptionId, errorId].filter(Boolean).join(\" \") || undefined;\n\n // Wire the control for assistive technologies. Slot lets explicit props on\n // the child win, so user-provided id/aria-* always take precedence.\n const control = childElement ? (\n \n {children}\n \n ) : (\n children\n );\n\n const labelNode = label ? (\n \n {label}\n {required && }\n \n ) : null;\n\n const descriptionNode = description ? (\n \n {description}\n \n ) : null;\n\n const errorNode = hasError ? (\n \n {messageArray ? undefined : error}\n \n ) : null;\n\n const isInlineField = orientation !== \"vertical\";\n return (\n <>\n {isInlineField ? control : labelNode}\n \n {isInlineField ? labelNode : control}\n {descriptionNode}\n {errorNode}\n \n \n );\n};\n\nexport const Field: React.FC = ({\n label,\n description,\n error,\n required,\n invalid,\n disabled,\n orientation = \"vertical\",\n htmlFor,\n labelClassName,\n descriptionClassName,\n errorClassName,\n contentClassName,\n children,\n className,\n ...restProps\n}) => {\n const messageArray = toMessageArray(error);\n const hasError = messageArray\n ? messageArray.some((item) => item?.message)\n : Boolean(error);\n const isInvalid = invalid ?? hasError;\n // Only content slots switch to the flat layout; bare styling/marker props\n // must not restructure composition-mode children.\n const hasFlatSlots =\n label !== undefined || description !== undefined || error !== undefined;\n\n return (\n \n {hasFlatSlots ? (\n \n {children}\n \n ) : (\n children\n )}\n \n );\n};\n\nconst FieldContent = BaseFieldContent;\nconst FieldDescription = BaseFieldDescription;\nconst FieldError = BaseFieldError;\nconst FieldLabel = BaseFieldLabel;\nconst FieldTitle = BaseFieldTitle;\n\nexport { FieldContent, FieldDescription, FieldError, FieldLabel, FieldTitle };\n\nexport default Field;\n", "type": "registry:component", "target": "components/easy/field.tsx" } ], "type": "registry:component" }