{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-slider",
  "title": "Elastic Slider",
  "description": "Slider with elastic rubber-band drag and magnetic snap feedback.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/base/ui/elastic-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\nconst CLICK_THRESHOLD = 3\nconst DEAD_ZONE = 32\nconst MAX_CURSOR_RANGE = 200\nconst MAX_STRETCH = 8\n\n// Layout offsets used by the \"handle dodges label/value\" calculation.\nconst HANDLE_BUFFER = 8\nconst LABEL_OFFSET = 12 + 4\nconst VALUE_OFFSET = 12 - 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\n// Magnetic snap to the nearest decile when within 3.125% of it.\nfunction snapToDecile(rawValue: number, min: number, max: number): number {\n  const normalized = (rawValue - min) / (max - min)\n  const nearest = Math.round(normalized * 10) / 10\n  if (Math.abs(normalized - nearest) <= 0.03125) {\n    return min + nearest * (max - min)\n  }\n  return rawValue\n}\n\nexport type ElasticSliderProps = {\n  /** Label shown inside the track. */\n  label: string\n\n  /** Controlled value. Use together with `onValueChange` */\n  value?: number\n  /** Initial value for uncontrolled mode. Falls back to `min` */\n  defaultValue?: number\n  /** Called with the new value on drag, click, or key press. */\n  onValueChange?: (value: number) => void\n\n  /**\n   * Minimum value.\n   * @defaultValue 0 */\n  min?: number\n  /**\n   * Maximum value.\n   * @defaultValue 1 */\n  max?: number\n  /**\n   * Smallest increment.\n   * @defaultValue 0.01 */\n  step?: number\n  /** Format the displayed value. Defaults to `value.toFixed(...)` based on `step` */\n  formatValue?: (value: number) => string\n\n  className?: string\n  /** Accessible name. Falls back to `label` */\n  \"aria-label\"?: string\n}\n\nexport function ElasticSlider({\n  label,\n\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n\n  min = 0,\n  max = 1,\n  step = 0.01,\n  formatValue,\n\n  className,\n  \"aria-label\": ariaLabel,\n}: ElasticSliderProps) {\n  const [value = min, setValue] = useControllableState({\n    prop: valueProp,\n    defaultProp: defaultValue ?? min,\n    onChange: onValueChange,\n  })\n\n  const shouldReduceMotion = useReducedMotion()\n\n  const wrapperRef = useRef<HTMLDivElement>(null)\n  const trackRef = useRef<HTMLDivElement>(null)\n  const labelRef = useRef<HTMLSpanElement>(null)\n  const valueRef = useRef<HTMLSpanElement>(null)\n\n  const [isInteracting, setIsInteracting] = useState(false)\n  const [isDragging, setIsDragging] = useState(false)\n  const [isHovered, setIsHovered] = useState(false)\n  /** Ring only for Tab focus or keyboard value nudges, not pointer press/drag. */\n  const [keyboardFocusRing, setKeyboardFocusRing] = useState(false)\n\n  // Pointer session state — mutable, does not trigger re-renders.\n  const pointerDownPos = useRef<{ x: number; y: number } | null>(null)\n  const pendingPointerFocusRef = useRef(false)\n  const isClickRef = useRef(true)\n  const animRef = useRef<ReturnType<typeof animate> | null>(null)\n  const wrapperRectRef = useRef<DOMRect | null>(null)\n  const scaleRef = useRef(1)\n\n  /**\n   * Reads the ambient text direction where the slider actually renders, since\n   * this project is RTL-first by default. Value 0 always maps to the reading\n   * start (right edge in RTL) and value max to the reading end, matching how\n   * a native <input type=\"range\"> behaves in RTL browsers.\n   */\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 percentage = ((value - min) / (max - min)) * 100\n  const isActive = isInteracting || isHovered\n  const displayValue = formatValue\n    ? formatValue(value)\n    : value.toFixed(decimalsForStep(step))\n\n  // Fill + handle driven by a single motion value for imperative updates.\n  const fillPercent = useMotionValue(percentage)\n  const fillWidth = useTransform(fillPercent, (pct) => `${pct}%`)\n  const handleInsetStart = useTransform(\n    fillPercent,\n    (pct) => `max(4px, calc(${pct}% - 8px))`\n  )\n\n  // Rubber band: widens the track and pulls it toward the overshot edge.\n  // The track is anchored at the reading start (inset-inline-start: 0), so the\n  // shift direction is direction-aware: in LTR the track is pinned at the\n  // physical left (overshooting left shifts it: x = s, s < 0); in RTL it is\n  // pinned at the physical right (overshooting right shifts it: x = s, s > 0).\n  // The width grows by |s| either way, so the anchored edge stays put and only\n  // the overshot edge stretches.\n  const rubberStretch = useMotionValue(0)\n  const rubberWidth = useTransform(\n    rubberStretch,\n    (s) => `calc(100% + ${Math.abs(s)}px)`\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 interacting and no spring is in flight.\n  useEffect(() => {\n    if (!isInteracting && !animRef.current) {\n      fillPercent.jump(percentage)\n    }\n  }, [percentage, isInteracting, fillPercent])\n\n  const positionToValue = useCallback(\n    (clientX: number) => {\n      const rect = wrapperRectRef.current\n      if (!rect) return min\n\n      const sceneX = (clientX - rect.left) / scaleRef.current\n      const nativeWidth = wrapperRef.current?.offsetWidth ?? rect.width\n      let percent = clamp(sceneX / nativeWidth, 0, 1)\n      if (dirRef.current === \"rtl\") percent = 1 - percent\n\n      return clamp(min + percent * (max - min), min, max)\n    },\n    [min, max]\n  )\n\n  const percentFromValue = useCallback(\n    (v: number) => ((v - min) / (max - min)) * 100,\n    [min, max]\n  )\n\n  // Animate fill to a target percent, or jump instantly when the user prefers\n  // reduced motion. Position still updates — only the spring is skipped.\n  const animateFillTo = useCallback(\n    (targetPercent: number) => {\n      animRef.current?.stop()\n\n      if (shouldReduceMotion) {\n        fillPercent.jump(targetPercent)\n        animRef.current = null\n        return\n      }\n\n      animRef.current = animate(fillPercent, targetPercent, {\n        type: \"spring\",\n        stiffness: 300,\n        damping: 25,\n        mass: 0.8,\n        onComplete: () => {\n          animRef.current = null\n        },\n      })\n    },\n    [fillPercent, shouldReduceMotion]\n  )\n\n  const stopFillAnimation = useCallback(() => {\n    animRef.current?.stop()\n    animRef.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((e: React.PointerEvent) => {\n    e.preventDefault()\n    ;(e.target as HTMLElement).setPointerCapture(e.pointerId)\n\n    pointerDownPos.current = { x: e.clientX, y: e.clientY }\n\n    isClickRef.current = true\n\n    setIsInteracting(true)\n\n    pendingPointerFocusRef.current = true\n    setKeyboardFocusRing(false)\n\n    // Pointer interactions should move focus to the slider so subsequent\n    // keyboard input is received and focus styles match the active state.\n    trackRef.current?.focus({ preventScroll: true })\n    requestAnimationFrame(() => {\n      pendingPointerFocusRef.current = false\n    })\n\n    // Snapshot the wrapper rect so later math is immune to layout shifts.\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  const handlePointerMove = useCallback(\n    (e: React.PointerEvent) => {\n      if (!isInteracting || !pointerDownPos.current) return\n\n      const dx = e.clientX - pointerDownPos.current.x\n      const dy = e.clientY - pointerDownPos.current.y\n\n      if (isClickRef.current && Math.hypot(dx, dy) > CLICK_THRESHOLD) {\n        isClickRef.current = false\n        setIsDragging(true)\n      }\n\n      if (isClickRef.current) return\n\n      const rect = wrapperRectRef.current\n      if (rect && !shouldReduceMotion) {\n        if (e.clientX < rect.left) {\n          rubberStretch.jump(computeRubberStretch(e.clientX, -1))\n        } else if (e.clientX > rect.right) {\n          rubberStretch.jump(computeRubberStretch(e.clientX, 1))\n        } else {\n          rubberStretch.jump(0)\n        }\n      }\n\n      const newValue = positionToValue(e.clientX)\n      stopFillAnimation()\n      fillPercent.jump(percentFromValue(newValue))\n      setValue(roundValue(newValue, step))\n    },\n    [\n      isInteracting,\n      positionToValue,\n      percentFromValue,\n      setValue,\n      step,\n      fillPercent,\n      rubberStretch,\n      computeRubberStretch,\n      shouldReduceMotion,\n      stopFillAnimation,\n    ]\n  )\n\n  const handlePointerUp = useCallback(\n    (e: React.PointerEvent) => {\n      if (!isInteracting) return\n\n      if (isClickRef.current) {\n        // Coarse sliders (≤10 positions) snap to the nearest step;\n        // continuous sliders keep the decile-magnetic behavior.\n        const rawValue = positionToValue(e.clientX)\n        const discreteSteps = (max - min) / step\n        const snapped =\n          discreteSteps <= 10\n            ? clamp(min + Math.round((rawValue - min) / step) * step, min, max)\n            : snapToDecile(rawValue, min, max)\n\n        animateFillTo(percentFromValue(snapped))\n        setValue(roundValue(snapped, step))\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      setIsInteracting(false)\n      setIsDragging(false)\n      pointerDownPos.current = null\n    },\n    [\n      isInteracting,\n      positionToValue,\n      percentFromValue,\n      setValue,\n      min,\n      max,\n      step,\n      animateFillTo,\n      rubberStretch,\n      shouldReduceMotion,\n    ]\n  )\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      // Shift + Arrow is a Figma-style fast nudge: jumps by 10x the step,\n      // independent of the WAI-ARIA Page step (which scales with range).\n      const arrowStep = e.shiftKey ? step * 10 : step\n\n      // Right/Left arrows follow reading direction: in RTL, ArrowRight moves\n      // toward the reading start (decreases value) like a native range input.\n      const forwardKey = dirRef.current === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\"\n      const backwardKey = dirRef.current === \"rtl\" ? \"ArrowRight\" : \"ArrowLeft\"\n\n      const next = (() => {\n        switch (e.key) {\n          case forwardKey:\n          case \"ArrowUp\":\n            return value + arrowStep\n\n          case backwardKey:\n          case \"ArrowDown\":\n            return value - arrowStep\n\n          case \"Home\":\n            return min\n\n          case \"End\":\n            return max\n\n          default:\n            return null\n        }\n      })()\n\n      if (next === null) return\n\n      e.preventDefault()\n\n      setKeyboardFocusRing(true)\n\n      const snapped = roundValue(clamp(next, min, max), step)\n      animateFillTo(percentFromValue(snapped))\n      setValue(snapped)\n    },\n    [value, min, max, step, animateFillTo, percentFromValue, setValue]\n  )\n\n  const handleTrackFocus = useCallback(() => {\n    if (!pendingPointerFocusRef.current) {\n      setKeyboardFocusRing(true)\n    }\n  }, [])\n\n  const handleTrackBlur = useCallback(() => {\n    setKeyboardFocusRing(false)\n  }, [])\n\n  // Measure label + value to derive \"dodge\" thresholds so the handle fades\n  // when it would overlap either text.\n  const [dodge, setDodge] = useState({ left: 38, right: 72 })\n\n  useLayoutEffect(() => {\n    const wrapper = wrapperRef.current\n    if (!wrapper) return\n\n    const measure = () => {\n      const trackWidth = wrapper.offsetWidth\n      if (trackWidth <= 0) return\n\n      const labelEl = labelRef.current\n      const valueEl = valueRef.current\n\n      const left = labelEl\n        ? ((LABEL_OFFSET + labelEl.offsetWidth + HANDLE_BUFFER) / trackWidth) *\n          100\n        : 38\n\n      const right = valueEl\n        ? ((trackWidth - VALUE_OFFSET - valueEl.offsetWidth - HANDLE_BUFFER) /\n            trackWidth) *\n          100\n        : 72\n\n      setDodge((prev) => {\n        return prev.left === left && prev.right === right\n          ? prev\n          : { left, right }\n      })\n    }\n\n    measure()\n\n    const observer = new ResizeObserver(measure)\n    observer.observe(wrapper)\n\n    if (labelRef.current) observer.observe(labelRef.current)\n    if (valueRef.current) observer.observe(valueRef.current)\n\n    return () => observer.disconnect()\n  }, [label, displayValue])\n\n  const valueDodge = percentage < dodge.left || percentage > dodge.right\n  const handleOpacity = !isActive\n    ? 0\n    : valueDodge\n      ? 0.1\n      : isDragging\n        ? 0.8\n        : 0.5\n\n  const discreteSteps = (max - min) / step\n  const hashMarkCount = discreteSteps <= 10 ? discreteSteps - 1 : 9\n\n  const hashMarkPct = (i: number) => {\n    return discreteSteps <= 10\n      ? (((i + 1) * step) / (max - min)) * 100\n      : (i + 1) * 10\n  }\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"elastic-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        role=\"slider\"\n        tabIndex={0}\n        data-slot=\"elastic-slider-track\"\n        data-active={isActive}\n        data-focus-visible={keyboardFocusRing}\n        aria-label={ariaLabel ?? label}\n        aria-orientation=\"horizontal\"\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-valuetext={displayValue}\n        className={cn(\n          \"group/elastic-slider absolute inset-y-0 start-0 cursor-pointer touch-none overflow-hidden rounded-(--elastic-slider-radius) bg-(--elastic-slider-bg) outline-none select-none\",\n          \"data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-ring/50 data-[focus-visible=true]:ring-offset-1 data-[focus-visible=true]:ring-offset-background\"\n        )}\n        style={{ width: rubberWidth, x: rubberX }}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onFocus={handleTrackFocus}\n        onBlur={handleTrackBlur}\n        onKeyDown={handleKeyDown}\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                insetInlineStart: `${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={cn(\n            \"pointer-events-none absolute inset-y-0 start-0 transition-colors\",\n            \"bg-(--elastic-slider-fill) group-data-[active=true]/elastic-slider:bg-(--elastic-slider-fill-active)\"\n          )}\n          style={{ width: fillWidth }}\n        />\n\n        <motion.div\n          data-slot=\"elastic-slider-handle\"\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-1/2 h-5 w-1 rounded-full bg-(--elastic-slider-handle)\"\n          style={{ insetInlineStart: handleInsetStart, y: \"-50%\" }}\n          animate={{\n            opacity: handleOpacity,\n            scaleX: isActive ? 1 : 0.25,\n            scaleY: isActive && valueDodge ? 0.75 : 1,\n          }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  scaleX: {\n                    type: \"spring\",\n                    visualDuration: 0.25,\n                    bounce: 0.15,\n                  },\n                  scaleY: { type: \"spring\", visualDuration: 0.2, bounce: 0.1 },\n                  opacity: { duration: 0.15 },\n                }\n          }\n        />\n\n        <span\n          ref={labelRef}\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          ref={valueRef}\n          data-slot=\"elastic-slider-value\"\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute end-3 top-1/2 -translate-y-1/2 font-mono text-sm/none font-medium transition-colors\",\n            \"text-(--elastic-slider-label) group-data-[active=true]/elastic-slider:text-(--elastic-slider-focus)\"\n          )}\n        >\n          {displayValue}\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"
}