# useAttention

Something waits on a person, or finished while they were away: a count, a "(n) " badge on the document title, and, only where the app and the browser both allow it, a system notification or a sound. Nothing here is on by itself. The badge is removed and the count cleared the moment the window gets focus or becomes visible again, or when the app calls `clear()`. Safe to import on the server: the browser is touched only in effects and handlers.

const attention = useAttention({ notify: settings.notify, sound: { src: "/ping.mp3", enabled: settings.sound } }) attention.signal({ kind: "approval", title: "Approve: raise the export batch size" })

- Registry name: `use-attention` (`@candor/use-attention`)
- 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/use-attention

## Install

```bash
npx candor-ui add use-attention
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/use-attention`.

## Example

```tsx
"use client"

import * as React from "react"
import { useAttention } from "@/lib/use-attention"
import { Button } from "@/components/ui/button"

/*
 * Notifications and sound stay off until the person turns them on here; the hook never asks for permission itself,
 * so the button does, from the click. The title badge clears as soon as this window gets focus again.
 */
export default function Example() {
  const [notify, setNotify] = React.useState(false)
  const attention = useAttention({ notify })
  const canNotify = attention.permission === "granted"
  return (
    <div className="flex flex-col items-start gap-3 text-sm">
      <p>
        Waiting on you: <strong>{attention.count}</strong>. Switch to another tab, then look at this one’s title.
      </p>
      <div className="flex flex-wrap gap-2">
        <Button size="sm" variant="outline" onClick={() => attention.signal({ kind: "approval", title: "Approve: raise the export batch size", body: "Relay is waiting on you." })}>
          An approval arrives
        </Button>
        <Button size="sm" variant="outline" onClick={() => attention.signal({ kind: "finished", title: "Nightly import finished", body: "1,204 rows, no errors." })}>
          Work finishes
        </Button>
        <Button size="sm" variant="ghost" onClick={attention.clear}>
          Clear
        </Button>
      </div>
      <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
        {attention.permission === "unsupported" && <span>This browser has no system notifications.</span>}
        {attention.permission === "default" && (
          <Button size="xs" variant="outline" onClick={() => attention.requestPermission()}>
            Allow system notifications
          </Button>
        )}
        {attention.permission === "denied" && <span>System notifications are blocked in this browser’s settings.</span>}
        {canNotify && (
          <label className="inline-flex items-center gap-1.5">
            <input type="checkbox" checked={notify} onChange={(e) => setNotify(e.target.checked)} />
            Send a system notification for each one
          </label>
        )}
      </div>
    </div>
  )
}
```

## Functions

### useAttention

```ts
useAttention({ title, badge = true, notify = false, sound }: AttentionOptions = {})
```

Something waits on a person, or finished while they were away: a count, a "(n) " badge on the document title, and, only where the app and the browser both allow it, a system notification or a sound. Nothing here is on by itself. The badge is removed and the count cleared the moment the window gets focus or becomes visible again, or when the app calls `clear()`. Safe to import on the server: the browser is touched only in effects and handlers.

const attention = useAttention({ notify: settings.notify, sound: { src: "/ping.mp3", enabled: settings.sound } }) attention.signal({ kind: "approval", title: "Approve: raise the export batch size" })

## Types

### AttentionKind

What is asking for the person: a step waiting on them, or work that finished or failed while they were away.

```ts
"approval" | "finished" | "failed"
```

### AttentionSignal

One thing to tell the person about. `title` and `body` are shown as given, as text, in the system notification.

```ts
{ kind: AttentionKind title: string body?: string | undefined }
```

### AttentionPermission

`Notification.permission`, or "unsupported" where the browser has no Notification API (and on the server).

```ts
NotificationPermission | "unsupported"
```

### AttentionOptions

What the app allows. Everything is off unless named here, and what the browser hasn't granted stays off whatever the app says.

```ts
{ /** The page's own title, so "(2) " is put in front of it and it comes back exactly when the count clears. Left out, the title found when the first signal lands is kept and restored; pass it when the app sets titles itself, so a route change isn't undone. */ title?: string | undefined /** Put "(n) " in front of the document title while anything waits. On unless the app turns it off; the count is
```

### Attention

What useAttention returns: the count, and the three things the app can do about it.

```ts
{ /** Signals since the person was last here. 0 when nothing waits. */ count: number /** Record something that needs the person: adds one to the count, badges the title, and notifies or plays a sound when those are allowed. */ signal: (what: AttentionSignal) => void /** Back to 0: the title is restored and notifications the hook sent are closed. Also happens on its own when the window gets focus o
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
