{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "title": "Tabs",
  "description": "Tabs with a gliding indicator (underline or pill) and panels that cross-fade 6px in the direction of travel while their container's height springs. Base UI Tabs behaviour.",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-indicator.json",
    "https://eased.dev/r/eased-auto-height.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/tabs/tabs.tsx",
      "content": "\"use client\";\n\nimport { Tabs as BaseTabs } from \"@base-ui/react/tabs\";\nimport { motion } from \"motion/react\";\nimport { cloneElement, createContext, use, useRef, type ComponentProps, type ReactElement, type ReactNode } from \"react\";\nimport { useScrollFades } from \"../lib/dom\";\nimport { AutoHeight } from \"../_primitives/auto-height/auto-height\";\nimport { Indicator, IndicatorGroup } from \"../_primitives/indicator/indicator\";\n\ntype Variant = \"underline\" | \"pill\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\nconst TabsContext = createContext<{ variant: Variant; orientation: Orientation }>({\n  variant: \"underline\",\n  orientation: \"horizontal\",\n});\n\nexport type TabsProps = Omit<ComponentProps<typeof BaseTabs.Root>, \"className\" | \"orientation\"> & {\n  /**\n   * `underline`: a 2px bar under the active tab, for page and card sections (default).\n   * `pill`: a filled highlight behind the active tab, for dense toolbars and filters.\n   */\n  variant?: Variant;\n  orientation?: Orientation;\n  className?: string;\n};\n\n/**\n * Tabs with a gliding indicator and panels that cross-fade in the direction of travel.\n * Base UI's Tabs supplies the behaviour: roving focus, arrow keys, Home/End, ARIA wiring.\n * Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).\n */\nexport function Tabs({ variant = \"underline\", orientation = \"horizontal\", className = \"\", ...props }: TabsProps) {\n  return (\n    <TabsContext value={{ variant, orientation }}>\n      <BaseTabs.Root\n        orientation={orientation}\n        className={`min-w-0 ${orientation === \"vertical\" ? \"flex gap-6\" : \"\"} ${className}`}\n        {...props}\n      />\n    </TabsContext>\n  );\n}\n\nexport type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, \"className\" | \"render\"> & {\n  className?: string;\n};\n\n/**\n * The row of tabs. It scrolls sideways (with edge fades) when the tabs outgrow it, so it\n * never forces the page wider at 360px. Arrow keys move focus; Enter or Space activates,\n * unless you pass `activateOnFocus`.\n */\nexport function TabsList({ className = \"\", children, ...props }: TabsListProps) {\n  const { variant, orientation } = use(TabsContext);\n  const ref = useRef<HTMLDivElement>(null);\n  useScrollFades(ref, \"x\");\n  const vertical = orientation === \"vertical\";\n  const underline = variant === \"underline\";\n  // The rule lives on a wrapper, outside the scroller, so it never scrolls or clips; the\n  // list's -m-1 p-1 gives focus rings room inside the scroll container's clip.\n  return (\n    <div\n      className={`relative isolate min-w-0 shrink-0 ${\n        underline\n          ? `after:absolute after:-z-10 after:bg-line-subtle ${vertical ? \"after:inset-y-0 after:left-0 after:w-px\" : \"after:inset-x-0 after:bottom-0 after:h-px\"}`\n          : \"\"\n      } ${className}`}\n    >\n      <BaseTabs.List\n        ref={ref}\n        render={<motion.div layoutScroll />}\n        className={`relative -m-1 flex p-1 ${\n          vertical\n            ? `flex-col ${underline ? \"\" : \"gap-0.5\"}`\n            : `fade-x overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${underline ? \"gap-5\" : \"gap-0.5\"}`\n        }`}\n        {...props}\n      >\n        <IndicatorGroup>{children}</IndicatorGroup>\n      </BaseTabs.List>\n    </div>\n  );\n}\n\nexport type TabProps = Omit<ComponentProps<typeof BaseTabs.Tab>, \"className\" | \"render\" | \"children\"> & {\n  children: ReactNode;\n  /**\n   * Render the tab as another element, e.g. a Next `<Link href=\"/settings/domains\" />` for routed\n   * tabs. The indicator, icon and label go inside it. Non-button elements set `nativeButton` false\n   * for you.\n   */\n  render?: ReactElement;\n  /** A 16px icon before the label. */\n  icon?: ReactNode;\n  className?: string;\n};\n\nconst TAB = {\n  underline: {\n    horizontal: \"h-control-lg px-0.5\",\n    vertical: \"h-control-md pl-4 pr-2 justify-start\",\n    indicator: {\n      horizontal: \"inset-x-0 bottom-0 h-0.5 rounded-pill bg-text\",\n      vertical: \"inset-y-1.5 left-0 w-0.5 rounded-pill bg-text\",\n    },\n  },\n  pill: {\n    horizontal: \"h-control-sm rounded-control-sm px-3\",\n    vertical: \"h-control-sm rounded-control-sm px-3 justify-start\",\n    indicator: {\n      horizontal: \"inset-0 rounded-[inherit] bg-surface-active\",\n      vertical: \"inset-0 rounded-[inherit] bg-surface-active\",\n    },\n  },\n} as const;\n\n/** One tab. Its label never changes weight; the active one darkens and stops looking clickable. */\nexport function Tab({ children, icon, render, nativeButton, className = \"\", ...props }: TabProps) {\n  const { variant, orientation } = use(TabsContext);\n  const t = TAB[variant];\n  return (\n    <BaseTabs.Tab\n      {...props}\n      nativeButton={nativeButton ?? (render === undefined || render.type === \"button\")}\n      render={(htmlProps, state) => {\n        const content = (\n          <>\n            {state.active && <Indicator className={t.indicator[orientation]} />}\n            {icon && <span className=\"relative flex size-4 shrink-0 items-center justify-center [&_svg]:size-4\">{icon}</span>}\n            <span className=\"relative\">{children}</span>\n          </>\n        );\n        return render ? (\n          cloneElement(render, htmlProps, content)\n        ) : (\n          <button type=\"button\" {...htmlProps}>\n            {content}\n          </button>\n        );\n      }}\n      className={(state) =>\n        `relative flex shrink-0 items-center justify-center gap-2 text-ui font-medium whitespace-nowrap select-none transition-colors duration-150 ease-standard focus-ring ${\n          t[orientation]\n        } ${\n          state.disabled\n            ? \"cursor-not-allowed text-text-disabled\"\n            : state.active\n              ? \"cursor-default text-text\"\n              : `cursor-pointer text-text-muted hover-fine:text-text-secondary ${variant === \"pill\" ? \"hover-fine:bg-surface-hover\" : \"\"}`\n        } ${className}`\n      }\n    />\n  );\n}\n\nexport type TabsPanelsProps = {\n  children: ReactNode;\n  /** Classes for the clipping root, e.g. a margin above the panels. */\n  className?: string;\n};\n\n/**\n * Holds the panels. Its height springs between panels of different lengths while the old\n * and new panels cross-fade over each other in one grid cell.\n */\nexport function TabsPanels({ children, className = \"\" }: TabsPanelsProps) {\n  const { orientation } = use(TabsContext);\n  return (\n    <AutoHeight fade={false} className={`min-w-0 ${orientation === \"vertical\" ? \"flex-1\" : \"\"} ${className}`} innerClassName=\"grid\">\n      {children}\n    </AutoHeight>\n  );\n}\n\nexport type TabsPanelProps = Omit<ComponentProps<typeof BaseTabs.Panel>, \"className\"> & { className?: string };\n\n/**\n * A panel. It enters from 6px on the side of the tab you came from and leaves 3px the other\n * way, faster than it arrived. Reduced motion: an opacity cross-fade only.\n */\nexport function TabsPanel({ className = \"\", ...props }: TabsPanelProps) {\n  return (\n    <BaseTabs.Panel\n      className={`col-start-1 row-start-1 min-w-0 rounded-control-xs focus-ring [transition:opacity_var(--eased-duration-standard)_var(--eased-ease-enter),translate_var(--eased-duration-spring-snappy)_var(--eased-spring-snappy)] data-ending-style:duration-(--eased-duration-fast) data-ending-style:ease-standard data-starting-style:opacity-0 data-ending-style:opacity-0 data-starting-style:data-[activation-direction=left]:-translate-x-1.5 data-starting-style:data-[activation-direction=right]:translate-x-1.5 data-ending-style:data-[activation-direction=left]:translate-x-0.75 data-ending-style:data-[activation-direction=right]:-translate-x-0.75 data-starting-style:data-[activation-direction=up]:-translate-y-1.5 data-starting-style:data-[activation-direction=down]:translate-y-1.5 data-ending-style:data-[activation-direction=up]:translate-y-0.75 data-ending-style:data-[activation-direction=down]:-translate-y-0.75 reduced:translate-none! reduced:duration-(--eased-duration-instant)! ${className}`}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/tabs/tabs.tsx"
    }
  ],
  "docs": "Import Tabs, TabsList, Tab, TabsPanels and TabsPanel from components/eased/tabs/tabs. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}