{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-tabs",
  "title": "Page Tabs",
  "description": "Primary tabs with counts, page actions, and horizontal overflow.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "tabs",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/page-tabs.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const PAGE_TABS_VARIANTS = [\"underline\", \"pill\", \"segmented\"] as const;\nexport type PageTabsVariant = (typeof PAGE_TABS_VARIANTS)[number];\nexport type PageTabsProps = Omit<ComponentProps<\"div\">, \"defaultValue\" | \"dir\"> & {\n  variant?: PageTabsVariant;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\ntype TabsContext = {\n  id: string;\n  value: string;\n  variant: PageTabsVariant;\n  select: (value: string) => void;\n};\nconst Context = createContext<TabsContext | null>(null);\nfunction useTabs(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within PageTabs`);\n  return context;\n}\nconst TabContext = createContext<boolean | null>(null);\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n// Fades whichever edge of a scroller still hides content.\nconst scrollEdgeFade =\n  \"data-overflow-start:[mask-image:linear-gradient(to_right,transparent,black_24px)] data-overflow-end:[mask-image:linear-gradient(to_left,transparent,black_24px)] data-overflow-start:data-overflow-end:[mask-image:linear-gradient(to_right,transparent,black_24px,black_calc(100%-24px),transparent)]\";\nfunction useScrollEdges(ref: React.RefObject<HTMLElement | null>) {\n  useEffect(() => {\n    const element = ref.current;\n    if (!element) return;\n    const update = () => {\n      const max = element.scrollWidth - element.clientWidth;\n      element.toggleAttribute(\"data-overflow-start\", element.scrollLeft > 1);\n      element.toggleAttribute(\"data-overflow-end\", element.scrollLeft < max - 1);\n    };\n    update();\n    element.addEventListener(\"scroll\", update, { passive: true });\n    const observer = typeof ResizeObserver === \"function\" ? new ResizeObserver(update) : null;\n    observer?.observe(element);\n    for (const child of element.children) observer?.observe(child);\n    return () => {\n      element.removeEventListener(\"scroll\", update);\n      observer?.disconnect();\n    };\n  }, [ref]);\n}\nfunction slug(value: string) {\n  return value.replace(/[^\\w-]/g, \"_\");\n}\n\nexport function PageTabs({\n  variant = \"underline\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  className,\n  ...props\n}: PageTabsProps) {\n  const id = useId();\n  const [internal, setInternal] = useState(defaultValue);\n  const current = value ?? internal;\n  const select = (next: string) => {\n    if (next === current) return;\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n  return (\n    <Context.Provider value={{ id, value: current, variant, select }}>\n      <Tabs\n        data-slot=\"page-tabs\"\n        data-variant={variant}\n        value={current}\n        onValueChange={select}\n        className={cn(\"grid min-w-0 gap-4 text-[13px]/[18px] text-foreground\", className)}\n        {...props}\n      />\n    </Context.Provider>\n  );\n}\n\n/** Lays out the tab list beside page actions. */\nexport function PageTabsBar({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useTabs(\"PageTabsBar\");\n  return (\n    <div\n      data-slot=\"page-tabs-bar\"\n      className={cn(\n        \"flex min-w-0 items-center gap-3\",\n        context.variant === \"underline\" && \"border-b\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype Thumb = { left: number; top: number; width: number; height: number };\nconst ListContext = createContext(false);\nconst easeOut = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n\nconst pageTabsThumbVariants = cva(\n  \"pointer-events-none absolute left-0 -z-1 transition-[transform,width] duration-240 ease-out-quint motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        underline: \"h-0.5 rounded-[2px] bg-foreground\",\n        pill: \"rounded-full bg-accent\",\n        segmented:\n          \"rounded-[9px] bg-card shadow-[0_0_0_1px_var(--border),0_1px_2px_oklch(0_0_0/0.08)]\",\n      },\n    },\n  },\n);\n\nfunction thumbStyle(variant: PageTabsVariant, thumb: Thumb): React.CSSProperties {\n  if (variant === \"underline\") {\n    return {\n      top: thumb.top + thumb.height - 2,\n      width: Math.max(0, thumb.width - 16),\n      transform: `translateX(${thumb.left + 8}px)`,\n    };\n  }\n  return {\n    top: thumb.top,\n    width: thumb.width,\n    height: thumb.height,\n    transform: `translateX(${thumb.left}px)`,\n  };\n}\n\nexport function PageTabsList({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = useTabs(\"PageTabsList\");\n  const ref = useRef<HTMLDivElement>(null);\n  const [thumb, setThumb] = useState<Thumb | null>(null);\n  const [settled, setSettled] = useState(false);\n  useScrollEdges(ref);\n  // Measure the selected tab so a single thumb can slide between tabs.\n  useIsomorphicLayoutEffect(() => {\n    const list = ref.current;\n    if (!list) return;\n    const measure = () => {\n      const tab = list.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]');\n      if (!tab) return setThumb(null);\n      const next = {\n        left: tab.offsetLeft,\n        top: tab.offsetTop,\n        width: tab.offsetWidth,\n        height: tab.offsetHeight,\n      };\n      setThumb((current) =>\n        current &&\n        current.left === next.left &&\n        current.top === next.top &&\n        current.width === next.width &&\n        current.height === next.height\n          ? current\n          : next,\n      );\n    };\n    measure();\n    if (typeof ResizeObserver !== \"function\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    for (const tab of list.querySelectorAll('[role=\"tab\"]')) observer.observe(tab);\n    return () => observer.disconnect();\n  }, [context.value]);\n  // Skip the slide on first paint so the thumb does not fly in from the left edge.\n  useEffect(() => {\n    if (!thumb || settled) return;\n    const frame = requestAnimationFrame(() => setSettled(true));\n    return () => cancelAnimationFrame(frame);\n  }, [thumb, settled]);\n  // Radix owns arrow, Home, and End navigation through roving focus.\n  return (\n    <TabsList\n      data-slot=\"page-tabs-list\"\n      // The Uai variant replaces the shadcn list variant so its active-tab shadow and line styles stay off.\n      data-variant={context.variant}\n      className={cn(\n        \"relative isolate flex h-auto w-auto min-w-0 flex-[1_1_auto] items-center justify-start overflow-x-auto overscroll-x-contain [scrollbar-width:none] group-data-[orientation=horizontal]/tabs:h-auto\",\n        context.variant === \"underline\" ? \"gap-1\" : \"gap-0.5\",\n        scrollEdgeFade,\n        context.variant === \"segmented\"\n          ? \"rounded-xl bg-muted p-0.75\"\n          : \"rounded-none bg-transparent p-0\",\n        className,\n      )}\n      {...props}\n      ref={ref}\n    >\n      {thumb ? (\n        <span\n          aria-hidden=\"true\"\n          data-page-tabs-thumb=\"\"\n          data-slot=\"page-tabs-thumb\"\n          className={cn(\n            pageTabsThumbVariants({ variant: context.variant }),\n            !settled && \"transition-none\",\n          )}\n          style={thumbStyle(context.variant, thumb)}\n        />\n      ) : null}\n      <ListContext.Provider value={thumb !== null}>{children}</ListContext.Provider>\n    </TabsList>\n  );\n}\n\nconst pageTabsTabVariants = cva(\n  \"relative inline-flex flex-none cursor-pointer items-center justify-start gap-1.5 border-0 bg-transparent py-0 font-medium whitespace-nowrap transition-[color,background-color,scale] duration-140 ease-[cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring enabled:active:scale-[0.97] disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-45 motion-reduce:transition-none motion-reduce:active:scale-100\",\n  {\n    variants: {\n      variant: {\n        underline: \"h-10 rounded-none px-2 text-[13px]\",\n        pill: \"h-7 rounded-full px-3 text-[12.5px]\",\n        segmented: \"h-7 rounded-[9px] px-3 text-[12.5px]\",\n      },\n      fallback: {\n        true: \"\",\n        false:\n          \"data-[state=active]:bg-transparent data-active:bg-transparent dark:data-[state=active]:bg-transparent dark:data-active:bg-transparent\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"underline\",\n        fallback: true,\n        className:\n          \"shadow-[inset_0_-2px_0_var(--foreground)] data-[state=active]:bg-transparent data-active:bg-transparent dark:data-[state=active]:bg-transparent dark:data-active:bg-transparent\",\n      },\n      {\n        variant: \"pill\",\n        fallback: true,\n        className:\n          \"data-[state=active]:bg-accent data-active:bg-accent dark:data-[state=active]:bg-accent dark:data-active:bg-accent\",\n      },\n      {\n        variant: \"segmented\",\n        fallback: true,\n        className:\n          \"shadow-[0_0_0_1px_var(--border),0_1px_2px_oklch(0_0_0/0.08)] data-[state=active]:bg-card data-active:bg-card dark:data-[state=active]:bg-card dark:data-active:bg-card\",\n      },\n    ],\n  },\n);\n\nexport function PageTabsTab({\n  value,\n  children,\n  className,\n  onClick,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"value\"> & { value: string }) {\n  const context = useTabs(\"PageTabsTab\");\n  // The list paints a sliding thumb once it has measured; until then the tab paints itself.\n  const thumb = useContext(ListContext);\n  const selected = context.value === value;\n  const { variant } = context;\n  const fallback = selected && !thumb;\n  return (\n    <TabContext.Provider value={selected}>\n      <TabsTrigger\n        type=\"button\"\n        data-slot=\"page-tabs-tab\"\n        className={cn(\n          pageTabsTabVariants({ variant, fallback }),\n          selected\n            ? \"text-foreground hover:text-foreground dark:text-foreground dark:hover:text-foreground\"\n            : \"text-subtle-foreground hover:text-muted-foreground dark:text-subtle-foreground dark:hover:text-muted-foreground\",\n          className,\n        )}\n        {...props}\n        value={value}\n        id={`${context.id}-tab-${slug(value)}`}\n        aria-controls={`${context.id}-panel-${slug(value)}`}\n        // Keep the selected tab in the tab order before the list has been focused.\n        tabIndex={selected ? 0 : -1}\n        onClick={(event) => {\n          onClick?.(event);\n          if (event.defaultPrevented) return;\n          event.currentTarget.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n        }}\n      >\n        {children}\n      </TabsTrigger>\n    </TabContext.Provider>\n  );\n}\n\nexport function PageTabsCount({ className, ...props }: ComponentProps<\"span\">) {\n  const selected = useContext(TabContext);\n  useTabs(\"PageTabsCount\");\n  if (selected === null) throw new Error(\"PageTabsCount must be used within PageTabsTab\");\n  return (\n    <span\n      data-slot=\"page-tabs-count\"\n      className={cn(\n        \"min-w-[18px] rounded-full px-1.5 text-center text-[11px]/[17px] font-medium tabular-nums transition-[background-color,color] duration-120 ease-[ease-out]\",\n        selected ? \"bg-foreground/10 text-foreground\" : \"bg-foreground/6 text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Page-level actions that sit beside the tabs. They are not part of the tab list. */\nexport function PageTabsActions({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"page-tabs-actions\"\n      className={cn(\"flex flex-none items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function PageTabsPanel({\n  value,\n  className,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"value\"> & { value: string }) {\n  const context = useTabs(\"PageTabsPanel\");\n  const selected = context.value === value;\n  const ref = useRef<HTMLDivElement>(null);\n  const shown = useRef(selected);\n  // Fade the incoming panel so switching tabs reads as one continuous motion.\n  useIsomorphicLayoutEffect(() => {\n    const wasShown = shown.current;\n    shown.current = selected;\n    const panel = ref.current;\n    if (!selected || wasShown || !panel || typeof panel.animate !== \"function\") return;\n    if (window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) return;\n    panel.animate(\n      [\n        { opacity: 0, transform: \"translateY(4px)\" },\n        { opacity: 1, transform: \"none\" },\n      ],\n      { duration: 220, easing: easeOut },\n    );\n  }, [selected]);\n  return (\n    // Inactive panels stay mounted so their state survives tab switches. A plain tabpanel rather\n    // than TabsContent: keeping it mounted needs `forceMount` on Radix and `keepMounted` on Base UI.\n    <div\n      role=\"tabpanel\"\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: a focusable tabpanel, as in the APG tabs pattern and TabsContent.\n      tabIndex={0}\n      data-slot=\"page-tabs-panel\"\n      data-state={selected ? \"active\" : \"inactive\"}\n      className={cn(\n        \"min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-panel-${slug(value)}`}\n      aria-labelledby={`${context.id}-tab-${slug(value)}`}\n      ref={ref}\n      hidden={!selected}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/page-tabs.tsx"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}