{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-range-slider",
  "title": "Elastic Range Slider",
  "description": "A single-track, dual-thumb range slider in the same visual language as Elastic Slider.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/base/ui/elastic-range-slider.tsx",
      "content": "\"use client\"\n\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\"\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\"\n\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { cn } from \"@/lib/utils\"\n\n// Drag detection & rubber band (only at the absolute min/max edges — the\n// two thumbs clamping against each other is a hard boundary, not elastic).\nconst CLICK_THRESHOLD = 3\nconst DEAD_ZONE = 32\nconst MAX_CURSOR_RANGE = 200\nconst MAX_STRETCH = 8\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.max(lo, Math.min(hi, v))\n}\n\nfunction decimalsForStep(step: number): number {\n  const s = step.toString()\n  const dot = s.indexOf(\".\")\n  return dot === -1 ? 0 : s.length - dot - 1\n}\n\nfunction roundValue(val: number, step: number): number {\n  const raw = Math.round(val / step) * step\n  return parseFloat(raw.toFixed(decimalsForStep(step)))\n}\n\ntype Thumb = \"min\" | \"max\"\n\nexport type ElasticRangeSliderProps = {\n  /** Label shown at the start of the track. */\n  label: string\n\n  /** Controlled [min, max] value. Use together with `onValueChange` */\n  value?: [number, number]\n  /** Initial [min, max] value for uncontrolled mode. Falls back to `[min, max]` */\n  defaultValue?: [number, number]\n  /** Called with the new [min, max] on drag, click, or key press. */\n  onValueChange?: (value: [number, number]) => void\n\n  /**\n   * Minimum value.\n   * @defaultValue 0 */\n  min?: number\n  /**\n   * Maximum value.\n   * @defaultValue 100 */\n  max?: number\n  /**\n   * Smallest increment.\n   * @defaultValue 1 */\n  step?: number\n  /** Format each displayed value. Defaults to `value.toFixed(...)` based on `step` */\n  formatValue?: (value: number) => string\n\n  className?: string\n  /** Accessible name for the lower thumb. @defaultValue \"Minimum\" */\n  minThumbLabel?: string\n  /** Accessible name for the upper thumb. @defaultValue \"Maximum\" */\n  maxThumbLabel?: string\n}\n\nexport function ElasticRangeSlider({\n  label,\n\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n\n  min: absoluteMin = 0,\n  max: absoluteMax = 100,\n  step = 1,\n  formatValue,\n\n  className,\n  minThumbLabel = \"Minimum\",\n  maxThumbLabel = \"Maximum\",\n}: ElasticRangeSliderProps) {\n  const [range = [absoluteMin, absoluteMax], setRange] = useControllableState({\n    prop: valueProp,\n    defaultProp: defaultValue ?? [absoluteMin, absoluteMax],\n    onChange: onValueChange,\n  })\n  const [min, max] = range\n\n  const shouldReduceMotion = useReducedMotion()\n\n  const wrapperRef = useRef<HTMLDivElement>(null)\n  const trackRef = useRef<HTMLDivElement>(null)\n\n  const [isHovered, setIsHovered] = useState(false)\n  const [isDragging, setIsDragging] = useState(false)\n  const [keyboardFocusThumb, setKeyboardFocusThumb] = useState<Thumb | null>(\n    null\n  )\n  const isActive = isHovered || isDragging\n\n  // Pointer session state — mutable, does not trigger re-renders.\n  const draggingThumb = useRef<Thumb | null>(null)\n  const pointerDownPos = useRef<{ x: number; y: number } | null>(null)\n  const isClickRef = useRef(true)\n  const wrapperRectRef = useRef<DOMRect | null>(null)\n  const scaleRef = useRef(1)\n  const minAnimRef = useRef<ReturnType<typeof animate> | null>(null)\n  const maxAnimRef = useRef<ReturnType<typeof animate> | null>(null)\n\n  const percentFromValue = useCallback(\n    (v: number) => ((v - absoluteMin) / (absoluteMax - absoluteMin)) * 100,\n    [absoluteMin, absoluteMax]\n  )\n\n  const displayMin = formatValue\n    ? formatValue(min)\n    : min.toFixed(decimalsForStep(step))\n  const displayMax = formatValue\n    ? formatValue(max)\n    : max.toFixed(decimalsForStep(step))\n\n  // Each thumb gets its own motion value so drags stay perfectly in sync\n  // frame-to-frame, instead of both re-deriving from combined React state.\n  const minPercent = useMotionValue(percentFromValue(min))\n  const maxPercent = useMotionValue(percentFromValue(max))\n  const minLeft = useTransform(\n    minPercent,\n    (pct) => `max(4px, min(calc(100% - 8px), calc(${pct}% - 4px)))`\n  )\n  const maxLeft = useTransform(\n    maxPercent,\n    (pct) => `max(4px, min(calc(100% - 8px), calc(${pct}% - 4px)))`\n  )\n  const fillLeft = useTransform(minPercent, (pct) => `${pct}%`)\n  const fillRight = useTransform(maxPercent, (pct) => `${100 - pct}%`)\n\n  // Rubber band at the absolute edges only.\n  const rubberStretch = useMotionValue(0)\n  const rubberWidth = useTransform(\n    rubberStretch,\n    (s) => `calc(100% + ${Math.abs(s)}px)`\n  )\n  // Track is anchored at the reading start (inset-inline-start: 0), so the\n  // shift is direction-aware: LTR pins the track's physical left (overshoot\n  // left shifts: x = s, s < 0); RTL pins the physical right (overshoot right\n  // shifts: x = s, s > 0). Only the overshot edge stretches.\n  const dirRef = useRef<\"ltr\" | \"rtl\">(\"ltr\")\n\n  useLayoutEffect(() => {\n    function updateDir() {\n      if (!wrapperRef.current) return\n      dirRef.current =\n        getComputedStyle(wrapperRef.current).direction === \"rtl\" ? \"rtl\" : \"ltr\"\n    }\n\n    updateDir()\n\n    const observer = new MutationObserver(updateDir)\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"dir\"],\n      subtree: true,\n    })\n\n    return () => observer.disconnect()\n  }, [])\n\n  const rubberX = useTransform(rubberStretch, (s) => {\n    if (s === 0) return 0\n    const overshootPastAnchor = dirRef.current === \"rtl\" ? s > 0 : s < 0\n    return overshootPastAnchor ? s : 0\n  })\n\n  // Sync from props when not actively dragging that thumb.\n  useEffect(() => {\n    if (draggingThumb.current !== \"min\" && !minAnimRef.current) {\n      minPercent.jump(percentFromValue(min))\n    }\n  }, [min, percentFromValue, minPercent])\n\n  useEffect(() => {\n    if (draggingThumb.current !== \"max\" && !maxAnimRef.current) {\n      maxPercent.jump(percentFromValue(max))\n    }\n  }, [max, percentFromValue, maxPercent])\n\n  const positionToValue = useCallback(\n    (clientX: number) => {\n      const rect = wrapperRectRef.current\n      if (!rect) return absoluteMin\n\n      const sceneX = (clientX - rect.left) / scaleRef.current\n      const nativeWidth = wrapperRef.current?.offsetWidth ?? rect.width\n      const percent = clamp(sceneX / nativeWidth, 0, 1)\n\n      return clamp(\n        absoluteMin + percent * (absoluteMax - absoluteMin),\n        absoluteMin,\n        absoluteMax\n      )\n    },\n    [absoluteMin, absoluteMax]\n  )\n\n  const animateThumbTo = useCallback(\n    (thumb: Thumb, targetPercent: number) => {\n      const motionValue = thumb === \"min\" ? minPercent : maxPercent\n      const animRef = thumb === \"min\" ? minAnimRef : maxAnimRef\n\n      animRef.current?.stop()\n\n      if (shouldReduceMotion) {\n        motionValue.jump(targetPercent)\n        animRef.current = null\n        return\n      }\n\n      animRef.current = animate(motionValue, targetPercent, {\n        type: \"spring\",\n        stiffness: 300,\n        damping: 25,\n        mass: 0.8,\n        onComplete: () => {\n          animRef.current = null\n        },\n      })\n    },\n    [minPercent, maxPercent, shouldReduceMotion]\n  )\n\n  const stopThumbAnimation = useCallback((thumb: Thumb) => {\n    const animation = thumb === \"min\" ? minAnimRef : maxAnimRef\n    animation.current?.stop()\n    animation.current = null\n  }, [])\n\n  const computeRubberStretch = useCallback((clientX: number, sign: number) => {\n    const rect = wrapperRectRef.current\n    if (!rect) return 0\n\n    const distancePast = sign < 0 ? rect.left - clientX : clientX - rect.right\n    const overflow = Math.max(0, distancePast - DEAD_ZONE)\n\n    return (\n      sign * MAX_STRETCH * Math.sqrt(Math.min(overflow / MAX_CURSOR_RANGE, 1))\n    )\n  }, [])\n\n  const handlePointerDown = useCallback((thumb: Thumb) => {\n    return (e: React.PointerEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      ;(e.target as HTMLElement).setPointerCapture(e.pointerId)\n\n      pointerDownPos.current = { x: e.clientX, y: e.clientY }\n      isClickRef.current = true\n      draggingThumb.current = thumb\n      setIsDragging(true)\n      setKeyboardFocusThumb(null)\n\n      const wrapper = wrapperRef.current\n      if (wrapper) {\n        const rect = wrapper.getBoundingClientRect()\n        wrapperRectRef.current = rect\n        scaleRef.current = rect.width / wrapper.offsetWidth\n      }\n    }\n  }, [])\n\n  const handleTrackPointerDown = useCallback(\n    (e: React.PointerEvent) => {\n      const wrapper = wrapperRef.current\n      if (wrapper) {\n        const rect = wrapper.getBoundingClientRect()\n        wrapperRectRef.current = rect\n        scaleRef.current = rect.width / wrapper.offsetWidth\n      }\n\n      const value = positionToValue(e.clientX)\n      const thumb: Thumb =\n        Math.abs(value - min) <= Math.abs(value - max) ? \"min\" : \"max\"\n\n      pointerDownPos.current = { x: e.clientX, y: e.clientY }\n      isClickRef.current = true\n      draggingThumb.current = thumb\n      setIsDragging(true)\n      setKeyboardFocusThumb(null)\n      ;(e.target as HTMLElement).setPointerCapture(e.pointerId)\n    },\n    [positionToValue, min, max]\n  )\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent) => {\n      const thumb = draggingThumb.current\n      if (!thumb || !pointerDownPos.current) return\n\n      const dx = e.clientX - pointerDownPos.current.x\n      const dy = e.clientY - pointerDownPos.current.y\n      if (isClickRef.current && Math.hypot(dx, dy) > CLICK_THRESHOLD) {\n        isClickRef.current = false\n      }\n      if (isClickRef.current) return\n\n      const rect = wrapperRectRef.current\n      if (rect && !shouldReduceMotion) {\n        // The min thumb only rubber-bands past the left edge (absoluteMin);\n        // the max thumb only past the right edge (absoluteMax). Dragging\n        // toward the other thumb is a hard clamp, not elastic.\n        if (thumb === \"min\" && e.clientX < rect.left) {\n          rubberStretch.jump(computeRubberStretch(e.clientX, -1))\n        } else if (thumb === \"max\" && e.clientX > rect.right) {\n          rubberStretch.jump(computeRubberStretch(e.clientX, 1))\n        } else {\n          rubberStretch.jump(0)\n        }\n      }\n\n      const rawValue = roundValue(positionToValue(e.clientX), step)\n\n      // Clamp one step short of the other thumb, not flush against it, so\n      // min and max can never meet or cross. Applied before the motion value\n      // jumps, so the thumb itself (not just the reported value) is bounded.\n      const value =\n        thumb === \"min\"\n          ? clamp(rawValue, absoluteMin, max - step)\n          : clamp(rawValue, min + step, absoluteMax)\n\n      const motionValue = thumb === \"min\" ? minPercent : maxPercent\n      stopThumbAnimation(thumb)\n      motionValue.jump(percentFromValue(value))\n\n      setRange(thumb === \"min\" ? [value, max] : [min, value])\n    },\n    [\n      positionToValue,\n      percentFromValue,\n      setRange,\n      step,\n      min,\n      max,\n      absoluteMin,\n      absoluteMax,\n      minPercent,\n      maxPercent,\n      rubberStretch,\n      computeRubberStretch,\n      shouldReduceMotion,\n      stopThumbAnimation,\n    ]\n  )\n\n  const handlePointerUp = useCallback(\n    (e: React.PointerEvent) => {\n      const thumb = draggingThumb.current\n      if (!thumb) return\n\n      if (isClickRef.current) {\n        const rawValue = positionToValue(e.clientX)\n        const snapped =\n          thumb === \"min\"\n            ? clamp(roundValue(rawValue, step), absoluteMin, max - step)\n            : clamp(roundValue(rawValue, step), min + step, absoluteMax)\n\n        animateThumbTo(thumb, percentFromValue(snapped))\n        setRange(([currentMin, currentMax]) =>\n          thumb === \"min\" ? [snapped, currentMax] : [currentMin, snapped]\n        )\n      } else {\n        // Snap the dragged value to the step grid on release.\n        const current = thumb === \"min\" ? min : max\n        const snapped = roundValue(current, step)\n        if (snapped !== current) {\n          animateThumbTo(thumb, percentFromValue(snapped))\n          setRange(([currentMin, currentMax]) =>\n            thumb === \"min\" ? [snapped, currentMax] : [currentMin, snapped]\n          )\n        }\n      }\n\n      if (!shouldReduceMotion && rubberStretch.get() !== 0) {\n        animate(rubberStretch, 0, {\n          type: \"spring\",\n          visualDuration: 0.35,\n          bounce: 0.15,\n        })\n      }\n\n      draggingThumb.current = null\n      pointerDownPos.current = null\n      setIsDragging(false)\n    },\n    [\n      positionToValue,\n      percentFromValue,\n      animateThumbTo,\n      setRange,\n      min,\n      max,\n      step,\n      absoluteMin,\n      absoluteMax,\n      rubberStretch,\n      shouldReduceMotion,\n    ]\n  )\n\n  const discreteSteps = (absoluteMax - absoluteMin) / step\n  const hashMarkCount = discreteSteps <= 10 ? discreteSteps - 1 : 9\n\n  const hashMarkPct = (i: number) => {\n    return discreteSteps <= 10\n      ? (((i + 1) * step) / (absoluteMax - absoluteMin)) * 100\n      : (i + 1) * 10\n  }\n\n  const handleKeyDown = useCallback(\n    (thumb: Thumb) => (e: React.KeyboardEvent) => {\n      const arrowStep = e.shiftKey ? step * 10 : step\n\n      const current = thumb === \"min\" ? min : max\n      const next = (() => {\n        switch (e.key) {\n          case \"ArrowRight\":\n          case \"ArrowUp\":\n            return current + arrowStep\n          case \"ArrowLeft\":\n          case \"ArrowDown\":\n            return current - arrowStep\n          case \"Home\":\n            return thumb === \"min\" ? absoluteMin : min\n          case \"End\":\n            return thumb === \"min\" ? max : absoluteMax\n          default:\n            return null\n        }\n      })()\n\n      if (next === null) return\n\n      e.preventDefault()\n      setKeyboardFocusThumb(thumb)\n\n      const bounds: [number, number] =\n        thumb === \"min\" ? [absoluteMin, max - step] : [min + step, absoluteMax]\n      const snapped = clamp(roundValue(next, step), bounds[0], bounds[1])\n\n      animateThumbTo(thumb, percentFromValue(snapped))\n      setRange(([currentMin, currentMax]) =>\n        thumb === \"min\" ? [snapped, currentMax] : [currentMin, snapped]\n      )\n    },\n    [\n      step,\n      min,\n      max,\n      absoluteMin,\n      absoluteMax,\n      animateThumbTo,\n      percentFromValue,\n      setRange,\n    ]\n  )\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"elastic-range-slider\"\n      className={cn(\n        \"[--elastic-slider-height:--spacing(9)] [--elastic-slider-radius:var(--radius-lg)]\",\n        \"[--elastic-slider-bg:var(--muted)]\",\n        \"[--elastic-slider-fill:var(--muted-foreground)]/10\",\n        \"[--elastic-slider-fill-active:var(--muted-foreground)]/20\",\n        \"[--elastic-slider-hash:var(--muted-foreground)]/30\",\n        \"[--elastic-slider-handle:var(--foreground)]\",\n        \"[--elastic-slider-label:var(--muted-foreground)]\",\n        \"[--elastic-slider-focus:var(--foreground)]\",\n        \"relative h-(--elastic-slider-height)\",\n        className\n      )}\n    >\n      <motion.div\n        ref={trackRef}\n        data-slot=\"elastic-slider-track\"\n        data-active={isActive}\n        className=\"group/elastic-slider absolute inset-y-0 start-0 touch-none overflow-hidden rounded-(--elastic-slider-radius) bg-(--elastic-slider-bg) select-none\"\n        style={{ width: rubberWidth, x: rubberX }}\n        onPointerDown={handleTrackPointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n      >\n        <div\n          data-slot=\"elastic-slider-hash-marks\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n        >\n          {Array.from({ length: hashMarkCount }, (_, i) => (\n            <div\n              key={i}\n              className={cn(\n                \"absolute top-1/2 h-2 w-px -translate-y-1/2 rounded-full transition-colors duration-200\",\n                \"bg-transparent group-data-[active=true]/elastic-slider:bg-(--elastic-slider-hash)\"\n              )}\n              style={{\n                left: `${hashMarkPct(i)}%`,\n                transform: \"translateX(-50%) translateY(-50%)\",\n              }}\n            />\n          ))}\n        </div>\n\n        <motion.div\n          data-slot=\"elastic-slider-fill\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-y-0 bg-(--elastic-slider-fill) transition-colors group-data-[active=true]/elastic-slider:bg-(--elastic-slider-fill-active)\"\n          style={{\n            left: fillLeft,\n            right: fillRight,\n          }}\n        />\n\n        <motion.div\n          role=\"slider\"\n          tabIndex={0}\n          data-slot=\"elastic-slider-handle\"\n          data-focus-visible={keyboardFocusThumb === \"min\"}\n          aria-label={minThumbLabel}\n          aria-valuemin={absoluteMin}\n          aria-valuemax={max - step}\n          aria-valuenow={min}\n          aria-valuetext={displayMin}\n          onPointerDown={handlePointerDown(\"min\")}\n          onKeyDown={handleKeyDown(\"min\")}\n          onFocus={() => setKeyboardFocusThumb(\"min\")}\n          onBlur={() => setKeyboardFocusThumb(null)}\n          className=\"absolute top-1/2 h-5 w-1 -translate-y-1/2 cursor-pointer touch-none rounded-full bg-(--elastic-slider-handle) opacity-0 transition-opacity outline-none group-data-[active=true]/elastic-slider:opacity-50 data-[focus-visible=true]:opacity-80 data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-ring/50\"\n          style={{ left: minLeft, y: \"-50%\" }}\n        />\n        <motion.div\n          role=\"slider\"\n          tabIndex={0}\n          data-slot=\"elastic-slider-handle\"\n          data-focus-visible={keyboardFocusThumb === \"max\"}\n          aria-label={maxThumbLabel}\n          aria-valuemin={min + step}\n          aria-valuemax={absoluteMax}\n          aria-valuenow={max}\n          aria-valuetext={displayMax}\n          onPointerDown={handlePointerDown(\"max\")}\n          onKeyDown={handleKeyDown(\"max\")}\n          onFocus={() => setKeyboardFocusThumb(\"max\")}\n          onBlur={() => setKeyboardFocusThumb(null)}\n          className=\"absolute top-1/2 h-5 w-1 -translate-y-1/2 cursor-pointer touch-none rounded-full bg-(--elastic-slider-handle) opacity-0 transition-opacity outline-none group-data-[active=true]/elastic-slider:opacity-50 data-[focus-visible=true]:opacity-80 data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-ring/50\"\n          style={{ left: maxLeft, y: \"-50%\" }}\n        />\n\n        <span\n          data-slot=\"elastic-slider-label\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute start-3 top-1/2 inline-flex -translate-y-1/2 items-center text-sm/none font-medium text-(--elastic-slider-label) transition-colors\"\n        >\n          {label}\n        </span>\n\n        <span\n          data-slot=\"elastic-slider-value\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute end-3 top-1/2 -translate-y-1/2 font-mono text-sm/none font-medium text-(--elastic-slider-label) transition-colors group-data-[active=true]/elastic-slider:text-(--elastic-slider-focus)\"\n        >\n          {displayMin} – {displayMax}\n        </span>\n      </motion.div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/base/hooks/use-controllable-state.ts",
      "content": "// Thanks @radix-ui\n\nimport * as React from \"react\"\n\n// use-layout-effect.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}\n\n// use-controllable-state.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n  (React as never)[\" useInsertionEffect \".trim().toString()] || useLayoutEffect\n\ntype ChangeHandler<T> = (state: T) => void\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>\n\ninterface UseControllableStateParams<T> {\n  prop?: T | undefined\n  defaultProp: T\n  onChange?: ChangeHandler<T>\n  caller?: string\n}\n\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange = () => {},\n  caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n  const [uncontrolledProp, setUncontrolledProp, onChangeRef] =\n    useUncontrolledState({\n      defaultProp,\n      onChange,\n    })\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolledProp\n\n  // OK to disable conditionally calling hooks here because they will always run\n  // consistently in the same environment. Bundlers should be able to remove the\n  // code block entirely in production.\n  /* eslint-disable react-hooks/rules-of-hooks */\n  if (process.env.NODE_ENV !== \"production\") {\n    const isControlledRef = React.useRef(prop !== undefined)\n    React.useEffect(() => {\n      const wasControlled = isControlledRef.current\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? \"controlled\" : \"uncontrolled\"\n        const to = isControlled ? \"controlled\" : \"uncontrolled\"\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n        )\n      }\n      isControlledRef.current = isControlled\n    }, [isControlled, caller])\n  }\n  /* eslint-enable react-hooks/rules-of-hooks */\n\n  const setValue = React.useCallback<SetStateFn<T>>(\n    (nextValue) => {\n      if (isControlled) {\n        const value = isFunction(nextValue) ? nextValue(prop) : nextValue\n        if (value !== prop) {\n          onChangeRef.current?.(value)\n        }\n      } else {\n        setUncontrolledProp(nextValue)\n      }\n    },\n    [isControlled, prop, setUncontrolledProp, onChangeRef]\n  )\n\n  return [value, setValue]\n}\n\nfunction useUncontrolledState<T>({\n  defaultProp,\n  onChange,\n}: Omit<UseControllableStateParams<T>, \"prop\">): [\n  Value: T,\n  setValue: React.Dispatch<React.SetStateAction<T>>,\n  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n  const [value, setValue] = React.useState(defaultProp)\n  const prevValueRef = React.useRef(value)\n\n  const onChangeRef = React.useRef(onChange)\n  useInsertionEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    if (prevValueRef.current !== value) {\n      onChangeRef.current?.(value)\n      prevValueRef.current = value\n    }\n  }, [value, prevValueRef])\n\n  return [value, setValue, onChangeRef]\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n  return typeof value === \"function\"\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:ui"
}