{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-picker",
  "type": "registry:ui",
  "title": "Card picker",
  "description": "One choice shown as cards: tiles of a picture over a word in one tray, or cards with a title, a line and a detail such as a price, in a row when they fit and stacked when they do not. Native radios; the picked card is the raised block, gliding to the pick.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/icons.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/card-picker.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/card-picker 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Card picker (components.md, \"Card picker\"): one choice shown as cards, such as a plan, an onboarding question, or a\n * theme or a layout. A fieldset of native radios, so the role, the arrow keys, required and the form value come from\n * the browser.\n *\n * - Material (Press, the instrument): one sunken tray (R3: --litt-tray, --litt-tray-shade) holds every card, and each card is a plate\n *   in it. The picked plate rises out of the tray and catches the light (--litt-block, --litt-highlight, --litt-block-lift); the\n *   others sit seated (sunk --litt-press-depth, --litt-block-press), so unpicked cards read as a choice and not as static cards,\n *   and a disabled one lies flat in the floor, level with the tray (--litt-cut-shade). Never an accent fill (R6).\n * - Flat (proposals/worlds.md): the page. No tray and no card: pictures and words, and the picked one in a grey bubble\n *   (--litt-block) with an ink tick and its word in --litt-fg. Grey means chosen.\n * - Device: a plate with a lamp in its corner, the picked one set down 2px into a well with its lamp lit.\n * - Every card in a picker has the same size: equal columns, or equal rows when stacked. So a plate or a bubble never\n *   changes shape between picks, and the details (a price) line up across a row.\n * - sm: tiles, a picture over a word in one row of 2 to 5, the label --litt-fg-subtle and the picked one --litt-fg. The tray\n *   sits at the control radius and the tiles 2px inside it. It draws no description or detail.\n * - lg (the default): cards with a title (--litt-fg), a line (--litt-fg-subtle) and a detail such as a price. The tray sits at\n *   the surface radius with a 4px inset, so the cards take the control radius and stay concentric. The cards sit in\n *   one row while each gets 12rem of the picker's own width (a container query, so it works in a sheet or a sidebar),\n *   and stack as rows otherwise. ARRANGE is keyed by the count, so no card is ever left alone on a row.\n * - Names: each radio is named by its title and described by the description, then the detail, so a screen reader\n *   hears \"Pro, radio button, Unlimited projects. $12 a month\". Pictures are hidden. A disabled card keeps its\n *   description in --litt-fg-subtle, since it says why.\n * - Nothing starts picked without value or defaultValue: a question must not answer itself. value={null} keeps a\n *   controlled picker empty.\n * - Bodies (proposals/worlds.md): this file keeps the contract (the fieldset, the legend, the radios, the pick and the\n *   words they are named by) and resolves it once into CardPickerBodyProps. It renders the body the material picks\n *   (byWorld). Press, here, is plates in a tray. Flat (bodies/card-picker.flat.tsx) and Device\n *   (bodies/card-picker.device.tsx) are the same radios in other bodies. The arrow keys and every accessible name are\n *   the same in all three.\n * - Motion: nothing glides in Press: a plate rises and sinks where it stands, and colors change on the instant tier. Flat's\n *   bubble glides on the responsive tier (fun mode's glide spring in fun mode) and appears at once on the first pick;\n *   Device switches in place in three steps. Reduced motion turns the glide off. The focus ring is drawn from the\n *   radio's :focus-visible, inset, so a mouse never shows it.\n */\nimport { useId, useState, type ReactNode } from \"react\";\nimport type { Transition } from \"motion/react\";\nimport { byWorld, useFun, useMaterial, useTransition } from \"../../lib/litt/motion\";\nimport { CardPickerDeviceBody } from \"./bodies/card-picker.device\";\nimport { CardPickerFlatBody } from \"./bodies/card-picker.flat\";\n\nexport type CardPickerSize = \"sm\" | \"lg\";\n\n/** One choice: a card. */\nexport type CardPickerOption<T extends string> = {\n  /** What onValueChange gets and what a form sends. */\n  value: T;\n  /** The choice's name: a card's title, or the word under a tile's picture. It is the radio's accessible name. A word or two at sm. */\n  label: string;\n  /** A line under a card's title: what the choice gives you. Read after the name. Not drawn at sm. */\n  description?: ReactNode;\n  /** A short detail at a card's end, such as a price (\"$12 a month\"). Read after the description. Not drawn at sm. */\n  detail?: ReactNode;\n  /** What the choice looks like, drawn in currentColor where it can be, so the picked card's picture steps up to --litt-fg. Hidden from screen readers. */\n  picture?: ReactNode;\n  /** Makes this one option unavailable, and the arrow keys skip it. Say why in its description. */\n  disabled?: boolean;\n};\n\nexport type CardPickerProps<T extends string> = {\n  /** The question or the setting's name, shown above the cards and read with each one. Required. */\n  legend: ReactNode;\n  /** Hides the legend from sight (a screen reader still reads it), when the page already asks the question. */\n  hideLegend?: boolean;\n  /** The radios' shared name, which is what makes the arrow keys move between them and what a form sends. Made up for you when left out. */\n  name?: string;\n  /** The picked option's value, when you control it. Pair it with onValueChange. null keeps it controlled with nothing picked. */\n  value?: T | null;\n  /** The option picked at first when you do not control it. Without one, nothing is picked until the person picks. */\n  defaultValue?: T;\n  /** Called with the picked option's value each time the pick changes, by click, tap or arrow key. */\n  onValueChange?: (value: T) => void;\n  /** The choices, in order: two to five at sm, two to six at lg. More belong in a Select. */\n  options: CardPickerOption<T>[];\n  /** sm: tiles, a picture over a word in one row, for a setting. lg: cards with a title, a line and a detail, in a row when each gets 12rem and stacked when not. */\n  size?: CardPickerSize;\n  /** Asks the browser to refuse a form sent with nothing picked. */\n  required?: boolean;\n  /** A hint under the cards. An error replaces it. */\n  help?: ReactNode;\n  /** Shown in danger color under the cards and linked to the group. Also marks every radio invalid. Say how to fix it. */\n  error?: ReactNode;\n  /** Disables every option, through the fieldset. */\n  disabled?: boolean;\n  /** Extra classes for the fieldset, such as a width. */\n  className?: string;\n};\n\n/** The class strings that decide where lg's cards sit: the tray's columns, the card's direction and the detail's place, all at one width. */\nexport type CardPickerArrange = { tray: string; card: string; detail: string };\n\n/** Where lg's cards go from stacked rows to columns, by count: each column gets 12rem, and no row holds one card alone. */\nconst ARRANGE: Record<2 | 3 | 4 | 5 | 6, CardPickerArrange> = {\n  2: { tray: \"@min-[24rem]:grid-cols-2\", card: \"@min-[24rem]:flex-col\", detail: \"@min-[24rem]:mt-auto\" },\n  3: { tray: \"@min-[36rem]:grid-cols-3\", card: \"@min-[36rem]:flex-col\", detail: \"@min-[36rem]:mt-auto\" },\n  4: { tray: \"@min-[24rem]:grid-cols-2 @min-[48rem]:grid-cols-4\", card: \"@min-[24rem]:flex-col\", detail: \"@min-[24rem]:mt-auto\" },\n  5: { tray: \"@min-[36rem]:grid-cols-3\", card: \"@min-[36rem]:flex-col\", detail: \"@min-[36rem]:mt-auto\" },\n  6: { tray: \"@min-[24rem]:grid-cols-2 @min-[36rem]:grid-cols-3\", card: \"@min-[24rem]:flex-col\", detail: \"@min-[24rem]:mt-auto\" },\n};\n\n/** One card, resolved: its option, whether it is picked or off, and the ids its radio is named and described by. */\nexport type CardPickerCard = {\n  option: CardPickerOption<string>;\n  picked: boolean;\n  /** Unavailable: the whole picker is disabled, or this option is. */\n  off: boolean;\n  /** The card's id: its title is `${id}-label`, its line `${id}-description` and its detail `${id}-detail`. */\n  id: string;\n  /** The ids the radio is described by (lg), space separated; empty when there is none. */\n  described: string;\n};\n\n/** What a body gets: the state resolved once, and the radios' wiring. Every body renders the same fieldset of native radios. */\nexport type CardPickerBodyProps = {\n  legend: ReactNode;\n  hideLegend: boolean;\n  /** The whole picker is disabled (the fieldset). */\n  disabled: boolean;\n  className: string;\n  /** The radios' shared name. */\n  name: string;\n  size: CardPickerSize;\n  required: boolean;\n  /** Any option has a picture (lg then keeps a column for it). */\n  pictures: boolean;\n  /** An error is showing: the tray outlines in danger and every radio is invalid. */\n  invalid: boolean;\n  /** The hint, or the error that replaces it. */\n  note: ReactNode;\n  /** The id the note is drawn with and the fieldset points at. */\n  noteId: string;\n  /** The picker's own id, so two pickers never share a gliding surface. */\n  groupId: string;\n  arrange: CardPickerArrange;\n  cards: CardPickerCard[];\n  /** How the picked surface glides, for the bodies that glide with the layout engine. */\n  glide: Transition;\n  /** Picks an option: sets the inner state and calls onValueChange. */\n  pick: (value: string) => void;\n};\n\nconst SIZES: Record<CardPickerSize, { tray: string; card: string; corner: string }> = {\n  sm: {\n    tray: \"grid grid-flow-col auto-cols-fr rounded-litt-control p-0.5\",\n    card: \"grid min-w-0 justify-items-center gap-2 px-1 pt-3 pb-2.5 text-litt-caption font-litt-title\",\n    corner: \"rounded-[calc(var(--litt-radius-control)-2px)]\",\n  },\n  lg: {\n    tray: \"grid grid-cols-1 auto-rows-fr gap-1 rounded-litt-surface p-1\",\n    card: \"flex min-h-16 min-w-0 items-start gap-3 p-4 text-left\",\n    /* 4px into the tray's surface radius: the control radius at every shape but the pill, where that would be rounder than the tray. */\n    corner: \"rounded-[calc(var(--litt-radius-surface)-4px)]\",\n  },\n};\n\n/* The plates. A plate in the tray sits seated (sunk the press depth, the cast tight); the picked one stands raised out of\n   the tray and lit (the cast wide, the top edge lit); a disabled one lies flat in the tray floor. */\nconst seated = \"bg-[color-mix(in_oklab,var(--litt-block)_45%,var(--litt-tray))] shadow-(--litt-block-press) [transform:translateY(var(--litt-press-depth))]\";\nconst raised = \"bg-litt-block shadow-[var(--litt-highlight),var(--litt-block-lift)]\";\nconst lying = \"bg-[color-mix(in_oklab,var(--litt-block)_45%,var(--litt-tray))] shadow-(--litt-cut-shade)\";\nconst hold = \"hover:bg-[color-mix(in_oklab,var(--litt-block)_70%,var(--litt-tray))] active:bg-litt-block active:shadow-(--litt-block-press)\";\nconst tweens = \"transition-[color,background-color,transform] duration-(--litt-dur-instant) ease-litt [-webkit-tap-highlight-color:transparent]\";\n\n/**\n * Press, the instrument: one tray cut into the page, and every card a plate standing in it. The plate you picked is\n * raised out of the tray and lit; the others sit seated in it; a disabled one lies flat in the floor. Nothing\n * glides: a plate rises and sinks where it stands.\n */\nfunction PressBody({ legend, hideLegend, disabled, className, name, size, required, invalid, note, noteId, arrange, cards, pictures, pick }: CardPickerBodyProps) {\n  const look = SIZES[size];\n  const tray = (\n    <div\n      className={\n        look.tray +\n        \" bg-litt-tray forced-colors:border \" +\n        (invalid ? \"shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-tray-shade)]\" : \"shadow-(--litt-tray-shade)\") +\n        (size === \"lg\" ? \" \" + arrange.tray : \"\")\n      }\n    >\n      {cards.map(({ option: o, picked, off, id, described }) => (\n        <label\n          key={o.value}\n          className={\n            \"relative select-none \" + look.corner + \" \" + look.card + (size === \"lg\" ? \" \" + arrange.card : \"\") + \" \" +\n            tweens + \" \" +\n            (picked ? raised : off ? lying : seated + \" \" + hold) + \" \" +\n            (off\n              ? \"cursor-not-allowed \" + (picked ? \"text-litt-fg-subtle\" : \"text-litt-fg-disabled\")\n              : \"cursor-pointer \" + (picked ? \"text-litt-fg\" : \"text-litt-fg-subtle hover:text-litt-fg\")) +\n            (picked ? \" forced-colors:border-2\" : \" forced-colors:border\")\n          }\n        >\n          <input\n            type=\"radio\"\n            className=\"peer sr-only\"\n            name={name}\n            value={o.value}\n            checked={picked}\n            disabled={o.disabled}\n            required={required}\n            aria-invalid={invalid ? true : undefined}\n            aria-labelledby={size === \"lg\" ? `${id}-label` : undefined}\n            aria-describedby={size === \"lg\" && described ? described : undefined}\n            onChange={() => pick(o.value)}\n          />\n          {size === \"sm\" ? (\n            <>\n              <span aria-hidden=\"true\" className=\"relative z-1 grid h-8 w-full place-items-center\">\n                {o.picture}\n              </span>\n              <span className=\"litt-trim relative z-1 max-w-full\">{o.label}</span>\n            </>\n          ) : (\n            <>\n              {pictures && (\n                <span aria-hidden=\"true\" className=\"relative z-1 grid min-h-6 min-w-6 shrink-0 items-center justify-items-start\">\n                  {o.picture}\n                </span>\n              )}\n              <span className=\"relative z-1 grid min-w-0 flex-1 gap-1\">\n                <span id={`${id}-label`} className={\"text-litt-body font-litt-title \" + (off ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>\n                  {o.label}\n                </span>\n                {o.description && (\n                  <span id={`${id}-description`} className=\"text-litt-small text-litt-fg-subtle\">\n                    {o.description}\n                  </span>\n                )}\n              </span>\n              {o.detail && (\n                <span id={`${id}-detail`} className={\"relative z-1 shrink-0 text-litt-body font-litt-title tabular-nums \" + arrange.detail + (off ? \" text-litt-fg-disabled\" : \" text-litt-fg\")}>\n                  {o.detail}\n                </span>\n              )}\n            </>\n          )}\n          <span\n            aria-hidden=\"true\"\n            className={\n              \"pointer-events-none absolute inset-0 z-1 \" + look.corner + \" peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-litt-focus\"\n            }\n          />\n        </label>\n      ))}\n    </div>\n  );\n  return (\n    <fieldset data-body=\"press\" disabled={disabled} aria-describedby={note ? noteId : undefined} className={\"m-0 min-w-0 border-0 p-0 \" + className}>\n      <legend className={hideLegend ? \"sr-only\" : \"mb-2 p-0 text-litt-small font-litt-title \" + (disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>{legend}</legend>\n      {size === \"lg\" ? <div className=\"@container\">{tray}</div> : tray}\n      {note && (\n        <p id={noteId} className={\"mt-2 text-litt-small \" + (invalid ? \"text-litt-danger\" : \"text-litt-fg-subtle\")}>\n          {note}\n        </p>\n      )}\n    </fieldset>\n  );\n}\n\n/** One choice shown as cards: tiles of a picture over a word (sm), or cards with a title, a line and a detail such as a price (lg). */\nexport function CardPicker<T extends string>({\n  legend,\n  hideLegend = false,\n  name: nameProp,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  options,\n  size = \"lg\",\n  required = false,\n  help,\n  error,\n  disabled = false,\n  className = \"\",\n}: CardPickerProps<T>) {\n  const auto = useId();\n  const name = nameProp ?? auto;\n  const responsive = useTransition(\"responsive\");\n  const fun = useFun();\n  const glide = fun ? fun.glide : responsive;\n  const [inner, setInner] = useState<T | null>(defaultValue ?? null);\n  const value = valueProp !== undefined ? valueProp : inner;\n  const note = error ?? help;\n  const arrange = ARRANGE[Math.min(Math.max(options.length, 2), 6) as 2 | 3 | 4 | 5 | 6];\n  const Body = byWorld(useMaterial(), { press: PressBody, flat: CardPickerFlatBody, device: CardPickerDeviceBody });\n\n  const cards: CardPickerCard[] = options.map((o, i) => {\n    const id = `${auto}-${i}`;\n    return {\n      option: o as CardPickerOption<string>,\n      picked: value === o.value,\n      off: disabled || !!o.disabled,\n      id,\n      described: [o.description && `${id}-description`, o.detail && `${id}-detail`].filter(Boolean).join(\" \"),\n    };\n  });\n\n  return (\n    <Body\n      legend={legend}\n      hideLegend={hideLegend}\n      disabled={disabled}\n      className={className}\n      name={name}\n      size={size}\n      required={required}\n      pictures={options.some((o) => o.picture)}\n      invalid={!!error}\n      note={note}\n      noteId={`${auto}-note`}\n      groupId={auto}\n      arrange={arrange}\n      cards={cards}\n      glide={glide}\n      pick={(v) => {\n        setInner(v as T);\n        onValueChange?.(v as T);\n      }}\n    />\n  );\n}\n",
      "target": "components/litt/card-picker.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/card-picker.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/card-picker 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Card picker, the Device body (proposals/worlds.md, Bodies; material.md, 8): a bank of plates with a lamp each.\n *\n * - One well (--litt-tray, --litt-tray-shade) holds every card, and each card is a key plate on its skirt (--litt-block, --litt-key-lift: a\n *   face, a dark rim and 2px of skirt), at the corner radius of the well less its inset, so the two stay concentric at\n *   every shape. The words are in the mono (font-litt-field); at sm the word under the picture is an engraved legend\n *   (litt-legend, --litt-engrave).\n * - A lamp sits in each plate's corner (lg: the top end; sm: the top middle, over the picture). It is size-1.5 and\n *   round. At lg it stands in from the corner by the card's radius at least, so a round shape never cuts across it.\n *   The picked plate is latched: set down the whole --litt-press-depth (2px) into the well, so its skirt is gone, its face a\n *   step darker (--litt-block mixed 55% toward --litt-tray) and inset-shaded (--litt-tray-shade), and its lamp lit in --litt-fg. The\n *   others stand, their lamp a dim ring. A press on a standing plate sinks it the same 2px before it latches, and\n *   picking one lets the last one up. The words say it too: the radio is checked.\n * - A disabled plate lies flat in the well (--litt-surface-2, a hairline, no skirt) with its lamp dim; its line keeps\n *   --litt-fg-subtle, since it says why.\n * - Nothing glides. The change switches in place in three steps on the instant tier (100ms), as the Switch's detent\n *   does; reduced motion zeroes the tier, so it happens at once.\n * - Forced colors drops shadows and fills, so the well and each plate get real borders, the picked one a 2px one, a lit\n *   lamp is a system highlight and a dim one an outline.\n */\nimport type { CSSProperties } from \"react\";\nimport type { CardPickerBodyProps, CardPickerSize } from \"../card-picker\";\n\n/* The detent: the sink, the colors and the cast change in three steps on the instant tier. */\nconst STEPS = \"steps(3, end)\";\nconst detent: CSSProperties = {\n  transitionProperty: \"transform, color, box-shadow, background-color\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: `${STEPS}, ${STEPS}, ${STEPS}, ${STEPS}`,\n};\n\n/* A plate standing, a plate down (latched), and a plate lying flat (disabled). */\nconst up = \"bg-litt-block shadow-(--litt-key-lift) active:shadow-(--litt-block-press) active:[transform:translateY(var(--litt-press-depth))]\";\nconst down = \"bg-[color-mix(in_oklab,var(--litt-block),var(--litt-tray)_55%)] shadow-(--litt-tray-shade) [transform:translateY(var(--litt-press-depth))] forced-colors:border-2\";\nconst flat = \"bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\nconst SIZES: Record<CardPickerSize, { well: string; plate: string; corner: string; lamp: string }> = {\n  sm: {\n    well: \"grid grid-flow-col auto-cols-fr rounded-litt-control p-0.5\",\n    plate: \"grid min-w-0 justify-items-center gap-2 px-1 pt-4 pb-2.5 text-litt-caption\",\n    corner: \"rounded-[calc(var(--litt-radius-control)-2px)]\",\n    lamp: \"top-1.5 left-1/2 -translate-x-1/2\",\n  },\n  lg: {\n    well: \"grid grid-cols-1 auto-rows-fr gap-1 rounded-litt-surface p-1\",\n    plate: \"flex min-h-16 min-w-0 items-start gap-3 p-4 pr-9 text-left\",\n    corner: \"rounded-[calc(var(--litt-radius-surface)-4px)]\",\n    /* On the title's line, in from the right edge by 12px or the card's radius if that is more, so the curve never cuts across it. */\n    lamp: \"top-[calc(1rem+9px)] right-[max(0.75rem,calc(var(--litt-radius-surface)-4px))]\",\n  },\n};\n\n/** Device, the plates: a bank of plates in a well, each with a lamp, the picked one set down into the well with its lamp lit. */\nexport function CardPickerDeviceBody({ legend, hideLegend, disabled, className, name, size, required, invalid, note, noteId, arrange, cards, pictures, pick }: CardPickerBodyProps) {\n  const look = SIZES[size];\n  const well = (\n    <div\n      className={\n        look.well +\n        \" bg-litt-tray forced-colors:border \" +\n        (invalid ? \"shadow-[inset_0_0_0_1px_var(--litt-danger),var(--litt-tray-shade)]\" : \"shadow-(--litt-tray-shade)\") +\n        (size === \"lg\" ? \" \" + arrange.tray : \"\")\n      }\n    >\n      {cards.map(({ option: o, picked, off, id, described }) => (\n        <label\n          key={o.value}\n          style={detent}\n          className={\n            \"relative select-none forced-colors:border \" + look.corner + \" \" + look.plate + (size === \"lg\" ? \" \" + arrange.card : \"\") + \" \" +\n            \"[-webkit-tap-highlight-color:transparent] \" +\n            (off ? \"cursor-not-allowed text-litt-fg-disabled \" + flat : \"cursor-pointer \" + (picked ? \"text-litt-fg \" + down : \"text-litt-fg-subtle hover:text-litt-fg \" + up))\n          }\n        >\n          <input\n            type=\"radio\"\n            className=\"peer sr-only\"\n            name={name}\n            value={o.value}\n            checked={picked}\n            disabled={o.disabled}\n            required={required}\n            aria-invalid={invalid ? true : undefined}\n            aria-labelledby={size === \"lg\" ? `${id}-label` : undefined}\n            aria-describedby={size === \"lg\" && described ? described : undefined}\n            onChange={() => pick(o.value)}\n          />\n          {/* The lamp: decoration, so a screen reader hears the radio alone. */}\n          <span\n            aria-hidden=\"true\"\n            style={detent}\n            className={\n              \"pointer-events-none absolute size-1.5 rounded-litt-pill forced-colors:border \" + look.lamp + \" \" +\n              (picked && !off ? \"bg-litt-fg shadow-[0_0_6px_var(--litt-fg)] forced-colors:bg-[Highlight]\" : \"shadow-[inset_0_0_0_1px_var(--litt-fg-disabled)]\")\n            }\n          />\n          {size === \"sm\" ? (\n            <>\n              <span aria-hidden=\"true\" className=\"relative z-1 grid h-8 w-full place-items-center\">\n                {o.picture}\n              </span>\n              <span className=\"litt-trim litt-legend relative z-1 max-w-full font-litt-field font-litt-title\">{o.label}</span>\n            </>\n          ) : (\n            <>\n              {pictures && (\n                <span aria-hidden=\"true\" className=\"relative z-1 grid min-h-6 min-w-6 shrink-0 items-center justify-items-start\">\n                  {o.picture}\n                </span>\n              )}\n              <span className=\"relative z-1 grid min-w-0 flex-1 gap-1\">\n                <span id={`${id}-label`} className={\"font-litt-field text-litt-body font-litt-title \" + (off ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>\n                  {o.label}\n                </span>\n                {o.description && (\n                  <span id={`${id}-description`} className=\"text-litt-small text-litt-fg-subtle\">\n                    {o.description}\n                  </span>\n                )}\n              </span>\n              {o.detail && (\n                <span id={`${id}-detail`} className={\"relative z-1 shrink-0 font-litt-field text-litt-body font-litt-title tabular-nums \" + arrange.detail + (off ? \" text-litt-fg-disabled\" : \" text-litt-fg\")}>\n                  {o.detail}\n                </span>\n              )}\n            </>\n          )}\n          <span\n            aria-hidden=\"true\"\n            className={\"pointer-events-none absolute inset-0 z-1 \" + look.corner + \" peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-litt-focus\"}\n          />\n        </label>\n      ))}\n    </div>\n  );\n  return (\n    <fieldset data-body=\"device\" disabled={disabled} aria-describedby={note ? noteId : undefined} className={\"m-0 min-w-0 border-0 p-0 \" + className}>\n      <legend className={hideLegend ? \"sr-only\" : \"mb-2 p-0 text-litt-small font-litt-title \" + (disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>{legend}</legend>\n      {size === \"lg\" ? <div className=\"@container\">{well}</div> : well}\n      {note && (\n        <p id={noteId} className={\"mt-2 text-litt-small \" + (invalid ? \"text-litt-danger\" : \"text-litt-fg-subtle\")}>\n          {note}\n        </p>\n      )}\n    </fieldset>\n  );\n}\n",
      "target": "components/litt/bodies/card-picker.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/card-picker.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/card-picker 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Card picker, the Flat body (proposals/worlds.md, Bodies; material.md, 7): pictures and words on the page. There is no\n * tray and no card: an option is its picture over or beside its words, in --litt-fg-muted, set apart by its own padding,\n * and the hit areas touch, so a pointer never lands between two. The one you chose sits in a grey bubble (--litt-block, at\n * the surface radius) with an ink tick (--litt-fg) and its word stepped up to --litt-fg. The bubble glides to the pick. Grey means\n * chosen, so an unpicked option has no ground until a hand is on it, and then it takes a grey wash.\n * Forced colors gives the bubble a real border, the only edge, since there is no tray to keep.\n */\nimport { LayoutGroup, motion } from \"motion/react\";\nimport { CheckIcon } from \"../icons\";\nimport type { CardPickerBodyProps, CardPickerSize } from \"../card-picker\";\n\n/* A hand on an unpicked option: a grey wash (fg 6%, color only), and the word steps up to --litt-fg. */\nconst WASH = \"hover:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] hover:text-litt-fg active:bg-[color-mix(in_oklab,var(--litt-fg)_10%,transparent)] active:text-litt-fg\";\n\nconst SIZES: Record<CardPickerSize, { tray: string; card: string }> = {\n  sm: {\n    tray: \"grid grid-flow-col auto-cols-fr\",\n    card: \"grid min-w-0 justify-items-center gap-2 px-1 pt-3 pb-2.5 text-litt-caption font-litt-title\",\n  },\n  lg: {\n    tray: \"grid grid-cols-1 auto-rows-fr\",\n    card: \"flex min-h-16 min-w-0 items-start gap-3 p-4 text-left\",\n  },\n};\n\n/** Flat, pictures and words, the pick in a grey bubble with an ink tick. */\nexport function CardPickerFlatBody({ legend, hideLegend, disabled, className, name, size, required, invalid, note, noteId, groupId, arrange, cards, pictures, glide, pick }: CardPickerBodyProps) {\n  const look = SIZES[size];\n  const tray = (\n    <div className={look.tray + (size === \"lg\" ? \" \" + arrange.tray : \"\")}>\n      {cards.map(({ option: o, picked, off, id, described }) => (\n        <label\n          key={o.value}\n          className={\n            \"relative select-none rounded-litt-surface \" + look.card + (size === \"lg\" ? \" \" + arrange.card : \"\") + \" \" +\n            \"transition-colors duration-(--litt-dur-instant) ease-litt [-webkit-tap-highlight-color:transparent] \" +\n            (off\n              ? \"cursor-not-allowed \" + (picked ? \"text-litt-fg-subtle\" : \"text-litt-fg-disabled\")\n              : \"cursor-pointer \" + (picked ? \"text-litt-fg\" : \"text-litt-fg-muted \" + WASH))\n          }\n        >\n          <input\n            type=\"radio\"\n            className=\"peer sr-only\"\n            name={name}\n            value={o.value}\n            checked={picked}\n            disabled={o.disabled}\n            required={required}\n            aria-invalid={invalid ? true : undefined}\n            aria-labelledby={size === \"lg\" ? `${id}-label` : undefined}\n            aria-describedby={size === \"lg\" && described ? described : undefined}\n            onChange={() => pick(o.value)}\n          />\n          {picked && (\n            <motion.span\n              layoutId=\"card-picker-bubble\"\n              transition={glide}\n              aria-hidden=\"true\"\n              className={\"absolute inset-0 rounded-litt-surface forced-colors:border-2 \" + (off ? \"bg-litt-surface-2\" : \"bg-litt-block\")}\n            />\n          )}\n          {size === \"sm\" ? (\n            <>\n              <span aria-hidden=\"true\" className=\"relative z-1 grid h-8 w-full place-items-center\">\n                {o.picture}\n              </span>\n              <span className=\"litt-trim relative z-1 max-w-full\">{o.label}</span>\n              {/* The tick: ink, in the bubble's corner. Decoration, since the radio says it is checked. */}\n              {picked && <CheckIcon className=\"absolute top-2 right-2 z-1\" />}\n            </>\n          ) : (\n            <>\n              {pictures && (\n                <span aria-hidden=\"true\" className=\"relative z-1 grid min-h-6 min-w-6 shrink-0 items-center justify-items-start\">\n                  {o.picture}\n                </span>\n              )}\n              <span className=\"relative z-1 grid min-w-0 flex-1 gap-1\">\n                <span id={`${id}-label`} className={\"flex items-center gap-2 text-litt-body font-litt-title \" + (off ? \"text-litt-fg-disabled\" : picked ? \"text-litt-fg\" : \"text-inherit\")}>\n                  <span className=\"min-w-0\">{o.label}</span>\n                  {picked && <CheckIcon className=\"shrink-0\" />}\n                </span>\n                {o.description && (\n                  <span id={`${id}-description`} className=\"text-litt-small text-litt-fg-subtle\">\n                    {o.description}\n                  </span>\n                )}\n              </span>\n              {o.detail && (\n                <span id={`${id}-detail`} className={\"relative z-1 shrink-0 text-litt-body font-litt-title tabular-nums \" + arrange.detail + (off ? \" text-litt-fg-disabled\" : picked ? \" text-litt-fg\" : \" text-inherit\")}>\n                  {o.detail}\n                </span>\n              )}\n            </>\n          )}\n          {/* The words and the ring sit above the bubble (z-1), so it never covers them while it glides. */}\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 z-1 rounded-litt-surface peer-focus-visible:outline-2 peer-focus-visible:-outline-offset-2 peer-focus-visible:outline-litt-focus\"\n          />\n        </label>\n      ))}\n    </div>\n  );\n  return (\n    <fieldset data-body=\"flat\" disabled={disabled} aria-describedby={note ? noteId : undefined} className={\"m-0 min-w-0 border-0 p-0 \" + className}>\n      <legend className={hideLegend ? \"sr-only\" : \"mb-2 p-0 text-litt-small font-litt-title \" + (disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>{legend}</legend>\n      <LayoutGroup id={groupId}>{size === \"lg\" ? <div className=\"@container\">{tray}</div> : tray}</LayoutGroup>\n      {note && (\n        <p id={noteId} className={\"mt-2 text-litt-small \" + (invalid ? \"text-litt-danger\" : \"text-litt-fg-subtle\")}>\n          {note}\n        </p>\n      )}\n    </fieldset>\n  );\n}\n",
      "target": "components/litt/bodies/card-picker.flat.tsx"
    }
  ],
  "docs": "Import CardPicker from components/litt/card-picker. Give it a legend, options ({ value, label, description, detail, picture, disabled }), and value with onValueChange. size=\"sm\" draws tiles; name and required make it a form field.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
