{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pad",
  "type": "registry:ui",
  "title": "Pad",
  "description": "Two values set at once on one surface, like a reply's tone against its length. A well with a static dot grid, a round thumb with dashed crosshairs, a dashed circle for the default, and a tooltip that says the value in words. It is the Slider in two dimensions, in the foreground and never the accent.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/pad.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/pad 2026.10.26. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Pad: two values set at once on one surface, like a reply's tone against its length. It is the Slider in two\n * dimensions: its cap, seen from above, never the accent (R6), and the same rule that position never eases.\n *\n * - Surface: a well you reach into, so it is the cut (R3, material.md 6): the --litt-cut floor inside the --litt-line-control\n *   line, with its top wall's shade (--litt-cut-shade), at the surface radius, and a static dot grid on the floor (fg at\n *   about 14%, 1.5px dots on a 16px pitch, an SVG pattern). The grid is a coordinate system, so it never moves. The\n *   four axis words sit inside the edges at the caption size in --litt-fg-subtle, in sentence case: y's maxLabel at the\n *   top, its minLabel at the bottom, x's minLabel at the left and maxLabel at the right. y grows upward. Each word\n *   sits on a plate of the well's floor, so the grid and the crosshairs break around it instead of running through\n *   the letters.\n * - Thumb: a round cap, the Slider's cap seen from above: a 20px circle (--litt-cap, --litt-cap-lift), lit on its top edge and\n *   casting straight down (R2). Held, its cast tightens (--litt-cap-press, --litt-cap-held) and it sinks --litt-press-depth, at once\n *   (R4); a key moves it without a hand, so it stays up. It never changes size. A 1px dashed --litt-line-strong line runs\n *   through it, once across and once down the whole pad.\n *   The thumb stays inside the pad: its centre travels from 14px in to 14px short of the far edge (its radius, the\n *   2px focus ring and the 2px offset), so the ring never crosses the edge, and the pointer maths uses the same\n *   range, so the thumb sits under the pointer.\n * - Default mark: pass defaultMark and a 20px dashed circle in --litt-fg-subtle sits there, with \"Default\" beside it in\n *   the caption. It is where Reset would take the thumb.\n * - Tooltip: the chart's tooltip look (solid --litt-surface-3, --litt-shadow-tooltip, the control radius) shows formatValue\n *   beside the thumb while you drag, while the pointer is over the pad, and while the keyboard has focus. It tries the\n *   right, then the left, then above, then below (above first for a finger), so it stays inside the pad and clear of\n *   the thumb. Esc hides it. It repeats what the inputs say, so it is hidden from screen readers.\n * - Pointer: pressing anywhere moves the thumb there and starts a drag. The pad captures the pointer, so a drag may\n *   leave the pad. Values snap to each axis's step. The pad is marked data-no-drag (a sheet never moves under it) and\n *   has touch-action none: the whole pad belongs to the thumb, so it is the whole target and needs no extra hit area.\n * - Keyboard (the React Aria ColorArea pattern): two visually hidden range inputs, one per axis, each named by its\n *   axis label. One tab stop: the x input is in the tab order and the y input is tabindex -1, still reachable by a\n *   screen reader. On either input, ← and → change x, ↑ and ↓ change y, Shift makes them 10 steps, Page Up and Page\n *   Down move y by 10 steps, and Home and End take x to its ends. Both inputs carry the formatted value as\n *   aria-valuetext. Native adjustments (a screen reader's swipe) go through onChange.\n * - Focus: when an input is focus-visible, the thumb gets the 2px focus ring, 2px away. A press focuses the x input\n *   without the ring.\n * - Disabled: everything lies flat (nothing to reach into or push): the well and the word plates --litt-surface-2 with the\n *   plain --litt-line hairline, the thumb --litt-surface-2 with a --litt-line edge, the crosshairs at --litt-line, every word in\n *   --litt-fg-disabled, no pointer and no tab stop.\n * - Motion: the thumb, the crosshairs and the tooltip follow the pointer and the keys at once. Position is a layout\n *   value, and an eased jump would lag the input. The only motion is the tooltip fading on the responsive tier; the\n *   sink is at once, and reduced motion sets its depth to 0.\n * - Forced colors drops box-shadow, so the well and the thumb get real borders, and the thumb takes the system\n *   highlight, as the Slider's cap does.\n */\nimport { useId, useLayoutEffect, useRef, useState, type ChangeEvent, type FocusEvent, type KeyboardEvent, type PointerEvent, type ReactNode } from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useDevice, useExit, useTransition } from \"../../lib/litt/motion\";\n\n/** One axis of the pad: its name, its range and the words at its ends. */\nexport type PadAxis = {\n  /** What the axis sets, such as \"Length\". It names the axis's input for screen readers. */\n  label: string;\n  /** The value at the left edge (x) or the bottom edge (y). */\n  min: number;\n  /** The value at the right edge (x) or the top edge (y). */\n  max: number;\n  /** The gap between values. The thumb snaps to it, and an arrow key moves one step. Defaults to 1. */\n  step?: number;\n  /** The word inside the left edge (x) or the bottom edge (y), such as \"Brief\" or \"Formal\". Say which way is which. */\n  minLabel?: string;\n  /** The word inside the right edge (x) or the top edge (y), such as \"Detailed\" or \"Casual\". */\n  maxLabel?: string;\n};\n\n/** The pad's value: one number per axis. y grows upward. */\nexport type PadValue = { x: number; y: number };\n\nexport type PadProps = {\n  /** The label above the pad. Required: it names the group and describes both inputs for screen readers. */\n  label: ReactNode;\n  /** Help text under the pad, linked to both inputs with aria-describedby, as on the Slider. */\n  help?: ReactNode;\n  /** The horizontal axis, left to right. */\n  x: PadAxis;\n  /** The vertical axis, bottom to top. */\n  y: PadAxis;\n  /** The value, when you control it. Pair it with onValueChange. */\n  value?: PadValue;\n  /** The value it starts at when you do not control it. Without one, the thumb starts in the middle. */\n  defaultValue?: PadValue;\n  /** Called with the new value on every move of the thumb, by pointer or key. */\n  onValueChange?: (value: PadValue) => void;\n  /** Called with the final value when a drag is released, or right after a key moves the thumb. Use it to save. */\n  onValueCommit?: (value: PadValue) => void;\n  /** Draws a dashed circle marked \"Default\" at this value, so people can see where Reset would go. */\n  defaultMark?: PadValue;\n  /** Turns a value into words, such as { x: 10, y: 55 } into \"Warm, brief, about 40 words\". It feeds the tooltip and the aria-valuetext of both inputs. Without it, the two numbers are shown with their axis labels. */\n  formatValue?: (value: PadValue) => string;\n  /** Height of the pad in px. The width is the container's. */\n  height?: number;\n  /** Makes the pad unavailable: muted, skipped by Tab, and ignoring the pointer. */\n  disabled?: boolean;\n  /** Extra classes for the wrapper, such as a width. */\n  className?: string;\n};\n\n/* ----------------------------------------------------------------- geometry */\n\nconst R = 10; // the thumb's radius\nconst INSET = 14; // how far the thumb's centre stays from every edge: its radius, the 2px ring and the 2px offset, so the ring never crosses the edge\nconst EDGE = 4; // the air the tooltip keeps from the pad's edge\nconst DOT = 16; // pitch of the dot grid\nconst GAP = 8; // between the thumb's edge and the tooltip\nconst GAP_TOUCH = 24; // a fingertip covers more than the thumb, so the tooltip stands further off\nconst FIGURE = 7.2; // advance of a 12px tabular figure in Geist (letters run narrower)\nconst DEFAULT_WORD = \"Default\";\n\nconst places = (n: number) => (String(n).split(\".\")[1] ?? \"\").length;\nconst clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));\n\n/** A value rounded to its axis's step and kept inside its range. Never 0.30000000000000004. */\nfunction snap(raw: number, axis: PadAxis) {\n  const step = axis.step ?? 1;\n  const n = Math.round((raw - axis.min) / step) * step + axis.min;\n  return clamp(Number(n.toFixed(Math.max(places(step), places(axis.min)))), axis.min, axis.max);\n}\n\n/** Where a value sits along its axis, from 0 at min to 1 at max. */\nconst fraction = (v: number, axis: PadAxis) => (axis.max > axis.min ? clamp((v - axis.min) / (axis.max - axis.min), 0, 1) : 0);\n\n/** The CSS for a centre that is `f` of the way along an edge, kept `INSET` clear of both ends. No measuring, so it works on the server. */\nconst along = (f: number) => `calc(${INSET}px + (100% - ${2 * INSET}px) * ${f})`;\n\n/** The pad's width, measured before paint and kept current. 0 until the first measure. */\nfunction useWidth() {\n  const ref = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const read = () => setWidth(Math.floor(el.getBoundingClientRect().width));\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(read);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n  return [ref, width] as const;\n}\n\n/**\n * Where the tooltip goes: the first side where it fits whole inside the pad, with the other axis kept inside. If no\n * side fits (a very small pad), the first choice is pushed inside.\n */\nfunction placeTip(cx: number, cy: number, w: number, h: number, width: number, height: number, gap: number, coarse: boolean) {\n  const reach = R + gap;\n  const fitX = (left: number) => clamp(left, EDGE, Math.max(EDGE, width - w - EDGE));\n  const fitY = (top: number) => clamp(top, EDGE, Math.max(EDGE, height - h - EDGE));\n  const sides = {\n    right: { left: cx + reach, top: fitY(cy - h / 2), fits: cx + reach + w <= width - EDGE },\n    left: { left: cx - reach - w, top: fitY(cy - h / 2), fits: cx - reach - w >= EDGE },\n    above: { left: fitX(cx - w / 2), top: cy - reach - h, fits: cy - reach - h >= EDGE },\n    below: { left: fitX(cx - w / 2), top: cy + reach, fits: cy + reach + h <= height - EDGE },\n  };\n  const order = coarse ? ([\"above\", \"below\", \"right\", \"left\"] as const) : ([\"right\", \"left\", \"above\", \"below\"] as const);\n  const pick = order.find((side) => sides[side].fits) ?? order[0];\n  const p = sides[pick];\n  return { left: fitX(p.left), top: fitY(p.top) };\n}\n\n/* ----------------------------------------------------------------- styles */\n\n/* The well: the cut, or flat when disabled. Forced colors drops box-shadow, so the edge becomes a real border. */\nconst well = \"relative w-full touch-none select-none rounded-litt-surface forced-colors:border \";\nconst wellOn = \"bg-litt-cut shadow-[inset_0_0_0_1px_var(--litt-line-control),var(--litt-cut-shade)]\";\nconst wellOff = \"bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\n/* The round cap. The pad never has a hover fill: the tooltip is the hover cue. Held, it sinks: the translate that\n   centres it on the value takes --litt-press-depth too. */\nconst thumb = \"pointer-events-none absolute block size-5 -translate-x-1/2 rounded-litt-pill forced-colors:border \";\nconst thumbUp = \"-translate-y-1/2 bg-litt-cap shadow-(--litt-cap-lift) forced-colors:bg-[Highlight]\";\nconst thumbHeld = \"translate-y-[calc(var(--litt-press-depth)-50%)] bg-litt-cap-held shadow-(--litt-cap-press) forced-colors:bg-[Highlight]\";\nconst thumbOff = \"-translate-y-1/2 bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\n/* An axis word sits on a plate of the well's floor, so the dots and the crosshairs do not run through the letters. */\nconst word = \"pointer-events-none absolute px-1 text-litt-caption font-normal whitespace-nowrap\";\n\n/* ----------------------------------------------------------------- the tooltip */\n\nfunction Tip({\n  cx,\n  cy,\n  width,\n  height,\n  coarse,\n  children,\n}: {\n  cx: number;\n  cy: number;\n  width: number;\n  height: number;\n  coarse: boolean;\n  children: ReactNode;\n}) {\n  const enter = useTransition(\"responsive\");\n  const exit = useExit(\"responsive\");\n  const ref = useRef<HTMLDivElement>(null);\n  const [size, setSize] = useState({ w: 0, h: 0 });\n  /* Measured before paint on every render, so a longer phrase is placed with its real width. */\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (el && (el.offsetWidth !== size.w || el.offsetHeight !== size.h)) setSize({ w: el.offsetWidth, h: el.offsetHeight });\n  });\n  const { left, top } = placeTip(cx, cy, size.w, size.h, width, height, coarse ? GAP_TOUCH : GAP, coarse);\n  return (\n    <div aria-hidden=\"true\" className=\"pointer-events-none absolute z-10\" style={{ left, top }}>\n      <motion.div\n        ref={ref}\n        className=\"rounded-litt-control bg-litt-surface-3 px-2 py-1.5 text-litt-small whitespace-nowrap text-litt-fg tabular-nums shadow-litt-tooltip inset-shadow-litt-highlight\"\n        initial={{ opacity: 0, scale: 0.98 }}\n        animate={{ opacity: 1, scale: 1, transition: enter }}\n        exit={{ opacity: 0, scale: 0.98, transition: exit }}\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n\n/* ----------------------------------------------------------------- Pad */\n\n/** Two values on one surface: a dot grid, a round thumb with crosshairs, and a tooltip that says the value in words. Drag it, tap it, or use the arrow keys. */\nexport function Pad({\n  label,\n  help,\n  x,\n  y,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onValueCommit,\n  defaultMark,\n  formatValue,\n  height = 192,\n  disabled = false,\n  className = \"\",\n}: PadProps) {\n  const id = useId().replace(/[^\\w-]/g, \"\");\n  const labelId = `${id}-label`;\n  const helpId = help ? `${id}-help` : undefined;\n  const defaultId = defaultMark ? `${id}-default` : undefined;\n  const xRangeId = x.minLabel && x.maxLabel ? `${id}-x-range` : undefined;\n  const yRangeId = y.minLabel && y.maxLabel ? `${id}-y-range` : undefined;\n  const pattern = `${id}-dots`;\n  const { coarse } = useDevice();\n\n  /* The pad keeps its own value when you do not control it, because a key needs to read it. */\n  const [inner, setInner] = useState<PadValue>(() => defaultValue ?? { x: snap((x.min + x.max) / 2, x), y: snap((y.min + y.max) / 2, y) });\n  const value = valueProp ?? inner;\n  const say = (v: PadValue) => (formatValue ? formatValue(v) : `${x.label} ${v.x}, ${y.label} ${v.y}`);\n  const text = say(value);\n\n  const [boxRef, width] = useWidth();\n  const xInput = useRef<HTMLInputElement>(null);\n  const [dragging, setDragging] = useState(false);\n  const [hover, setHover] = useState(false);\n  const [keys, setKeys] = useState(false); // the keyboard has focus, so the ring and the tooltip show\n  const [dismissed, setDismissed] = useState(false); // Esc hid the tooltip\n  const dragRef = useRef(false);\n  const lastRef = useRef<PadValue>(value);\n\n  const move = (next: PadValue) => {\n    if (next.x === value.x && next.y === value.y) return false;\n    if (valueProp === undefined) setInner(next);\n    onValueChange?.(next);\n    return true;\n  };\n\n  /* ---- pointer */\n\n  const fromPointer = (e: PointerEvent<HTMLDivElement>): PadValue => {\n    const box = e.currentTarget.getBoundingClientRect();\n    const fx = clamp((e.clientX - box.left - INSET) / Math.max(1, box.width - 2 * INSET), 0, 1);\n    const fy = clamp((box.bottom - INSET - e.clientY) / Math.max(1, box.height - 2 * INSET), 0, 1);\n    return { x: snap(x.min + fx * (x.max - x.min), x), y: snap(y.min + fy * (y.max - y.min), y) };\n  };\n\n  const drag = (e: PointerEvent<HTMLDivElement>) => {\n    const next = fromPointer(e);\n    lastRef.current = next;\n    move(next);\n  };\n\n  const end = () => {\n    if (!dragRef.current) return;\n    dragRef.current = false;\n    setDragging(false);\n    onValueCommit?.(lastRef.current);\n  };\n\n  /* ---- keyboard */\n\n  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return;\n    if (e.key === \"Escape\") {\n      setDismissed(true);\n      return;\n    }\n    if (e.altKey || e.ctrlKey || e.metaKey) return;\n    const xs = x.step ?? 1;\n    const ys = y.step ?? 1;\n    const n = e.shiftKey ? 10 : 1;\n    let next: PadValue | null = null;\n    switch (e.key) {\n      case \"ArrowLeft\":\n        next = { x: value.x - n * xs, y: value.y };\n        break;\n      case \"ArrowRight\":\n        next = { x: value.x + n * xs, y: value.y };\n        break;\n      case \"ArrowDown\":\n        next = { x: value.x, y: value.y - n * ys };\n        break;\n      case \"ArrowUp\":\n        next = { x: value.x, y: value.y + n * ys };\n        break;\n      case \"PageDown\":\n        next = { x: value.x, y: value.y - 10 * ys };\n        break;\n      case \"PageUp\":\n        next = { x: value.x, y: value.y + 10 * ys };\n        break;\n      case \"Home\":\n        next = { x: x.min, y: value.y };\n        break;\n      case \"End\":\n        next = { x: x.max, y: value.y };\n        break;\n    }\n    if (!next) return;\n    e.preventDefault();\n    setKeys(true);\n    setDismissed(false);\n    const snapped = { x: snap(next.x, x), y: snap(next.y, y) };\n    if (move(snapped)) onValueCommit?.(snapped);\n  };\n\n  /* A screen reader's swipe adjusts the input itself, and arrives here. */\n  const adjust = (axis: \"x\" | \"y\", raw: string) => {\n    const next = axis === \"x\" ? { x: snap(Number(raw), x), y: value.y } : { x: value.x, y: snap(Number(raw), y) };\n    if (move(next)) onValueCommit?.(next);\n  };\n\n  const onFocus = (e: FocusEvent<HTMLInputElement>) => {\n    /* A press focuses the input for the keys that follow, and that is not keyboard focus. */\n    if (!dragRef.current) setKeys(e.currentTarget.matches(\":focus-visible\"));\n    setDismissed(false);\n  };\n\n  /* ---- what is drawn */\n\n  const px = along(fraction(value.x, x));\n  const py = along(1 - fraction(value.y, y));\n  const cx = INSET + fraction(value.x, x) * Math.max(0, width - 2 * INSET);\n  const cy = INSET + (1 - fraction(value.y, y)) * Math.max(0, height - 2 * INSET);\n  const show = !disabled && !dismissed && (dragging || hover || keys);\n\n  /* The default mark's word stands to the right of its circle, or to the left when there is no room. */\n  const markFraction = defaultMark ? fraction(defaultMark.x, x) : 0;\n  const markX = INSET + markFraction * Math.max(0, width - 2 * INSET);\n  const wordLeft = width > 0 && markX + R + 4 + DEFAULT_WORD.length * FIGURE + 8 > width;\n\n  const describedBy = (range?: string) => [labelId, helpId, defaultId, range].filter(Boolean).join(\" \");\n  const plate = disabled ? \"bg-litt-surface-2 text-litt-fg-disabled\" : \"bg-litt-cut text-litt-fg-subtle\";\n  const cross = disabled ? \"border-litt-line\" : \"border-litt-line-strong\";\n\n  const inputs = (axis: \"x\" | \"y\") => {\n    const a = axis === \"x\" ? x : y;\n    return {\n      type: \"range\" as const,\n      min: a.min,\n      max: a.max,\n      step: a.step ?? 1,\n      value: value[axis],\n      disabled,\n      \"aria-label\": a.label,\n      \"aria-describedby\": describedBy(axis === \"x\" ? xRangeId : yRangeId),\n      \"aria-valuetext\": text,\n      onChange: (e: ChangeEvent<HTMLInputElement>) => adjust(axis, e.target.value),\n      onKeyDown,\n      onFocus,\n      onBlur: () => setKeys(false),\n    };\n  };\n\n  return (\n    <div className={[\"grid gap-2\", className].join(\" \")}>\n      <span id={labelId} className={\"text-litt-small font-litt-title \" + (disabled ? \"text-litt-fg-disabled\" : \"text-litt-fg\")}>\n        {label}\n      </span>\n      <div role=\"group\" aria-labelledby={labelId} aria-disabled={disabled || undefined}>\n        <div\n          ref={boxRef}\n          data-no-drag=\"\"\n          className={well + (disabled ? wellOff + \" cursor-not-allowed\" : wellOn + \" cursor-pointer\")}\n          style={{ height }}\n          onMouseDown={(e) => {\n            /* Without this the press would blur the input we are about to focus. */\n            if (!disabled) e.preventDefault();\n          }}\n          onPointerDown={(e) => {\n            if (disabled || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n            e.currentTarget.setPointerCapture(e.pointerId);\n            dragRef.current = true;\n            setDragging(true);\n            setKeys(false);\n            setDismissed(false);\n            drag(e);\n            xInput.current?.focus({ preventScroll: true });\n          }}\n          onPointerMove={(e) => {\n            if (dragRef.current) drag(e);\n          }}\n          onPointerUp={end}\n          onPointerCancel={end}\n          onLostPointerCapture={end}\n          onPointerEnter={(e) => e.pointerType !== \"touch\" && setHover(true)}\n          onPointerLeave={() => setHover(false)}\n        >\n          {/* The dot grid: a coordinate system, so it is static. */}\n          <svg aria-hidden=\"true\" focusable=\"false\" className=\"pointer-events-none absolute inset-0 size-full\">\n            <defs>\n              <pattern id={pattern} width={DOT} height={DOT} patternUnits=\"userSpaceOnUse\">\n                <circle\n                  cx={DOT / 2}\n                  cy={DOT / 2}\n                  r={0.75}\n                  className={disabled ? \"fill-[color-mix(in_oklab,var(--litt-fg)_8%,transparent)]\" : \"fill-[color-mix(in_oklab,var(--litt-fg)_14%,transparent)]\"}\n                />\n              </pattern>\n            </defs>\n            <rect width=\"100%\" height=\"100%\" fill={`url(#${pattern})`} />\n          </svg>\n\n          {/* The crosshairs, through the thumb and across the whole pad. */}\n          <span aria-hidden=\"true\" className={\"pointer-events-none absolute inset-y-0 w-0 border-l border-dashed \" + cross} style={{ left: px }} />\n          <span aria-hidden=\"true\" className={\"pointer-events-none absolute inset-x-0 h-0 border-t border-dashed \" + cross} style={{ top: py }} />\n\n          {defaultMark && (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute size-5 -translate-x-1/2 -translate-y-1/2\"\n              style={{ left: along(markFraction), top: along(1 - fraction(defaultMark.y, y)) }}\n            >\n              <svg width={20} height={20} viewBox=\"0 0 20 20\" focusable=\"false\" className=\"block overflow-visible\">\n                <circle\n                  cx={10}\n                  cy={10}\n                  r={9.5}\n                  fill=\"none\"\n                  strokeWidth={1}\n                  pathLength={24}\n                  strokeDasharray=\"1.5 1.5\"\n                  className={disabled ? \"stroke-litt-fg-disabled\" : \"stroke-litt-fg-subtle\"}\n                />\n              </svg>\n              <span\n                className={\n                  word + \" top-1/2 -translate-y-1/2 \" + (wordLeft ? \"right-full mr-1 \" : \"left-full ml-1 \") + plate\n                }\n              >\n                {DEFAULT_WORD}\n              </span>\n            </span>\n          )}\n\n          {/* The four axis words, inside the edges. */}\n          {y.maxLabel && (\n            <span aria-hidden=\"true\" className={word + \" top-1 left-1/2 -translate-x-1/2 \" + plate}>\n              {y.maxLabel}\n            </span>\n          )}\n          {y.minLabel && (\n            <span aria-hidden=\"true\" className={word + \" bottom-1 left-1/2 -translate-x-1/2 \" + plate}>\n              {y.minLabel}\n            </span>\n          )}\n          {x.minLabel && (\n            <span aria-hidden=\"true\" className={word + \" left-1 top-1/2 -translate-y-1/2 \" + plate}>\n              {x.minLabel}\n            </span>\n          )}\n          {x.maxLabel && (\n            <span aria-hidden=\"true\" className={word + \" right-1 top-1/2 -translate-y-1/2 \" + plate}>\n              {x.maxLabel}\n            </span>\n          )}\n\n          {/* One input per axis, for the keyboard and screen readers. One tab stop: the x input. */}\n          <input ref={xInput} className=\"sr-only\" {...inputs(\"x\")} />\n          <input className=\"sr-only\" tabIndex={-1} aria-orientation=\"vertical\" {...inputs(\"y\")} />\n          {defaultId && (\n            <span id={defaultId} className=\"sr-only\">\n              Default: {say(defaultMark!)}\n            </span>\n          )}\n          {xRangeId && (\n            <span id={xRangeId} className=\"sr-only\">\n              {x.minLabel} to {x.maxLabel}\n            </span>\n          )}\n          {yRangeId && (\n            <span id={yRangeId} className=\"sr-only\">\n              {y.minLabel} to {y.maxLabel}\n            </span>\n          )}\n\n          <span\n            aria-hidden=\"true\"\n            className={\n              thumb +\n              (disabled ? thumbOff : dragging ? thumbHeld : thumbUp) +\n              (keys ? \" outline-2 outline-offset-2 outline-litt-focus\" : \"\")\n            }\n            style={{ left: px, top: py }}\n          />\n\n          <AnimatePresence>\n            {show && width > 0 && (\n              <Tip key=\"tip\" cx={cx} cy={cy} width={width} height={height} coarse={coarse}>\n                {text}\n              </Tip>\n            )}\n          </AnimatePresence>\n        </div>\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",
      "target": "components/litt/pad.tsx"
    }
  ],
  "docs": "Import Pad from components/litt/pad. Give it a label, an x and a y axis ({ label, min, max, step, minLabel, maxLabel }), and hold the value in state with value and onValueChange. Pass defaultMark to show the default and formatValue to say the value in words. Each axis is a range input for the keyboard and screen readers, and the arrow keys move it.",
  "meta": {
    "version": "2026.10.26",
    "released": "2026-10-26",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
