{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "type": "registry:ui",
  "title": "Slider",
  "description": "A value on a range, such as an animation speed, or a span with two thumbs, such as builds that took 20s to 80s. A thin rail, a foreground range and round thumbs that follow the pointer one to one, with words for the two ends. The thumbs of a range never cross, and a form sends both values under one name.",
  "dependencies": [
    "@radix-ui/react-slider"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/slider.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slider 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Slider (components.md, \"Slider\"): one value on a continuous range, such as the default\n * animation speed. Built on Radix Slider (one thumb), which gives the role, the keys and the\n * form value. litt owns the look, the hand, the label row and the end words. RangeSlider, in the\n * same file, is the same control with two caps for a span (see \"Range\" below).\n *\n * - The post (material.md, 6; Nick's frames 119 and 121, made physical on the controls canvas, v1.0): a raised plate\n *   the control's height (44, md) at the control radius (--litt-block, --litt-plate-lift); a slot cut into it (--litt-slot,\n *   --litt-slot-shade: its top wall shades the floor and its lower wall catches the light); the part of the slot the cap has\n *   pulled out, a step darker (--litt-slot-fill); and a cap riding in the slot, 8px wide, standing taller than the plate\n *   (--litt-cap, --litt-cap-lift), with a grip line down its face. Only raised things cast, straight down (R3). Never the\n *   accent (R6): a form can hold many sliders.\n * - Notches: marks under the slot at the values you pass (marks), and a longer, darker one at home, the value the cap\n *   comes back to (by default the defaultValue). Home holds the cap a little: within about a step of it, the hand\n *   lets go there.\n * - Says its value (R5): at the end of the label row, at the small size in tabular figures, --litt-fg-muted, and --litt-fg\n *   while the cap is held. showValue={false} leaves it out. Words for the two ends sit under the plate (\"Slower\",\n *   \"Faster\").\n * - The label names the thumb (aria-labelledby), the help text describes it, and formatValue feeds both the shown\n *   value and aria-valuetext. The thumb Radix keeps is invisible; the cap you see is this file's, so it can glide.\n * - Hand (R4): press on the cap and it is held where it was taken, so it moves with the finger 1:1, with no easing;\n *   press on the slot and the cap goes there on the responsive spring, then follows. While held it sinks\n *   --litt-press-depth and its cast tightens (--litt-cap-press), at once. Pushed fast it leans into the move, at most 4°, and\n *   rights itself on the spring when the hand slows. The hand is this file's, not Radix's (Radix maps the pointer\n *   across the whole width, which would jump the cap under the finger).\n * - Keys: arrows step, Home and End go to the ends, Page Up and Page Down move about a tenth of the range (whole\n *   steps, never fewer than one); the cap glides there on the responsive spring. The focus ring is on the plate, for\n *   :focus-visible only.\n * - Motion moves only transforms: the cap and the fill slide by translate (the fill's rail is as wide as the cap's\n *   travel), and the lean and the sink are the cap's own transform. Reduced motion stills the glide and the lean.\n * - Touch: the plate is 44px tall. The slider owns horizontal drags: it is marked data-no-drag, so a sheet never moves\n *   under it, and its touch-action leaves vertical scrolling to the page.\n * - Disabled: everything lies flat (nothing to push): the plate --litt-surface-2 with a hairline, the slot --litt-surface-3,\n *   the cap --litt-surface-2 with a hairline. The label dims to --litt-fg-disabled; the value and the end words stay at\n *   --litt-fg-subtle, because they are still read.\n * - Bodies (proposals/worlds.md, Bodies): this file is the contract (the props, the Radix root and thumbs, the label and\n *   value words, the marks and home, the hand, the keys, the range form) and the Press body, which is the default: a deep slot with notches that light, and the value on a raised face that rides over the cap (a single slider with a value stands 68px tall for it). Flat\n *   (proposals/flat-pill.md: a pill track, an ink fill, a round knob, the value at the track's end) lives in bodies/slider.flat.tsx and Device (a\n *   fader: a well, engraved ticks, a key for a cap, the value on an LCD) in bodies/slider.device.tsx. Each takes\n *   SliderBodyProps and carries data-body on its root. The travel's inset is --sl-pad: the body's root sets it, and\n *   padOf reads it back as the root's left padding.\n * - Forced colors drops box-shadow and background colors, so the plate, the slot and the cap get real borders and\n *   the cap takes the system highlight.\n *\n * Range (components.md, \"Slider\", the Range bullet): RangeSlider is two caps in one slot with the fill between them,\n * such as builds that took between 20s and 80s in a table filter. The span shows at the end of the label row (\"20s\n * to 80s\") when showValue is on. It reuses the plate, the label row, the help text, the end words and the disabled\n * look above.\n * - Names: each thumb is named by the label and its end (\"Build time, from\", \"Build time, to\"), and gets its own\n *   aria-valuetext from formatValue. The shown value reads \"20s to 80s\": words, never a dash. The two thumbs sit in a\n *   group named by the label (and marked aria-disabled when it is), so a screen reader says they belong together,\n *   and the dimmed label of a disabled range is known to be inactive.\n * - Hand: a press takes the nearer cap, held where it was taken when the press is on it, or sent to the finger when\n *   it is not. The focused cap draws the focus ring around itself, so you can tell which one the keys move.\n * - The caps never cross and stay minStepsBetweenThumbs steps apart. Radix would swap them (or mis-round a decimal\n *   step), so Radix keeps their order (preserveThumbOrder) and this file holds the gap.\n * - Keys act on the focused thumb. The arrows are Radix's. Home, End, Page Up and Page Down are handled here,\n *   because Radix sends Home to the first thumb and End to the last, whichever has focus.\n * - A form sends both values under name (FormData.getAll(name) is [from, to]), not under name[].\n */\nimport { useEffect, useId, useRef, useState, type ComponentProps, type CSSProperties, type KeyboardEvent, type PointerEvent, type ReactNode, type Ref, type RefObject } from \"react\";\nimport * as RadixSlider from \"@radix-ui/react-slider\";\nimport { byWorld, useMaterial } from \"../../lib/litt/motion\";\nimport { SliderDevice } from \"./bodies/slider.device\";\nimport { SliderFlat } from \"./bodies/slider.flat\";\n\nexport type SliderProps = {\n  /** The label above the slider. Required, because it names the thumb for screen readers. */\n  label: ReactNode;\n  /** A hint under the slider, linked to the thumb with aria-describedby. */\n  help?: ReactNode;\n  /** The value, when you control it. Pair it with onValueChange. */\n  value?: number;\n  /** The value it starts at when you do not control it. Without one, it starts at min. */\n  defaultValue?: number;\n  /** Called with the new value on every move of the cap, by pointer or key. */\n  onValueChange?: (value: number) => void;\n  /** Called with the final value when a drag is released, or right after a key moves the cap. Use it to save. */\n  onValueCommit?: (value: number) => void;\n  /** The lowest value, with the cap at the start of the slot. */\n  min?: number;\n  /** The highest value, with the cap at the end of the slot. */\n  max?: number;\n  /** The gap between values. The cap snaps to it, and an arrow key moves one step. */\n  step?: number;\n  /** Values to mark with a notch under the slot, such as each whole step. Leave it out for a plain slot. */\n  marks?: number[];\n  /** The value the cap comes home to, marked with a longer notch: within about a step of it, the hand lets go there. Defaults to defaultValue; pass null for none. */\n  home?: number | null;\n  /** A word under the start of the plate, such as \"Slower\". Say which way is which. */\n  minLabel?: ReactNode;\n  /** A word under the end of the plate, such as \"Faster\". */\n  maxLabel?: ReactNode;\n  /** Says the value at the end of the label row (R5). On by default; false leaves it out. */\n  showValue?: boolean;\n  /** Turns a value into text, such as 1 into \"1.0x\". It feeds the shown value and aria-valuetext. Without it, the number is shown as it is. */\n  formatValue?: (value: number) => string;\n  /** Makes the slider unavailable: muted, skipped by Tab, and left out of a form, like a native field. */\n  disabled?: boolean;\n  /** The field name a form sends the value under. */\n  name?: string;\n  /** The id of the thumb, when you need your own. The label and the help text derive their ids from it. */\n  id?: string;\n  /** Extra classes for the wrapper, such as a width. */\n  className?: string;\n  /** The thumb, for focus or a form library. */\n  ref?: Ref<HTMLSpanElement>;\n};\n\nexport type RangeSliderProps = {\n  /** The label above the slider. Required: each thumb is named by it and its end, such as \"Build time, from\". */\n  label: ReactNode;\n  /** A hint under the slider, linked to both thumbs with aria-describedby. */\n  help?: ReactNode;\n  /** The two values, [from, to], when you control them. Pair it with onValueChange. */\n  value?: [number, number];\n  /** The values it starts at when you do not control them. Without them, it starts open, at [min, max]. */\n  defaultValue?: [number, number];\n  /** Called with [from, to] on every move of either end, by pointer or key. */\n  onValueChange?: (value: [number, number]) => void;\n  /** Called with [from, to] when a drag is released, or right after a key moves an end. Use it to save or to run the filter. */\n  onValueCommit?: (value: [number, number]) => void;\n  /** The lowest value, at the start of the slot. */\n  min?: number;\n  /** The highest value, at the end of the slot. */\n  max?: number;\n  /** The gap between values. Both ends snap to it, and an arrow key moves the focused end one step. */\n  step?: number;\n  /** How many steps the ends stay apart. They never cross, and never meet unless you pass 0. */\n  minStepsBetweenThumbs?: number;\n  /** The words that finish each thumb's name, \"<label>, from\" and \"<label>, to\". Change them for another language or another meaning. */\n  thumbLabels?: [string, string];\n  /** A word under the start of the plate, such as \"Faster\". Say which way is which. */\n  minLabel?: ReactNode;\n  /** A word under the end of the plate, such as \"Slower\". */\n  maxLabel?: ReactNode;\n  /** Shows the span at the end of the label row, in tabular figures: \"20s to 80s\". Off by default. */\n  showValue?: boolean;\n  /** Turns one value into text, such as 20 into \"20s\". It feeds the shown span (\"20s to 80s\") and each thumb's aria-valuetext. Without it, the number is shown as it is. */\n  formatValue?: (value: number) => string;\n  /** Makes the slider unavailable: muted, skipped by Tab, and left out of a form, like a native field. */\n  disabled?: boolean;\n  /** The field name a form sends both values under: FormData.getAll(name) is [from, to]. */\n  name?: string;\n  /** The id of the from thumb, when you need your own. The label, the help text and the to thumb (\"<id>-to\") derive their ids from it. */\n  id?: string;\n  /** Extra classes for the wrapper, such as a width. */\n  className?: string;\n  /** The from thumb, for focus or a form library. */\n  ref?: Ref<HTMLSpanElement>;\n};\n\n/* The plate: raised, at the control radius (R3: --litt-block, --litt-plate-lift). It is the hit area and, for a single slider,\n   carries the focus ring. Disabled, it lies flat. With a value to carry it is 68px tall, so the face that says the\n   value rides above the cap (tall); without, it is the control's height (44, md). Its --sl-pad is the cap's inset. */\nconst plateBase =\n  \"group relative block w-full cursor-grab touch-pan-y select-none rounded-litt-control bg-litt-block shadow-(--litt-plate-lift) \" +\n  \"pl-(--sl-pad) data-held:cursor-grabbing \" +\n  \"data-disabled:cursor-not-allowed data-disabled:bg-litt-surface-2 data-disabled:shadow-[inset_0_0_0_1px_var(--litt-line)] \" +\n  \"forced-colors:border\";\nconst plateShort = \" h-litt-control-md [--sl-pad:max(14px,min(var(--litt-radius-control),calc(var(--litt-control-md)/2)))]\";\nconst plateTall = \" h-17 [--sl-pad:max(14px,min(var(--litt-radius-control),34px))]\";\nconst plateRing = \"has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-litt-focus\";\n\n/* The slot: cut deep, 16px tall and 8px outside the cap's travel at each end, its corners the control radius less 2px.\n   Its top wall shades the floor and its lower wall catches the light (--litt-slot-shade). It clips the fill. Disabled, it is\n   a flat --litt-surface-3. */\nconst slot =\n  \"pointer-events-none absolute h-4 overflow-hidden rounded-[calc(var(--litt-radius-control)-2px)] bg-litt-slot shadow-(--litt-slot-shade) \" +\n  \"group-data-disabled:bg-litt-surface-3 group-data-disabled:shadow-none forced-colors:border\";\nconst slotShort = \" top-3.5\";\nconst slotTall = \" top-10\";\n\n/* The cap's travel: inset (see inset) from each end of the plate, which is 8px in from the slot's ends. A rail as wide\n   as the travel moves by translate, with the cap on its right edge, so the cap and the fill move by transform only. */\nconst capTravel = \"pointer-events-none absolute inset-y-0\";\nconst fillTravel = \"absolute inset-y-0 left-2 right-2\";\nconst fillLook =\n  \"bg-litt-slot-fill shadow-(--litt-slot-fill-shade) \" +\n  \"group-data-disabled:bg-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)] group-data-disabled:shadow-none forced-colors:bg-[Highlight]\";\n/* The glide: a jump of the cap (a key, or a press on the slot) settles on the responsive spring. A drag follows 1:1. */\nconst glides = \" transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\n\n/* The cap: 8px wide, standing proud of the slot (36px tall, 4px from the plate's top, or 28px tall in the tall plate),\n   centered on its rail's right edge. It leans from its foot and sinks while held, on the responsive spring. */\nconst capBoxBase = \"absolute -right-1 w-2 origin-bottom transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\nconst capBox = capBoxBase + \" top-1 h-9\";\nconst capBoxTall = capBoxBase + \" top-8.5 h-7\";\nconst capFace = (held: boolean) =>\n  \"absolute inset-0 rounded-[calc(var(--litt-radius-control)-2px)] forced-colors:border forced-colors:bg-[Highlight] \" +\n  (held ? \"bg-litt-cap-held shadow-(--litt-cap-press) \" : \"bg-litt-cap shadow-(--litt-cap-lift) \") +\n  \"group-data-disabled:bg-litt-surface-2 group-data-disabled:shadow-[inset_0_0_0_1px_var(--litt-line)]\";\nconst capGrip = \"absolute inset-y-3 left-1/2 w-px -translate-x-1/2 bg-litt-cap-grip group-data-disabled:bg-litt-line\";\n\n/* The value's face: a small block raised on the plate above the cap (--litt-block, --litt-block-lift) that rides with it. Its\n   edge over the cap's center slides from its left (value at the start) to its right (value at the end), so it stays\n   over the cap and clear of the plate's ends. Held, it sinks with the cap. Disabled, it lies flat. */\nconst faceBox =\n  \"absolute top-2.5 right-0 flex h-5.5 min-w-11 items-center justify-center rounded-[calc(var(--litt-radius-control)-2px)] px-2 \" +\n  \"text-litt-small tabular-nums transition-colors duration-(--litt-dur-instant) ease-litt forced-colors:border \" +\n  \"group-data-disabled:bg-litt-surface-2 group-data-disabled:text-litt-fg-subtle group-data-disabled:shadow-[inset_0_0_0_1px_var(--litt-line)]\";\nconst faceLook = (held: boolean) => (held ? \"bg-litt-block text-litt-fg shadow-(--litt-block-press) \" : \"bg-litt-block text-litt-fg-muted shadow-(--litt-block-lift) \");\n\n/* Radix's thumb holds the role, the keys and the value. The cap you see is drawn above it, so the thumb is invisible\n   and as wide as the cap, which keeps it on the same travel. */\nconst thumbTrack = \"absolute inset-y-0\";\nconst thumbHidden = \"block h-full w-2 opacity-0 focus-visible:outline-none\";\n\n/** How many digits a number needs after the point, so snapping to a step of 0.1 never leaves 0.30000000000000004. */\nfunction places(n: number) {\n  return (String(n).split(\".\")[1] ?? \"\").length;\n}\n\n/** The nearest value on the step grid, kept on the rail. */\nfunction snapToStep(raw: number, min: number, max: number, step: number) {\n  const snapped = Math.round((raw - min) / step) * step + min;\n  return Math.min(max, Math.max(min, Number(snapped.toFixed(Math.max(places(step), places(min))))));\n}\n\n/** What Page Up and Page Down move: about a tenth of the range, in whole steps, never fewer than one. Radix moves ten steps. */\nfunction pageSteps(min: number, max: number, step: number) {\n  return Math.max(1, Math.round((max - min) / step / 10));\n}\n\n/** Hands an element to a ref the caller passed, of either kind. */\nfunction setRef<T>(ref: Ref<T> | undefined, el: T | null) {\n  if (typeof ref === \"function\") ref(el);\n  else if (ref) (ref as RefObject<T | null>).current = el;\n}\n\n/* How far in from each end of the plate the cap's center can go: 14px, or the plate's corner radius when that is\n   larger (the plate draws it at most half its height). On a pill the cap then stands at the center of each end's\n   half-circle, as clear of the curve as it is of the top and bottom, instead of rubbing it. The travel is the plate\n   less twice this. --sl-pad (plateBase) is the CSS rule, and padOf reads it back as the plate's left padding; the 14 in both is PAD. */\nconst PAD = 14;\nconst inset = (less = 0) => `calc(var(--sl-pad) - ${less}px)`;\nconst insetX = (less = 0): CSSProperties => ({ left: inset(less), right: inset(less) });\nfunction padOf(plate: HTMLElement) {\n  return parseFloat(getComputedStyle(plate).paddingLeft) || PAD;\n}\n\n/** The pointer on the plate, undoing any scale above it: its x, the plate's width and the travel's inset, in the plate's own px. */\nfunction measure(plate: HTMLElement, clientX: number) {\n  const r = plate.getBoundingClientRect();\n  const w = plate.offsetWidth || r.width;\n  const scale = r.width / w || 1;\n  return { x: (clientX - r.left) / scale, w, pad: padOf(plate) };\n}\ntype Measure = ReturnType<typeof measure>;\n\n/** Where the cap's center is on the plate when it is p of the way along. */\nconst capAt = (p: number, m: Measure) => m.pad + p * (m.w - 2 * m.pad);\n\n/** Where x is along the travel, from 0 to 1. */\nconst along = (x: number, m: Measure) => Math.min(1, Math.max(0, (x - m.pad) / (m.w - 2 * m.pad || 1)));\n\n/** A rail as wide as the travel, moved so its right edge sits p of the way along. */\nconst railAt = (p: number): CSSProperties => ({ transform: `translateX(${((p - 1) * 100).toFixed(3)}%)` });\n\n/** A notch in the slot, p of the way along, lit once the cap has pulled the fill past it. Home's is longer and always lit. */\nfunction Notch({ p, home, lit, tall }: { p: number; home: boolean; lit: boolean; tall: boolean }) {\n  const y = tall ? (home ? \"top-10.5 h-3\" : \"top-11 h-2\") : home ? \"top-4 h-3\" : \"top-[18px] h-2\";\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={\n        \"pointer-events-none absolute w-px -translate-x-1/2 transition-colors duration-(--litt-dur-instant) ease-litt group-data-disabled:hidden forced-colors:bg-[CanvasText] \" +\n        y +\n        (home || lit ? \" bg-litt-notch-home\" : \" bg-litt-notch\")\n      }\n      style={{ left: `calc(${inset()} + (100% - 2 * ${inset()}) * ${p.toFixed(4)})` }}\n    />\n  );\n}\n\n/** The value under the hand: home holds it within about a step, then the step grid. */\nfunction valueAt(at: number, min: number, max: number, step: number, home: number | null) {\n  const raw = min + at * (max - min);\n  if (home !== null && Math.abs(raw - home) < step * 0.9) return home;\n  return snapToStep(raw, min, max, step);\n}\n\n/** The lean (R4): a cap pushed fast tips into the move, at most 4°, eased toward the hand's speed, and rights itself\n    when the hand slows or lets go. Reduced motion keeps it upright. */\nfunction useLean() {\n  const [lean, setLean] = useState(0);\n  const last = useRef<{ x: number; t: number; still: boolean } | null>(null);\n  const settle = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(settle.current), []);\n  return {\n    lean,\n    start(x: number, t: number) {\n      last.current = { x, t, still: window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches };\n    },\n    move(x: number, t: number) {\n      const l = last.current;\n      if (!l) return;\n      last.current = { x, t, still: l.still };\n      if (l.still) return;\n      const target = Math.max(-4, Math.min(4, ((x - l.x) / Math.max(1, t - l.t)) * 5));\n      setLean((now) => Math.round((now * 0.5 + target * 0.5) * 100) / 100);\n      clearTimeout(settle.current);\n      settle.current = setTimeout(() => setLean(0), 90);\n    },\n    end() {\n      last.current = null;\n      clearTimeout(settle.current);\n      setLean(0);\n    },\n  };\n}\n\n/** One cap on its rail: the face, the grip line, the sink while held and the lean. The ring is drawn when `ring` is on. In the tall plate it carries the value's face, which rides with it. */\nfunction Cap({ p, held, lean, glide, ring = false, shown }: { p: number; held: boolean; lean: number; glide: boolean; ring?: boolean; shown?: string }) {\n  const sink = held ? \"var(--litt-press-depth)\" : \"0px\";\n  return (\n    <span aria-hidden=\"true\" className={capTravel + (glide ? glides : \"\")} style={{ ...insetX(), ...railAt(p) }}>\n      {shown !== undefined && (\n        <span\n          className={faceBox + \" \" + faceLook(held) + (glide ? glides : \"\")}\n          style={{ transform: `translate(${((1 - p) * 100).toFixed(3)}%, ${sink})` }}\n        >\n          {shown}\n        </span>\n      )}\n      <span className={shown !== undefined ? capBoxTall : capBox} style={{ transform: `translateY(${sink}) rotate(${lean}deg)` }}>\n        <span className={capFace(held) + (ring ? \" outline-2 outline-offset-2 outline-litt-focus\" : \"\")} />\n        <span className={capGrip} />\n      </span>\n    </span>\n  );\n}\n\n/** One cap as a body draws it: where it is along the travel (0 to 1), whether a hand holds it, its lean in degrees, and whether it rings itself for keyboard focus (a range only). */\nexport type SliderBodyCap = { p: number; held: boolean; lean: number; ring: boolean };\n\n/**\n * What the Slider and the RangeSlider resolve for a body (proposals/worlds.md, Bodies): the values as fractions, the\n * notches, the Radix root and thumb props and the ref the hand measures. A body draws the root, the track, the cap\n * or caps and the ticks, and carries data-body on its root; it never changes a prop, a key or the hand.\n */\nexport type SliderBodyProps = {\n  /** Two caps with the fill between them, or one. */\n  range: boolean;\n  /** The caps, one or two, left to right. */\n  caps: SliderBodyCap[];\n  /** The pulled-out part's transform on its rail as wide as the travel: its right edge at the cap, or the band between the two caps. */\n  fill: CSSProperties;\n  /** The marks (as fractions of the travel), then home last, which a body draws longer. */\n  notches: { p: number; home: boolean }[];\n  /** A jump of the cap settles on the responsive spring; a drag follows 1:1. */\n  glide: boolean;\n  /** A hand holds a cap. */\n  held: boolean;\n  disabled: boolean;\n  /** The value as words, for a body that carries it itself (Press on a face that rides with the cap, Flat at the end of the track). Left out when showValue is off, for a range, and for Device, whose label row sets it on an LCD. */\n  shown?: string;\n  /** The root's ref, which the hand measures. Its left padding is the travel's inset. */\n  plateRef: Ref<HTMLSpanElement>;\n  /** The Radix root's props: the values, the range, the form name and the hand's handlers. */\n  root: Omit<ComponentProps<typeof RadixSlider.Root>, \"ref\" | \"className\" | \"children\" | \"data-held\" | \"data-no-drag\">;\n  /** The Radix thumbs' props (the role, the names, the keys), left to right, each with its key. */\n  thumbs: (Omit<ComponentProps<typeof RadixSlider.Thumb>, \"className\"> & { key: string })[];\n};\n\ntype FieldProps = {\n  labelId: string;\n  label: ReactNode;\n  /** The value as it is shown at the end of the label row. Leave it out to show none. */\n  shown?: string;\n  /** Whether a hand holds a cap: the shown value steps up to --litt-fg. */\n  held?: boolean;\n  helpId?: string;\n  help?: ReactNode;\n  minLabel?: ReactNode;\n  maxLabel?: ReactNode;\n  disabled: boolean;\n  className: string;\n  /** Marks the field as a group named by its label, for a control with more than one thumb. */\n  group?: boolean;\n  /** Sets the value on an LCD well in the mono (the Device body). */\n  lcd?: boolean;\n  /** The plate, and anything that must sit inside the field. */\n  children: ReactNode;\n};\n\n/** What the Slider and the RangeSlider share around the plate: the label row, the end words and the help text. */\nfunction Field({ labelId, label, shown, held = false, helpId, help, minLabel, maxLabel, disabled, className, group = false, lcd = false, children }: FieldProps) {\n  return (\n    <div\n      role={group ? \"group\" : undefined}\n      aria-labelledby={group ? labelId : undefined}\n      aria-disabled={group && disabled ? true : undefined}\n      className={[\"grid gap-2\", className].join(\" \")}\n    >\n      <div className=\"flex items-baseline justify-between gap-4\">\n        <span id={labelId} className={\"text-litt-small font-litt-title \" + (disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>\n          {label}\n        </span>\n        {shown !== undefined && (\n          /* Disabled, the value steps down to --litt-fg-subtle, not --litt-fg-disabled: it is still information, and it must stay readable. */\n          <span\n            aria-hidden=\"true\"\n            className={\n              \"text-litt-small tabular-nums transition-colors duration-(--litt-dur-instant) ease-litt \" +\n              (lcd\n                ? \"rounded-litt-control bg-litt-field px-2.5 py-0.5 font-litt-field shadow-(--litt-field-shade) \" + (disabled ? \"text-litt-field-ink-subtle\" : held ? \"text-litt-field-ink\" : \"text-litt-field-ink-subtle\")\n                : disabled ? \"text-litt-fg-subtle\" : held ? \"text-litt-fg\" : \"text-litt-fg-muted\")\n            }\n          >\n            {shown}\n          </span>\n        )}\n      </div>\n      <div className=\"grid gap-1\">\n        {children}\n        {(minLabel || maxLabel) && (\n          <div className=\"flex justify-between gap-4 text-litt-caption text-litt-fg-subtle\">\n            <span>{minLabel}</span>\n            <span className=\"text-right\">{maxLabel}</span>\n          </div>\n        )}\n      </div>\n      {help && (\n        <p id={helpId} className=\"text-litt-small text-litt-fg-subtle\">\n          {help}\n        </p>\n      )}\n    </div>\n  );\n}\n\n/** The Press body: a raised plate, a deep slot cut into it with notches that light as the fill passes them, a cap riding in the slot that casts, and the value on a small raised face that rides over the cap. */\nfunction SliderPress({ range, caps, fill, notches, glide, held, shown, plateRef, root, thumbs }: SliderBodyProps) {\n  const tall = shown !== undefined;\n  return (\n    <RadixSlider.Root\n      ref={plateRef}\n      {...root}\n      data-body=\"press\"\n      data-no-drag=\"\"\n      data-held={held ? \"\" : undefined}\n      className={plateBase + (tall ? plateTall : plateShort) + (range ? \"\" : \" \" + plateRing)}\n    >\n      <span aria-hidden=\"true\" className={slot + (tall ? slotTall : slotShort)} style={insetX(8)}>\n        <span className={fillTravel + (range ? \" origin-left\" : \"\") + (glide ? glides : \"\")} style={fill}>\n          <span className={(range ? \"absolute inset-0 \" : \"absolute inset-y-0 right-0 w-[calc(100%+0.5rem)] \") + fillLook} />\n        </span>\n      </span>\n      {notches.map((n) => (\n        <Notch key={n.p} p={n.p} home={n.home} lit={!range && n.p <= (caps[0]?.p ?? 0) + 1e-6} tall={tall} />\n      ))}\n      {caps.map((cap, i) => (\n        <Cap key={i} p={cap.p} held={cap.held} lean={cap.lean} glide={glide} ring={cap.ring} shown={shown} />\n      ))}\n      <RadixSlider.Track className={thumbTrack} style={insetX(4)}>\n        {thumbs.map(({ key, ...thumb }) => (\n          <RadixSlider.Thumb key={key} {...thumb} className={thumbHidden} />\n        ))}\n      </RadixSlider.Track>\n    </RadixSlider.Root>\n  );\n}\n\n/** The body for the material the slider stands in: the nearest MaterialProvider's, else <html>'s. */\nfunction useBody() {\n  const material = useMaterial();\n  return { Body: byWorld(material, { press: SliderPress, flat: SliderFlat, device: SliderDevice }), material };\n}\n\n/** One slider: a label above that says the value, a cap riding in a slot on a raised plate, and optional words under its ends. Every value is a number, and a form sends it under name. */\nexport function Slider({\n  label,\n  help,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onValueCommit,\n  min = 0,\n  max = 100,\n  step = 1,\n  marks,\n  home: homeProp,\n  minLabel,\n  maxLabel,\n  showValue = true,\n  formatValue,\n  disabled = false,\n  name,\n  id: idProp,\n  className = \"\",\n  ref,\n}: SliderProps) {\n  const auto = useId();\n  const id = idProp ?? auto;\n  const labelId = `${id}-label`;\n  const helpId = help ? `${id}-help` : undefined;\n\n  /* The slider keeps its own value when you do not control it, because Page Up and the hand need to read it. */\n  const [inner, setInner] = useState(defaultValue ?? min);\n  const value = valueProp ?? inner;\n  const fmt = (v: number) => (formatValue ? formatValue(v) : String(v));\n  const text = fmt(value);\n  const span = max - min || 1;\n  const p = (value - min) / span;\n  const home = homeProp === null ? null : (homeProp ?? defaultValue ?? null);\n\n  const latest = useRef(value);\n  latest.current = value;\n  const change = (next: number | undefined) => {\n    if (next === undefined || next === latest.current) return;\n    latest.current = next;\n    if (valueProp === undefined) setInner(next);\n    onValueChange?.(next);\n  };\n\n  /* Radix moves ten steps on Page Up and Page Down. The spec says about a tenth of the range, in whole steps. */\n  const pageKeys = (event: KeyboardEvent<HTMLSpanElement>) => {\n    if (disabled || (event.key !== \"PageUp\" && event.key !== \"PageDown\")) return;\n    event.preventDefault();\n    const raw = value + (event.key === \"PageUp\" ? 1 : -1) * pageSteps(min, max, step) * step;\n    const next = snapToStep(raw, min, max, step);\n    if (next === value) return;\n    change(next);\n    onValueCommit?.(next);\n  };\n\n  /* The hand. A press on the cap holds it where it was taken (offset); a press on the slot sends the cap there on the\n     spring, then it follows. Each handler calls preventDefault, so Radix's own pointer handling stays out. */\n  const plateRef = useRef<HTMLSpanElement>(null);\n  const thumbRef = useRef<HTMLSpanElement | null>(null);\n  const hand = useRef<{ id: number; offset: number } | null>(null);\n  const [held, setHeld] = useState(false);\n  const [glide, setGlide] = useState(true);\n  const lean = useLean();\n  const to = (x: number, m: Measure) => change(valueAt(along(x, m), min, max, step, home));\n  const onPointerDown = (e: PointerEvent<HTMLSpanElement>) => {\n    if (disabled || (e.pointerType === \"mouse\" && e.button !== 0) || !plateRef.current) return;\n    e.preventDefault();\n    const m = measure(plateRef.current, e.clientX);\n    const cap = capAt(p, m);\n    const onCap = Math.abs(m.x - cap) <= 10;\n    hand.current = { id: e.pointerId, offset: onCap ? cap - m.x : 0 };\n    e.currentTarget.setPointerCapture(e.pointerId);\n    thumbRef.current?.focus({ preventScroll: true });\n    setHeld(true);\n    setGlide(!onCap);\n    lean.start(m.x, e.timeStamp);\n    if (!onCap) to(m.x, m);\n  };\n  const onPointerMove = (e: PointerEvent<HTMLSpanElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId || !plateRef.current) return;\n    e.preventDefault();\n    const m = measure(plateRef.current, e.clientX);\n    setGlide(false);\n    lean.move(m.x, e.timeStamp);\n    to(m.x + h.offset, m);\n  };\n  const onPointerEnd = (e: PointerEvent<HTMLSpanElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId) return;\n    e.preventDefault();\n    hand.current = null;\n    setHeld(false);\n    setGlide(true);\n    lean.end();\n    if (e.type === \"pointerup\") onValueCommit?.(latest.current);\n  };\n\n  const notched = (marks ?? []).filter((v) => v !== home && v >= min && v <= max);\n  const notches = [\n    ...notched.map((v) => ({ p: (v - min) / span, home: false })),\n    ...(home !== null && home >= min && home <= max ? [{ p: (home - min) / span, home: true }] : []),\n  ];\n  const { Body, material } = useBody();\n  /* Press and Flat carry the value in the body (a face, or the end of the track); Device's label row sets it on an LCD. */\n  const inBody = material !== \"device\";\n\n  return (\n    <Field\n      lcd={material === \"device\"}\n      labelId={labelId}\n      label={label}\n      shown={showValue && !inBody ? text : undefined}\n      held={held}\n      helpId={helpId}\n      help={help}\n      minLabel={minLabel}\n      maxLabel={maxLabel}\n      disabled={disabled}\n      className={className}\n    >\n      <Body\n        plateRef={plateRef}\n        range={false}\n        caps={[{ p, held, lean: lean.lean, ring: false }]}\n        fill={railAt(p)}\n        notches={notches}\n        glide={glide}\n        held={held}\n        disabled={disabled}\n        shown={showValue && inBody ? text : undefined}\n        root={{\n          /* The cap runs from the left and the hand is measured from the left, so Radix places the thumb from the left too, under any DirectionProvider. */\n          dir: \"ltr\",\n          name: disabled ? undefined : name,\n          min,\n          max,\n          step,\n          disabled,\n          value: [value],\n          onValueChange: (v) => change(v[0]),\n          onValueCommit: (v) => v[0] !== undefined && onValueCommit?.(v[0]),\n          onKeyDown: pageKeys,\n          onPointerDown,\n          onPointerMove,\n          onPointerUp: onPointerEnd,\n          onPointerCancel: onPointerEnd,\n        }}\n        thumbs={[\n          {\n            key: \"thumb\",\n            ref: (el: HTMLSpanElement | null) => {\n              thumbRef.current = el;\n              setRef(ref, el);\n            },\n            id,\n            \"aria-labelledby\": labelId,\n            \"aria-describedby\": helpId,\n            \"aria-valuetext\": formatValue ? text : undefined,\n          },\n        ]}\n      />\n    </Field>\n  );\n}\n\n/** Two values in one slot, [from, to]: two caps with the fill between them. The caps never cross, each is named by the label and its end, and a form sends both under name. */\nexport function RangeSlider({\n  label,\n  help,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onValueCommit,\n  min = 0,\n  max = 100,\n  step = 1,\n  minStepsBetweenThumbs = 1,\n  thumbLabels = [\"from\", \"to\"],\n  minLabel,\n  maxLabel,\n  showValue = false,\n  formatValue,\n  disabled = false,\n  name,\n  id: idProp,\n  className = \"\",\n  ref,\n}: RangeSliderProps) {\n  const auto = useId();\n  const id = idProp ?? auto;\n  const labelId = `${id}-label`;\n  const helpId = help ? `${id}-help` : undefined;\n  const thumbId = (i: 0 | 1) => (i === 0 ? id : `${id}-to`);\n  const nameId = (i: 0 | 1) => `${id}-${i === 0 ? \"from\" : \"to\"}-name`;\n\n  /* The slider keeps its own values when you do not control them, because Page Up and the hand need to read them. They\n     are sorted and kept in the slot, so a stray default can never draw the caps outside it or crossed. */\n  const [inner, setInner] = useState<[number, number]>(defaultValue ?? [min, max]);\n  const given = valueProp ?? inner;\n  const from = Math.min(max, Math.max(min, Math.min(given[0], given[1])));\n  const to = Math.min(max, Math.max(min, Math.max(given[0], given[1])));\n  const values: [number, number] = [from, to];\n  const fmt = (v: number) => (formatValue ? formatValue(v) : String(v));\n  const text = `${fmt(from)} to ${fmt(to)}`;\n  const span = max - min || 1;\n  const pf = (from - min) / span;\n  const pt = (to - min) / span;\n\n  /* Radix keeps the thumbs in order (preserveThumbOrder) and could hold the gap too, but it rounds a decimal step badly\n     (0.2 plus 0.1 is 0.30000000000000004). So Radix gets no gap, and the thumb that moved is held here, on the step grid.\n     A thumb is never pushed back past where it already was. */\n  const digits = Math.max(places(step), places(min));\n  const gap = Math.max(0, minStepsBetweenThumbs) * step;\n  const down = (n: number) => Number((Math.floor((n - min) / step + 1e-9) * step + min).toFixed(digits));\n  const up = (n: number) => Number((Math.ceil((n - min) / step - 1e-9) * step + min).toFixed(digits));\n  const hold = (next: number[]): [number, number] => {\n    let [a = from, b = to] = next;\n    if (a !== from) a = Math.min(a, Math.max(from, down(to - gap)));\n    else if (b !== to) b = Math.max(b, Math.min(to, up(from + gap)));\n    return [a, b];\n  };\n  const same = (a: number[], b: number[]) => a[0] === b[0] && a[1] === b[1];\n\n  const latest = useRef(values);\n  latest.current = values;\n  const change = (next: number[]) => {\n    const held = hold(next);\n    if (same(held, latest.current)) return;\n    latest.current = held;\n    if (valueProp === undefined) setInner(held);\n    onValueChange?.(held);\n  };\n\n  /* Radix commits a key press with the move it wanted. When the other thumb held it back, nothing moved, so nothing is saved.\n     A released drag commits the values as they are, which are always whole. */\n  const commit = (next: number[]) => {\n    const held = hold(next);\n    if (!same(next, values) && same(held, values)) return;\n    onValueCommit?.(held);\n  };\n\n  /* The keys act on the focused thumb. Radix sends Home to the first thumb and End to the last, whichever has focus, and\n     moves ten steps on Page Up and Page Down. The spec says Home and End go to the end of the slot (the other thumb stops\n     them) and the pages are about a tenth of the range, in whole steps. */\n  const thumbKeys = (i: 0 | 1) => (event: KeyboardEvent<HTMLSpanElement>) => {\n    if (disabled) return;\n    const at = values[i];\n    let target: number;\n    if (event.key === \"Home\") target = min;\n    else if (event.key === \"End\") target = max;\n    else if (event.key === \"PageUp\") target = at + pageSteps(min, max, step) * step;\n    else if (event.key === \"PageDown\") target = at - pageSteps(min, max, step) * step;\n    else return;\n    event.preventDefault();\n    const snapped = snapToStep(target, min, max, step);\n    const held = hold(i === 0 ? [snapped, to] : [from, snapped]);\n    if (same(held, values)) return;\n    change(held);\n    onValueCommit?.(held);\n  };\n\n  /* The hand: a press takes the nearer cap, held where it was taken when the press is on it, or sent to the finger on\n     the spring when it is not. Each handler calls preventDefault, so Radix's pointer handling stays out. */\n  const plateRef = useRef<HTMLSpanElement>(null);\n  const thumbs = useRef<(HTMLSpanElement | null)[]>([]);\n  const hand = useRef<{ id: number; end: 0 | 1; offset: number } | null>(null);\n  const [held, setHeld] = useState<0 | 1 | null>(null);\n  const [glide, setGlide] = useState(true);\n  const [ringed, setRinged] = useState<0 | 1 | null>(null);\n  const lean = useLean();\n  const { Body, material } = useBody();\n  const moveEnd = (end: 0 | 1, x: number, m: Measure) => {\n    const v = valueAt(along(x, m), min, max, step, null);\n    const [a, b] = latest.current;\n    change(end === 0 ? [v, b] : [a, v]);\n  };\n  const onPointerDown = (e: PointerEvent<HTMLSpanElement>) => {\n    if (disabled || (e.pointerType === \"mouse\" && e.button !== 0) || !plateRef.current) return;\n    e.preventDefault();\n    const m = measure(plateRef.current, e.clientX);\n    const caps = [capAt(pf, m), capAt(pt, m)] as const;\n    const end: 0 | 1 = caps[0] === caps[1] ? (m.x < caps[0] ? 0 : 1) : m.x < (caps[0] + caps[1]) / 2 ? 0 : 1;\n    const onCap = Math.abs(m.x - caps[end]) <= 10;\n    hand.current = { id: e.pointerId, end, offset: onCap ? caps[end] - m.x : 0 };\n    e.currentTarget.setPointerCapture(e.pointerId);\n    thumbs.current[end]?.focus({ preventScroll: true });\n    setHeld(end);\n    setGlide(!onCap);\n    lean.start(m.x, e.timeStamp);\n    if (!onCap) moveEnd(end, m.x, m);\n  };\n  const onPointerMove = (e: PointerEvent<HTMLSpanElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId || !plateRef.current) return;\n    e.preventDefault();\n    const m = measure(plateRef.current, e.clientX);\n    setGlide(false);\n    lean.move(m.x, e.timeStamp);\n    moveEnd(h.end, m.x + h.offset, m);\n  };\n  const onPointerEnd = (e: PointerEvent<HTMLSpanElement>) => {\n    const h = hand.current;\n    if (!h || h.id !== e.pointerId) return;\n    e.preventDefault();\n    hand.current = null;\n    setHeld(null);\n    setGlide(true);\n    lean.end();\n    if (e.type === \"pointerup\") onValueCommit?.(latest.current);\n  };\n\n  return (\n    <Field\n      lcd={material === \"device\"}\n      labelId={labelId}\n      label={label}\n      shown={showValue ? text : undefined}\n      held={held !== null}\n      helpId={helpId}\n      help={help}\n      minLabel={minLabel}\n      maxLabel={maxLabel}\n      disabled={disabled}\n      className={className}\n      group\n    >\n      <Body\n        plateRef={plateRef}\n        range\n        caps={([0, 1] as const).map((i) => ({ p: i === 0 ? pf : pt, held: held === i, lean: held === i ? lean.lean : 0, ring: ringed === i }))}\n        /* The fill between the caps: a rail-wide band moved to the first cap and scaled to the second, transform only. */\n        fill={{ transform: `translateX(${(pf * 100).toFixed(3)}%) scaleX(${Math.max(0, pt - pf).toFixed(4)})` }}\n        notches={[]}\n        glide={glide}\n        held={held !== null}\n        disabled={disabled}\n        root={{\n          dir: \"ltr\", // as the Slider's: the caps and the hand run from the left\n          min,\n          max,\n          step,\n          disabled,\n          value: values,\n          preserveThumbOrder: true,\n          onValueChange: change,\n          onValueCommit: commit,\n          onPointerDown,\n          onPointerMove,\n          onPointerUp: onPointerEnd,\n          onPointerCancel: onPointerEnd,\n        }}\n        thumbs={([0, 1] as const).map((i) => ({\n          key: `thumb-${i}`,\n          ref: (el: HTMLSpanElement | null) => {\n            thumbs.current[i] = el;\n            if (i === 0) setRef(ref, el);\n          },\n          id: thumbId(i),\n          /* A form sends both values under the same name (Radix would add [] to it). */\n          name: disabled ? undefined : name,\n          \"aria-labelledby\": nameId(i),\n          \"aria-describedby\": helpId,\n          \"aria-valuetext\": formatValue ? fmt(values[i]) : undefined,\n          onKeyDown: thumbKeys(i),\n          /* The focused cap rings itself, for keyboard focus only, so you can tell which one the keys move. */\n          onFocus: (e) => setRinged(e.currentTarget.matches(\":focus-visible\") ? i : null),\n          onBlur: () => setRinged(null),\n        }))}\n      />\n      {/* The names: the label and the end, as one string, so a screen reader says \"Build time, from\". Hidden text still names a thumb. */}\n      {([0, 1] as const).map((i) => (\n        <span key={i} id={nameId(i)} hidden>\n          {label}, {thumbLabels[i]}\n        </span>\n      ))}\n    </Field>\n  );\n}\n",
      "target": "components/litt/slider.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/slider.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slider 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * The Slider's Device body (proposals/worlds.md, Bodies; proposals/device.md): a fader. The body is the aluminium, so\n * there is no plate. A well is cut into it (--litt-slot, --litt-slot-shade), its pulled-out part lit, the way a meter lights:\n * in the accent, which is Device's orange, or the ink under Monochrome. Under the well, small engraved ticks (--litt-notch,\n * --litt-engrave) at the marks, or at every tenth of the track when there are none, and a longer one at home. The cap is a\n * key (--litt-key-lift) with a line across its face, standing a little proud of the well. Held, it sinks the whole\n * --litt-press-depth, 2px, onto its skirt, and the key's shade steps down (--litt-block-press). The value is not here: the Slider's\n * label row sets it on an LCD well in the mono when the body is Device.\n *\n * It takes the contract the Slider and the RangeSlider resolve (SliderBodyProps): the props, the keys and the hand are\n * theirs. A mechanism does not lean, so the lean is ignored. The cap glides on the responsive tier and, under reduced\n * motion, jumps. The travel's inset is --sl-pad: the cap's half width, 7px, and a 3px lip of well past it, and 7px of\n * body beyond that. The cap stands proud of the 16px well, so it is never a box inside a curve.\n */\nimport * as RadixSlider from \"@radix-ui/react-slider\";\nimport type { CSSProperties } from \"react\";\nimport type { SliderBodyProps } from \"../slider\";\n\nconst rootLook =\n  \"group relative block h-litt-control-md w-full cursor-grab touch-pan-y select-none \" +\n  \"[--sl-pad:17px] pl-(--sl-pad) data-held:cursor-grabbing data-disabled:cursor-not-allowed forced-colors:border\";\n/* The well: 16px tall, 12px down, past the cap's travel by its half width and a 3px lip, at the control radius (a pill at most). */\nconst well =\n  \"pointer-events-none absolute top-3 h-4 overflow-hidden rounded-litt-control bg-litt-slot shadow-(--litt-slot-shade) \" +\n  \"group-data-disabled:bg-litt-surface-3 group-data-disabled:shadow-none forced-colors:border\";\nconst capTravel = \"pointer-events-none absolute inset-y-0\";\nconst fillTravel = \"absolute inset-y-0 left-2.5 right-2.5\";\n/* Lit like a meter: the accent. Disabled, it goes dark. */\nconst fillLook =\n  \"bg-litt-accent shadow-(--litt-slot-fill-shade) \" +\n  \"group-data-disabled:bg-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)] group-data-disabled:shadow-none forced-colors:bg-[Highlight]\";\nconst glides = \" transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\n/* The cap: a key 14px wide and 28px tall, 6px from the top, centered on its rail's right edge. It sinks while held. */\nconst capBox = \"absolute top-1.5 -right-1.75 h-7 w-3.5 transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\nconst capFace = (held: boolean) =>\n  \"absolute inset-0 rounded-[calc(var(--litt-radius-control)-2px)] forced-colors:border forced-colors:bg-[Highlight] \" +\n  (held ? \"bg-litt-cap-held shadow-(--litt-block-press) \" : \"bg-litt-cap shadow-(--litt-key-lift) \") +\n  \"group-data-disabled:bg-litt-surface-2 group-data-disabled:shadow-[inset_0_0_0_1px_var(--litt-line)]\";\nconst capLine = \"absolute inset-y-1.5 left-1/2 w-px -translate-x-1/2 bg-litt-cap-grip group-data-disabled:bg-litt-line forced-colors:bg-[CanvasText]\";\nconst ringOne = \" group-has-[:focus-visible]:outline-2 group-has-[:focus-visible]:outline-offset-2 group-has-[:focus-visible]:outline-litt-focus\";\nconst ringOf = \" outline-2 outline-offset-2 outline-litt-focus\";\nconst thumbTrack = \"absolute inset-y-0\";\nconst thumbHidden = \"block h-full w-3.5 opacity-0 focus-visible:outline-none\";\n\nconst inset = (less = 0) => `calc(var(--sl-pad) - ${less}px)`;\nconst insetX = (less = 0): CSSProperties => ({ left: inset(less), right: inset(less) });\n\n/** The ticks: the marks and home when the Slider has them, else every tenth of the track (home's one longer). */\nfunction ticksOf(notches: SliderBodyProps[\"notches\"]) {\n  if (notches.some((n) => !n.home)) return notches;\n  const home = notches.find((n) => n.home);\n  const tenths = Array.from({ length: 11 }, (_, i) => ({ p: i / 10, home: false })).filter((n) => !home || Math.abs(n.p - home.p) > 0.001);\n  return home ? [...tenths, home] : tenths;\n}\n\n/** The Device body: a fader in a well on a notched rail, for one cap or two. */\nexport function SliderDevice({ range, caps, fill, notches, glide, held, plateRef, root, thumbs }: SliderBodyProps) {\n  return (\n    <RadixSlider.Root ref={plateRef} {...root} data-body=\"device\" data-no-drag=\"\" data-held={held ? \"\" : undefined} className={rootLook}>\n      <span aria-hidden=\"true\" className={well} style={insetX(10)}>\n        <span className={fillTravel + (range ? \" origin-left\" : \"\") + (glide ? glides : \"\")} style={fill}>\n          <span className={(range ? \"absolute inset-0 \" : \"absolute inset-y-0 right-0 w-[calc(100%+0.625rem)] \") + fillLook} />\n        </span>\n      </span>\n      {ticksOf(notches).map((n) => (\n        <span\n          key={n.p}\n          aria-hidden=\"true\"\n          className={\n            \"pointer-events-none absolute top-9 w-px -translate-x-1/2 shadow-(--litt-engrave) group-data-disabled:hidden forced-colors:bg-[CanvasText] \" +\n            (n.home ? \"h-2 bg-litt-notch-home\" : \"h-1 bg-litt-notch\")\n          }\n          style={{ left: `calc(${inset()} + (100% - 2 * ${inset()}) * ${n.p.toFixed(4)})` }}\n        />\n      ))}\n      {caps.map((cap, i) => (\n        <span key={i} aria-hidden=\"true\" className={capTravel + (glide ? glides : \"\")} style={{ ...insetX(), transform: `translateX(${((cap.p - 1) * 100).toFixed(3)}%)` }}>\n          <span className={capBox} style={{ transform: `translateY(${cap.held ? \"var(--litt-press-depth)\" : \"0px\"})` }}>\n            <span className={capFace(cap.held) + (range ? (cap.ring ? ringOf : \"\") : ringOne)} />\n            <span className={capLine} />\n          </span>\n        </span>\n      ))}\n      <RadixSlider.Track className={thumbTrack} style={insetX(7)}>\n        {thumbs.map(({ key, ...thumb }) => (\n          <RadixSlider.Thumb key={key} {...thumb} className={thumbHidden} />\n        ))}\n      </RadixSlider.Track>\n    </RadixSlider.Root>\n  );\n}\n",
      "target": "components/litt/bodies/slider.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/slider.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slider 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * The Slider's Flat body (proposals/worlds.md, Bodies; proposals/flat-pill.md): a page, not an instrument. No plate, no\n * shadow, no notch cut into anything. The track is a 6px --litt-surface-3 pill, the pulled-out part is the ink, and the cap is\n * a round 20px ink knob with no grip line and nothing to sink into: held, it only swells a little under the finger. The\n * marks are dots under the track and home's is a larger dot. The value is said in words at the end of the track, in the\n * ink, so a slider reads as a line of text with a track in it. The travel's inset is --sl-pad, the knob's radius (10px),\n * which the root sets and the hand reads back as its left padding. The ring for keyboard focus goes on the knob, since\n * the root is invisible. It takes the contract the Slider and the RangeSlider resolve (SliderBodyProps): the props, the\n * keys and the hand are theirs, not this file's. A range has no end to say its span at, so its label row does.\n */\nimport * as RadixSlider from \"@radix-ui/react-slider\";\nimport type { CSSProperties } from \"react\";\nimport type { SliderBodyProps } from \"../slider\";\n\nconst rootLook =\n  \"group relative block h-litt-control-md min-w-0 cursor-grab touch-pan-y select-none rounded-none bg-transparent shadow-none \" +\n  \"[--sl-pad:10px] pl-(--sl-pad) data-held:cursor-grabbing \" +\n  \"data-disabled:cursor-not-allowed forced-colors:border\";\nconst track =\n  \"pointer-events-none absolute top-4.75 h-1.5 overflow-hidden rounded-litt-pill bg-litt-surface-3 \" +\n  \"group-data-disabled:bg-litt-surface-2 forced-colors:border\";\nconst capTravel = \"pointer-events-none absolute inset-y-0\";\nconst fillTravel = \"absolute inset-y-0 left-2 right-2\";\nconst fillLook = \"bg-litt-fg group-data-disabled:bg-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)] forced-colors:bg-[Highlight]\";\nconst glides = \" transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\nconst capBox = \"absolute top-3 -right-2.5 h-5 w-5 origin-center transition-transform duration-(--litt-dur-responsive) ease-litt-responsive\";\nconst capFace = \"absolute inset-0 rounded-litt-pill bg-litt-fg forced-colors:border forced-colors:bg-[Highlight] group-data-disabled:bg-litt-fg-disabled\";\n/* One cap rings itself when the root has keyboard focus; on a range the focused cap rings itself (cap.ring). */\nconst ringOne = \" group-has-[:focus-visible]:outline-2 group-has-[:focus-visible]:outline-offset-2 group-has-[:focus-visible]:outline-litt-focus\";\nconst ringOf = \" outline-2 outline-offset-2 outline-litt-focus\";\nconst thumbTrack = \"absolute inset-y-0\";\nconst thumbHidden = \"block h-full w-2 opacity-0 focus-visible:outline-none\";\n/* The value in words: the ink, tabular, a fixed width so the track does not breathe as it changes. */\nconst valueLook = \"min-w-[4ch] text-right text-litt-small font-litt-title tabular-nums text-litt-fg group-data-disabled:text-litt-fg-subtle\";\n\nconst inset = (less = 0) => `calc(var(--sl-pad) - ${less}px)`;\nconst insetX = (less = 0): CSSProperties => ({ left: inset(less), right: inset(less) });\n\n/** The Flat body: a pill track, an ink fill and a round knob, for one cap or two, with the value in words at the track's end. */\nexport function SliderFlat({ range, caps, fill, notches, glide, held, shown, disabled, plateRef, root, thumbs }: SliderBodyProps) {\n  const slider = (\n    <RadixSlider.Root\n      ref={plateRef}\n      {...root}\n      data-body={shown === undefined ? \"flat\" : undefined}\n      data-no-drag=\"\"\n      data-held={held ? \"\" : undefined}\n      className={rootLook + (shown === undefined ? \" w-full\" : \" flex-1\")}\n    >\n      <span aria-hidden=\"true\" className={track} style={insetX(8)}>\n        <span className={fillTravel + (range ? \" origin-left\" : \"\") + (glide ? glides : \"\")} style={fill}>\n          <span className={(range ? \"absolute inset-0 \" : \"absolute inset-y-0 right-0 w-[calc(100%+0.5rem)] \") + fillLook} />\n        </span>\n      </span>\n      {notches.map((n) => (\n        <span\n          key={n.p}\n          aria-hidden=\"true\"\n          className={\n            \"pointer-events-none absolute -translate-x-1/2 rounded-litt-pill group-data-disabled:hidden forced-colors:bg-[CanvasText] \" +\n            (n.home ? \"top-9 size-1.5 bg-litt-fg\" : \"top-9.5 size-1 bg-litt-fg-subtle\")\n          }\n          style={{ left: `calc(${inset()} + (100% - 2 * ${inset()}) * ${n.p.toFixed(4)})` }}\n        />\n      ))}\n      {caps.map((cap, i) => (\n        <span key={i} aria-hidden=\"true\" className={capTravel + (glide ? glides : \"\")} style={{ ...insetX(), transform: `translateX(${((cap.p - 1) * 100).toFixed(3)}%)` }}>\n          <span className={capBox} style={{ transform: `scale(${cap.held ? 1.12 : 1})` }}>\n            <span className={capFace + (range ? (cap.ring ? ringOf : \"\") : ringOne)} />\n          </span>\n        </span>\n      ))}\n      <RadixSlider.Track className={thumbTrack} style={insetX(4)}>\n        {thumbs.map(({ key, ...thumb }) => (\n          <RadixSlider.Thumb key={key} {...thumb} className={thumbHidden} />\n        ))}\n      </RadixSlider.Track>\n    </RadixSlider.Root>\n  );\n  if (shown === undefined) return slider;\n  return (\n    <div data-body=\"flat\" data-disabled={disabled ? \"\" : undefined} className=\"group flex items-center gap-3\">\n      {slider}\n      <span aria-hidden=\"true\" className={valueLook}>\n        {shown}\n      </span>\n    </div>\n  );\n}\n",
      "target": "components/litt/bodies/slider.flat.tsx"
    }
  ],
  "docs": "Import Slider, or RangeSlider for a span, from components/litt/slider. Give it a label, and minLabel and maxLabel to say which way is which. A range value is [from, to], and each thumb is named by the label and its end.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
