{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:ui","title":"Tabs","description":"Switch between related views without leaving the page.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json"],"files":[{"path":"registry/dashral/tabs.tsx","type":"registry:file","target":"components/dashral/tabs.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { cn } from \"./lib/utils\"\n\ntype TabItem = {\n  value: string\n  label: React.ReactNode\n  icon?: React.ReactNode\n  /** After the label, e.g. a count. */\n  badge?: React.ReactNode\n  disabled?: boolean\n  /** The panel shown while this tab is picked. Without any, Tabs is just the row, a switcher for content of your own. */\n  content?: React.ReactNode\n}\n\ntype TabsProps = Omit<React.ComponentProps<typeof TabsPrimitive.Root>, \"children\"> & {\n  items: TabItem[]\n  /** default: a raised card on a muted track; line: plain tabs with an underline, for the top of a page or a panel. */\n  variant?: \"default\" | \"line\"\n  /** The tab row fills the width, tabs sharing it equally. */\n  fullWidth?: boolean\n  /** At the end of the tab row, e.g. buttons that act on the current tab. */\n  listAction?: React.ReactNode\n  /** More content in the tabs, under the panels — e.g. one view that changes with the picked tab. */\n  children?: React.ReactNode\n  classNames?: { header?: string; list?: string; trigger?: string; content?: string }\n}\n\n/**\n * Tabs in one tag: items with a label (and an icon or badge) and the panel each shows. The picked tab's card or\n * underline glides to the tab you pick (it just moves with reduced motion). Opens on the first tab.\n * `orientation=\"vertical\"`: the tabs in a column beside their panel (↑ ↓ move between them), the card or a line at\n * their start edge gliding up and down; stack them above the panel on narrow screens with `className`.\n */\nfunction Tabs({ items, variant = \"default\", fullWidth, listAction, defaultValue, orientation = \"horizontal\", className, classNames, children, ...props }: TabsProps) {\n  const vertical = orientation === \"vertical\"\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      orientation={orientation}\n      defaultValue={props.value === undefined ? (defaultValue ?? items[0]?.value) : undefined}\n      className={cn(\"flex flex-col gap-2\", vertical && \"flex-row gap-6\", className)}\n      {...props}\n    >\n      {/* The action wraps below the tabs when they don't fit side by side (a phone); in a column it sits under them. */}\n      <div\n        data-slot=\"tabs-header\"\n        className={cn(\"flex flex-wrap items-center justify-between gap-x-3 gap-y-2\", vertical && \"flex-col flex-nowrap items-stretch\", classNames?.header)}\n      >\n        <TabsList variant={variant} className={cn(fullWidth && \"w-full\", classNames?.list)}>\n          {items.map((item) => (\n            <TabsTrigger key={item.value} value={item.value} disabled={item.disabled} className={classNames?.trigger}>\n              {item.icon}\n              {item.label}\n              {item.badge}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n        {listAction}\n      </div>\n      {items.map((item) =>\n        item.content === undefined ? null : (\n          <TabsContent key={item.value} value={item.value} className={classNames?.content}>\n            {item.content}\n          </TabsContent>\n        )\n      )}\n      {children}\n    </TabsPrimitive.Root>\n  )\n}\n\n/**\n * The tab row. `default`: the selected tab is a raised card on a muted track. `line`: plain tabs with an underline,\n * for the top of a page or a panel. Either way the card or line slides to the tab you pick (it just moves with\n * reduced motion), easing to a soft stop.\n */\nfunction TabsList({\n  className,\n  children,\n  variant = \"default\",\n  ref: forwardedRef,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> & { variant?: \"default\" | \"line\" }) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const [thumb, setThumb] = React.useState<{ x: number; y: number; width: number; height: number } | null>(null)\n\n  const [vertical, setVertical] = React.useState(false)\n  const [overflowing, setOverflowing] = React.useState(false)\n\n  // Measure the selected tab so the card can move to it; again when the row resizes or the selection changes.\n  React.useLayoutEffect(() => {\n    const list = ref.current\n    if (!list) return\n    const measure = () => {\n      const tab = list.querySelector<HTMLElement>(\":scope > [data-slot=tabs-trigger][data-state=active]\")\n      // Radix marks the list with its orientation.\n      setVertical(list.dataset.orientation === \"vertical\")\n      setThumb(tab ? { x: tab.offsetLeft, y: tab.offsetTop, width: tab.offsetWidth, height: tab.offsetHeight } : null)\n      // A row wider than its space scrolls, and keeps the picked tab in sight (only the row moves, never the page).\n      const wide = list.dataset.orientation !== \"vertical\" && list.scrollWidth > list.clientWidth + 1\n      setOverflowing(wide)\n      if (tab && wide) {\n        if (tab.offsetLeft < list.scrollLeft) list.scrollLeft = tab.offsetLeft\n        else if (tab.offsetLeft + tab.offsetWidth > list.scrollLeft + list.clientWidth) list.scrollLeft = tab.offsetLeft + tab.offsetWidth - list.clientWidth\n      }\n    }\n    measure()\n    const resize = new ResizeObserver(measure)\n    resize.observe(list)\n    const changes = new MutationObserver(measure)\n    changes.observe(list, { attributes: true, subtree: true, attributeFilter: [\"data-state\"], childList: true })\n    return () => {\n      resize.disconnect()\n      changes.disconnect()\n    }\n  }, [])\n\n  return (\n    <TabsPrimitive.List\n      ref={(node) => {\n        ref.current = node\n        if (typeof forwardedRef === \"function\") forwardedRef(node)\n        else if (forwardedRef) forwardedRef.current = node\n      }}\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      data-measured={thumb ? \"\" : undefined}\n      data-overflowing={overflowing ? \"\" : undefined}\n      className={cn(\n        \"relative inline-flex w-fit items-center justify-center text-muted-foreground\",\n        // Wider than its space (a phone): the row scrolls sideways, without a scrollbar, instead of running out of it. Only\n        // then, so a row that fits keeps its focus rings unclipped.\n        \"max-w-full data-overflowing:justify-start data-overflowing:overflow-x-auto data-overflowing:[scrollbar-width:none] data-overflowing:[&::-webkit-scrollbar]:hidden\",\n        variant === \"default\" ? \"h-9 rounded-lg bg-muted p-[3px]\" : \"h-10 gap-5\",\n        // A column: the tabs stacked and as wide as the widest; line tabs on a hairline at their start edge.\n        \"data-[orientation=vertical]:h-auto data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[orientation=vertical]:justify-start\",\n        variant === \"line\" && \"data-[orientation=vertical]:gap-0 data-[orientation=vertical]:border-s data-[orientation=vertical]:border-border\",\n        className\n      )}\n      {...props}\n    >\n      {thumb && (\n        <span\n          aria-hidden\n          data-slot=\"tabs-thumb\"\n          className={cn(\n            \"absolute top-0 transition-[translate,width,height] duration-(--duration-considered) ease-standard motion-reduce:transition-none\",\n            // A vertical line sits on the list's hairline at the start edge (right in RTL); the rest go by offsets.\n            variant === \"line\" && vertical ? \"-start-px\" : \"left-0\",\n            variant === \"default\"\n              ? \"rounded-md border border-border bg-card shadow-sm dark:border-transparent dark:bg-input\"\n              : \"rounded-full bg-foreground\"\n          )}\n          style={\n            variant === \"default\"\n              ? { translate: `${thumb.x}px ${thumb.y}px`, width: thumb.width, height: thumb.height }\n              : vertical\n                ? { translate: `0 ${thumb.y}px`, width: 2, height: thumb.height }\n                : { translate: `${thumb.x}px ${thumb.y + thumb.height - 2}px`, width: thumb.width, height: 2 }\n          }\n        />\n      )}\n      {children}\n    </TabsPrimitive.List>\n  )\n}\n\n// The selected tab is a raised card on the muted track: white with an edge and a shadow in light mode, a lighter\n// fill in dark mode. (The page background is nearly as light as the track, so it can't be the selected color.) Once\n// the list has measured it, the card is TabsList's sliding thumb; before that (server render), the tab draws its own.\nfunction TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"relative z-10 inline-flex h-full flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 hover:text-foreground data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        // Card tabs: before the thumb is measured, the selected tab draws its own card.\n        \"in-data-[variant=default]:not-in-data-measured:data-[state=active]:border-border in-data-[variant=default]:not-in-data-measured:data-[state=active]:bg-card in-data-[variant=default]:not-in-data-measured:data-[state=active]:shadow-sm dark:in-data-[variant=default]:not-in-data-measured:data-[state=active]:border-transparent dark:in-data-[variant=default]:not-in-data-measured:data-[state=active]:bg-input\",\n        // Line tabs: no padding at the sides, and the same fallback as an underline.\n        \"in-data-[variant=line]:flex-none in-data-[variant=line]:rounded-none in-data-[variant=line]:px-0.5 in-data-[variant=line]:not-in-data-measured:data-[state=active]:shadow-[inset_0_-2px_0_var(--foreground)]\",\n        // In a column: as wide as the list, the label at the start and free to wrap; line tabs indented from their line,\n        // whose fallback is at the start edge instead of underneath.\n        \"data-[orientation=vertical]:h-auto data-[orientation=vertical]:justify-start data-[orientation=vertical]:py-1.5 data-[orientation=vertical]:text-start data-[orientation=vertical]:whitespace-normal\",\n        \"in-data-[variant=line]:data-[orientation=vertical]:py-2 in-data-[variant=line]:data-[orientation=vertical]:ps-4 in-data-[variant=line]:data-[orientation=vertical]:pe-1 in-data-[variant=line]:not-in-data-measured:data-[orientation=vertical]:data-[state=active]:shadow-[inset_2px_0_0_var(--foreground)] rtl:in-data-[variant=line]:not-in-data-measured:data-[orientation=vertical]:data-[state=active]:shadow-[inset_-2px_0_0_var(--foreground)]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\n        \"flex-1 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:blur-in-[3px] data-[state=active]:duration-(--duration-standard) data-[state=active]:ease-enter motion-reduce:data-[state=active]:blur-in-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Tabs, type TabsProps, type TabItem }\n"}],"cssVars":{"theme":{"ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{}}}