{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "count-tabs",
  "title": "Count tabs",
  "description": "An underlined tab row where each tab carries a count chip, the open tab in the lead colour with a 2px underline on the row's rule and its chip on the lead wash.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/count-tabs/count-tabs.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A row of underlined tabs, each carrying a count chip, for switching between\n * queues or lists that live on one page. The open tab turns lead with a 2px\n * underline sitting on the row's rule; its chip takes the lead wash.\n *\n * Keyboard follows the WAI-ARIA tabs pattern with automatic activation: one\n * tab stop for the row, ←/→ move and open (wrapping), Home/End jump to the\n * ends. On a phone the row scrolls sideways as a strip rather than wrapping or\n * widening the page.\n *\n * Pair each tab with a panel: spread `countTabPanelProps(idPrefix, id, value)`\n * onto it so `aria-controls`/`aria-labelledby` line up.\n */\n\nexport type CountTabItem = {\n  id: string;\n  label: React.ReactNode;\n  /** The chip's number. Zero still shows; omit (or null) for no chip. */\n  count?: number | null;\n};\n\nexport interface CountTabsProps {\n  items: readonly CountTabItem[];\n  /** Controlled open tab. */\n  value?: string;\n  /** Starting tab when uncontrolled. Defaults to the first item. */\n  defaultValue?: string;\n  /** Called with the id of the tab opened, by click or keyboard. */\n  onValueChange?: (id: string) => void;\n  /** Accessible name of the tablist. */\n  ariaLabel?: string;\n  /**\n   * Prefix for the tab and panel element ids (`<prefix>-tab-<id>`,\n   * `<prefix>-panel-<id>`). Pass one when you render panels, so they can be\n   * linked with `countTabPanelProps`.\n   */\n  idPrefix?: string;\n  className?: string;\n}\n\nconst tabId = (prefix: string, id: string) => `${prefix}-tab-${id}`;\nconst panelId = (prefix: string, id: string) => `${prefix}-panel-${id}`;\n\n/** Props for the panel behind tab `id`: role, ids, and `hidden` when closed. */\nexport function countTabPanelProps(idPrefix: string, id: string, value: string | undefined) {\n  return {\n    id: panelId(idPrefix, id),\n    role: \"tabpanel\" as const,\n    \"aria-labelledby\": tabId(idPrefix, id),\n    hidden: value !== id,\n  };\n}\n\nexport function CountTabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  ariaLabel,\n  idPrefix,\n  className,\n}: CountTabsProps) {\n  const reactId = React.useId();\n  const prefix = idPrefix ?? `count-tabs-${reactId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const [ownValue, setOwnValue] = React.useState(defaultValue ?? items[0]?.id);\n  const current = value !== undefined ? value : ownValue;\n  const tabRefs = React.useRef(new Map<string, HTMLButtonElement>());\n\n  const open = (id: string) => {\n    if (value === undefined) setOwnValue(id);\n    onValueChange?.(id);\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n    const order = items.map((item) => item.id);\n    const at = order.indexOf(current ?? \"\");\n    let next: string | undefined;\n    if (event.key === \"ArrowRight\") next = order[(at + 1) % order.length];\n    if (event.key === \"ArrowLeft\") next = order[(at - 1 + order.length) % order.length];\n    if (event.key === \"Home\") next = order[0];\n    if (event.key === \"End\") next = order[order.length - 1];\n    if (next === undefined) return;\n\n    event.preventDefault();\n    open(next);\n    tabRefs.current.get(next)?.focus();\n  };\n\n  // If the controlled value is not a tab, the first tab keeps the tab stop.\n  const focusable = items.some((item) => item.id === current) ? current : items[0]?.id;\n\n  return (\n    <div\n      role=\"tablist\"\n      aria-label={ariaLabel}\n      className={cn(\n        // Scrolls sideways, at any width the row does not fit, and never up and\n        // down. The rule is an inset line inside the box rather than a border,\n        // so the open tab's underline can sit exactly on it without poking out\n        // of the scroll area (which is what made it scroll vertically).\n        \"flex items-end gap-6 overflow-x-auto overflow-y-hidden font-body shadow-[inset_0_-1px_0_var(--rule)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        className,\n      )}\n    >\n      {items.map((item) => {\n        const active = item.id === current;\n        const count = item.count ?? null;\n        return (\n          <button\n            key={item.id}\n            ref={(node) => {\n              if (node) tabRefs.current.set(item.id, node);\n              else tabRefs.current.delete(item.id);\n            }}\n            type=\"button\"\n            role=\"tab\"\n            id={tabId(prefix, item.id)}\n            aria-controls={panelId(prefix, item.id)}\n            aria-selected={active}\n            tabIndex={item.id === focusable ? 0 : -1}\n            onClick={() => open(item.id)}\n            onKeyDown={handleKeyDown}\n            className={cn(\n              \"relative inline-flex shrink-0 cursor-pointer items-center gap-2 pb-3 text-sm font-semibold whitespace-nowrap transition-colors pointer-coarse:min-h-11 focus-visible:ring-2 focus-visible:ring-lead/30 focus-visible:outline-none\",\n              active\n                ? \"text-lead after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-lead\"\n                : \"text-dim hover:text-ink\",\n            )}\n          >\n            {item.label}\n            {count !== null && (\n              <span\n                className={cn(\n                  \"rounded-full px-2 py-0.5 text-[11px] tabular-nums\",\n                  active ? \"bg-lead-wash text-lead\" : \"bg-paper-sunk text-dim\",\n                )}\n              >\n                {count}\n              </span>\n            )}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use to switch between queues or lists on one page where how many items wait in each is the point (a review page, an inbox's folders). Not for page-level section navigation between routes; use gooey-tabs or a link row for that.",
    "origin": {
      "project": "180Connect",
      "path": "src/app/(app)/admin/review/review-panel.tsx"
    },
    "credits": []
  },
  "type": "registry:component"
}
