{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "connector-search",
  "type": "registry:component",
  "title": "Connector Search",
  "description": "Type to find a service, step through with the arrow keys, and open one to check how its sync is doing or connect it right there.",
  "categories": [
    "component",
    "Workspace"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/badge.json",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/kbd.json"
  ],
  "files": [
    {
      "path": "registry/devl/connector-search.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  ArrowLeftIcon,\n  CheckIcon,\n  ChevronRightIcon,\n  CornerDownLeftIcon,\n  type LucideIcon,\n  PlugIcon,\n  RefreshCwIcon,\n  SearchIcon,\n  SearchXIcon,\n  Settings2Icon,\n  UnplugIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ConnectorTone = \"orange\" | \"teal\" | \"violet\" | \"rose\" | \"info\" | \"success\" | \"warning\";\n\nexport interface ConnectorConnection {\n  account: string;\n  connectedBy: string;\n  connectedAt: string;\n  scopes: string[];\n  lastSync: string;\n  records: number;\n  history: number[];\n  health: \"healthy\" | \"degraded\";\n}\n\nexport interface Connector {\n  id: string;\n  name: string;\n  category: string;\n  description: string;\n  icon: LucideIcon;\n  tone?: ConnectorTone;\n  connection?: ConnectorConnection;\n  permissions?: string[];\n  auth?: string;\n  direction?: \"One-way\" | \"Two-way\";\n}\n\nexport interface ConnectorSearchProps {\n  connectors: Connector[];\n  defaultOpenId?: string;\n  onConnect?: (id: string) => void;\n  onDisconnect?: (id: string) => void;\n  onOpenSettings?: (id: string) => void;\n  className?: string;\n}\n\nconst press = \"transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]\";\n\nconst toneClass: Record<ConnectorTone, string> = {\n  orange: \"bg-orange-500 text-white\",\n  teal: \"bg-teal-600 text-white\",\n  violet: \"bg-violet-500 text-white\",\n  rose: \"bg-rose-500 text-white\",\n  info: \"bg-info text-white\",\n  success: \"bg-success text-white\",\n  warning: \"bg-warning text-black/80\",\n};\n\nfunction ServiceIcon({ item, size = \"sm\" }: { item: Connector; size?: \"sm\" | \"lg\" }) {\n  const Icon = item.icon;\n  return (\n    <span\n      className={cn(\n        \"flex shrink-0 items-center justify-center shadow-xs/5\",\n        toneClass[item.tone ?? \"info\"],\n        size === \"sm\" ? \"size-7 rounded-lg\" : \"size-10 rounded-xl\",\n      )}\n    >\n      <Icon className={size === \"sm\" ? \"size-3.5\" : \"size-5\"} strokeWidth={2.25} />\n    </span>\n  );\n}\n\nfunction SyncBars({ values, reduce }: { values: number[]; reduce: boolean | null }) {\n  const max = Math.max(...values, 1);\n  return (\n    <svg aria-hidden className=\"h-7 w-full\" preserveAspectRatio=\"none\" viewBox={`0 0 ${values.length * 6} 28`}>\n      {values.map((v, i) => {\n        const h = Math.max(2, (v / max) * 26);\n        return (\n          <motion.rect\n            animate={{ height: h, y: 28 - h }}\n            className={i === values.length - 1 ? \"fill-foreground\" : \"fill-foreground/20\"}\n            initial={reduce ? false : { height: 0, y: 28 }}\n            key={i}\n            rx={1}\n            transition={{ type: \"spring\", stiffness: 260, damping: 26, delay: reduce ? 0 : i * 0.02 }}\n            width={4}\n            x={i * 6 + 1}\n          />\n        );\n      })}\n    </svg>\n  );\n}\n\nfunction Tile({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) {\n  return (\n    <div className={cn(\"flex min-w-0 flex-col gap-3 rounded-xl border bg-background p-3 dark:bg-input/16\", className)}>\n      <p className=\"font-medium text-muted-foreground text-xs\">{label}</p>\n      {children}\n    </div>\n  );\n}\n\nfunction Row({ k, v, mono }: { k: string; v: React.ReactNode; mono?: boolean }) {\n  return (\n    <div className=\"flex items-baseline justify-between gap-3 text-xs\">\n      <span className=\"shrink-0 text-muted-foreground\">{k}</span>\n      <span className={cn(\"truncate text-right font-medium\", mono && \"font-mono tabular-nums\")}>{v}</span>\n    </div>\n  );\n}\n\nfunction useThemeRadius(scale: number) {\n  const probe = useRef<HTMLSpanElement>(null);\n  const [radius, setRadius] = useState(scale * 10);\n  useLayoutEffect(() => {\n    const el = probe.current;\n    if (!el) return;\n    const read = () => setRadius(el.offsetWidth);\n    read();\n    const observer = new ResizeObserver(read);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n  const marker = (\n    <span\n      aria-hidden\n      className=\"pointer-events-none invisible absolute h-0\"\n      ref={probe}\n      style={{ width: `calc(var(--radius) * ${scale})` }}\n    />\n  );\n  return [radius, marker] as const;\n}\n\nexport function ConnectorSearch({\n  connectors,\n  defaultOpenId,\n  onConnect,\n  onDisconnect,\n  onOpenSettings,\n  className,\n}: ConnectorSearchProps) {\n  const [radius, radiusProbe] = useThemeRadius(1.6);\n  const reduce = useReducedMotion();\n  const listId = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const detailRef = useRef<HTMLDivElement>(null);\n  const [items, setItems] = useState(connectors);\n  const [query, setQuery] = useState(\"\");\n  const [active, setActive] = useState(0);\n  const [openId, setOpenId] = useState<string | undefined>(defaultOpenId);\n  const [connecting, setConnecting] = useState<string | null>(null);\n  const timers = useRef(new Set<ReturnType<typeof setTimeout>>());\n  const later = (fn: () => void, ms: number) => {\n    const t = setTimeout(() => {\n      timers.current.delete(t);\n      fn();\n    }, ms);\n    timers.current.add(t);\n  };\n\n  useEffect(() => {\n    const pending = timers.current;\n    return () => {\n      for (const t of pending) clearTimeout(t);\n    };\n  }, []);\n\n  const groups = useMemo(() => {\n    const q = query.trim().toLowerCase();\n    const hits = items.filter((i) =>\n      q ? `${i.name} ${i.category} ${i.description}`.toLowerCase().includes(q) : true,\n    );\n    return [\n      { label: \"Connected\", items: hits.filter((i) => i.connection) },\n      { label: \"Available\", items: hits.filter((i) => !i.connection) },\n    ].filter((g) => g.items.length);\n  }, [items, query]);\n  const flat = groups.flatMap((g) => g.items);\n  const activeIndex = Math.min(active, Math.max(flat.length - 1, 0));\n  const open = items.find((i) => i.id === openId);\n  const listRef = useRef<HTMLDivElement>(null);\n  const [atEnd, setAtEnd] = useState(false);\n  const measureEnd = () => {\n    const el = listRef.current;\n    if (el) setAtEnd(el.scrollTop + el.clientHeight >= el.scrollHeight - 2);\n  };\n  useEffect(measureEnd, [query, openId, items]);\n  const activeId = flat[activeIndex]?.id;\n\n  useEffect(() => {\n    if (!activeId || openId) return;\n    document.getElementById(`${listId}-${activeId}`)?.scrollIntoView({ block: \"nearest\" });\n  }, [activeId, listId, openId]);\n\n  const openItem = (id: string) => {\n    setOpenId(id);\n    requestAnimationFrame(() => detailRef.current?.focus());\n  };\n  const back = () => {\n    setOpenId(undefined);\n    requestAnimationFrame(() => inputRef.current?.focus());\n  };\n\n  const connect = (id: string) => {\n    setConnecting(id);\n    later(() => {\n      setItems((prev) =>\n        prev.map((i) =>\n          i.id === id\n            ? {\n                ...i,\n                connection: {\n                  account: \"northwind-ops\",\n                  connectedBy: \"You\",\n                  connectedAt: \"Just now\",\n                  scopes: (i.permissions ?? [\"read\"]).slice(0, 3),\n                  lastSync: \"Syncing…\",\n                  records: 0,\n                  history: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],\n                  health: \"healthy\",\n                },\n              }\n            : i,\n        ),\n      );\n      setConnecting(null);\n      onConnect?.(id);\n      requestAnimationFrame(() => detailRef.current?.focus());\n      later(() => {\n        setItems((prev) =>\n          prev.map((i) =>\n            i.id === id && i.connection\n              ? {\n                  ...i,\n                  connection: {\n                    ...i.connection,\n                    lastSync: \"Just now\",\n                    records: 214,\n                    history: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 214],\n                  },\n                }\n              : i,\n          ),\n        );\n      }, 1600);\n    }, 1100);\n  };\n\n  const [syncing, setSyncing] = useState<string | null>(null);\n  const syncNow = (id: string) => {\n    setSyncing(id);\n    later(() => {\n      setItems((prev) =>\n        prev.map((i) =>\n          i.id === id && i.connection\n            ? {\n                ...i,\n                connection: {\n                  ...i.connection,\n                  lastSync: \"Just now\",\n                  health: \"healthy\",\n                  records: i.connection.records + 37,\n                  history: [...i.connection.history.slice(1), Math.max(...i.connection.history, 40)],\n                },\n              }\n            : i,\n        ),\n      );\n      setSyncing(null);\n      requestAnimationFrame(() => detailRef.current?.focus());\n    }, 1200);\n  };\n\n  const disconnect = (id: string) => {\n    setItems((prev) =>\n      prev.map((i) =>\n        i.id === id ? { ...i, permissions: i.permissions ?? i.connection?.scopes, connection: undefined } : i,\n      ),\n    );\n    requestAnimationFrame(() => detailRef.current?.focus());\n    onDisconnect?.(id);\n  };\n\n  const onListKey = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setActive((a) => (flat.length ? (Math.min(a, flat.length - 1) + 1) % flat.length : 0));\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setActive((a) => (flat.length ? (Math.min(a, flat.length - 1) - 1 + flat.length) % flat.length : 0));\n    } else if (e.key === \"Enter\" && flat[activeIndex]) {\n      e.preventDefault();\n      openItem(flat[activeIndex].id);\n    } else if (e.key === \"Escape\" && query) {\n      e.preventDefault();\n      setQuery(\"\");\n    }\n  };\n\n  const onDetailKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (e.key === \"Escape\" || (e.key === \"Backspace\" && e.target === e.currentTarget)) {\n      e.preventDefault();\n      back();\n    }\n  };\n\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 520, damping: 42 };\n  const enter = reduce ? { duration: 0 } : { duration: 0.22, ease: [0.23, 1, 0.32, 1] as const };\n  const leave = reduce ? { duration: 0 } : { duration: 0.1, ease: \"easeOut\" as const };\n  const slide = reduce ? 0 : 12;\n\n  return (\n    <motion.div\n      className={cn(\n        \"w-full max-w-[440px] overflow-hidden rounded-2xl border bg-popover text-popover-foreground shadow-xs/5\",\n        className,\n      )}\n      layout\n      style={{ borderRadius: radius }}\n      transition={spring}\n    >\n      {radiusProbe}\n      <motion.div className=\"flex h-12 items-center gap-2 border-b px-3\" layout=\"position\" transition={spring}>\n        {open ? (\n          <>\n            <Button aria-label=\"Back to connectors\" onClick={back} size=\"icon-sm\" variant=\"ghost\">\n              <ArrowLeftIcon />\n            </Button>\n            <nav aria-label=\"Breadcrumb\" className=\"flex min-w-0 items-center gap-1 text-sm\">\n              <button\n                className=\"-mx-1 shrink-0 cursor-pointer rounded-md px-1 py-1.5 text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                onClick={back}\n                type=\"button\"\n              >\n                Connectors\n              </button>\n              <ChevronRightIcon className=\"size-3.5 shrink-0 text-muted-foreground/60\" />\n              <span className=\"truncate font-medium\">{open.name}</span>\n            </nav>\n          </>\n        ) : (\n          <>\n            <SearchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            <input\n              aria-activedescendant={flat[activeIndex] ? `${listId}-${flat[activeIndex].id}` : undefined}\n              aria-controls={listId}\n              aria-expanded\n              aria-label=\"Search connectors\"\n              autoComplete=\"off\"\n              className=\"h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/72\"\n              onChange={(e) => {\n                setQuery(e.target.value);\n                setActive(0);\n              }}\n              onKeyDown={onListKey}\n              placeholder=\"Search connectors…\"\n              ref={inputRef}\n              role=\"combobox\"\n              value={query}\n            />\n            <Badge className=\"shrink-0\" size=\"sm\" variant=\"outline\">\n              {items.filter((i) => i.connection).length} connected\n            </Badge>\n          </>\n        )}\n      </motion.div>\n\n      <AnimatePresence initial={false} mode=\"wait\">\n        {open ? (\n          <motion.div\n            animate={{ opacity: 1, x: 0, transition: enter }}\n            aria-label={`${open.name} details`}\n            className=\"flex flex-col gap-4 p-4 outline-none\"\n            exit={{ opacity: 0, transition: leave }}\n            initial={{ opacity: 0, x: slide }}\n            key={`detail-${open.id}`}\n            onKeyDown={onDetailKey}\n            ref={detailRef}\n            role=\"region\"\n            tabIndex={-1}\n          >\n            <div className=\"flex items-center gap-3\">\n              <ServiceIcon item={open} size=\"lg\" />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"flex items-center gap-2\">\n                  <h3 className=\"truncate font-medium text-sm\">{open.name}</h3>\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    {open.connection ? (\n                      <motion.span\n                        animate={{ opacity: 1, scale: 1 }}\n                        initial={{ opacity: 0, scale: 0.95 }}\n                        key=\"on\"\n                        transition={enter}\n                      >\n                        <Badge size=\"sm\" variant={open.connection.health === \"healthy\" ? \"success\" : \"warning\"}>\n                          {open.connection.health === \"healthy\" ? \"Connected\" : \"Needs attention\"}\n                        </Badge>\n                      </motion.span>\n                    ) : (\n                      <motion.span animate={{ opacity: 1 }} initial={{ opacity: 0 }} key=\"off\" transition={enter}>\n                        <Badge size=\"sm\" variant=\"secondary\">\n                          Not connected\n                        </Badge>\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </div>\n                <p className=\"truncate text-muted-foreground text-xs\">{open.description}</p>\n              </div>\n            </div>\n\n            <div className=\"grid grid-cols-1 gap-2 min-[400px]:grid-cols-2\">\n              {open.connection ? (\n                <>\n                  <Tile label=\"Connection\">\n                    <div className=\"flex flex-col gap-1.5\">\n                      <Row k=\"Account\" mono v={open.connection.account} />\n                      <Row k=\"By\" v={open.connection.connectedBy} />\n                      <Row k=\"Since\" v={open.connection.connectedAt} />\n                    </div>\n                    <div className=\"flex flex-wrap gap-1\">\n                      {open.connection.scopes.map((s) => (\n                        <span className=\"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground\" key={s}>\n                          {s}\n                        </span>\n                      ))}\n                    </div>\n                  </Tile>\n                  <Tile label=\"Sync\">\n                    <div className=\"flex items-baseline justify-between gap-2\">\n                      <span className=\"font-medium text-lg tabular-nums leading-none\">\n                        {open.connection.records.toLocaleString(\"en-US\")}\n                      </span>\n                      <span className=\"text-muted-foreground text-xs\">records / 24h</span>\n                    </div>\n                    <SyncBars reduce={reduce} values={open.connection.history} />\n                    <div className=\"flex items-center gap-1.5 text-xs\">\n                      <span\n                        className={cn(\n                          \"size-1.5 rounded-full\",\n                          open.connection.health === \"healthy\" ? \"bg-success\" : \"bg-warning\",\n                        )}\n                      />\n                      <span className=\"text-muted-foreground\">Last sync</span>\n                      <span className=\"ms-auto font-medium tabular-nums\">{open.connection.lastSync}</span>\n                    </div>\n                  </Tile>\n                </>\n              ) : (\n                <>\n                  <Tile label=\"About\">\n                    <div className=\"flex flex-col gap-1.5\">\n                      <Row k=\"Category\" v={open.category} />\n                      <Row k=\"Sign-in\" v={open.auth ?? \"OAuth\"} />\n                      <Row k=\"Sync\" v={open.direction ?? \"One-way\"} />\n                    </div>\n                    <p className=\"mt-auto text-muted-foreground text-xs\">Takes about a minute. You can disconnect any time.</p>\n                  </Tile>\n                  <Tile label=\"It will ask for\">\n                    <ul className=\"flex flex-col gap-1.5\">\n                      {(open.permissions ?? []).map((p) => (\n                        <li className=\"flex items-center gap-1.5 text-xs\" key={p}>\n                          <CheckIcon className=\"size-3 shrink-0 text-success\" strokeWidth={3} />\n                          <span className=\"truncate font-mono text-[11px]\">{p}</span>\n                        </li>\n                      ))}\n                    </ul>\n                  </Tile>\n                </>\n              )}\n            </div>\n\n            <div className=\"flex items-center gap-2\">\n              {open.connection ? (\n                <>\n                  <Button className={cn(\"flex-1\", press)} onClick={() => onOpenSettings?.(open.id)} variant=\"outline\">\n                    <Settings2Icon />\n                    Settings\n                  </Button>\n                  <Button\n                    className={cn(\"flex-1\", press)}\n                    loading={syncing === open.id}\n                    onClick={() => syncNow(open.id)}\n                    variant=\"outline\"\n                  >\n                    <RefreshCwIcon />\n                    Sync now\n                  </Button>\n                  <Button\n                    aria-label={`Disconnect ${open.name}`}\n                    className={press}\n                    onClick={() => disconnect(open.id)}\n                    size=\"icon\"\n                    variant=\"destructive-outline\"\n                  >\n                    <UnplugIcon />\n                  </Button>\n                </>\n              ) : (\n                <Button\n                  className={cn(\"flex-1\", press)}\n                  loading={connecting === open.id}\n                  onClick={() => connect(open.id)}\n                >\n                  <PlugIcon />\n                  Connect {open.name}\n                </Button>\n              )}\n            </div>\n          </motion.div>\n        ) : (\n          <motion.div\n            animate={{ opacity: 1, x: 0, transition: enter }}\n            exit={{ opacity: 0, transition: leave }}\n            initial={{ opacity: 0, x: -slide }}\n            key=\"list\"\n          >\n            <div\n              aria-label=\"Connectors\"\n              className={cn(\n                \"max-h-[292px] scroll-py-1.5 overflow-y-auto p-1.5 transition-[mask-size]\",\n                !atEnd && \"mask-b-from-[calc(100%-2.5rem)]\",\n              )}\n              id={listId}\n              onScroll={measureEnd}\n              ref={listRef}\n              role=\"listbox\"\n            >\n              {groups.length === 0 && (\n                <div className=\"flex flex-col items-center gap-2 px-4 py-10 text-center\">\n                  <SearchXIcon className=\"size-5 text-muted-foreground\" />\n                  <p className=\"text-sm\">No connectors match “{query}”</p>\n                  <p className=\"text-muted-foreground text-xs\">Try a category, like “mail” or “storage”.</p>\n                </div>\n              )}\n              {groups.map((g) => (\n                <div className=\"not-first:mt-1.5\" key={g.label} role=\"group\" aria-label={g.label}>\n                  <p className=\"px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs\">{g.label}</p>\n                  {g.items.map((item) => {\n                    const idx = flat.indexOf(item);\n                    const isActive = idx === activeIndex;\n                    return (\n                      <div\n                        aria-selected={isActive}\n                        className={cn(\n                          \"relative flex cursor-pointer items-center gap-3 rounded-lg px-2 py-1.5\",\n                          isActive && \"bg-accent\",\n                        )}\n                        id={`${listId}-${item.id}`}\n                        key={item.id}\n                        onClick={() => openItem(item.id)}\n                        onMouseMove={() => idx !== activeIndex && setActive(idx)}\n                        role=\"option\"\n                      >\n                        <ServiceIcon item={item} />\n                        <span className=\"min-w-0 flex-1\">\n                          <span className=\"block truncate font-medium text-sm\">{item.name}</span>\n                          <span className=\"block truncate text-muted-foreground text-xs\">{item.category}</span>\n                        </span>\n                        <span className=\"flex shrink-0 items-center gap-2\">\n                          {item.connection ? (\n                            <span className=\"flex items-center gap-1.5 text-muted-foreground text-xs tabular-nums\">\n                              <span\n                                className={cn(\n                                  \"size-1.5 rounded-full\",\n                                  item.connection.health === \"healthy\" ? \"bg-success\" : \"bg-warning\",\n                                )}\n                              />\n                              {item.connection.lastSync}\n                            </span>\n                          ) : (\n                            <span className=\"text-muted-foreground text-xs\">Connect</span>\n                          )}\n                          <Kbd className={cn(isActive ? \"opacity-100\" : \"opacity-0\")}>\n                            <CornerDownLeftIcon />\n                          </Kbd>\n                        </span>\n                      </div>\n                    );\n                  })}\n                </div>\n              ))}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <motion.div\n        className=\"flex h-10 items-center gap-3 border-t bg-muted/40 px-3 text-muted-foreground text-xs\"\n        layout=\"position\"\n        transition={spring}\n      >\n        {open ? (\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd>Esc</Kbd> Back\n          </span>\n        ) : (\n          <>\n            <span className=\"flex items-center gap-1.5\">\n              <Kbd>↑</Kbd>\n              <Kbd>↓</Kbd> Move\n            </span>\n            <span className=\"flex items-center gap-1.5\">\n              <Kbd>\n                <CornerDownLeftIcon />\n              </Kbd>\n              Open\n            </span>\n          </>\n        )}\n        <span className=\"ms-auto tabular-nums\">{items.length} services</span>\n      </motion.div>\n    </motion.div>\n  );\n}\n"
    }
  ]
}