{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs-badge",
  "title": "Badge tabs",
  "description": "Styled tabs: the Badge set. 5 decorative tabs variations (CountTabs, DotTabs, StatusTabs, IconBadgeTabs, AlertTabs) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/tabs-badge.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { AlertTriangle, Inbox, LayoutGrid, MessageSquare, Users } from \"lucide-react\"\r\nimport { motion, useReducedMotion } from \"motion/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\n/* Badge tabs family: 5 tab strips where every tab carries a count or a status badge.\n   Count numbers, dots, status labels, an icon + badge and a warning badge.\n   Each export is a complete tabs component - internal active-tab state,\n   role=\"tablist\"/role=\"tab\" and a role=\"tabpanel\" panel. The badge text is part of\n   the tab's accessible name; it is also visual. The ids are produced with\n   React.useId(). Color comes ONLY from tokens, via alpha color-mix. The indicator\n   slides with a framer-motion layoutId; it switches instantly under reduced\n   motion. */\r\n\r\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\r\n\r\ntype TabItem = {\r\n  value: string\r\n  label: React.ReactNode\r\n  panel: React.ReactNode\r\n  badge?: React.ReactNode\r\n}\r\n\r\ntype TabsProps = {\r\n  className?: string\r\n  size?: StyledSize\r\n  tabs?: TabItem[]\r\n  value?: string\r\n  defaultValue?: string\r\n  onValueChange?: (v: string) => void\r\n}\r\n\r\nconst sizeTab: Record<StyledSize, string> = {\r\n  sm: \"h-8 px-3 text-xs\",\r\n  md: \"h-9 px-4 text-sm\",\r\n  lg: \"h-10 px-5 text-sm\",\r\n  xl: \"h-11 px-6 text-base\",\r\n}\r\n\r\nconst tabBase =\r\n  \"relative z-10 inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:shrink-0 [&_i]:text-base [&_i]:leading-none [&>i]:shrink-0\"\r\n\r\nconst panelBase =\r\n  \"rounded-lg border border-border bg-card p-4 text-sm text-muted-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\r\n\r\nconst countPill =\r\n  \"inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[0.6875rem] font-semibold leading-none tabular-nums\"\r\n\r\nconst dot = \"size-1.5 shrink-0 rounded-full\"\r\n\r\nconst statusPill =\r\n  \"inline-flex h-5 items-center justify-center rounded-full px-2 text-[0.6875rem] font-semibold leading-none\"\r\n\r\nfunction useTabsState(props: TabsProps, fallback: TabItem[]) {\r\n  const { tabs, value, defaultValue, onValueChange } = props\r\n  const list = React.useMemo(() => (tabs && tabs.length ? tabs : fallback), [tabs, fallback])\r\n  const [internal, setInternal] = React.useState<string>(() => defaultValue ?? list[0]?.value ?? \"\")\r\n  const active = value ?? internal\r\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\r\n  const select = React.useCallback(\r\n    (v: string) => {\r\n      if (value === undefined) setInternal(v)\r\n      onValueChange?.(v)\r\n    },\r\n    [value, onValueChange]\r\n  )\r\n  const onKeyDown = (e: React.KeyboardEvent) => {\r\n    if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\") return\r\n    e.preventDefault()\r\n    const idx = list.findIndex((t) => t.value === active)\r\n    const dir = e.key === \"ArrowRight\" ? 1 : -1\r\n    const ni = (idx + dir + list.length) % list.length\r\n    select(list[ni].value)\r\n    refs.current[ni]?.focus()\r\n  }\r\n  return { list, active, select, refs, onKeyDown }\r\n}\r\n\r\nfunction useSlide() {\r\n  const reduce = useReducedMotion()\r\n  return reduce\r\n    ? { duration: 0 }\r\n    : { type: \"spring\" as const, stiffness: 420, damping: 34 }\r\n}\r\n\r\ntype BadgeConfig = {\r\n  fallback: TabItem[]\r\n  bar: string\r\n  tab: string\r\n  indicator: string\r\n  active: string\r\n  inactive: string\r\n}\r\n\r\n/* Shared shell: badged tabs plus a sliding indicator plus panel. */\r\nfunction BadgeTabsShell({ props, config }: { props: TabsProps; config: BadgeConfig }) {\r\n  const { className, size = \"md\" } = props\r\n  const { list, active, select, refs, onKeyDown } = useTabsState(props, config.fallback)\r\n  const uid = React.useId()\r\n  const transition = useSlide()\r\n  const current = list.find((t) => t.value === active) ?? list[0]\r\n\r\n  return (\r\n    <div data-slot=\"styled-tabs\" className={cn(\"flex flex-col gap-3\", className)}>\r\n      {/* 200% text (WCAG 1.4.4): the strip is `w-fit`, so growing text widened\n          the page. Scrolling was moved inside the strip itself; the wrapper is\n          REQUIRED because `overflow-x-auto` clips vertically too and the focus\n          ring would be cut off. The `-m-1 p-1` pair opens room for the ring and\n          preserves the outer measurements. The same fix is documented in detail\n          in registry/ai2/ui/tabs.tsx. */}\r\n      <div\r\n        data-slot=\"styled-tabs-viewport\"\r\n        className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n      >\r\n        <div\r\n          role=\"tablist\"\r\n          aria-orientation=\"horizontal\"\r\n          onKeyDown={onKeyDown}\r\n          className={cn(\"inline-flex w-fit items-center gap-1\", config.bar)}\r\n        >\r\n          {list.map((tab, i) => {\r\n            const on = tab.value === active\r\n            return (\r\n              <button\r\n                key={tab.value}\r\n                ref={(el) => {\r\n                  refs.current[i] = el\r\n                }}\r\n                type=\"button\"\r\n                role=\"tab\"\r\n                id={`${uid}-tab-${tab.value}`}\r\n                aria-selected={on}\r\n                aria-controls={`${uid}-panel-${tab.value}`}\r\n                tabIndex={on ? 0 : -1}\r\n                onClick={() => select(tab.value)}\r\n                className={cn(tabBase, sizeTab[size], config.tab, on ? config.active : config.inactive)}\r\n              >\r\n                {on ? (\r\n                  <motion.span\r\n                    layoutId={`${uid}-badge-indicator`}\r\n                    transition={transition}\r\n                    className={cn(\"absolute -z-10\", config.indicator)}\r\n                  />\r\n                ) : null}\r\n                {tab.label}\r\n                {tab.badge}\r\n              </button>\r\n            )\r\n          })}\r\n        </div>\r\n      </div>\r\n      {current ? (\r\n        <div\r\n          role=\"tabpanel\"\r\n          id={`${uid}-panel-${current.value}`}\r\n          aria-labelledby={`${uid}-tab-${current.value}`}\r\n          tabIndex={0}\r\n          className={panelBase}\r\n        >\r\n          {current.panel}\r\n        </div>\r\n      ) : null}\r\n    </div>\r\n  )\r\n}\r\n\r\nconst COUNT_TABS: TabItem[] = [\r\n  {\r\n    value: \"inbox\",\r\n    label: \"Inbox\",\r\n    badge: <span className={cn(countPill, \"bg-primary text-primary-foreground\")}>12</span>,\r\n    panel: \"Twelve unread threads are waiting in the shared inbox.\",\r\n  },\r\n  {\r\n    value: \"drafts\",\r\n    label: \"Drafts\",\r\n    badge: <span className={cn(countPill, \"bg-muted text-muted-foreground\")}>4</span>,\r\n    panel: \"Four drafts have not been sent yet.\",\r\n  },\r\n  {\r\n    value: \"archive\",\r\n    label: \"Archive\",\r\n    badge: <span className={cn(countPill, \"bg-muted text-muted-foreground\")}>96</span>,\r\n    panel: \"Everything that has been resolved and filed away.\",\r\n  },\r\n]\r\n\r\n/* Count: her sekmede sayisal rozet, aktif sekmede primary pill. */\r\nexport function CountTabs(props: TabsProps) {\r\n  return (\r\n    <BadgeTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: COUNT_TABS,\r\n        bar: \"rounded-lg bg-muted p-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md border border-border bg-background shadow-sm\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\nconst DOT_TABS: TabItem[] = [\r\n  {\r\n    value: \"running\",\r\n    label: <><span className={cn(dot, \"bg-success\")} aria-hidden=\"true\" /> Running</>,\r\n    panel: \"Three jobs are running right now.\",\r\n  },\r\n  {\r\n    value: \"queued\",\r\n    label: <><span className={cn(dot, \"bg-warning\")} aria-hidden=\"true\" /> Queued</>,\r\n    panel: \"Jobs waiting for a free worker.\",\r\n  },\r\n  {\r\n    value: \"failed\",\r\n    label: <><span className={cn(dot, \"bg-danger\")} aria-hidden=\"true\" /> Failed</>,\r\n    panel: \"Jobs that stopped with an error and can be retried.\",\r\n  },\r\n]\r\n\r\n/* Dot: etiketin onunde token renkli durum noktasi. */\r\nexport function DotTabs(props: TabsProps) {\r\n  return (\r\n    <BadgeTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: DOT_TABS,\r\n        bar: \"border-b border-border\",\r\n        tab: \"-mb-px rounded-t-md\",\r\n        indicator: \"inset-x-0 -bottom-px h-0.5 rounded-full bg-primary\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\nconst STATUS_TABS: TabItem[] = [\r\n  {\r\n    value: \"live\",\r\n    label: \"Live\",\r\n    badge: <span className={cn(statusPill, \"bg-success-soft text-success-soft-foreground\")}>OK</span>,\r\n    panel: \"The published version that visitors are seeing.\",\r\n  },\r\n  {\r\n    value: \"staging\",\r\n    label: \"Staging\",\r\n    badge: <span className={cn(statusPill, \"bg-warning-soft text-warning-soft-foreground\")}>Build</span>,\r\n    panel: \"The preview build waiting for review.\",\r\n  },\r\n  {\r\n    value: \"draft\",\r\n    label: \"Draft\",\r\n    badge: <span className={cn(statusPill, \"bg-muted text-muted-foreground\")}>Idle</span>,\r\n    panel: \"Work in progress that has never been deployed.\",\r\n  },\r\n]\r\n\r\n/* Status: sekme basina metinli durum rozeti (soft token tonlari). */\r\nexport function StatusTabs(props: TabsProps) {\r\n  return (\r\n    <BadgeTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: STATUS_TABS,\r\n        bar: \"rounded-lg border border-border bg-card p-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md bg-[color-mix(in_oklab,var(--color-foreground)_7%,transparent)]\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\nconst ICON_BADGE_TABS: TabItem[] = [\r\n  {\r\n    value: \"inbox\",\r\n    label: <><Inbox /> Inbox</>,\r\n    badge: <span className={cn(countPill, \"bg-primary text-primary-foreground\")}>8</span>,\r\n    panel: \"Eight unread threads are waiting in the shared inbox.\",\r\n  },\r\n  {\r\n    value: \"comments\",\r\n    label: <><MessageSquare /> Comments</>,\r\n    badge: <span className={cn(countPill, \"bg-muted text-muted-foreground\")}>23</span>,\r\n    panel: \"Comments left on documents you follow.\",\r\n  },\r\n  {\r\n    value: \"team\",\r\n    label: <><Users /> Team</>,\r\n    badge: <span className={cn(countPill, \"bg-muted text-muted-foreground\")}>6</span>,\r\n    panel: \"Six people have access to this workspace.\",\r\n  },\r\n]\r\n\r\n/* IconBadge: ikon + etiket + sayi rozeti, primary tintli aktif gosterge. */\r\nexport function IconBadgeTabs(props: TabsProps) {\r\n  return (\r\n    <BadgeTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: ICON_BADGE_TABS,\r\n        bar: \"inline-flex items-center gap-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)]\",\r\n        active: \"text-primary\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\nconst ALERT_TABS: TabItem[] = [\r\n  {\r\n    value: \"overview\",\r\n    label: <><LayoutGrid /> Overview</>,\r\n    panel: \"A summary of the workspace: recent activity, open items and the team.\",\r\n  },\r\n  {\r\n    value: \"issues\",\r\n    label: <><AlertTriangle /> Issues</>,\r\n    badge: <span className={cn(countPill, \"bg-danger text-danger-foreground\")}>3</span>,\r\n    panel: \"Three checks are failing and need attention.\",\r\n  },\r\n  {\r\n    value: \"warnings\",\r\n    label: \"Warnings\",\r\n    badge: <span className={cn(countPill, \"bg-warning-soft text-warning-soft-foreground\")}>7</span>,\r\n    panel: \"Seven non-blocking warnings from the last run.\",\r\n  },\r\n]\r\n\r\n/* Alert: dikkat isteyen sekmeler danger / warning rozetiyle one cikar. */\r\nexport function AlertTabs(props: TabsProps) {\r\n  return (\r\n    <BadgeTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: ALERT_TABS,\r\n        bar: \"rounded-lg border border-border bg-muted p-1\",\r\n        tab: \"rounded-md\",\r\n        indicator: \"inset-0 rounded-md border border-border bg-background shadow-sm\",\r\n        active: \"text-foreground\",\r\n        inactive: \"text-muted-foreground hover:text-foreground\",\r\n      }}\r\n    />\r\n  )\r\n}\r\n",
      "type": "registry:component",
      "target": "components/ui/tabs-badge.tsx"
    }
  ],
  "categories": [
    "styled",
    "tabs"
  ],
  "type": "registry:component"
}