# Suggestion

> An AI or rule-based suggestion shown in place and never applied on its own: a quiet chip on a row, or ghost words that would continue a field. Taken, it says so in place, with Undo.

- Name: `suggestion`
- Status: beta
- Categories: ai, actions
- File: the install writes `suggestion.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/button`, `@arbokui/tooltip`, `@arbokui/kbd`, `@arbokui/tool-button`, `@arbokui/action-keys`. The install adds them.
- Gallery: https://arbokui.com/components/suggestion
- Registry item: https://arbokui.com/r/suggestion.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/suggestion
   ```

## Use it for

- Suggestion: a chip on a row with a next step, like 'Ask Cal Nash' or 'Start as reviewer'. Give it a reason, so hover shows why.
- SuggestionText: ghost words over a field, after what is typed, like ' at 13:05'. Tab takes them while keys is on.
- Say the ghost words in the field's description, like 'Suggested: 13:05. Press Tab to take it.' They are hidden from screen readers.
- Turn keys on for the focused row or field only, so one suggestion answers at a time.
- Give it onUndo when taking it changes something, so its record offers Undo for 8 seconds.
- When where it lands is off screen, show your own toast from onAccept. Suggestion shows no toast.

## Avoid it for

- Applying a suggestion on its own. It waits for a press, a tap, its key, or Tab.
- A dashed edge around it. Dashes mark a place where something can be dropped.
- A chip for the row's main action. Use a Button.
- Ghost text in a field that already has a placeholder showing. Keep the mark on in an empty field, so the two can't be mixed up.

## Props

- `children`: `ReactNode`. Required. Suggestion: its words. SuggestionText: the ghost words, a string. Spaces show as typed.
- `reason`: `string`. Suggestion only. Why it was suggested. The tooltip shows it, and it is the button's description.
- `onAccept`: `() => void | Promise<unknown>`. Takes it. Return a promise to read busy until it lands. A failure turns it into Retry. Show a toast here when where it lands is off screen.
- `doneLabel`: `string`. What it reads as once taken, and the record it leaves, like 'Asked Cal Nash'. Default its own words.
- `onUndo`: `() => void`. Takes it back, like clearing a prefill. With it, the record offers Undo while the window lasts.
- `onCommit`: `() => void | Promise<unknown>`. The acceptance is final: the window ended, or it left the page mid-window. Return a promise, and a failure turns it into Retry.
- `say`: `string`. What screen readers hear as it's taken. Default its doneLabel, or 'Accepted: ' and its words.
- `failed`: `string`. What it says when taking it didn't go through, like "Couldn't ask Mark Clarke". It stays on offer as Retry.
- `toast`: `string`. What a toast says once it's taken, only for what the suggestion can't show, like "Cal Nash added to Saturday". Needs a Toaster that called setToastSlot from tool-button. If the suggestion leaves mid-window, the toast keeps its Undo.
- `onDismiss`: `() => void`. Turns it down. A chip shows its × button while on offer, and Esc calls it while keys is on.
- `keys`: `boolean`. Default `false`. Listens for its keys: a chip's shortcut, Tab for ghost text in its field, and Esc for both. Turn it on for the focused row or field only.
- `shortcut`: `string`. Suggestion only. One key that takes it while keys is on, like 'S'. The tooltip teaches it. Never Tab.
- `dismissLabel`: `string`. Default `"Dismiss suggestion"`. Suggestion only. Names the × button.
- `disabled`: `boolean`. Default `false`. Suggestion only. Blocks taking it.
- `mark`: `boolean`. Default `true`. SuggestionText only. Ends the words in the sparkles mark, a button that takes it. Keep it while the field is empty.
- `ref`: `Ref<HTMLSpanElement>`. Reaches the outer span.
- `className`: `string`. Extra classes on the outer span. Later classes win.

## Accessibility

- A chip is a button named by its words. Its reason is the description. Tab moves past it, and Enter or Space takes it.
- Ghost words are hidden from screen readers, so the field's description must say them. The mark is a button named 'Accept suggestion:' and the words.
- Taken, the button is named by its record. While Undo is shown it says '… Press to undo.' After the window it stays focusable but can't be pressed.
- What happened is said once through the page's one polite live region: the record, 'Undone', or what failed. So it is still heard after the suggestion leaves its row or field.
- Keys are ignored while a modifier is held. A chip's keys are also ignored while someone types, or inside an open dialog or menu.
- Every suggestion carries the sparkles mark, so it never passes for the app's own words or a placeholder.

## Examples

### A chip on a row

```tsx
import { useState } from "react";

function NextStep() {
  const [shown, setShown] = useState(true);
  if (!shown) return null;
  return (
    <Suggestion
      reason="He came to the last three syncs"
      doneLabel="Asked Cal Nash"
      onAccept={() => fetch("/api/ask", { method: "POST" })}
      onUndo={() => {}}
      onDismiss={() => setShown(false)}
    >
      Ask Cal Nash
    </Suggestion>
  );
}
```

### Ghost text in an empty field

```tsx
import { useState } from "react";

function MeetAt() {
  const [value, setValue] = useState("");
  const [focused, setFocused] = useState(false);
  return (
    <div className="relative">
      <input
        aria-label="Meet at"
        aria-describedby="meet-hint"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        onFocus={() => setFocused(true)}
        onBlur={() => setFocused(false)}
        className="h-9 w-full rounded-md border border-input bg-card px-3 text-sm"
      />
      {value === "" ? (
        <div className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-sm">
          <SuggestionText keys={focused} onAccept={() => setValue("13:05")}>
            13:05
          </SuggestionText>
        </div>
      ) : null}
      <p id="meet-hint" className="sr-only">Suggested: 13:05. Press Tab to take it.</p>
    </div>
  );
}
```
