{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "type": "registry:ui",
  "title": "Slider",
  "description": "A slider whose thumb grows while held and springs back on release, settling on the nearest step. A bubble shows the value through your formatter and stays inside the slider at the ends. WAI-ARIA slider keys (arrows, Page Up, Page Down, Home, End) and pointer capture for mouse, pen and touch.",
  "author": "ARLing s. r. o. (https://arling.sk)",
  "registryDependencies": [
    "https://arling.sk/motion/r/core.json"
  ],
  "files": [
    {
      "path": "registry/arling/lib/arling-motion-slider.js",
      "content": "'use strict';\n// ARLing Motion: slider. MIT licence, https://arling.sk/motion/ . Installed from the ARLing\n// Motion registry (source: components/slider/slider.js). Plain JavaScript; the .d.ts next to it types the exports.\n/*\n * ARLing Motion: Slider.\n * A slider whose thumb grows a little while you hold it and springs back to its size when you\n * let go. While you drag, the thumb follows the pointer through a very short spring without\n * overshoot; on release it settles on the nearest step and keeps the speed it had. A press on\n * the track glides the thumb there, and the keys glide it on the same spring. A bubble above\n * the thumb shows the value through your formatter (\"400 conversations\", \"19 €\"); near the ends\n * it stays inside the slider and its tail keeps pointing at the thumb. Every position is a pure\n * function of time, so seek(t) paints any moment of a scheduled drag.\n *\n * Markup:\n *   <div class=\"am-slider\" aria-label=\"Conversations per month\"\n *        data-min=\"0\" data-max=\"2000\" data-step=\"50\" data-value=\"400\"></div>\n * The component adds the parts that are missing: the track (.am-slider-track) with its range\n * (.am-slider-range), the thumb (.am-slider-thumb, role=\"slider\", the one focusable part) and\n * the bubble (.am-slider-bubble). A name on the root (aria-label or aria-labelledby) moves to\n * the thumb. Horizontal, left to right.\n *\n * Keyboard (WAI-ARIA APG, slider): Right and Up add one step, Left and Down take one away,\n * Page Up and Page Down move by a big step (a tenth of the range by default), Home and End go\n * to the minimum and the maximum. The thumb carries aria-valuemin, aria-valuemax,\n * aria-valuenow and aria-valuetext (the formatted value). The bubble is aria-hidden.\n * Pointer: mouse, pen and touch through pointer events with pointer capture; the root has\n * touch-action: none.\n * MIT licence.\n */\nimport { track, presence, driver, steps, attr, spring, PRESETS } from './arling-motion';\n\nconst clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);\nconst round2 = (v) => Math.round(v * 100) / 100;\nconst round4 = (v) => Math.round(v * 1e4) / 1e4;\nconst px = (v) => `${round2(v)}px`;\n\n/** While you drag, the thumb follows the pointer through this spring: quick, no overshoot. */\nexport const FOLLOW = spring(0.1, 1);\n/** Scale of the thumb while it is held. */\nexport const HOLD_SCALE = 1.2;\n/** The bubble grows by this share while the thumb is held. */\nexport const BUBBLE_GROW = 0.06;\n/** When the bubble shows: while the thumb is held or has keyboard focus, always, or never. */\nexport const BUBBLE_MODES = ['active', 'always', 'none'];\n/** Samples per second of a scheduled drag (demos and video). */\nexport const DRAG_RATE = 120;\n/** Easing of a scheduled pointer path. */\nexport const EASE = {\n  linear: (u) => u,\n  in: (u) => u * u,\n  out: (u) => 1 - (1 - u) * (1 - u),\n  inOut: (u) => u * u * (3 - 2 * u),\n};\n/** The bubble's tail keeps this far from its rounded ends, px. */\nconst TAIL_INSET = 10;\nconst BUBBLE_MOTION = { dyIn: 6, dyOut: 4, blur: 3, scaleFrom: 0.85 };\nconst SHOWN = { opacity: 1, blur: 0, y: 0, scale: 1, visible: true };\n\nconst decimalsOf = (n) => {\n  const s = String(n);\n  const i = s.indexOf('.');\n  return i < 0 || /e/i.test(s) ? 0 : s.length - i - 1;\n};\n\n/** The value on the grid min + k * step nearest to v, kept inside [min, max]. */\nexport function snapValue(v, min, max, step) {\n  if (!(max > min)) return min;\n  const s = step > 0 ? step : 1;\n  const dec = Math.max(decimalsOf(s), decimalsOf(min));\n  const r = Number((min + Math.round((v - min) / s) * s).toFixed(dec));\n  return r < min ? min : r > max ? max : r;\n}\n\n/**\n * createSlider({ root, min, max, step, bigStep, value, format, locale, prefix, suffix, bubble,\n *   label, disabled, onChange, onCommit, clock, reduced })\n * format(value) returns the text of the bubble and of aria-valuetext (default: the number in\n * the locale with prefix and suffix). onChange(value) runs whenever user input changes the\n * value, onCommit(value) when a drag, a press on the track or a key ends with a new value.\n * drag(value, { t, duration, ease }), tap(value, { t }) and key(name, { t }) schedule input for\n * demos. Returns { set, value, text, key, drag, tap, offsetOf, measure, seek, settled, destroy,\n * driver, bounds, root, thumb, track, range, bubble, keep }.\n */\nexport function createSlider(o) {\n  const root = o.root;\n  const doc = root.ownerDocument || document;\n  const data = (k) => (root.hasAttribute(`data-${k}`) ? root.getAttribute(`data-${k}`) : undefined);\n  const num = (v, fallback) => (v === undefined || v === null || v === '' || !Number.isFinite(Number(v)) ? fallback : Number(v));\n\n  const min = num(o.min ?? data('min'), 0);\n  const max = Math.max(min, num(o.max ?? data('max'), 100));\n  const stepIn = num(o.step ?? data('step'), 1);\n  const step = stepIn > 0 ? stepIn : 1;\n  const bigIn = num(o.bigStep ?? data('big-step'), 0);\n  const bigStep = bigIn > 0 ? bigIn : Math.max(step, Math.round((max - min) / 10 / step) * step);\n  const snap = (v) => snapValue(v, min, max, step);\n  const frac = (v) => (max > min ? clamp01((v - min) / (max - min)) : 0);\n  const fromFrac = (f) => snap(min + clamp01(f) * (max - min));\n\n  const locale = o.locale ?? data('locale') ?? 'en-US';\n  const prefix = o.prefix ?? data('prefix') ?? '';\n  const suffix = o.suffix ?? data('suffix') ?? '';\n  const nf = new Intl.NumberFormat(locale, { maximumFractionDigits: Math.max(decimalsOf(step), decimalsOf(min)) });\n  const format = (v) => {\n    const s = o.format ? o.format(v) : undefined;\n    return s === undefined || s === null ? `${prefix}${nf.format(v)}${suffix}` : String(s);\n  };\n  const asked = o.bubble === false ? 'none' : o.bubble ?? data('bubble');\n  const mode = BUBBLE_MODES.includes(asked) ? asked : 'active';\n\n  // ---------------------------------------------------------------- parts\n  const make = (cls, parent) => {\n    const s = doc.createElement('span');\n    s.className = cls;\n    parent.appendChild(s);\n    return s;\n  };\n  root.classList.add('am-slider');\n  const rail = root.querySelector('.am-slider-track') || make('am-slider-track', root);\n  const range = rail.querySelector('.am-slider-range') || make('am-slider-range', rail);\n  const thumb = root.querySelector('.am-slider-thumb') || make('am-slider-thumb', root);\n  let bubble = root.querySelector('.am-slider-bubble');\n  if (mode === 'none') {\n    if (bubble) bubble.style.visibility = 'hidden';\n    bubble = null;\n  } else if (!bubble) bubble = make('am-slider-bubble', root);\n  attr(rail, 'aria-hidden', 'true');\n  if (bubble) attr(bubble, 'aria-hidden', 'true');\n\n  attr(thumb, 'role', 'slider');\n  for (const name of ['aria-label', 'aria-labelledby']) {\n    if (!root.hasAttribute(name)) continue;\n    if (!thumb.hasAttribute(name)) attr(thumb, name, root.getAttribute(name));\n    root.removeAttribute(name);\n  }\n  if (o.label) attr(thumb, 'aria-label', o.label);\n  attr(thumb, 'aria-valuemin', String(min));\n  attr(thumb, 'aria-valuemax', String(max));\n  if (o.disabled !== undefined) attr(root, 'data-disabled', o.disabled ? true : null);\n  if (thumb.getAttribute('aria-disabled') === 'true') attr(root, 'data-disabled', true);\n  const disabled = () => root.hasAttribute('data-disabled');\n\n  // ---------------------------------------------------------------- geometry, measured per gesture and on resize\n  function measureGeo() {\n    const r = root.getBoundingClientRect();\n    const W = root.offsetWidth || r.width || 0;\n    const T = Math.min(thumb.offsetWidth || 0, W);\n    // k: screen pixels per layout pixel, for a slider inside a scaled parent\n    return { left: r.left, k: W > 0 && r.width > 0 ? r.width / W : 1, W, T };\n  }\n  let geo = measureGeo();\n  const travel = () => Math.max(0, geo.W - geo.T);\n  /** The fraction of the way under a page x (not clamped). */\n  const fracAt = (clientX) => {\n    const run = travel();\n    return run > 0 ? ((clientX - geo.left) / geo.k - geo.T / 2) / run : 0;\n  };\n  /** Horizontal distance from the centre of the root to the centre of the thumb at value, px. */\n  const offsetOf = (value) => geo.T / 2 + frac(value) * travel() - geo.W / 2;\n\n  // ---------------------------------------------------------------- state as functions of time\n  const v0 = snap(num(o.value ?? data('value'), min));\n  const x = track(frac(v0), FOLLOW); // the thumb, as a fraction of the way\n  const press = track(0, PRESETS.press);\n  const held = steps(false);\n  const pres = bubble && mode === 'active' ? presence(false, BUBBLE_MOTION) : null;\n  let bubbleOn = false;\n  let keyboard = false;\n  let endT = -Infinity;\n  const mark = (t) => { if (t > endT) endT = t; };\n  const settledAll = (t) => x.settled(t) && press.settled(t) && (!pres || pres.settled(t));\n  /** The value at time t: the step nearest to where the pointer or the last key aimed. */\n  const valueAt = (t) => fromFrac(x.target(t));\n\n  let shownText = null;\n  let bw = 0;\n\n  function paint(t, { reduced }) {\n    const v = valueAt(t);\n    const text = format(v);\n    const off = disabled();\n    attr(thumb, 'aria-valuenow', String(v));\n    attr(thumb, 'aria-valuetext', text);\n    attr(thumb, 'aria-disabled', off ? 'true' : null);\n    attr(thumb, 'tabindex', off ? '-1' : '0');\n    const on = held.at(t);\n    attr(thumb, 'data-state', on ? 'active' : 'idle');\n    attr(root, 'data-state', on ? 'active' : 'idle');\n\n    const f = clamp01(reduced ? x.target(t) : x.at(t));\n    const p = reduced ? 0 : press.at(t);\n    const s = 1 + (HOLD_SCALE - 1) * p;\n    const run = travel();\n    const cx = geo.T / 2 + f * run;\n    thumb.style.transform = `translateX(${px(f * run)}) scale(${round4(s)})`;\n    // the range ends under the centre of the thumb; the track's rounded clip shapes its start\n    range.style.transform = `scaleX(${round4(geo.W > 0 ? cx / geo.W : f)})`;\n    if (!bubble) return;\n\n    if (text !== shownText) {\n      bubble.textContent = text;\n      shownText = text;\n      bw = bubble.offsetWidth || 0; // once per new text, never per frame\n    }\n    const st = pres ? (reduced ? pres.still(t) : pres.at(t)) : SHOWN;\n    const lift = ((s - 1) * geo.T) / 2; // keeps the gap to the growing thumb\n    let left = cx;\n    let tail = 0;\n    let centre = ' translateX(-50%)';\n    if (bw > 0 && geo.W > 0) {\n      left = bw >= geo.W ? (geo.W - bw) / 2 : Math.min(Math.max(cx - bw / 2, 0), geo.W - bw);\n      const lim = Math.max(0, bw / 2 - TAIL_INSET);\n      tail = Math.max(-lim, Math.min(lim, cx - left - bw / 2));\n      centre = '';\n    }\n    bubble.style.transform = `translate(${px(left)}, ${px(st.y - lift)})${centre} scale(${round4(st.scale * (1 + BUBBLE_GROW * p))})`;\n    bubble.style.transformOrigin = bw > 0 ? `${px(bw / 2 + tail)} 100%` : '50% 100%';\n    bubble.style.setProperty('--am-slider-tail', px(tail));\n    bubble.style.opacity = String(Math.round(st.opacity * 1000) / 1000);\n    bubble.style.filter = st.blur > 0.05 ? `blur(${st.blur.toFixed(2)}px)` : '';\n    bubble.style.visibility = st.visible ? 'visible' : 'hidden';\n  }\n\n  let live = null; // a pointer that holds the thumb: { id, offset, from }\n\n  function draw(t, st) {\n    paint(t, st);\n    if (!api.keep && !live && t >= endT && settledAll(t)) {\n      x.compact(t);\n      press.compact(t);\n      held.compact();\n      if (pres) {\n        pres.p.compact(t);\n        pres.marks = pres.marks.slice(-1);\n      }\n    }\n  }\n\n  const d = driver(draw, { clock: o.clock, reduced: o.reduced });\n  d.busy = (t) => !!live || t < endT || !settledAll(t);\n  const commit = () => { paint(d.now(), { reduced: d.reduced }); d.kick(); };\n  const when = (opt) => (opt && opt.t !== undefined ? { t: opt.t, live: false } : { t: d.now(), live: true });\n\n  // ---------------------------------------------------------------- changes (in time order)\n  function showBubble(t) {\n    if (!pres) return;\n    const want = held.last || keyboard;\n    if (want === bubbleOn) return;\n    bubbleOn = want;\n    if (want) pres.enter(t);\n    else pres.exit(t);\n    mark(t);\n  }\n\n  /** Aims the thumb at fraction f from time t on spring sp. */\n  function aim(t, f, sp) {\n    const g = clamp01(f);\n    if (Math.abs(g - x.target(Infinity)) < 1e-12) return;\n    x.to(t, g, sp);\n    mark(t);\n  }\n\n  /** The thumb is taken: it grows and the bubble shows. */\n  function grab(t) {\n    keyboard = false;\n    held.set(t, true);\n    press.to(t, 1, PRESETS.press);\n    showBubble(t);\n    mark(t);\n  }\n\n  /** The thumb is let go: it settles on the step from its speed and springs back to its size. */\n  function letGo(t) {\n    aim(t, frac(valueAt(Infinity)), PRESETS.snappy);\n    held.set(t, false);\n    press.to(t, 0, PRESETS.release);\n    showBubble(t);\n    mark(t);\n  }\n\n  let reported = v0;\n  const emit = () => {\n    const v = valueAt(Infinity);\n    if (v === reported) return;\n    reported = v;\n    if (o.onChange) o.onChange(v);\n  };\n  const done = (from) => {\n    const v = valueAt(Infinity);\n    if (v !== from && o.onCommit) o.onCommit(v);\n  };\n\n  /** Moves to value (snapped to the step) on the snappy spring. No callbacks. opt: { t }. */\n  function set(value, opt = {}) {\n    const { t } = when(opt);\n    const v = snap(num(value, valueAt(Infinity)));\n    aim(t, frac(v), PRESETS.snappy);\n    reported = v;\n    commit();\n    return api;\n  }\n\n  /** The value a key leads to from v, or null when the key does not move a slider. */\n  function keyTarget(name, v) {\n    switch (name) {\n      case 'ArrowRight': case 'ArrowUp': return v + step;\n      case 'ArrowLeft': case 'ArrowDown': return v - step;\n      case 'PageUp': return v + bigStep;\n      case 'PageDown': return v - bigStep;\n      case 'Home': return min;\n      case 'End': return max;\n      default: return null;\n    }\n  }\n\n  function applyKey(name, t, now) {\n    const from = valueAt(Infinity);\n    const to = keyTarget(name, from);\n    if (to === null) return false;\n    if (now && disabled()) return true;\n    keyboard = true;\n    showBubble(t);\n    aim(t, frac(snap(to)), PRESETS.snappy);\n    commit();\n    if (now) {\n      emit();\n      done(from);\n    }\n    return true;\n  }\n\n  /** A key on the thumb (ArrowRight, PageUp, Home and so on). opt: { t } to schedule it. */\n  function key(name, opt = {}) {\n    const { t, live: now } = when(opt);\n    applyKey(name, t, now);\n    return api;\n  }\n\n  /** Scheduled drag for demos: the pointer takes the thumb where it is and moves it to value. */\n  function drag(value, opt = {}) {\n    const t0 = opt.t ?? d.now();\n    const dur = Math.max(1 / DRAG_RATE, opt.duration ?? 0.5);\n    const ease = EASE[opt.ease] || EASE.inOut;\n    const f0 = x.target(t0);\n    const f1 = frac(num(value, min));\n    grab(t0);\n    const n = Math.max(1, Math.ceil(dur * DRAG_RATE));\n    for (let k = 1; k <= n; k++) x.to(t0 + (dur * k) / n, f0 + (f1 - f0) * ease(k / n), FOLLOW);\n    letGo(t0 + dur);\n    commit();\n    return api;\n  }\n\n  /** Scheduled press on the track for demos: the thumb glides to value. opt: { t, hold }. */\n  function tap(value, opt = {}) {\n    const t = opt.t ?? d.now();\n    const hold = opt.hold ?? 0.12;\n    grab(t);\n    aim(t, frac(num(value, min)), PRESETS.snappy);\n    letGo(t + hold);\n    commit();\n    return api;\n  }\n\n  /** Re-measure after a layout change; positions are fractions, so nothing is planned again. */\n  function measure() {\n    geo = measureGeo();\n    shownText = null; // measure the bubble again too\n    commit();\n    return api;\n  }\n\n  // ---------------------------------------------------------------- live input\n  const listeners = [];\n  const on = (el, type, fn) => { el.addEventListener(type, fn); listeners.push([el, type, fn]); };\n  let pointerFocus = false;\n\n  on(root, 'pointerdown', (e) => {\n    if (live || disabled() || (e.button !== undefined && e.button !== 0)) return;\n    e.preventDefault(); // no text selection, and no mouse down that would move focus away\n    geo = measureGeo();\n    const t = d.now();\n    const f = fracAt(e.clientX);\n    const onThumb = !!(e.target && thumb.contains(e.target));\n    const shown = clamp01(d.reduced ? x.target(t) : x.at(t));\n    live = { id: e.pointerId, offset: onThumb ? shown - f : 0, from: valueAt(Infinity) };\n    pointerFocus = true;\n    try { thumb.focus({ preventScroll: true }); } finally { pointerFocus = false; }\n    grab(t);\n    // on the thumb it stays under the pointer; on the track it glides to the pointer\n    if (onThumb) aim(t, shown, FOLLOW);\n    else aim(t, f, PRESETS.snappy);\n    if (root.setPointerCapture && e.pointerId !== undefined) {\n      try { root.setPointerCapture(e.pointerId); } catch { /* not captured */ }\n    }\n    commit();\n    emit();\n  });\n  const mine = (e) => live && (live.id === undefined || e.pointerId === undefined || e.pointerId === live.id);\n  on(root, 'pointermove', (e) => {\n    if (!mine(e)) return;\n    aim(d.now(), fracAt(e.clientX) + live.offset, FOLLOW);\n    commit();\n    emit();\n  });\n  const finish = (e) => {\n    if (!mine(e)) return;\n    const { id, from } = live;\n    live = null;\n    if (root.releasePointerCapture && id !== undefined) {\n      try { root.releasePointerCapture(id); } catch { /* already released */ }\n    }\n    letGo(d.now());\n    commit();\n    emit();\n    done(from);\n  };\n  on(root, 'pointerup', finish);\n  on(root, 'pointercancel', finish);\n  on(root, 'lostpointercapture', finish);\n  on(thumb, 'keydown', (e) => {\n    if (applyKey(e.key, d.now(), true)) e.preventDefault();\n  });\n  // keyboard focus shows the bubble (like :focus-visible); focus from a pointer does not\n  on(thumb, 'focus', () => {\n    if (pointerFocus || live) return;\n    keyboard = true;\n    showBubble(d.now());\n    commit();\n  });\n  on(thumb, 'blur', () => {\n    if (!keyboard) return;\n    keyboard = false;\n    showBubble(d.now());\n    commit();\n  });\n\n  let ro = null;\n  if (typeof ResizeObserver === 'function') {\n    ro = new ResizeObserver(() => measure());\n    ro.observe(root);\n  }\n\n  const api = {\n    root,\n    thumb,\n    track: rail,\n    range,\n    bubble,\n    driver: d,\n    keep: false,\n    bounds: { min, max, step, bigStep },\n    set,\n    key,\n    drag,\n    tap,\n    measure,\n    offsetOf,\n    /** The value now, or at time t of a scheduled timeline. */\n    value: (t) => valueAt(t === undefined ? Infinity : t),\n    text: (t) => format(valueAt(t === undefined ? Infinity : t)),\n    seek: (t) => paint(t, { reduced: d.reduced }),\n    settled: (t) => !live && t >= endT && settledAll(t),\n    destroy() {\n      d.stop();\n      if (ro) ro.disconnect();\n      for (const [el, type, fn] of listeners) el.removeEventListener(type, fn);\n    },\n  };\n  paint(d.now(), { reduced: d.reduced });\n  return api;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/arling/lib/arling-motion-slider.d.ts",
      "content": "// ARLing Motion: slider. MIT licence, https://arling.sk/motion/ . Types for the plain JavaScript file next to this one.\nexport declare const FOLLOW: any;\nexport declare const HOLD_SCALE: any;\nexport declare const BUBBLE_GROW: any;\nexport declare const BUBBLE_MODES: any;\nexport declare const DRAG_RATE: any;\nexport declare const EASE: any;\nexport declare function snapValue(...args: any[]): any;\nexport declare function createSlider(...args: any[]): any;\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/arling/ui/motion-slider.tsx",
      "content": "'use client';\n// ARLing Motion: Slider for React. A thin wrapper: the vanilla component owns the thumb springs,\n// the bubble, the ARIA values, pointer capture and the keyboard.\n// In the registry the core lives at '@/lib/arling-motion' and this logic at\n// '@/lib/arling-motion-slider'.\nimport * as React from 'react';\nimport { createSlider } from '@/lib/arling-motion-slider';\n\ntype SliderApi = {\n  set: (v: number) => unknown;\n  value: () => number;\n  seek: (t: number) => void;\n  driver: { now: () => number };\n  destroy: () => void;\n};\n\nconst cx = (...c: Array<string | false | null | undefined>) => c.filter(Boolean).join(' ');\n\nexport interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue' | 'children'> {\n  min?: number;\n  max?: number;\n  step?: number;\n  /** Page Up and Page Down step (default: a tenth of the range, on the step grid). */\n  bigStep?: number;\n  defaultValue?: number;\n  /** Controlled value (optional). */\n  value?: number;\n  /** Every change while dragging or pressing keys. */\n  onValueChange?: (value: number) => void;\n  /** Once a drag, a press on the track or a key ends with a new value. */\n  onValueCommit?: (value: number) => void;\n  /** Text of the bubble and of aria-valuetext, for example (v) => `${v} conversations`. */\n  format?: (value: number) => string;\n  /** When the bubble shows: while held or keyboard focused (default), always, or never. */\n  bubble?: 'active' | 'always' | 'none';\n  disabled?: boolean;\n  reducedMotion?: boolean;\n}\n\nexport function Slider({\n  min = 0,\n  max = 100,\n  step = 1,\n  bigStep,\n  defaultValue,\n  value,\n  onValueChange,\n  onValueCommit,\n  format,\n  bubble = 'active',\n  disabled,\n  reducedMotion,\n  className,\n  'aria-label': ariaLabel,\n  'aria-labelledby': ariaLabelledby,\n  ...rest\n}: SliderProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const apiRef = React.useRef<SliderApi | null>(null);\n  const changeRef = React.useRef(onValueChange);\n  changeRef.current = onValueChange;\n  const commitRef = React.useRef(onValueCommit);\n  commitRef.current = onValueCommit;\n  const formatRef = React.useRef(format);\n  formatRef.current = format;\n\n  React.useEffect(() => {\n    if (!ref.current) return;\n    const api = createSlider({\n      root: ref.current,\n      min,\n      max,\n      step,\n      bigStep,\n      value: value ?? defaultValue ?? min,\n      bubble,\n      disabled: !!disabled,\n      reduced: reducedMotion,\n      format: (v: number) => (formatRef.current ? formatRef.current(v) : undefined),\n      onChange: (v: number) => changeRef.current?.(v),\n      onCommit: (v: number) => commitRef.current?.(v),\n    }) as unknown as SliderApi;\n    apiRef.current = api;\n    return () => {\n      api.destroy();\n      apiRef.current = null;\n    };\n    // built once per range and option; later values go through set()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [min, max, step, bigStep, bubble, disabled, reducedMotion]);\n\n  React.useEffect(() => {\n    const api = apiRef.current;\n    if (api && value !== undefined && value !== api.value()) api.set(value);\n  }, [value]);\n\n  // a new formatter shows at once\n  React.useEffect(() => {\n    const api = apiRef.current;\n    if (api) api.seek(api.driver.now());\n  }, [format]);\n\n  return (\n    <div ref={ref} className={cx('am-slider', className)} data-disabled={disabled ? '' : undefined} {...rest}>\n      <span className=\"am-slider-track\" aria-hidden=\"true\">\n        <span className=\"am-slider-range\" />\n      </span>\n      <span className=\"am-slider-thumb\" role=\"slider\" tabIndex={disabled ? -1 : 0} aria-label={ariaLabel} aria-labelledby={ariaLabelledby} />\n      {bubble !== 'none' && <span className=\"am-slider-bubble\" aria-hidden=\"true\" />}\n    </div>\n  );\n}\n\nexport default Slider;\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    "@layer components": {
      ".am-slider": {
        "--am-slider-thumb": "1.25rem",
        "--am-slider-rail": "0.375rem",
        "position": "relative",
        "display": "block",
        "height": "var(--am-slider-thumb)",
        "touch-action": "none",
        "-webkit-user-select": "none",
        "user-select": "none",
        "cursor": "pointer",
        "-webkit-tap-highlight-color": "transparent"
      },
      ".am-slider-track": {
        "position": "absolute",
        "top": "50%",
        "right": "0",
        "left": "0",
        "height": "var(--am-slider-rail)",
        "margin-top": "calc(var(--am-slider-rail) / -2)",
        "overflow": "hidden",
        "border-radius": "9999px",
        "background": "var(--input, #e5e5e5)"
      },
      ".am-slider-range": {
        "position": "absolute",
        "inset": "0",
        "background": "var(--primary, #171717)",
        "transform": "scaleX(0)",
        "transform-origin": "0 50%"
      },
      ".am-slider-thumb": {
        "position": "absolute",
        "top": "50%",
        "left": "0",
        "box-sizing": "border-box",
        "width": "var(--am-slider-thumb)",
        "height": "var(--am-slider-thumb)",
        "margin-top": "calc(var(--am-slider-thumb) / -2)",
        "border": "2px solid var(--primary, #171717)",
        "border-radius": "9999px",
        "background": "var(--background, #ffffff)",
        "box-shadow": "0 1px 2px rgb(0 0 0 / 0.12), 0 2px 6px -2px rgb(0 0 0 / 0.16)",
        "transform-origin": "50% 50%",
        "cursor": "grab",
        "touch-action": "none"
      },
      ".am-slider-thumb[data-state=\"active\"]": {
        "cursor": "grabbing"
      },
      ".am-slider-thumb:focus-visible": {
        "outline": "2px solid var(--ring, #a3a3a3)",
        "outline-offset": "2px"
      },
      ".am-slider[data-disabled]": {
        "opacity": "0.5",
        "cursor": "not-allowed"
      },
      ".am-slider[data-disabled] .am-slider-thumb": {
        "cursor": "not-allowed"
      },
      ".am-slider-bubble": {
        "position": "absolute",
        "bottom": "calc(100% + 0.625rem)",
        "left": "0",
        "box-sizing": "border-box",
        "padding": "0.25rem 0.625rem",
        "border-radius": "calc(var(--radius, 0.625rem) - 2px)",
        "background": "var(--primary, #171717)",
        "color": "var(--primary-foreground, #fafafa)",
        "font-size": "0.8125rem",
        "font-weight": "500",
        "line-height": "1.25rem",
        "white-space": "nowrap",
        "box-shadow": "0 4px 12px -4px rgb(0 0 0 / 0.25)",
        "transform-origin": "50% 100%",
        "visibility": "hidden",
        "pointer-events": "none"
      },
      ".am-slider-bubble::after": {
        "content": "\"\"",
        "position": "absolute",
        "top": "100%",
        "left": "calc(50% + var(--am-slider-tail, 0px))",
        "width": "0",
        "height": "0",
        "margin-left": "-5px",
        "border": "5px solid transparent",
        "border-top-color": "var(--primary, #171717)"
      }
    }
  },
  "docs": "Slider from ARLing Motion (MIT).\nReact: import { Slider } from \"@/components/ui/motion-slider\".\nThe logic is in lib/arling-motion-slider.js (typed by the .d.ts next to it) and works without React: createSlider on your own markup.\nStyles were added to your global CSS under @layer components (Tailwind v4).\nWith Tailwind v3 or no Tailwind, add https://arling.sk/motion/components/slider/slider.css to your styles instead.\nLive demo and docs: https://arling.sk/motion/#slider",
  "categories": [
    "motion",
    "slider",
    "range",
    "input",
    "calculator"
  ]
}
