{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-launcher",
  "title": "App Launcher",
  "description": "An icon-triggered Nexus app launcher: a spacious searchable dialog with branded app cards, current-app context and clear unavailable states. An alternative to the compact app-switcher popover.",
  "dependencies": [
    "@untitledui/icons@^0.0.23"
  ],
  "registryDependencies": [
    "@nx-ui/app-switcher",
    "@nx-ui/button",
    "@nx-ui/dialog",
    "@nx-ui/input",
    "@nx-ui/nx-app-icon",
    "@nx-ui/tooltip",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/app-launcher.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowUpRight,\n  Check,\n  Grid01,\n  Lock01,\n  SearchLg,\n} from \"@untitledui/icons\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { NxAppIcon } from \"@/components/blocks/nx-app-icon\"\nimport type { AppSwitcherApp } from \"@/components/blocks/app-switcher\"\n\n/** An icon-only alternative to AppSwitcher. Accepts the same resolved directory;\n * available destinations remain ordinary links and access checks belong to apps. */\nfunction AppLauncher({\n  apps,\n  onSelect,\n  className,\n}: {\n  apps: AppSwitcherApp[]\n  /** Called on link selection without preventing ordinary browser navigation. */\n  onSelect?: (app: AppSwitcherApp) => void\n  className?: string\n}) {\n  const [open, setOpen] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const searchRef = React.useRef<HTMLInputElement>(null)\n  const resultsRef = React.useRef<HTMLUListElement>(null)\n  const activeApp = apps.find((app) => app.active)\n  const visible = apps.filter((app) =>\n    [app.name, app.description, ...(app.keywords ?? [])]\n      .join(\" \")\n      .toLowerCase()\n      .includes(query.trim().toLowerCase())\n  )\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (next) setQuery(\"\")\n      }}\n    >\n      <TooltipProvider delay={300}>\n        <Tooltip>\n          <TooltipTrigger\n            render={\n              <DialogTrigger\n                render={\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label=\"Switch app\"\n                    className={cn(\n                      \"size-9 text-muted-foreground hover:text-foreground\",\n                      className\n                    )}\n                  />\n                }\n              />\n            }\n          >\n            <Grid01 className=\"size-5\" aria-hidden=\"true\" />\n          </TooltipTrigger>\n          <TooltipContent side=\"bottom\">Switch app</TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n      <DialogContent\n        initialFocus={searchRef}\n        className=\"max-h-[calc(100dvh-2rem)] flex flex-col gap-0 overflow-hidden p-0 sm:max-w-[800px] motion-reduce:animate-none\"\n      >\n        <div className=\"shrink-0 border-b border-border px-6 pt-6 pb-4 sm:px-8 sm:pt-8 sm:pb-5\">\n          <div className=\"flex items-start gap-4 pr-8\">\n            <div className=\"flex-1\">\n              <DialogTitle className=\"text-2xl tracking-tight\">\n                Nexus apps\n              </DialogTitle>\n              <DialogDescription className=\"mt-2\">\n                One connected workspace. Choose where to go next.\n              </DialogDescription>\n            </div>\n          </div>\n          {activeApp && (\n            <div className=\"mt-5 inline-flex items-center gap-2.5 rounded-full border border-border bg-muted/50 py-1.5 pr-3 pl-1.5 text-xs\">\n              <NxAppIcon\n                tone={activeApp.tone}\n                mark={activeApp.mark}\n                size={24}\n              />\n              <span className=\"text-muted-foreground\">\n                You’re in{\" \"}\n                <strong className=\"font-semibold text-foreground\">\n                  {activeApp.name}\n                </strong>\n              </span>\n            </div>\n          )}\n          <div className=\"relative mt-5\">\n            <SearchLg\n              className=\"pointer-events-none absolute top-3 left-3.5 size-5 text-muted-foreground\"\n              aria-hidden=\"true\"\n            />\n            <Input\n              ref={searchRef}\n              type=\"search\"\n              aria-label=\"Search apps\"\n              placeholder=\"Find an app…\"\n              value={query}\n              onChange={(event) => setQuery(event.target.value)}\n              onKeyDown={(event) => {\n                if (event.key === \"ArrowDown\") {\n                  event.preventDefault()\n                  resultsRef.current\n                    ?.querySelector<HTMLAnchorElement>(\"a\")\n                    ?.focus()\n                }\n              }}\n              className=\"h-11 bg-background pl-11 shadow-none\"\n            />\n          </div>\n        </div>\n        <div className=\"min-h-0 overflow-y-auto px-6 py-6 sm:px-8\">\n          <p\n            className=\"mb-4 text-xs font-medium text-muted-foreground\"\n            role=\"status\"\n          >\n            {query.trim()\n              ? `${visible.length} ${visible.length === 1 ? \"app\" : \"apps\"} found`\n              : \"All apps\"}\n          </p>\n          <ul\n            ref={resultsRef}\n            className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\"\n          >\n            {visible.map((app) => {\n              const state = app.active\n                ? \"Current app\"\n                : app.comingSoon\n                  ? \"Coming soon\"\n                  : !app.href\n                    ? \"Unavailable\"\n                    : app.enabled === false\n                      ? \"No access\"\n                      : app.statusBadge\n              const available =\n                !app.active &&\n                !app.comingSoon &&\n                app.enabled !== false &&\n                Boolean(app.href)\n              const content = (\n                <>\n                  <div className=\"flex w-full items-start justify-between\">\n                    <NxAppIcon\n                      tone={app.tone}\n                      mark={app.mark}\n                      size={48}\n                      className={cn(\n                        \"shrink-0\",\n                        !available && !app.active && \"saturate-50\"\n                      )}\n                    />\n                    {app.active ? (\n                      <Check\n                        className=\"size-4 text-muted-foreground\"\n                        aria-hidden=\"true\"\n                      />\n                    ) : available ? (\n                      <ArrowUpRight\n                        className=\"size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5 motion-reduce:transform-none motion-reduce:transition-none\"\n                        aria-hidden=\"true\"\n                      />\n                    ) : app.enabled === false && !app.comingSoon && app.href ? (\n                      <Lock01\n                        className=\"size-4 text-muted-foreground\"\n                        aria-hidden=\"true\"\n                      />\n                    ) : null}\n                  </div>\n                  <div className=\"mt-4\">\n                    <p className=\"font-semibold text-foreground\">{app.name}</p>\n                    {app.description && (\n                      <p className=\"mt-1 text-xs leading-5 text-muted-foreground\">\n                        {app.description}\n                      </p>\n                    )}\n                  </div>\n                  {state && (\n                    <span className=\"mt-3 w-fit rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground\">\n                      {state}\n                    </span>\n                  )}\n                </>\n              )\n              const tileClass = cn(\n                \"group flex h-full min-h-40 flex-col rounded-xl border p-4 text-left outline-none\",\n                app.active ? \"border-border bg-muted/60\" : \"border-border/70\",\n                available &&\n                  \"transition-[background-color,border-color,box-shadow] duration-150 hover:border-border hover:bg-muted/40 hover:shadow-sm focus-visible:ring-4 focus-visible:ring-ring/25 motion-reduce:transition-none\"\n              )\n              return (\n                <li key={app.id}>\n                  {available ? (\n                    <a\n                      href={app.href}\n                      className={tileClass}\n                      onClick={() => {\n                        onSelect?.(app)\n                        setOpen(false)\n                      }}\n                    >\n                      {content}\n                    </a>\n                  ) : (\n                    <div\n                      className={tileClass}\n                      aria-current={app.active ? \"true\" : undefined}\n                      aria-disabled={!app.active ? true : undefined}\n                    >\n                      {content}\n                    </div>\n                  )}\n                </li>\n              )\n            })}\n          </ul>\n          {!visible.length && (\n            <div className=\"py-10 text-center\">\n              <SearchLg\n                className=\"mx-auto mb-4 size-7 text-muted-foreground\"\n                aria-hidden=\"true\"\n              />\n              <p className=\"font-semibold\">\n                {apps.length ? \"No matching apps\" : \"No apps available\"}\n              </p>\n              <p className=\"mt-2 text-sm text-muted-foreground\">\n                {apps.length\n                  ? \"Try an app name or something you want to do.\"\n                  : \"Your app directory is empty.\"}\n              </p>\n              {query && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"mt-5\"\n                  onClick={() => {\n                    setQuery(\"\")\n                    searchRef.current?.focus()\n                  }}\n                >\n                  Clear search\n                </Button>\n              )}\n            </div>\n          )}\n        </div>\n        <div className=\"flex shrink-0 items-center justify-between border-t border-border bg-muted/30 px-6 py-3.5 text-xs text-muted-foreground sm:px-8\">\n          <span>Nexus ReGen</span>\n          <span>\n            Press{\" \"}\n            <kbd className=\"rounded border border-border px-1 py-0.5 text-[10px]\">\n              esc\n            </kbd>{\" \"}\n            to close\n          </span>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nexport { AppLauncher }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}