{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "type": "registry:ui",
  "title": "Switch",
  "description": "An on or off setting. A pill track that fills with the foreground color when on, never the accent, and a thumb that slides on the instant tier.",
  "dependencies": [
    "@radix-ui/react-switch"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/switch.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/switch 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Switch (components.md, \"Switch\"): an on or off setting that takes effect as a setting.\n *\n * - Built on @radix-ui/react-switch. The root is a <button role=\"switch\"> with aria-checked,\n *   Space toggles it, and inside a <form> a hidden input sends the value like a checkbox:\n *   the value when on, nothing when off. The hidden input is Radix's, so name, value and\n *   required are passed straight to it.\n * - A thick block thrown along a deep tray (material.md, 6, \"Press\"). The tray is 92 by 32, cut deep: --litt-slot with\n *   --litt-slot-shade, at the control radius. The block fills half of it, 44 by 28, set 2px in, raised: --litt-block with\n *   --litt-block-lift and a lit top edge (--litt-highlight). Its corners are the control radius less those 2px, so the two\n *   stay concentric at every shape.\n * - The tray's floor says the state: \"On\" set in the floor's start, \"Off\" at its end, at the caption size and the\n *   title weight, in --litt-fg. The block carries no word; it covers the word on its side and its travel uncovers the\n *   other. Position and word both change, so color is never the signal.\n * - Two tones. neutral (the default) keeps the block white when on. accent fills it with the accent. The accent is one fill per view (R6), so accent is for a lone switch that is the view's\n *   main choice; a list of settings stays neutral.\n * - No invalid state. A choice that can be wrong, such as accepting terms, is a Checkbox.\n * - The label is a real <label for> stretched over the whole row, so a click anywhere in it toggles the switch. Its\n *   accessible name is only its text (the floor's words are hidden from screen readers, aria-checked says the state),\n *   and the description is linked with aria-describedby. By default the switch comes first, like the checkbox.\n *   With align=\"end\" the label and description come first and the switch sits at the row's end, so the switches\n *   of a settings list line up in one column.\n * - Hand: press anywhere on the row and the block presses down (its cast tightens). A click, a tap or Space toggles\n *   it. Or take the block and drag it: it follows the finger one to one, says the side it is over, and lands on the\n *   nearer side when let go. A drag that ends where it started changes nothing.\n * - Motion: the block crosses on the responsive tier (bounce 0), by translate; its fill, word and cast change on the\n *   instant tier. The tray never moves. Hover deepens the tray's floor a little.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): the block crosses on fun mode's glide spring,\n *   as a CSS linear() easing, so it meets the far wall with a little life and settles. At rest it is exactly the\n *   block above. Reduced motion turns it off.\n * - Touch: rows are at least 44px tall. The tray owns a sideways drag and leaves vertical scrolling to the page.\n * - One contract, three bodies (proposals/worlds.md, Bodies). This file is the contract: the props, the Radix root with\n *   its role and aria-checked, the hidden input, the label and description, the hand (press, drag, keys) and the state.\n *   It resolves them once into SwitchBodyProps and renders the body the material picks (byWorld). PressBody, above, is\n *   the default. bodies/switch.flat.tsx and bodies/switch.device.tsx are the other two; each draws its own root\n *   element, and its own --sw-travel, which a drag measures. A frame that sets its own look (a study's strip, a\n *   preview inside a MaterialProvider) draws its own body, not <html>'s.\n * - Flat (R1 to R3, proposals/flat-pill.md), bodies/switch.flat.tsx: a small ink pill with a canvas knob when on and an\n *   outline pill (the 1.5px --litt-line-control edge, a --litt-line-control knob) when off, the accent only for tone=\"accent\".\n *   The word sits in type beside it in the text weight, both words in one cell so the row never reflows. The pill is\n *   44 by 26 and the knob 20, so the crossing is 18px.\n * - Device, bodies/switch.device.tsx: a toggle key in a well that flips with a detent (three steps on the instant\n *   tier, never a spring), and its word on a small LCD beside it in the mono, with a lamp that lights when on.\n * - Forced colors drops box-shadow and background colors, so the tray and the block get real borders and system\n *   colors mark the on state.\n */\nimport {\n  useId,\n  useRef,\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport { byWorld, useFun, useMaterial } from \"../../lib/litt/motion\";\nimport { SwitchDeviceBody } from \"./bodies/switch.device\";\nimport { SwitchFlatBody } from \"./bodies/switch.flat\";\n\nexport type SwitchAlign = \"start\" | \"end\";\nexport type SwitchTone = \"neutral\" | \"accent\";\n\nexport type SwitchProps = {\n  /** The label, beside the switch. Required, because a switch alone has no name. The whole row is clickable. */\n  label: ReactNode;\n  /** A line of detail under the label. It is linked to the switch with aria-describedby. */\n  description?: ReactNode;\n  /** Whether the switch is on, when you control it. Pair it with onCheckedChange. */\n  checked?: boolean;\n  /** Whether the switch starts on, when you do not control it. */\n  defaultChecked?: boolean;\n  /** Called with the new state each time the switch is toggled, by click, tap, drag, label or Space. */\n  onCheckedChange?: (checked: boolean) => void;\n  /** \"neutral\" keeps the block white when on. \"accent\" fills it with the accent: one per view (R6), for a lone switch that is the view's main choice. */\n  tone?: SwitchTone;\n  /** The block's two words, [off, on]. Keep them to three letters or so, such as [\"Aus\", \"An\"]. The state itself is read from aria-checked. */\n  words?: [string, string];\n  /** Mutes the row, skips it in the tab order and keeps a form from sending it. */\n  disabled?: boolean;\n  /** Marks the switch as required, for a form that must have it on. It sets aria-required. */\n  required?: boolean;\n  /** The name a form sends the value under. A form sends it only while the switch is on. */\n  name?: string;\n  /** What a form sends while the switch is on. */\n  value?: string;\n  /** The id of the switch, which the label points at. Made up for you when left out. */\n  id?: string;\n  /** \"start\" puts the switch first, then the label, like the checkbox. \"end\" puts the label first and the switch at the end of a full-width row, for settings lists. */\n  align?: SwitchAlign;\n  /** Extra classes for the row, such as a width. */\n  className?: string;\n  /** The switch button, for focus or a form library. */\n  ref?: Ref<HTMLButtonElement>;\n};\n\n/**\n * What the contract hands a body: the switch resolved once. A body draws the control (the Radix root and what is\n * inside it); the row, the label and the description stay with the contract. A body keeps the root's role, aria-checked\n * and wiring (`root`), says the state without color alone, gives the row 44px, sets --sw-travel on the root (what a\n * drag measures) and carries data-body on it.\n */\nexport type SwitchBodyProps = {\n  /** The side shown: the switch's state, or the side a drag is over. */\n  shown: boolean;\n  disabled: boolean;\n  /** A hand is dragging the knob, so it follows 1:1 and nothing eases it. */\n  dragging: boolean;\n  /** The knob's translate: the drag's px while dragging, else the start or --sw-travel. */\n  x: string;\n  tone: SwitchTone;\n  /** [off, on]. */\n  words: [string, string];\n  /** The knob's transitions: the crossing on the responsive tier or fun mode's glide, and while dragging only the colors. */\n  motion: CSSProperties;\n  /** The Radix root's props: id, checked, handlers, aria-describedby, name, value, required, ref. */\n  root: ComponentProps<typeof SwitchPrimitive.Root> & { \"data-no-drag\": \"\" };\n  /** Where the control sits in the row's grid. */\n  className: string;\n};\n\n/** How far the block crosses in Press, in px: the tray's 92, less 2px on each side, less the block's own 44. Flat and Device set their own through --sw-travel. */\nconst TRAVEL = 44;\n/** How far a finger moves before a press becomes a drag, in px. */\nconst SLOP = 4;\n\nconst focusRing = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The tray is the button, cut deep into the plate (--litt-slot, --litt-slot-shade). The row's label is stretched over the whole\n   row, so the tray sits above it (z-1) and a press on the tray itself still lands on the button. It clips the block,\n   so a cast never spills past its walls. */\nconst tray =\n  \"[--sw-travel:44px] \" +\n  \"peer relative z-1 inline-flex h-8 w-23 shrink-0 cursor-pointer touch-pan-y items-center self-start overflow-hidden rounded-litt-control p-0.5 \" +\n  \"bg-litt-slot shadow-(--litt-slot-shade) \" +\n  \"transition-[background-color] duration-(--litt-dur-instant) ease-litt \" +\n  \"enabled:group-hover/switch:bg-[color-mix(in_oklab,var(--litt-slot),var(--litt-fg)_4%)] \" +\n  \"disabled:cursor-not-allowed disabled:bg-litt-surface-2 disabled:shadow-[inset_0_0_0_1px_var(--litt-line)] \" +\n  \"forced-colors:border forced-colors:p-px \" + // the border is real, so the padding gives it back its px\n  focusRing;\n\n/* A word set in the floor, one in each half. The block covers the one on its side and uncovers the other, so travel\n   is what reveals the state. --litt-fg on the --litt-slot floor keeps 4.5:1. */\nconst floorWord =\n  \"pointer-events-none absolute inset-y-0 flex w-1/2 select-none items-center justify-center text-litt-caption font-litt-title text-litt-fg \" +\n  \"forced-colors:text-[CanvasText]\";\n\n/* The block, thick and bare: a lit top edge (--litt-highlight) over its cast, no word on it. Its corners are the control\n   radius less the 2px it is set in, so they stay concentric with the tray's. */\nconst blockBase =\n  \"pointer-events-none flex h-7 w-11 shrink-0 rounded-[calc(var(--litt-radius-control)-2px)] \" +\n  \"forced-colors:border\";\n\nconst blockLift = \"shadow-[var(--litt-block-lift),var(--litt-highlight)] group-active/switch:shadow-[var(--litt-block-press),var(--litt-highlight)]\";\nconst blockOff = \"bg-litt-block \" + blockLift;\nconst blockOn: Record<SwitchTone, string> = {\n  neutral: \"bg-litt-block \" + blockLift + \" forced-colors:bg-[Highlight]\",\n  accent: \"bg-litt-accent \" + blockLift + \" forced-colors:bg-[Highlight]\",\n};\n/* Disabled, the block lies flat: no cast, a hairline. */\nconst blockDisabled = \"bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\n/** Press, the default body: a thick block thrown along a deep tray to a stop, uncovering the word set in the floor. */\nfunction PressBody({ shown, disabled, x, tone, words, motion, root, className }: SwitchBodyProps) {\n  const block = blockBase + \" \" + (disabled ? blockDisabled : shown ? blockOn[tone] : blockOff);\n  const word = floorWord + (disabled ? \" text-litt-fg-disabled\" : \"\");\n  /* The word under the block is out of sight, so it is out of the page too: a contrast check never reads it through the block. */\n  const covered = \" opacity-0\";\n  return (\n    <SwitchPrimitive.Root {...root} data-body=\"press\" className={tray + className}>\n      <span aria-hidden=\"true\" className={word + \" left-0\" + (shown ? \"\" : covered)}>{words[1]}</span>\n      <span aria-hidden=\"true\" className={word + \" right-0\" + (shown ? covered : \"\")}>{words[0]}</span>\n      <SwitchPrimitive.Thumb className={block} style={{ ...motion, translate: `${x} 0` }} />\n    </SwitchPrimitive.Root>\n  );\n}\n\n/* The block's crossing on the responsive tier, and its fill, word and cast on the instant tier. While a hand drags it,\n   it follows one to one, so only the colors keep their fade. */\nconst crossing: CSSProperties = {\n  transitionProperty: \"translate, background-color, color, box-shadow\",\n  transitionDuration: \"var(--litt-dur-responsive), var(--litt-dur-instant), var(--litt-dur-instant), var(--litt-dur-instant)\",\n  transitionTimingFunction: \"var(--litt-ease-responsive), var(--litt-ease-out), var(--litt-ease-out), var(--litt-ease-out)\",\n};\nconst following: CSSProperties = {\n  transitionProperty: \"background-color, color, box-shadow\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: \"var(--litt-ease-out)\",\n};\n\n/** One switch with its label. Use checked and onCheckedChange to control it, or defaultChecked to leave it alone. */\nexport function Switch({\n  label,\n  description,\n  checked,\n  defaultChecked,\n  onCheckedChange,\n  tone = \"neutral\",\n  words = [\"Off\", \"On\"],\n  disabled,\n  required,\n  name,\n  value,\n  id: idProp,\n  align = \"start\",\n  className = \"\",\n  ref,\n}: SwitchProps) {\n  const auto = useId();\n  const id = idProp ?? auto;\n  const descriptionId = description ? `${id}-description` : undefined;\n  const end = align === \"end\";\n  const Body = byWorld(useMaterial(), { press: PressBody, flat: SwitchFlatBody, device: SwitchDeviceBody });\n\n  /* The switch keeps its own state when you do not control it, because a drag sets it to a side, not a flip. */\n  const [inner, setInner] = useState(defaultChecked ?? false);\n  const on = checked ?? inner;\n  const set = (next: boolean) => {\n    if (checked === undefined) setInner(next);\n    onCheckedChange?.(next);\n  };\n\n  /* A hand on the block: where it took hold, and where the block is now. A press becomes a drag past SLOP px. */\n  const hand = useRef<{ id: number; x0: number; from: number; at: number; scale: number; travel: number; moved: boolean } | null>(null);\n  const dropped = useRef(-1);\n  const [dragX, setDragX] = useState<number | null>(null);\n\n  const onPointerDown = (e: PointerEvent<HTMLButtonElement>) => {\n    if (disabled || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n    const el = e.currentTarget;\n    el.setPointerCapture(e.pointerId);\n    const scale = el.getBoundingClientRect().width / (el.offsetWidth || 1) || 1;\n    const travel = parseFloat(getComputedStyle(el).getPropertyValue(\"--sw-travel\")) || TRAVEL;\n    const from = on ? travel : 0;\n    hand.current = { id: e.pointerId, x0: e.clientX, from, at: from, scale, travel, moved: false };\n  };\n  const onPointerMove = (e: PointerEvent<HTMLButtonElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId) return;\n    const dx = (e.clientX - h.x0) / h.scale;\n    if (!h.moved && Math.abs(dx) < SLOP) return;\n    h.moved = true;\n    h.at = Math.min(h.travel, Math.max(0, h.from + dx));\n    setDragX(h.at);\n  };\n  const onPointerEnd = (e: PointerEvent<HTMLButtonElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId) return;\n    hand.current = null;\n    if (!h.moved) return;\n    setDragX(null);\n    if (e.type === \"pointercancel\") return;\n    /* The click that follows a drag must not flip it back: the drop already chose the side. */\n    dropped.current = e.timeStamp;\n    const next = h.at > h.travel / 2;\n    if (next !== on) set(next);\n  };\n  const onClick = (e: MouseEvent<HTMLButtonElement>) => {\n    if (dropped.current >= 0 && e.timeStamp - dropped.current < 500) e.preventDefault(); // Radix skips its toggle\n    dropped.current = -1;\n  };\n\n  const dragging = dragX !== null;\n  const shown = dragging ? dragX > (hand.current?.travel ?? TRAVEL) / 2 : on;\n  const x = dragging ? `${dragX}px` : on ? \"var(--sw-travel)\" : \"0px\";\n\n  /* Fun mode: the crossing (the first transitioned property) takes the glide spring; the rest keep the instant tier. */\n  const fun = useFun();\n  const motion: CSSProperties = dragging\n    ? following\n    : fun\n      ? {\n          ...crossing,\n          transitionDuration: `${fun.glideCss.duration}ms, var(--litt-dur-instant), var(--litt-dur-instant), var(--litt-dur-instant)`,\n          transitionTimingFunction: `${fun.glideCss.easing}, var(--litt-ease-out), var(--litt-ease-out), var(--litt-ease-out)`,\n        }\n      : crossing;\n\n  return (\n    <div\n      className={\n        \"group/switch relative grid select-none py-1.5 \" +\n        (end ? \"w-full grid-cols-[1fr_auto] gap-x-4 \" : \"w-fit grid-cols-[auto_1fr] gap-x-3 \") +\n        className\n      }\n    >\n      <Body\n        shown={shown}\n        disabled={!!disabled}\n        dragging={dragging}\n        x={x}\n        tone={tone}\n        words={words}\n        motion={motion}\n        className={end ? \" col-start-2 row-start-1\" : \" col-start-1 row-start-1\"}\n        root={{\n          ref,\n          id,\n          checked: on,\n          onCheckedChange: set,\n          disabled,\n          required,\n          name,\n          value,\n          \"aria-describedby\": descriptionId,\n          onPointerDown,\n          onPointerMove,\n          onPointerUp: onPointerEnd,\n          onPointerCancel: onPointerEnd,\n          onClick,\n          \"data-no-drag\": \"\",\n        }}\n      />\n      {/* The label is stretched over the whole row, so a click anywhere in it toggles the switch. */}\n      <label\n        htmlFor={id}\n        className={\n          \"row-start-1 flex min-h-8 cursor-pointer items-center text-litt-small font-litt-title text-litt-fg after:absolute after:inset-0 peer-disabled:cursor-not-allowed peer-disabled:text-litt-fg-disabled \" +\n          (end ? \"col-start-1\" : \"col-start-2\")\n        }\n      >\n        {label}\n      </label>\n      {description && (\n        <p\n          id={descriptionId}\n          /* A disabled switch keeps its description at --litt-fg-subtle: it is often what says why the switch is off-limits. */\n          className={\"row-start-2 text-litt-small text-litt-fg-subtle \" + (end ? \"col-start-1\" : \"col-start-2\")}\n        >\n          {description}\n        </p>\n      )}\n    </div>\n  );\n}\n",
      "target": "components/litt/switch.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/switch.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/switch 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Switch, the Device body (proposals/worlds.md, Bodies; material.md, 8): a toggle key that flips with a detent.\n *\n * - A key (--litt-block, --litt-key-lift: its face, a dark rim and a 2px skirt) in a well (--litt-tray, --litt-tray-shade), 48 by 32, the\n *   key 24 by 26 and 3px in, so it crosses 18px (--sw-travel). Its corners are the well's less those 2px, so the two\n *   stay concentric at every shape. Two grip lines on its face tell which way it faces.\n * - The detent: the key crosses in three even steps on the instant tier (100ms), as a display's segments light, never\n *   a spring. Reduced motion zeroes the tier, so it flips at once. A drag follows the finger 1:1 and lands on the\n *   nearer side, like the other bodies.\n * - The hand: press and the key sinks the whole --litt-press-depth (2px) onto its well and its skirt goes with it.\n * - Its word (\"Off\", \"On\") sits on a small LCD beside the key, in the mono at the caption size (--litt-field, --litt-field-ink,\n *   --litt-field-shade). A lamp on the glass lights when on: the accent for tone=\"accent\", else the glass's own ink, so a\n *   list of switches stays one accent fill at most (R6).\n * - Forced colors drops shadows and fills, so the well, the key and the glass get real borders, the lamp is a system\n *   highlight when lit and an outline when not, and the key's side says the rest.\n */\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport type { CSSProperties } from \"react\";\nimport type { SwitchBodyProps } from \"../switch\";\n\nconst focusRing = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The button holds the well and the glass side by side; its label is the row's, stretched over the whole row. */\nconst housing =\n  \"[--sw-travel:20px] \" +\n  \"peer relative z-1 inline-flex h-8 shrink-0 cursor-pointer touch-pan-y items-center gap-2 self-start rounded-litt-control \" +\n  \"disabled:cursor-not-allowed \" +\n  focusRing;\n\n/* The well is cut into the body. It clips the key's cast, so nothing spills past its walls. */\nconst well =\n  \"relative inline-flex h-8 w-12 shrink-0 items-center overflow-hidden rounded-litt-control p-0.5 \" +\n  \"forced-colors:border forced-colors:p-px\";\nconst wellOn = \" bg-litt-tray shadow-(--litt-tray-shade) group-hover/switch:bg-[color-mix(in_oklab,var(--litt-tray),var(--litt-fg)_4%)]\";\nconst wellDisabled = \" bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\n/* The key's face. At rest its skirt shows 2px below it; pressed it sinks that 2px and the skirt closes. */\nconst keyBase =\n  \"pointer-events-none flex h-7 w-6 shrink-0 items-center justify-center gap-1 rounded-[calc(var(--litt-radius-control)-2px)] \" +\n  \"bg-litt-block shadow-(--litt-key-lift) \" +\n  \"group-active/switch:shadow-(--litt-block-press) group-active/switch:[transform:translateY(var(--litt-press-depth))] \" +\n  \"forced-colors:border\";\nconst keyDisabled = \"bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\nconst grip = \"h-3 w-px bg-[color-mix(in_oklab,var(--litt-fg)_28%,transparent)] forced-colors:bg-[CanvasText]\";\n\n/* LCD glass beside the key: the word in the mono at the caption size, and the lamp. */\nconst glass =\n  \"inline-flex h-6 min-w-14 shrink-0 items-center gap-1.5 rounded-litt-control bg-litt-field px-2.5 font-litt-field text-litt-caption font-litt-title \" +\n  \"shadow-(--litt-field-shade) forced-colors:border\";\nconst lamp = \"size-1.5 shrink-0 rounded-litt-pill forced-colors:border\";\n\n/* The detent: the key crosses in three steps on the instant tier; the press, the colors and the cast fade on the\n   same tier. Dragging, only the colors keep it, so the key follows the finger. */\nconst STEPS = \"steps(3, end)\";\nconst flip: CSSProperties = {\n  transitionProperty: \"translate, transform, background-color, box-shadow\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: `${STEPS}, var(--litt-ease-out), var(--litt-ease-out), var(--litt-ease-out)`,\n};\nconst follow: CSSProperties = {\n  transitionProperty: \"transform, background-color, box-shadow\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: \"var(--litt-ease-out)\",\n};\n\n/** Device, the toggle key: a key that flips in a well with a detent, its word and a lamp on LCD glass beside it. */\nexport function SwitchDeviceBody({ shown, disabled, dragging, x, tone, words, root, className }: SwitchBodyProps) {\n  const lit = !disabled && shown;\n  return (\n    <SwitchPrimitive.Root {...root} data-body=\"device\" className={housing + className}>\n      <span aria-hidden=\"true\" className={well + (disabled ? wellDisabled : wellOn)}>\n        <SwitchPrimitive.Thumb\n          className={keyBase + (disabled ? \" \" + keyDisabled : \"\")}\n          style={{ ...(dragging ? follow : flip), translate: `${x} 0` }}\n        >\n          <span className={grip} />\n          <span className={grip} />\n        </SwitchPrimitive.Thumb>\n      </span>\n      <span aria-hidden=\"true\" className={glass + (disabled ? \" opacity-50\" : \"\")}>\n        <span\n          className={\n            lamp +\n            (lit\n              ? (tone === \"accent\" ? \" bg-litt-accent shadow-[0_0_6px_var(--litt-accent)]\" : \" bg-litt-field-ink shadow-[0_0_6px_var(--litt-field-ink)]\") + \" forced-colors:bg-[Highlight]\"\n              : \" bg-litt-field-line forced-colors:bg-transparent\")\n          }\n        />\n        <span className={lit ? \"text-litt-field-ink\" : \"text-litt-field-ink-subtle\"}>{shown ? words[1] : words[0]}</span>\n      </span>\n    </SwitchPrimitive.Root>\n  );\n}\n",
      "target": "components/litt/bodies/switch.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/switch.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/switch 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Switch, the Flat body (proposals/worlds.md, Bodies; material.md, 7): a small ink pill on paper. On, the pill is ink\n * with a canvas knob at the end; off, it is an outline with a --litt-line-control knob at the start. Nothing stands up:\n * no tray, no cast. The word (\"Off\", \"On\") sits in type beside it at the small size and the text weight, in --litt-fg.\n * Both words share one grid cell, the unused one invisible, so the word never reflows the row. It is aria-hidden:\n * aria-checked says the state to a screen reader. The pill is 44 by 26 with the knob 20 and 3px in, so the crossing\n * is 18px (--sw-travel). The accent fills the pill only for tone=\"accent\", the one main switch (R6).\n */\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport type { SwitchBodyProps } from \"../switch\";\n\nconst focusRing = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The button holds the pill and its word side by side. The row's label is stretched over the whole row, so the button\n   sits above it (z-1) and a press on it still lands on the button. */\nconst root =\n  \"[--sw-travel:18px] group/pill \" +\n  \"peer relative z-1 inline-flex h-6.5 shrink-0 cursor-pointer touch-pan-y items-center gap-2 self-start rounded-litt-control \" +\n  \"disabled:cursor-not-allowed \" +\n  focusRing;\n\n/* The pill: an outline when off, ink when on. */\nconst pill =\n  \"relative inline-flex h-6.5 w-11 shrink-0 items-center overflow-hidden rounded-litt-control p-[3px] \" +\n  \"bg-transparent shadow-[inset_0_0_0_1.5px_var(--litt-line-control)] group-data-[state=checked]/pill:bg-litt-fg group-data-[state=checked]/pill:shadow-none \" +\n  \"transition-[background-color] duration-(--litt-dur-instant) ease-litt \" +\n  \"group-enabled/pill:group-hover/switch:group-data-[state=checked]/pill:bg-[color-mix(in_oklab,var(--litt-fg)_86%,var(--litt-canvas))] \" +\n  \"group-disabled/pill:bg-litt-surface-2 group-disabled/pill:opacity-40 group-disabled/pill:shadow-[inset_0_0_0_1px_var(--litt-line)] group-disabled/pill:group-data-[state=checked]/pill:bg-litt-fg group-disabled/pill:group-data-[state=checked]/pill:shadow-none \" +\n  \"forced-colors:border forced-colors:p-px\"; // the border is real, so the padding gives it back its px\n\nconst pillAccent = \" group-data-[state=checked]/pill:bg-litt-accent group-enabled/pill:group-hover/switch:group-data-[state=checked]/pill:bg-litt-accent-hover\";\n\n/* The knob keeps the control radius less 2px, as the Press block does. */\nconst knob =\n  \"pointer-events-none flex size-5 shrink-0 rounded-[calc(var(--litt-radius-control)-2px)] forced-colors:border \" +\n  \"shadow-none\";\nconst knobOff = \"bg-litt-line-control\";\nconst knobOn = \"bg-litt-canvas forced-colors:bg-[Highlight]\";\n\n/* The word, in type beside the pill, in the text weight. Both words stack in one cell so the row never reflows. */\nconst wordCell = \"grid select-none text-litt-small font-normal leading-none whitespace-nowrap text-litt-fg group-disabled/pill:text-litt-fg-disabled\";\n\n/** Flat, the pill: ink when on, an outline when off, a round knob that slides, the word in type beside it. */\nexport function SwitchFlatBody({ shown, disabled, x, tone, words, motion, root: rootProps, className }: SwitchBodyProps) {\n  return (\n    <SwitchPrimitive.Root {...rootProps} data-body=\"flat\" className={root + className}>\n      <span className={pill + (tone === \"accent\" ? pillAccent : \"\")}>\n        {/* The empty child is not decoration: without it Chromium rasters the pill and knob edges a shade differently. */}\n        <SwitchPrimitive.Thumb className={knob + \" \" + (disabled || !shown ? knobOff : knobOn)} style={{ ...motion, translate: `${x} 0` }}><span aria-hidden=\"true\" className=\"hidden\" /></SwitchPrimitive.Thumb>\n      </span>\n      <span aria-hidden=\"true\" className={wordCell}>\n        <span className={\"col-start-1 row-start-1\" + (shown ? \" invisible\" : \"\")}>{words[0]}</span>\n        <span className={\"col-start-1 row-start-1\" + (shown ? \"\" : \" invisible\")}>{words[1]}</span>\n      </span>\n    </SwitchPrimitive.Root>\n  );\n}\n",
      "target": "components/litt/bodies/switch.flat.tsx"
    }
  ],
  "docs": "Import Switch from components/litt/switch. Give it a label. Use align=\"end\" in a settings list so the switches line up at the row's end.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
