# Agent Setup Pill

> A floating pill that copies a setup prompt for the user's AI coding agent in one click — outcome label, a row of supported agents, a tooltip that says what the click does, and a separate dismiss button.

- Category: onboarding
- Job: Start — Help people take the first step: onboarding, starter prompts and suggested next steps.
- Slug: `agent-setup-pill`
- HTML: https://ai-patterns.app/patterns/onboarding/agent-setup-pill
- JSON: https://ai-patterns.app/api/patterns/onboarding/agent-setup-pill
- Seen in: [cloudflare](https://developers.cloudflare.com/)

## Summary
A small floating pill that hands setup off to the user's AI coding agent in one click. The label names the outcome ("Onboard your agent to Acme"), a row of agent marks shows which tools it works with, a tooltip says what the click will do, and clicking copies a ready-made setup prompt to the clipboard. A separate round button dismisses it.

## When to use
- Docs sites, dashboards and quickstarts where many users will set the product up through Claude Code, Cursor, Copilot or similar instead of by hand.
- As a lightweight, always-reachable entry point that sits alongside the regular docs — not instead of them.
- When the setup can be described as one self-contained prompt (install, configure, verify).

## When not to use
- Inside a guided onboarding step that needs to wait for a first event — use the Prompt Onboarding card, which shows the prompt and tracks status.
- When the setup needs account-specific secrets the page can't safely put in a prompt on a public page.
- As a general "Ask AI" entry point — this pill only copies a setup prompt; it doesn't open a chat.

## Anatomy
- Pill button: outcome label + a row of 3–5 agent marks.
- Tooltip above the pill: one line describing the action ("Copies a setup prompt for your AI coding tool").
- Copied state: the label swaps to a check and a short instruction; the marks collapse.
- Dismiss button: a separate circular button with an ×, visually detached from the pill so it can't be hit by accident.

## Behavior
- Enters with a short rise-and-fade once the page settles; agent marks stagger in after the label.
- Hover or keyboard focus shows the tooltip; Escape, blur and pointer leave hide it. The tooltip never appears in the copied state.
- On hover, the agent marks fan out slightly as an affordance that the pill is clickable.
- Click copies the prompt. The label rolls to "Prompt copied — paste it into your agent" with a check for ~2s, then returns. The pill resizes smoothly between the two widths.
- If the clipboard API is blocked, still confirm, and make sure the prompt is reachable elsewhere (e.g. a docs page) so the user isn't stuck.
- Dismiss hides the pill with a short exit animation and should persist (per user or per browser) — don't bring it back on the next page view.

## Content guidelines
- Label: outcome-first and product-specific — "Onboard your agent to Acme", "Set up Acme with your agent". Not "Copy prompt".
- Tooltip: one sentence, says exactly what happens — the click copies, it doesn't open anything.
- Prompt: imperative, numbered steps, an explicit definition of done, keep secrets in env vars, and ask the agent to summarize its changes.
- Agent marks: only tools the prompt has actually been tested with; keep the brand marks small and monochrome except where the brand colour is the mark.

## Accessibility
- The pill is a real `<button>`; its accessible name is the visible label. The agent marks are `aria-hidden` decoration (each still has a `title` for sighted pointer users).
- The tooltip uses `role="tooltip"` and is linked with `aria-describedby` while visible; it opens on focus as well as hover.
- The copied confirmation is announced through an `aria-live="polite"` region.
- The dismiss button has `aria-label="Dismiss"` and a 40px hit target.
- Respect `prefers-reduced-motion`: drop the rise, roll and fan-out; keep plain fades.

## Related patterns
- **Prompt-Driven Onboarding** — Use it instead when there is room for the full card with a prompt preview, snippet fallback and first-event status.
- **Setup Checklist** — Use it as well so a dismissed pill's step can live on as a checklist item.
- **Shiny Button** — Use it instead for a different single call to action, and don't show both on one screen.

## Implementation

Reference implementation (React, TypeScript, Tailwind v4, Motion, Radix). Keep every rule from the spec above; if the host project has its own design system, re-express the markup and motion in it rather than copying these classes.

npm dependencies: `lucide-react`, `motion`

Local imports to provide: `@/lib/utils` (`cn` is the usual `clsx` + `tailwind-merge` helper).

```tsx title="component.tsx"
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Check, X } from "lucide-react";

import { cn } from "@/lib/utils";

export interface SetupAgent {
  id: string;
  label: string;
  /** A small mark (~16px). Brand marks read best as monochrome or their own colours. */
  icon: React.ReactNode;
}

export interface AgentSetupPillProps {
  /** Outcome-first label, e.g. "Onboard your agent to Acme". */
  label: string;
  /** Says what the click does before it happens. */
  tooltip?: string;
  /** The setup prompt copied to the clipboard. */
  prompt: string;
  /** The coding agents the prompt works in, shown as a row of marks. */
  agents: SetupAgent[];
  /** Shown in place of the label for a moment after copying. */
  copiedLabel?: string;
  onCopy?: () => void;
  /** Omit to hide the dismiss button. */
  onDismiss?: () => void;
  className?: string;
}

const spring = { type: "spring", stiffness: 420, damping: 32 } as const;

export function AgentSetupPill({
  label,
  tooltip = "Copies a setup prompt for your AI coding tool",
  prompt,
  agents,
  copiedLabel = "Prompt copied — paste it into your agent",
  onCopy,
  onDismiss,
  className,
}: AgentSetupPillProps) {
  const reduce = useReducedMotion();
  const tooltipId = React.useId();
  const [copied, setCopied] = React.useState(false);
  const [hint, setHint] = React.useState(false);
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined);

  React.useEffect(() => () => clearTimeout(timer.current), []);

  async function copy() {
    try {
      await navigator.clipboard.writeText(prompt);
    } catch {
      // Clipboard can be blocked (insecure context, iframe); still confirm.
    }
    setCopied(true);
    setHint(false);
    onCopy?.();
    clearTimeout(timer.current);
    timer.current = setTimeout(() => setCopied(false), 2200);
  }

  return (
    <motion.div
      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.96 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.96 }}
      transition={spring}
      className={cn("flex items-center gap-2", className)}
    >
      <div className="relative">
        <AnimatePresence>
          {hint && !copied && (
            <motion.div
              id={tooltipId}
              role="tooltip"
              initial={{ opacity: 0, y: 4 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: 4 }}
              transition={{ duration: 0.15 }}
              className="pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 rounded-lg border bg-popover px-3 py-1.5 text-xs whitespace-nowrap text-popover-foreground shadow-sm"
            >
              {tooltip}
            </motion.div>
          )}
        </AnimatePresence>

        <motion.button
          type="button"
          onClick={copy}
          onPointerEnter={() => setHint(true)}
          onPointerLeave={() => setHint(false)}
          onFocus={() => setHint(true)}
          onBlur={() => setHint(false)}
          onKeyDown={(e) => e.key === "Escape" && setHint(false)}
          aria-describedby={hint ? tooltipId : undefined}
          whileTap={reduce ? undefined : { scale: 0.97 }}
          layout={!reduce}
          transition={spring}
          className="group flex h-10 items-center gap-3 rounded-full border bg-card pr-3 pl-4 text-sm font-medium text-card-foreground shadow-sm transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
        >
          <span className="relative grid overflow-hidden">
            <AnimatePresence mode="popLayout" initial={false}>
              <motion.span
                key={copied ? "copied" : "idle"}
                initial={reduce ? { opacity: 0 } : { opacity: 0, y: 12 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -12 }}
                transition={{ duration: 0.2 }}
                className="flex items-center gap-1.5 whitespace-nowrap"
              >
                {copied && <Check className="size-4 text-emerald-600 dark:text-emerald-400" aria-hidden />}
                {copied ? copiedLabel : label}
              </motion.span>
            </AnimatePresence>
          </span>

          <AnimatePresence initial={false}>
            {!copied && (
              <motion.span
                aria-hidden
                initial={{ opacity: 0, width: 0 }}
                animate={{ opacity: 1, width: "auto" }}
                exit={{ opacity: 0, width: 0 }}
                transition={{ duration: 0.2 }}
                className="flex items-center"
              >
                {agents.map((agent, i) => (
                  <motion.span
                    key={agent.id}
                    title={agent.label}
                    initial={reduce ? false : { opacity: 0, y: 6 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ ...spring, delay: reduce ? 0 : 0.15 + i * 0.05 }}
                    className="grid size-5 place-items-center transition-[margin,transform] duration-200 not-first:ml-0.5 group-hover:not-first:ml-1.5 group-hover:-translate-y-px [&_svg]:size-4"
                  >
                    {agent.icon}
                  </motion.span>
                ))}
              </motion.span>
            )}
          </AnimatePresence>
        </motion.button>

        <span className="sr-only" aria-live="polite">
          {copied ? copiedLabel : ""}
        </span>
      </div>

      {onDismiss && (
        <motion.button
          type="button"
          onClick={onDismiss}
          aria-label="Dismiss"
          whileTap={reduce ? undefined : { scale: 0.92 }}
          className="grid size-10 shrink-0 place-items-center rounded-full border bg-card text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
        >
          <X className="size-4" />
        </motion.button>
      )}
    </motion.div>
  );
}
```
