{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slide-confirm",
  "type": "registry:ui",
  "title": "Slide to confirm",
  "description": "A deliberate yes to something that cannot be undone: the segmented control's track and a raised thumb to slide to the end. The thumb follows the finger one to one, springs back on the responsive tier if let go early, and confirms at the end with a check. The thumb is also a button: Enter, Space or a screen reader confirms, a mouse click alone does not.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/button.json",
    "https://littmd.vercel.app/r/icons.json",
    "https://littmd.vercel.app/r/input.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/slide-confirm.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slide-confirm 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Slide to confirm: a deliberate yes to something that cannot be undone, such as deleting a project, made by\n * sliding a thumb to the end of its track (components.md, \"Slide to confirm\"). It asks for more than a tap, so a\n * stray press on a phone does nothing.\n *\n * - Anatomy (Press, the instrument): a plate (--litt-block, --litt-plate-lift) with a slot cut into it (--litt-slot, --litt-slot-shade) and\n *   a gripped cap riding in the slot (--litt-cap, --litt-cap-lift, grip lines in --litt-cap-grip, 48px wide, 4px proud of the slot) to a\n *   raised end stop. The words (\"Slide to delete Atelier Nord\") are in the slot's floor at the small size in\n *   --litt-fg-subtle, and the part the cap has pulled out is a step darker (--litt-slot-fill). Held, the cap sinks --litt-press-depth\n *   with --litt-cap-press. A disabled plate lies flat (surface-2 and a hairline): nothing to push.\n * - Flat (the page): a grey --litt-surface-3 pill and a plain ink circle, no chevron, the words in ink; once confirmed the\n *   whole pill is ink with --litt-canvas words. A disabled thumb is a flat --litt-surface-3 on --litt-surface-2.\n * - Sizes: md is 44 tall, lg 56; on a coarse pointer md grows to 56, because a finger does the sliding.\n * - Drag: the thumb follows the pointer one to one, with no easing, as the Slider's does. The words fade as it\n *   goes, by its position, not by a timer. Let go before the end and it springs back to the start on the\n *   responsive tier (bounce 0). Reach the end and it confirms: the thumb stays there with a check, the words\n *   become the confirmed label, and onConfirm runs once. Esc while dragging sends it back.\n * - Keyboard and assistive tech: the thumb is a real button named by the label. Enter, Space or a screen reader's\n *   activate confirms, because a key press is already deliberate; a mouse click on the thumb does not, or the slide\n *   would be pointless. Focus is the 2px ring on the thumb.\n * - Typing (confirmText): for a mouse or a pen that cannot drag (WCAG 2.5.7), a quiet \"Or type to confirm\" under the\n *   track opens a field labelled with the words to type and a danger button that says the label's verb (\"Delete\n *   Atelier Nord\"). The button waits until the field holds exactly those words (trimmed, case kept, pasting is fine);\n *   it or Enter confirms by the same path as the slide. Esc in the field closes it and puts focus back on the link.\n *   It shows at once, with nothing animating. Without confirmText nothing is added.\n * - After it confirms, the thumb is aria-disabled, not disabled, so focus stays on it (a typed confirm moves focus\n *   there too), and a polite status says the confirmed label. Remount it (a new key) to ask again.\n * - One contract, three bodies (proposals/worlds.md, Bodies). This file is the contract: the props, the drag and its\n *   thresholds, the keyboard confirm, the typing path, the confirmed state and the reset. It resolves them once into\n *   SlideConfirmBodyProps and renders the body the material picks (byWorld). PressBody, below, is the default (the plate,\n *   slot and cap above). bodies/slide-confirm.flat.tsx is the grey pill with an ink circle. bodies/slide-confirm.device.tsx\n *   is a latch key pushed along a well to an end stop: the words sit on an LCD strip in the well, a lamp at the stop\n *   lights once confirmed, and a key let go short goes home in three steps on the instant tier, not on a spring.\n * - Touch: the thumb owns the drag (touch-action none, data-no-drag), so a sheet never moves under it.\n * - Disabled: the track at fg 3%, the thumb --litt-surface-2 with a --litt-line edge, no pointer, no tab stop. The words stay\n *   --litt-fg-subtle, because they still say what it would do, as a disabled Slider's end words do.\n * - Motion: position follows the pointer and never animates, except the spring back. Only transform and opacity\n *   change: the passed part is a full-width band scaled from the left. Under reduced motion it snaps back.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): the thumb can be thrown. Let go and it keeps\n *   the speed it had: if that would carry it to the end, it coasts there and confirms when it lands; if not, it\n *   springs back with it. Both landings use fun mode's throw spring, and the ends of the track are walls: a thumb\n *   that arrives with speed squashes against one and springs back to its shape, instead of passing it. The check\n *   pops in. Still only transform and opacity, and the thumb rests exactly where it does without fun mode. A key\n *   or a screen reader still confirms at once. Reduced motion turns it off.\n */\nimport { useEffect, useId, useLayoutEffect, useRef, useState, type ComponentProps, type KeyboardEvent, type PointerEvent, type ReactNode, type Ref } from \"react\";\nimport { animate, motion, useMotionValue, useTransform, type AnimationPlaybackControls, type MotionValue } from \"motion/react\";\nimport { byWorld, useFun, useMaterial, useTransition } from \"../../lib/litt/motion\";\nimport { CheckIcon, ChevronRightIcon } from \"./icons\";\nimport { TextField } from \"./input\";\nimport { Button } from \"./button\";\nimport { SlideConfirmDeviceBody } from \"./bodies/slide-confirm.device\";\nimport { SlideConfirmFlatBody } from \"./bodies/slide-confirm.flat\";\n\nexport type SlideConfirmSize = \"md\" | \"lg\";\n\nexport type SlideConfirmProps = {\n  /** What sliding does, in the track and as the thumb's name: \"Slide to delete Atelier Nord\". */\n  label: string;\n  /** What the track says once it has confirmed: \"Deleting Atelier Nord\". It is also announced. */\n  confirmedLabel?: string;\n  /** Runs once, when the thumb reaches the end or the thumb is activated by a key or a screen reader. */\n  onConfirm?: () => void;\n  /** Height on the control scale: md is 44 (56 on a coarse pointer), lg is 56. */\n  size?: SlideConfirmSize;\n  /** Makes it unavailable: muted, skipped by Tab, and ignoring the pointer. */\n  disabled?: boolean;\n  /**\n   * The words to type instead of sliding, such as the project's name: \"Atelier Nord\". Adds \"Or type to confirm\" under\n   * the track, for anyone who cannot drag. The match is exact after trimming, case included. The confirm button says\n   * the label without its \"Slide to\".\n   */\n  confirmText?: string;\n  /** Extra classes for the track (with confirmText, the track and its typing panel), such as a width. It fills its container by default. */\n  className?: string;\n};\n\n/**\n * What a body draws: the contract (slide-confirm.tsx) resolves the drag, the keys and the state once, and a body only\n * draws them with its own markup. A body keeps the root's `data-no-drag` and its three children in order (the passed\n * part or the slot that holds it, the words, the thumb), then `tail`; carries data-body on its root; and never changes a prop, a key or a threshold.\n */\nexport type SlideConfirmBodyProps = {\n  /** What sliding does: the words in the track before it confirms. */\n  label: string;\n  /** The words once it has confirmed. */\n  confirmedLabel: string;\n  /** Confirmed: the thumb is at the end, aria-disabled, and the words are the confirmed label. */\n  done: boolean;\n  disabled: boolean;\n  /** The size, so a body can place what sits beside the thumb. */\n  size: SlideConfirmSize;\n  /** The track's height on the control scale, plus any width class the caller gave it. */\n  track: string;\n  /** The tray, measured for the end of the travel. */\n  box: Ref<HTMLDivElement>;\n  /** The passed part as a scale from the left, 0 to 1. */\n  band: MotionValue<number>;\n  /** The words' opacity, by the thumb's position. */\n  words: MotionValue<number>;\n  /** The thumb's chevron, or the check once confirmed (fun mode pops it in). */\n  glyph: ReactNode;\n  /** The thumb button's props: its ref, name, handlers, position and aria. A body adds only its look. */\n  thumb: ComponentProps<typeof motion.button>;\n  /** The hint and the polite status, screen reader only. Rendered last inside the tray. */\n  tail: ReactNode;\n};\n\nconst trackSizes: Record<SlideConfirmSize, string> = {\n  md: \"h-litt-control-md pointer-coarse:h-litt-control-lg\",\n  lg: \"h-litt-control-lg\",\n};\n\n/**\n * Press, the instrument: a plate that stands on the page (--litt-block, --litt-plate-lift) with a slot cut into it (--litt-slot,\n * --litt-slot-shade, 8px in from the plate's edge). The words are in the slot's floor, the part the cap has pulled out is a\n * step darker (--litt-slot-fill, --litt-slot-fill-shade), and a small raised stop (--litt-block, --litt-block-lift) at the slot's end is\n * what the cap comes to rest against. The cap (--litt-cap, --litt-cap-lift) is 48px wide, stands 4px proud of the slot with\n * grip lines on its face (--litt-cap-grip), and sinks --litt-press-depth with --litt-cap-press while it is held.\n */\nfunction PressBody({ label, confirmedLabel, done, disabled, track, box, band, words, glyph, thumb, tail }: SlideConfirmBodyProps) {\n  /* The words sit in the slot's floor, a step darker than the plate, so they take the full ink to keep 4.5:1 there;\n     disabled, the slot is surface-3 and the words stay fg-subtle, as a disabled Slider's end words do. */\n  const ink = disabled && !done ? \"text-litt-fg-subtle\" : \"text-litt-fg\";\n  const grip = \"w-px \" + (disabled ? \"bg-litt-line\" : \"bg-litt-cap-grip\");\n  return (\n    <div\n      ref={box}\n      data-body=\"press\"\n      data-no-drag=\"\"\n      className={\n        \"relative isolate flex w-full items-center rounded-litt-control px-4 py-1 select-none forced-colors:border \" +\n        (disabled ? \"bg-litt-surface-2 shadow-[inset_0_0_0_1px_var(--litt-line)] \" : \"bg-litt-block shadow-(--litt-plate-lift) \") +\n        track\n      }\n    >\n      {/* The slot, cut into the plate. Its floor holds the part the cap has passed: a full-width band scaled from the left, so only a transform changes. */}\n      <span\n        aria-hidden=\"true\"\n        className={\n          \"pointer-events-none absolute inset-2 overflow-hidden rounded-[calc(var(--litt-radius-control)-8px)] forced-colors:border \" +\n          (disabled ? \"bg-litt-surface-3\" : \"bg-litt-slot shadow-(--litt-slot-shade)\")\n        }\n      >\n        <motion.span\n          className={\"absolute inset-y-0 left-2 w-[calc(100%-1rem)] origin-left \" + (disabled ? \"bg-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)]\" : \"bg-litt-slot-fill shadow-(--litt-slot-fill-shade) forced-colors:bg-[Highlight]\")}\n          style={{ scaleX: band }}\n        />\n        {/* The end stop: a small raised block the cap comes to rest against. */}\n        <span className={\"absolute inset-y-1 right-0.5 w-1 rounded-litt-pill \" + (disabled ? \"bg-litt-surface-2\" : \"bg-litt-block shadow-(--litt-block-lift)\")} />\n      </span>\n      <motion.span\n        aria-hidden=\"true\"\n        className={\"pointer-events-none absolute inset-2 grid place-items-center pl-16 pr-10 text-center text-litt-small \" + ink}\n        style={{ opacity: done ? 1 : words }}\n      >\n        <span className=\"max-w-full min-w-0 truncate\">{done ? confirmedLabel : label}</span>\n      </motion.span>\n      <motion.button\n        {...thumb}\n        className={\n          \"relative grid h-full w-12 shrink-0 touch-none place-items-center rounded-[calc(var(--litt-radius-control)-4px)] [-webkit-tap-highlight-color:transparent] \" +\n          \"transition-[translate,background-color] duration-(--litt-dur-instant) ease-litt \" +\n          \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus forced-colors:border \" +\n          (disabled\n            ? \"cursor-not-allowed bg-litt-surface-2 text-litt-fg-disabled shadow-[inset_0_0_0_1px_var(--litt-line)]\"\n            : \"bg-litt-cap text-litt-fg shadow-(--litt-cap-lift) \" + (done ? \"cursor-default\" : \"cursor-grab active:translate-y-(--litt-press-depth) active:cursor-grabbing active:bg-litt-cap-held active:shadow-(--litt-cap-press)\"))\n        }\n      >\n        {done ? (\n          glyph\n        ) : (\n          <span aria-hidden=\"true\" className=\"flex h-4 items-stretch gap-1\">\n            <span className={grip} />\n            <span className={grip} />\n            <span className={grip} />\n          </span>\n        )}\n      </motion.button>\n      {tail}\n    </div>\n  );\n}\n\n/** How close to the end counts as the end, in px, so a finger need not land on the last pixel. */\nconst SLACK = 4;\n\n/** A track with a thumb to slide to the end. The thumb is also a button: Enter, Space or a screen reader confirms. */\nexport function SlideConfirm({ label, confirmedLabel = \"Done\", onConfirm, confirmText, size = \"md\", disabled = false, className = \"\" }: SlideConfirmProps) {\n  const material = useMaterial();\n  const Body = byWorld(material, { press: PressBody, flat: SlideConfirmFlatBody, device: SlideConfirmDeviceBody });\n  const springHome = useTransition(\"responsive\");\n  /* Device's key goes home in three steps on the instant tier, as the Switch's detent does: no spring. */\n  const instant = useTransition(\"instant\");\n  const back = material === \"device\" ? { ...instant, ease: (t: number) => (t >= 1 ? 1 : Math.floor(t * 3) / 3) } : springHome;\n  const hintId = useId();\n  const box = useRef<HTMLDivElement>(null);\n  const thumb = useRef<HTMLButtonElement>(null);\n  const x = useMotionValue(0);\n  const [max, setMax] = useState(0); // how far the thumb can travel\n  const [inner, setInner] = useState(0); // the track's width inside its padding\n  const [done, setDone] = useState(false);\n  const [typing, setTyping] = useState(false);\n  const [typed, setTyped] = useState(\"\");\n  const panelId = useId();\n  const opener = useRef<HTMLButtonElement>(null);\n  const field = useRef<HTMLInputElement>(null);\n  const drag = useRef<{ from: number; id: number } | null>(null);\n  /* Fun mode: the throw. The last moves, to read the speed the thumb is let go at, and the flight it is on. */\n  const fun = useFun();\n  const trail = useRef<{ t: number; x: number }[]>([]);\n  const flight = useRef<AnimationPlaybackControls | null>(null);\n  /* A thumb still in the air when the track goes away never lands, so it never confirms. */\n  useEffect(\n    () => () => {\n      flight.current?.stop();\n      flight.current = null;\n    },\n    [],\n  );\n\n  /* Measured before paint and kept current, so the end is always the end (and again if the material swaps the body). */\n  useLayoutEffect(() => {\n    const el = box.current;\n    const t = thumb.current;\n    if (!el || !t) return;\n    const read = () => {\n      const pad = parseFloat(getComputedStyle(el).paddingLeft) || 0;\n      const w = el.clientWidth - 2 * pad;\n      setInner(w);\n      setMax(Math.max(0, w - t.offsetWidth));\n    };\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(read);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [Body]); // a new body is new elements, so it is measured again\n\n  /* A confirmed thumb stays at the end, also when the track is resized. */\n  useEffect(() => {\n    if (done) x.set(max);\n  }, [done, max, x]);\n\n  /* The transforms read the sizes from a ref, so a resize never leaves them holding an old width. */\n  const span = useRef({ max: 0, inner: 0 });\n  span.current = { max, inner };\n  const words = useTransform(x, (v) => Math.max(0, 1 - v / Math.max(1, span.current.max * 0.6)));\n  const band = useTransform(x, (v) => (span.current.inner > 0 && thumb.current ? (v + thumb.current.offsetWidth) / span.current.inner : 0));\n  /* Fun mode's walls: a throw's spring may carry x past an end, but the thumb stops there and squashes by how far it\n     went, against the wall it hit, then springs back to its shape as x comes back. */\n  const walled = useTransform(x, (v) => Math.min(span.current.max, Math.max(0, v)));\n  const squash = useTransform(x, (v) => {\n    const over = v > span.current.max ? v - span.current.max : v < 0 ? -v : 0;\n    const size = thumb.current?.offsetWidth || 1;\n    return 1 - Math.min(0.5, over / size);\n  });\n  const wall = useTransform(x, (v) => (v < 0 ? 0 : 1));\n\n  const confirm = () => {\n    if (done || disabled) return;\n    drag.current = null;\n    x.set(max);\n    setDone(true);\n    onConfirm?.();\n  };\n  const matches = confirmText !== undefined && typed.trim() === confirmText;\n  const typeConfirm = () => {\n    if (!matches || done || disabled) return;\n    /* Focus moves before the commit that disables the field and the button, so it never falls to the page. */\n    thumb.current?.focus();\n    confirm();\n  };\n  useEffect(() => {\n    if (typing) field.current?.focus();\n  }, [typing]);\n  const onFieldKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      typeConfirm();\n    } else if (e.key === \"Escape\") {\n      e.preventDefault();\n      setTyping(false);\n      opener.current?.focus();\n    }\n  };\n  const verb = label.replace(/^slide to /i, \"\").replace(/^./, (c) => c.toUpperCase());\n  const goBack = () => {\n    drag.current = null;\n    if (fun) return fly(0, 0);\n    void animate(x, 0, back);\n  };\n\n  /* Fun mode: a throw to `to`, carrying `velocity` (px/s). Landing at the end confirms, as reaching it by hand does. */\n  const fly = (to: number, velocity: number) => {\n    if (!fun) return;\n    flight.current?.stop();\n    const run = animate(x, to, { ...fun.throw, velocity });\n    flight.current = run;\n    void run.then(() => {\n      if (flight.current !== run) return;\n      flight.current = null;\n      if (to === max) confirm();\n    });\n  };\n  /* The speed over the last 100ms of moves, in px/s: zero if the finger rested before letting go. */\n  const releaseSpeed = () => {\n    const now = performance.now();\n    const recent = trail.current.filter((p) => now - p.t <= 100);\n    const first = recent[0];\n    const last = recent[recent.length - 1];\n    return first && last && last.t > first.t ? ((last.x - first.x) / (last.t - first.t)) * 1000 : 0;\n  };\n\n  const onPointerDown = (e: PointerEvent<HTMLButtonElement>) => {\n    if (done || disabled || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    if (fun) {\n      flight.current?.stop(); // caught mid-flight: it is in the hand again, wherever it was\n      flight.current = null;\n      x.set(Math.min(max, Math.max(0, x.get())));\n      trail.current = [];\n    }\n    drag.current = { from: e.clientX - x.get(), id: e.pointerId };\n  };\n  const onPointerMove = (e: PointerEvent<HTMLButtonElement>) => {\n    if (!drag.current || drag.current.id !== e.pointerId) return;\n    x.set(Math.min(max, Math.max(0, e.clientX - drag.current.from)));\n    if (fun) trail.current.push({ t: performance.now(), x: x.get() });\n  };\n  const onPointerUp = () => {\n    if (!drag.current) return;\n    if (fun) {\n      drag.current = null;\n      const velocity = releaseSpeed();\n      const at = x.get();\n      return fly(at + fun.coast(velocity) >= max - SLACK ? max : 0, velocity);\n    }\n    if (x.get() >= max - SLACK) confirm();\n    else goBack();\n  };\n  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n    if (e.key === \"Escape\" && drag.current) {\n      e.preventDefault();\n      goBack();\n    }\n  };\n\n  const glyph = fun ? (\n    /* Fun mode: the check pops in where the chevron was. */\n    <motion.span key={done ? \"done\" : \"slide\"} className=\"grid place-items-center\" initial={done ? { scale: 0.4, opacity: 0 } : false} animate={{ scale: 1, opacity: 1 }} transition={fun.pop}>\n      {done ? <CheckIcon /> : <ChevronRightIcon />}\n    </motion.span>\n  ) : done ? (\n    <CheckIcon />\n  ) : (\n    <ChevronRightIcon />\n  );\n\n  const slider = (\n    <Body\n      label={label}\n      confirmedLabel={confirmedLabel}\n      done={done}\n      disabled={disabled}\n      size={size}\n      track={trackSizes[size] + \" \" + (confirmText === undefined ? className : \"\")}\n      box={box}\n      band={band}\n      words={words}\n      glyph={glyph}\n      thumb={{\n        ref: thumb,\n        type: \"button\",\n        disabled,\n        \"aria-disabled\": done || undefined,\n        \"aria-label\": label,\n        \"aria-describedby\": hintId,\n        style: fun ? { x: walled, scaleX: squash, originX: wall } : { x },\n        onPointerDown,\n        onPointerMove,\n        onPointerUp,\n        onPointerCancel: goBack,\n        onKeyDown,\n        /* detail is 0 for a click made by a key or by assistive tech, and 1 or more for a real press. */\n        onClick: (e) => e.detail === 0 && confirm(),\n      }}\n      tail={\n        <>\n          <span id={hintId} className=\"sr-only\">\n            Drag the thumb to the end, or press Enter.\n          </span>\n          <span role=\"status\" className=\"sr-only\">\n            {done ? confirmedLabel : \"\"}\n          </span>\n        </>\n      }\n    />\n  );\n  if (confirmText === undefined) return slider;\n  return (\n    <div className={\"grid w-full gap-2 \" + className}>\n      {slider}\n      <Button\n        ref={opener}\n        variant=\"ghost\"\n        size=\"sm\"\n        disabled={disabled || done}\n        aria-expanded={typing}\n        aria-controls={typing ? panelId : undefined}\n        onClick={() => setTyping((t) => !t)}\n        className=\"justify-self-start pointer-coarse:h-litt-control-md\"\n      >\n        Or type to confirm\n      </Button>\n      {typing && (\n        <div id={panelId} className=\"grid gap-3\">\n          <TextField\n            ref={field}\n            label={`Type \"${confirmText}\" to confirm`}\n            value={typed}\n            onChange={(e) => setTyped(e.target.value)}\n            onKeyDown={onFieldKeyDown}\n            disabled={disabled || done}\n            autoComplete=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck={false}\n          />\n          <Button variant=\"danger\" disabled={!matches || disabled || done} onClick={typeConfirm} className=\"justify-self-start\">\n            {verb}\n          </Button>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "target": "components/litt/slide-confirm.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/slide-confirm.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slide-confirm 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Slide to confirm, the Device body (proposals/worlds.md, Bodies; material.md, 8): a latch key pushed along a well to an\n * end stop.\n *\n * - A key (--litt-block, --litt-key-lift: its face, a dark rim and a 2px skirt) in a well (--litt-tray, --litt-tray-shade), the key as tall\n *   as the well less its 2px lip, so its corners are the well's less those 2px and the two stay concentric. The\n *   chevron, or the check once it has latched, is on its face.\n * - The track's words sit on an LCD strip (--litt-field, --litt-field-shade) cut into the well between the key's two ends, in the\n *   mono at the caption size, so the key never covers them at rest. They fade by the key's position, as in the other\n *   bodies. A lamp at the stop, on the glass, lights once confirmed: the glass's own ink, never the accent (R6), and the\n *   confirmed words say it too, so the lamp never says a state alone.\n * - The hand: press and the key sinks the whole --litt-press-depth (2px) onto its skirt; it keeps that sink while it is\n *   dragged, and follows the finger 1:1 along the well. Confirmed, it stays latched down at the stop, sunk, with its\n *   lamp lit. A disabled key lies flat with a hairline, and the glass keeps its words.\n * - Let go short and the key goes home in three steps on the instant tier (the contract's drive, steps(3, end)), never a\n *   spring; the press and the shade change in the same three steps. Reduced motion zeroes the tier, so it is home at\n *   once. Fun mode's throw and pop come through the contract and leave the rest as it is.\n * - Forced colors drops shadows and fills, so the well, the key and the glass get real borders and the lamp is a\n *   system highlight when lit and an outline when not.\n */\nimport type { CSSProperties } from \"react\";\nimport { motion } from \"motion/react\";\nimport type { SlideConfirmBodyProps } from \"../slide-confirm\";\n\nconst ring = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The key sinks and its shade steps in the same three steps as the home run, on the instant tier. */\nconst STEPS = \"steps(3, end)\";\nconst stepped: CSSProperties = {\n  transitionProperty: \"translate, background-color, box-shadow\",\n  transitionDuration: \"var(--litt-dur-instant)\",\n  transitionTimingFunction: `${STEPS}, var(--litt-ease-out), var(--litt-ease-out)`,\n};\n\nconst keyBase =\n  \"relative grid aspect-square h-full shrink-0 touch-none place-items-center rounded-[calc(var(--litt-radius-control)-2px)] [-webkit-tap-highlight-color:transparent] \" +\n  ring +\n  \" forced-colors:border\";\nconst keyUp = \"cursor-grab bg-litt-block text-litt-fg shadow-(--litt-key-lift) active:translate-y-(--litt-press-depth) active:cursor-grabbing active:shadow-(--litt-block-press)\";\nconst keyLatched = \"cursor-default translate-y-(--litt-press-depth) bg-litt-block text-litt-fg shadow-(--litt-block-press)\";\nconst keyDisabled = \"cursor-not-allowed bg-litt-surface-2 text-litt-fg-disabled shadow-[inset_0_0_0_1px_var(--litt-line)]\";\n\n/* The glass is cut between the key's two ends: the key (the well's height less its 4px lip) and a 2px gap. */\nconst glassSize = { md: \"inset-x-11.5 pointer-coarse:inset-x-14\", lg: \"inset-x-14\" };\n\n/** Device, the latch key: a key pushed along a well to a stop, its words on an LCD strip and a lamp that lights at the stop. */\nexport function SlideConfirmDeviceBody({ label, confirmedLabel, done, disabled, size, track, box, band, words, glyph, thumb, tail }: SlideConfirmBodyProps) {\n  return (\n    <div\n      ref={box}\n      data-body=\"device\"\n      data-no-drag=\"\"\n      className={\"relative isolate flex w-full items-center overflow-hidden rounded-litt-control bg-litt-tray p-0.5 shadow-(--litt-tray-shade) select-none forced-colors:border \" + track}\n    >\n      {/* The part the key has passed: a full-width band scaled from the left, so only a transform changes. */}\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-y-0.5 left-0.5 -z-1 w-[calc(100%-0.25rem)] origin-left rounded-[calc(var(--litt-radius-control)-2px)] bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)]\"\n        style={{ scaleX: band }}\n      />\n      <span\n        aria-hidden=\"true\"\n        className={\n          \"pointer-events-none absolute inset-y-1.5 flex items-center justify-center rounded-[calc(var(--litt-radius-control)-2px)] bg-litt-field px-5 font-litt-field text-litt-caption font-litt-title shadow-(--litt-field-shade) forced-colors:border \" +\n          glassSize[size]\n        }\n      >\n        <motion.span className={\"truncate \" + (done ? \"text-litt-field-ink\" : \"text-litt-field-ink-subtle\")} style={{ opacity: done ? 1 : words }}>\n          {done ? confirmedLabel : label}\n        </motion.span>\n        <span\n          className={\n            \"absolute right-2 size-1.5 shrink-0 rounded-litt-pill forced-colors:border \" +\n            (done ? \"bg-litt-field-ink shadow-[0_0_6px_var(--litt-field-ink)] forced-colors:bg-[Highlight]\" : \"bg-litt-field-line forced-colors:bg-transparent\")\n          }\n        />\n      </span>\n      <motion.button\n        {...thumb}\n        style={{ ...thumb.style, ...stepped }}\n        className={keyBase + \" \" + (disabled ? keyDisabled : done ? keyLatched : keyUp)}\n      >\n        {glyph}\n      </motion.button>\n      {tail}\n    </div>\n  );\n}\n",
      "target": "components/litt/bodies/slide-confirm.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/slide-confirm.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/slide-confirm 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Slide to confirm, the Flat body (proposals/worlds.md, Bodies; proposals/flat-pill.md): ink on paper. A grey pill\n * (--litt-surface-3, no frame, nothing cast) and a plain ink circle (--litt-fg) with nothing on it, no chevron. The words sit in\n * the pill in ink and fade by the circle's position; the part it has passed is not drawn. Confirmed, the whole pill\n * turns ink and the words and the check go --litt-canvas. A disabled circle is a flat --litt-surface-3 on --litt-surface-2. The drag,\n * the keys and the thresholds are slide-confirm.tsx's.\n */\nimport { motion } from \"motion/react\";\nimport type { SlideConfirmBodyProps } from \"../slide-confirm\";\n\n/** Flat, the pill: a grey pill, an ink circle that slides in it, and ink words. */\nexport function SlideConfirmFlatBody({ label, confirmedLabel, done, disabled, track, box, band, words, glyph, thumb, tail }: SlideConfirmBodyProps) {\n  const ink = done ? \"text-litt-canvas\" : disabled ? \"text-litt-fg-subtle\" : \"text-litt-fg\"; // disabled words stay fg-subtle: they still say what it would do\n  return (\n    <div\n      ref={box}\n      data-body=\"flat\"\n      data-no-drag=\"\"\n      className={\n        \"relative isolate flex w-full items-center overflow-hidden rounded-litt-pill p-1 transition-colors duration-(--litt-dur-instant) ease-litt select-none forced-colors:border \" +\n        (done ? \"bg-litt-fg \" : disabled ? \"bg-litt-surface-2 \" : \"bg-litt-surface-3 \") +\n        track\n      }\n    >\n      {/* The passed part: not drawn. Ink on paper has no fill to show how far it went, the circle says it; the span stays so the children keep their order. */}\n      <motion.span aria-hidden=\"true\" className=\"hidden\" style={{ scaleX: band }} />\n      <motion.span\n        aria-hidden=\"true\"\n        className={\"pointer-events-none absolute inset-0 grid place-items-center px-12 text-center text-litt-small font-litt-title \" + ink}\n        style={{ opacity: done ? 1 : words }}\n      >\n        <span className=\"max-w-full min-w-0 truncate\">{done ? confirmedLabel : label}</span>\n      </motion.span>\n      <motion.button\n        {...thumb}\n        className={\n          \"relative grid aspect-square h-full shrink-0 touch-none place-items-center rounded-litt-pill [-webkit-tap-highlight-color:transparent] \" +\n          \"transition-colors duration-(--litt-dur-instant) ease-litt \" +\n          \"focus-visible:outline-2 focus-visible:outline-offset-2 forced-colors:border \" +\n          (done ? \"focus-visible:outline-litt-canvas \" : \"focus-visible:outline-litt-focus \") +\n          (disabled ? \"cursor-not-allowed bg-litt-surface-3 text-litt-fg-disabled\" : \"bg-litt-fg text-litt-canvas \" + (done ? \"cursor-default\" : \"cursor-grab active:cursor-grabbing\"))\n        }\n      >\n        {done ? glyph : null}\n      </motion.button>\n      {tail}\n    </div>\n  );\n}\n",
      "target": "components/litt/bodies/slide-confirm.flat.tsx"
    }
  ],
  "docs": "Import SlideConfirm from components/litt/slide-confirm. Give it a label that says what happens (\"Slide to delete Atelier Nord\"), a confirmedLabel and onConfirm, which runs once. Remount it with a new key to ask again. Use it for what cannot be undone, on touch screens; anything that can be undone should just happen, with Undo in a toast.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
