# Input group

A text field with things joined to it in one bordered row: a prefix or suffix (a currency, a unit, a domain, an icon), and a count against a limit. Put an `Input` (or a `Textarea`) inside with `InputGroupAddon`s at its start or end and an optional `InputGroupCount`; the group draws the border and the focus ring, and the field inside draws neither. It goes inside a `Field` like the input it holds.

An addon that names something the field needs to be understood ("per month", "@company.com") sets `describes`: it is read with the field, joined to its `aria-describedby`. One that only decorates (an icon) is hidden from screen readers.

- Registry name: `input-group` (`@candor/input-group`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/input-group

## Install

```bash
npx candor-ui add input-group
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/input-group`.
It brings in: locale, motion, tokens, use-announcer, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { AtSign } from "lucide-react"
import { Field } from "@/components/candor/ui/field"
import { Input } from "@/components/candor/ui/input"
import { Textarea } from "@/components/candor/ui/textarea"
import { InputGroup, InputGroupAddon, InputGroupCount } from "@/components/candor/ui/input-group"

// Addons that name something the field needs ("per month") are read with it; an icon is decoration. The count is
// announced only at 80% of the limit, at the limit and over it.
export default function Example() {
  const [reply, setReply] = React.useState("")
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Field label="Budget">
        <InputGroup>
          <InputGroupAddon>$</InputGroupAddon>
          <Input inputMode="decimal" />
          <InputGroupAddon align="end" describes>
            per month
          </InputGroupAddon>
        </InputGroup>
      </Field>
      <Field label="Handle">
        <InputGroup>
          <InputGroupAddon>
            <AtSign />
          </InputGroupAddon>
          <Input autoComplete="username" />
        </InputGroup>
      </Field>
      <Field label="Reply to the customer">
        <InputGroup className="items-end">
          <Textarea value={reply} onChange={(e) => setReply(e.target.value)} />
          <InputGroupCount value={reply} max={280} className="pb-2" />
        </InputGroup>
      </Field>
    </div>
  )
}
```

## <InputGroup>

Props:

- `className?: string | undefined`: Classes for the bordered row.
- `children: React.ReactNode`: An `Input` or `Textarea`, with `InputGroupAddon`s and an `InputGroupCount` beside it.
- `id?: string | undefined`: Field's wiring, passed on to the field inside: its id.
- `"aria-describedby"?: string | undefined`: Field's wiring, passed on to the field inside, before its own description.
- `"aria-invalid"?: boolean | "true" | "false" | undefined`: Field's wiring, passed on to the field inside.
- `"aria-required"?: boolean | "true" | "false" | undefined`: Field's wiring, passed on to the field inside.
- `"data-trust"?: string | undefined`: Field's trust state, passed on to the field inside.
- `attend?: boolean | undefined`: Field's ring after a failed submit, passed on to the field inside.
- `onAnimationEnd?: React.AnimationEventHandler | undefined`: Field's handler that clears the ring, passed on to the field inside.

## <InputGroupAddon>

Text or an icon joined to a field in an `InputGroup`: "$", "kg", "@company.com". At the start by default; `align="end"` puts it after the field.

Props:

- `align?: "start" | "end" | undefined` (default `"start"`): Which side of the field it sits on, in the reading direction.
- `describes?: boolean | undefined` (default `false`): It names something the field needs to be understood ("per month", "@company.com"): read with the field, joined to its description. Left out, it is decoration and hidden from screen readers.
- `className?: string | undefined`: Classes, merged with Candor's.
- `children: React.ReactNode`: The addon's text or icon.

## <InputGroupCount>

The field's length against a limit, "12 / 280", at the end of an `InputGroup` (or under a field). Counts characters as a person sees them (grapheme clusters, so an emoji is one). Screen readers hear it only when it matters: once at 80% of the limit ("56 characters left"), at the limit, and when it goes over, each once as the count crosses into it, never per key. Past the limit it turns the destructive colour; whether that stops a submit is the app's.

Props:

- `value: string`: The field's text, which it counts.
- `max?: number | undefined`: The limit. Left out, it shows the count alone and announces nothing.
- `className?: string | undefined`: Classes, merged with Candor's.

## Strings (`inputGroup`)

The fixed words, English by default. Override any of them under `inputGroup` in `<LocaleProvider strings={{ inputGroup: { … } }}>`; the table is exported as `inputGroupStrings`.

- `countOf`: {count} / {max}
- `near`: { one: "{left} character left", other: "{left} characters left" }
- `full`: Limit reached: {max} characters
- `over`: { one: "{over} character over the limit", other: "{over} characters over the limit" }

## Motion

- **count** (value, fade): The count crossfades when it changes, at most once a second, so a burst of typing plays it once, not per key. Its text is always the current count.
- **prefix** (arrive, rise): A start addon that appears after the group first drew rises in.
- **suffix** (pop, scale): An end addon that appears after the group first drew fades in.

Set with `--cd-motion-input-group-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
