{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"integration-cards","type":"registry:component","title":"Integration cards","description":"App connect grid, single-integration consent and sync health cards — optimistic connect, spring permission switches and a failed sync that heals on retry.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/geturui/examples/integration-cards/connect-grid.tsx","type":"registry:component","target":"components/examples/integration-cards/connect-grid.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { CheckCircle2, LoaderCircle, Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type IntegrationGlyph = \"ring\" | \"diamond\" | \"stack\" | \"wave\" | \"split\" | \"bolt\";\n\nexport interface IntegrationApp {\n  id: string;\n  name: string;\n  category: string;\n  description: string;\n  glyph?: IntegrationGlyph;\n  connected?: boolean;\n}\n\nexport interface IntegrationGridProps {\n  apps?: IntegrationApp[];\n  /** Resolve to finish connecting; defaults to a short simulated delay. */\n  onConnect?: (id: string) => Promise<void> | void;\n  onDisconnect?: (id: string) => void;\n  className?: string;\n}\n\ntype Status = \"idle\" | \"connecting\" | \"connected\";\nconst spring = { type: \"spring\", stiffness: 380, damping: 30 } as const;\n\n/** Monochrome mark drawn from simple shapes, so third-party apps never bring their own colours. */\nexport function Glyph({ kind = \"ring\", className }: { kind?: IntegrationGlyph; className?: string }) {\n  const shapes: Record<IntegrationGlyph, React.ReactNode> = {\n    ring: (\n      <>\n        <circle cx=\"12\" cy=\"12\" r=\"7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" />\n        <circle cx=\"12\" cy=\"12\" r=\"2.5\" fill=\"currentColor\" />\n      </>\n    ),\n    diamond: <path d=\"M12 3 21 12 12 21 3 12Z M12 8 16 12 12 16 8 12Z\" fill=\"currentColor\" fillRule=\"evenodd\" />,\n    stack: (\n      <>\n        <rect x=\"4\" y=\"4\" width=\"16\" height=\"4\" rx=\"2\" fill=\"currentColor\" />\n        <rect x=\"4\" y=\"10\" width=\"11\" height=\"4\" rx=\"2\" fill=\"currentColor\" opacity=\".7\" />\n        <rect x=\"4\" y=\"16\" width=\"7\" height=\"4\" rx=\"2\" fill=\"currentColor\" opacity=\".45\" />\n      </>\n    ),\n    wave: <path d=\"M3 15c3-6 6-6 9 0s6 6 9 0\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" />,\n    split: (\n      <>\n        <path d=\"M12 3a9 9 0 0 1 0 18Z\" fill=\"currentColor\" />\n        <circle cx=\"12\" cy=\"12\" r=\"8\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" />\n      </>\n    ),\n    bolt: <path d=\"M13 2 5 14h6l-1 8 8-12h-6Z\" fill=\"currentColor\" />,\n  };\n  return (\n    <svg viewBox=\"0 0 24 24\" aria-hidden className={className}>\n      {shapes[kind]}\n    </svg>\n  );\n}\n\n/** Grid of integrations with All/Connected filters and an optimistic connect flow: spinner, then a springy connected state. */\nexport function IntegrationGrid({\n  apps = [\n    { id: \"lumen\", name: \"Lumen CRM\", category: \"CRM\", description: \"Sync contacts and deal stages.\", glyph: \"ring\", connected: true },\n    { id: \"ledgerly\", name: \"Ledgerly\", category: \"Accounting\", description: \"Push paid invoices to your books.\", glyph: \"stack\" },\n    { id: \"parcel\", name: \"Parcel\", category: \"Shipping\", description: \"Create labels when orders ship.\", glyph: \"diamond\" },\n    { id: \"helio\", name: \"Helio Docs\", category: \"Docs\", description: \"Attach specs to any project.\", glyph: \"split\", connected: true },\n    { id: \"quartz\", name: \"Quartz\", category: \"Analytics\", description: \"Stream events to your dashboards.\", glyph: \"wave\" },\n    { id: \"relay\", name: \"Relay\", category: \"Automation\", description: \"Trigger workflows on new records.\", glyph: \"bolt\" },\n  ],\n  onConnect,\n  onDisconnect,\n  className,\n}: IntegrationGridProps) {\n  const reduce = useReducedMotion();\n  const [status, setStatus] = React.useState<Record<string, Status>>(() => Object.fromEntries(apps.map((a) => [a.id, a.connected ? \"connected\" : \"idle\"])));\n  const [filter, setFilter] = React.useState<\"all\" | \"connected\">(\"all\");\n  const timers = React.useRef<number[]>([]);\n  const tabsId = React.useId();\n  React.useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), []);\n\n  const connectedCount = apps.filter((a) => status[a.id] === \"connected\").length;\n  const visible = apps.filter((a) => filter === \"all\" || status[a.id] === \"connected\");\n\n  async function connect(id: string) {\n    setStatus((s) => ({ ...s, [id]: \"connecting\" }));\n    if (onConnect) await onConnect(id);\n    else await new Promise<void>((r) => timers.current.push(window.setTimeout(r, 1100)));\n    setStatus((s) => ({ ...s, [id]: \"connected\" }));\n  }\n\n  return (\n    <div className={cn(\"w-full max-w-[560px] rounded-lg border border-border bg-surface-raised p-4 shadow-sm\", className)}>\n      <div className=\"mb-3.5 flex flex-wrap items-center justify-between gap-2\">\n        <div>\n          <h3 className=\"font-display text-[17px] font-semibold tracking-tight text-ink\">Integrations</h3>\n          <p className=\"text-xs text-ink-subtle\">Connect the tools your team already uses.</p>\n        </div>\n        <div role=\"tablist\" aria-label=\"Filter integrations\" className=\"flex rounded-full bg-surface-sunken p-0.5\">\n          {([\"all\", \"connected\"] as const).map((f) => (\n            <button\n              key={f}\n              type=\"button\"\n              role=\"tab\"\n              aria-selected={filter === f}\n              aria-controls={tabsId}\n              onClick={() => setFilter(f)}\n              className={cn(\n                \"relative h-7 rounded-full px-3 text-[12px] font-medium outline-none transition-colors focus-visible:shadow-[var(--focus-ring)]\",\n                filter === f ? \"text-ink\" : \"text-ink-subtle hover:text-ink\",\n              )}\n            >\n              {filter === f && <motion.span layoutId={`${tabsId}-pill`} transition={spring} className=\"absolute inset-0 rounded-full border border-border bg-surface-raised shadow-sm\" />}\n              <span className=\"relative\">\n                {f === \"all\" ? \"All\" : \"Connected\"}{\" \"}\n                <span className=\"font-mono text-ink-subtle tabular-nums\">{f === \"all\" ? apps.length : connectedCount}</span>\n              </span>\n            </button>\n          ))}\n        </div>\n      </div>\n\n      <motion.ul id={tabsId} role=\"tabpanel\" layout className=\"grid grid-cols-1 gap-2 min-[400px]:grid-cols-2\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {visible.map((a) => {\n            const st = status[a.id];\n            return (\n              <motion.li\n                key={a.id}\n                layout\n                initial={{ opacity: 0, scale: reduce ? 1 : 0.94 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: reduce ? 1 : 0.94 }}\n                transition={spring}\n                className=\"flex flex-col gap-3 rounded-md border border-border bg-surface p-3\"\n              >\n                <div className=\"flex items-center justify-between gap-2\">\n                  <span className=\"grid size-10 shrink-0 place-items-center rounded-sm border border-border bg-surface-raised text-ink shadow-sm\">\n                    <Glyph kind={a.glyph} className=\"size-5\" />\n                  </span>\n                  <motion.button\n                    type=\"button\"\n                    whileTap={{ scale: 0.94 }}\n                    disabled={st === \"connecting\"}\n                    aria-label={`${st === \"connected\" ? \"Disconnect\" : \"Connect\"} ${a.name}`}\n                    onClick={() => {\n                      if (st === \"connected\") {\n                        setStatus((s) => ({ ...s, [a.id]: \"idle\" }));\n                        onDisconnect?.(a.id);\n                      } else connect(a.id);\n                    }}\n                    className={cn(\n                      \"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)]\",\n                      st === \"connected\" ? \"text-ink-muted hover:bg-surface-hover hover:text-ink\" : \"border border-border-strong text-ink hover:bg-surface-hover\",\n                    )}\n                  >\n                    {st === \"connecting\" ? (\n                      <LoaderCircle aria-hidden className=\"size-3.5 animate-spin\" />\n                    ) : st === \"connected\" ? (\n                      \"Disconnect\"\n                    ) : (\n                      <>\n                        <Plus aria-hidden className=\"size-3.5\" />\n                        Connect\n                      </>\n                    )}\n                  </motion.button>\n                </div>\n                <div className=\"min-w-0\">\n                  <p className=\"truncate text-[13.5px] font-medium text-ink\">\n                    {a.name} <span className=\"text-[11.5px] font-normal text-ink-subtle\">· {a.category}</span>\n                  </p>\n                  <AnimatePresence mode=\"popLayout\" initial={false}>\n                    {st === \"connected\" ? (\n                      <motion.p\n                        key=\"on\"\n                        initial={{ opacity: 0, y: reduce ? 0 : 4 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0 }}\n                        className=\"mt-0.5 flex min-h-[34px] items-start gap-1 text-[12px] font-medium text-success\"\n                      >\n                        <CheckCircle2 aria-hidden className=\"mt-0.5 size-3.5\" /> Connected and syncing\n                      </motion.p>\n                    ) : (\n                      <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\">\n                        {a.description}\n                      </motion.p>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n        {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>}\n      </motion.ul>\n    </div>\n  );\n}\n"},{"path":"registry/geturui/examples/integration-cards/detail.tsx","type":"registry:component","target":"components/examples/integration-cards/detail.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Eye, FileText, Lock, Pencil, ShieldCheck, Users } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface IntegrationPermission {\n  id: string;\n  title: string;\n  description: string;\n  icon?: \"read\" | \"write\" | \"people\" | \"files\";\n  /** Cannot be turned off. */\n  required?: boolean;\n  defaultGranted?: boolean;\n}\n\nexport interface IntegrationDetailProps {\n  app?: string;\n  appMonogram?: string;\n  workspace?: string;\n  workspaceMonogram?: string;\n  publisher?: string;\n  permissions?: IntegrationPermission[];\n  /** Receives the ids of granted permissions. */\n  onAuthorize?: (granted: string[]) => void;\n  onCancel?: () => void;\n  className?: string;\n}\n\nconst icons = { read: Eye, write: Pencil, people: Users, files: FileText };\nconst spring = { type: \"spring\", stiffness: 420, damping: 30 } as const;\n\n/** Consent card for a single integration: linked marks, a permissions list with spring switches and an authorize flow that resolves into a drawn check. */\nexport function IntegrationDetail({\n  app = \"Ledgerly\",\n  appMonogram = \"L\",\n  workspace = \"Fieldnote\",\n  workspaceMonogram = \"F\",\n  publisher = \"Ledgerly Inc. · Verified publisher\",\n  permissions = [\n    { id: \"read\", title: \"Read invoices and payouts\", description: \"Needed to match payments to your books.\", icon: \"read\", required: true },\n    { id: \"write\", title: \"Create draft invoices\", description: \"Drafts stay unsent until you approve them.\", icon: \"write\", defaultGranted: true },\n    { id: \"people\", title: \"View customer contacts\", description: \"Name, company and billing email only.\", icon: \"people\", defaultGranted: true },\n    { id: \"files\", title: \"Attach receipts\", description: \"Upload PDFs to matching transactions.\", icon: \"files\" },\n  ],\n  onAuthorize,\n  onCancel,\n  className,\n}: IntegrationDetailProps) {\n  const reduce = useReducedMotion();\n  const [granted, setGranted] = React.useState<Record<string, boolean>>(() => Object.fromEntries(permissions.map((p) => [p.id, !!(p.required || p.defaultGranted)])));\n  const [phase, setPhase] = React.useState<\"review\" | \"working\" | \"done\">(\"review\");\n  const uid = React.useId();\n  const count = Object.values(granted).filter(Boolean).length;\n\n  React.useEffect(() => {\n    if (phase !== \"working\") return;\n    const id = window.setTimeout(() => setPhase(\"done\"), 1100);\n    return () => window.clearTimeout(id);\n  }, [phase]);\n\n  const mark = (m: string, strong?: boolean) => (\n    <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\")}>\n      {m}\n    </span>\n  );\n\n  return (\n    <div className={cn(\"w-full max-w-[400px] overflow-hidden rounded-lg border border-border bg-surface-raised shadow-sm\", className)}>\n      <div className=\"flex flex-col items-center px-5 pt-6 text-center\">\n        <div aria-hidden className=\"relative flex items-center\">\n          {mark(workspaceMonogram, true)}\n          <svg viewBox=\"0 0 72 12\" className=\"h-3 w-[72px] text-border-strong\">\n            <motion.line\n              x1=\"4\"\n              y1=\"6\"\n              x2=\"68\"\n              y2=\"6\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeDasharray=\"3 4\"\n              animate={reduce || phase === \"done\" ? { strokeDashoffset: 0 } : { strokeDashoffset: [0, -14] }}\n              transition={{ duration: phase === \"working\" ? 0.3 : 1.2, repeat: reduce || phase === \"done\" ? 0 : Infinity, ease: \"linear\" }}\n            />\n          </svg>\n          {mark(appMonogram)}\n          <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\">\n            <Lock className=\"size-3\" />\n          </span>\n        </div>\n        <h3 className=\"mt-4 font-display text-[17px] font-semibold tracking-tight text-ink\">\n          Connect {app} to {workspace}\n        </h3>\n        <p className=\"mt-1 inline-flex items-center gap-1 text-[12px] text-ink-subtle\">\n          <ShieldCheck aria-hidden className=\"size-3.5\" />\n          {publisher}\n        </p>\n      </div>\n\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {phase !== \"done\" ? (\n          <motion.div key=\"review\" exit={{ opacity: 0, filter: \"blur(8px)\" }} transition={{ duration: 0.18 }}>\n            <p className=\"px-5 pt-5 text-[12px] font-medium text-ink-muted\">{app} will be able to</p>\n            <ul className=\"mt-1.5 px-3\">\n              {permissions.map((p) => {\n                const Icon = icons[p.icon ?? \"read\"];\n                const on = granted[p.id];\n                return (\n                  <li key={p.id} className=\"flex items-center gap-3 rounded-md px-2 py-2.5\">\n                    <span aria-hidden className=\"grid size-8 shrink-0 place-items-center rounded-sm bg-surface-sunken text-ink-muted\">\n                      <Icon className=\"size-4\" />\n                    </span>\n                    <div className=\"min-w-0 flex-1\">\n                      <p id={`${uid}-${p.id}`} className=\"text-[13px] font-medium text-ink\">\n                        {p.title}\n                      </p>\n                      <p className=\"text-[11.5px] text-ink-subtle\">{p.required ? \"Required · \" : \"\"}{p.description}</p>\n                    </div>\n                    <button\n                      type=\"button\"\n                      role=\"switch\"\n                      aria-checked={on}\n                      aria-labelledby={`${uid}-${p.id}`}\n                      disabled={p.required || phase === \"working\"}\n                      onClick={() => setGranted((g) => ({ ...g, [p.id]: !g[p.id] }))}\n                      className={cn(\n                        \"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\",\n                        on ? \"bg-ink\" : \"bg-ink/15\",\n                        p.required && \"opacity-50\",\n                      )}\n                    >\n                      <motion.span layout transition={spring} className={cn(\"block size-5 rounded-full bg-surface-raised shadow-sm\", on && \"ml-auto\")} />\n                    </button>\n                  </li>\n                );\n              })}\n            </ul>\n            <div className=\"flex gap-2 border-t border-border p-4\">\n              <motion.button\n                type=\"button\"\n                whileTap={{ scale: 0.96 }}\n                onClick={onCancel}\n                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)]\"\n              >\n                Cancel\n              </motion.button>\n              <motion.button\n                type=\"button\"\n                whileTap={{ scale: 0.96 }}\n                disabled={phase === \"working\"}\n                aria-busy={phase === \"working\"}\n                onClick={() => {\n                  setPhase(\"working\");\n                  onAuthorize?.(Object.keys(granted).filter((k) => granted[k]));\n                }}\n                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)]\"\n              >\n                {phase === \"working\" && (\n                  <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\" }} />\n                )}\n                <span className=\"relative\">\n                  {phase === \"working\" ? \"Authorizing…\" : `Authorize ${count} permission${count === 1 ? \"\" : \"s\"}`}\n                </span>\n              </motion.button>\n            </div>\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"done\"\n            role=\"status\"\n            initial={{ opacity: 0, y: reduce ? 0 : 10, filter: \"blur(8px)\" }}\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            transition={spring}\n            className=\"flex flex-col items-center px-5 pb-6 pt-5 text-center\"\n          >\n            <svg viewBox=\"0 0 48 48\" aria-hidden className=\"size-12 text-success\">\n              <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 }} />\n              <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 }} />\n            </svg>\n            <p className=\"mt-3 text-[14px] font-medium text-ink\">{app} is connected</p>\n            <p className=\"mt-1 text-[12.5px] text-ink-muted\">\n              <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.\n            </p>\n            <button\n              type=\"button\"\n              onClick={() => setPhase(\"review\")}\n              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)]\"\n            >\n              Review permissions\n            </button>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"registry/geturui/examples/integration-cards/sync-status.tsx","type":"registry:component","target":"components/examples/integration-cards/sync-status.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { CheckCircle2, LoaderCircle, OctagonAlert, RefreshCw } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SyncRun {\n  id: string;\n  /** e.g. \"Oct 2, 09:14\". */\n  at: string;\n  ok: boolean;\n  /** Seconds the run took; sets the bar height. */\n  seconds: number;\n}\n\nexport interface SyncStatusCardProps {\n  app?: string;\n  appMonogram?: string;\n  runs?: SyncRun[];\n  /** Shown while the latest run is failed. */\n  error?: string;\n  records?: number;\n  /** Records the simulated retry syncs. */\n  retryRecords?: number;\n  /** Timestamp label the retry run gets. */\n  retryAt?: string;\n  nextRun?: string;\n  defaultAutoSync?: boolean;\n  /** Resolve when your real retry finishes; defaults to a simulated run. */\n  onRetry?: () => Promise<void> | void;\n  onAutoSyncChange?: (on: boolean) => void;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 380, damping: 32 } as const;\nconst nf = new Intl.NumberFormat(\"en-US\");\n\n/** Sync health card: run history bars, a failure that collapses away on retry, a live record counter and an auto-sync switch. */\nexport function SyncStatusCard({\n  app = \"Ledgerly\",\n  appMonogram = \"L\",\n  runs = [\n    { id: \"r1\", at: \"Sep 30, 06:00\", ok: true, seconds: 42 },\n    { id: \"r2\", at: \"Sep 30, 12:00\", ok: true, seconds: 38 },\n    { id: \"r3\", at: \"Sep 30, 18:00\", ok: true, seconds: 51 },\n    { id: \"r4\", at: \"Oct 1, 00:00\", ok: true, seconds: 29 },\n    { id: \"r5\", at: \"Oct 1, 06:00\", ok: true, seconds: 44 },\n    { id: \"r6\", at: \"Oct 1, 12:00\", ok: false, seconds: 12 },\n    { id: \"r7\", at: \"Oct 1, 18:00\", ok: true, seconds: 63 },\n    { id: \"r8\", at: \"Oct 2, 00:00\", ok: true, seconds: 35 },\n    { id: \"r9\", at: \"Oct 2, 06:00\", ok: true, seconds: 40 },\n    { id: \"r10\", at: \"Oct 2, 09:14\", ok: false, seconds: 9 },\n  ],\n  error = \"Ledgerly rejected 3 invoices with a missing tax ID. Fix them or retry to skip them.\",\n  records = 12480,\n  retryRecords = 318,\n  retryAt = \"Oct 2, 09:41\",\n  nextRun = \"12:00\",\n  defaultAutoSync = true,\n  onRetry,\n  onAutoSyncChange,\n  className,\n}: SyncStatusCardProps) {\n  const reduce = useReducedMotion();\n  const [history, setHistory] = React.useState(runs);\n  const [phase, setPhase] = React.useState<\"idle\" | \"syncing\">(\"idle\");\n  const [synced, setSynced] = React.useState(0);\n  const [auto, setAuto] = React.useState(defaultAutoSync);\n  const last = history[history.length - 1];\n  const failed = !last.ok;\n  const max = Math.max(...history.map((r) => r.seconds));\n  const autoId = React.useId();\n  const [extra, setExtra] = React.useState(0);\n  const runNo = React.useRef(0);\n  const total = records + extra;\n\n  const finish = React.useCallback(() => {\n    runNo.current += 1;\n    const id = `retry-${runNo.current}`;\n    setHistory((h) => [...h.slice(1), { id, at: retryAt, ok: true, seconds: 31 }]);\n    setExtra((e) => e + retryRecords);\n    setPhase(\"idle\");\n  }, [retryAt, retryRecords]);\n\n  React.useEffect(() => {\n    if (phase !== \"syncing\" || onRetry) return;\n    if (synced >= retryRecords) {\n      const id = window.setTimeout(finish, 250);\n      return () => window.clearTimeout(id);\n    }\n    const id = window.setTimeout(() => setSynced((n) => Math.min(retryRecords, n + Math.ceil(retryRecords / 18))), 70);\n    return () => window.clearTimeout(id);\n  }, [phase, synced, retryRecords, onRetry, finish]);\n\n  async function retry() {\n    setSynced(0);\n    setPhase(\"syncing\");\n    if (onRetry) {\n      await onRetry();\n      finish();\n    }\n  }\n\n  const pill =\n    phase === \"syncing\"\n      ? { cls: \"bg-info/12 text-info\", Icon: LoaderCircle, label: \"Syncing\", spin: true }\n      : failed\n        ? { cls: \"bg-danger/12 text-danger\", Icon: OctagonAlert, label: \"Failed\", spin: false }\n        : { cls: \"bg-success/12 text-success\", Icon: CheckCircle2, label: \"Healthy\", spin: false };\n\n  return (\n    <div className={cn(\"w-full max-w-[400px] rounded-lg border border-border bg-surface-raised p-4 shadow-sm\", className)}>\n      <div className=\"flex items-center gap-3\">\n        <span aria-hidden className=\"grid size-10 shrink-0 place-items-center rounded-md bg-ink font-display text-[16px] font-bold text-bg\">\n          {appMonogram}\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"text-[14px] font-medium text-ink\">{app} sync</p>\n          <p className=\"text-xs text-ink-subtle\">\n            Last run <span className=\"font-mono tabular-nums\">{last.at}</span>\n          </p>\n        </div>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={pill.label}\n            role=\"status\"\n            initial={{ opacity: 0, scale: 0.85, filter: \"blur(6px)\" }}\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            exit={{ opacity: 0, scale: 0.85, filter: \"blur(6px)\" }}\n            transition={spring}\n            className={cn(\"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium\", pill.cls)}\n          >\n            <pill.Icon aria-hidden className={cn(\"size-3\", pill.spin && \"animate-spin\")} />\n            {pill.label}\n          </motion.span>\n        </AnimatePresence>\n      </div>\n\n      <AnimatePresence initial={false}>\n        {failed && phase === \"idle\" && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : spring}\n            className=\"overflow-hidden\"\n          >\n            <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\">\n              <OctagonAlert aria-hidden className=\"mt-0.5 size-4 shrink-0 text-danger\" />\n              {error}\n            </p>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <dl className=\"mt-4 grid grid-cols-2 gap-3\">\n        <div className=\"rounded-md bg-surface-sunken p-3\">\n          <dt className=\"text-[11px] text-ink-subtle\">{phase === \"syncing\" ? \"Records this run\" : \"Records synced\"}</dt>\n          <dd className=\"mt-0.5 font-mono text-[18px] font-medium text-ink tabular-nums\">{nf.format(phase === \"syncing\" ? synced : total)}</dd>\n        </div>\n        <div className=\"rounded-md bg-surface-sunken p-3\">\n          <dt className=\"text-[11px] text-ink-subtle\">Next scheduled run</dt>\n          <dd className=\"mt-0.5 font-mono text-[18px] font-medium text-ink tabular-nums\">{auto ? nextRun : \"Paused\"}</dd>\n        </div>\n      </dl>\n\n      <div className=\"mt-4\">\n        <p className=\"mb-2 flex justify-between text-[11px] text-ink-subtle\">\n          <span>Last {history.length} runs</span>\n          <span>\n            <span className=\"font-mono tabular-nums\">{history.filter((r) => !r.ok).length}</span> failed\n          </span>\n        </p>\n        <ol className=\"flex h-12 items-end gap-1\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {history.map((r) => (\n              <motion.li\n                key={r.id}\n                layout\n                initial={{ opacity: 0, scaleY: reduce ? 1 : 0 }}\n                animate={{ opacity: 1, scaleY: 1 }}\n                exit={{ opacity: 0, scaleX: 0 }}\n                transition={spring}\n                title={`${r.at} · ${r.ok ? \"Succeeded\" : \"Failed\"} · ${r.seconds}s`}\n                className={cn(\"flex-1 origin-bottom rounded-[3px]\", r.ok ? \"bg-ink/20\" : \"bg-danger\")}\n                style={{ height: `${Math.max(14, (r.seconds / max) * 100)}%` }}\n              >\n                <span className=\"sr-only\">\n                  {r.at}: {r.ok ? \"succeeded\" : \"failed\"} in {r.seconds} seconds\n                </span>\n              </motion.li>\n            ))}\n          </AnimatePresence>\n        </ol>\n      </div>\n\n      <div className=\"mt-4 flex items-center gap-3 border-t border-border pt-4\">\n        <label htmlFor={autoId} className=\"flex flex-1 cursor-pointer items-center gap-2.5 text-[12.5px] text-ink-muted\">\n          <button\n            id={autoId}\n            type=\"button\"\n            role=\"switch\"\n            aria-checked={auto}\n            onClick={() => {\n              setAuto(!auto);\n              onAutoSyncChange?.(!auto);\n            }}\n            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\")}\n          >\n            <motion.span layout transition={spring} className={cn(\"block size-5 rounded-full bg-surface-raised shadow-sm\", auto && \"ml-auto\")} />\n          </button>\n          Auto-sync every 6 hours\n        </label>\n        <motion.button\n          type=\"button\"\n          whileTap={{ scale: 0.96 }}\n          disabled={phase === \"syncing\"}\n          onClick={retry}\n          className={cn(\n            \"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\",\n            failed ? \"bg-volt text-on-volt\" : \"border border-border-strong text-ink hover:bg-surface-hover\",\n          )}\n        >\n          <RefreshCw aria-hidden className={cn(\"size-3.5\", phase === \"syncing\" && \"animate-spin\")} />\n          {phase === \"syncing\" ? \"Syncing…\" : failed ? \"Retry sync\" : \"Sync now\"}\n        </motion.button>\n      </div>\n    </div>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["examples","integrations","apps","connect","oauth","permissions","sync","status","settings"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/integration-cards"}}