{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "integrations-directory",
  "title": "Integrations Directory",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/integrations-directory/IntegrationsDirectory.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n  BarChart3,\n  Check,\n  Container,\n  CreditCard,\n  LineChart,\n  Mail,\n  MessageSquare,\n  Radar,\n  Receipt,\n  ScrollText,\n  Search,\n  SearchX,\n} from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Chip } from '@/components/ui/chip'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'\nimport { EmptyState } from '@/components/ui/empty-state'\nimport { Input } from '@/components/ui/input'\nimport { Switch } from '@/components/ui/switch'\n\nexport type CategoryId = 'analytics' | 'devops' | 'communication' | 'billing'\nexport type CategoryFilter = 'all' | CategoryId\n\nexport interface Integration {\n  id: string\n  name: string\n  description: string\n  /** Longer paragraph shown in the detail view. */\n  detail: string\n  category: CategoryId\n  author: string\n  /** Lucide glyph standing in for the product logo. */\n  icon: LucideIcon\n  connected?: boolean\n  permissions: string[]\n}\n\nexport interface IntegrationsDirectoryProps {\n  /** Replace the built-in stub catalog. */\n  integrations?: Integration[]\n  initialQuery?: string\n  initialCategory?: CategoryFilter\n  initialInstalledOnly?: boolean\n  /** Render this integration's detail body as an always-visible panel (docs/story aid). */\n  featuredId?: string\n  className?: string\n}\n\nconst CATEGORIES: { id: CategoryFilter; label: string }[] = [\n  { id: 'all', label: 'All' },\n  { id: 'analytics', label: 'Analytics' },\n  { id: 'devops', label: 'DevOps' },\n  { id: 'communication', label: 'Communication' },\n  { id: 'billing', label: 'Billing' },\n]\n\nconst STUB_INTEGRATIONS: Integration[] = [\n  {\n    id: 'pulseboard',\n    name: 'Pulseboard',\n    description: 'Funnels, retention curves and cohort exploration for product teams.',\n    detail:\n      'Pulseboard plugs into your event stream and turns raw activity into funnels, retention curves and cohort views. Dashboards sync hourly, and annotations push back into your workspace so launches stay marked on every chart.',\n    category: 'analytics',\n    author: 'Pulseboard Labs',\n    icon: BarChart3,\n    connected: true,\n    permissions: ['Read workspace profile', 'Read dashboards and events', 'Create annotations and highlights'],\n  },\n  {\n    id: 'metrica',\n    name: 'Metrica',\n    description: 'Privacy-friendly web metrics with no cookie banner required.',\n    detail:\n      'Metrica collects pageviews and referrers without cookies or personal data, so sites stay compliant out of the box. Live visitor counts, entry pages and campaign attribution arrive as lightweight embeds or a full dashboard.',\n    category: 'analytics',\n    author: 'Metrica Inc.',\n    icon: LineChart,\n    permissions: ['Read site metadata', 'Record aggregate pageviews'],\n  },\n  {\n    id: 'shipyard',\n    name: 'Shipyard',\n    description: 'Zero-config preview environments for every pull request.',\n    detail:\n      'Shipyard builds an isolated environment for each pull request, complete with seeded databases and shareable URLs. Deploys post status checks back to your repository, and idle previews sleep automatically to keep costs flat.',\n    category: 'devops',\n    author: 'Shipyard Systems',\n    icon: Container,\n    connected: true,\n    permissions: ['Read pull requests and statuses', 'Create deployment check runs', 'Publish preview URLs'],\n  },\n  {\n    id: 'watchtower',\n    name: 'Watchtower',\n    description: 'Uptime, SSL and domain monitoring with on-call escalations.',\n    detail:\n      'Watchtower probes your endpoints from twelve regions and escalates incidents through on-call schedules when thresholds break. SSL expiry, DNS drift and port changes are tracked alongside uptime so surprises surface early.',\n    category: 'devops',\n    author: 'Northwind Ops',\n    icon: Radar,\n    permissions: ['Read incident history', 'Register webhook endpoints', 'Send alert notifications'],\n  },\n  {\n    id: 'loghound',\n    name: 'Loghound',\n    description: 'Structured log search across every service and region.',\n    detail:\n      'Loghound ingests structured logs from every service, indexes them in seconds and supports SQL-ish queries with saved views. Tail live streams during deploys and pipe matched events straight into your alerting rules.',\n    category: 'devops',\n    author: 'Houndworks',\n    icon: ScrollText,\n    permissions: ['Read log streams', 'Create saved queries', 'Forward events to webhooks'],\n  },\n  {\n    id: 'chatterbox',\n    name: 'Chatterbox',\n    description: 'Team chat with threads, huddles and shared channels.',\n    detail:\n      'Chatterbox keeps conversations organized with threaded channels, quick huddles and shared external channels. Bots receive messages through webhooks and can post rich updates, keeping build and incident feeds where the team already works.',\n    category: 'communication',\n    author: 'Chatterbox Co.',\n    icon: MessageSquare,\n    permissions: ['Read channel names and topics', 'Post messages as the app', 'Upload notification files'],\n  },\n  {\n    id: 'mailroom',\n    name: 'Mailroom',\n    description: 'Transactional email with templates and delivery insights.',\n    detail:\n      'Mailroom sends transactional email from versioned templates with per-recipient variables. Delivery webhooks report bounces, complaints and opens, and suppression lists keep problematic addresses out of future sends.',\n    category: 'communication',\n    author: 'Mailroom Ltd.',\n    icon: Mail,\n    permissions: ['Send email on your behalf', 'Read delivery and bounce events', 'Manage suppression list'],\n  },\n  {\n    id: 'ledgerly',\n    name: 'Ledgerly',\n    description: 'Invoicing, tax handling and revenue recognition in one flow.',\n    detail:\n      'Ledgerly generates invoices from your billing events, applies regional tax rules automatically and reconciles payments against your ledger. Revenue schedules export to your accounting package month by month.',\n    category: 'billing',\n    author: 'Ledgerly Inc.',\n    icon: Receipt,\n    permissions: ['Read customer records', 'Create and send invoices', 'Read payment statuses'],\n  },\n  {\n    id: 'billfold',\n    name: 'Billfold',\n    description: 'Subscription management and dunning recovery built in.',\n    detail:\n      'Billfold manages plans, proration and trial conversions with hosted checkout and self-serve portals. Smart retries and expiry reminders recover failed payments before customers ever notice.',\n    category: 'billing',\n    author: 'Billfold Labs',\n    icon: CreditCard,\n    permissions: ['Read subscription states', 'Start checkout sessions', 'Send payment reminders'],\n  },\n]\n\nfunction categoryLabel(id: CategoryId): string {\n  return CATEGORIES.find((c) => c.id === id)?.label ?? id\n}\n\nfunction initials(author: string): string {\n  return author\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((w) => w[0]?.toUpperCase() ?? '')\n    .join('')\n}\n\nexport function IntegrationsDirectory({\n  integrations,\n  initialQuery = '',\n  initialCategory = 'all',\n  initialInstalledOnly = false,\n  featuredId,\n  className,\n}: IntegrationsDirectoryProps) {\n  const [items, setItems] = React.useState<Integration[]>(integrations ?? STUB_INTEGRATIONS)\n  const [query, setQuery] = React.useState(initialQuery)\n  const [category, setCategory] = React.useState<CategoryFilter>(initialCategory)\n  const [installedOnly, setInstalledOnly] = React.useState(initialInstalledOnly)\n  const [detailId, setDetailId] = React.useState<string | null>(null)\n  const [detailOpen, setDetailOpen] = React.useState(false)\n\n  const counts = React.useMemo(() => {\n    const acc: Record<string, number> = {}\n    for (const c of CATEGORIES) acc[c.id] = 0\n    for (const i of items) acc[i.category] = (acc[i.category] ?? 0) + 1\n    return acc\n  }, [items])\n\n  const visible = React.useMemo(() => {\n    const q = query.trim().toLowerCase()\n    return items.filter((i) => {\n      if (category !== 'all' && i.category !== category) return false\n      if (installedOnly && !i.connected) return false\n      if (!q) return true\n      const hay = `${i.name} ${i.description} ${i.author} ${categoryLabel(i.category)}`.toLowerCase()\n      return hay.includes(q)\n    })\n  }, [items, query, category, installedOnly])\n\n  const featured = featuredId ? (items.find((i) => i.id === featuredId) ?? null) : null\n  const activeDetail = detailId ? (items.find((i) => i.id === detailId) ?? null) : null\n\n  function openDetail(id: string) {\n    setDetailId(id)\n    setDetailOpen(true)\n  }\n\n  function setConnected(id: string, connected: boolean) {\n    setItems((prev) => prev.map((i) => (i.id === id ? { ...i, connected } : i)))\n  }\n\n  function install(id: string) {\n    setConnected(id, true)\n  }\n\n  function disconnect(id: string) {\n    setConnected(id, false)\n  }\n\n  function clearFilters() {\n    setQuery('')\n    setCategory('all')\n    setInstalledOnly(false)\n  }\n\n  return (\n    <div data-slot=\"integrations-directory\" className={cn('space-y-4', className)}>\n      {/* Toolbar */}\n      <div className=\"space-y-3\">\n        <div className=\"flex flex-wrap items-center gap-3\">\n          <Input\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            prefixIcon={<Search className=\"size-4\" />}\n            placeholder=\"Search integrations…\"\n            aria-label=\"Search integrations\"\n            className=\"min-w-56 flex-1\"\n          />\n          <div className=\"ml-auto flex items-center gap-2\">\n            <Switch\n              checked={installedOnly}\n              onCheckedChange={setInstalledOnly}\n              size=\"sm\"\n              aria-label=\"Show installed integrations only\"\n            />\n            <span className=\"text-muted-foreground text-sm select-none\">Installed</span>\n          </div>\n        </div>\n        <div className=\"flex flex-wrap items-center gap-1.5\">\n          {CATEGORIES.map((c) => (\n            <button\n              key={c.id}\n              type=\"button\"\n              className=\"focus-visible:ring-ring rounded-full focus-visible:ring-2 focus-visible:outline-none\"\n              aria-pressed={category === c.id}\n              onClick={() => setCategory(c.id)}\n            >\n              <Chip variant={category === c.id ? 'filled' : 'outline'} size=\"sm\">\n                {c.label}\n                <span className=\"tabular-nums opacity-70\">{counts[c.id] ?? 0}</span>\n              </Chip>\n            </button>\n          ))}\n        </div>\n      </div>\n\n      {/* Featured detail panel (always-visible variant of the dialog body) */}\n      {featured && (\n        <section\n          data-slot=\"integration-detail-panel\"\n          className=\"bg-card space-y-4 rounded-xl border p-6 shadow-sm\"\n          aria-label=\"Integration details\"\n        >\n          <div className=\"flex items-start gap-4\">\n            <span\n              className=\"bg-primary/10 text-primary flex size-14 shrink-0 items-center justify-center rounded-xl\"\n              aria-hidden=\"true\"\n            >\n              <featured.icon className=\"size-7\" />\n            </span>\n            <div className=\"min-w-0\">\n              <h2 className=\"text-base font-semibold tracking-tight\">{featured.name}</h2>\n              <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n                <Chip variant=\"outline\" size=\"sm\">\n                  {categoryLabel(featured.category)}\n                </Chip>\n                <span className=\"text-muted-foreground text-xs\">by {featured.author}</span>\n              </div>\n            </div>\n          </div>\n          <p className=\"text-muted-foreground text-sm leading-relaxed\">{featured.detail}</p>\n          <div>\n            <p className=\"text-muted-foreground text-xs font-semibold tracking-widest uppercase\">Permissions</p>\n            <ul className=\"mt-2 space-y-1.5\">\n              {featured.permissions.map((p) => (\n                <li key={p} className=\"flex items-start gap-2 text-sm\">\n                  <Check className=\"text-success mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n                  <span>{p}</span>\n                </li>\n              ))}\n            </ul>\n          </div>\n          <div className=\"border-border flex items-center gap-2 border-t pt-4\">\n            {featured.connected ? (\n              <>\n                <Badge variant=\"success\">\n                  <Check aria-hidden=\"true\" /> Connected\n                </Badge>\n                <Button variant=\"ghost\" size=\"sm\" className=\"ml-auto\" onClick={() => disconnect(featured.id)}>\n                  Disconnect\n                </Button>\n              </>\n            ) : (\n              <Button className=\"w-full\" onClick={() => install(featured.id)}>\n                Install\n              </Button>\n            )}\n          </div>\n        </section>\n      )}\n\n      {/* Results */}\n      {visible.length > 0 ? (\n        <>\n          <p className=\"text-muted-foreground text-xs tabular-nums\">\n            Showing {visible.length} of {items.length} integrations\n          </p>\n          <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n            {visible.map((i) => (\n              <article\n                key={i.id}\n                data-slot=\"integration-card\"\n                className=\"bg-card hover:border-ring/50 relative flex flex-col gap-3 rounded-xl border p-5 shadow-sm transition-colors hover:shadow-md\"\n              >\n                <button\n                  type=\"button\"\n                  className=\"focus-visible:ring-ring/50 absolute inset-0 rounded-xl focus-visible:ring-2 focus-visible:outline-none\"\n                  aria-label={`View ${i.name} details`}\n                  onClick={() => openDetail(i.id)}\n                />\n                <div className=\"flex items-start justify-between gap-2\">\n                  <span\n                    className=\"bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-lg\"\n                    aria-hidden=\"true\"\n                  >\n                    <i.icon className=\"size-5\" />\n                  </span>\n                  <Chip variant=\"outline\" size=\"sm\">\n                    {categoryLabel(i.category)}\n                  </Chip>\n                </div>\n                <div>\n                  <p className=\"text-sm font-semibold tracking-tight\">{i.name}</p>\n                  <p className=\"text-muted-foreground mt-0.5 truncate text-xs\">{i.description}</p>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <span\n                    className=\"bg-muted text-muted-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium\"\n                    aria-hidden=\"true\"\n                  >\n                    {initials(i.author)}\n                  </span>\n                  <span className=\"text-muted-foreground truncate text-xs\">{i.author}</span>\n                </div>\n                <div className=\"border-border relative mt-auto flex items-center gap-2 border-t pt-3\">\n                  {i.connected ? (\n                    <>\n                      <Badge variant=\"success\">\n                        <Check aria-hidden=\"true\" /> Connected\n                      </Badge>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"xs\"\n                        className=\"ml-auto\"\n                        onClick={(e) => {\n                          e.stopPropagation()\n                          disconnect(i.id)\n                        }}\n                      >\n                        Disconnect\n                      </Button>\n                    </>\n                  ) : (\n                    <Button\n                      size=\"sm\"\n                      className=\"w-full\"\n                      onClick={(e) => {\n                        e.stopPropagation()\n                        install(i.id)\n                      }}\n                    >\n                      Install\n                    </Button>\n                  )}\n                </div>\n              </article>\n            ))}\n          </div>\n        </>\n      ) : (\n        /* Empty state */\n        <div className=\"rounded-xl border border-dashed\">\n          <EmptyState\n            icon={SearchX}\n            title=\"No integrations found\"\n            description=\"Nothing matches your current search and filters.\"\n          >\n            <Button variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={clearFilters}>\n              Clear filters\n            </Button>\n          </EmptyState>\n        </div>\n      )}\n\n      {/* Detail dialog */}\n      <Dialog open={detailOpen} onOpenChange={setDetailOpen}>\n        {activeDetail && (\n          <DialogContent className=\"sm:max-w-md\">\n            <DialogHeader>\n              <div className=\"flex items-start gap-4\">\n                <span\n                  className=\"bg-primary/10 text-primary flex size-14 shrink-0 items-center justify-center rounded-xl\"\n                  aria-hidden=\"true\"\n                >\n                  <activeDetail.icon className=\"size-7\" />\n                </span>\n                <div className=\"min-w-0\">\n                  <DialogTitle className=\"text-base tracking-tight\">{activeDetail.name}</DialogTitle>\n                  <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n                    <Chip variant=\"outline\" size=\"sm\">\n                      {categoryLabel(activeDetail.category)}\n                    </Chip>\n                    <span className=\"text-muted-foreground text-xs\">by {activeDetail.author}</span>\n                  </div>\n                </div>\n              </div>\n            </DialogHeader>\n            <DialogDescription className=\"leading-relaxed\">{activeDetail.detail}</DialogDescription>\n            <div>\n              <p className=\"text-muted-foreground text-xs font-semibold tracking-widest uppercase\">Permissions</p>\n              <ul className=\"mt-2 space-y-1.5\">\n                {activeDetail.permissions.map((p) => (\n                  <li key={p} className=\"flex items-start gap-2 text-sm\">\n                    <Check className=\"text-success mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n                    <span>{p}</span>\n                  </li>\n                ))}\n              </ul>\n            </div>\n            <div className=\"border-border flex items-center gap-2 border-t pt-4\">\n              {activeDetail.connected ? (\n                <>\n                  <Badge variant=\"success\">\n                    <Check aria-hidden=\"true\" /> Connected\n                  </Badge>\n                  <Button variant=\"ghost\" size=\"sm\" className=\"ml-auto\" onClick={() => disconnect(activeDetail.id)}>\n                    Disconnect\n                  </Button>\n                </>\n              ) : (\n                <Button className=\"w-full\" onClick={() => install(activeDetail.id)}>\n                  Install\n                </Button>\n              )}\n            </div>\n          </DialogContent>\n        )}\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/integrations-directory/IntegrationsDirectory.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/chip.json",
    "https://uipkge.dev/r/react/dialog.json",
    "https://uipkge.dev/r/react/empty-state.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/switch.json"
  ],
  "description": "App marketplace surface: functional search, category chips with counts and an installed-only switch above a responsive grid of integration cards (logo tile, one-line blurb, author row, install/connected footer). Clicking a card opens a detail dialog with permissions; pass `featuredId` to render that dialog body as an always-visible panel. Stateful demo — swap the stub catalog for your source.",
  "categories": [
    "devops",
    "marketing",
    "dashboard"
  ]
}