# Capability (Notchset): prompt.md (v2.0.0)

- id: `capability` · version 2.0.0 · component · free
- category: Actions
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add https://notchset.dev/r/capability.json`
- npm dependencies: none
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json, @notchset/tooltip
- docs: https://notchset.dev/components/capability
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

What the backend can do decides what is drawn. A capability it doesn't declare hides the control (no button, shortcut or command); one unavailable right now (offline, rate-limited) is drawn disabled, its reason on hover, focus and tap. can() and unavailable() for keys..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: none beyond React.
- Files: `components/ui/notchset/capability.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `tooltip`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `Capable`, `CapabilitiesProvider`, `useCapabilities`, `can`, `unavailable`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import { Button } from "@/components/ui/notchset/button";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/capability.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when

### Not when

## Built from
- tooltip (`tooltip`), installed with it: reuse it, don't rebuild it

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import * as React from "react";
import { Ellipsis, Mail, Phone, StickyNote } from "lucide-react";
import { Button } from "@/components/ui/notchset/button";
import { Capable, CapabilitiesProvider } from "@/components/ui/notchset/capability";
import { IconButton } from "@/components/ui/notchset/icon-button";
import { Switch } from "@/components/ui/notchset/switch";

/* one record's actions under two backends: one without email (Email isn't drawn), one with email while offline */
function Actions({ title, note }: { title: string; note: string }) {
  return (
    <section className="flex min-w-0 flex-col gap-2.5 rounded-[var(--ns-radius-lg)] bg-[var(--ns-panel)] p-3.5 shadow-[0_0_0_1px_var(--ns-line)]">
      <span className="font-[family-name:var(--ns-font-mono)] text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)] uppercase">{title}</span>
      <span className="text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">{note}</span>
      <div className="flex flex-wrap items-center gap-2">
        <Capable cap="email">
          <Button>
            <Mail />
            Email
          </Button>
        </Capable>
        <Button>
          <Phone />
          Log call
        </Button>
        <Button>
          <StickyNote />
          Add note
        </Button>
        <IconButton label="More">
          <Ellipsis />
        </IconButton>
      </div>
    </section>
  );
}

export default function CapabilityDemo() {
  const [offline, setOffline] = React.useState(true);
  return (
    <div className="grid w-full max-w-[640px] gap-3 min-[640px]:grid-cols-2">
      <CapabilitiesProvider capabilities={{ email: false }}>
        <Actions title="No email in this backend" note="Email isn't drawn: nothing to press, nothing to explain." />
      </CapabilitiesProvider>
      <CapabilitiesProvider capabilities={{ email: true }} unavailable={{ email: offline ? "You're offline. Email sends once you're back." : null }}>
        <Actions title="Email, offline now" note="Disabled with its reason on hover, focus or tap. It comes back by itself." />
      </CapabilitiesProvider>
      <Switch checked={offline} onCheckedChange={(on) => setOffline(on)}>
        Offline
      </Switch>
    </div>
  );
}
```

