{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "description": "Views of the same content, with an indicator that slides to the active tab.",
  "dependencies": [
    "@base-ui/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/tabs.tsx",
      "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { createContext, use, useMemo } from \"react\";\n\nimport type { Sx } from \"@/lib/constella/styles\";\nimport { typeStyles } from \"@/lib/constella/styles\";\nimport {\n  colors,\n  durations,\n  easings,\n  fontWeights,\n  media,\n  opacities,\n  radii,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\nconst tabsListVariants = [\"default\", \"line\"] as const;\nconst tabsListSizes = [\"sm\", \"default\", \"lg\"] as const;\n\ntype TabsListVariant = (typeof tabsListVariants)[number];\ntype TabsListSize = (typeof tabsListSizes)[number];\n\ntype Styled<T> = Omit<T, \"className\" | \"style\"> & {\n  sx?: Sx;\n};\n\ntype TabsListProps = Styled<TabsPrimitive.List.Props> & {\n  size?: TabsListSize;\n  variant?: TabsListVariant;\n};\n\nconst vertical = \":is([data-orientation='vertical'])\";\n\nconst besideColumn = \":is([data-slot='tabs'][data-orientation='vertical'] > *)\";\n\nconst styles = stylex.create({\n  tabs: {\n    display: \"flex\",\n    flexDirection: { default: \"column\", [vertical]: \"row\" },\n    flexWrap: { default: null, [vertical]: \"wrap\" },\n    gap: space.sm,\n  },\n  // Beside a column of tabs while it has a menu's width, under it once it hasn't.\n  panel: {\n    flexBasis: { default: null, [besideColumn]: 0 },\n    flexGrow: { default: null, [besideColumn]: 1 },\n    minWidth: { default: null, [besideColumn]: sizes.menu },\n  },\n  // A row too long for its container scrolls; arrow keys bring the next tab into view.\n  list: {\n    display: \"flex\",\n    flexDirection: { default: \"row\", [vertical]: \"column\" },\n    flexShrink: 0,\n    maxWidth: \"100%\",\n    overflowX: { default: \"auto\", [vertical]: \"visible\" },\n    position: \"relative\",\n    scrollbarWidth: { default: \"none\", [vertical]: \"auto\" },\n    width: \"fit-content\",\n  },\n  // A tinted pill slides between tabs, with no track around them.\n  listDefault: {\n    gap: 0,\n  },\n  // An accent line slides along a faint baseline: under the row, beside the column.\n  // The row's baseline is an inset shadow in its padding, not a border, so the scrolling list\n  // doesn't clip the indicator that covers it.\n  listLine: {\n    boxShadow: {\n      default: `inset 0 calc(-1 * ${strokes.border}) ${colors.borderSubtle}`,\n      [vertical]: \"none\",\n    },\n    paddingBlockEnd: { default: strokes.border, [vertical]: 0 },\n    borderInlineStartColor: colors.borderSubtle,\n    borderInlineStartStyle: { default: \"none\", [vertical]: \"solid\" },\n    borderInlineStartWidth: { default: 0, [vertical]: strokes.border },\n  },\n  tab: {\n    alignItems: \"center\",\n    backgroundColor: \"transparent\",\n    borderStartStartRadius: radii.full,\n    borderStartEndRadius: radii.full,\n    borderEndStartRadius: radii.full,\n    borderEndEndRadius: radii.full,\n    borderBlockEndWidth: 0,\n    borderBlockStartWidth: 0,\n    borderInlineEndWidth: 0,\n    borderInlineStartWidth: 0,\n    color: {\n      default: colors.textSecondary,\n      \":is([data-active])\": colors.textPrimary,\n      [media.hover]: {\n        default: colors.textSecondary,\n        \":hover:not([data-disabled])\": colors.textPrimary,\n        \":is([data-active])\": colors.textPrimary,\n      },\n      \":active:not([data-disabled])\": colors.textPrimary,\n    },\n    cursor: { default: \"pointer\", \":is([data-disabled])\": \"not-allowed\" },\n    display: \"inline-flex\",\n    flexGrow: { default: 1, [vertical]: 0 },\n    fontWeight: fontWeights.semimedium,\n    gap: space.xs,\n    height: sizes.controlSm,\n    justifyContent: { default: \"center\", [vertical]: \"flex-start\" },\n    opacity: { default: 1, \":is([data-disabled])\": opacities.disabled },\n    paddingInline: space.sm,\n    position: \"relative\",\n    transitionDuration: durations.hover,\n    transitionProperty: \"color\",\n    transitionTimingFunction: \"ease\",\n    userSelect: \"none\",\n    WebkitTapHighlightColor: \"transparent\",\n    whiteSpace: \"nowrap\",\n    zIndex: 1,\n  },\n  tabSm: {\n    gap: space.xxs,\n    height: sizes.controlXs,\n    paddingInline: space.popup,\n  },\n  tabLg: {\n    height: sizes.controlMd,\n  },\n  // Line tabs sit on the baseline, so they take the list's height, not a pill's corners.\n  tabLine: {\n    borderStartStartRadius: 0,\n    borderStartEndRadius: 0,\n    borderEndStartRadius: 0,\n    borderEndEndRadius: 0,\n  },\n  // Base UI measures from the physical left, so the indicator anchors there too.\n  indicator: {\n    left: 0,\n    position: \"absolute\",\n    top: 0,\n    // Arrow keys move focus visibly and a click doesn't, so only the pointer slides it.\n    transitionDuration: {\n      default: durations.move,\n      \":is([data-slot='tabs-list']:has(:focus-visible) > *)\": \"0s\",\n      [media.reducedMotion]: \"0s\",\n    },\n    transitionProperty: \"transform, width, height\",\n    transitionTimingFunction: easings.inOut,\n  },\n  indicatorDefault: {\n    backgroundColor: colors.fill,\n    borderStartStartRadius: radii.full,\n    borderStartEndRadius: radii.full,\n    borderEndStartRadius: radii.full,\n    borderEndEndRadius: radii.full,\n    height: \"var(--active-tab-height)\",\n    transform: \"translate(var(--active-tab-left), var(--active-tab-top))\",\n    width: \"var(--active-tab-width)\",\n  },\n  // Over the baseline, so the active tab's line replaces it.\n  indicatorLine: {\n    backgroundColor: colors.accentSolid,\n    height: {\n      default: strokes.indicator,\n      [vertical]: \"var(--active-tab-height)\",\n    },\n    insetInlineStart: {\n      default: null,\n      [vertical]: `calc(-1 * ${strokes.border})`,\n    },\n    left: { default: 0, [vertical]: null },\n    top: {\n      default: `calc(100% - ${strokes.indicator})`,\n      [vertical]: 0,\n    },\n    transform: {\n      default: \"translateX(var(--active-tab-left))\",\n      [vertical]: \"translateY(var(--active-tab-top))\",\n    },\n    width: {\n      default: \"var(--active-tab-width)\",\n      [vertical]: strokes.indicator,\n    },\n  },\n});\n\nconst listVariantStyles = {\n  default: styles.listDefault,\n  line: styles.listLine,\n} satisfies Record<TabsListVariant, stylex.StyleXStyles>;\n\nconst indicatorVariantStyles = {\n  default: styles.indicatorDefault,\n  line: styles.indicatorLine,\n} satisfies Record<TabsListVariant, stylex.StyleXStyles>;\n\nconst tabSizeStyles = {\n  default: null,\n  lg: styles.tabLg,\n  sm: [typeStyles.textSm, styles.tabSm],\n} satisfies Record<TabsListSize, stylex.StyleXStyles | null>;\n\nconst TabsListContext = createContext<{\n  size: TabsListSize;\n  variant: TabsListVariant;\n}>({ size: \"default\", variant: \"default\" });\n\nfunction Tabs({ sx, ...props }: Styled<TabsPrimitive.Root.Props>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      {...props}\n      {...stylex.props(styles.tabs, sx)}\n    />\n  );\n}\n\n/** Renders the list and the indicator that slides between its tabs. */\nfunction TabsList({\n  children,\n  size = \"default\",\n  sx,\n  variant = \"default\",\n  ...props\n}: TabsListProps) {\n  const context = useMemo(() => ({ size, variant }), [size, variant]);\n  return (\n    <TabsListContext value={context}>\n      <TabsPrimitive.List\n        // Panels show at once, so arrows switch tabs instead of only moving focus.\n        activateOnFocus\n        data-size={size}\n        data-slot=\"tabs-list\"\n        data-variant={variant}\n        {...props}\n        {...stylex.props(styles.list, listVariantStyles[variant], sx)}\n      >\n        {children}\n        <TabsIndicator sx={indicatorVariantStyles[variant]} />\n      </TabsPrimitive.List>\n    </TabsListContext>\n  );\n}\n\nfunction TabsTrigger({ sx, ...props }: Styled<TabsPrimitive.Tab.Props>) {\n  const { size, variant } = use(TabsListContext);\n  return (\n    <TabsPrimitive.Tab\n      data-slot=\"tabs-trigger\"\n      {...props}\n      {...stylex.props(\n        [typeStyles.textSm, styles.tab],\n        tabSizeStyles[size],\n        variant === \"line\" && styles.tabLine,\n        sx\n      )}\n    />\n  );\n}\n\nfunction TabsIndicator({\n  sx,\n  ...props\n}: Styled<TabsPrimitive.Indicator.Props>) {\n  return (\n    <TabsPrimitive.Indicator\n      data-slot=\"tabs-indicator\"\n      renderBeforeHydration\n      {...props}\n      {...stylex.props(styles.indicator, sx)}\n    />\n  );\n}\n\nfunction TabsContent({ sx, ...props }: Styled<TabsPrimitive.Panel.Props>) {\n  return (\n    <TabsPrimitive.Panel\n      data-slot=\"tabs-content\"\n      {...props}\n      {...stylex.props(styles.panel, sx)}\n    />\n  );\n}\n\nexport {\n  Tabs,\n  TabsContent,\n  TabsIndicator,\n  TabsList,\n  tabsListSizes,\n  tabsListVariants,\n  TabsTrigger,\n};\nexport type { TabsListProps, TabsListSize, TabsListVariant };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/tabs/index.md",
  "type": "registry:ui"
}