{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"range-slider-inline","type":"registry:component","title":"Range Slider Inline Slider","description":"An inset fill and inline label and value. Ten evenly spaced stops span the track; markers under text stay hidden but remain interactive, and the thumb parts around either label as it passes.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/range-slider-inline.tsx","type":"registry:component","target":"@components/motion/range-slider-inline.tsx","content":"\"use client\";\n// beui.dev/components/motion/range-slider\n\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { SPRING_GLIDE } from \"@/lib/ease\";\nimport { type SliderOptions, snapSliderValue, useSlider } from \"@/lib/hooks/use-slider\";\nimport { capturePointer, releasePointer, TOUCH_GESTURE_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nconst STOP_COUNT = 10;\nconst HANDLE_START = 8;\nconst HANDLE_END_INSET = 12;\nconst TEXT_INSET = 20;\n// Matches RangeSlider's bouncy grab and release feedback.\nconst SPRING_BOUNCY = { type: \"spring\", stiffness: 500, damping: 14, mass: 0.7 } as const;\n\ntype Stop = { value: number; x: number };\n\nfunction mapBetweenStops(\n  stops: Stop[],\n  point: number,\n  from: keyof Stop,\n  to: keyof Stop,\n) {\n  const upperIndex = stops.findIndex((stop) => stop[from] >= point);\n  const upper = stops[upperIndex < 0 ? stops.length - 1 : upperIndex];\n  const lower = stops[Math.max(0, upperIndex - 1)];\n  if (lower[from] === upper[from]) return upper[to];\n  return lower[to] +\n    ((point - lower[from]) / (upper[from] - lower[from])) * (upper[to] - lower[to]);\n}\n\nfunction nearestStop(stops: Stop[], x: number) {\n  return stops.reduce((nearest, stop) =>\n    Math.abs(stop.x - x) < Math.abs(nearest.x - x) ? stop : nearest,\n  );\n}\n\nexport interface InlineSliderProps extends SliderOptions {\n  /** Rounds snap-stop values. While dragging, the readout keeps this step's decimal precision. */\n  step?: number;\n  /** Compact label inside the left edge of the track. */\n  label: string;\n  /** Formats the inline value without changing its precision. */\n  format?: (value: number) => string;\n  /** Show markers for the ten evenly spaced snap stops, except where they overlap inline text. */\n  showTicks?: boolean;\n  className?: string;\n}\n\n/** An always-visible inline slider with an inset fill and a thumb that parts\n * around its labels, keeping their text readable as the handle passes them. */\nexport function InlineSlider({\n  label,\n  format = String,\n  showTicks = true,\n  className,\n  ...options\n}: InlineSliderProps) {\n  const reduce = useReducedMotion();\n  const step = options.step && options.step > 0 ? options.step : 1;\n  const precision = step.toFixed(6).replace(/0+$/, \"\").split(\".\")[1]?.length ?? 0;\n  const { current, min, max, commit, trackProps, sliderProps } = useSlider({\n    ...options,\n    step: 10 ** -precision,\n    \"aria-label\": options[\"aria-label\"] ?? label,\n    formatValueText: options.formatValueText ?? format,\n  });\n  const labelRef = useRef<HTMLSpanElement>(null);\n  const readoutRef = useRef<HTMLSpanElement>(null);\n  const [geometry, setGeometry] = useState({\n    width: 292,\n    labelWidth: 22,\n    readoutWidth: 24,\n  });\n  const [dragging, setDragging] = useState(false);\n  const dragFrame = useRef<number | null>(null);\n  const pendingDragValue = useRef<number | null>(null);\n  const gesture = useRef<{\n    id: number;\n    left: number;\n    offset: number;\n    x: number;\n  } | null>(null);\n\n  useLayoutEffect(() => {\n    const track = trackProps.ref.current;\n    const labelElement = labelRef.current;\n    const readout = readoutRef.current;\n    if (!track || !labelElement || !readout) return;\n    const measure = () => {\n      const width = track.getBoundingClientRect().width;\n      if (!width) return;\n      const next = {\n        width,\n        labelWidth: labelElement.getBoundingClientRect().width,\n        readoutWidth: readout.getBoundingClientRect().width,\n      };\n      setGeometry((previous) =>\n        previous.width === next.width && previous.labelWidth === next.labelWidth &&\n        previous.readoutWidth === next.readoutWidth ? previous : next,\n      );\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(track);\n    observer.observe(labelElement);\n    observer.observe(readout);\n    return () => observer.disconnect();\n  }, [trackProps.ref]);\n\n  // Each stop owns both a value and a physical position. As in RangeSlider,\n  // the stops span the entire track at even intervals; text only affects\n  // whether a marker is painted, never whether its stop remains interactive.\n  const endX = Math.max(HANDLE_START, geometry.width - HANDLE_END_INSET);\n  const stops = useMemo(() => {\n    const values = [...new Set(Array.from({ length: STOP_COUNT }, (_, index) =>\n      snapSliderValue(min + (index / (STOP_COUNT - 1)) * (max - min), min, max, step),\n    ))];\n    return values.map((value, index) => ({\n      value,\n      x: values.length === 1\n        ? HANDLE_START\n        : HANDLE_START + (index / (values.length - 1)) * (endX - HANDLE_START),\n    }));\n  }, [min, max, step, endX]);\n  const restingX = mapBetweenStops(stops, current, \"value\", \"x\");\n  // One motion value owns the thumb for the entire gesture. Pointer movement\n  // writes pixels directly; only release/click/keyboard changes use a spring.\n  // Never derive dragging pixels from a rounded value or swap to a lagging spring.\n  const handleX = useMotionValue(restingX);\n  const restingTarget = useRef(restingX);\n  const settleTo = useCallback((x: number) => {\n    restingTarget.current = x;\n    handleX.jump(handleX.get());\n    if (reduce) handleX.jump(x);\n    else animate(handleX, x, { type: \"spring\", ...SPRING_GLIDE });\n  }, [handleX, reduce]);\n  useLayoutEffect(() => {\n    if (gesture.current || restingTarget.current === restingX) return;\n    settleTo(restingX);\n  }, [restingX, settleTo]);\n  useEffect(() => () => {\n    handleX.stop();\n    if (dragFrame.current !== null) cancelAnimationFrame(dragFrame.current);\n  }, [handleX]);\n  const fillRight = useTransform(handleX, (x) =>\n    x >= endX ? geometry.width - 2 : x + 8,\n  );\n  // Slide a fixed-size fill inside the inset clipping window. The labels and\n  // dots stay above it, and only transforms animate.\n  const fillX = useTransform(fillRight, (right) => right - geometry.width + 2);\n  // Part progressively over six pixels at each text edge instead of\n  // toggling the stem on/off in a single pointer frame. The thumb uses the\n  // same two-dot treatment across both the label and numeric readout.\n  const split = useTransform(handleX, (x) => {\n    const overlap = (start: number, end: number) => Math.max(0, Math.min(\n      1,\n      (x + 4 - start) / 6,\n      (end - x) / 6,\n    ));\n    return Math.max(\n      overlap(TEXT_INSET, TEXT_INSET + geometry.labelWidth),\n      overlap(\n        geometry.width - TEXT_INSET - geometry.readoutWidth,\n        geometry.width - TEXT_INSET,\n      ),\n    );\n  });\n  const stemOpacity = useTransform(split, (amount) => 1 - amount);\n  const capTop = useTransform(split, (amount) => -amount);\n  const capBottom = useTransform(split, (amount) => amount);\n  const labelBounds = { start: TEXT_INSET, end: TEXT_INSET + geometry.labelWidth };\n  const readoutBounds = {\n    start: geometry.width - TEXT_INSET - geometry.readoutWidth,\n    end: geometry.width - TEXT_INSET,\n  };\n  const overlapsText = (x: number, bounds: { start: number; end: number }) =>\n    x + 2 >= bounds.start && x - 2 <= bounds.end;\n  const ticks = showTicks\n    ? stops\n      .map((stop) => stop.x)\n      .filter((x) => !overlapsText(x, labelBounds) && !overlapsText(x, readoutBounds))\n    : [];\n\n  const queueDragCommit = (value: number) => {\n    pendingDragValue.current = value;\n    if (dragFrame.current !== null) return;\n    dragFrame.current = requestAnimationFrame(() => {\n      dragFrame.current = null;\n      if (pendingDragValue.current !== null) commit(pendingDragValue.current);\n      pendingDragValue.current = null;\n    });\n  };\n\n  const cancelDragCommit = () => {\n    if (dragFrame.current !== null) cancelAnimationFrame(dragFrame.current);\n    dragFrame.current = null;\n    pendingDragValue.current = null;\n  };\n\n  const endGesture = (event: PointerEvent<HTMLDivElement>) => {\n    const active = gesture.current;\n    if (!active || active.id !== event.pointerId) return;\n    // Clear before releasing capture: its lost-capture event must not commit twice.\n    gesture.current = null;\n    cancelDragCommit();\n    setDragging(false);\n    if (!options.disabled && geometry.width > 0) {\n      const x = event.type === \"pointerup\"\n        ? event.clientX - active.left - active.offset\n        : active.x;\n      const stop = nearestStop(stops, x);\n      commit(stop.value);\n      settleTo(options.value === undefined ? stop.x : restingX);\n    } else {\n      settleTo(restingX);\n    }\n    releasePointer(event.currentTarget, event.pointerId);\n  };\n\n  return (\n    <div\n      {...trackProps}\n      onPointerDown={(event) => {\n        if (options.disabled || event.button !== 0 || gesture.current) return;\n        const rect = event.currentTarget.getBoundingClientRect();\n        if (!rect.width) return;\n        event.preventDefault();\n        const pointerX = event.clientX - rect.left;\n        const thumbX = handleX.get();\n        // Grabbing the thumb preserves the exact grab point. A track click\n        // waits for release, so it glides to a dot without an intermediate jump.\n        const offset = Math.abs(pointerX - thumbX - 2) <= 12 ? pointerX - thumbX : 2;\n        gesture.current = { id: event.pointerId, left: rect.left, offset, x: thumbX };\n        setDragging(true);\n        handleX.stop();\n        cancelDragCommit();\n        capturePointer(event.currentTarget, event.pointerId);\n        event.currentTarget.querySelector<HTMLButtonElement>(\"[role=slider]\")?.focus({ preventScroll: true });\n      }}\n      onPointerMove={(event) => {\n        const active = gesture.current;\n        if (!active || active.id !== event.pointerId || options.disabled) return;\n        const x = Math.min(\n          endX,\n          Math.max(HANDLE_START, event.clientX - active.left - active.offset),\n        );\n        active.x = x;\n        handleX.set(x);\n        // Use the same piecewise map as the resting stops, so value and\n        // position agree throughout the drag.\n        queueDragCommit(mapBetweenStops(stops, x, \"x\", \"value\"));\n      }}\n      onPointerUp={endGesture}\n      onPointerCancel={endGesture}\n      onLostPointerCapture={endGesture}\n      className={cn(\n        \"relative h-10 w-full touch-none select-none overflow-hidden rounded-lg bg-muted\",\n        TOUCH_GESTURE_CLASS,\n        options.disabled\n          ? \"pointer-events-none opacity-50\"\n          : \"cursor-grab active:cursor-grabbing\",\n        className,\n      )}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-x-[2px] inset-y-0 overflow-hidden rounded-lg\"\n      >\n        <motion.div\n          className=\"absolute inset-0 rounded-lg bg-foreground/15\"\n          style={{ x: fillX }}\n        />\n      </div>\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 text-foreground\">\n        <span\n          ref={labelRef}\n          className=\"absolute left-5 top-1/2 max-w-[40%] -translate-y-1/2 truncate text-sm font-medium leading-5\"\n        >\n          {label}\n        </span>\n        <span\n          ref={readoutRef}\n          className=\"absolute right-5 top-1/2 max-w-[40%] -translate-y-1/2 truncate text-[13px] font-semibold leading-[18px] tracking-tight tabular-nums\"\n        >\n          {format(current)}\n        </span>\n        {ticks.map((left) => (\n          <span\n            key={left}\n            className=\"absolute top-1/2 size-1 -translate-y-1/2 rounded-full bg-foreground/25\"\n            style={{ left }}\n          />\n        ))}\n      </div>\n      <motion.div\n        aria-hidden=\"true\"\n        animate={reduce ? undefined : { scaleY: dragging ? 1.35 : 1 }}\n        transition={SPRING_BOUNCY}\n        className=\"pointer-events-none absolute left-0 top-2 h-6 w-1 text-foreground\"\n        style={{ x: handleX }}\n      >\n        <motion.span className=\"absolute top-0 size-1 rounded-full bg-current\" style={{ y: reduce ? 0 : capTop }} />\n        <motion.span className=\"absolute inset-y-0 w-1 rounded-full bg-current\" style={{ opacity: stemOpacity }} />\n        <motion.span className=\"absolute bottom-0 size-1 rounded-full bg-current\" style={{ y: reduce ? 0 : capBottom }} />\n      </motion.div>\n      <button\n        type=\"button\"\n        {...sliderProps}\n        onKeyDown={(event) => {\n          if (options.disabled) return;\n          const next = {\n            ArrowRight: stops.find((stop) => stop.value > current)?.value ?? max,\n            ArrowUp: stops.find((stop) => stop.value > current)?.value ?? max,\n            ArrowLeft: stops.findLast((stop) => stop.value < current)?.value ?? min,\n            ArrowDown: stops.findLast((stop) => stop.value < current)?.value ?? min,\n            Home: min,\n            End: max,\n            PageUp: max,\n            PageDown: min,\n          }[event.key];\n          if (next !== undefined) {\n            event.preventDefault();\n            commit(next);\n          }\n        }}\n        className=\"absolute inset-0 cursor-inherit touch-none rounded-lg border-0 outline-none\"\n      />\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-slider.ts","type":"registry:hook","target":"@lib/hooks/use-slider.ts","content":"\"use client\";\n\nimport { type KeyboardEvent, type PointerEvent, useCallback, useRef, useState } from \"react\";\nimport { capturePointer, releasePointer } from \"@/lib/touch\";\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\n/** Nearest legal value on [min, max] for the given step. max counts as a\n * candidate when the step does not divide the range, so a pointer near the end\n * does not snap back onto the last whole step. */\nexport function snapSliderValue(next: number, min: number, max: number, step: number): number {\n  // Neither case has a grid to walk. An empty range has exactly one legal\n  // point, and a non-positive step only needs a clamp, which also keeps the\n  // division below away from zero.\n  if (!(max > min)) return min;\n  if (!(step > 0)) return clamp(next, min, max);\n  const whole = Math.floor(Number(((max - min) / step).toFixed(6)));\n  const lastWhole = Number((min + whole * step).toFixed(6));\n  const toGrid = clamp(Math.round((next - min) / step) * step + min, min, lastWhole);\n  const snapped =\n    lastWhole < max && Math.abs(next - max) <= Math.abs(next - toGrid) ? max : toGrid;\n  return Number(snapped.toFixed(6));\n}\n\nexport interface SliderOptions {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  disabled?: boolean;\n  \"aria-label\"?: string;\n  /** Announced instead of the raw number — pass one when the value carries a\n   * unit or a suffix (\"72.5 kg\", \"35%\"); a bare number needs no valueText. */\n  formatValueText?: (value: number) => string;\n}\n\n/**\n * Shared value + input plumbing for slider designs: controlled/uncontrolled\n * value, step snapping, pointer-capture drag along a track and arrow-key\n * control. Visuals and motion live in the component; this only owns the number.\n */\nexport function useSlider({\n  value,\n  defaultValue = 0,\n  onValueChange,\n  min = 0,\n  max = 100,\n  step = 1,\n  disabled = false,\n  \"aria-label\": ariaLabel,\n  formatValueText,\n}: SliderOptions) {\n  const trackRef = useRef<HTMLDivElement>(null);\n  const sliderEl = useRef<HTMLElement | null>(null);\n  // The state drives visuals. Move reads this ref instead, so the first\n  // pointermove after pointerdown does not have to wait on a re-render.\n  const draggingRef = useRef(false);\n  const [internal, setInternal] = useState(defaultValue);\n  const [dragging, setDragging] = useState(false);\n  const controlled = value !== undefined;\n  // Collapse inverted or empty ranges and non-positive steps here, so that\n  // percent, ticks and the keyboard maths never divide by zero or walk a\n  // NaN grid.\n  const lo = min;\n  const hi = max > min ? max : min;\n  const stride = step > 0 ? step : 1;\n  const current = clamp(controlled ? value : internal, lo, hi);\n  const percent = hi > lo ? ((current - lo) / (hi - lo)) * 100 : 0;\n\n  const commit = useCallback(\n    (next: number) => {\n      const clean = snapSliderValue(next, lo, hi, stride);\n      if (!controlled) setInternal(clean);\n      onValueChange?.(clean);\n    },\n    [controlled, onValueChange, lo, hi, stride],\n  );\n\n  const commitFromX = useCallback(\n    (clientX: number) => {\n      const rect = trackRef.current?.getBoundingClientRect();\n      if (!rect || rect.width === 0) return;\n      const ratio = clamp((clientX - rect.left) / rect.width, 0, 1);\n      commit(lo + ratio * (hi - lo));\n    },\n    [commit, lo, hi],\n  );\n\n  const onPointerDown = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (disabled) return;\n      // Start the drag first: capture is a convenience, and a browser that\n      // refuses it — or a test DOM that has no pointer capture at all — must\n      // not take the drag down with it.\n      draggingRef.current = true;\n      setDragging(true);\n      capturePointer(event.currentTarget, event.pointerId);\n      // A click on the track should land keyboard focus on the handle.\n      sliderEl.current?.focus({ preventScroll: true });\n      commitFromX(event.clientX);\n    },\n    [disabled, commitFromX],\n  );\n\n  const onPointerMove = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (!draggingRef.current || disabled) return;\n      commitFromX(event.clientX);\n    },\n    [disabled, commitFromX],\n  );\n\n  const endDrag = useCallback((event: PointerEvent<HTMLDivElement>) => {\n    releasePointer(event.currentTarget, event.pointerId);\n    draggingRef.current = false;\n    setDragging(false);\n  }, []);\n\n  const onKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLElement>) => {\n      if (disabled) return;\n      const map: Record<string, number> = {\n        ArrowRight: current + stride,\n        ArrowUp: current + stride,\n        ArrowLeft: current - stride,\n        ArrowDown: current - stride,\n        PageUp: current + stride * 10,\n        PageDown: current - stride * 10,\n        Home: lo,\n        End: hi,\n      };\n      if (event.key in map) {\n        event.preventDefault();\n        commit(map[event.key]);\n      }\n    },\n    [disabled, current, stride, lo, hi, commit],\n  );\n\n  return {\n    current,\n    percent,\n    dragging,\n    min: lo,\n    max: hi,\n    step: stride,\n    commit,\n    /** Pointer handlers for the track element — drag anywhere on it. */\n    trackProps: {\n      ref: trackRef,\n      onPointerDown,\n      onPointerMove,\n      onPointerUp: endDrag,\n      onPointerCancel: endDrag,\n      onLostPointerCapture: endDrag,\n    },\n    /** ARIA + keyboard props for the focusable slider element. */\n    sliderProps: {\n      // Callback keeps the handle typed across button/div/motion hosts.\n      ref: (node: HTMLElement | null) => {\n        sliderEl.current = node;\n      },\n      role: \"slider\" as const,\n      tabIndex: disabled ? -1 : 0,\n      \"aria-label\": ariaLabel,\n      \"aria-valuemin\": lo,\n      \"aria-valuemax\": hi,\n      \"aria-valuenow\": current,\n      \"aria-valuetext\": formatValueText?.(current),\n      \"aria-disabled\": disabled || undefined,\n      onKeyDown,\n    },\n  };\n}\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}