{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "integrations-3",
  "title": "Searchable App Directory",
  "description": "Searchable integrations directory with a search input, category filter pills, and result cards that open a per-app connect dialog.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "dialog",
    "input",
    "label",
    "sonner",
    "switch",
    "toggle-group"
  ],
  "files": [
    {
      "path": "registry/blocks/integrations/3/integrations-block.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { toast } from \"sonner\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Toaster } from \"@/components/ui/sonner\"\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\n/** Props a call site may pass through to an icon. */\ntype IconProps = { className?: string; size?: number | string }\n\ntype Filter = \"All\" | \"Communication\" | \"Analytics\" | \"Payments\" | \"Dev Tools\"\n\nconst filters: Filter[] = [\n  \"All\",\n  \"Communication\",\n  \"Analytics\",\n  \"Payments\",\n  \"Dev Tools\",\n]\n\ntype Integration = {\n  name: string\n  description: string\n  category: Exclude<Filter, \"All\">\n  Icon: typeof SlackMark\n}\n\nconst integrations: Integration[] = [\n  {\n    name: \"Slack\",\n    description: \"Send alerts and approvals straight to your channels.\",\n    category: \"Communication\",\n    Icon: (p: IconProps) => <SlackMark {...p} />,\n  },\n  {\n    name: \"Intercom\",\n    description: \"Sync conversations and route tickets automatically.\",\n    category: \"Communication\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"Layers\"\n        tabler=\"IconStack\"\n        hugeicons=\"Layers01Icon\"\n        phosphor=\"Stack\"\n        remixicon=\"RiStackLine\"\n        {...p}\n      />\n    ),\n  },\n  {\n    name: \"Gmail\",\n    description: \"Trigger workflows from inbound and outbound mail.\",\n    category: \"Communication\",\n    Icon: (p: IconProps) => <GoogleMark {...p} />,\n  },\n  {\n    name: \"Mixpanel\",\n    description: \"Stream product events into actionable funnels.\",\n    category: \"Analytics\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"BarChart2\"\n        tabler=\"IconChartBar\"\n        hugeicons=\"BarChartIcon\"\n        phosphor=\"ChartBar\"\n        remixicon=\"RiBarChart2Line\"\n        {...p}\n      />\n    ),\n  },\n  {\n    name: \"Looker\",\n    description: \"Embed dashboards and share live reports with teams.\",\n    category: \"Analytics\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"Layers\"\n        tabler=\"IconStack\"\n        hugeicons=\"Layers01Icon\"\n        phosphor=\"Stack\"\n        remixicon=\"RiStackLine\"\n        {...p}\n      />\n    ),\n  },\n  {\n    name: \"Stripe\",\n    description: \"Reconcile payments, refunds and subscriptions.\",\n    category: \"Payments\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"CreditCard\"\n        tabler=\"IconCreditCard\"\n        hugeicons=\"CreditCardIcon\"\n        phosphor=\"CreditCard\"\n        remixicon=\"RiBankCardFill\"\n        {...p}\n      />\n    ),\n  },\n  {\n    name: \"Paddle\",\n    description: \"Handle global tax and merchant-of-record billing.\",\n    category: \"Payments\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"CreditCard\"\n        tabler=\"IconCreditCard\"\n        hugeicons=\"CreditCardIcon\"\n        phosphor=\"CreditCard\"\n        remixicon=\"RiBankCardLine\"\n        {...p}\n      />\n    ),\n  },\n  {\n    name: \"GitHub\",\n    description: \"Link commits and deploys to your release pipeline.\",\n    category: \"Dev Tools\",\n    Icon: (p: IconProps) => <GithubMark {...p} />,\n  },\n  {\n    name: \"Sentry\",\n    description: \"Capture errors and open issues without leaving Acme.\",\n    category: \"Dev Tools\",\n    Icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"SquareTerminal\"\n        tabler=\"IconTerminal2\"\n        hugeicons=\"ComputerTerminal01Icon\"\n        phosphor=\"TerminalWindow\"\n        remixicon=\"RiTerminalBoxLine\"\n        {...p}\n      />\n    ),\n  },\n]\n\nconst categoryIcon: Record<Exclude<Filter, \"All\">, typeof SlackMark> = {\n  Communication: (p: IconProps) => (\n    <IconPlaceholder\n      lucide=\"Layers\"\n      tabler=\"IconStack\"\n      hugeicons=\"Layers01Icon\"\n      phosphor=\"Stack\"\n      remixicon=\"RiStackLine\"\n      {...p}\n    />\n  ),\n  Analytics: (p: IconProps) => (\n    <IconPlaceholder\n      lucide=\"BarChart2\"\n      tabler=\"IconChartBar\"\n      hugeicons=\"BarChartIcon\"\n      phosphor=\"ChartBar\"\n      remixicon=\"RiBarChart2Line\"\n      {...p}\n    />\n  ),\n  Payments: (p: IconProps) => (\n    <IconPlaceholder\n      lucide=\"CircleDollarSign\"\n      tabler=\"IconCoin\"\n      hugeicons=\"DollarCircleIcon\"\n      phosphor=\"CurrencyCircleDollar\"\n      remixicon=\"RiMoneyDollarCircleLine\"\n      {...p}\n    />\n  ),\n  \"Dev Tools\": (p: IconProps) => (\n    <IconPlaceholder\n      lucide=\"SquareTerminal\"\n      tabler=\"IconTerminal2\"\n      hugeicons=\"ComputerTerminal01Icon\"\n      phosphor=\"TerminalWindow\"\n      remixicon=\"RiTerminalBoxLine\"\n      {...p}\n    />\n  ),\n}\n\nexport default function IntegrationsBlock() {\n  const [query, setQuery] = useState(\"\")\n  const [active, setActive] = useState<Filter>(\"All\")\n  const [selected, setSelected] = useState<Integration | null>(null)\n  const [notify, setNotify] = useState(true)\n\n  const normalized = query.trim().toLowerCase()\n  const results = integrations.filter((item) => {\n    const matchesFilter = active === \"All\" || item.category === active\n    const matchesQuery =\n      normalized === \"\" ||\n      item.name.toLowerCase().includes(normalized) ||\n      item.description.toLowerCase().includes(normalized) ||\n      item.category.toLowerCase().includes(normalized)\n    return matchesFilter && matchesQuery\n  })\n\n  function openConnect(item: Integration) {\n    setNotify(true)\n    setSelected(item)\n  }\n\n  function handleConnect() {\n    if (selected) {\n      toast.success(`${selected.name} connected`, {\n        description: notify\n          ? `Notifications enabled for your ${selected.category} workflows.`\n          : `Added to your ${selected.category} integrations.`,\n      })\n    }\n    setSelected(null)\n  }\n\n  const SelectedIcon = selected?.Icon\n\n  return (\n    <section className=\"flex min-h-svh w-full justify-center bg-muted/30 px-6 py-16 text-foreground\">\n      <Toaster />\n      <div className=\"mx-auto w-full max-w-5xl\">\n        <div className=\"flex flex-col gap-4\">\n          <Badge variant=\"outline\" className=\"w-fit\">\n            <IconPlaceholder\n              lucide=\"Layers\"\n              tabler=\"IconStack\"\n              hugeicons=\"Layers01Icon\"\n              phosphor=\"Stack\"\n              remixicon=\"RiStackLine\"\n              data-icon=\"inline-start\"\n            />\n            Integration directory\n          </Badge>\n          <div className=\"flex flex-col gap-2\">\n            <h2 className=\"font-heading text-2xl font-semibold tracking-tight sm:text-3xl\">\n              Connect Acme to your stack\n            </h2>\n            <p className=\"max-w-xl text-sm text-muted-foreground\">\n              Browse our directory of native integrations. Search by name or\n              filter by category to find the right connection.\n            </p>\n          </div>\n        </div>\n\n        <div className=\"mt-8 flex flex-col gap-4\">\n          <div className=\"relative w-full max-w-sm\">\n            <IconPlaceholder\n              lucide=\"Search\"\n              tabler=\"IconSearch\"\n              hugeicons=\"SearchIcon\"\n              phosphor=\"MagnifyingGlass\"\n              remixicon=\"RiSearchLine\"\n              className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\"\n              aria-hidden=\"true\"\n            />\n            <Input\n              type=\"search\"\n              value={query}\n              onChange={(event) => setQuery(event.target.value)}\n              placeholder=\"Search integrations...\"\n              aria-label=\"Search integrations\"\n              className=\"h-9 pl-8 text-sm\"\n            />\n          </div>\n\n          <ToggleGroup\n            variant=\"outline\"\n            size=\"sm\"\n            value={[active]}\n            onValueChange={(value) => {\n              const next = value[0] as Filter | undefined\n              if (next) setActive(next)\n            }}\n            aria-label=\"Filter integrations by category\"\n            className=\"flex-wrap\"\n          >\n            {filters.map((filter) => (\n              <ToggleGroupItem\n                key={filter}\n                value={filter}\n                aria-label={`Show ${filter} integrations`}\n                className=\"text-xs font-medium tracking-wide\"\n              >\n                {filter}\n              </ToggleGroupItem>\n            ))}\n          </ToggleGroup>\n        </div>\n\n        {results.length > 0 ? (\n          <div className=\"mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3\">\n            {results.map((item) => {\n              const CategoryIcon = categoryIcon[item.category]\n              return (\n                <Card\n                  key={item.name}\n                  className=\"group/tile transition-shadow hover:ring-foreground/25\"\n                >\n                  <CardContent className=\"flex h-full flex-col gap-4\">\n                    <div className=\"flex items-start justify-between gap-3\">\n                      <div className=\"flex size-10 items-center justify-center rounded-md bg-muted text-foreground\">\n                        <item.Icon className=\"size-5\" aria-hidden=\"true\" />\n                      </div>\n                      <Badge variant=\"secondary\" className=\"gap-1\">\n                        <CategoryIcon data-icon=\"inline-start\" />\n                        {item.category}\n                      </Badge>\n                    </div>\n                    <div className=\"flex flex-col gap-1\">\n                      <h3 className=\"font-heading text-sm font-medium\">\n                        {item.name}\n                      </h3>\n                      <p className=\"text-xs/relaxed text-muted-foreground\">\n                        {item.description}\n                      </p>\n                    </div>\n                    <div className=\"mt-auto pt-2\">\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() => openConnect(item)}\n                        className=\"-ml-2 text-primary\"\n                      >\n                        Connect\n                        <IconPlaceholder\n                          lucide=\"ArrowRight\"\n                          tabler=\"IconArrowRight\"\n                          hugeicons=\"ArrowRightIcon\"\n                          phosphor=\"ArrowRight\"\n                          remixicon=\"RiArrowRightLine\"\n                          className=\"transition-transform group-hover/tile:translate-x-0.5\"\n                        />\n                      </Button>\n                    </div>\n                  </CardContent>\n                </Card>\n              )\n            })}\n          </div>\n        ) : (\n          <div className=\"mt-8 flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-background px-6 py-16 text-center\">\n            <div className=\"flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n              <IconPlaceholder\n                lucide=\"Search\"\n                tabler=\"IconSearch\"\n                hugeicons=\"SearchIcon\"\n                phosphor=\"MagnifyingGlass\"\n                remixicon=\"RiSearchLine\"\n                className=\"size-5\"\n                aria-hidden=\"true\"\n              />\n            </div>\n            <p className=\"text-sm font-medium\">No integrations found</p>\n            <p className=\"max-w-xs text-xs text-muted-foreground\">\n              Try a different search term or clear the active filter.\n            </p>\n          </div>\n        )}\n      </div>\n\n      <Dialog\n        open={selected !== null}\n        onOpenChange={(open) => {\n          if (!open) setSelected(null)\n        }}\n      >\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <span className=\"flex size-10 items-center justify-center rounded-lg border border-border bg-background\">\n              {SelectedIcon && (\n                <SelectedIcon className=\"size-5\" aria-hidden=\"true\" />\n              )}\n            </span>\n            <DialogTitle className=\"mt-3\">Connect {selected?.name}</DialogTitle>\n            <DialogDescription>\n              Authorize Acme to access your {selected?.name} account. This adds\n              it to your{\" \"}\n              <span className=\"text-foreground\">{selected?.category}</span>{\" \"}\n              integrations. You can disconnect any time.\n            </DialogDescription>\n          </DialogHeader>\n\n          <div className=\"flex flex-col gap-3\">\n            <div className=\"flex flex-col gap-1.5\">\n              <Label htmlFor=\"workspace\" className=\"text-xs font-medium\">\n                Workspace\n              </Label>\n              <Input\n                id=\"workspace\"\n                defaultValue=\"acme-team\"\n                className=\"h-9 text-sm\"\n              />\n            </div>\n            <div className=\"flex items-start justify-between gap-4 rounded-lg border border-border bg-muted/40 p-3\">\n              <div className=\"flex flex-col gap-0.5\">\n                <Label htmlFor=\"notify\" className=\"text-xs font-medium\">\n                  Enable notifications\n                </Label>\n                <span className=\"text-xs text-muted-foreground\">\n                  Send workflow updates to this integration.\n                </span>\n              </div>\n              <Switch\n                id=\"notify\"\n                checked={notify}\n                onCheckedChange={setNotify}\n                aria-label=\"Enable notifications\"\n              />\n            </div>\n          </div>\n\n          <DialogFooter>\n            <DialogClose render={<Button variant=\"outline\" />}>\n              Cancel\n            </DialogClose>\n            <Button onClick={handleConnect}>Connect {selected?.name}</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </section>\n  )\n}\n\n// Brand marks are inlined rather than imported from an icon library: the rest\n// of this file uses IconPlaceholder, which resolves to whichever icon set the\n// consumer already has, and one brand import would drag a whole extra package\n// into their install for a handful of glyphs.\ntype MarkProps = React.ComponentProps<\"svg\"> & { size?: number | string }\n\nfunction GithubMark({ size = 24, ...props }: MarkProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <path d=\"M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z\" />\n    </svg>\n  )\n}\n\nfunction GoogleMark({ size = 24, ...props }: MarkProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <path d=\"M3.06364 7.50914C4.70909 4.24092 8.09084 2 12 2C14.6954 2 16.959 2.99095 18.6909 4.60455L15.8227 7.47274C14.7864 6.48185 13.4681 5.97727 12 5.97727C9.39542 5.97727 7.19084 7.73637 6.40455 10.1C6.2045 10.7 6.09086 11.3409 6.09086 12C6.09086 12.6591 6.2045 13.3 6.40455 13.9C7.19084 16.2636 9.39542 18.0227 12 18.0227C13.3454 18.0227 14.4909 17.6682 15.3864 17.0682C16.4454 16.3591 17.15 15.3 17.3818 14.05H12V10.1818H21.4181C21.5364 10.8363 21.6 11.5182 21.6 12.2273C21.6 15.2727 20.5091 17.8363 18.6181 19.5773C16.9636 21.1046 14.7 22 12 22C8.09084 22 4.70909 19.7591 3.06364 16.4909C2.38638 15.1409 2 13.6136 2 12C2 10.3864 2.38638 8.85911 3.06364 7.50914Z\" />\n    </svg>\n  )\n}\n\nfunction SlackMark({ size = 24, ...props }: MarkProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <path d=\"M6.52739 14.5136C6.52739 15.5966 5.64264 16.4814 4.55959 16.4814C3.47654 16.4814 2.5918 15.5966 2.5918 14.5136C2.5918 13.4305 3.47654 12.5458 4.55959 12.5458H6.52739V14.5136ZM7.51892 14.5136C7.51892 13.4305 8.40366 12.5458 9.48671 12.5458C10.5698 12.5458 11.4545 13.4305 11.4545 14.5136V19.4407C11.4545 20.5238 10.5698 21.4085 9.48671 21.4085C8.40366 21.4085 7.51892 20.5238 7.51892 19.4407V14.5136ZM9.48671 6.52715C8.40366 6.52715 7.51892 5.6424 7.51892 4.55935C7.51892 3.4763 8.40366 2.59155 9.48671 2.59155C10.5698 2.59155 11.4545 3.4763 11.4545 4.55935V6.52715H9.48671ZM9.48671 7.51867C10.5698 7.51867 11.4545 8.40342 11.4545 9.48647C11.4545 10.5695 10.5698 11.4543 9.48671 11.4543H4.55959C3.47654 11.4543 2.5918 10.5695 2.5918 9.48647C2.5918 8.40342 3.47654 7.51867 4.55959 7.51867H9.48671ZM17.4732 9.48647C17.4732 8.40342 18.3579 7.51867 19.4409 7.51867C20.524 7.51867 21.4087 8.40342 21.4087 9.48647C21.4087 10.5695 20.524 11.4543 19.4409 11.4543H17.4732V9.48647ZM16.4816 9.48647C16.4816 10.5695 15.5969 11.4543 14.5138 11.4543C13.4308 11.4543 12.546 10.5695 12.546 9.48647V4.55935C12.546 3.4763 13.4308 2.59155 14.5138 2.59155C15.5969 2.59155 16.4816 3.4763 16.4816 4.55935V9.48647ZM14.5138 17.4729C15.5969 17.4729 16.4816 18.3577 16.4816 19.4407C16.4816 20.5238 15.5969 21.4085 14.5138 21.4085C13.4308 21.4085 12.546 20.5238 12.546 19.4407V17.4729H14.5138ZM14.5138 16.4814C13.4308 16.4814 12.546 15.5966 12.546 14.5136C12.546 13.4305 13.4308 12.5458 14.5138 12.5458H19.4409C20.524 12.5458 21.4087 13.4305 21.4087 14.5136C21.4087 15.5966 20.524 16.4814 19.4409 16.4814H14.5138Z\" />\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/integrations-3.tsx"
    }
  ],
  "meta": {
    "height": "983px",
    "tier": "free"
  },
  "docs": "Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this block uses.",
  "categories": [
    "integrations"
  ],
  "type": "registry:block"
}