{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"field","type":"registry:ui","title":"Field","description":"A label, a hint and an error around a form control, with the ids and aria attributes wired up.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json"],"files":[{"path":"registry/dashral/field.tsx","type":"registry:file","target":"components/dashral/field.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Label as LabelPrimitive } from \"radix-ui\"\n\nimport { cn } from \"./lib/utils\"\n\ntype FieldContextValue = {\n  id: string\n  labelId?: string\n  descriptionId?: string\n  errorId?: string\n  invalid: boolean\n  disabled?: boolean\n  required?: boolean\n}\n\nconst FieldContext = React.createContext<FieldContextValue | null>(null)\n\n/** The label every field uses. Reach for it only when building a control of your own; it dims with a disabled field. */\nfunction FieldLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"field-label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** What a form control takes to become a field: a label above it, a hint and an error message under it. */\ntype FieldOwnProps = {\n  label?: React.ReactNode\n  /** A hint under the control. */\n  description?: React.ReactNode\n  /** true shows the error style; a message also shows under the control. */\n  error?: boolean | React.ReactNode\n  /** At the end of the label's row, e.g. a \"Forgot password?\" link. */\n  labelAction?: React.ReactNode\n  /** Marks the label with an asterisk. */\n  withAsterisk?: boolean\n}\n\n/** What a checkbox, switch or radio takes: a label and hint beside it. */\ntype ChoiceOwnProps = Pick<FieldOwnProps, \"label\" | \"description\" | \"error\">\n\n/**\n * Draws a field's description and error in place of the field's own (the motion editions' animated ones). The ids are\n * the ones the control's aria-describedby already points at; error is the message, or null when there's none.\n */\ntype FieldMessages = (parts: {\n  description?: React.ReactNode\n  descriptionId?: string\n  error: React.ReactNode | null\n  errorId?: string\n  /** Under a control (a field) or beside one (a checkbox's or switch's label column): how it's spaced. */\n  layout: \"field\" | \"choice\"\n  classNames?: FieldClassNames\n}) => React.ReactNode\n\ntype FieldClassNames = {\n  root?: string\n  label?: string\n  description?: string\n  error?: string\n}\n\ntype FieldProps = Omit<React.ComponentProps<\"div\">, \"id\"> &\n  FieldOwnProps & {\n    /** The control's id. Generated when left out. */\n    id?: string\n    /** Makes the control required. */\n    required?: boolean\n    disabled?: boolean\n    /** The control is a group (radios, checkboxes): the label names it through aria-labelledby, not htmlFor. */\n    group?: boolean\n    classNames?: Omit<FieldClassNames, \"root\">\n    /** Draws the description and error in place of the field's own; see FieldMessages. */\n    renderMessages?: FieldMessages\n  }\n\n/** The error's message, or null when it's only a flag (true) or nothing at all. */\nconst errorMessage = (error: FieldOwnProps[\"error\"]) => (error && typeof error !== \"boolean\" ? error : null)\n\n/**\n * A label, a hint and an error around any form control. The control inside picks up its id, aria-describedby,\n * aria-invalid, disabled and required, so they never have to be wired by hand.\n */\nfunction Field({ id, label, description, error, labelAction, required, withAsterisk, disabled, group, className, classNames, renderMessages, children, ...props }: FieldProps) {\n  const generatedId = React.useId()\n  const controlId = id ?? generatedId\n  const message = errorMessage(error)\n  const descriptionId = description ? `${controlId}-description` : undefined\n  const errorId = message ? `${controlId}-error` : undefined\n  const labelId = label && group ? `${controlId}-label` : undefined\n  const invalid = Boolean(error)\n  const labelElement = label && (\n    <FieldLabel id={labelId} htmlFor={group ? undefined : controlId} className={classNames?.label}>\n      {label}\n      {withAsterisk && (\n        <span aria-hidden className=\"-ms-1 text-destructive-text\">\n          *\n        </span>\n      )}\n    </FieldLabel>\n  )\n\n  return (\n    <FieldContext value={{ id: controlId, labelId, descriptionId, errorId, invalid, disabled, required }}>\n      <div\n        data-slot=\"field\"\n        data-disabled={disabled ? \"true\" : undefined}\n        data-invalid={invalid || undefined}\n        className={cn(\"group grid min-w-0 content-start gap-2\", group && \"gap-3\", className)}\n        {...props}\n      >\n        {labelElement && labelAction ? (\n          <div className=\"flex items-center justify-between gap-2\">\n            {labelElement}\n            <span data-slot=\"field-label-action\" className=\"text-xs text-muted-foreground\">\n              {labelAction}\n            </span>\n          </div>\n        ) : (\n          labelElement\n        )}\n        {children}\n        {renderMessages ? (\n          renderMessages({ description, descriptionId, error: message, errorId, layout: \"field\", classNames })\n        ) : (\n          <>\n            {description && (\n              <p id={descriptionId} data-slot=\"field-description\" className={cn(\"text-xs text-muted-foreground\", classNames?.description)}>\n                {description}\n              </p>\n            )}\n            {message && (\n              <FieldError id={errorId} className={classNames?.error}>\n                {message}\n              </FieldError>\n            )}\n          </>\n        )}\n      </div>\n    </FieldContext>\n  )\n}\n\ntype FieldControlProps = {\n  id?: string\n  disabled?: boolean\n  required?: boolean\n  \"aria-describedby\"?: string\n  \"aria-labelledby\"?: string\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"]\n}\n\n/** Fills in a control's id and aria attributes from the Field around it. What the control sets itself wins. */\nfunction useFieldControl<P extends FieldControlProps>(props: P): P {\n  const field = React.useContext(FieldContext)\n  if (!field) return props\n  const describedBy = joinIds(props[\"aria-describedby\"], field.descriptionId, field.errorId)\n  return {\n    ...props,\n    id: props.id ?? field.id,\n    disabled: props.disabled ?? field.disabled,\n    required: props.required ?? field.required,\n    \"aria-describedby\": describedBy,\n    \"aria-labelledby\": props[\"aria-labelledby\"] ?? field.labelId,\n    \"aria-invalid\": props[\"aria-invalid\"] ?? (field.invalid || undefined),\n  }\n}\n\n/** An error message under a control; it fades in as it appears. */\nfunction FieldError({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"field-error\"\n      className={cn(\n        \"animate-in text-xs text-destructive-text duration-(--duration-standard) ease-enter fade-in-0 slide-in-from-top-1 motion-reduce:slide-in-from-top-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** Joins ids for aria-describedby, leaving out the missing ones. */\nconst joinIds = (...ids: (string | undefined)[]) => ids.filter(Boolean).join(\" \") || undefined\n\n/** Whether a control was given anything that needs a Field around it. */\nconst needsField = ({ label, description, error }: FieldOwnProps) => Boolean(label || description || errorMessage(error))\n\ntype FieldWrapperProps = FieldOwnProps &\n  Pick<FieldProps, \"id\" | \"required\" | \"disabled\" | \"group\" | \"renderMessages\"> & {\n    classNames?: FieldClassNames\n    children: React.ReactNode\n  }\n\n/** Puts a control in a Field when it was given a label, description or error message; otherwise leaves it bare. */\nfunction FieldWrapper({ classNames, children, ...field }: FieldWrapperProps) {\n  if (!needsField(field)) return children\n  return (\n    <Field {...field} className={classNames?.root} classNames={classNames}>\n      {children}\n    </Field>\n  )\n}\n\ntype SplittableProps = FieldOwnProps & {\n  id?: string\n  required?: boolean\n  disabled?: boolean\n  classNames?: FieldClassNames\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"]\n}\n\n/**\n * Splits a control's props in two: what its FieldWrapper takes, and what the control itself takes. The control keeps\n * its id, required, disabled and classNames, and is marked invalid when there's an error.\n */\nfunction splitFieldProps<P extends SplittableProps>(props: P) {\n  const { label, description, error, labelAction, withAsterisk, ...control } = props\n  const field = {\n    label,\n    description,\n    error,\n    labelAction,\n    withAsterisk,\n    id: props.id,\n    required: props.required,\n    disabled: props.disabled,\n    classNames: props.classNames,\n  }\n  return [field, { ...control, \"aria-invalid\": props[\"aria-invalid\"] ?? (error ? true : undefined) }] as const\n}\n\ntype ChoiceControlProps = {\n  id?: string\n  \"aria-describedby\"?: string\n  \"aria-invalid\"?: true\n}\n\ntype ChoiceFieldProps = ChoiceOwnProps & {\n  id?: string\n  disabled?: boolean\n  /** The label's line height, so its first line sits level with the control: \"leading-4\" for a 16px control. */\n  leading: string\n  /** \"start\" puts the label first and the control at the far end, as in a settings row. */\n  labelPosition?: \"start\" | \"end\"\n  classNames?: FieldClassNames\n  /** Renders the control with the id and aria attributes it needs. */\n  children: (control: ChoiceControlProps) => React.ReactNode\n  /** Draws the description and error in place of the field's own; see FieldMessages. */\n  renderMessages?: FieldMessages\n}\n\n/**\n * A label and hint beside a small control: a checkbox, a switch or a radio. Clicking the label works the control.\n * With neither, it's only the control.\n */\nfunction ChoiceField({ id, label, description, error, disabled, leading, labelPosition = \"end\", classNames, children, renderMessages }: ChoiceFieldProps) {\n  const generatedId = React.useId()\n  const controlId = id ?? generatedId\n  const message = errorMessage(error)\n  const invalid = error ? (true as const) : undefined\n\n  if (!label && !description && !message) return children({ id, \"aria-invalid\": invalid })\n\n  const descriptionId = description ? `${controlId}-description` : undefined\n  const errorId = message ? `${controlId}-error` : undefined\n\n  return (\n    <div\n      data-slot=\"choice-field\"\n      data-disabled={disabled ? \"true\" : undefined}\n      className={cn(\n        \"group flex min-w-0 items-start gap-2.5\",\n        labelPosition === \"start\" && \"flex-row-reverse justify-between gap-4\",\n        classNames?.root\n      )}\n    >\n      {children({ id: controlId, \"aria-describedby\": joinIds(descriptionId, errorId), \"aria-invalid\": invalid })}\n      <div className=\"grid min-w-0 gap-1\">\n        {label && (\n          <FieldLabel\n            htmlFor={controlId}\n            className={cn(leading, description ? \"font-medium\" : \"font-normal\", classNames?.label)}\n          >\n            {label}\n          </FieldLabel>\n        )}\n        {renderMessages ? (\n          renderMessages({ description, descriptionId, error: message, errorId, layout: \"choice\", classNames })\n        ) : (\n          <>\n            {description && (\n              <p id={descriptionId} data-slot=\"field-description\" className={cn(\"text-xs text-muted-foreground\", classNames?.description)}>\n                {description}\n              </p>\n            )}\n            {message && (\n              <FieldError id={errorId} className={classNames?.error}>\n                {message}\n              </FieldError>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldError,\n  FieldWrapper,\n  ChoiceField,\n  useFieldControl,\n  needsField,\n  joinIds,\n  splitFieldProps,\n  type FieldOwnProps,\n  type ChoiceOwnProps,\n  type FieldControlProps,\n  type FieldClassNames,\n  type FieldMessages,\n}\n"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c"}}}