{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","title":"Tabs","description":"Keyboard-navigable single-selection content panels.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/motion"],"files":[{"path":"packages/ui/src/components/tabs/tabs.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { Button } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\nimport { useLeavingPane } from \"../motion/motion-follow\";\nimport { panelSwitchClassName } from \"../motion/motion-presence\";\nimport { layoutSize, railIndicatorGeometry } from \"./tabs-geometry\";\nimport { useTabsOverflow } from \"./tabs-overflow\";\n\nexport type TabsVariant = \"default\" | \"pill\" | \"underline\";\nconst TabsSizeContext = React.createContext<SiteplaneControlSize | null>(null);\nconst TabsVariantContext = React.createContext<TabsVariant>(\"default\");\n\nexport interface TabsProps extends Omit<\n  WithStringClassName<TabsPrimitive.Root.Props>,\n  \"size\"\n> {\n  size?: SiteplaneControlSize;\n}\nexport function Tabs({\n  className,\n  size,\n  ...props\n}: TabsProps): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  return (\n    <TabsSizeContext.Provider value={resolvedSize}>\n      <TabsPrimitive.Root\n        // The list or rail never shrinks: when the panels sit in the root, it\n        // follows a taller panel's height from below the content, and a list\n        // that could shrink collapsed for a moment (13 October 2026). The list\n        // sits in its frame, which adds no box until its tabs overflow.\n        className={cn(\n          slotClass(\"tabs\", props),\n          \"flex min-w-0 max-w-full flex-col gap-2 data-[orientation=vertical]:flex-row [&>.sp-tabs-list-frame]:shrink-0 [&>.sp-tabs-list-frame>.sp-tabs-list]:shrink-0 [&>.sp-tabs-rail]:shrink-0\",\n          className,\n        )}\n        data-size={resolvedSize}\n        data-slot=\"tabs\"\n        {...props}\n      />\n    </TabsSizeContext.Provider>\n  );\n}\n\n/**\n * Places a rail's selection on its active tab: the target given, or the\n * selected tab. It reads every box before it writes, so a placement forces one\n * layout. Returns whether the target has a size, as Base UI shows its own\n * indicator only then.\n */\nfunction placeRailIndicator(\n  rail: HTMLElement,\n  indicator: HTMLElement,\n  active?: HTMLElement | null,\n): boolean {\n  const target =\n    active ??\n    rail.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]');\n  if (!target) return false;\n  // Scrolled tabs must not paint their selection under the adjacent action.\n  // The strip that shows the tabs is the list, or the scroll viewport of a\n  // ScrollArea that holds the list inside this rail.\n  const list = target.closest('[role=\"tablist\"]');\n  const viewport = list?.closest<HTMLElement>(\n    '[data-slot=\"scroll-area-viewport\"]',\n  );\n  const strip = viewport && rail.contains(viewport) ? viewport : list;\n  // The boxes are read on screen and turned into the rail's own px, so a rail\n  // in a popup that still zooms in places its selection on the tab, not beside\n  // it (tabs-geometry.ts).\n  const place = railIndicatorGeometry({\n    rail: rail.getBoundingClientRect(),\n    railSize: layoutSize(rail),\n    target: target.getBoundingClientRect(),\n    targetSize: layoutSize(target),\n    strip: strip?.getBoundingClientRect(),\n  });\n\n  indicator.style.setProperty(\"--active-tab-left\", `${place.left}px`);\n  indicator.style.setProperty(\"--active-tab-width\", `${place.width}px`);\n  indicator.style.setProperty(\"--active-tab-height\", `${place.height}px`);\n  indicator.parentElement!.style.clipPath = strip\n    ? `inset(0 ${place.clipEnd}px 0 ${place.clipStart}px)`\n    : \"inset(0)\";\n  return place.width > 0 && place.height > 0;\n}\n\n/** Shared rail and selection indicator for tabs composed with an adjacent action. */\nexport function TabsRail({\n  className,\n  children,\n  activeIndicatorRef,\n  ref,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  activeIndicatorRef?: React.RefObject<HTMLElement | null> | undefined;\n  variant?: \"default\" | \"pill\" | undefined;\n}): React.ReactElement {\n  const railRef = React.useRef<HTMLDivElement>(null);\n  const indicatorRef = React.useRef<HTMLDivElement>(null);\n  // As with Base UI's indicator in a list, the rail's indicator stays hidden\n  // until it has a place, and the active tab paints the same selection until\n  // then (design-rework.css, \"pending\"). The server HTML shows the selection\n  // that way, with no inline script; the layout effect below hands it to the\n  // indicator before the first paint after hydration, and the indicator\n  // appears in place, without a transition.\n  const [placed, setPlaced] = React.useState(false);\n  React.useImperativeHandle(ref, () => railRef.current as HTMLDivElement);\n\n  React.useLayoutEffect(() => {\n    const rail = railRef.current;\n    const indicator = indicatorRef.current;\n    if (!rail || !indicator) return;\n\n    function measure() {\n      setPlaced(\n        placeRailIndicator(rail!, indicator!, activeIndicatorRef?.current),\n      );\n    }\n\n    measure();\n    const resize = new ResizeObserver(measure);\n    resize.observe(rail);\n    for (const element of rail.querySelectorAll<HTMLElement>(\n      '[role=\"tab\"], button',\n    )) {\n      resize.observe(element);\n    }\n    const selection = new MutationObserver(measure);\n    selection.observe(rail, {\n      attributes: true,\n      attributeFilter: [\"aria-selected\"],\n      subtree: true,\n    });\n    rail.addEventListener(\"scroll\", measure, true);\n    return () => {\n      resize.disconnect();\n      selection.disconnect();\n      rail.removeEventListener(\"scroll\", measure, true);\n    };\n  }, [activeIndicatorRef]);\n\n  return (\n    <div\n      ref={railRef}\n      className={cn(\n        slotClass(\"tabs-rail\", props),\n        \"relative isolate flex min-w-0 items-center\",\n        className,\n      )}\n      data-indicator={placed ? undefined : \"pending\"}\n      data-slot=\"tabs-rail\"\n      data-variant={variant}\n      {...props}\n    >\n      <div className=\"pointer-events-none absolute inset-0 -z-10 overflow-hidden\">\n        <div\n          ref={indicatorRef}\n          className=\"sp-tab-indicator absolute\"\n          data-slot=\"tab-indicator\"\n          data-variant={variant}\n          hidden={!placed}\n        />\n      </div>\n      {children}\n    </div>\n  );\n}\n\nexport interface TabsListProps extends Omit<\n  WithStringClassName<TabsPrimitive.List.Props>,\n  \"size\"\n> {\n  variant?: TabsVariant;\n  embedded?: boolean;\n  /**\n   * Underline variant: a hairline under all tabs, along their side when\n   * vertical, that the active underline sits on. It spans the list, so a list\n   * widened with a class such as `w-full` carries it further; the tabs stay at\n   * the start.\n   */\n  divider?: boolean;\n  size?: SiteplaneControlSize;\n}\n\n/**\n * The button at one edge of a list whose tabs overflow, shown while more tabs\n * wait in its direction. It is a pointer control: keyboard users move through\n * the tabs with the arrow keys, which scroll the list, so the button stays out\n * of the tab order and of the accessibility tree, and a press keeps the focus\n * where it was. Its wrapper fades it in and out; the Button keeps its own\n * states (the muted inline action, rulebook T2). A touch screen hides it, as\n * the list scrolls by swiping there (design-rework.css).\n */\nfunction TabsScrollButton({\n  direction,\n  onScroll,\n  shown,\n  size,\n  visible,\n}: {\n  direction: \"end\" | \"start\";\n  onScroll: (direction: \"end\" | \"start\") => void;\n  /** The list overflows, so the button holds its place. */\n  shown: boolean;\n  size: SiteplaneControlSize;\n  /** More tabs wait in its direction. */\n  visible: boolean;\n}): React.ReactElement {\n  const Icon = direction === \"start\" ? ChevronLeftIcon : ChevronRightIcon;\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"sp-tabs-scroll-button\"\n      data-direction={direction}\n      data-slot=\"tabs-scroll-button\"\n      data-visible={visible || undefined}\n      hidden={!shown}\n    >\n      <Button\n        onClick={() => onScroll(direction)}\n        onMouseDown={(event) => event.preventDefault()}\n        profile=\"inline\"\n        profileTone=\"muted\"\n        shape=\"square\"\n        size={`icon-${size}`}\n        tabIndex={-1}\n        type=\"button\"\n        variant=\"text\"\n      >\n        <Icon aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />\n      </Button>\n    </span>\n  );\n}\n\nexport function TabsList({\n  divider = false,\n  embedded = false,\n  variant = \"default\",\n  size,\n  className,\n  children,\n  ref,\n  ...props\n}: TabsListProps): React.ReactElement {\n  const groupSize = React.useContext(TabsSizeContext);\n  const resolvedSize = useResolvedControlSize(size ?? groupSize);\n  const listRef = React.useRef<HTMLDivElement | null>(null);\n  const { overflow, scrollToward } = useTabsOverflow(listRef);\n  const setListRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      listRef.current = element;\n      if (typeof ref === \"function\") ref(element);\n      else if (ref) ref.current = element;\n    },\n    [ref],\n  );\n  return (\n    <TabsSizeContext.Provider value={resolvedSize}>\n      <TabsVariantContext.Provider value={variant}>\n        {/* The frame adds no box while the tabs fit. When they overflow, it\n            lays a scroll button on either side of the list, which shows while\n            more tabs wait in its direction; a touch screen swipes the list\n            instead and keeps no room for them (design-rework.css). */}\n        <div\n          className=\"sp-tabs-list-frame\"\n          data-overflow={overflow ? \"\" : undefined}\n          data-slot=\"tabs-list-frame\"\n        >\n          <TabsScrollButton\n            direction=\"start\"\n            onScroll={scrollToward}\n            shown={overflow !== null}\n            size={resolvedSize}\n            visible={overflow?.start ?? false}\n          />\n          <TabsPrimitive.List\n            className={cn(\n              slotClass(\"tabs-list\", props),\n              \"scrollbar-none relative isolate flex w-fit max-w-full items-center overflow-x-auto overflow-y-hidden data-[orientation=vertical]:flex-col data-[orientation=vertical]:overflow-visible\",\n              className,\n            )}\n            data-size={resolvedSize}\n            data-slot=\"tabs-list\"\n            data-divider={divider || undefined}\n            data-embedded={embedded || undefined}\n            data-variant={variant}\n            ref={setListRef}\n            {...props}\n          >\n            {/* Base UI keeps the indicator hidden until it has measured the\n                active tab. Until then the active tab paints the same selection\n                (design-rework.css), so the selection shows from the first paint\n                without an inline script, and the indicator takes over in place\n                once it shows. The indicator leads the list, so that rule reaches\n                the tabs as its siblings. */}\n            {!embedded && (\n              <TabsPrimitive.Indicator\n                className=\"sp-tab-indicator pointer-events-none absolute z-0\"\n                data-slot=\"tab-indicator\"\n                data-variant={variant}\n              />\n            )}\n            {children}\n          </TabsPrimitive.List>\n          <TabsScrollButton\n            direction=\"end\"\n            onScroll={scrollToward}\n            shown={overflow !== null}\n            size={resolvedSize}\n            visible={overflow?.end ?? false}\n          />\n        </div>\n      </TabsVariantContext.Provider>\n    </TabsSizeContext.Provider>\n  );\n}\n\nexport interface TabsTabProps extends Omit<\n  WithStringClassName<TabsPrimitive.Tab.Props>,\n  \"size\"\n> {\n  size?: SiteplaneControlSize;\n}\nexport function TabsTab({\n  className,\n  size,\n  render,\n  ...props\n}: TabsTabProps): React.ReactElement {\n  const groupSize = React.useContext(TabsSizeContext);\n  const variant = React.useContext(TabsVariantContext);\n  const resolvedSize = useResolvedControlSize(size ?? groupSize);\n  return (\n    <TabsPrimitive.Tab\n      className={cn(\n        // Its place in the list (positioned, at its own width, on one line) is\n        // the chassis default, or the kit base for a render of the app's\n        // (design-rework.css).\n        slotClass(\"tabs-tab\", props, render),\n        \"z-10 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      data-profile=\"tabs-tab\"\n      data-profile-mode={variant}\n      data-size={resolvedSize}\n      data-slot=\"tabs-tab\"\n      render={\n        render ?? (\n          <KitButton\n            profile=\"tabs-tab\"\n            profileMode={variant}\n            size={resolvedSize}\n            variant=\"text\"\n          />\n        )\n      }\n      {...props}\n    />\n  );\n}\n\n/** The panel that takes over: the one that is shown and not on its way out. */\nconst enteringPanel = (container: HTMLElement) =>\n  container.querySelector(\n    ':scope > [data-slot=\"tabs-content\"]:not([data-ending-style]):not([hidden])',\n  );\n\n/**\n * Switching tabs shows the new panel at once (rulebook M8): the panel that\n * leaves is hidden the moment the new one shows, and the indicator and the\n * height of the panels' container carry the motion. No moment shows two panels\n * or none. Nothing to set up; a panel with `keepMounted` keeps its state, which\n * a `MotionSwap` would remount, so Base UI keeps mounting the panels and the\n * motion component pins the one that leaves while the container follows.\n */\nexport function TabsPanel({\n  className,\n  render,\n  ...props\n}: WithStringClassName<TabsPrimitive.Panel.Props>): React.ReactElement {\n  return (\n    <TabsPrimitive.Panel\n      // Base UI makes the shown panel a tab stop; its keyboard focus takes the\n      // kit focus outline (design-rework.css).\n      className={cn(\n        slotClass(\"tabs-content\", props),\n        \"flex-1\",\n        panelSwitchClassName,\n        className,\n      )}\n      data-slot=\"tabs-content\"\n      render={(panelProps, state) => (\n        <TabsPanelSwap panelProps={panelProps} render={render} state={state} />\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsPanelSwap({\n  panelProps,\n  render,\n  state,\n}: {\n  panelProps: React.ComponentPropsWithRef<\"div\">;\n  render: TabsPrimitive.Panel.Props[\"render\"];\n  state: TabsPrimitive.Panel.State;\n}): React.ReactElement {\n  const ref = React.useRef<HTMLDivElement>(null);\n  // Base UI keeps the panel mounted while it leaves, `ending` until its fade\n  // has run, and unmounts or hides it after.\n  const leaving = state.transitionStatus === \"ending\";\n  const style = useLeavingPane(ref, {\n    enter: enteringPanel,\n    leaving,\n    open: !state.hidden && !leaving,\n  });\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(panelProps, { style }),\n    ref,\n    render: render as useRender.ComponentProps<\"div\">[\"render\"],\n    state: { ...state },\n  });\n}\n\nexport { TabsPrimitive, TabsTab as TabsTrigger, TabsPanel as TabsContent };\n","type":"registry:ui","target":"~/src/siteplane/components/tabs/tabs.tsx"},{"path":"packages/ui/src/components/tabs/tabs-overflow.ts","content":"\"use client\";\n\nimport * as React from \"react\";\n\nimport { type ScrollMotion, scrollInlineTo } from \"../motion/motion-scroll\";\n\n/** Where a list that overflows has more tabs: before the visible ones, after them, or both. */\nexport type TabsOverflow = { end: boolean; start: boolean };\n\n/**\n * The horizontal overflow of a tab list (S-13), measured whenever the list, a\n * tab or the scroll position changes, and the scrolling that reveals tabs: the\n * selected tab when the list starts to overflow and whenever the selection\n * changes, and a page of tabs for each scroll button. A vertical list never\n * overflows here. Keyboard focus scrolls its tab into view itself, as the\n * browser does for any element that takes focus.\n */\nexport function useTabsOverflow(\n  listRef: React.RefObject<HTMLDivElement | null>,\n): {\n  overflow: TabsOverflow | null;\n  scrollToward: (direction: \"end\" | \"start\") => void;\n} {\n  const [overflow, setOverflow] = React.useState<TabsOverflow | null>(null);\n  const animationRef = React.useRef<ScrollMotion | null>(null);\n\n  // The list scrolls through the motion component, over the tokens of a list\n  // that scrolls to a target, and at once under reduced motion.\n  const scrollTo = React.useCallback(\n    (left: number, smooth: boolean) => {\n      const list = listRef.current;\n      if (!list) return;\n      animationRef.current?.stop();\n      // scrollLeft runs from 0 toward the end in LTR and from 0 toward\n      // negative values in RTL; the target stays within that range.\n      const extent = list.scrollWidth - list.clientWidth;\n      const rtl = getComputedStyle(list).direction === \"rtl\";\n      const target = rtl\n        ? Math.min(0, Math.max(-extent, left))\n        : Math.max(0, Math.min(extent, left));\n      animationRef.current = scrollInlineTo(list, target, smooth);\n    },\n    [listRef],\n  );\n\n  /** Scrolls the list just far enough to show a tab whole, clear of the list's inset. */\n  const reveal = React.useCallback(\n    (tab: HTMLElement, smooth: boolean) => {\n      const list = listRef.current;\n      if (!list) return;\n      const box = list.getBoundingClientRect();\n      const tabBox = tab.getBoundingClientRect();\n      const style = getComputedStyle(list);\n      const left = box.left + list.clientLeft;\n      const before =\n        tabBox.left - (left + (Number.parseFloat(style.paddingLeft) || 0));\n      const after =\n        tabBox.right -\n        (left +\n          list.clientWidth -\n          (Number.parseFloat(style.paddingRight) || 0));\n      if (before < 0) scrollTo(list.scrollLeft + before, smooth);\n      else if (after > 0) scrollTo(list.scrollLeft + after, smooth);\n    },\n    [listRef, scrollTo],\n  );\n\n  const scrollToward = React.useCallback(\n    (direction: \"end\" | \"start\") => {\n      const list = listRef.current;\n      if (!list) return;\n      // A page is as many whole tabs as fit in the visible width, and the\n      // visible width when a single tab is wider.\n      let page = 0;\n      for (const tab of list.querySelectorAll<HTMLElement>('[role=\"tab\"]')) {\n        const width = tab.getBoundingClientRect().width;\n        if (page + width > list.clientWidth) {\n          if (page === 0) page = list.clientWidth;\n          break;\n        }\n        page += width;\n      }\n      const rtl = getComputedStyle(list).direction === \"rtl\";\n      const sign = (direction === \"end\" ? 1 : -1) * (rtl ? -1 : 1);\n      scrollTo(list.scrollLeft + sign * page, true);\n    },\n    [listRef, scrollTo],\n  );\n\n  React.useLayoutEffect(() => {\n    const list = listRef.current;\n    if (!list) return;\n\n    const measure = () => {\n      const extent = list.scrollWidth - list.clientWidth;\n      const next =\n        list.getAttribute(\"aria-orientation\") === \"vertical\" || extent <= 1\n          ? null\n          : {\n              end: Math.abs(list.scrollLeft) < extent - 1,\n              start: Math.abs(list.scrollLeft) > 1,\n            };\n      setOverflow((current) =>\n        current?.start === next?.start && current?.end === next?.end\n          ? current\n          : next,\n      );\n    };\n\n    measure();\n    const resize = new ResizeObserver(measure);\n    const observeTabs = () => {\n      resize.observe(list);\n      for (const tab of list.querySelectorAll<HTMLElement>('[role=\"tab\"]'))\n        resize.observe(tab);\n    };\n    observeTabs();\n    // A selection made outside the list, or a click on a tab at its edge,\n    // brings the selected tab into view; tabs that join are measured too.\n    const mutations = new MutationObserver((records) => {\n      for (const record of records) {\n        if (record.type === \"childList\") observeTabs();\n        const tab = record.target;\n        if (\n          record.type === \"attributes\" &&\n          tab instanceof HTMLElement &&\n          tab.getAttribute(\"aria-selected\") === \"true\"\n        )\n          reveal(tab, true);\n      }\n      measure();\n    });\n    mutations.observe(list, {\n      attributeFilter: [\"aria-selected\"],\n      attributes: true,\n      childList: true,\n      subtree: true,\n    });\n    // A scroll by hand stops a scroll the kit runs.\n    const stop = () => animationRef.current?.stop();\n    list.addEventListener(\"scroll\", measure, { passive: true });\n    list.addEventListener(\"wheel\", stop, { passive: true });\n    list.addEventListener(\"pointerdown\", stop, { passive: true });\n    return () => {\n      resize.disconnect();\n      mutations.disconnect();\n      list.removeEventListener(\"scroll\", measure);\n      list.removeEventListener(\"wheel\", stop);\n      list.removeEventListener(\"pointerdown\", stop);\n      animationRef.current?.stop();\n    };\n  }, [listRef, reveal]);\n\n  // When the list starts to overflow, such as after its first layout, it\n  // shows the selected tab at once.\n  const overflowing = overflow !== null;\n  React.useLayoutEffect(() => {\n    if (!overflowing) return;\n    const selected = listRef.current?.querySelector<HTMLElement>(\n      '[role=\"tab\"][aria-selected=\"true\"]',\n    );\n    if (selected) reveal(selected, false);\n  }, [listRef, overflowing, reveal]);\n\n  return { overflow, scrollToward };\n}\n","type":"registry:ui","target":"~/src/siteplane/components/tabs/tabs-overflow.ts"},{"path":"packages/ui/src/components/tabs/tabs-geometry.ts","content":"/** A box measured on screen (getBoundingClientRect): transforms scale it. */\nexport interface ScreenBox {\n  left: number;\n  right: number;\n  width: number;\n}\n\n/** A box's size in layout px, which transforms do not scale. */\nexport interface LayoutSize {\n  width: number;\n  height: number;\n}\n\n/**\n * An element's size in layout px: its computed width and height, or its offset\n * size where the computed size is no length, as Floating UI's getCssDimensions\n * reads it.\n */\nexport function layoutSize(element: HTMLElement): LayoutSize {\n  const style = getComputedStyle(element);\n  const width = Number.parseFloat(style.width) || 0;\n  const height = Number.parseFloat(style.height) || 0;\n  return Math.round(width) === element.offsetWidth &&\n    Math.round(height) === element.offsetHeight\n    ? { width, height }\n    : { width: element.offsetWidth, height: element.offsetHeight };\n}\n\n/**\n * Where a rail's indicator sits, in the rail's own layout px. The boxes are\n * measured on screen, so inside a scaled ancestor, such as a Dialog popup that\n * zooms in while it opens, every distance is scaled as well. Each one is\n * divided by the rail's scale, the way Base UI's Tabs.Indicator measures its\n * tab; the size comes from the tab's layout box. A rail scaled to nothing keeps\n * the screen distances.\n */\nexport function railIndicatorGeometry({\n  rail,\n  railSize,\n  target,\n  targetSize,\n  strip,\n}: {\n  rail: ScreenBox;\n  railSize: LayoutSize;\n  target: ScreenBox;\n  targetSize: LayoutSize;\n  strip?: ScreenBox | undefined;\n}): {\n  left: number;\n  width: number;\n  height: number;\n  clipStart: number;\n  clipEnd: number;\n} {\n  const scale = railSize.width > 0 ? rail.width / railSize.width : 1;\n  const unscale = (length: number) =>\n    scale > Number.EPSILON ? length / scale : length;\n  return {\n    left: unscale(target.left - rail.left),\n    width: targetSize.width,\n    height: targetSize.height,\n    clipStart: strip ? Math.max(0, unscale(strip.left - rail.left)) : 0,\n    clipEnd: strip ? Math.max(0, unscale(rail.right - strip.right)) : 0,\n  };\n}\n","type":"registry:ui","target":"~/src/siteplane/components/tabs/tabs-geometry.ts"},{"path":"packages/ui/src/components/tabs/index.ts","content":"export * from \"./tabs\";\n","type":"registry:ui","target":"~/src/siteplane/components/tabs/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/tabs.md","categories":["Navigation"],"meta":{"avoidWhen":["Avoid for navigation between pages; use Navigation Menu or links.","Avoid for a related list of expandable sections; use Accordion."],"description":"Keyboard-navigable single-selection content panels.","knownLimitations":["Hidden panels unmount unless keepMounted is set; keep costly state outside the panel.","Switching tabs swaps the panels: the panel that leaves stays mounted until it has faded out and is taken out of the flow where it sits. The element that holds the panels follows the new height and is clipped while it does; a container that scrolls keeps its own height."],"providerNotes":["Panel swaps and the scroll of a list whose tabs overflow use the motion package, which the registry installs with the component.","SiteplaneMotionProvider preset=\"reduced\" and the system setting make a swap instant."],"releaseStatus":"stable","searchTerms":["tabs"],"useWhen":["Use to switch between associated content panels, one visible at a time, within a page."]}}