{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider-range",
  "title": "Range slider",
  "description": "Styled slider: the Range set. 5 decorative slider variations (DualSlider, MinMaxSlider, StepsRangeSlider, LabelsRangeSlider, FillRangeSlider) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/slider-range.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Slider range family: 5 dual-thumb range sliders. A self-sufficient structure (a\n   native input[type=range] cannot take tokens): a track div + a fill div between the\n   thumbs + two thumb buttons. Pointer dragging (clientX within the track rect, with\n   capture on the track) AND keyboard (arrows/Shift+arrow/Home/End). The thumbs cannot\n   pass each other: the lower thumb clamps to the upper value and the upper thumb to\n   the lower one. Color comes ONLY from tokens. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst trackHeight: Record<StyledSize, string> = {\n  sm: \"h-1\",\n  md: \"h-1.5\",\n  lg: \"h-2.5\",\n  xl: \"h-3.5\",\n}\nconst thumbSize: Record<StyledSize, string> = {\n  sm: \"size-3.5\",\n  md: \"size-4\",\n  lg: \"size-5\",\n  xl: \"size-6\",\n}\nconst thickTrackHeight: Record<StyledSize, string> = {\n  sm: \"h-2.5\",\n  md: \"h-3.5\",\n  lg: \"h-5\",\n  xl: \"h-6\",\n}\nconst thickThumbSize: Record<StyledSize, string> = {\n  sm: \"size-5\",\n  md: \"size-6\",\n  lg: \"size-7\",\n  xl: \"size-9\",\n}\nconst labelText: Record<StyledSize, string> = {\n  sm: \"text-[10px]\",\n  md: \"text-[11px]\",\n  lg: \"text-xs\",\n  xl: \"text-sm\",\n}\n\nconst rootBase =\n  \"relative flex w-56 max-w-full touch-none select-none items-center outline-none\"\n/* Basparmak 24px altinda: gorunmez dokunma alani genisletmesi zorunlu. */\nconst thumbBase =\n  \"absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full outline-none after:absolute after:-inset-1.5 after:content-[''] focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nexport type RangeSliderProps = {\n  className?: string\n  size?: StyledSize\n  min?: number\n  max?: number\n  step?: number\n  value?: [number, number]\n  defaultValue?: [number, number]\n  onValueChange?: (v: [number, number]) => void\n}\n\ntype ThumbIndex = 0 | 1\n\nfunction clamp(v: number, min: number, max: number) {\n  return Math.max(min, Math.min(max, v))\n}\n\n/* Shared range behaviour: controlled/uncontrolled pair of values, pointer dragging, keyboard. Each thumb clamps against the other, so the order can never break. */\nfunction useRangeSlider(props: RangeSliderProps) {\n  const {\n    value,\n    defaultValue = [25, 70],\n    onValueChange,\n    min = 0,\n    max = 100,\n    step = 1,\n  } = props\n  const trackRef = React.useRef<HTMLDivElement>(null)\n  const [internal, setInternal] = React.useState<[number, number]>(() => [\n    clamp(Math.min(defaultValue[0], defaultValue[1]), min, max),\n    clamp(Math.max(defaultValue[0], defaultValue[1]), min, max),\n  ])\n  const [active, setActive] = React.useState<ThumbIndex | null>(null)\n  const current = value ?? internal\n  const lo = clamp(current[0], min, max)\n  const hi = clamp(current[1], min, max)\n\n  const commit = React.useCallback(\n    (index: ThumbIndex, next: number) => {\n      const snapped = clamp(Math.round(next / step) * step, min, max)\n      const pair: [number, number] =\n        index === 0 ? [clamp(snapped, min, hi), hi] : [lo, clamp(snapped, lo, max)]\n      if (value === undefined) setInternal(pair)\n      onValueChange?.(pair)\n    },\n    [hi, lo, max, min, onValueChange, step, value]\n  )\n\n  const fromClientX = React.useCallback(\n    (clientX: number) => {\n      const el = trackRef.current\n      if (!el) return lo\n      const rect = el.getBoundingClientRect()\n      const ratio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0\n      return min + clamp(ratio, 0, 1) * (max - min)\n    },\n    [lo, max, min]\n  )\n\n  const capture = React.useCallback((pointerId: number) => {\n    trackRef.current?.setPointerCapture?.(pointerId)\n  }, [])\n\n  /* On a press on the track: the nearest thumb is captured and jumps there. */\n  const onTrackPointerDown = React.useCallback(\n    (e: React.PointerEvent) => {\n      e.preventDefault()\n      capture(e.pointerId)\n      const raw = fromClientX(e.clientX)\n      const index: ThumbIndex = Math.abs(raw - lo) <= Math.abs(raw - hi) ? 0 : 1\n      setActive(index)\n      commit(index, raw)\n    },\n    [capture, commit, fromClientX, hi, lo]\n  )\n\n  /* When the thumb is pressed: the value does not change, only dragging begins. */\n  const thumbPointerDown = React.useCallback(\n    (index: ThumbIndex) => (e: React.PointerEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      capture(e.pointerId)\n      setActive(index)\n    },\n    [capture]\n  )\n\n  const onPointerMove = React.useCallback(\n    (e: React.PointerEvent) => {\n      if (active === null) return\n      commit(active, fromClientX(e.clientX))\n    },\n    [active, commit, fromClientX]\n  )\n\n  const onPointerUp = React.useCallback((e: React.PointerEvent) => {\n    trackRef.current?.releasePointerCapture?.(e.pointerId)\n    setActive(null)\n  }, [])\n\n  const keyDown = React.useCallback(\n    (index: ThumbIndex) => (e: React.KeyboardEvent) => {\n      const cur = index === 0 ? lo : hi\n      const delta = e.shiftKey ? step * 10 : step\n      let next = cur\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") next = cur - delta\n      else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") next = cur + delta\n      else if (e.key === \"Home\") next = min\n      else if (e.key === \"End\") next = max\n      else return\n      e.preventDefault()\n      commit(index, next)\n    },\n    [commit, hi, lo, max, min, step]\n  )\n\n  const span = max > min ? max - min : 1\n  const pctOf = React.useCallback(\n    (v: number) => ((v - min) / span) * 100,\n    [min, span]\n  )\n\n  return {\n    trackRef,\n    lo,\n    hi,\n    min,\n    max,\n    step,\n    active,\n    pctLo: pctOf(lo),\n    pctHi: pctOf(hi),\n    pctOf,\n    onTrackPointerDown,\n    thumbPointerDown,\n    onPointerMove,\n    onPointerUp,\n    keyDown,\n  }\n}\n\ntype RangeState = ReturnType<typeof useRangeSlider>\n\n/* Ortak thumb: rol, aria, klavye ve gorsel sinif tek yerde. */\nfunction RangeThumb({\n  s,\n  index,\n  size,\n  reduce,\n  className,\n  sizeClass,\n}: {\n  s: RangeState\n  index: ThumbIndex\n  size: StyledSize\n  reduce: boolean | null\n  className?: string\n  sizeClass?: string\n}) {\n  const v = index === 0 ? s.lo : s.hi\n  return (\n    <motion.button\n      type=\"button\"\n      data-slot=\"styled-slider-thumb\"\n      role=\"slider\"\n      aria-label={index === 0 ? \"Minimum\" : \"Maximum\"}\n      aria-valuenow={Math.round(v)}\n      aria-valuemin={index === 0 ? s.min : Math.round(s.lo)}\n      aria-valuemax={index === 0 ? Math.round(s.hi) : s.max}\n      aria-orientation=\"horizontal\"\n      tabIndex={0}\n      onPointerDown={s.thumbPointerDown(index)}\n      onKeyDown={s.keyDown(index)}\n      className={cn(thumbBase, sizeClass ?? thumbSize[size], className)}\n      style={{ left: `${s.pctOf(v)}%` }}\n      animate={{ scale: !reduce && s.active === index ? 1.18 : 1 }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : { type: \"spring\" as const, stiffness: 500, damping: 30, mass: 0.6 }\n      }\n    />\n  )\n}\n\n/* Dual: sade cift basparmak, arada primary dolum. */\nexport function DualSlider({ className, size = \"md\", ...rest }: RangeSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useRangeSlider(rest)\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onTrackPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 rounded-full bg-primary\"\n          style={{ left: `${s.pctLo}%`, width: `${s.pctHi - s.pctLo}%` }}\n        />\n      </div>\n      <RangeThumb\n        s={s}\n        index={0}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n      <RangeThumb\n        s={s}\n        index={1}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n    </div>\n  )\n}\n\n/* MinMax: live lower and upper value badges above the thumbs. */\nexport function MinMaxSlider({ className, size = \"md\", ...rest }: RangeSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useRangeSlider(rest)\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, \"mt-5\", className)}>\n      <div\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute -top-5 left-0 w-full font-medium text-muted-foreground\",\n          labelText[size]\n        )}\n      >\n        <span\n          className=\"absolute -translate-x-1/2 tabular-nums\"\n          style={{ left: `${s.pctLo}%` }}\n        >\n          {Math.round(s.lo)}\n        </span>\n        <span\n          className=\"absolute -translate-x-1/2 tabular-nums\"\n          style={{ left: `${s.pctHi}%` }}\n        >\n          {Math.round(s.hi)}\n        </span>\n      </div>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onTrackPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 rounded-full bg-info\"\n          style={{ left: `${s.pctLo}%`, width: `${s.pctHi - s.pctLo}%` }}\n        />\n      </div>\n      <RangeThumb\n        s={s}\n        index={0}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-info shadow-sm\"\n      />\n      <RangeThumb\n        s={s}\n        index={1}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-info shadow-sm\"\n      />\n    </div>\n  )\n}\n\n/* StepsRange: a range clamped to a step of 10, with step lines along the track. */\nexport function StepsRangeSlider({\n  className,\n  size = \"md\",\n  step = 10,\n  ...rest\n}: RangeSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useRangeSlider({ step, ...rest })\n  const ticks = 11\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onTrackPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 rounded-full bg-primary\"\n          style={{ left: `${s.pctLo}%`, width: `${s.pctHi - s.pctLo}%` }}\n        />\n        <div className=\"pointer-events-none absolute inset-0\" aria-hidden=\"true\">\n          {Array.from({ length: ticks }).map((_, i) => (\n            <span\n              key={i}\n              className=\"absolute top-1/2 h-1.5 w-px -translate-x-1/2 -translate-y-1/2 rounded-full bg-border\"\n              style={{ left: `${(i / (ticks - 1)) * 100}%` }}\n            />\n          ))}\n        </div>\n      </div>\n      <RangeThumb\n        s={s}\n        index={0}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n      <RangeThumb\n        s={s}\n        index={1}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n    </div>\n  )\n}\n\n/* LabelsRange: izin altinda min / orta / max olcek etiketleri. */\nexport function LabelsRangeSlider({ className, size = \"md\", ...rest }: RangeSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useRangeSlider(rest)\n  const mid = Math.round((s.min + s.max) / 2)\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, \"mb-5\", className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onTrackPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 rounded-full bg-primary\"\n          style={{ left: `${s.pctLo}%`, width: `${s.pctHi - s.pctLo}%` }}\n        />\n      </div>\n      <RangeThumb\n        s={s}\n        index={0}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n      <RangeThumb\n        s={s}\n        index={1}\n        size={size}\n        reduce={reduce}\n        className=\"border-2 border-background bg-primary shadow-sm\"\n      />\n      <div\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute -bottom-5 left-0 flex w-full justify-between tabular-nums text-muted-foreground\",\n          labelText[size]\n        )}\n      >\n        <span>{s.min}</span>\n        <span>{mid}</span>\n        <span>{s.max}</span>\n      </div>\n    </div>\n  )\n}\n\n/* FillRange: kalin iz, aradaki dolum cok tonlu token gradyani. */\nexport function FillRangeSlider({ className, size = \"md\", ...rest }: RangeSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useRangeSlider(rest)\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, \"min-h-9\", className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onTrackPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer overflow-hidden rounded-full bg-secondary\",\n          thickTrackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 rounded-full [background-image:linear-gradient(90deg,var(--info),var(--brand),var(--success))]\"\n          style={{ left: `${s.pctLo}%`, width: `${s.pctHi - s.pctLo}%` }}\n        />\n      </div>\n      <RangeThumb\n        s={s}\n        index={0}\n        size={size}\n        reduce={reduce}\n        sizeClass={thickThumbSize[size]}\n        className=\"border-[3px] border-background bg-primary shadow-md\"\n      />\n      <RangeThumb\n        s={s}\n        index={1}\n        size={size}\n        reduce={reduce}\n        sizeClass={thickThumbSize[size]}\n        className=\"border-[3px] border-background bg-primary shadow-md\"\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/slider-range.tsx"
    }
  ],
  "categories": [
    "styled",
    "slider"
  ],
  "type": "registry:component"
}