# Integration cards

> App connect grid, single-integration consent and sync health cards — optimistic connect, spring permission switches and a failed sync that heals on retry.

- Collection: Examples
- Tier: free
- Docs: https://geturui.com/docs/integration-cards
- Install: `npx shadcn@latest add @geturui/integration-cards`
- Dependencies: motion, lucide-react

## Variants

### Connect grid

Grid of apps with All and Connected filters, a connect flow that spins then springs to Connected, and layout-animated filtering.

Install: `npx shadcn@latest add @geturui/integration-cards-connect-grid`

```tsx
import { IntegrationGrid } from "@/components/examples/integration-cards/connect-grid";

export function Integrations() {
  return <IntegrationGrid apps={apps} onConnect={async (id) => await api.connect(id)} onDisconnect={api.disconnect} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| apps | `{ id; name; category; description; glyph?; connected? }[]` | — | Apps; glyph picks a drawn mark. |
| onConnect | `(id) => Promise<void> | void` | — | Awaited before showing Connected; defaults to a short delay. |
| onDisconnect | `(id) => void` | — | Disconnect handler. |

### Integration consent

Two linked marks, a permissions list with spring switches and locked required scopes, and an authorize flow that ends in a drawn check.

Install: `npx shadcn@latest add @geturui/integration-cards-detail`

```tsx
import { IntegrationDetail } from "@/components/examples/integration-cards/detail";

