{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "Range sliders with value bubbles, ticks and multiple thumbs.",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/slider.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"radix-ui\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype SliderProps = React.ComponentProps<typeof SliderPrimitive.Root> & {\n  /** A value tag above each thumb while dragging, hovering or focused. It swings with drag speed. */\n  showValue?: boolean;\n  /** Formats the tag and tick labels, e.g. `(v) => \\`${v}%\\``. */\n  format?: (value: number) => string;\n  /** Values to label under the track; they bump like detents as the thumb passes them. */\n  ticks?: number[];\n};\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\n/**\n * A range slider with physical feedback (GSAP): the thumb swells on grab, the value tag springs in and swings\n * with the speed of the drag like a hanging label, ticks bump as you pass them, and the thumb squashes at the ends.\n */\nfunction Slider({ className, defaultValue, value, min = 0, max = 100, onValueChange, showValue = false, format = String, ticks, orientation, ...props }: SliderProps) {\n  const [internal, setInternal] = React.useState<number[]>(() => value ?? defaultValue ?? [min]);\n  const values = value ?? internal;\n  const [active, setActive] = React.useState(false);\n  const [focus, setFocus] = React.useState(false);\n  const root = React.useRef<HTMLDivElement>(null);\n  const last = React.useRef({ values, t: 0 });\n  const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const range = max - min || 1;\n  const vertical = orientation === \"vertical\";\n\n  const all = (slot: string) => Array.from(root.current?.querySelectorAll<HTMLElement>(`[data-slot=\"${slot}\"]`) ?? []);\n\n  React.useEffect(() => {\n    if (!active) return;\n    const up = () => setActive(false);\n    window.addEventListener(\"pointerup\", up);\n    window.addEventListener(\"pointercancel\", up);\n    return () => {\n      window.removeEventListener(\"pointerup\", up);\n      window.removeEventListener(\"pointercancel\", up);\n    };\n  }, [active]);\n\n  React.useEffect(() => {\n    const scale = root.current ? motionScale(root.current) : 0;\n    gsap.to(all(\"slider-thumb\"), { scale: active ? 1 + 0.2 * Math.min(scale, 1.5) : 1, duration: scale ? 0.2 : 0, ease: \"cue\" });\n  }, [active]);\n\n  // The tag is for precise adjustment: it appears while dragging or using the keyboard, not on casual hover.\n  const show = showValue && (active || focus);\n  React.useEffect(() => {\n    const tags = all(\"slider-tag\");\n    if (!tags.length) return;\n    const scale = root.current ? motionScale(root.current) : 0;\n    gsap.to(tags, show ? { opacity: 1, scale: 1, y: 0, duration: scale ? 0.32 : 0, ease: \"back.out(2.2)\", overwrite: \"auto\" } : { opacity: 0, scale: 0.85, y: 6, duration: scale ? 0.15 : 0, ease: \"cue\", overwrite: \"auto\" });\n  }, [show]);\n\n  const react = (next: number[]) => {\n    const el = root.current;\n    const scale = el ? motionScale(el) : 0;\n    const now = performance.now();\n    const prev = last.current;\n    last.current = { values: next, t: now };\n    if (!el || !scale) return;\n    const dt = Math.max(16, now - prev.t);\n    next.forEach((v, i) => {\n      const p = prev.values[i] ?? v;\n      // Swing the tag against the direction of travel, proportional to speed.\n      const tag = el.querySelector<HTMLElement>(`[data-slot=\"slider-tag\"][data-index=\"${i}\"]`);\n      if (tag && !vertical) gsap.to(tag, { rotation: clamp((-(v - p) / range) * (1000 / dt) * 22 * scale, -18, 18), duration: 0.12, ease: \"cue\", overwrite: \"auto\" });\n      // Detents: bump each tick the thumb crossed.\n      ticks?.forEach((t) => {\n        if ((p < t && v >= t) || (p > t && v <= t)) {\n          const tick = el.querySelector<HTMLElement>(`[data-slot=\"slider-tick\"][data-value=\"${t}\"]`);\n          if (tick) gsap.fromTo(tick, { y: -3 * scale, scale: 1.15 }, { y: 0, scale: 1, duration: 0.35, ease: \"cue-snap\" });\n        }\n      });\n      // Ends: a small squash when the thumb hits min or max.\n      if ((v === min || v === max) && p !== v) {\n        const thumb = el.querySelectorAll<HTMLElement>('[data-slot=\"slider-thumb\"]')[i];\n        if (thumb) gsap.fromTo(thumb, { scaleX: vertical ? 1.15 : 0.8, scaleY: vertical ? 0.8 : 1.15 }, { scaleX: 1, scaleY: 1, duration: 0.4, ease: \"elastic.out(1, 0.5)\" });\n      }\n    });\n    if (settle.current) clearTimeout(settle.current);\n    settle.current = setTimeout(() => gsap.to(all(\"slider-tag\"), { rotation: 0, duration: 0.7, ease: \"elastic.out(1, 0.35)\" }), 80);\n  };\n\n  const nearestTick = ticks?.reduce((a, b) => (Math.abs(b - (values[0] ?? 0)) < Math.abs(a - (values[0] ?? 0)) ? b : a), ticks[0]!);\n\n  return (\n    <div data-slot=\"slider-wrapper\" className={cn(\"grid w-full gap-2 data-[orientation=vertical]:h-full\", className)} data-orientation={orientation}>\n      <SliderPrimitive.Root\n        ref={root}\n        data-slot=\"slider\"\n        data-active={active || undefined}\n        value={value}\n        defaultValue={defaultValue}\n        min={min}\n        max={max}\n        orientation={orientation}\n        onValueChange={(v) => {\n          setInternal(v);\n          react(v);\n          onValueChange?.(v);\n        }}\n        onPointerDown={() => setActive(true)}\n        onFocus={() => setFocus(true)}\n        onBlur={() => setFocus(false)}\n        className=\"group/slider relative flex w-full touch-none items-center py-1.5 select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[orientation=vertical]:px-1.5 data-[orientation=vertical]:py-0\"\n        {...props}\n      >\n        <SliderPrimitive.Track\n          data-slot=\"slider-track\"\n          className=\"relative grow overflow-hidden rounded-full bg-muted shadow-well transition-[height,width] duration-200 ease-cue data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full group-data-[active]/slider:data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 group-data-[active]/slider:data-[orientation=vertical]:w-2.5\"\n        >\n          <SliderPrimitive.Range data-slot=\"slider-range\" className=\"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\" />\n        </SliderPrimitive.Track>\n        {values.map((v, i) => (\n          <SliderPrimitive.Thumb\n            key={i}\n            aria-label={props[\"aria-label\"] ? (values.length > 1 ? `${props[\"aria-label\"]} ${i + 1}` : props[\"aria-label\"]) : undefined}\n            data-slot=\"slider-thumb\"\n            className=\"relative block size-4.5 shrink-0 cursor-grab rounded-full border border-primary/80 bg-background shadow-control outline-none transition-[box-shadow] duration-150 ease-cue hover:ring-4 hover:ring-ring/20 focus-visible:ring-4 focus-visible:ring-ring/40 active:cursor-grabbing disabled:pointer-events-none\"\n          >\n            {showValue && (\n              <span aria-hidden=\"true\" className=\"pointer-events-none absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2\">\n                <span\n                  data-slot=\"slider-tag\"\n                  data-index={i}\n                  className=\"relative block origin-bottom rounded-full bg-foreground px-2.5 py-1.5 font-mono text-xs leading-none font-medium whitespace-nowrap text-background tabular-nums opacity-0 shadow-overlay after:absolute after:top-[calc(100%-3px)] after:left-1/2 after:size-2 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-foreground\"\n                >\n                  {format(v)}\n                </span>\n              </span>\n            )}\n          </SliderPrimitive.Thumb>\n        ))}\n      </SliderPrimitive.Root>\n      {ticks && !vertical && (\n        <div data-slot=\"slider-ticks\" aria-hidden=\"true\" className=\"relative h-4 font-mono text-[0.6875rem] text-muted-foreground tabular-nums\">\n          {ticks.map((t) => {\n            const pct = (t - min) / range;\n            return (\n              <span key={t} className=\"absolute -translate-x-1/2\" style={{ left: `calc(${pct * 100}% + ${9 - pct * 18}px)` }}>\n                <span\n                  data-slot=\"slider-tick\"\n                  data-value={t}\n                  className={cn(\"block transition-colors duration-150\", t === nearestTick && values.length === 1 && \"font-medium text-foreground\")}\n                >\n                  {format(t)}\n                </span>\n              </span>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport { Slider };\nexport type { SliderProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/slider",
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}