{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "type": "registry:ui",
  "title": "Tabs",
  "description": "Underline or segmented tabs. The indicator slides between tabs, the label only changes color and the panel fades in from the side of the tab you picked.",
  "dependencies": [
    "@radix-ui/react-tabs",
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/tabs.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/tabs 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Tabs: underline or segmented, built on Radix Tabs (roles, arrow keys, Home\n * and End come from Radix; everything visual is ours).\n *\n * - Labels sit at the title weight (R8) and never change weight or size. Only the color\n *   changes (fg-subtle to fg), on the instant tier.\n * - The indicator (a 2px underline, or the segmented fill block) slides\n *   between tabs with Motion layoutId on the responsive tier. It moves through\n *   transform, so it is allowed.\n * - Panels crossfade. The incoming panel fades in and travels travel/2 from\n *   the side of the tab that was just chosen (right if the new tab sits to the\n *   right of the old one, left if it sits to the left). The outgoing panel\n *   leaves with useExit and drifts travel/4 the other way. It is taken out of\n *   the flow while it leaves (popLayout), so the two never stack and the page\n *   below does not jump. Give panels a shared min-height to keep it still when\n *   their content differs.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): the chosen face (Press's raised tab or block, Flat's\n *   bubble) glides on fun mode's glide spring instead of the responsive one. It rests exactly as it does without fun\n *   mode. Reduced motion turns it off.\n * - Bodies (proposals/worlds.md): this file keeps the contract (value, variant, size, direction, the roles, the roving\n *   focus and arrow keys, which Radix supplies) and Press's body. It resolves each piece into TabsListBodyProps and\n *   TabsTriggerBodyProps and picks a body by the material on <html> (byWorld, useMaterial).\n *   - Press, the instrument. Underline is folder tabs cut from the top edge of a plate: the panel is the plate\n *     (--litt-block, --litt-plate-lift), the chosen tab stands forward in the plate's own face, lit on its top edge and open at its\n *     foot, so it and the panel are one lit surface; the others sit lower in --litt-tray, seated, a shade darker. Segmented is\n *     blocks in a tray (--litt-tray, --litt-tray-shade): the others seated flush, the chosen one raised (--litt-block, --litt-block-lift) and\n *     gliding. A tab under the hand sinks --litt-press-depth (--litt-block-press).\n *   - Flat's words with a grey pill behind the chosen one is bodies/tabs.flat.tsx; Device's engraved legends, a lamp under\n *     the current one that switches on in place (and, segmented, a row of keys on one skirt with the current one latched\n *     down) is bodies/tabs.device.tsx. Only Press's and Flat's faces glide.\n * - Touch: there is no hover, so on a coarse pointer a tab that is held down fills and takes\n *   the --litt-fg text (PRESS, color only, instant tier). The box never moves, and the OS tap flash\n *   is off so the two do not double. Small tabs grow to the control-md height on a coarse\n *   pointer, so no touch target is under 44px: the underline tab itself, and the segmented\n *   track (the same track Segmented uses there).\n */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { AnimatePresence, LayoutGroup, motion, type Variants } from \"motion/react\";\nimport { byWorld, useExit, useFun, useMaterial, useTransition, useTravel } from \"../../lib/litt/motion\";\nimport { TabsListFlatBody, TabsTriggerFlatBody } from \"./bodies/tabs.flat\";\nimport { TabsListDeviceBody, TabsTriggerDeviceBody } from \"./bodies/tabs.device\";\n\n/* Radix's asChild, spread in: shadcn's CLI rewrites a written asChild to Base UI's render prop in base-* hosts. */\nconst asChildProps = { asChild: true } as const;\n\nexport type TabsVariant = \"underline\" | \"segmented\";\nexport type TabsSize = \"sm\" | \"md\";\n\ntype Ctx = {\n  value: string;\n  variant: TabsVariant;\n  size: TabsSize;\n  /** +1 when the newest tab sits after the previous one, -1 before it. */\n  dir: 1 | -1;\n  register: (value: string, el: HTMLElement | null) => void;\n  triggers: RefObject<Map<string, HTMLElement>>;\n};\nconst TabsContext = createContext<Ctx | null>(null);\nfunction useTabs() {\n  const ctx = useContext(TabsContext);\n  if (!ctx) throw new Error(\"Tabs parts must be used inside <Tabs>\");\n  return ctx;\n}\n\nexport type TabsProps = {\n  /** The selected tab, when you control it. Pair it with onValueChange. */\n  value?: string;\n  /** The tab that starts selected when you do not control it. Pass one, because the default selects none and no panel shows. */\n  defaultValue?: string;\n  /** Called with the new tab's value each time the selected tab changes, by click, tap or key. */\n  onValueChange?: (value: string) => void;\n  /** Underline puts a 2px line under the selected tab. Segmented puts a filled block in a track, for view switches. */\n  variant?: TabsVariant;\n  /** Height on the shared control scale: sm is 32, md is 44. Use sm in dense UI and md everywhere else. On a coarse pointer sm grows to 44. */\n  size?: TabsSize;\n  /** Extra classes for the root, such as a width. */\n  className?: string;\n  /** A TabsList and a TabsPanels. */\n  children: ReactNode;\n};\n\n/** The root. It holds the selected tab and passes the variant and size to the other parts. */\nexport function Tabs({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  variant = \"underline\",\n  size = \"md\",\n  className = \"\",\n  children,\n}: TabsProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const value = valueProp ?? inner;\n  const group = useId();\n\n  /* Trigger elements, so the direction can come from their DOM order. */\n  const triggers = useRef(new Map<string, HTMLElement>());\n  const register = useCallback((v: string, el: HTMLElement | null) => {\n    if (el) triggers.current.set(v, el);\n    else triggers.current.delete(v);\n  }, []);\n\n  /* Derive the direction while rendering, so panels get it in the same pass as\n     the new value, whether the value is controlled or not. */\n  const [seen, setSeen] = useState(value);\n  const [dir, setDir] = useState<1 | -1>(1);\n  if (seen !== value) {\n    setSeen(value);\n    const from = triggers.current.get(seen);\n    const to = triggers.current.get(value);\n    if (from && to) setDir(from.compareDocumentPosition(to) & Node.DOCUMENT_POSITION_FOLLOWING ? 1 : -1);\n  }\n\n  return (\n    <TabsContext.Provider value={{ value, variant, size, dir, register, triggers }}>\n      <LayoutGroup id={group}>\n        <TabsPrimitive.Root\n          value={value}\n          onValueChange={(v) => {\n            setInner(v);\n            onValueChange?.(v);\n          }}\n          className={\"min-w-0 \" + className}\n        >\n          {children}\n        </TabsPrimitive.Root>\n      </LayoutGroup>\n    </TabsContext.Provider>\n  );\n}\n\n/** What a list body gets from the contract: the variant and size, the extra classes, and the tablist's own attributes. A body adds its look and its data-body, and nothing else about what the list is. */\nexport type TabsListBodyProps = {\n  variant: TabsVariant;\n  size: TabsSize;\n  className: string;\n  rootProps: ComponentPropsWithoutRef<typeof TabsPrimitive.List>;\n};\n\n/** Press's list: the row of folder tabs standing on the plate's top edge (underline), or a tray of blocks (segmented). */\nfunction PressListBody({ variant, size, className, rootProps }: TabsListBodyProps) {\n  if (variant === \"underline\") {\n    /* The row sits 1px into the plate (-mb-px), above it (z-10), so the chosen tab's face, which runs to the row's foot,\n       covers the plate's lit top edge and ring under it: tab and plate are one surface. The 1px of padding keeps the\n       tab's ring from being clipped by the scroll. */\n    return (\n      <div data-body=\"press\" className=\"relative z-10 -mb-px\">\n        <TabsPrimitive.List\n          className={\"-mx-px flex w-[calc(100%+2px)] items-end gap-0.5 overflow-x-auto px-px pt-px [scrollbar-width:none] \" + className}\n          {...rootProps}\n        />\n      </div>\n    );\n  }\n  return (\n    <TabsPrimitive.List\n      data-body=\"press\"\n      className={\n        \"inline-flex w-fit max-w-full gap-0.5 overflow-x-auto overflow-y-hidden rounded-litt-control bg-litt-tray p-0.5 shadow-(--litt-tray-shade) forced-colors:border [scrollbar-width:none] \" +\n        (size === \"sm\" ? \"h-litt-control-sm pointer-coarse:h-litt-control-md \" : \"h-litt-control-md \") +\n        className\n      }\n      {...rootProps}\n    />\n  );\n}\n\n/** The row of tabs. It is a named group, so give it an aria-label. */\nexport function TabsList({ className = \"\", ...rest }: ComponentPropsWithoutRef<typeof TabsPrimitive.List>) {\n  const { variant, size } = useTabs();\n  const Body = byWorld(useMaterial(), { press: PressListBody, flat: TabsListFlatBody, device: TabsListDeviceBody });\n  return <Body variant={variant} size={size} className={className} rootProps={rest} />;\n}\n\n/** Concentric with the track: control radius minus the 2px inset. */\nconst FILL_RADIUS = \"rounded-[calc(var(--litt-radius-control)-2px)]\"; // litt-allow: concentric inside the segmented track's 2px inset\n\n/* isolate: the faces sit at -z-10 inside the tab, behind the label and the focus ring. The chosen tab is above its\n   neighbours (z-10), so its face glides over them whichever way it goes. */\nconst triggerBase =\n  \"litt-target group/tab relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap select-none font-litt-title \" +\n  \"text-litt-fg-subtle transition-colors duration-(--litt-dur-instant) ease-litt hover:text-litt-fg data-[state=active]:z-10 data-[state=active]:text-litt-fg \" +\n  \"[-webkit-tap-highlight-color:transparent] enabled:pointer-coarse:active:text-litt-fg \" +\n  \"disabled:cursor-not-allowed disabled:text-litt-fg-disabled \" +\n  \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The radius is the body's: Press's tab is cut square at its foot, its segment is concentric with the tray. */\nconst triggerShape: Record<TabsVariant, Record<TabsSize, string>> = {\n  underline: {\n    md: \"h-litt-control-md px-litt-control-pad-md text-litt-body\",\n    sm: \"h-litt-control-sm pointer-coarse:h-litt-control-md px-litt-control-pad-sm text-litt-small\",\n  },\n  segmented: {\n    md: \"h-full px-litt-control-pad-md text-litt-small\",\n    sm: \"h-full px-litt-control-pad-sm text-litt-caption pointer-coarse:text-litt-small\",\n  },\n};\n\ntype TabsTriggerProps = ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {\n  /** Which tab this is. Match it to the value of its TabsPanel. */\n  value: string;\n};\n\n/** What a trigger body gets from the contract: the tab's value, its geometry on the control scale (without a radius), whether the sliding face belongs on this tab, and the button's attributes. A body adds its look, its radius and its data-body; the selected state is Radix's (aria-selected, data-state). */\nexport type TabsTriggerBodyProps = {\n  value: string;\n  variant: TabsVariant;\n  size: TabsSize;\n  /** The size and padding classes the control scale gives this variant and size. */\n  shapeClass: string;\n  /** Whether the sliding face (Press's raised tab or block, Flat's bubble) belongs on this tab: it is the selected one. */\n  active: boolean;\n  /** The spring the face slides on. */\n  enter: ReturnType<typeof useTransition>;\n  className: string;\n  triggerRef: (el: HTMLButtonElement | null) => void;\n  rootProps: Omit<ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>, \"value\" | \"className\" | \"children\">;\n  children: ReactNode;\n};\n\n/* A segment under the hand sinks the --litt-press-depth with the pressed cast; on touch the held tab shows it too. */\nconst SINK = \"group-active/tab:translate-y-(--litt-press-depth) group-active/tab:shadow-(--litt-block-press)\";\n\n/** Press's tab: a folder tab cut from the plate's top edge (underline) or a block seated in the tray (segmented). */\nfunction PressTriggerBody({ value, variant, shapeClass, active, enter, className, triggerRef, rootProps, children }: TabsTriggerBodyProps) {\n  const underline = variant === \"underline\";\n  return (\n    <TabsPrimitive.Trigger\n      ref={triggerRef}\n      value={value}\n      data-body=\"press\"\n      className={[triggerBase, shapeClass, underline ? \"rounded-t-litt-control\" : FILL_RADIUS, className].join(\" \")}\n      {...rootProps}\n    >\n      {/* Every tab has its seated face: a tab a shade darker and lower than the plate, or a block flush with the tray. */}\n      <span\n        aria-hidden=\"true\"\n        className={\n          (underline\n            ? \"absolute inset-x-0 top-1 bottom-0.5 -z-10 rounded-t-litt-control bg-litt-tray shadow-(--litt-tray-shade) forced-colors:border group-hover/tab:bg-[color-mix(in_oklab,var(--litt-tray),var(--litt-fg)_4%)] group-active/tab:translate-y-(--litt-press-depth) \"\n            : \"absolute inset-0 -z-10 bg-[color-mix(in_oklab,var(--litt-block),var(--litt-tray)_55%)] group-hover/tab:bg-[color-mix(in_oklab,var(--litt-block),var(--litt-tray)_30%)] \" + FILL_RADIUS + \" \") +\n          \"transition-colors duration-(--litt-dur-instant) ease-litt\"\n        }\n      />\n      {active && (\n        <motion.span\n          layoutId=\"tabs-indicator\"\n          transition={enter}\n          aria-hidden=\"true\"\n          className={\n            underline\n              ? /* Standing forward: the plate's own face, lit on its top edge, open at its foot (the cast below is clipped). */\n                \"absolute inset-x-0 top-0 bottom-0 -z-10 rounded-t-litt-control bg-litt-block shadow-(--litt-plate-lift) [clip-path:inset(-4px_-4px_0_-4px)] forced-colors:border forced-colors:border-b-0 forced-colors:border-[Highlight] forced-colors:[forced-color-adjust:none]\"\n              : \"absolute inset-0 -z-10 bg-litt-block [box-shadow:var(--litt-highlight),var(--litt-block-lift)] forced-colors:border forced-colors:border-[Highlight] \" + FILL_RADIUS + \" \" + SINK\n          }\n        />\n      )}\n      <span className={\"litt-trim relative\" + (underline && !active ? \" translate-y-0.5\" : \"\")}>{children}</span>\n    </TabsPrimitive.Trigger>\n  );\n}\n\n/** One tab. Its label only changes color, so the tab never moves or resizes. */\nexport function TabsTrigger({\n  value,\n  className = \"\",\n  children,\n  ...rest\n}: TabsTriggerProps) {\n  const { value: current, variant, size, register } = useTabs();\n  const responsive = useTransition(\"responsive\");\n  const material = useMaterial();\n  /* Fun mode: the chosen face glides on the glide spring. */\n  const fun = useFun();\n  const enter = fun ? fun.glide : responsive;\n  const active = current === value;\n  const ref = useCallback((el: HTMLButtonElement | null) => register(value, el), [register, value]);\n  const Body = byWorld(material, { press: PressTriggerBody, flat: TabsTriggerFlatBody, device: TabsTriggerDeviceBody });\n\n  return (\n    <Body\n      value={value}\n      variant={variant}\n      size={size}\n      shapeClass={triggerShape[variant][size]}\n      active={active}\n      enter={enter}\n      className={className}\n      triggerRef={ref}\n      rootProps={rest}\n    >\n      {children}\n    </Body>\n  );\n}\n\n/** The positioned box the panels live in. Put the shared min-height here. */\nexport function TabsPanels({\n  className = \"\",\n  children,\n}: {\n  /** Extra classes for the box, such as a shared min-height and padding. */\n  className?: string;\n  /** The TabsPanel for each tab. */\n  children: ReactNode;\n}) {\n  const { variant } = useTabs();\n  /* Press's underline tabs stand on a plate: the panel box is that plate (the chosen tab joins it), square at the corner\n     the first tab stands on. Every other body, and the segmented variant, leaves the panel bare. */\n  const plate = useMaterial() === \"press\" && variant === \"underline\";\n  return (\n    <div\n      data-body={plate ? \"press\" : undefined}\n      className={\"relative \" + (plate ? \"rounded-litt-surface rounded-tl-none bg-litt-block pb-4 shadow-(--litt-plate-lift) forced-colors:border \" : \"\") + className}\n    >\n      {children}\n    </div>\n  );\n}\n\n/** The content for one tab. Only the selected panel is mounted, so state inside the others resets when you leave them. */\nexport function TabsPanel({\n  value,\n  className = \"\",\n  tabIndex,\n  children,\n}: {\n  /** Which tab this panel belongs to. Match it to the value of its TabsTrigger. */\n  value: string;\n  /** Extra classes for this panel. */\n  className?: string;\n  /** The panel is a Tab stop by default. Pass -1 when it starts with its own focusable content, so it is not an extra stop. */\n  tabIndex?: number;\n  /** What shows when this tab is selected. */\n  children: ReactNode;\n}) {\n  const { value: current, dir } = useTabs();\n  const enter = useTransition(\"responsive\");\n  const exit = useExit(\"responsive\");\n  const travel = useTravel();\n\n  /* The first panel appears without a fade. Not through AnimatePresence initial={false}: that reaches\n     every motion component inside the panel and swallows their own entrances, such as a list cascade. */\n  const mounted = useRef(false);\n  useEffect(() => {\n    mounted.current = true;\n  }, []);\n\n  const variants: Variants = {\n    in: (d: 1 | -1) => ({ opacity: 0, x: d * (travel / 2) }),\n    show: { opacity: 1, x: 0, transition: enter },\n    out: (d: 1 | -1) => ({ opacity: 0, x: -d * (travel / 4), transition: exit }),\n  };\n\n  return (\n    <AnimatePresence mode=\"popLayout\" custom={dir}>\n      {current === value && (\n        <TabsPrimitive.Content key={value} value={value} forceMount {...asChildProps} {...(tabIndex === undefined ? {} : { tabIndex })}>\n          <motion.div\n            custom={dir}\n            variants={variants}\n            initial={mounted.current ? \"in\" : false}\n            animate=\"show\"\n            exit=\"out\"\n            className={\"data-[state=inactive]:pointer-events-none \" + className}\n          >\n            {children}\n          </motion.div>\n        </TabsPrimitive.Content>\n      )}\n    </AnimatePresence>\n  );\n}\n",
      "target": "components/litt/tabs.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/tabs.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/tabs 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Tabs, Device's body (proposals/worlds.md, Bodies; material.md, 8): legends engraved on the body, with a lamp under\n * the current one. It switches on in place and never glides.\n *\n * - Underline: each tab is a legend, a caption in the mono as engraved capitals (litt-legend: --litt-label-case,\n *   --litt-label-tracking, --litt-engrave), --litt-fg-subtle until chosen and --litt-fg when it is. Under every legend sits a small lamp\n *   (6px). The current one is lit in --litt-fg, the rest are dim rings. A lamp never says the state alone: aria-selected does,\n *   and so does the word's ink. Tabs take no accent (R6 keeps it for the view's one accent). A hairline runs under the\n *   row, as on the other bodies.\n * - Segmented: a row of keys on one skirt. The track is the skirt, a darker plate with a rim, and the keys stand on it a\n *   hair apart, each a key face (--litt-block, the lit top edge, the darker lower edge). The chosen key is latched down: it\n *   has sunk the whole --litt-press-depth (2px) onto the skirt, so its skirt is gone, its face is a step darker (--litt-block mixed\n *   55% toward --litt-tray) and inset-shaded with --litt-tray-shade, as the Segmented control's latched key is, and its lamp is lit. A key under the hand sinks the same 2px, and never more, whatever fun mode asks.\n * - Motion: nothing travels between tabs. The change switches in place in three steps on the instant tier (steps(3,\n *   end), --litt-dur-instant), as the switch's detent does: the lamp lights, the word's ink changes and a key drops.\n *   Reduced motion zeroes the tier, so the step is at once.\n * - Forced colors drops shadows and fills, so the skirt and the keys get real borders, a lit lamp is a system highlight\n *   and an unlit one an outline, and the latched key's border is the highlight.\n * - The contract (value, variant, size, the roles, roving focus, arrow keys, the touch fill) is tabs.tsx's.\n */\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport type { CSSProperties } from \"react\";\nimport type { TabsListBodyProps, TabsTriggerBodyProps } from \"../tabs\";\n\n/* The step: three even steps on the instant tier, as the display's segments light. */\nconst STEPS = \"steps(3, end)\";\nconst STEP: CSSProperties = {\n  transitionProperty: \"color, background-color, box-shadow, transform\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: STEPS,\n};\n\n/** The row: a hairline under the legends (underline), or one skirt the keys stand on (segmented). */\nexport function TabsListDeviceBody({ variant, size, className, rootProps }: TabsListBodyProps) {\n  if (variant === \"underline\") {\n    return (\n      <div data-body=\"device\" className=\"border-b border-litt-line\">\n        <TabsPrimitive.List className={\"-mb-px flex w-full overflow-x-auto pb-px [scrollbar-width:none] \" + className} {...rootProps} />\n      </div>\n    );\n  }\n  return (\n    <TabsPrimitive.List\n      data-body=\"device\"\n      className={\n        \"inline-flex w-fit max-w-full gap-px overflow-x-auto overflow-y-hidden rounded-litt-control pb-0.5 [scrollbar-width:none] \" +\n        \"bg-[color-mix(in_oklab,black_42%,var(--litt-block))] shadow-[0_0_0_1px_color-mix(in_oklab,black_22%,transparent)] forced-colors:border \" +\n        (size === \"sm\" ? \"h-litt-control-sm pointer-coarse:h-litt-control-md \" : \"h-litt-control-md \") +\n        className\n      }\n      {...rootProps}\n    />\n  );\n}\n\nconst base =\n  \"litt-target group/tab relative inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap select-none \" +\n  \"font-litt-title font-litt-mono text-litt-caption litt-legend [-webkit-tap-highlight-color:transparent] \" +\n  \"disabled:cursor-not-allowed disabled:text-litt-fg-disabled \" +\n  \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* Underline: a legend with its lamp below. */\nconst legend: Record<TabsTriggerBodyProps[\"size\"], string> = {\n  md: \"h-litt-control-md px-litt-control-pad-md pb-1.5\",\n  sm: \"h-litt-control-sm pointer-coarse:h-litt-control-md px-litt-control-pad-sm pb-1\",\n};\nconst legendInk = \"rounded-litt-control text-litt-fg-subtle hover:text-litt-fg data-[state=active]:text-litt-fg enabled:pointer-coarse:active:text-litt-fg\";\n\n/* Segmented: a key on the skirt. Latched, it has sunk its 2px and its cast is the pressed one; under the hand it sinks the same. */\nconst key: Record<TabsTriggerBodyProps[\"size\"], string> = {\n  md: \"h-full px-litt-control-pad-md\",\n  sm: \"h-full px-litt-control-pad-sm\",\n};\nconst keyFace =\n  \"rounded-litt-control bg-litt-block text-litt-fg-muted hover:bg-(--litt-block-hover) hover:text-litt-fg \" +\n  \"[--key-edge:var(--litt-highlight),inset_0_-1px_0_color-mix(in_oklab,black_10%,transparent)] shadow-(--key-edge) \" +\n  \"data-[state=active]:bg-[color-mix(in_oklab,var(--litt-block),var(--litt-tray)_55%)] data-[state=active]:hover:bg-[color-mix(in_oklab,var(--litt-block),var(--litt-tray)_55%)] \" +\n  \"data-[state=active]:text-litt-fg data-[state=active]:shadow-(--litt-tray-shade) data-[state=active]:[transform:translateY(var(--litt-press-depth))] \" +\n  \"enabled:active:text-litt-fg enabled:active:shadow-(--litt-block-press) enabled:active:[transform:translateY(var(--litt-press-depth))] \" +\n  \"forced-colors:border forced-colors:data-[state=active]:border-[Highlight]\";\n\n/* The lamp: unlit a dim ring, lit the ink of the body. */\nconst lamp =\n  \"size-1.5 shrink-0 rounded-litt-pill shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--litt-fg)_30%,transparent)] \" +\n  \"group-data-[state=active]/tab:bg-litt-fg group-data-[state=active]/tab:shadow-[0_0_5px_color-mix(in_oklab,var(--litt-fg)_45%,transparent)] \" +\n  \"forced-colors:border forced-colors:bg-transparent forced-colors:group-data-[state=active]/tab:bg-[Highlight]\";\n\n/** One tab: a legend and its lamp, or a key with its lamp. The selected state is Radix's data-state. */\nexport function TabsTriggerDeviceBody({ value, variant, size, className, triggerRef, rootProps, children }: TabsTriggerBodyProps) {\n  const underline = variant === \"underline\";\n  return (\n    <TabsPrimitive.Trigger\n      ref={triggerRef}\n      value={value}\n      data-body=\"device\"\n      className={[base, underline ? legend[size] + \" \" + legendInk : key[size] + \" \" + keyFace, className].join(\" \")}\n      {...rootProps}\n      style={{ ...STEP, ...rootProps.style }}\n    >\n      {underline ? (\n        <>\n          <span className=\"litt-trim relative\">{children}</span>\n          <span aria-hidden=\"true\" className={lamp + \" absolute bottom-1 left-1/2 -translate-x-1/2\"} style={STEP} />\n        </>\n      ) : (\n        <span className=\"relative inline-flex items-center gap-2\">\n          <span aria-hidden=\"true\" className={lamp} style={STEP} />\n          <span className=\"litt-trim\">{children}</span>\n        </span>\n      )}\n    </TabsPrimitive.Trigger>\n  );\n}\n",
      "target": "components/litt/bodies/tabs.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/tabs.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/tabs 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Tabs, Flat's body (proposals/worlds.md, Bodies; proposals/flat-pill.md): words on the page, and a grey bubble behind\n * the chosen one. Ink on paper: nothing stands up, nothing is framed, and the panel below is bare.\n *\n * - Both variants are the same few things: words in a line, space between them, no hairline under the row and no\n *   tray around it. The unchosen are --litt-fg-muted words; the chosen is --litt-fg (ink) with a grey bubble behind it (--litt-block,\n *   which is --litt-surface-3 under Flat), a pill, the one shape for what a hand touches. The bubble glides between tabs\n *   with layoutId on the responsive tier (transform only); under fun mode it glides on the glide spring.\n * - Underline reads as a heading row at the body size; segmented is the same row set small, for switching a view.\n *   Neither has a track, so the bubble is the only thing drawn, and it says \"chosen\" the way Flat always does.\n * - Under forced colors the bubble is a Highlight outline, not a fill, so the chosen tab still differs from its\n *   neighbours.\n * - Nothing here is lifted by light or shade (R2, R3), and the row never scrolls a ring out of its box: the focus\n *   outline is inside the tab.\n * - The contract (value, variant, size, the roles, roving focus, arrow keys, the touch fill) is tabs.tsx's. This file\n *   only says what the tab is made of.\n */\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { motion } from \"motion/react\";\nimport type { TabsListBodyProps, TabsTriggerBodyProps } from \"../tabs\";\n\n/** The row: words in a line, space between them. No hairline, no tray. */\nexport function TabsListFlatBody({ variant, size, className, rootProps }: TabsListBodyProps) {\n  if (variant === \"underline\") {\n    return (\n      <div data-body=\"flat\">\n        <TabsPrimitive.List className={\"flex w-full gap-1 overflow-x-auto [scrollbar-width:none] \" + className} {...rootProps} />\n      </div>\n    );\n  }\n  return (\n    <TabsPrimitive.List\n      data-body=\"flat\"\n      className={\n        \"inline-flex w-fit max-w-full gap-0.5 overflow-x-auto overflow-y-hidden [scrollbar-width:none] \" +\n        (size === \"sm\" ? \"h-litt-control-sm pointer-coarse:h-litt-control-md \" : \"h-litt-control-md \") +\n        className\n      }\n      {...rootProps}\n    />\n  );\n}\n\n/* What a finger holding a tab shows: the fg 6% fill that rows and segments share, and the stronger text color. */\nconst press =\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"enabled:pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] enabled:pointer-coarse:active:text-litt-fg\";\n\nconst trigger =\n  \"litt-target relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap select-none rounded-litt-pill font-litt-title \" +\n  \"text-litt-fg-muted transition-colors duration-(--litt-dur-instant) ease-litt hover:text-litt-fg data-[state=active]:text-litt-fg \" +\n  press + \" \" +\n  \"disabled:cursor-not-allowed disabled:text-litt-fg-disabled \" +\n  \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-litt-focus\";\n\n/** One tab: the word, and the bubble behind it while it is the chosen one. */\nexport function TabsTriggerFlatBody({ value, active, shapeClass, enter, className, triggerRef, rootProps, children }: TabsTriggerBodyProps) {\n  return (\n    <TabsPrimitive.Trigger ref={triggerRef} value={value} data-body=\"flat\" className={[trigger, shapeClass, className].join(\" \")} {...rootProps}>\n      {active && (\n        <motion.span\n          layoutId=\"tabs-indicator\"\n          transition={enter}\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 -z-10 rounded-litt-pill bg-litt-block forced-colors:border-2 forced-colors:border-[Highlight] forced-colors:bg-transparent forced-colors:[forced-color-adjust:none]\"\n        />\n      )}\n      <span className=\"litt-trim relative\">{children}</span>\n    </TabsPrimitive.Trigger>\n  );\n}\n",
      "target": "components/litt/bodies/tabs.flat.tsx"
    }
  ],
  "docs": "Import Tabs, TabsList, TabsTrigger, TabsPanels and TabsPanel from components/litt/tabs.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
