{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-slider",
  "type": "registry:ui",
  "title": "Elastic slider",
  "description": "A slider that stretches like rubber past its ends, with a swaying value tag and detents the thumb clicks into.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/elastic-slider/elastic-slider.tsx",
      "type": "registry:component",
      "target": "@components/xzero/elastic-slider/elastic-slider.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type PointerEvent, type ReactNode } from \"react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity, type MotionStyle } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./elastic-slider.module.css\";\n\nexport interface ElasticSliderMark { value: number; label?: string; }\n\nexport interface ElasticSliderProps {\n  /** Visible label; it also names the thumb. */\n  label: string;\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  /** Runs once a drag is released or a key changes the value. */\n  onValueCommit?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  /** PageUp, PageDown, and Shift with an arrow move this far. Defaults to a tenth of the range. */\n  largeStep?: number;\n  /** Detents. A dragged thumb clicks into each one and holds for a few pixels before letting go. Labels print under the track. */\n  marks?: (number | ElasticSliderMark)[];\n  /** Formats the readout, the bubble, and the spoken value. */\n  format?: (value: number) => string;\n  /** Show the value beside the label. The bubble over the thumb still appears while dragging or using keys. */\n  showValue?: boolean;\n  /** Icons at either end. The one on the side being pulled past its limit is pushed outward with the track. */\n  startIcon?: ReactNode;\n  endIcon?: ReactNode;\n  /** Submits the value with a form. */\n  name?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\ntype Drag = { pointer: number; grab: number; samples: { t: number; p: number }[]; detent: number | null; edge: number; type: string };\n\nconst { spring, duration, blur } = motionTokens;\nconst enterEase = [...motionTokens.ease.enter] as [number, number, number, number];\nconst exitEase = [...motionTokens.ease.standard] as [number, number, number, number];\n/** Motion drops velocity on time-defined springs, so anything a gesture hands off to runs the same spring written as stiffness and damping. */\nconst physical = ({ visualDuration, bounce }: { visualDuration: number; bounce: number }, restDelta = .002) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: \"spring\" as const, stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 10 }; };\nconst thumbSpring = physical(spring.snappy);\n/** The stretch lets go with more life than anything else here: it is the one thing the user threw. */\nconst snapBack = physical({ visualDuration: .5, bounce: .42 }, .05);\n/** Pixels the track may stretch past a limit, the pixel reach of a detent, and the speed a key press at a limit hits the wall with. */\nconst STRETCH = 26, DETENT_PX = 7, BUMP_PX = 260;\n/** iOS style resistance: each pixel past the limit gives less, and never more than `limit`. */\nconst rubber = (distance: number, limit = STRETCH) => Math.sign(distance) * (1 - 1 / (Math.abs(distance) * .55 / limit + 1)) * limit;\nconst clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));\nconst decimalsOf = (value: number) => (String(value).split(\".\")[1] ?? \"\").length;\n\nconst bubbleIn = { opacity: 0, scale: .7, y: 6, filter: `blur(${blur.subtle}px)` };\nconst bubbleRest = { opacity: 1, scale: 1, y: 0, filter: \"blur(0px)\", transitionEnd: { filter: \"none\" } };\nconst bubbleOut = { opacity: 0, scale: .85, y: 4, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: exitEase } };\nconst bubbleEnter = { ...spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.fast, ease: enterEase } };\n\n/**\n * A single value slider made of something that gives. The thumb swells under the finger, the track stretches like rubber past either\n * end and springs back with the release velocity, a value tag hangs off the thumb and sways with its speed, and marks act as detents\n * the thumb clicks into. Arrows, Shift+arrows, PageUp, PageDown, Home, and End work as in a native range input.\n */\nexport function ElasticSlider({ label, value, defaultValue, onValueChange, onValueCommit, min = 0, max = 100, step: stepProp = 1, largeStep, marks, format, showValue = true, startIcon, endIcon, name, disabled, className }: ElasticSliderProps) {\n  const reduced = useReducedMotion() ?? false;\n  const labelId = useId();\n  const step = stepProp > 0 ? stepProp : 1;\n  const span = max - min || 1;\n  const decimals = Math.max(decimalsOf(step), decimalsOf(min));\n  const [internal, setInternal] = useState(() => clamp(defaultValue ?? min, min, max));\n  const current = value === undefined ? internal : clamp(value, min, max);\n  const formatValue = format ?? ((input: number) => input.toLocaleString(\"en-US\", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }));\n  const pct = (input: number) => ((input - min) / span) * 100;\n  const valueAt = (percent: number) => min + (percent / 100) * span;\n  const snap = (input: number) => clamp(Number((min + Math.round((input - min) / step) * step).toFixed(decimals)), min, max);\n  const markList = (marks ?? []).map(mark => typeof mark === \"number\" ? { value: mark } as ElasticSliderMark : mark).filter(mark => mark.value >= min && mark.value <= max);\n\n  const trackRef = useRef<HTMLDivElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const tickRefs = useRef(new Map<number, HTMLSpanElement>());\n  const latest = useRef(current);\n  const goal = useRef(pct(current));\n  const drag = useRef<Drag | null>(null);\n  const width = useRef(1);\n  const lingerTimer = useRef<number | undefined>(undefined);\n  const [dragging, setDragging] = useState(false);\n  const [keyed, setKeyed] = useState(false);\n  const [linger, setLinger] = useState(false);\n\n  // The thumb is a committed position plus a catch-up offset, so it can glide into a detent or to a pressed spot while the pointer\n  // keeps moving the position 1:1. `over` is the rubber-band stretch in pixels, signed by side.\n  const pos = useMotionValue(pct(current));\n  const lag = useMotionValue(0);\n  const over = useMotionValue(0);\n  const press = useMotionValue(0);\n  const shown = useTransform(() => pos.get() + lag.get());\n  const thumbX = useTransform(shown, at => `${at - 100}%`);\n  const clipPath = useTransform(shown, at => `inset(0 ${(100 - clamp(at, 0, 100)).toFixed(3)}% 0 0 round 999px)`);\n  // Stretch: the track grows away from its anchored end and thins as it does, like a band pulled tight. A press thickens it a little.\n  const scaleX = useTransform(over, px => 1 + Math.abs(px) / width.current);\n  const originX = useTransform(over, px => px < 0 ? 1 : 0);\n  const scaleY = useTransform(() => (1 + press.get() * .45) * (1 - (Math.abs(over.get()) / STRETCH) * .32));\n  const startShift = useTransform(over, px => Math.min(0, px));\n  const endShift = useTransform(over, px => Math.max(0, px));\n  // The tag trails the thumb on a soft spring, so it lags more the faster the thumb moves, and swings on its tail with that speed.\n  const trailed = useSpring(shown, { stiffness: 420, damping: 30, mass: .7 });\n  const bubbleAt = reduced ? shown : trailed;\n  const bubbleX = useTransform(bubbleAt, at => `${at - 100}%`);\n  const speed = useVelocity(trailed);\n  const rotate = useTransform(speed, velocity => reduced ? 0 : clamp((-velocity / 100) * width.current * .009, -11, 11));\n\n  const emit = (next: number) => {\n    if (latest.current === next) return;\n    latest.current = next;\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n\n  // Values from outside a drag (keys, props) spring the thumb to their place from wherever it is on screen.\n  useLayoutEffect(() => {\n    latest.current = current;\n    const target = pct(current);\n    if (drag.current || goal.current === target) return;\n    goal.current = target;\n    const from = pos.get() + lag.get();\n    lag.jump(0);\n    pos.jump(from);\n    if (reduced) pos.jump(target); else animate(pos, target, thumbSpring);\n  });\n  useEffect(() => {\n    const node = trackRef.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(([entry]) => { if (entry) width.current = entry.contentRect.width || 1; });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n  useEffect(() => () => window.clearTimeout(lingerTimer.current), []);\n\n  /** A detent answers with a small pop of its tick and, where the device supports it, a short tap of vibration. */\n  function pulse(mark: number, haptic: boolean) {\n    const node = tickRefs.current.get(mark);\n    if (node && !reduced) animate(node, { scale: [1, 2.1, 1] }, { duration: .34, ease: enterEase });\n    if (haptic && typeof navigator !== \"undefined\" && \"vibrate\" in navigator) navigator.vibrate?.(6);\n  }\n  /** Moves the thumb to a new resting spot through a spring on the catch-up offset, so the jump reads as travel. */\n  function glide(placed: number) {\n    const from = pos.get() + lag.get();\n    pos.set(placed);\n    if (reduced) { lag.jump(0); return; }\n    lag.jump(from - placed);\n    animate(lag, 0, thumbSpring);\n  }\n  const detentAt = (percent: number) => {\n    let best: number | null = null, gap = DETENT_PX + 1;\n    for (const mark of markList) { const distance = Math.abs(((percent - pct(mark.value)) / 100) * width.current); if (distance <= DETENT_PX && distance < gap) { best = mark.value; gap = distance; } }\n    return best;\n  };\n\n  function follow(state: Drag, at: number, time: number, first = false) {\n    const raw = at - state.grab;\n    const edge = clamp(raw, 0, 100);\n    over.set(reduced ? 0 : rubber(((raw - edge) / 100) * width.current));\n    const detent = detentAt(edge);\n    const placed = detent === null ? edge : pct(detent);\n    if (first || detent !== state.detent) {\n      if (detent !== null && !first) pulse(detent, state.type === \"touch\");\n      glide(placed);\n    } else pos.set(placed);\n    state.detent = detent;\n    state.edge = edge;\n    state.samples.push({ t: time, p: placed });\n    if (state.samples.length > 6) state.samples.shift();\n    emit(detent ?? snap(valueAt(edge)));\n  }\n\n  function onPointerDown(event: PointerEvent<HTMLDivElement>) {\n    if (disabled || event.button !== 0 || !event.isPrimary) return;\n    const rect = trackRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    width.current = rect.width || 1;\n    const at = ((event.clientX - rect.left) / width.current) * 100;\n    const grabbed = !!(event.target as HTMLElement).closest(\"[data-thumb]\");\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const from = pos.get() + lag.get();\n    lag.jump(0);\n    pos.jump(from);\n    const state: Drag = { pointer: event.pointerId, grab: grabbed ? at - from : 0, samples: [], detent: null, edge: from, type: event.pointerType };\n    drag.current = state;\n    setDragging(true);\n    window.clearTimeout(lingerTimer.current);\n    setLinger(false);\n    thumbRef.current?.focus({ preventScroll: true });\n    if (reduced) press.jump(1); else animate(press, 1, spring.snappy);\n    // A grabbed thumb stays put under the finger; a pressed track pulls the thumb over on a spring.\n    follow(state, grabbed ? from + state.grab : at, event.timeStamp, true);\n  }\n  function onPointerMove(event: PointerEvent<HTMLDivElement>) {\n    const state = drag.current;\n    if (!state || state.pointer !== event.pointerId) return;\n    const rect = trackRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    follow(state, ((event.clientX - rect.left) / (rect.width || 1)) * 100, event.timeStamp);\n  }\n  function onPointerEnd(event: PointerEvent<HTMLDivElement>) {\n    const state = drag.current;\n    if (!state || state.pointer !== event.pointerId) return;\n    drag.current = null;\n    const samples = state.samples, first = samples[0], last = samples[samples.length - 1];\n    const elapsed = last && first ? (last.t - first.t) / 1000 : 0;\n    const velocity = (elapsed > .008 && event.timeStamp - last!.t < 60 ? (last!.p - first!.p) / elapsed : 0) + lag.getVelocity();\n    const next = state.detent ?? snap(valueAt(state.edge));\n    const from = pos.get() + lag.get(), target = pct(next);\n    lag.jump(0);\n    pos.jump(from);\n    goal.current = target;\n    if (reduced) { pos.jump(target); over.jump(0); press.jump(0); }\n    else {\n      animate(pos, target, { ...thumbSpring, velocity });\n      animate(over, 0, { ...snapBack, velocity: over.getVelocity() });\n      animate(press, 0, spring.snappy);\n    }\n    emit(next);\n    setDragging(false);\n    setLinger(true);\n    lingerTimer.current = window.setTimeout(() => setLinger(false), 700);\n    onValueCommit?.(next);\n  }\n\n  function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n    if (disabled) return;\n    const now = latest.current;\n    const large = largeStep ?? Math.max(step, snap(min + span / 10) - min);\n    const moves: Record<string, number> = { ArrowRight: step, ArrowUp: step, ArrowLeft: -step, ArrowDown: -step, PageUp: large, PageDown: -large };\n    let wanted: number;\n    if (event.key === \"Home\") wanted = min;\n    else if (event.key === \"End\") wanted = max;\n    else if (event.key in moves) wanted = now + (event.shiftKey && event.key.startsWith(\"Arrow\") ? Math.sign(moves[event.key]!) * large : moves[event.key]!);\n    else return;\n    event.preventDefault();\n    setKeyed(true);\n    const next = snap(wanted);\n    // At a limit the track still stretches toward the press and snaps back, so the key visibly lands on something.\n    if (next === now) {\n      const toward = Math.sign(wanted - now);\n      if (toward && !reduced) animate(over, 0, { ...snapBack, velocity: toward * BUMP_PX });\n      return;\n    }\n    emit(next);\n    if (markList.some(mark => mark.value === next)) pulse(next, false);\n    onValueCommit?.(next);\n  }\n\n  const text = formatValue(current);\n  const labelled = markList.filter(mark => mark.label);\n  const bubble = dragging || keyed || linger;\n\n  return <div className={[styles.root, className].filter(Boolean).join(\" \")} data-disabled={disabled || undefined} data-dragging={dragging || undefined} data-tag={bubble || undefined} data-marks={labelled.length > 0 || undefined}>\n    <div className={styles.header}>\n      <span id={labelId} className={styles.label}>{label}</span>\n      {showValue && <span className={styles.readout} aria-hidden=\"true\">{text}</span>}\n    </div>\n    <div className={styles.body}>\n      {startIcon && <motion.span className={styles.start} style={{ x: startShift }} aria-hidden=\"true\">{startIcon}</motion.span>}\n      <div className={styles.control} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerEnd} onPointerCancel={onPointerEnd} onLostPointerCapture={onPointerEnd} onMouseDown={event => event.preventDefault()}>\n        <div ref={trackRef} className={styles.rail}>\n          <motion.div className={styles.track} style={{ scaleX, scaleY, originX }}>\n            <motion.div className={styles.fill} style={{ clipPath }} />\n          </motion.div>\n          {markList.filter(mark => mark.value > min && mark.value < max).map(mark => <span key={mark.value} ref={node => { if (node) tickRefs.current.set(mark.value, node); else tickRefs.current.delete(mark.value); }} className={styles.tick} data-on={mark.value <= current || undefined} style={{ left: `${pct(mark.value)}%` }} />)}\n        </div>\n        <div className={styles.thumbs}>\n          <motion.div className={styles.layer} style={{ x: bubbleX }}>\n            <motion.span className={styles.bubbleAnchor} style={{ x: over, \"--at\": bubbleAt } as MotionStyle}>\n              <AnimatePresence>{bubble && <motion.span key=\"bubble\" className={styles.bubble} aria-hidden=\"true\" style={{ rotate }} initial={reduced ? { opacity: 0 } : bubbleIn} animate={bubbleRest} exit={reduced ? { opacity: 0, transition: { duration: duration.instant } } : bubbleOut} transition={reduced ? { duration: .15 } : bubbleEnter}>{text}</motion.span>}</AnimatePresence>\n            </motion.span>\n          </motion.div>\n          <motion.div className={styles.layer} style={{ x: thumbX }}>\n            <motion.div className={styles.shift} style={{ x: over }}>\n              <motion.div ref={thumbRef} role=\"slider\" data-thumb=\"\" className={styles.thumb} tabIndex={disabled ? -1 : 0} aria-labelledby={labelId} aria-valuemin={min} aria-valuemax={max} aria-valuenow={current} aria-valuetext={text} aria-orientation=\"horizontal\" aria-disabled={disabled || undefined}\n                initial={false} animate={{ scale: dragging ? 1.28 : 1 }} transition={reduced ? { duration: 0 } : spring.snappy} onKeyDown={onKeyDown} onBlur={() => setKeyed(false)} />\n            </motion.div>\n          </motion.div>\n        </div>\n      </div>\n      {endIcon && <motion.span className={styles.end} style={{ x: endShift }} aria-hidden=\"true\">{endIcon}</motion.span>}\n      {labelled.length > 0 && <div className={styles.marks} aria-hidden=\"true\">\n        {labelled.map(mark => <span key={mark.value} className={styles.markLabel} data-on={mark.value === current || undefined} data-edge={mark.value === min ? \"start\" : mark.value === max ? \"end\" : undefined} style={{ left: `${pct(mark.value)}%` }}\n          onClick={() => { if (disabled || mark.value === latest.current) return; emit(mark.value); pulse(mark.value, false); onValueCommit?.(mark.value); }}>{mark.label}</span>)}\n      </div>}\n    </div>\n    {name && <input type=\"hidden\" name={name} value={current} disabled={disabled} />}\n  </div>;\n}\n\nexport default ElasticSlider;\n"
    },
    {
      "path": "registry/components/elastic-slider/elastic-slider.module.css",
      "type": "registry:component",
      "target": "@components/xzero/elastic-slider/elastic-slider.module.css",
      "content": "/* Set --elastic-thumb and --elastic-track to resize. The rail is inset by half a thumb, so a thumb at either limit stays inside the box. */\n.root { --elastic-thumb: 24px; --elastic-track: 8px; display: grid; width: 100%; min-width: 0; gap: var(--space-2); color: var(--foreground); font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); user-select: none; -webkit-user-select: none; }\n.header { display: flex; min-width: 0; align-items: baseline; justify-content: space-between; gap: var(--space-4); }\n.label { min-width: 0; font-weight: 500; }\n.readout { flex: none; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; transition: opacity var(--duration-fast) var(--ease-standard); }\n/* While the tag rides the thumb it carries the value, so the readout steps back instead of competing with it. */\n.root[data-tag] .readout { opacity: 0; }\n.body { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; }\n.start, .end { display: flex; color: var(--text-muted); will-change: transform; }\n.start { grid-area: 1 / 1; margin-right: var(--space-3); }\n.end { grid-area: 1 / 3; margin-left: var(--space-3); }\n/* The whole row takes the press; vertical pans still scroll the page on touch. */\n.control { position: relative; grid-area: 1 / 2; height: var(--control-height-md); cursor: pointer; touch-action: pan-y; -webkit-tap-highlight-color: transparent; }\n.rail { position: absolute; top: 50%; right: calc(var(--elastic-thumb) / 2); left: calc(var(--elastic-thumb) / 2); height: var(--elastic-track); margin-top: calc(var(--elastic-track) / -2); }\n/* Only the band stretches. Ticks, thumb, and tag ride along without distorting. */\n.track { position: absolute; inset: 0; border-radius: var(--radius-pill); background: var(--control-track); will-change: transform; }\n.fill { position: absolute; inset: 0; border-radius: inherit; background: var(--control-fill); }\n.tick { position: absolute; top: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: var(--radius-pill); background: color-mix(in oklab, var(--foreground) 32%, transparent); pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }\n.tick[data-on] { background: color-mix(in oklab, var(--control-glyph) 60%, transparent); }\n.thumbs { position: absolute; inset: 0 calc(var(--elastic-thumb) / 2); container-type: inline-size; pointer-events: none; }\n/* Each layer is as wide as the rail and slides by a percentage of itself, so placing the thumb needs no measuring. */\n.layer { position: absolute; inset: 0; }\n.shift { position: absolute; inset: 0; }\n.thumb { position: absolute; top: 50%; right: 0; box-sizing: border-box; width: var(--elastic-thumb); height: var(--elastic-thumb); translate: 50% -50%; border: 0; border-radius: var(--radius-pill); background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); pointer-events: auto; cursor: grab; outline: none; transition: box-shadow var(--duration-fast) var(--ease-standard); }\n/* A generous invisible hit area for fingertips. */\n.thumb::after { content: \"\"; position: absolute; inset: -12px; border-radius: inherit; }\n.root[data-dragging], .root[data-dragging] .control, .root[data-dragging] .thumb { cursor: grabbing; }\n/* The tag centers on the thumb until it would pass the component's edge, then holds there. It hangs from its bottom edge, so a swing pivots on the thumb. */\n.bubbleAnchor { --edge: calc(var(--elastic-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--elastic-thumb) / 2 + 10px); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; }\n@media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--elastic-thumb) / 2 + 20px); } }\n.bubble { display: inline-flex; min-height: 1.75rem; align-items: center; padding: 0 var(--space-3); border-radius: var(--radius-pill); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; transform-origin: 50% 130%; }\n.marks { position: relative; grid-area: 2 / 2; height: 1.25rem; margin: 0 calc(var(--elastic-thumb) / 2); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }\n.markLabel { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }\n.markLabel[data-edge=\"start\"] { translate: max(-50%, calc(var(--elastic-thumb) / -2)) 0; }\n.markLabel[data-edge=\"end\"] { translate: min(-50%, calc(-100% + var(--elastic-thumb) / 2)) 0; }\n.markLabel[data-on] { color: var(--foreground); }\n@media (hover: hover) and (pointer: fine) { .markLabel:hover { color: var(--foreground); } }\n.root[data-disabled] { opacity: .5; }\n.root[data-disabled] .control, .root[data-disabled] .thumb, .root[data-disabled] .markLabel { cursor: not-allowed; }\n@media (prefers-reduced-motion: reduce) { .thumb, .tick, .markLabel { transition: none; } }\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/elastic-slider",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "inputs",
      "new"
    ],
    "docs": "https://ui.xzero.co.uk/components/elastic-slider",
    "markdown": "https://ui.xzero.co.uk/components/elastic-slider/markdown"
  }
}