{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-switcher",
  "title": "App Switcher",
  "description": "The sidebar-header launcher for the Nexus app family: a grid popover of product tiles (NxAppIcon tones) with the active app marked and coming-soon entries disabled.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/nx-app-icon",
    "@nx-ui/popover",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/app-switcher.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronSelectorVertical } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { NxAppIcon, type NxAppIconTone } from \"@/components/blocks/nx-app-icon\"\n\n/**\n * AppSwitcher — the sidebar-header launcher for the Nexus app family\n * (Materials Exchange, Assurance, Procurement…). The trigger is a compact\n * lockup of the ACTIVE app's tile and name; opening it reveals a grid popover\n * of every app — NxAppIcon tones + names — with the active app marked and\n * \"coming soon\" entries disabled.\n *\n * CROSS-APP SCOPE CARRY-OVER: the apps live on sibling subdomains and share\n * the org ▸ project scope — following an `href` here is expected to land the\n * user in the SAME organisation (and project, where the target app knows it)\n * on the other side. The carry mechanism (a scope cookie on the shared parent\n * domain, or claims in the shared session JWT) is the platform's concern, not\n * this block's: AppSwitcher only renders links. Consumers should point `href`\n * at a stable entry URL and let the target app restore scope on arrival.\n * See `docs/scoped-shell.md`.\n */\n\nexport type AppSwitcherApp = {\n  id: string\n  name: string\n  /** One-line descriptor shown under the name in the popover grid. */\n  description?: string\n  /** Brand tone of the app's tile (the NxAppIcon family). */\n  tone: NxAppIconTone\n  /** Entry URL of the app (usually a sibling subdomain). */\n  href?: string\n  /** Marks the app the user is currently in. */\n  active?: boolean\n  /** Renders the tile disabled with a \"Soon\" badge. */\n  comingSoon?: boolean\n}\n\nfunction AppTile({ app }: { app: AppSwitcherApp }) {\n  const content = (\n    <>\n      <span className=\"relative\">\n        <NxAppIcon tone={app.tone} size={40} />\n        {app.active ? (\n          <span className=\"absolute -top-1 -right-1 grid size-4 place-items-center rounded-full bg-background ring-1 ring-border\">\n            <Check className=\"size-3 stroke-[3px] text-fg-brand-primary\" aria-hidden=\"true\" />\n          </span>\n        ) : null}\n      </span>\n      <span className=\"flex min-w-0 flex-col items-center gap-0.5\">\n        <span className=\"max-w-full truncate text-xs font-semibold text-foreground\">\n          {app.name}\n        </span>\n        {app.comingSoon ? (\n          <Badge variant=\"gray\" className=\"h-4 px-1.5 text-[0.625rem]\">\n            Soon\n          </Badge>\n        ) : app.description ? (\n          <span className=\"max-w-full truncate text-[0.625rem] text-muted-foreground\">\n            {app.description}\n          </span>\n        ) : null}\n      </span>\n    </>\n  )\n\n  const isLink = !app.comingSoon && !app.active && Boolean(app.href)\n  const tileClass = cn(\n    \"flex w-full flex-col items-center gap-2 rounded-lg px-2 py-3 text-center outline-none transition\",\n    app.comingSoon && \"cursor-not-allowed opacity-55\",\n    app.active && \"bg-muted/60\",\n    isLink &&\n      \"cursor-pointer hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\"\n  )\n\n  if (!isLink) {\n    // Coming soon, the app the user is already in, or an app with no\n    // destination yet — not a link, just the (marked/disabled) tile.\n    return (\n      <div\n        className={tileClass}\n        aria-disabled={app.comingSoon ? \"true\" : undefined}\n        aria-current={app.active ? \"true\" : undefined}\n      >\n        {content}\n      </div>\n    )\n  }\n\n  return (\n    <a href={app.href} className={tileClass} aria-label={`Open ${app.name}`}>\n      {content}\n    </a>\n  )\n}\n\nfunction AppSwitcher({\n  apps,\n  className,\n}: {\n  apps: AppSwitcherApp[]\n  className?: string\n}) {\n  const activeApp = apps.find((app) => app.active) ?? apps[0]\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        className={cn(\n          \"flex w-full cursor-pointer items-center gap-2.5 rounded-lg p-2 text-left ring-1 ring-border ring-inset outline-none transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\",\n          className\n        )}\n        aria-label=\"Switch app\"\n      >\n        {activeApp ? (\n          <NxAppIcon tone={activeApp.tone} size={28} radius={0.28} />\n        ) : null}\n        <span className=\"flex min-w-0 flex-1 flex-col\">\n          <span className=\"truncate text-sm font-semibold text-foreground\">\n            {activeApp?.name}\n          </span>\n          <span className=\"truncate text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase\">\n            Nexus ReGen\n          </span>\n        </span>\n        <ChevronSelectorVertical className=\"size-4 shrink-0 text-fg-quaternary\" />\n      </PopoverTrigger>\n\n      <PopoverContent align=\"start\" className=\"w-80 p-2\">\n        <p className=\"px-2 pt-1 pb-2 text-xs font-semibold text-muted-foreground\">\n          Nexus apps\n        </p>\n        <div className=\"grid grid-cols-3 gap-1\">\n          {apps.map((app) => (\n            <AppTile key={app.id} app={app} />\n          ))}\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport { AppSwitcher }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}