{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "integration-card",
  "title": "Integration card",
  "description": "A list of integrations; each row opens into a card with what it does and a connect button.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/integration-card/integration-card.tsx",
      "content": "// Based on Watermelon UI's “Integration card” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten as a list of rows that open into a detail card, with no brand icons.\nimport { Check, Plus, X } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useEffect, useId, useRef, useState, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type Integration = {\n  id: string\n  name: string\n  description: string\n  icon: ReactNode\n  tags?: string[]\n  /** Short lines shown in the open card, e.g. what it listens for and what it does. */\n  triggers?: string[]\n  actions?: string[]\n  connected?: boolean\n}\n\nexport type IntegrationCardProps = {\n  title?: string\n  items: Integration[]\n  onConnectChange?: (id: string, connected: boolean) => Promise<void> | void\n  connectLabel?: string\n  connectedLabel?: string\n  disconnectLabel?: string\n  triggersLabel?: string\n  actionsLabel?: string\n  closeLabel?: string\n  className?: string\n}\n\n/** A list of integrations. Each row opens into a card with what it does and a connect button. */\nexport function IntegrationCard({\n  title = 'Integrations',\n  items,\n  onConnectChange,\n  connectLabel = 'Connect',\n  connectedLabel = 'Connected',\n  disconnectLabel = 'Disconnect',\n  triggersLabel = 'Triggers',\n  actionsLabel = 'Actions',\n  closeLabel = 'Close',\n  className,\n}: IntegrationCardProps) {\n  const [openId, setOpenId] = useState<string | null>(null)\n  const [state, setState] = useState(() => Object.fromEntries(items.map((i) => [i.id, !!i.connected])))\n  const [busy, setBusy] = useState<string | null>(null)\n  const base = useId()\n  const rows = useRef<Record<string, HTMLButtonElement | null>>({})\n  const closeRef = useRef<HTMLButtonElement>(null)\n  const open = items.find((i) => i.id === openId)\n\n  useEffect(() => {\n    if (open) closeRef.current?.focus()\n  }, [open])\n\n  const close = () => {\n    const id = openId\n    setOpenId(null)\n    if (id) requestAnimationFrame(() => rows.current[id]?.focus())\n  }\n\n  const toggle = async (id: string) => {\n    const next = !state[id]\n    setBusy(id)\n    try {\n      await onConnectChange?.(id, next)\n      setState((s) => ({ ...s, [id]: next }))\n    } finally {\n      setBusy(null)\n    }\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ type: 'spring', stiffness: 380, damping: 34 }}>\n      <motion.section\n        layout\n        className={cn('relative w-full max-w-md overflow-hidden rounded-3xl border bg-card text-card-foreground shadow-sm', className)}\n        aria-label={title}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {!open ? (\n            <motion.div key=\"list\" layout=\"position\" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>\n              <h3 className=\"px-5 pt-5 pb-2 text-sm font-semibold\">{title}</h3>\n              <ul className=\"p-2\">\n                {items.map((it) => (\n                  <li key={it.id}>\n                    <button\n                      ref={(el) => {\n                        rows.current[it.id] = el\n                      }}\n                      type=\"button\"\n                      onClick={() => setOpenId(it.id)}\n                      aria-haspopup=\"dialog\"\n                      className=\"flex w-full items-center gap-3 rounded-2xl p-3 text-left transition-colors duration-150 hover:bg-muted/60\"\n                    >\n                      <motion.span\n                        layoutId={`${base}-icon-${it.id}`}\n                        className=\"grid size-10 shrink-0 place-items-center rounded-xl border bg-card shadow-sm [&_svg]:size-5\"\n                      >\n                        {it.icon}\n                      </motion.span>\n                      <span className=\"min-w-0 flex-1\">\n                        <motion.span layoutId={`${base}-name-${it.id}`} className=\"block w-fit text-sm font-medium\">\n                          {it.name}\n                        </motion.span>\n                        <span className=\"block truncate text-xs text-muted-foreground\">{it.description}</span>\n                      </span>\n                      {state[it.id] && (\n                        <span className=\"inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-xs font-medium text-success\">\n                          <Check className=\"size-3\" strokeWidth={3} aria-hidden />\n                          {connectedLabel}\n                        </span>\n                      )}\n                    </button>\n                  </li>\n                ))}\n              </ul>\n            </motion.div>\n          ) : (\n            <motion.div\n              key={open.id}\n              layout=\"position\"\n              role=\"dialog\"\n              aria-modal=\"false\"\n              aria-labelledby={`${base}-title`}\n              onKeyDown={(e) => e.key === 'Escape' && close()}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              className=\"flex flex-col p-5\"\n            >\n              <div className=\"flex items-start gap-3\">\n                <motion.span\n                  layoutId={`${base}-icon-${open.id}`}\n                  className=\"grid size-12 shrink-0 place-items-center rounded-2xl border bg-card shadow-md [&_svg]:size-6\"\n                >\n                  {open.icon}\n                </motion.span>\n                <div className=\"min-w-0 flex-1\">\n                  <motion.h4 layoutId={`${base}-name-${open.id}`} id={`${base}-title`} className=\"w-fit font-semibold\">\n                    {open.name}\n                  </motion.h4>\n                  <p className=\"text-sm text-muted-foreground\">{open.description}</p>\n                </div>\n                <button\n                  ref={closeRef}\n                  type=\"button\"\n                  onClick={close}\n                  aria-label={closeLabel}\n                  className=\"grid size-9 shrink-0 place-items-center rounded-full hover:bg-muted\"\n                >\n                  <X className=\"size-4\" aria-hidden />\n                </button>\n              </div>\n\n              <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.08 }} className=\"mt-4 space-y-4\">\n                {!!open.tags?.length && (\n                  <ul className=\"flex flex-wrap gap-1.5\">\n                    {open.tags.map((t) => (\n                      <li key={t} className=\"rounded-full border px-2.5 py-0.5 text-xs text-muted-foreground\">\n                        {t}\n                      </li>\n                    ))}\n                  </ul>\n                )}\n                {[\n                  [triggersLabel, open.triggers],\n                  [actionsLabel, open.actions],\n                ].map(([label, list]) =>\n                  list?.length ? (\n                    <div key={label as string}>\n                      <p className=\"mb-1.5 text-xs font-medium tracking-wide text-muted-foreground uppercase\">{label}</p>\n                      <ul className=\"space-y-1 text-sm\">\n                        {(list as string[]).map((l) => (\n                          <li key={l} className=\"flex gap-2\">\n                            <span className=\"mt-2 size-1 shrink-0 rounded-full bg-foreground/40\" aria-hidden />\n                            {l}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  ) : null,\n                )}\n              </motion.div>\n\n              <button\n                type=\"button\"\n                onClick={() => busy !== open.id && toggle(open.id)}\n                aria-disabled={busy === open.id}\n                aria-busy={busy === open.id}\n                className={cn(\n                  'mt-5 inline-flex min-h-11 items-center justify-center gap-2 rounded-xl px-4 text-sm font-medium transition-colors duration-150 aria-disabled:opacity-60',\n                  state[open.id] ? 'border bg-card hover:bg-muted' : 'bg-foreground text-background hover:bg-foreground/90',\n                )}\n              >\n                {state[open.id] ? <X className=\"size-4\" aria-hidden /> : <Plus className=\"size-4\" aria-hidden />}\n                {state[open.id] ? disconnectLabel : connectLabel}\n              </button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.section>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}