export function ConnectLedgerly() {
  return <IntegrationDetail app="Ledgerly" workspace="Fieldnote" onAuthorize={(scopes) => api.authorize(scopes)} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| app / appMonogram / workspace / workspaceMonogram | `string` | — | The two sides being linked. |
| publisher | `string` | — | Verified publisher line. |
| permissions | `{ id; title; description; icon?; required?; defaultGranted? }[]` | — | Scopes; required ones cannot be turned off. |
| onAuthorize | `(granted: string[]) => void` | — | Receives granted scope ids. |
| onCancel | `() => void` | — | Cancel handler. |

### Sync status

Sync health with run-history bars, a failure notice that collapses on retry, a live record counter and an auto-sync switch.

Install: `npx shadcn@latest add @geturui/integration-cards-sync-status`

```tsx
import { SyncStatusCard } from "@/components/examples/integration-cards/sync-status";

export function LedgerlySync() {
  return <SyncStatusCard app="Ledgerly" runs={runs} onRetry={() => api.retrySync()} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| app / appMonogram | `string` | — | Integration name and mark. |
| runs | `{ id; at; ok; seconds }[]` | — | Recent runs, oldest first. |
| error | `string` | — | Shown while the latest run is failed. |
| records / nextRun | `number / string` | — | Totals and schedule. |
| onRetry | `() => Promise<void> | void` | — | Awaited; defaults to a simulated run. |
| defaultAutoSync / onAutoSyncChange | `boolean / (on) => void` | — | Auto-sync switch. |

## Accessibility

- Third-party apps are drawn as neutral monochrome marks with their name in text, so nothing relies on brand colour.
- Permission and auto-sync toggles are role=switch with labels; connect and retry buttons expose busy state.
- Run history bars carry a screen-reader list of each run's time and outcome.

## Source

### components/examples/integration-cards/connect-grid.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { CheckCircle2, LoaderCircle, Plus } from "lucide-react";
import { cn } from "@/lib/utils";

export type IntegrationGlyph = "ring" | "diamond" | "stack" | "wave" | "split" | "bolt";

export interface IntegrationApp {
  id: string;
  name: string;
  category: string;
  description: string;
  glyph?: IntegrationGlyph;
  connected?: boolean;
}

export interface IntegrationGridProps {
  apps?: IntegrationApp[];
  /** Resolve to finish connecting; defaults to a short simulated delay. */
  onConnect?: (id: string) => Promise<void> | void;
  onDisconnect?: (id: string) => void;
  className?: string;
}

type Status = "idle" | "connecting" | "connected";
const spring = { type: "spring", stiffness: 380, damping: 30 } as const;

/** Monochrome mark drawn from simple shapes, so third-party apps never bring their own colours. */
export function Glyph({ kind = "ring", className }: { kind?: IntegrationGlyph; className?: string }) {
  const shapes: Record<IntegrationGlyph, React.ReactNode> = {
    ring: (
      <>
        <circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" strokeWidth="2.5" />
        <circle cx="12" cy="12" r="2.5" fill="currentColor" />
      </>
    ),
    diamond: <path d="M12 3 21 12 12 21 3 12Z M12 8 16 12 12 16 8 12Z" fill="currentColor" fillRule="evenodd" />,
    stack: (
      <>
        <rect x="4" y="4" width="16" height="4" rx="2" fill="currentColor" />
        <rect x="4" y="10" width="11" height="4" rx="2" fill="currentColor" opacity=".7" />
        <rect x="4" y="16" width="7" height="4" rx="2" fill="currentColor" opacity=".45" />
      </>
    ),
    wave: <path d="M3 15c3-6 6-6 9 0s6 6 9 0" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />,
    split: (
      <>
        <path d="M12 3a9 9 0 0 1 0 18Z" fill="currentColor" />
        <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="2" />
      </>
    ),
    bolt: <path d="M13 2 5 14h6l-1 8 8-12h-6Z" fill="currentColor" />,
  };
  return (
    <svg viewBox="0 0 24 24" aria-hidden className={className}>
      {shapes[kind]}
    </svg>
  );
}

/** Grid of integrations with All/Connected filters and an optimistic connect flow: spinner, then a springy connected state. */
export function IntegrationGrid({
  apps = [
    { id: "lumen", name: "Lumen CRM", category: "CRM", description: "Sync contacts and deal stages.", glyph: "ring", connected: true },
    { id: "ledgerly", name: "Ledgerly", category: "Accounting", description: "Push paid invoices to your books.", glyph: "stack" },
    { id: "parcel", name: "Parcel", category: "Shipping", description: "Create labels when orders ship.", glyph: "diamond" },
    { id: "helio", name: "Helio Docs", category: "Docs", description: "Attach specs to any project.", glyph: "split", connected: true },
    { id: "quartz", name: "Quartz", category: "Analytics", description: "Stream events to your dashboards.", glyph: "wave" },
    { id: "relay", name: "Relay", category: "Automation", description: "Trigger workflows on new records.", glyph: "bolt" },
  ],
  onConnect,
  onDisconnect,
  className,
}: IntegrationGridProps) {
  const reduce = useReducedMotion();
  const [status, setStatus] = React.useState<Record<string, Status>>(() => Object.fromEntries(apps.map((a) => [a.id, a.connected ? "connected" : "idle"])));
  const [filter, setFilter] = React.useState<"all" | "connected">("all");
  const timers = React.useRef<number[]>([]);
  const tabsId = React.useId();
  React.useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), []);

  const connectedCount = apps.filter((a) => status[a.id] === "connected").length;
  const visible = apps.filter((a) => filter === "all" || status[a.id] === "connected");

  async function connect(id: string) {
    setStatus((s) => ({ ...s, [id]: "connecting" }));
    if (onConnect) await onConnect(id);
    else await new Promise<void>((r) => timers.current.push(window.setTimeout(r, 1100)));
    setStatus((s) => ({ ...s, [id]: "connected" }));
  }

  return (
    <div className={cn("w-full max-w-[560px] rounded-lg border border-border bg-surface-raised p-4 shadow-sm", className)}>
      <div className="mb-3.5 flex flex-wrap items-center justify-between gap-2">
        <div>
          <h3 className="font-display text-[17px] font-semibold tracking-tight text-ink">Integrations</h3>
          <p className="text-xs text-ink-subtle">Connect the tools your team already uses.</p>
        </div>
        <div role="tablist" aria-label="Filter integrations" className="flex rounded-full bg-surface-sunken p-0.5">
          {(["all", "connected"] as const).map((f) => (
            <button
              key={f}
              type="button"
              role="tab"
              aria-selected={filter === f}
              aria-controls={tabsId}
              onClick={() => setFilter(f)}
              className={cn(
                "relative h-7 rounded-full px-3 text-[12px] font-medium outline-none transition-colors focus-visible:shadow-[var(--focus-ring)]",
                filter === f ? "text-ink" : "text-ink-subtle hover:text-ink",
              )}
            >
              {filter === f && <motion.span layoutId={`${tabsId}-pill`} transition={spring} className="absolute inset-0 rounded-full border border-border bg-surface-raised shadow-sm" />}
              <span className="relative">
                {f === "all" ? "All" : "Connected"}{" "}
                <span className="font-mono text-ink-subtle tabular-nums">{f === "all" ? apps.length : connectedCount}</span>
              </span>
            </button>
          ))}
        </div>
      </div>

      <motion.ul id={tabsId} role="tabpanel" layout className="grid grid-cols-1 gap-2 min-[400px]:grid-cols-2">
        <AnimatePresence mode="popLayout" initial={false}>
          {visible.map((a) => {
            const st = status[a.id];
            return (
              <motion.li
                key={a.id}
                layout
                initial={{ opacity: 0, scale: reduce ? 1 : 0.94 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: reduce ? 1 : 0.94 }}
                transition={spring}
                className="flex flex-col gap-3 rounded-md border border-border bg-surface p-3"
              >
                <div className="flex items-center justify-between gap-2">
                  <span className="grid size-10 shrink-0 place-items-center rounded-sm border border-border bg-surface-raised text-ink shadow-sm">
                    <Glyph kind={a.glyph} className="size-5" />
                  </span>
                  <motion.button
                    type="button"
                    whileTap={{ scale: 0.94 }}
                    disabled={st === "connecting"}
                    aria-label={`${st === "connected" ? "Disconnect" : "Connect"} ${a.name}`}
                    onClick={() => {
                      if (st === "connected") {
                        setStatus((s) => ({ ...s, [a.id]: "idle" }));
                        onDisconnect?.(a.id);
                      } else connect(a.id);
                    }}
                    className={cn(
                      "inline-flex h-8 shrink-0 items-center gap-1 rounded-full px-3 text-[12px] font-medium outline-none transition-colors focus-visible:shadow-[var(--focus-ring)]",
                      st === "connected" ? "text-ink-muted hover:bg-surface-hover hover:text-ink" : "border border-border-strong text-ink hover:bg-surface-hover",
                    )}
                  >
                    {st === "connecting" ? (
                      <LoaderCircle aria-hidden className="size-3.5 animate-spin" />
                    ) : st === "connected" ? (
                      "Disconnect"
                    ) : (
                      <>
                        <Plus aria-hidden className="size-3.5" />
                        Connect
                      </>
                    )}
                  </motion.button>
                </div>
                <div className="min-w-0">
                  <p className="truncate text-[13.5px] font-medium text-ink">
                    {a.name} <span className="text-[11.5px] font-normal text-ink-subtle">· {a.category}</span>
                  </p>
                  <AnimatePresence mode="popLayout" initial={false}>
                    {st === "connected" ? (
                      <motion.p
                        key="on"
                        initial={{ opacity: 0, y: reduce ? 0 : 4 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0 }}
                        className="mt-0.5 flex min-h-[34px] items-start gap-1 text-[12px] font-medium text-success"
                      >
                        <CheckCircle2 aria-hidden className="mt-0.5 size-3.5" /> Connected and syncing
                      </motion.p>
                    ) : (
                      <motion.p key="off" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="mt-0.5 line-clamp-2 min-h-[34px] text-[12px] leading-[17px] text-ink-muted">
                        {a.description}
                      </motion.p>
                    )}
                  </AnimatePresence>
                </div>
              </motion.li>
            );
          })}
        </AnimatePresence>
        {visible.length === 0 && <li className="col-span-full py-8 text-center text-[13px] text-ink-muted">Nothing connected yet. Pick a tool from All to get started.</li>}
      </motion.ul>
    </div>
  );
}

```

### components/examples/integration-cards/detail.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Eye, FileText, Lock, Pencil, ShieldCheck, Users } from "lucide-react";
import { cn } from "@/lib/utils";

export interface IntegrationPermission {
  id: string;
  title: string;
  description: string;
  icon?: "read" | "write" | "people" | "files";
  /** Cannot be turned off. */
  required?: boolean;
  defaultGranted?: boolean;
}

export interface IntegrationDetailProps {
  app?: string;
  appMonogram?: string;
  workspace?: string;
  workspaceMonogram?: string;
  publisher?: string;
  permissions?: IntegrationPermission[];
  /** Receives the ids of granted permissions. */
  onAuthorize?: (granted: string[]) => void;
  onCancel?: () => void;
  className?: string;
}

const icons = { read: Eye, write: Pencil, people: Users, files: FileText };
const spring = { type: "spring", stiffness: 420, damping: 30 } as const;

/** Consent card for a single integration: linked marks, a permissions list with spring switches and an authorize flow that resolves into a drawn check. */
export function IntegrationDetail({
  app = "Ledgerly",
  appMonogram = "L",
  workspace = "Fieldnote",
  workspaceMonogram = "F",
  publisher = "Ledgerly Inc. · Verified publisher",
  permissions = [
    { id: "read", title: "Read invoices and payouts", description: "Needed to match payments to your books.", icon: "read", required: true },
    { id: "write", title: "Create draft invoices", description: "Drafts stay unsent until you approve them.", icon: "write", defaultGranted: true },
    { id: "people", title: "View customer contacts", description: "Name, company and billing email only.", icon: "people", defaultGranted: true },
    { id: "files", title: "Attach receipts", description: "Upload PDFs to matching transactions.", icon: "files" },
  ],
  onAuthorize,
  onCancel,
  className,
}: IntegrationDetailProps) {
  const reduce = useReducedMotion();
  const [granted, setGranted] = React.useState<Record<string, boolean>>(() => Object.fromEntries(permissions.map((p) => [p.id, !!(p.required || p.defaultGranted)])));
  const [phase, setPhase] = React.useState<"review" | "working" | "done">("review");
  const uid = React.useId();
  const count = Object.values(granted).filter(Boolean).length;

  React.useEffect(() => {
    if (phase !== "working") return;
    const id = window.setTimeout(() => setPhase("done"), 1100);
    return () => window.clearTimeout(id);
  }, [phase]);

  const mark = (m: string, strong?: boolean) => (
    <span className={cn("grid size-12 place-items-center rounded-md font-display text-[18px] font-bold shadow-sm", strong ? "bg-ink text-bg" : "border border-border bg-surface text-ink")}>
      {m}
    </span>
  );

  return (
    <div className={cn("w-full max-w-[400px] overflow-hidden rounded-lg border border-border bg-surface-raised shadow-sm", className)}>
      <div className="flex flex-col items-center px-5 pt-6 text-center">
        <div aria-hidden className="relative flex items-center">
          {mark(workspaceMonogram, true)}
          <svg viewBox="0 0 72 12" className="h-3 w-[72px] text-border-strong">
            <motion.line
              x1="4"
              y1="6"
              x2="68"
              y2="6"
              stroke="currentColor"
              strokeWidth="1.5"
              strokeDasharray="3 4"
              animate={reduce || phase === "done" ? { strokeDashoffset: 0 } : { strokeDashoffset: [0, -14] }}
              transition={{ duration: phase === "working" ? 0.3 : 1.2, repeat: reduce || phase === "done" ? 0 : Infinity, ease: "linear" }}
            />
          </svg>
          {mark(appMonogram)}
          <span className="absolute left-1/2 top-1/2 grid size-6 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-border bg-surface-raised text-ink-muted">
            <Lock className="size-3" />
          </span>
        </div>
        <h3 className="mt-4 font-display text-[17px] font-semibold tracking-tight text-ink">
          Connect {app} to {workspace}
        </h3>
        <p className="mt-1 inline-flex items-center gap-1 text-[12px] text-ink-subtle">
          <ShieldCheck aria-hidden className="size-3.5" />
          {publisher}
        </p>
      </div>

      <AnimatePresence mode="wait" initial={false}>
        {phase !== "done" ? (
          <motion.div key="review" exit={{ opacity: 0, filter: "blur(8px)" }} transition={{ duration: 0.18 }}>
            <p className="px-5 pt-5 text-[12px] font-medium text-ink-muted">{app} will be able to</p>
            <ul className="mt-1.5 px-3">
              {permissions.map((p) => {
                const Icon = icons[p.icon ?? "read"];
                const on = granted[p.id];
                return (
                  <li key={p.id} className="flex items-center gap-3 rounded-md px-2 py-2.5">
                    <span aria-hidden className="grid size-8 shrink-0 place-items-center rounded-sm bg-surface-sunken text-ink-muted">
                      <Icon className="size-4" />
                    </span>
                    <div className="min-w-0 flex-1">
                      <p id={`${uid}-${p.id}`} className="text-[13px] font-medium text-ink">
                        {p.title}
                      </p>
                      <p className="text-[11.5px] text-ink-subtle">{p.required ? "Required · " : ""}{p.description}</p>
                    </div>
                    <button
                      type="button"
                      role="switch"
                      aria-checked={on}
                      aria-labelledby={`${uid}-${p.id}`}
                      disabled={p.required || phase === "working"}
                      onClick={() => setGranted((g) => ({ ...g, [p.id]: !g[p.id] }))}
                      className={cn(
                        "relative h-6 w-10 shrink-0 rounded-full p-0.5 outline-none transition-colors focus-visible:shadow-[var(--focus-ring)] disabled:cursor-not-allowed",
                        on ? "bg-ink" : "bg-ink/15",
                        p.required && "opacity-50",
                      )}
                    >
                      <motion.span layout transition={spring} className={cn("block size-5 rounded-full bg-surface-raised shadow-sm", on && "ml-auto")} />
                    </button>
                  </li>
                );
              })}
            </ul>
            <div className="flex gap-2 border-t border-border p-4">
              <motion.button
                type="button"
                whileTap={{ scale: 0.96 }}
                onClick={onCancel}
                className="inline-flex h-10 flex-1 items-center justify-center rounded-full border border-border-strong text-[13px] font-medium text-ink outline-none transition-colors hover:bg-surface-hover focus-visible:shadow-[var(--focus-ring)]"
              >
                Cancel
              </motion.button>
              <motion.button
                type="button"
                whileTap={{ scale: 0.96 }}
                disabled={phase === "working"}
                aria-busy={phase === "working"}
                onClick={() => {
                  setPhase("working");
                  onAuthorize?.(Object.keys(granted).filter((k) => granted[k]));
                }}
                className="relative inline-flex h-10 flex-[1.4] items-center justify-center overflow-hidden rounded-full bg-volt text-[13px] font-medium text-on-volt outline-none focus-visible:shadow-[var(--focus-ring)]"
              >
                {phase === "working" && (
                  <motion.span aria-hidden className="absolute inset-y-0 left-0 bg-on-volt/15" initial={{ width: "0%" }} animate={{ width: "100%" }} transition={{ duration: 1.1, ease: "easeInOut" }} />
                )}
                <span className="relative">
                  {phase === "working" ? "Authorizing…" : `Authorize ${count} permission${count === 1 ? "" : "s"}`}
                </span>
              </motion.button>
            </div>
          </motion.div>
        ) : (
          <motion.div
            key="done"
            role="status"
            initial={{ opacity: 0, y: reduce ? 0 : 10, filter: "blur(8px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={spring}
            className="flex flex-col items-center px-5 pb-6 pt-5 text-center"
          >
            <svg viewBox="0 0 48 48" aria-hidden className="size-12 text-success">
              <motion.circle cx="24" cy="24" r="21" fill="none" stroke="currentColor" strokeWidth="2.5" initial={{ pathLength: reduce ? 1 : 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.5 }} />
              <motion.path d="M15 24.5 21.5 31 33 18" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" initial={{ pathLength: reduce ? 1 : 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.35, delay: 0.4 }} />
            </svg>
            <p className="mt-3 text-[14px] font-medium text-ink">{app} is connected</p>
            <p className="mt-1 text-[12.5px] text-ink-muted">
              <span className="font-mono tabular-nums">{count}</span> of <span className="font-mono tabular-nums">{permissions.length}</span> permissions granted. You can change them in Settings at any time.
            </p>
            <button
              type="button"
              onClick={() => setPhase("review")}
              className="mt-4 rounded-full px-3 py-1.5 text-[12.5px] font-medium text-ink-muted outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:shadow-[var(--focus-ring)]"
            >
              Review permissions
            </button>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

```

### components/examples/integration-cards/sync-status.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { CheckCircle2, LoaderCircle, OctagonAlert, RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SyncRun {
  id: string;
  /** e.g. "Oct 2, 09:14". */
  at: string;
  ok: boolean;
  /** Seconds the run took; sets the bar height. */
  seconds: number;
}

export interface SyncStatusCardProps {
  app?: string;
  appMonogram?: string;
  runs?: SyncRun[];
  /** Shown while the latest run is failed. */
  error?: string;
  records?: number;
  /** Records the simulated retry syncs. */
  retryRecords?: number;
  /** Timestamp label the retry run gets. */
  retryAt?: string;
  nextRun?: string;
  defaultAutoSync?: boolean;
  /** Resolve when your real retry finishes; defaults to a simulated run. */
  onRetry?: () => Promise<void> | void;
  onAutoSyncChange?: (on: boolean) => void;
  className?: string;
}

const spring = { type: "spring", stiffness: 380, damping: 32 } as const;
const nf = new Intl.NumberFormat("en-US");

/** Sync health card: run history bars, a failure that collapses away on retry, a live record counter and an auto-sync switch. */
export function SyncStatusCard({
  app = "Ledgerly",
  appMonogram = "L",
  runs = [
    { id: "r1", at: "Sep 30, 06:00", ok: true, seconds: 42 },
    { id: "r2", at: "Sep 30, 12:00", ok: true, seconds: 38 },
    { id: "r3", at: "Sep 30, 18:00", ok: true, seconds: 51 },
    { id: "r4", at: "Oct 1, 00:00", ok: true, seconds: 29 },
    { id: "r5", at: "Oct 1, 06:00", ok: true, seconds: 44 },
    { id: "r6", at: "Oct 1, 12:00", ok: false, seconds: 12 },
    { id: "r7", at: "Oct 1, 18:00", ok: true, seconds: 63 },
    { id: "r8", at: "Oct 2, 00:00", ok: true, seconds: 35 },
    { id: "r9", at: "Oct 2, 06:00", ok: true, seconds: 40 },
    { id: "r10", at: "Oct 2, 09:14", ok: false, seconds: 9 },
  ],
  error = "Ledgerly rejected 3 invoices with a missing tax ID. Fix them or retry to skip them.",
  records = 12480,
  retryRecords = 318,
  retryAt = "Oct 2, 09:41",
  nextRun = "12:00",
  defaultAutoSync = true,
  onRetry,
  onAutoSyncChange,
  className,
}: SyncStatusCardProps) {
  const reduce = useReducedMotion();
  const [history, setHistory] = React.useState(runs);
  const [phase, setPhase] = React.useState<"idle" | "syncing">("idle");
  const [synced, setSynced] = React.useState(0);
  const [auto, setAuto] = React.useState(defaultAutoSync);
  const last = history[history.length - 1];
  const failed = !last.ok;
  const max = Math.max(...history.map((r) => r.seconds));
  const autoId = React.useId();
  const [extra, setExtra] = React.useState(0);
  const runNo = React.useRef(0);
  const total = records + extra;

  const finish = React.useCallback(() => {
    runNo.current += 1;
    const id = `retry-${runNo.current}`;
    setHistory((h) => [...h.slice(1), { id, at: retryAt, ok: true, seconds: 31 }]);
    setExtra((e) => e + retryRecords);
    setPhase("idle");
  }, [retryAt, retryRecords]);

  React.useEffect(() => {
    if (phase !== "syncing" || onRetry) return;
    if (synced >= retryRecords) {
      const id = window.setTimeout(finish, 250);
      return () => window.clearTimeout(id);
    }
    const id = window.setTimeout(() => setSynced((n) => Math.min(retryRecords, n + Math.ceil(retryRecords / 18))), 70);
    return () => window.clearTimeout(id);
  }, [phase, synced, retryRecords, onRetry, finish]);

  async function retry() {
    setSynced(0);
    setPhase("syncing");
    if (onRetry) {
      await onRetry();
      finish();
    }
  }

  const pill =
    phase === "syncing"
      ? { cls: "bg-info/12 text-info", Icon: LoaderCircle, label: "Syncing", spin: true }
      : failed
        ? { cls: "bg-danger/12 text-danger", Icon: OctagonAlert, label: "Failed", spin: false }
        : { cls: "bg-success/12 text-success", Icon: CheckCircle2, label: "Healthy", spin: false };

  return (
    <div className={cn("w-full max-w-[400px] rounded-lg border border-border bg-surface-raised p-4 shadow-sm", className)}>
      <div className="flex items-center gap-3">
        <span aria-hidden className="grid size-10 shrink-0 place-items-center rounded-md bg-ink font-display text-[16px] font-bold text-bg">
          {appMonogram}
        </span>
        <div className="min-w-0 flex-1">
          <p className="text-[14px] font-medium text-ink">{app} sync</p>
          <p className="text-xs text-ink-subtle">
            Last run <span className="font-mono tabular-nums">{last.at}</span>
          </p>
        </div>
        <AnimatePresence mode="popLayout" initial={false}>
          <motion.span
            key={pill.label}
            role="status"
            initial={{ opacity: 0, scale: 0.85, filter: "blur(6px)" }}
            animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
            exit={{ opacity: 0, scale: 0.85, filter: "blur(6px)" }}
            transition={spring}
            className={cn("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium", pill.cls)}
          >
            <pill.Icon aria-hidden className={cn("size-3", pill.spin && "animate-spin")} />
            {pill.label}
          </motion.span>
        </AnimatePresence>
      </div>

      <AnimatePresence initial={false}>
        {failed && phase === "idle" && (
          <motion.div
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={reduce ? { duration: 0 } : spring}
            className="overflow-hidden"
          >
            <p className="mt-3.5 flex gap-2 rounded-md border border-danger/25 bg-danger/8 p-3 text-[12.5px] leading-relaxed text-ink">
              <OctagonAlert aria-hidden className="mt-0.5 size-4 shrink-0 text-danger" />
              {error}
            </p>
          </motion.div>
        )}
      </AnimatePresence>

      <dl className="mt-4 grid grid-cols-2 gap-3">
        <div className="rounded-md bg-surface-sunken p-3">
          <dt className="text-[11px] text-ink-subtle">{phase === "syncing" ? "Records this run" : "Records synced"}</dt>
          <dd className="mt-0.5 font-mono text-[18px] font-medium text-ink tabular-nums">{nf.format(phase === "syncing" ? synced : total)}</dd>
        </div>
        <div className="rounded-md bg-surface-sunken p-3">
          <dt className="text-[11px] text-ink-subtle">Next scheduled run</dt>
          <dd className="mt-0.5 font-mono text-[18px] font-medium text-ink tabular-nums">{auto ? nextRun : "Paused"}</dd>
        </div>
      </dl>

      <div className="mt-4">
        <p className="mb-2 flex justify-between text-[11px] text-ink-subtle">
          <span>Last {history.length} runs</span>
          <span>
            <span className="font-mono tabular-nums">{history.filter((r) => !r.ok).length}</span> failed
          </span>
        </p>
        <ol className="flex h-12 items-end gap-1">
          <AnimatePresence initial={false} mode="popLayout">
            {history.map((r) => (
              <motion.li
                key={r.id}
                layout
                initial={{ opacity: 0, scaleY: reduce ? 1 : 0 }}
                animate={{ opacity: 1, scaleY: 1 }}
                exit={{ opacity: 0, scaleX: 0 }}
                transition={spring}
                title={`${r.at} · ${r.ok ? "Succeeded" : "Failed"} · ${r.seconds}s`}
                className={cn("flex-1 origin-bottom rounded-[3px]", r.ok ? "bg-ink/20" : "bg-danger")}
                style={{ height: `${Math.max(14, (r.seconds / max) * 100)}%` }}
              >
                <span className="sr-only">
                  {r.at}: {r.ok ? "succeeded" : "failed"} in {r.seconds} seconds
                </span>
              </motion.li>
            ))}
          </AnimatePresence>
        </ol>
      </div>

      <div className="mt-4 flex items-center gap-3 border-t border-border pt-4">
        <label htmlFor={autoId} className="flex flex-1 cursor-pointer items-center gap-2.5 text-[12.5px] text-ink-muted">
          <button
            id={autoId}
            type="button"
            role="switch"
            aria-checked={auto}
            onClick={() => {
              setAuto(!auto);
              onAutoSyncChange?.(!auto);
            }}
            className={cn("relative h-6 w-10 shrink-0 rounded-full p-0.5 outline-none transition-colors focus-visible:shadow-[var(--focus-ring)]", auto ? "bg-ink" : "bg-ink/15")}
          >
            <motion.span layout transition={spring} className={cn("block size-5 rounded-full bg-surface-raised shadow-sm", auto && "ml-auto")} />
          </button>
          Auto-sync every 6 hours
        </label>
        <motion.button
          type="button"
          whileTap={{ scale: 0.96 }}
          disabled={phase === "syncing"}
          onClick={retry}
          className={cn(
            "inline-flex h-9 items-center gap-1.5 rounded-full px-4 text-[13px] font-medium outline-none transition-colors focus-visible:shadow-[var(--focus-ring)] disabled:opacity-70",
            failed ? "bg-volt text-on-volt" : "border border-border-strong text-ink hover:bg-surface-hover",
          )}
        >
          <RefreshCw aria-hidden className={cn("size-3.5", phase === "syncing" && "animate-spin")} />
          {phase === "syncing" ? "Syncing…" : failed ? "Retry sync" : "Sync now"}
        </motion.button>
      </div>
    </div>
  );
}

```
