{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "field-label", "title": "Field Label", "description": "A form label that automatically shows a required asterisk based on the Zod schema. Use SchemaFormProvider instead of Form to enable schema introspection.", "dependencies": [ "react-hook-form", "zod" ], "registryDependencies": [ "label", "form" ], "files": [ { "path": "components/ui/field-label.tsx", "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { FormProvider, type UseFormReturn } from \"react-hook-form\"\r\nimport * as z from \"zod\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\nimport { useFormField } from \"@/registry/ui/form\"\r\nimport { Label } from \"@/registry/ui/label\"\r\n\r\n// ——— Schema context ———\r\n\r\nconst ZodSchemaContext = React.createContext | null>(null)\r\n\r\nexport function useZodSchema() {\r\n return React.useContext(ZodSchemaContext)\r\n}\r\n\r\n// ——— SchemaFormProvider ———\r\n\r\ninterface SchemaFormProviderProps {\r\n form: UseFormReturn\r\n schema: z.ZodObject\r\n children: React.ReactNode\r\n}\r\n\r\nexport function SchemaFormProvider({ form, schema, children }: Readonly) {\r\n return (\r\n \r\n {children}\r\n \r\n )\r\n}\r\n\r\n// ——— Schema traversal ———\r\n\r\n// Uses any internally: Zod v4 shape values are typed as $ZodType (internal base class),\r\n// not ZodTypeAny (public), so we work untyped and cast at the boundary.\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nfunction unwrapZodType(type: any): any {\r\n // ZodOptional / ZodNullable → .unwrap()\r\n if (typeof type?.unwrap === \"function\") {\r\n return unwrapZodType(type.unwrap())\r\n }\r\n // ZodDefault → .removeDefault()\r\n if (typeof type?.removeDefault === \"function\") {\r\n return unwrapZodType(type.removeDefault())\r\n }\r\n // ZodEffects (.refine, .transform) → .innerType()\r\n if (typeof type?.innerType === \"function\") {\r\n return unwrapZodType(type.innerType())\r\n }\r\n return type\r\n}\r\n\r\nfunction resolveSchemaForPath(\r\n schema: z.ZodObject,\r\n path: string\r\n): z.ZodTypeAny | null {\r\n try {\r\n const parts = path.split(\".\")\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n let current: any = schema\r\n for (const part of parts) {\r\n const unwrapped = unwrapZodType(current)\r\n if (!unwrapped?.shape || typeof unwrapped.shape !== \"object\") return null\r\n current = unwrapped.shape[part]\r\n if (!current) return null\r\n }\r\n return current as z.ZodTypeAny\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\n// ——— FieldLabel ———\r\n\r\ninterface FieldLabelProps extends React.ComponentProps {\r\n /** Override the field name for required detection. Defaults to the name from FormField context. */\r\n name?: string\r\n}\r\n\r\nexport function FieldLabel({ name: nameProp, className, children, ...props }: Readonly) {\r\n const { name: contextName, formItemId, error } = useFormField()\r\n const schema = useZodSchema()\r\n const name = nameProp ?? contextName\r\n\r\n const fieldSchema = schema ? resolveSchemaForPath(schema, name) : null\r\n const required = fieldSchema ? !fieldSchema.isOptional() : false\r\n\r\n return (\r\n \r\n {children}\r\n {required && (\r\n \r\n *\r\n \r\n )}\r\n \r\n )\r\n}\r\n", "type": "registry:ui" } ], "type": "registry:ui" }