{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "type": "registry:ui",
  "title": "Select",
  "description": "One value from a short list. The trigger looks like the input, and the list is the menu's glass panel with 44px rows, a gliding highlight and a check on the current option. It stays anchored on phones.",
  "dependencies": [
    "@radix-ui/react-select",
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/glide.json",
    "https://littmd.vercel.app/r/icons.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/select.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/select 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Select: one value from a short list (components.md, \"Select\"), on @radix-ui/react-select\n * with position=\"popper\". Radix supplies the roles, typeahead, keyboard, focus return and the\n * hidden native <select> a form reads. Everything visual is written here.\n *\n * - Two parts, like Input. Select is the bare trigger and list, for a toolbar or inline use, and it\n *   needs an aria-label or aria-labelledby. SelectField adds the label above and the help or error\n *   below, all wired up, in the same layout as TextField.\n * - Trigger (Press): you press it, so it is raised, not cut (R3, material.md 6): the secondary Button's block\n *   (--litt-block, --litt-block-lift), its hover fill and its press, which tightens the cast (--litt-block-press) at once\n *   and sinks it --litt-press-depth. While its list is open it stays pressed. The Input's sizes (sm 32, md 44,\n *   lg 56, 16px text on coarse pointers), the value in --litt-fg at the text weight, the placeholder in\n *   --litt-fg-subtle, and at its end a window cut into the block (--litt-cut, --litt-cut-shade, 4px in) holding a 16px up and down mark that never turns. Focus is the 2px --litt-focus outline at a 2px offset,\n *   as on every button. An error draws a 1px --litt-danger edge inside the block and keeps its cast; disabled,\n *   it lies flat on --litt-surface-2 with the hairline. A long value truncates with an ellipsis.\n * - Flat (bodies/select.flat.tsx): a grey pill and a solid grey sheet; the current row is a grey bubble, the title weight and a check.\n * - List (Press): glass at the surface radius with a tray cut into it 4px in (--litt-tray, --litt-tray-shade), 44px slats in the tray\n *   with 12px side padding, one gliding highlight (glide.tsx), and the current slat lifted: a block face behind it (z -10, so\n *   the highlight and its ring show over it) and a 16px check in a fixed cell at its end. It is at least as wide as the trigger and never taller\n *   than the room on screen, and it scrolls inside. Opened inside glass (a dialog, a sheet, the\n *   header, a popover) it is solid (bg-litt-surface-2 + shadow-litt-float), found from the DOM when it opens.\n * - Motion, responsive tier, as the Menu. The list grows from the trigger\n *   (--radix-select-content-transform-origin) and travels a quarter of the travel token from the side\n *   it really opens on, after collision flipping. Radix Select unmounts its content on close and keeps\n *   the options in a detached fragment, so the exit is held with Content's forceMount: the select\n *   marks itself \"leaving\" in the same render that closes it, plays the exit on useExit, and then lets\n *   Radix unmount. Radix Select overrides onPlaced, so \"placed\" is read from the content's inline\n *   `animation: none`, which Radix's popper removes once it has measured and positioned the list.\n * - The highlight follows Radix's focus: Radix focuses the highlighted option, so each row reports\n *   onFocus to the glide, as Menu rows do. Opened with the pointer, nothing is lit until the pointer\n *   moves or a key is pressed. Opened with Space, Enter or an arrow, the current option is lit with\n *   the keyboard ring. A finger's press shows on the row itself (pointer-coarse:active:), and a tap\n *   never takes focus, so nothing stays lit after it.\n * - On compact screens the list stays anchored, like a dropdown menu. It is not a sheet.\n * - Closing returns focus to the trigger at once, not when the exit has finished.\n * - One contract, three bodies (proposals/worlds.md, Bodies). This file is the contract: the props, Radix's roles and keys,\n *   the value, the open state, the exit and the highlight's focus. It resolves them into SelectTriggerBodyProps,\n *   SelectListBodyProps and SelectRowBodyProps and renders the body the material picks (byWorld). The Press bodies are\n *   below, the default. bodies/select.flat.tsx is the grey pill and the solid card. bodies/select.device.tsx is an LCD\n *   readout with a pair of step keys at its end (a pointer extra: they are out of the tab order and out of the\n *   accessibility tree, and the keyboard stays Radix's) and a list drawn as a screen, its current row a lit lamp and its\n *   highlight switching row to row with no glide.\n */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type MutableRefObject,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { animate, motion, useMotionValue, type AnimationPlaybackControls, type HTMLMotionProps } from \"motion/react\";\nimport { byWorld, useExit, useMaterial, useTransition, useTravel } from \"../../lib/litt/motion\";\nimport { useOverlayMotion } from \"../../lib/litt/motion/deal\";\nimport { CheckIcon, ChevronsUpDownIcon } from \"./icons\";\nimport { GLIDE_ROW, useGlide, type Glide } from \"../../lib/litt/glide\";\nimport { SelectDeviceList, SelectDeviceRow, SelectDeviceTrigger } from \"./bodies/select.device\";\nimport { SelectFlatList, SelectFlatRow, SelectFlatTrigger } from \"./bodies/select.flat\";\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 SelectSize = \"sm\" | \"md\" | \"lg\";\n\nexport type SelectOption = {\n  /** What the form sends and onValueChange receives. Keep it unique, and not an empty string. */\n  value: string;\n  /** What people read, in the list and in the trigger. It truncates with an ellipsis instead of wrapping. */\n  label: ReactNode;\n  /** Dims the option and skips it for the pointer, the arrow keys and typeahead. */\n  disabled?: boolean;\n  /** Plain text for typeahead, and for the hidden native field, when label is not a string. */\n  textValue?: string;\n};\n\nexport type SelectProps = {\n  /** The choices, in the order they show. */\n  options: SelectOption[];\n  /** The picked value. Pair it with onValueChange. Leave it out and the select keeps its own. */\n  value?: string;\n  /** The value to start with when you do not control it. Leave it out to start on the placeholder. */\n  defaultValue?: string;\n  /** Called with the value of the option that was picked. */\n  onValueChange?: (value: string) => void;\n  /** Shown while nothing is picked, in the subtle color. It is not a label. */\n  placeholder?: ReactNode;\n  /** Height on the shared control scale: sm is 32, md is 44, lg is 56. Use lg next to an lg button, and sm in a toolbar. */\n  size?: SelectSize;\n  /** Draws the error edge and sets aria-invalid. The bare Select shows no message, so link yours with aria-describedby. */\n  invalid?: boolean;\n  /** Dims the trigger and keeps the list closed. */\n  disabled?: boolean;\n  /** Marks the field as required for assistive tech and for the hidden native field. */\n  required?: boolean;\n  /** The field name a form sends the value under. The value travels in a hidden native select inside the form. */\n  name?: string;\n  /** The id of the trigger. SelectField sets one for you. */\n  id?: string;\n  /** Extra classes for the trigger. It fills its container, so set a width on a wrapper. */\n  className?: string;\n  /** The select's accessible name, when no visible label names it. The bare Select needs this or aria-labelledby. */\n  \"aria-label\"?: string;\n  /** The id of the element that names the select, such as a visible label. */\n  \"aria-labelledby\"?: string;\n  /** The id of the help or error text under the select. */\n  \"aria-describedby\"?: string;\n  /** Whether the list is open. Pair it with onOpenChange. Leave it out and the select keeps its own state. */\n  open?: boolean;\n  /** Start open when you do not control it. */\n  defaultOpen?: boolean;\n  /** Called with the next state when the trigger, a pick, Esc or a click outside asks to change it. */\n  onOpenChange?: (open: boolean) => void;\n};\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\n/** What the list must not be glass over: any glass surface, and any popover. */\nconst FLOATING = \".litt-glass, [data-popover-surface]\";\n\n/* ---------------------------------------------------------------- trigger */\n\n/** What a trigger body draws: Radix's trigger props, the picked label and the step keys' state. */\nexport type SelectTriggerBodyProps = {\n  /** Everything Radix's trigger needs (ref, id, aria-*, the pointer and key handlers). Spread it onto the one trigger button. */\n  root: ComponentProps<typeof SelectPrimitive.Trigger>;\n  size: SelectSize;\n  /** The caller's extra classes; the body puts them on its outermost box. */\n  className: string;\n  invalid: boolean;\n  disabled: boolean;\n  /** Whether the list is open. */\n  open: boolean;\n  /** The picked option's label, or undefined while nothing is picked. */\n  selected: ReactNode;\n  placeholder: ReactNode;\n  /** Stepping through the options without opening the list: the Device body's keys. Out of the tab order. */\n  step: { prev: () => void; next: () => void; canPrev: boolean; canNext: boolean };\n};\n\n/* The Press trigger, which the Date and Color pickers copy: a raised block with a window cut into its end. */\nconst field =\n  \"group/trigger flex w-full min-w-0 cursor-pointer items-center gap-litt-control rounded-litt-control bg-litt-block text-left font-normal text-litt-fg \" +\n  \"data-placeholder:text-litt-fg-subtle \" +\n  \"pointer-coarse:text-(length:--litt-control-text-lg) \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"shadow-(--litt-block-lift) enabled:active:shadow-(--litt-block-press) data-[state=open]:shadow-(--litt-block-press) \" +\n  \"hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover) \" +\n  \"transition-[background-color,color,translate] duration-(--litt-dur-instant) ease-litt \" +\n  \"active:translate-y-(--litt-press-depth) disabled:active:translate-y-0 data-[state=open]:translate-y-(--litt-press-depth) \" +\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus \" +\n  \"aria-invalid:shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-block-lift)] \" +\n  \"aria-invalid:enabled:active:shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-block-press)] \" +\n  \"aria-invalid:data-[state=open]:shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-block-press)] \" +\n  \"disabled:cursor-not-allowed disabled:bg-litt-surface-2 disabled:text-litt-fg-disabled disabled:data-placeholder:text-litt-fg-disabled \" +\n  \"disabled:shadow-[inset_0_0_0_1px_var(--litt-line)] \" +\n  /* Forced colors (Windows high contrast) drops box-shadow, so the block keeps its shape with a real border. */\n  \"forced-colors:border\";\n\n/* The window sits 4px in from the block's end, top and bottom, so its corners are the block's less 4px. */\nconst sizes: Record<SelectSize, string> = {\n  sm: \"h-litt-control-sm pl-litt-control-pad-sm pr-1 text-litt-small pointer-coarse:h-litt-control-md\",\n  md: \"h-litt-control-md pl-litt-control-pad-md pr-1 text-litt-body\",\n  lg: \"h-litt-control-lg pl-litt-control-pad-lg pr-1 text-(length:--litt-control-text-lg)\",\n};\n\n/** The Press trigger: a raised block with a cut window at its end holding the up and down mark. */\nfunction PressTrigger({ root, size, className, selected, placeholder }: SelectTriggerBodyProps) {\n  return (\n    <SelectPrimitive.Trigger {...root} data-body=\"press\" className={[field, sizes[size], className].join(\" \")}>\n      {/* Radix drops a className on Value, so this span carries the truncation and pushes the window to the end. */}\n      <span className=\"min-w-0 flex-1 truncate\">\n        <SelectPrimitive.Value placeholder={placeholder}>{selected}</SelectPrimitive.Value>\n      </span>\n      <span\n        aria-hidden=\"true\"\n        className=\"my-1 grid aspect-square shrink-0 self-stretch place-items-center rounded-[calc(var(--litt-radius-control)-4px)] bg-litt-cut shadow-(--litt-cut-shade) group-disabled/trigger:bg-transparent group-disabled/trigger:shadow-none forced-colors:border\"\n      >\n        <SelectPrimitive.Icon {...asChildProps}>\n          <ChevronsUpDownIcon className=\"shrink-0 text-litt-fg-subtle group-disabled/trigger:text-litt-fg-disabled\" />\n        </SelectPrimitive.Icon>\n      </span>\n    </SelectPrimitive.Trigger>\n  );\n}\n\n/* --------------------------------------------------------------- the list */\n\n/** Shared by the rows of one open list. */\ntype ListContext = {\n  glide: Glide;\n  /** True while a pointer opened the list and nothing has moved or been pressed yet: no row is lit. */\n  quiet: MutableRefObject<boolean>;\n  /** True when the last press was a finger's. */\n  touch: MutableRefObject<boolean>;\n};\nconst ListCtx = createContext<ListContext | null>(null);\n\n/* A slat sits in the tray with 4px around it, so its corners are the tray's less 4px, and the tray's the glass's less 4px. The\n   current slat's raised face is a pseudo-element behind everything, so the gliding highlight and its keyboard ring still show over it. */\nconst row =\n  \"relative flex h-litt-control-md w-full scroll-my-1 cursor-pointer select-none items-center gap-3 rounded-[calc(var(--litt-radius-surface)-8px)] px-3 text-litt-small outline-none \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--litt-radius-surface)-8px)] \" +\n  \"data-[state=checked]:before:bg-litt-block data-[state=checked]:before:shadow-[var(--litt-highlight),var(--litt-block-lift)] forced-colors:data-[state=checked]:before:border \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt \" +\n  \"text-litt-fg-muted data-[state=checked]:text-litt-fg group-data-[quiet=false]/list:data-[highlighted]:text-litt-fg \" +\n  \"forced-colors:group-data-[quiet=false]/list:data-[highlighted]:[forced-color-adjust:none] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:text-[HighlightText] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:**:text-[HighlightText] \" +\n  \"data-[disabled]:pointer-events-none data-[disabled]:text-litt-fg-disabled \" +\n  \"pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-fg)_7%,transparent)] pointer-coarse:active:text-litt-fg\";\n\n/** What a row body draws: the props Radix's item needs, and the option. */\nexport type SelectRowBodyProps = {\n  item: Omit<ComponentProps<typeof SelectPrimitive.Item>, \"className\" | \"children\">;\n  option: SelectOption;\n};\n\nfunction PressRow({ item, option }: SelectRowBodyProps) {\n  return (\n    <SelectPrimitive.Item {...item} className={row}>\n      <span className=\"min-w-0 flex-1 truncate\">\n        <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n      </span>\n      <span className=\"grid size-4 shrink-0 place-items-center\">\n        <SelectPrimitive.ItemIndicator className=\"grid place-items-center text-litt-fg\">\n          <CheckIcon />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction Row({ option }: { option: SelectOption }) {\n  const ctx = useContext(ListCtx);\n  const Body = byWorld(useMaterial(), { press: PressRow, flat: SelectFlatRow, device: SelectDeviceRow });\n  if (!ctx) return null;\n  const { glide, quiet, touch } = ctx;\n  return (\n    <Body\n      option={option}\n      item={{\n        ...GLIDE_ROW,\n        value: option.value,\n        disabled: option.disabled,\n        textValue: option.textValue,\n        /* A tap would focus the row it lands on, and a focused row looks keyboard-lit. */\n        onMouseDown: (e) => {\n          if (touch.current) e.preventDefault();\n        },\n        onFocus: (e) => {\n          if (!quiet.current) glide.focusRow(e.currentTarget);\n        },\n      }}\n    />\n  );\n}\n\n/** What a list body draws: the animated surface's props, the glide, and the rows. */\nexport type SelectListBodyProps = {\n  /** The props for the one surface element: Radix's, the motion values, the quiet flag and the pointer and key handlers. */\n  root: HTMLMotionProps<\"div\"> & { ref?: Ref<HTMLDivElement>; \"data-quiet\"?: boolean };\n  /** True when the list sits inside glass (a dialog, a sheet, the header, a popover) and must be solid. */\n  flat: boolean;\n  glide: Glide;\n  children: ReactNode;\n};\n\n/** The Press list: a glass panel with a tray cut into it, the options as slats, the current one a raised block. */\nfunction PressList({ root, flat, glide, children }: SelectListBodyProps) {\n  return (\n    <motion.div\n      {...root}\n      data-body=\"press\"\n      className={\n        \"group/list relative z-50 max-h-(--radix-select-content-available-height) max-w-(--radix-select-content-available-width) \" +\n        \"min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) rounded-litt-surface p-1 outline-none \" +\n        (flat ? \"bg-litt-surface-2 shadow-litt-float\" : \"litt-glass\")\n      }\n    >\n      {/* The glass cannot clip, its shadow lives on ::before, so the viewport is the tray: it clips and scrolls the slats.\n          Radix hides the scrollbar by default; a long list gets a thin one back, so it reads as scrollable. It is its own\n          stacking context, so a slat's raised face (z -10) stays above the tray and under the highlight. */}\n      <SelectPrimitive.Viewport\n        ref={glide.ref}\n        className=\"isolate rounded-[calc(var(--litt-radius-surface)-4px)] bg-litt-tray p-1 shadow-(--litt-tray-shade) forced-colors:border overscroll-contain [&>[data-ring]]:rounded-[calc(var(--litt-radius-surface)-8px)] [scrollbar-color:var(--litt-line-strong)_transparent]! [scrollbar-width:thin]!\"\n      >\n        {glide.highlight}\n        {children}\n      </SelectPrimitive.Viewport>\n    </motion.div>\n  );\n}\n\nconst compose =\n  <A extends unknown[]>(a: ((...args: A) => void) | undefined, b: (...args: A) => void) =>\n  (...args: A) => {\n    a?.(...args);\n    b(...args);\n  };\n\ntype SurfaceProps = Omit<HTMLMotionProps<\"div\">, \"ref\" | \"children\"> & {\n  /** Radix hands the content its ref through Slot. */\n  ref?: Ref<HTMLDivElement>;\n  options: SelectOption[];\n  open: boolean;\n  leaving: boolean;\n  flat: boolean;\n  input: MutableRefObject<\"key\" | \"pointer\">;\n  onExited: () => void;\n};\n\n/**\n * The animated surface. It is the direct child of SelectPrimitive.Content (asChild), so it receives the props\n * Radix puts on the content (role, id, data-state, the keyboard handlers, the focus trap) and puts them on the\n * element. It mounts when the list opens, and stays mounted through the exit.\n */\nfunction Surface({ ref, options, open, leaving, flat, input, onExited, style, ...rest }: SurfaceProps) {\n  /* The material's motion language (@/motion/deal): it grows from om.grow, and under Device it switches on and off\n     in steps where it stands, with no slide. */\n  const om = useOverlayMotion();\n  const springIn = useTransition(\"responsive\");\n  const springOut = useExit(\"responsive\");\n  const enter = om.switches ? om.switchIn : springIn;\n  const exit = om.switches ? om.switchOut : springOut;\n  const fullTravel = useTravel();\n  const travel = om.slides ? fullTravel : 0;\n  const latest = useRef({ enter, exit, travel, onExited });\n  latest.current = { enter, exit, travel, onExited };\n\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const scale = useMotionValue(om.grow);\n  const opacity = useMotionValue(0);\n  const node = useRef<HTMLDivElement | null>(null);\n  const side = useRef<Side>(\"bottom\");\n  const entered = useRef(false);\n\n  const setNode = useCallback(\n    (el: HTMLDivElement | null) => {\n      node.current = el;\n      if (typeof ref === \"function\") return ref(el);\n      if (ref) ref.current = el;\n    },\n    [ref],\n  );\n\n  /* A quarter of the travel, starting nearer the trigger than the resting place. */\n  const offset = (s: Side) => {\n    const d = latest.current.travel / 4;\n    return { x: s === \"left\" ? d : s === \"right\" ? -d : 0, y: s === \"top\" ? d : s === \"bottom\" ? -d : 0 };\n  };\n\n  /* Enter. Radix removes the inline `animation: none` once it has measured and placed the list, and until\n     then the list sits off the page, so wait for that, then read the side it really opened on. */\n  useEffect(() => {\n    const el = node.current;\n    if (!open || !el || !el.isConnected) return;\n    let alive = true;\n    let frame = 0;\n    let runs: AnimationPlaybackControls[] = [];\n    const start = () => {\n      if (!alive) return;\n      if (el.style.animationName === \"none\") {\n        frame = requestAnimationFrame(start);\n        return;\n      }\n      side.current = (el.dataset.side as Side | undefined) ?? \"bottom\";\n      const { enter } = latest.current;\n      /* Opened again while it was still leaving: carry on from where it is. */\n      if (!entered.current) {\n        const o = offset(side.current);\n        x.jump(o.x);\n        y.jump(o.y);\n        scale.jump(om.grow);\n        opacity.jump(0);\n      }\n      entered.current = true;\n      runs = [animate(x, 0, enter), animate(y, 0, enter), animate(scale, 1, enter), animate(opacity, 1, enter)];\n    };\n    start();\n    return () => {\n      alive = false;\n      cancelAnimationFrame(frame);\n      runs.forEach((r) => r.stop());\n    };\n  }, [open, x, y, scale, opacity]);\n\n  /* Exit. The select holds this mounted (forceMount) until it is told the exit is done. */\n  useEffect(() => {\n    if (open || !leaving) return;\n    let alive = true;\n    const { exit } = latest.current;\n    const o = offset(side.current);\n    const runs = [\n      animate(x, o.x, exit),\n      animate(y, o.y, exit),\n      animate(scale, om.grow, exit),\n      animate(opacity, 0, {\n        ...exit,\n        onComplete: () => {\n          if (alive) latest.current.onExited();\n        },\n      }),\n    ];\n    return () => {\n      alive = false;\n      runs.forEach((r) => r.stop());\n    };\n  }, [open, leaving, x, y, scale, opacity]);\n\n  const glide = useGlide({ focusRows: true });\n  const gp = glide.props;\n  /* Opened by a pointer, no row is lit until the pointer moves or a key is pressed. */\n  const quietRef = useRef(input.current === \"pointer\");\n  const [quiet, setQuiet] = useState(quietRef.current);\n  const touch = useRef(false);\n  const wake = () => {\n    if (!quietRef.current) return;\n    quietRef.current = false;\n    setQuiet(false);\n  };\n  const ctx: ListContext = { glide, quiet: quietRef, touch };\n\n  const List = byWorld(useMaterial(), { press: PressList, flat: SelectFlatList, device: SelectDeviceList });\n  const root: SelectListBodyProps[\"root\"] = {\n    ...rest,\n    ref: setNode,\n    \"data-quiet\": quiet,\n    style: { ...style, x, y, scale, opacity, ...(open ? null : { pointerEvents: \"none\" as const }) },\n    onPointerEnter: compose(rest.onPointerEnter, gp.onPointerEnter),\n    onPointerLeave: compose(rest.onPointerLeave, gp.onPointerLeave),\n    onPointerMoveCapture: compose(rest.onPointerMoveCapture, (e) => {\n      if (e.pointerType !== \"touch\") wake();\n      gp.onPointerMoveCapture(e);\n    }),\n    onPointerDownCapture: compose(rest.onPointerDownCapture, (e) => {\n      touch.current = e.pointerType === \"touch\";\n      /* A finger's press shows on the row itself (its own fill), so the glide does not light it as well. */\n      if (!touch.current) gp.onPointerDownCapture(e);\n    }),\n    onKeyDownCapture: compose(rest.onKeyDownCapture, (e) => {\n      wake();\n      gp.onKeyDownCapture(e);\n    }),\n  };\n\n  return (\n    <ListCtx.Provider value={ctx}>\n      <List root={root} flat={flat} glide={glide}>\n        {options.map((o) => (\n          <Row key={o.value} option={o} />\n        ))}\n      </List>\n    </ListCtx.Provider>\n  );\n}\n\n/* ----------------------------------------------------------------- select */\n\n/**\n * The bare select: a trigger and its list, with no label. Use it in a toolbar or inline, and give it an aria-label\n * or aria-labelledby, because a placeholder is not a label. For a form field use SelectField.\n */\nexport function Select({\n  options,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  placeholder,\n  size = \"md\",\n  invalid = false,\n  disabled,\n  required,\n  name,\n  id,\n  className = \"\",\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  \"aria-describedby\": ariaDescribedBy,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n}: SelectProps) {\n  const [innerValue, setInnerValue] = useState(defaultValue ?? \"\");\n  const value = valueProp ?? innerValue;\n  const pick = (next: string) => {\n    setInnerValue(next);\n    onValueChange?.(next);\n  };\n\n  const [innerOpen, setInnerOpen] = useState(defaultOpen);\n  const open = openProp ?? innerOpen;\n  const change = (next: boolean) => {\n    setInnerOpen(next);\n    onOpenChange?.(next);\n  };\n\n  /* Closing marks the list as leaving in the same render, so Radix never sees it unmounted before the exit has played. */\n  const [prevOpen, setPrevOpen] = useState(open);\n  const [leaving, setLeaving] = useState(false);\n  if (prevOpen !== open) {\n    setPrevOpen(open);\n    setLeaving(!open);\n  }\n\n  const trigger = useRef<HTMLButtonElement | null>(null);\n  const input = useRef<\"key\" | \"pointer\">(\"key\");\n\n  /* Focus goes back to the trigger as the list starts to leave, so the next key is meant for the trigger. */\n  const wasOpen = useRef(open);\n  useEffect(() => {\n    if (wasOpen.current && !open) trigger.current?.focus({ preventScroll: true });\n    wasOpen.current = open;\n  }, [open]);\n\n  /* Glass never sits on glass: is the trigger inside another floating surface? Found before the first paint. */\n  const [flat, setFlat] = useState(false);\n  useLayoutEffect(() => {\n    if (open) setFlat(trigger.current?.closest(FLOATING) != null);\n  }, [open]);\n\n  const selected = options.find((o) => o.value === value);\n  const done = useCallback(() => setLeaving(false), []);\n\n  /* The step keys (Device): the neighbour among the options a hand can pick. Nothing picked, either key picks the first. */\n  const Trigger = byWorld(useMaterial(), { press: PressTrigger, flat: SelectFlatTrigger, device: SelectDeviceTrigger });\n  const pickable = options.filter((o) => !o.disabled);\n  const at = pickable.findIndex((o) => o.value === value);\n  const step = {\n    canPrev: !disabled && pickable.length > 0 && at !== 0,\n    canNext: !disabled && pickable.length > 0 && at !== pickable.length - 1,\n    prev: () => pickable.length > 0 && pick(pickable[Math.max(at - 1, 0)].value),\n    next: () => pickable.length > 0 && pick(pickable[Math.min(at + 1, pickable.length - 1)].value),\n  };\n\n  return (\n    <SelectPrimitive.Root\n      value={value}\n      onValueChange={pick}\n      open={open}\n      onOpenChange={change}\n      disabled={disabled}\n      required={required}\n      name={name}\n    >\n      <Trigger\n        root={{\n          ref: trigger,\n          id,\n          \"aria-label\": ariaLabel,\n          \"aria-labelledby\": ariaLabelledBy,\n          \"aria-describedby\": ariaDescribedBy,\n          \"aria-invalid\": invalid || undefined,\n          onPointerDown: () => {\n            input.current = \"pointer\";\n          },\n          onKeyDown: () => {\n            input.current = \"key\";\n          },\n        }}\n        size={size}\n        className={className}\n        invalid={invalid}\n        disabled={!!disabled}\n        open={open}\n        selected={selected?.label}\n        placeholder={placeholder}\n        step={step}\n      />\n\n      <SelectPrimitive.Portal>\n        <SelectPrimitive.Content\n          {...asChildProps}\n          position=\"popper\"\n          forceMount={leaving ? true : undefined}\n          side=\"bottom\"\n          align=\"start\"\n          sideOffset={8}\n          collisionPadding={8}\n        >\n          <Surface\n            options={options}\n            open={open}\n            leaving={leaving}\n            flat={flat}\n            input={input}\n            onExited={done}\n            aria-label={ariaLabel}\n            aria-labelledby={ariaLabelledBy}\n          />\n        </SelectPrimitive.Content>\n      </SelectPrimitive.Portal>\n    </SelectPrimitive.Root>\n  );\n}\n\n/* ------------------------------------------------------------ select field */\n\nexport type SelectFieldProps = Omit<SelectProps, \"invalid\"> & {\n  /** The label above the trigger. Required, because a placeholder is not a label. Clicking it focuses the trigger. */\n  label: ReactNode;\n  /** A hint under the field. An error replaces it. */\n  help?: ReactNode;\n  /** Shown in danger color and linked to the trigger with aria-describedby. Also sets aria-invalid. Say how to fix it. */\n  error?: ReactNode;\n};\n\n/**\n * A labelled select. The label, the hint and the error are wired to the trigger for you, in the same layout as\n * TextField: label above, hint or error below. Pass name and it works in a form.\n */\nexport function SelectField({\n  label,\n  help,\n  error,\n  id: idProp,\n  \"aria-describedby\": describedByProp,\n  \"aria-labelledby\": labelledByProp,\n  ...rest\n}: SelectFieldProps) {\n  const auto = useId();\n  const id = idProp ?? auto;\n  const labelId = `${id}-label`;\n  const noteId = `${id}-note`;\n  const note = error ?? help;\n  const describedBy = [describedByProp, note ? noteId : undefined].filter(Boolean).join(\" \") || undefined;\n  return (\n    <div className=\"grid gap-2\">\n      {/* The label points at the trigger, but a click only moves focus: a label click would otherwise open the list. */}\n      <label\n        id={labelId}\n        htmlFor={id}\n        onClick={(e) => {\n          e.preventDefault();\n          document.getElementById(id)?.focus();\n        }}\n        className={\"text-litt-small font-litt-title \" + (rest.disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}\n      >\n        {label}\n      </label>\n      <Select\n        {...rest}\n        id={id}\n        invalid={!!error}\n        aria-labelledby={[labelId, labelledByProp].filter(Boolean).join(\" \")}\n        aria-describedby={describedBy}\n      />\n      {note && (\n        <p id={noteId} className={\"text-litt-small \" + (error ? \"text-litt-danger\" : \"text-litt-fg-subtle\")}>\n          {note}\n        </p>\n      )}\n    </div>\n  );\n}\n",
      "target": "components/litt/select.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/select.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/select 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Select, the Device body (proposals/worlds.md, Bodies; material.md, 8): an LCD readout with step keys, and a list drawn\n * as a screen.\n *\n * - The trigger is a readout: the value in the mono on LCD glass (--litt-field, --litt-field-shade, --litt-field-ink), at the Input's\n *   sizes. Pressing the readout opens the list, as Radix's trigger does. Its end carries a pair of small keys\n *   (--litt-block, --litt-key-lift) that step to the previous and the next option without opening the list. They sit side by side, each\n *   a real key the full height of the glass (40px wide, 28px tall at sm and 40px at md), and 44px wide with a 44px\n *   target on a coarse pointer. They sink the whole\n *   --litt-press-depth (2px) under the hand, and one lies flat when there is nowhere left to step.\n * - The keys are a pointer extra. They are out of the tab order (tabIndex -1) and out of the accessibility tree\n *   (aria-hidden), and a press on them never moves focus, so the keyboard is Radix's alone: Space, Enter or an arrow\n *   opens the list, typing on the closed trigger picks, and Tab never lands on a key.\n * - The list is a screen: LCD glass at the control radius with the float shadow under it, rows in the mono. The current\n *   row is marked by a lamp at its start, lit in the glass's ink (the accent is the view's one fill, R6), an unlit lamp\n *   a dim dot on the other rows, so the lamp never says the state alone: the word stays, and aria-selected says it.\n * - The highlight is not a glide. It switches from row to row in place, a tint of the ink behind the row, in steps on the\n *   instant tier. The list itself switches on and off in steps where it stands (useOverlayMotion), with no slide.\n * - Forced colors drops shadows and fills, so the readout, the keys and the screen get real borders, a lit lamp is a\n *   system highlight and an unlit one an outline, and the highlighted row is Highlight with HighlightText.\n * - The contract (props, roles, keys, the exit) is select.tsx's.\n */\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { motion } from \"motion/react\";\nimport type { CSSProperties } from \"react\";\nimport type { SelectListBodyProps, SelectRowBodyProps, SelectSize, SelectTriggerBodyProps } from \"../select\";\n\n\n/* The readout is the glass; the trigger and the keys sit in it, 2px in, so their corners are the glass's less 2px. */\n/* Device changes in place, in three steps on the instant tier: the class names the tier, this is the step. */\nconst STEPS = \"steps(3, end)\";\nconst stepped: CSSProperties = { transitionTimingFunction: `${STEPS}` };\n\nconst glass =\n  \"group/readout relative flex w-full min-w-0 items-stretch gap-0.5 rounded-litt-control bg-litt-field p-0.5 font-litt-field text-litt-field-ink \" +\n  \"shadow-(--litt-field-shade) forced-colors:border \" +\n  \"has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-litt-focus \" +\n  \"data-[invalid]:shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-field-shade)] \" +\n  \"data-[disabled]:bg-litt-surface-2 data-[disabled]:text-litt-fg-disabled data-[disabled]:shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\nconst sizes: Record<SelectSize, string> = {\n  sm: \"h-litt-control-sm text-litt-small pointer-coarse:h-litt-control-md\",\n  md: \"h-litt-control-md text-litt-body\",\n  lg: \"h-litt-control-lg text-(length:--litt-control-text-lg)\",\n};\nconst pads: Record<SelectSize, string> = {\n  sm: \"pl-litt-control-pad-sm\",\n  md: \"pl-litt-control-pad-md\",\n  lg: \"pl-litt-control-pad-lg\",\n};\n\nconst readout =\n  \"flex h-full min-w-0 flex-1 cursor-pointer items-center rounded-[calc(var(--litt-radius-control)-2px)] text-left font-normal text-litt-field-ink outline-none \" +\n  \"data-placeholder:text-litt-field-ink-subtle pointer-coarse:text-(length:--litt-control-text-lg) \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt \" +\n  \"hover:bg-[color-mix(in_oklab,var(--litt-field-ink)_8%,transparent)] data-[state=open]:bg-[color-mix(in_oklab,var(--litt-field-ink)_14%,transparent)] \" +\n  \"disabled:cursor-not-allowed disabled:text-litt-fg-disabled disabled:hover:bg-transparent\";\n\nconst keys = \"flex shrink-0 gap-0.5\";\nconst keyBox =\n  \"litt-target relative grid w-10 cursor-pointer place-items-center rounded-[calc(var(--litt-radius-control)-2px)] bg-litt-block text-litt-fg-muted shadow-(--litt-key-lift) \" +\n  \"pointer-coarse:w-11 [-webkit-tap-highlight-color:transparent] \" +\n  \"transition-[transform,box-shadow,color,background-color] duration-(--litt-dur-instant) ease-litt \" +\n  \"enabled:hover:bg-(--litt-block-hover) enabled:hover:text-litt-fg \" +\n  \"enabled:active:shadow-(--litt-block-press) enabled:active:[transform:translateY(var(--litt-press-depth))] \" +\n  \"disabled:cursor-not-allowed disabled:bg-transparent disabled:text-litt-field-ink-subtle disabled:shadow-[inset_0_0_0_1px_var(--litt-field-line)] \" +\n  \"forced-colors:border\";\n\n/* Small solid triangles, the way a hardware key is marked. They draw in the key's ink. */\nfunction Arrow({ up }: { up?: boolean }) {\n  return (\n    <svg viewBox=\"0 0 8 8\" aria-hidden=\"true\" className=\"size-2 shrink-0 forced-colors:fill-[CanvasText]\">\n      <path d={up ? \"M1 5.5 4 2.5 7 5.5Z\" : \"M1 2.5 4 5.5 7 2.5Z\"} fill=\"currentColor\" />\n    </svg>\n  );\n}\n\nfunction Key({ up, onPress, disabled }: { up?: boolean; onPress: () => void; disabled: boolean }) {\n  return (\n    <button\n      type=\"button\"\n      tabIndex={-1}\n      aria-hidden=\"true\"\n      disabled={disabled}\n      /* A press never takes focus from the readout, and never starts a drag on the page behind. */\n      onMouseDown={(e) => e.preventDefault()}\n      onClick={onPress}\n      className={keyBox}\n      style={stepped}\n    >\n      <Arrow up={up} />\n    </button>\n  );\n}\n\n/** Device, the trigger: an LCD readout with a pair of step keys at its end. */\nexport function SelectDeviceTrigger({ root, size, className, invalid, disabled, selected, placeholder, step }: SelectTriggerBodyProps) {\n  return (\n    <div\n      data-body=\"device\"\n      data-invalid={invalid ? \"\" : undefined}\n      data-disabled={disabled ? \"\" : undefined}\n      className={[glass, sizes[size], className].join(\" \")}\n    >\n      <SelectPrimitive.Trigger {...root} className={[readout, pads[size]].join(\" \")} style={stepped}>\n        <span className=\"min-w-0 flex-1 truncate\">\n          <SelectPrimitive.Value placeholder={placeholder}>{selected}</SelectPrimitive.Value>\n        </span>\n      </SelectPrimitive.Trigger>\n      <span className={keys}>\n        <Key up onPress={step.prev} disabled={disabled || !step.canPrev} />\n        <Key onPress={step.next} disabled={disabled || !step.canNext} />\n      </span>\n    </div>\n  );\n}\n\n/** Device, the list: a screen of LCD glass. Its highlight is the rows' own tint, with no glide. */\nexport function SelectDeviceList({ root, glide, children }: SelectListBodyProps) {\n  return (\n    <motion.div\n      {...root}\n      data-body=\"device\"\n      className={\n        \"group/list relative z-50 max-h-(--radix-select-content-available-height) max-w-(--radix-select-content-available-width) \" +\n        \"min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) rounded-litt-control bg-litt-field font-litt-field text-litt-field-ink outline-none \" +\n        \"shadow-[var(--litt-field-shade),var(--litt-shadow-float)] forced-colors:border\"\n      }\n    >\n      <SelectPrimitive.Viewport\n        ref={glide.ref}\n        className=\"rounded-litt-control p-1 overscroll-contain [scrollbar-color:var(--litt-field-line)_transparent]! [scrollbar-width:thin]!\"\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n    </motion.div>\n  );\n}\n\nconst row =\n  \"relative flex h-litt-control-md w-full scroll-my-1 cursor-pointer select-none items-center gap-3 rounded-[calc(var(--litt-radius-control)-4px)] px-3 text-litt-small outline-none \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt \" +\n  \"text-litt-field-ink-subtle data-[state=checked]:text-litt-field-ink \" +\n  \"group-data-[quiet=false]/list:data-[highlighted]:text-litt-field-ink group-data-[quiet=false]/list:data-[highlighted]:bg-[color-mix(in_oklab,var(--litt-field-ink)_16%,transparent)] \" +\n  \"forced-colors:group-data-[quiet=false]/list:data-[highlighted]:[forced-color-adjust:none] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:bg-[Highlight] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:text-[HighlightText] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:**:text-[HighlightText] \" +\n  \"data-[disabled]:pointer-events-none data-[disabled]:opacity-40 \" +\n  \"pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-field-ink)_16%,transparent)] pointer-coarse:active:text-litt-field-ink\";\n\nconst unlit = \"size-1.5 rounded-litt-pill bg-litt-field-line forced-colors:border forced-colors:bg-transparent\";\nconst lit = \"size-1.5 rounded-litt-pill bg-litt-field-ink shadow-[0_0_6px_var(--litt-field-ink)] forced-colors:bg-[Highlight]\";\n\n/** Device, a row: a lamp at its start, lit on the current option, and the word in the mono. */\nexport function SelectDeviceRow({ item, option }: SelectRowBodyProps) {\n  return (\n    <SelectPrimitive.Item {...item} className={row} style={stepped}>\n      <span aria-hidden=\"true\" className=\"relative grid size-4 shrink-0 place-items-center\">\n        <span className={unlit} />\n        <SelectPrimitive.ItemIndicator className=\"absolute inset-0 grid place-items-center\">\n          <span className={lit} />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <span className=\"min-w-0 flex-1 truncate\">\n        <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n      </span>\n    </SelectPrimitive.Item>\n  );\n}\n",
      "target": "components/litt/bodies/select.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/select.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/select 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Select, the Flat body (proposals/worlds.md, Bodies; proposals/flat-pill.md): ink on paper.\n *\n * - The trigger is a grey pill (--litt-block is --litt-surface-3) with no outline, no shade and no window: the value in ink and a\n *   plain chevron after it. Under the hand it takes a lighter grey, and while its list is open a darker one. Nothing sinks.\n *   Focus is the ink ring (--litt-focus-ring), an error the danger ring, and disabled lies flat on --litt-surface-2.\n * - The list is a sheet, not a tray: one solid grey card (--litt-surface-3) with no glass, no shadow and no ring, which grows from\n *   the trigger. Inside glass it is the same sheet. Its rows are words parted by space, with nothing between them.\n * - The current row is the grey bubble: a darker grey behind it, its word in the title weight and its check at the end,\n *   so it is never the fill alone. The hand's highlight is the same family, a lighter bubble that glides under the pointer.\n * - Rows are pills (the control radius less the sheet's 4px padding). The bubble is a pseudo-element behind the row, so the\n *   gliding highlight and its keyboard ring still show over it.\n * - The contract (props, roles, keys, the highlight's focus, the exit) is select.tsx's.\n */\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { motion } from \"motion/react\";\nimport { CheckIcon, ChevronDownIcon } from \"../icons\";\nimport type { SelectListBodyProps, SelectRowBodyProps, SelectSize, SelectTriggerBodyProps } from \"../select\";\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\nconst field =\n  \"group/trigger flex w-full min-w-0 cursor-pointer items-center gap-litt-control rounded-litt-control bg-litt-block text-left font-normal text-litt-fg \" +\n  \"data-placeholder:text-litt-fg-subtle \" +\n  \"pointer-coarse:text-(length:--litt-control-text-lg) \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover) \" +\n  \"data-[state=open]:bg-[color-mix(in_oklab,var(--litt-block),var(--litt-fg)_9%)] \" +\n  \"transition-[background-color,color] duration-(--litt-dur-instant) ease-litt \" +\n  \"focus-visible:outline-none focus-visible:shadow-(--litt-focus-ring) \" +\n  \"aria-invalid:shadow-[0_0_0_2px_var(--litt-canvas),0_0_0_4px_var(--litt-danger)] \" +\n  \"disabled:cursor-not-allowed disabled:bg-litt-surface-2 disabled:text-litt-fg-disabled disabled:data-placeholder:text-litt-fg-disabled \" +\n  \"disabled:shadow-none \" +\n  \"forced-colors:border\";\n\nconst sizes: Record<SelectSize, string> = {\n  sm: \"h-litt-control-sm px-litt-control-pad-sm text-litt-small pointer-coarse:h-litt-control-md\",\n  md: \"h-litt-control-md px-litt-control-pad-md text-litt-body\",\n  lg: \"h-litt-control-lg px-litt-control-pad-lg text-(length:--litt-control-text-lg)\",\n};\n\n/** Flat, the trigger: a grey pill, the value in ink and a plain chevron. */\nexport function SelectFlatTrigger({ root, size, className, selected, placeholder }: SelectTriggerBodyProps) {\n  return (\n    <SelectPrimitive.Trigger {...root} data-body=\"flat\" className={[field, sizes[size], className].join(\" \")}>\n      <span className=\"min-w-0 flex-1 truncate\">\n        <SelectPrimitive.Value placeholder={placeholder}>{selected}</SelectPrimitive.Value>\n      </span>\n      <SelectPrimitive.Icon {...asChildProps}>\n        <ChevronDownIcon className=\"shrink-0 text-litt-fg-subtle group-disabled/trigger:text-litt-fg-disabled\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\n/** Flat, the list: a solid grey sheet that grows from the trigger. */\nexport function SelectFlatList({ root, glide, children }: SelectListBodyProps) {\n  return (\n    <motion.div\n      {...root}\n      data-body=\"flat\"\n      className={\n        \"group/list relative z-50 max-h-(--radix-select-content-available-height) max-w-(--radix-select-content-available-width) \" +\n        \"min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) rounded-litt-control bg-litt-surface-3 outline-none forced-colors:border\"\n      }\n    >\n      <SelectPrimitive.Viewport\n        ref={glide.ref}\n        className=\"isolate rounded-litt-control p-1 overscroll-contain [&>[data-ring]]:rounded-[calc(var(--litt-radius-control)-4px)] [scrollbar-color:var(--litt-line-strong)_transparent]! [scrollbar-width:thin]!\"\n      >\n        {glide.highlight}\n        {children}\n      </SelectPrimitive.Viewport>\n    </motion.div>\n  );\n}\n\nconst row =\n  \"relative flex h-litt-control-md w-full scroll-my-1 cursor-pointer select-none items-center gap-3 rounded-[calc(var(--litt-radius-control)-4px)] px-4 text-litt-small outline-none \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--litt-radius-control)-4px)] \" +\n  \"data-[state=checked]:before:bg-[color-mix(in_oklab,var(--litt-surface-3),var(--litt-fg)_12%)] forced-colors:data-[state=checked]:before:border \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt \" +\n  \"text-litt-fg-muted data-[state=checked]:font-litt-title data-[state=checked]:text-litt-fg group-data-[quiet=false]/list:data-[highlighted]:text-litt-fg \" +\n  \"forced-colors:group-data-[quiet=false]/list:data-[highlighted]:[forced-color-adjust:none] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:text-[HighlightText] forced-colors:group-data-[quiet=false]/list:data-[highlighted]:**:text-[HighlightText] \" +\n  \"data-[disabled]:pointer-events-none data-[disabled]:text-litt-fg-disabled \" +\n  \"pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-fg)_7%,transparent)] pointer-coarse:active:text-litt-fg\";\n\n/** Flat, a row: a word, and on the current one a grey bubble, the title weight and a check. */\nexport function SelectFlatRow({ item, option }: SelectRowBodyProps) {\n  return (\n    <SelectPrimitive.Item {...item} className={row}>\n      <span className=\"min-w-0 flex-1 truncate\">\n        <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n      </span>\n      <span className=\"grid size-4 shrink-0 place-items-center\">\n        <SelectPrimitive.ItemIndicator className=\"grid place-items-center text-litt-fg\">\n          <CheckIcon />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n    </SelectPrimitive.Item>\n  );\n}\n",
      "target": "components/litt/bodies/select.flat.tsx"
    }
  ],
  "docs": "Import Select or SelectField from components/litt/select. Pass options as { value, label } and a label, because a placeholder is not a label.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
