{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "type": "registry:component",
  "title": "Tabs",
  "description": "A few views of one thing, one at a time, chosen from a row of tabs whose marker slides to the one chosen.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/tabs.tsx",
      "type": "registry:component",
      "target": "@components/panelui/tabs.tsx",
      "content": "'use client';\n\n/**\n * Tabs — a few views of one thing, one at a time, chosen from a row of tabs.\n *\n * ```tsx\n * <Tabs defaultValue=\"overview\">\n *   <Tabs.List>\n *     <Tabs.Trigger value=\"overview\">Overview</Tabs.Trigger>\n *     <Tabs.Trigger value=\"activity\">\n *       Activity <Tabs.Badge>12</Tabs.Badge>\n *     </Tabs.Trigger>\n *   </Tabs.List>\n *   <Tabs.Content value=\"overview\">…</Tabs.Content>\n *   <Tabs.Content value=\"activity\">…</Tabs.Content>\n * </Tabs>\n * ```\n *\n * ## Three weights\n *\n * `segmented` sets the tabs in a track with the chosen one raised out of it:\n * a control for switching what a panel shows. `underline` is lighter, a line\n * under the chosen tab, for the sections of a page. `pills` fills the chosen\n * tab with the primary colour, for a filter that should be seen from across\n * the screen.\n *\n * ## It moves the way the hand did\n *\n * The marker slides to the new tab. The underline stretches as it goes, its\n * leading end arriving first and the trailing end catching up, so a long jump\n * reads as one movement and not as a line teleporting. The new panel slides\n * in from the side the tab was on, so going right looks like going right.\n *\n * ## A row that does not fit scrolls\n *\n * When the tabs are wider than the space for them, the row scrolls sideways\n * and fades at the end that has more, rather than wrapping onto a second line\n * or shrinking the labels.\n */\nimport { Tabs as BaseTabs } from '@base-ui/react/tabs';\nimport { createContext, useContext, useEffect, useRef, type ComponentProps } from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type TabsVariant = 'segmented' | 'underline' | 'pills';\n\n/** One curve for the marker and the panel, so they move as one thing. */\nconst EASE = 'ease-[cubic-bezier(0.3,0.7,0.1,1)]';\n\nconst tabsVariants = tv({\n  slots: {\n    root: 'group/tabs flex flex-col gap-4 data-[orientation=vertical]:flex-row',\n    list: [\n      'relative z-0 flex shrink-0 items-center',\n      'data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch',\n      // Scrolls rather than wraps, with no bar of its own; the fade says there is more.\n      'data-[orientation=horizontal]:max-w-full data-[orientation=horizontal]:overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',\n      'data-overflow-start:[--fade-start:3rem] data-overflow-end:[--fade-end:3rem]',\n      '[mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start,0px),#000_calc(100%-var(--fade-end,0px)),transparent)]',\n    ],\n    trigger: [\n      'relative z-10 inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-medium outline-none select-none',\n      'text-muted-foreground transition-colors duration-200 hover:text-foreground data-active:text-foreground',\n      'data-disabled:pointer-events-none data-disabled:opacity-50',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n      'group-data-[orientation=vertical]/tabs:justify-start',\n    ],\n    indicator: 'absolute -z-10',\n    badge: [\n      'inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-medium tabular-nums',\n      'bg-foreground/[0.07] text-muted-foreground transition-colors duration-200',\n      'in-data-active:bg-foreground/10 in-data-active:text-foreground',\n    ],\n    content: [\n      'min-w-0 flex-1 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n      // The new panel arrives from the side its tab was on. The old one goes at once.\n      `transition-[opacity,translate] duration-300 ${EASE} motion-reduce:transition-none`,\n      'data-starting-style:opacity-0',\n      'data-[activation-direction=right]:data-starting-style:translate-x-3 data-[activation-direction=left]:data-starting-style:-translate-x-3',\n      'data-[activation-direction=down]:data-starting-style:translate-y-2 data-[activation-direction=up]:data-starting-style:-translate-y-2',\n      'data-ending-style:hidden',\n    ],\n  },\n  variants: {\n    variant: {\n      segmented: {\n        list: 'w-fit gap-0.5 rounded-xl bg-muted p-1',\n        trigger: 'h-8 rounded-lg px-3',\n        indicator: [\n          'top-(--active-tab-top) left-(--active-tab-left) h-(--active-tab-height) w-(--active-tab-width) rounded-lg',\n          'bg-background shadow-sm ring-1 ring-foreground/[0.06] dark:bg-input dark:ring-foreground/10',\n          `transition-[left,top,width,height] duration-300 ${EASE} motion-reduce:transition-none`,\n        ],\n      },\n      underline: {\n        list: [\n          'gap-1 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:shadow-[inset_0_-1px_0_var(--border)]',\n          'data-[orientation=vertical]:shadow-[inset_-1px_0_0_var(--border)]',\n        ],\n        trigger: [\n          'h-10 px-3',\n          'group-data-[orientation=vertical]/tabs:h-9 group-data-[orientation=vertical]/tabs:pe-5',\n          // The hover fill sits inside the row, clear of the line.\n          \"before:absolute before:inset-x-0.5 before:inset-y-1.5 before:-z-10 before:rounded-md before:transition-colors before:content-[''] hover:before:bg-muted/70\",\n        ],\n        indicator: [\n          'bg-foreground',\n          // The two ends move separately: the one going first is quick and the\n          // other follows, so the line stretches on the way and settles.\n          'data-[orientation=horizontal]:bottom-0 data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:rounded-t-full',\n          'data-[orientation=horizontal]:left-(--active-tab-left) data-[orientation=horizontal]:right-(--active-tab-right)',\n          'data-[orientation=vertical]:right-0 data-[orientation=vertical]:w-0.5 data-[orientation=vertical]:rounded-s-full',\n          'data-[orientation=vertical]:top-(--active-tab-top) data-[orientation=vertical]:bottom-(--active-tab-bottom)',\n          `transition-[left,right,top,bottom] ${EASE} motion-reduce:transition-none`,\n          '[transition-duration:300ms]',\n          'data-[activation-direction=right]:[transition-duration:420ms,200ms] data-[activation-direction=left]:[transition-duration:200ms,420ms]',\n          'data-[activation-direction=down]:[transition-duration:0ms,0ms,420ms,200ms] data-[activation-direction=up]:[transition-duration:0ms,0ms,200ms,420ms]',\n        ],\n      },\n      pills: {\n        list: 'w-fit gap-1',\n        trigger:\n          'h-8 rounded-full px-3.5 hover:bg-muted data-active:text-primary-foreground data-active:hover:bg-transparent',\n        badge: 'in-data-active:bg-primary-foreground/20 in-data-active:text-primary-foreground',\n        indicator: [\n          'top-(--active-tab-top) left-(--active-tab-left) h-(--active-tab-height) w-(--active-tab-width) rounded-full bg-primary',\n          `transition-[left,top,width,height] duration-300 ${EASE} motion-reduce:transition-none`,\n        ],\n      },\n    },\n  },\n  compoundVariants: [\n    // A vertical segmented list is a column of full-width rows.\n    { variant: 'segmented', className: { list: 'data-[orientation=vertical]:w-48' } },\n    { variant: 'pills', className: { list: 'data-[orientation=vertical]:w-48' } },\n  ],\n  defaultVariants: { variant: 'segmented' },\n});\n\nexport interface TabsProps extends Omit<BaseTabs.Root.Props, 'className'> {\n  /** How the tabs are drawn. Default `segmented`. */\n  variant?: TabsVariant;\n  className?: string;\n}\n\n/**\n * Holds which tab is chosen: `defaultValue`, or `value` with `onValueChange`.\n * `orientation=\"vertical\"` puts the tabs in a column beside the content.\n */\nfunction TabsRoot({ variant = 'segmented', className, ...props }: TabsProps) {\n  return (\n    <VariantContext.Provider value={variant}>\n      <BaseTabs.Root\n        data-slot=\"tabs\"\n        data-variant={variant}\n        className={tabsVariants({ variant }).root({ className })}\n        {...props}\n      />\n    </VariantContext.Provider>\n  );\n}\nTabsRoot.displayName = 'Tabs';\n\nconst VariantContext = createContext<TabsVariant>('segmented');\n\nexport interface TabsListProps extends Omit<BaseTabs.List.Props, 'className'> {\n  className?: string;\n}\n\n/** The row of tabs, with the marker under the chosen one. */\nfunction TabsList({ className, children, ...props }: TabsListProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const slots = tabsVariants({ variant: useContext(VariantContext) });\n\n  // Marks the ends that have more to scroll to, for the fade.\n  useEffect(() => {\n    const list = ref.current;\n    if (!list) return;\n    const read = () => {\n      const start = list.scrollLeft > 1;\n      const end = list.scrollLeft + list.clientWidth < list.scrollWidth - 1;\n      list.toggleAttribute('data-overflow-start', start);\n      list.toggleAttribute('data-overflow-end', end);\n    };\n    read();\n    list.addEventListener('scroll', read, { passive: true });\n    const observer = new ResizeObserver(read);\n    observer.observe(list);\n    return () => {\n      list.removeEventListener('scroll', read);\n      observer.disconnect();\n    };\n  }, []);\n\n  return (\n    <BaseTabs.List ref={ref} data-slot=\"tabs-list\" className={slots.list({ className })} {...props}>\n      {children}\n      <BaseTabs.Indicator data-slot=\"tabs-indicator\" className={slots.indicator()} renderBeforeHydration />\n    </BaseTabs.List>\n  );\n}\nTabsList.displayName = 'Tabs.List';\n\nexport interface TabsTriggerProps extends Omit<BaseTabs.Tab.Props, 'className'> {\n  className?: string;\n}\n\n/**\n * One tab. Its `value` names the `Tabs.Content` it shows. An icon and a\n * `Tabs.Badge` can sit beside its label.\n */\nfunction TabsTrigger({ className, ...props }: TabsTriggerProps) {\n  const slots = tabsVariants({ variant: useContext(VariantContext) });\n  return <BaseTabs.Tab data-slot=\"tabs-trigger\" className={slots.trigger({ className })} {...props} />;\n}\nTabsTrigger.displayName = 'Tabs.Trigger';\n\nexport interface TabsBadgeProps extends ComponentProps<'span'> {}\n\n/** A count beside a tab's label: unread, open, waiting. */\nfunction TabsBadge({ className, render, ...props }: TabsBadgeProps & RenderProp<'span'>) {\n  const slots = tabsVariants({ variant: useContext(VariantContext) });\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps({ 'data-slot': 'tabs-badge', className: slots.badge({ className }) }, props),\n  });\n}\nTabsBadge.displayName = 'Tabs.Badge';\n\nexport interface TabsContentProps extends Omit<BaseTabs.Panel.Props, 'className'> {\n  className?: string;\n}\n\n/** What one tab shows. Only the chosen one is in the page unless `keepMounted` is set. */\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n  return <BaseTabs.Panel data-slot=\"tabs-content\" className={tabsVariants().content({ className })} {...props} />;\n}\nTabsContent.displayName = 'Tabs.Content';\n\nexport const Tabs = Object.assign(TabsRoot, {\n  List: TabsList,\n  Trigger: TabsTrigger,\n  Badge: TabsBadge,\n  Content: TabsContent,\n});\n\nexport { TabsList, TabsTrigger, TabsBadge, TabsContent };\n"
    }
  ]
}
