{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field",
  "title": "Field",
  "author": "AniBT <https://anibt.net>",
  "description": "AniBT form grammar on shadcn's field names: label above the control, tags and meta on the label row, a hint or an error under it, choice rows and cards, container-query field rows and stitched field-group cards.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "@anibt/theme",
    "@anibt/label"
  ],
  "files": [
    {
      "path": "registry/anibt/ui/field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { AlertCircleIcon, LockIcon } from \"lucide-react\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/registry/anibt/ui/label\"\n\n/**\n * Field — AniBT's form grammar (A 「贴纸手账」), on shadcn's field names.\n *\n * The rules, the same in every form:\n * - The label sits above its control: 13/600 `--anibt-ink2`, 6px over it.\n *   Required is a pink `*` (read as 「必填」). Beside the label only small\n *   facts (`FieldTag`: a source, 已锁定, 只读); the label row's far end holds\n *   meta (a count, a time) and wraps under the label on a narrow field.\n * - Under the control: a 12px hint in `--muted-foreground`, or a 12px error\n *   in `--destructive` behind its glyph (`aria-live`) — never both.\n * - 16px between fields. A group of fields is one card (`FieldGroup` with a\n *   `title`: paper, a stitched 14/700 head, 16px padding); 20px between cards.\n * - Two or three fields side by side only for short values, and only when\n *   the row itself is wide enough (`FieldRow`, a container query).\n * - Option text beside a checkbox or a switch is body copy, 13/400 ink\n *   (`orientation=\"horizontal\"`).\n *\n * Two ways to write it:\n * - the one-call form: `<Field label=\"标题\" hint=\"…\" error={…}>` with the\n *   control inside `FieldControl`, which gives it the field's id, its\n *   `aria-describedby` and `aria-invalid`;\n * - shadcn's composition (`FieldSet`, `FieldLegend`, `FieldLabel`,\n *   `FieldContent`, `FieldTitle`, `FieldDescription`, `FieldError`,\n *   `FieldSeparator`), with the same exports and props as shadcn's field so\n *   code written for it keeps compiling.\n */\n\ntype FieldContextValue = {\n  controlId: string\n  labelId: string\n  hintId: string\n  errorId: string\n  hasHint: boolean\n  hasError: boolean\n  labelFor: \"control\" | \"group\"\n}\n\nconst FieldContext = React.createContext<FieldContextValue | null>(null)\n\n/** Groups of fields: a plain stack, or — with a `title` — one A card. */\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n  return (\n    <fieldset\n      data-slot=\"field-set\"\n      className={cn(\n        \"flex min-w-0 flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** A fieldset's caption: `legend` is the 14/700 head, `label` the 13/600 label tier. */\nfunction FieldLegend({\n  className,\n  variant = \"legend\",\n  ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n  return (\n    <legend\n      data-slot=\"field-legend\"\n      data-variant={variant}\n      className={cn(\n        \"mb-1.5 data-[variant=label]:text-[13px] data-[variant=label]:leading-5 data-[variant=label]:font-semibold data-[variant=label]:text-anibt-ink2 data-[variant=legend]:text-[14px] data-[variant=legend]:leading-[1.4] data-[variant=legend]:font-bold data-[variant=legend]:text-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype FieldGroupProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** Turns the group into an A card with this stitched 14/700 head. */\n  title?: React.ReactNode\n  /** A 12px note beside the title (the card form only). */\n  note?: React.ReactNode\n  /** Tools at the head's far end (the card form only). */\n  actions?: React.ReactNode\n  /** Classes for the card's body, where the fields stack. */\n  bodyClassName?: string\n}\n\nfunction FieldGroup({ title, note, actions, bodyClassName, className, children, ...props }: FieldGroupProps) {\n  const headingId = React.useId()\n  const stack =\n    \"group/field-group @container/field-group flex w-full min-w-0 flex-col gap-4 has-[>[data-slot=field-group]]:gap-5 data-[slot=checkbox-group]:gap-3\"\n\n  if (title === undefined || title === null) {\n    return (\n      <div data-slot=\"field-group\" className={cn(stack, className)} {...props}>\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <section\n      data-slot=\"field-group\"\n      data-variant=\"card\"\n      aria-labelledby={headingId}\n      className={cn(\"anibt-sheet min-w-0 scroll-mt-3 overflow-clip\", className)}\n      {...(props as React.ComponentProps<\"section\">)}\n    >\n      <div className=\"anibt-stitch-under flex min-h-11 min-w-0 items-center gap-2 px-4 py-2\">\n        <h3 id={headingId} className=\"anibt-ell text-[14px] leading-[1.4] font-bold text-foreground\">\n          {title}\n        </h3>\n        {note ? <span className=\"anibt-ell text-[12px] text-muted-foreground\">{note}</span> : null}\n        {actions ? <div className=\"ml-auto flex shrink-0 items-center gap-2\">{actions}</div> : null}\n      </div>\n      <div className={cn(stack, \"p-4\", bodyClassName)}>{children}</div>\n    </section>\n  )\n}\n\nconst fieldVariants = cva(\"group/field w-full min-w-0\", {\n  variants: {\n    orientation: {\n      // Label row, control, hint: 6px apart. A select trigger takes the width.\n      vertical: \"grid content-start gap-1.5 *:data-[slot=select-trigger]:w-full\",\n      // A choice row: the control, then its option text (13/400 ink).\n      horizontal:\n        \"flex flex-row items-center gap-2 has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto *:data-[slot=field-label]:font-normal *:data-[slot=field-label]:text-foreground has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>:disabled]:*:data-[slot=field-label]:text-muted-foreground data-[disabled=true]:*:data-[slot=field-label]:text-muted-foreground\",\n      // Stacked in a narrow group, label beside the control from 28rem.\n      responsive:\n        \"flex flex-col gap-1.5 *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto\",\n    },\n  },\n  defaultVariants: {\n    orientation: \"vertical\",\n  },\n})\n\ntype FieldProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  VariantProps<typeof fieldVariants> & {\n    /** The one-call form: the field's label, drawn above the control. */\n    label?: React.ReactNode\n    /** The control's id; generated when absent. The label points at it. */\n    id?: string\n    /** A pink `*` after the label, read as `requiredLabel`. */\n    required?: boolean\n    /** Shown but not editable here: a 只读 tag beside the label. */\n    readOnly?: boolean\n    /** Small tags beside the label: a source, a lock (`FieldTag`). */\n    tags?: React.ReactNode\n    /** The label row's far end: a count, a time, a disclosure. */\n    meta?: React.ReactNode\n    /** 12px help under the control. Hidden while there is an error. */\n    hint?: React.ReactNode\n    /** The error under the control; also marks the control invalid. */\n    error?: React.ReactNode\n    /**\n     * `group` for a set of controls (a segmented control, a chip row) that\n     * is named with `aria-labelledby` instead of `for`.\n     */\n    labelFor?: \"control\" | \"group\"\n    /** What a screen reader says for the `*`. */\n    requiredLabel?: string\n    /** The read-only tag's word. */\n    readOnlyLabel?: string\n    children?: React.ReactNode\n  }\n\nfunction Field({\n  className,\n  orientation = \"vertical\",\n  label,\n  id,\n  required,\n  readOnly,\n  tags,\n  meta,\n  hint,\n  error,\n  labelFor = \"control\",\n  requiredLabel = \"必填\",\n  readOnlyLabel = \"只读\",\n  children,\n  ...props\n}: FieldProps) {\n  const reactId = React.useId()\n  const controlId = id ?? `${reactId}-control`\n  const hasHint = Boolean(hint)\n  const hasError = Boolean(error)\n  const context = React.useMemo<FieldContextValue>(\n    () => ({\n      controlId,\n      labelId: `${controlId}-label`,\n      hintId: `${controlId}-hint`,\n      errorId: `${controlId}-error`,\n      hasHint,\n      hasError,\n      labelFor,\n    }),\n    [controlId, hasHint, hasError, labelFor]\n  )\n\n  // The one-call form puts the label above the control (or, horizontal,\n  // beside a choice); \"responsive\" is for shadcn's composition only.\n  const layout = label !== undefined && label !== null && orientation === \"responsive\" ? \"vertical\" : orientation\n\n  const shell = (body: React.ReactNode) => (\n    <FieldContext.Provider value={context}>\n      <div\n        role=\"group\"\n        data-slot=\"field\"\n        data-orientation={layout}\n        data-invalid={hasError || undefined}\n        className={cn(fieldVariants({ orientation: layout }), className)}\n        {...props}\n      >\n        {body}\n      </div>\n    </FieldContext.Provider>\n  )\n\n  if (label === undefined || label === null) return shell(children)\n\n  const labelText = (\n    <>\n      {label}\n      {required ? (\n        <>\n          <span className=\"text-accent-foreground\" aria-hidden=\"true\">\n            *\n          </span>\n          <span className=\"sr-only\">{requiredLabel}</span>\n        </>\n      ) : null}\n    </>\n  )\n  const under = hasError ? (\n    <FieldError id={context.errorId}>{error}</FieldError>\n  ) : hasHint ? (\n    <FieldDescription id={context.hintId}>{hint}</FieldDescription>\n  ) : null\n\n  if (orientation === \"horizontal\") {\n    return shell(\n      <>\n        {children}\n        <FieldContent>\n          <FieldLabel\n            id={context.labelId}\n            htmlFor={labelFor === \"control\" ? controlId : undefined}\n            className=\"font-normal text-foreground\"\n          >\n            {labelText}\n          </FieldLabel>\n          {under}\n        </FieldContent>\n      </>\n    )\n  }\n\n  return shell(\n    <>\n      {/* The label row wraps: on a narrow field the meta drops under the\n          label instead of squeezing it. */}\n      <div className=\"flex min-h-6 min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1\">\n        <FieldLabel id={context.labelId} htmlFor={labelFor === \"control\" ? controlId : undefined} className=\"min-w-0 shrink\">\n          {labelText}\n        </FieldLabel>\n        {readOnly ? <FieldTag icon={LockIcon}>{readOnlyLabel}</FieldTag> : null}\n        {tags}\n        {meta ? (\n          <div className=\"ml-auto flex max-w-full min-w-0 items-center gap-1.5 text-[12px] text-muted-foreground\">{meta}</div>\n        ) : null}\n      </div>\n      {children}\n      {under}\n    </>\n  )\n}\n\n/**\n * Gives the single child the field's id (or, for `labelFor=\"group\"`, its\n * `aria-labelledby`), the hint or error as its description and its invalid\n * state. Outside a `Field` it passes the child through.\n */\nfunction FieldControl(props: React.ComponentProps<typeof Slot.Root>) {\n  const field = React.useContext(FieldContext)\n  if (!field) return <Slot.Root {...props} />\n  const describedBy = field.hasError ? field.errorId : field.hasHint ? field.hintId : undefined\n  return (\n    <Slot.Root\n      id={field.controlId}\n      aria-labelledby={field.labelFor === \"group\" ? field.labelId : undefined}\n      aria-describedby={describedBy}\n      aria-invalid={field.hasError || undefined}\n      {...props}\n    />\n  )\n}\n\n/** Title and description stacked beside a control in a horizontal field. */\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-content\"\n      className={cn(\"group/field-content flex min-w-0 flex-1 flex-col gap-0.5 leading-snug\", className)}\n      {...props}\n    />\n  )\n}\n\n/**\n * The field's label (a `Label`). Wrapping a whole `Field` in it makes a\n * choice card: an A well that turns pink when its control is checked.\n */\nfunction FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {\n  return (\n    <Label\n      data-slot=\"field-label\"\n      className={cn(\n        \"group/field-label peer/field-label flex w-fit gap-1.5 leading-5 group-data-[disabled=true]/field:text-muted-foreground\",\n        \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:items-stretch has-[>[data-slot=field]]:rounded-[10px] has-[>[data-slot=field]]:bg-card has-[>[data-slot=field]]:font-normal has-[>[data-slot=field]]:text-foreground has-[>[data-slot=field]]:inset-ring-1 has-[>[data-slot=field]]:inset-ring-border has-[>[data-slot=field]]:transition-[background-color,box-shadow] has-[>[data-slot=field]]:duration-150 *:data-[slot=field]:px-3 *:data-[slot=field]:py-2.5\",\n        \"has-[>[data-slot=field]]:not-has-[:disabled]:hover:inset-ring-anibt-wash-line\",\n        // Picked (a radio or a checkbox inside): the pink wash. A switch is a\n        // setting, not a pick, so an \"on\" settings card stays paper.\n        \"has-[>[data-slot=field]]:has-[[role=radio][data-state=checked]]:bg-accent has-[>[data-slot=field]]:has-[[role=radio][data-state=checked]]:inset-ring-anibt-wash-line has-[>[data-slot=field]]:has-[[role=checkbox][data-state=checked]]:bg-accent has-[>[data-slot=field]]:has-[[role=checkbox][data-state=checked]]:inset-ring-anibt-wash-line\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** A label-tier title that labels nothing (a choice card's head). */\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-label\"\n      className={cn(\n        \"flex w-fit items-center gap-1.5 text-[13px] leading-5 font-semibold text-foreground group-data-[disabled=true]/field:text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** The hint under a control (or under a title): 12px, muted. */\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"field-description\"\n      className={cn(\n        \"text-[12px] leading-normal font-normal text-muted-foreground [[data-variant=legend]+&]:-mt-1\",\n        \"[&>a]:text-accent-foreground [&>a]:underline-offset-4 [&>a:hover]:underline\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** A stitched rule between fields, with an optional word in the middle. */\nfunction FieldSeparator({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  children?: React.ReactNode\n}) {\n  return (\n    <div\n      data-slot=\"field-separator\"\n      data-content={!!children}\n      className={cn(\"relative -my-1 h-5 text-[12px]\", className)}\n      {...props}\n    >\n      <div aria-hidden=\"true\" className=\"anibt-stitch absolute inset-x-0 top-1/2\" />\n      {children ? (\n        <span\n          data-slot=\"field-separator-content\"\n          className=\"relative mx-auto block w-fit bg-card px-2 leading-5 text-muted-foreground\"\n        >\n          {children}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\n/**\n * The error under a control: 12px `--destructive` behind its glyph,\n * announced politely. Takes `children`, or shadcn's `errors` list (a\n * validator's `{ message }` objects; duplicates are dropped).\n */\nfunction FieldError({\n  className,\n  children,\n  errors,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  errors?: Array<{ message?: string } | undefined>\n}) {\n  const content = React.useMemo(() => {\n    if (children) return children\n    if (!errors?.length) return null\n\n    const unique = [...new Map(errors.map((error) => [error?.message, error])).values()]\n    if (unique.length === 1) return unique[0]?.message\n\n    return (\n      <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n        {unique.map((error, index) => (error?.message ? <li key={index}>{error.message}</li> : null))}\n      </ul>\n    )\n  }, [children, errors])\n\n  if (!content) return null\n\n  return (\n    <div\n      data-slot=\"field-error\"\n      aria-live=\"polite\"\n      className={cn(\"flex items-start gap-1 text-[12px] leading-normal font-normal text-destructive\", className)}\n      {...props}\n    >\n      <AlertCircleIcon className=\"mt-px size-3.5 shrink-0\" aria-hidden=\"true\" />\n      <div className=\"min-w-0\">{content}</div>\n    </div>\n  )\n}\n\n/** AniBT's name for the hint under a control. */\nconst FieldHint = FieldDescription\n\ntype FieldTagTone = \"neutral\" | \"accent\" | \"ok\" | \"warn\" | \"danger\"\n\nconst TAG_TONE: Record<FieldTagTone, string> = {\n  neutral: \"\",\n  accent: \"anibt-tag-lang\",\n  ok: \"anibt-tag-ok\",\n  warn: \"anibt-tag-warn\",\n  danger: \"anibt-tag-danger\",\n}\n\n/**\n * A small tag beside a label: a fact about the field (a source, 已锁定,\n * 只读), not a control. Never tilted — form rows stay straight.\n */\nfunction FieldTag({\n  icon: Icon,\n  tone = \"neutral\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"span\"> & {\n  icon?: React.ComponentType<{ className?: string; \"aria-hidden\"?: boolean | \"true\" }>\n  tone?: FieldTagTone\n}) {\n  return (\n    <span data-slot=\"field-tag\" data-tone={tone} className={cn(\"anibt-tag shrink-0\", TAG_TONE[tone], className)} {...props}>\n      {Icon ? <Icon className=\"size-3 shrink-0\" aria-hidden=\"true\" /> : null}\n      {children}\n    </span>\n  )\n}\n\n/**\n * Short fields side by side — only once the row itself is wide enough (30rem\n * for two, 34rem for three), stacked otherwise. Titles, URLs and prose take\n * a row of their own.\n */\nfunction FieldRow({ columns = 2, className, ...props }: React.ComponentProps<\"div\"> & { columns?: 2 | 3 }) {\n  return (\n    <div className=\"@container/field-row min-w-0\">\n      <div\n        data-slot=\"field-row\"\n        className={cn(\n          \"grid min-w-0 gap-4\",\n          columns === 3 ? \"@[34rem]/field-row:grid-cols-3\" : \"@[30rem]/field-row:grid-cols-2\",\n          className\n        )}\n        {...props}\n      />\n    </div>\n  )\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLegend,\n  FieldSeparator,\n  FieldSet,\n  FieldContent,\n  FieldTitle,\n  FieldControl,\n  FieldHint,\n  FieldRow,\n  FieldTag,\n  fieldVariants,\n}\nexport type { FieldProps, FieldGroupProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}