This example also uses `button` (https://notchset.dev/components/button), `icon-button` (https://notchset.dev/components/icon-button), `switch` (https://notchset.dev/components/switch), the npm package `lucide-react`. Installing this item doesn't bring them; to paste the example as it is: `npx shadcn@latest add https://notchset.dev/r/capability.json https://notchset.dev/r/button.json https://notchset.dev/r/icon-button.json https://notchset.dev/r/switch.json`.

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `cap` | `string` |  | The capability the control needs ("email"). |
| `capabilities` | `Capabilities` |  | Overrides the provider's declaration. |
| `unavailable` | `Unavailability` |  | Overrides the provider's reasons. |
| `side` | `"top" \| "bottom"` | `"top"` |  |

Full docs: https://notchset.dev/components/capability

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add https://notchset.dev/r/capability.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/capability`. Every components.json style gets the same files.

## Continue the workflow

- [Button](https://notchset.dev/components/button)
- [Icon button](https://notchset.dev/components/icon-button)

## Source

### components/ui/notchset/capability.tsx

```tsx
// Notchset capability 2.0.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Tooltip } from "@/components/ui/notchset/tooltip";

/*
 * Capability: what the backend can do decides what is drawn. Two different answers, never mixed:
 *
 * - Unsupported (the adapter has no email): `can` is false and the control is not drawn at all: no button, no menu
 *   item, no shortcut, no command. Nothing to press, nothing to explain.
 * - Temporarily unavailable (offline, rate-limited, a provider down): the control is drawn disabled, and its reason
 *   shows on hover, on keyboard focus and on a tap, so a phone can ask why too. It comes back by itself.
 *
 *   <CapabilitiesProvider capabilities={adapter.capabilities} unavailable={{ email: offline ? "You're offline. Email sends once you're back." : null }}>
 *     <Capable cap="email"><Button><Mail />Email</Button></Capable>
 *   </CapabilitiesProvider>
 *
 * A capability counts only when declared true: an adapter says what it supports, and anything it doesn't name is
 * hidden (fail closed). Pages read can()/unavailable() for shortcuts and commands, so a key never runs what isn't drawn.
 */

/** What the backend supports: { email: true, sync: false, import: true }. Undeclared means unsupported. */
export type Capabilities = Readonly<Record<string, boolean | undefined>>;
/** Why a supported capability can't be used right now, in one sentence; null or absent when it can. */
export type Unavailability = Readonly<Record<string, string | null | undefined>>;

/** True only when the backend declares the capability. */
export function can(capabilities: Capabilities | undefined, cap: string): boolean {
  return capabilities?.[cap] === true;
}

/** The reason a supported capability is unavailable right now, or null when it can be used. */
export function unavailable(reasons: Unavailability | undefined, cap: string): string | null {
  return reasons?.[cap] || null;
}

type Ctx = { capabilities?: Capabilities | undefined; unavailable?: Unavailability | undefined };
const CapabilityContext = React.createContext<Ctx>({});

/** Declares, once near the root, what the backend supports and what is unavailable right now. */
function CapabilitiesProvider({ capabilities, unavailable: reasons, children }: Ctx & { children: React.ReactNode }) {
  const value = React.useMemo(() => ({ capabilities, unavailable: reasons }), [capabilities, reasons]);
  return <CapabilityContext.Provider value={value}>{children}</CapabilityContext.Provider>;
}

/** can() and unavailable() over the nearest provider, for shortcuts, commands and menus. */
function useCapabilities() {
  const ctx = React.useContext(CapabilityContext);
  return React.useMemo(() => ({ can: (cap: string) => can(ctx.capabilities, cap), unavailable: (cap: string) => (can(ctx.capabilities, cap) ? unavailable(ctx.unavailable, cap) : null) }), [ctx]);
}

type CapableProps = {
  /** The capability the control needs ("email"). */
  cap: string;
  /** The control: one element that takes a ref (a Button, an IconButton). */
  children: React.ReactElement;
  /** Overrides the provider's declaration. */
  capabilities?: Capabilities | undefined;
  /** Overrides the provider's reasons. */
  unavailable?: Unavailability | undefined;
  side?: "top" | "bottom" | undefined;
};

/** Draws its control only when the backend supports it; disabled, with its reason, while it's unavailable. */
function Capable({ cap, children, capabilities, unavailable: reasons, side = "top" }: CapableProps) {
  const ctx = React.useContext(CapabilityContext);
  if (!can(capabilities ?? ctx.capabilities, cap)) return null;
  const why = unavailable(reasons ?? ctx.unavailable, cap);
  if (!why) return children;
  return (
    <WithReason reason={why} side={side}>
      {children}
    </WithReason>
  );
}

type ChildProps = { className?: string; onClick?: (e: React.MouseEvent) => void };

/*
 * Disabled with its reason: aria-disabled rather than disabled, so the control keeps focus (keyboard users can
 * reach the reason) and takes a tap; a press says why instead of acting. The reason is also its description.
 */
function WithReason({ reason, side, children }: { reason: string; side: "top" | "bottom"; children: React.ReactElement }) {
  const [open, setOpen] = React.useState(false);
  const id = React.useId();
  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  React.useEffect(() => () => clearTimeout(timer.current), []);
  const child = children as React.ReactElement<ChildProps>;
  const control = React.cloneElement(child, {
    "aria-disabled": true,
    "aria-describedby": id,
    className: cn(child.props.className, "cursor-default"),
    onClick: (e: React.MouseEvent) => {
      e.preventDefault();
      e.stopPropagation();
      setOpen(true);
      clearTimeout(timer.current);
      timer.current = setTimeout(() => setOpen(false), 2600);
    },
  } as ChildProps);
  return (
    <span data-slot="capability" data-state="unavailable" className="inline-flex">
      <Tooltip label={reason} side={side} open={open} onOpenChange={setOpen}>
        {control}
      </Tooltip>
      <span id={id} hidden>
        {reason}
      </span>
    </span>
  );
}

export { Capable, CapabilitiesProvider, useCapabilities, type CapableProps };
```

The foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
