{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs-glass",
  "title": "Glass tabs",
  "description": "Styled tabs: the Glass set. 5 decorative tabs variations (FrostTabs, TintTabs, SmokeTabs, CrystalTabs, DepthTabs) 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",
    "@ai2/glass"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/tabs-glass.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { Activity, LayoutGrid, Settings } from \"lucide-react\"\r\nimport { motion, useReducedMotion } from \"motion/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\nimport { glassDepth } from \"@/registry/ai2/ui/glass\"\r\n\r\n/* Glass tabs family: 5 frosted-glass tab strips. The difference is only on the\n   surface of the tab strip: frost, tint, smoke, crystal and depth. Each export is a\n   complete tabs component - internal active-tab state, role=\"tablist\"/role=\"tab\"\n   and a panel with role=\"tabpanel\". The ids are produced with React.useId(), so\n   several examples on the same page do not collide. Color comes ONLY from tokens,\n   via alpha color-mix. The indicator slides with a framer-motion layoutId; it\n   switches instantly under reduced motion.\n\n   The glass surface derives from the glassDepth scale in @ai2/glass (AGENTS.md 4.5)\n   - the blur is never hand-written here. The step says how much it hides, and the\n   variant itself gives the character (tint, edge). Note: glassDepth carries a\n   [box-shadow], which is why a variant never adds shadow-lg/shadow-inner -\n   tailwind-merge keeps the last of the same group and the signature would silently\n   drop; extra depth is given with a ring. */\r\n\r\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\r\n\r\ntype TabItem = { value: string; label: React.ReactNode; panel: React.ReactNode }\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 DEFAULT_TABS: TabItem[] = [\r\n  { value: \"overview\", label: \"Overview\", panel: \"A summary of the workspace: recent activity, open items and the team.\" },\r\n  { value: \"activity\", label: \"Activity\", panel: \"Every event from the last seven days, newest first.\" },\r\n  { value: \"settings\", label: \"Settings\", panel: \"Workspace name, members and notification preferences.\" },\r\n]\r\n\r\nconst ICON_TABS: TabItem[] = [\r\n  { value: \"overview\", label: <><LayoutGrid /> Overview</>, panel: \"A summary of the workspace: recent activity, open items and the team.\" },\r\n  { value: \"activity\", label: <><Activity /> Activity</>, panel: \"Every event from the last seven days, newest first.\" },\r\n  { value: \"settings\", label: <><Settings /> Settings</>, panel: \"Workspace name, members and notification preferences.\" },\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\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\n/* Sliding indicator transition: instant under reduced motion. */\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 GlassConfig = {\r\n  fallback: TabItem[]\r\n  bar: string\r\n  indicator: string\r\n  active: string\r\n  inactive: string\r\n}\r\n\r\n/* Shared shell: a glass bar plus a sliding indicator plus panel. */\r\nfunction GlassTabsShell({ props, config }: { props: TabsProps; config: GlassConfig }) {\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], \"rounded-full\", on ? config.active : config.inactive)}\r\n              >\r\n                {on ? (\r\n                  <motion.span\r\n                    layoutId={`${uid}-glass-indicator`}\r\n                    transition={transition}\r\n                    className={cn(\"absolute inset-0 -z-10 rounded-full\", config.indicator)}\r\n                  />\r\n                ) : null}\r\n                {tab.label}\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\n/* Frost: a neutral frosted bar where the active tab carries an opaque card surface. Depth md (8px): the family default, the same step as glass.overlay. The neutral ground comes from the library (exactly the same as the old hand-written background/60); the character is the plain border alone. */\r\nexport function FrostTabs(props: TabsProps) {\r\n  return (\r\n    <GlassTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: DEFAULT_TABS,\r\n        bar: cn(glassDepth.md, \"rounded-full border border-border p-1\"),\r\n        indicator: \"border border-border bg-card 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\n/* Tint: an info-toned glass bar, with an info tint on the active tab.\n   Depth md (8px): the same step as Frost - deliberately. Tint is Frost in color;\n   the only difference should be the hue. An info film takes over the background,\n   which is why the supports- variant is overridden too (otherwise the library's\n   bg-background/60 would hide the tint inside @supports). */\r\nexport function TintTabs(props: TabsProps) {\r\n  return (\r\n    <GlassTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: DEFAULT_TABS,\r\n        bar: cn(\r\n          glassDepth.md,\r\n          \"rounded-full border border-[color-mix(in_oklab,var(--color-info)_22%,transparent)] p-1\",\r\n          \"bg-[color-mix(in_oklab,var(--color-info)_10%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-info)_10%,transparent)]\"\r\n        ),\r\n        indicator: \"border border-[color-mix(in_oklab,var(--color-info)_28%,transparent)] bg-[color-mix(in_oklab,var(--color-info)_18%,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\n/* Smoke: dark smoky glass where what is behind turns into texture. Depth lg (16px): the scale step closest to the old 24px's intent of \"make what is behind unreadable\". The character is foreground smoke. */\r\nexport function SmokeTabs(props: TabsProps) {\r\n  return (\r\n    <GlassTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: DEFAULT_TABS,\r\n        bar: cn(\r\n          glassDepth.lg,\r\n          \"rounded-full border border-[color-mix(in_oklab,var(--color-foreground)_14%,transparent)] p-1\",\r\n          \"bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]\"\r\n        ),\r\n        indicator: \"bg-[color-mix(in_oklab,var(--color-foreground)_16%,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\n/* Crystal: a very transparent bar with a bright line along the top edge.\n   Depth sm (4px): crystal glass does NOT diffuse, it is clear - its character is\n   transparency and a bright edge, not blur. The lowest step, deliberately. Because a\n   gradient is a background-IMAGE it covers the library's background-COLOR; separate\n   merge groups, so both survive. */\r\nexport function CrystalTabs(props: TabsProps) {\r\n  return (\r\n    <GlassTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: ICON_TABS,\r\n        bar: cn(\r\n          glassDepth.sm,\r\n          \"rounded-full border border-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)] p-1\",\r\n          \"bg-gradient-to-b from-[color-mix(in_oklab,var(--color-background)_70%,transparent)] to-[color-mix(in_oklab,var(--color-background)_35%,transparent)]\"\r\n        ),\r\n        indicator: \"border border-[color-mix(in_oklab,var(--color-background)_80%,transparent)] bg-[color-mix(in_oklab,var(--color-background)_75%,transparent)] 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\n/* Depth: layered glass, with the active tab standing out on a raised shadow.\n   Depth xl (24px): the deepest in the family - the bar should recede and the active\n   tab should rise in front of it. The old shadow-inner was REMOVED: it is in the\n   same tailwind-merge group as glassDepth's [box-shadow] signature, and adding it\n   would silently drop the signature. ring-* is NO CURE either: in Tailwind v4 a ring\n   compiles into box-shadow and an arbitrary [box-shadow] overrides it entirely\n   (measured: --tw-ring-shadow is computed but box-shadow never looks at it). The\n   sense of recession is carried by the muted background + the xl blur; the active\n   tab's opaque raised indicator already provides the contrast. */\r\nexport function DepthTabs(props: TabsProps) {\r\n  return (\r\n    <GlassTabsShell\r\n      props={props}\r\n      config={{\r\n        fallback: DEFAULT_TABS,\r\n        bar: cn(\r\n          glassDepth.xl,\r\n          \"rounded-full border border-border p-1\",\r\n          \"bg-[color-mix(in_oklab,var(--color-muted)_70%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-muted)_70%,transparent)]\"\r\n        ),\r\n        indicator: \"border border-border bg-popover shadow-lg\",\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-glass.tsx"
    }
  ],
  "categories": [
    "styled",
    "tabs"
  ],
  "type": "registry:component"
}