{
  "name": "angle-slider",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/angle-slider.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { VisuallyHiddenInput } from \"@/registry/bases/radix/components/visually-hidden-input\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"AngleSlider\";\nconst THUMB_NAME = \"AngleSliderThumb\";\n\nconst PAGE_KEYS = [\"PageUp\", \"PageDown\"];\nconst ARROW_KEYS = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"];\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof AngleSlider>;\ntype ThumbElement = React.ComponentRef<typeof AngleSliderThumb>;\n\nfunction clamp(value: number, [min, max]: [number, number]) {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction getNextSortedValues(\n  prevValues: number[] = [],\n  nextValue: number,\n  atIndex: number,\n) {\n  const nextValues = [...prevValues];\n  nextValues[atIndex] = nextValue;\n  return nextValues.sort((a, b) => a - b);\n}\n\nfunction getStepsBetweenValues(values: number[]) {\n  return values.slice(0, -1).map((value, index) => {\n    const nextValue = values[index + 1];\n    return nextValue !== undefined ? nextValue - value : 0;\n  });\n}\n\nfunction hasMinStepsBetweenValues(\n  values: number[],\n  minStepsBetweenValues: number,\n) {\n  if (minStepsBetweenValues > 0) {\n    const stepsBetweenValues = getStepsBetweenValues(values);\n    const actualMinStepsBetweenValues =\n      stepsBetweenValues.length > 0 ? Math.min(...stepsBetweenValues) : 0;\n    return actualMinStepsBetweenValues >= minStepsBetweenValues;\n  }\n  return true;\n}\n\nfunction getDecimalCount(value: number) {\n  return (String(value).split(\".\")[1] ?? \"\").length;\n}\n\nfunction roundValue(value: number, decimalCount: number) {\n  const rounder = 10 ** decimalCount;\n  return Math.round(value * rounder) / rounder;\n}\n\nfunction getClosestValueIndex(values: number[], nextValue: number) {\n  if (values.length === 1) return 0;\n  const distances = values.map((value) => Math.abs(value - nextValue));\n  const closestDistance = Math.min(...distances);\n  return distances.indexOf(closestDistance);\n}\n\ninterface ThumbData {\n  id: string;\n  element: ThumbElement;\n  index: number;\n  value: number;\n}\n\ninterface StoreState {\n  values: number[];\n  thumbs: Map<number, ThumbData>;\n  valueIndexToChange: number;\n  min: number;\n  max: number;\n  step: number;\n  size: number;\n  thickness: number;\n  startAngle: number;\n  endAngle: number;\n  minStepsBetweenThumbs: number;\n  disabled: boolean;\n  inverted: boolean;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n  addThumb: (index: number, thumbData: ThumbData) => void;\n  removeThumb: (index: number) => void;\n  updateValue: (\n    value: number,\n    atIndex: number,\n    options?: { commit?: boolean },\n  ) => void;\n  getValueFromPointer: (\n    clientX: number,\n    clientY: number,\n    rect: DOMRect,\n  ) => number;\n  getAngleFromValue: (value: number) => number;\n  getPositionFromAngle: (angle: number) => { x: number; y: number };\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useStore<T>(selector: (state: StoreState) => T): T {\n  const store = useStoreContext(\"useStore\");\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface SliderContextValue {\n  dir: Direction;\n  name?: string;\n  form?: string;\n}\n\nconst SliderContext = React.createContext<SliderContextValue | null>(null);\n\nfunction useSliderContext(consumerName: string) {\n  const context = React.useContext(SliderContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface AngleSliderProps extends Omit<DivProps, \"defaultValue\"> {\n  value?: number[];\n  defaultValue?: number[];\n  onValueChange?: (value: number[]) => void;\n  onValueCommit?: (value: number[]) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n  minStepsBetweenThumbs?: number;\n  size?: number;\n  thickness?: number;\n  startAngle?: number;\n  endAngle?: number;\n  dir?: Direction;\n  form?: string;\n  name?: string;\n  disabled?: boolean;\n  inverted?: boolean;\n}\n\nfunction AngleSlider(props: AngleSliderProps) {\n  const {\n    value,\n    defaultValue = [0],\n    onValueChange,\n    onValueCommit,\n    min = 0,\n    max = 100,\n    step = 1,\n    minStepsBetweenThumbs = 0,\n    size = 60,\n    thickness = 8,\n    startAngle = -90,\n    endAngle = 270,\n    dir: dirProp,\n    form,\n    name,\n    disabled = false,\n    inverted = false,\n    asChild,\n    className,\n    ref,\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n    onPointerDown: onPointerDownProp,\n    onKeyDown: onKeyDownProp,\n    ...rootProps\n  } = props;\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => ({\n    values: value ?? defaultValue,\n    thumbs: new Map(),\n    valueIndexToChange: 0,\n    min,\n    max,\n    step,\n    minStepsBetweenThumbs,\n    disabled,\n    inverted,\n    size,\n    thickness,\n    startAngle,\n    endAngle,\n  }));\n\n  const propsRef = useAsRef({\n    onValueChange,\n    onValueCommit,\n    onPointerMove: onPointerMoveProp,\n    onPointerUp: onPointerUpProp,\n    onPointerDown: onPointerDownProp,\n    onKeyDown: onKeyDownProp,\n  });\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      setState: (key, value) => {\n        if (Object.is(stateRef.current[key], value)) return;\n\n        if (key === \"values\" && Array.isArray(value)) {\n          const hasChanged = String(stateRef.current.values) !== String(value);\n          stateRef.current.values = value;\n          if (hasChanged) {\n            propsRef.current.onValueChange?.(value);\n          }\n        } else {\n          stateRef.current[key] = value;\n        }\n\n        store.notify();\n      },\n      addThumb: (index, thumbData) => {\n        stateRef.current.thumbs.set(index, thumbData);\n        store.notify();\n      },\n      removeThumb: (index) => {\n        stateRef.current.thumbs.delete(index);\n        store.notify();\n      },\n      updateValue: (value, atIndex, { commit = false } = {}) => {\n        const { min, max, step, minStepsBetweenThumbs } = stateRef.current;\n        const decimalCount = getDecimalCount(step);\n        const snapToStep = roundValue(\n          Math.round((value - min) / step) * step + min,\n          decimalCount,\n        );\n        const nextValue = clamp(snapToStep, [min, max]);\n\n        const nextValues = getNextSortedValues(\n          stateRef.current.values,\n          nextValue,\n          atIndex,\n        );\n\n        if (\n          hasMinStepsBetweenValues(nextValues, minStepsBetweenThumbs * step)\n        ) {\n          stateRef.current.valueIndexToChange = nextValues.indexOf(nextValue);\n          const hasChanged =\n            String(nextValues) !== String(stateRef.current.values);\n\n          if (hasChanged) {\n            stateRef.current.values = nextValues;\n            propsRef.current.onValueChange?.(nextValues);\n            if (commit) propsRef.current.onValueCommit?.(nextValues);\n            store.notify();\n          }\n        }\n      },\n      getValueFromPointer: (clientX, clientY, rect) => {\n        const { min, max, inverted, startAngle, endAngle } = stateRef.current;\n        const centerX = rect.left + rect.width / 2;\n        const centerY = rect.top + rect.height / 2;\n\n        const deltaX = clientX - centerX;\n        const deltaY = clientY - centerY;\n        let angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;\n\n        if (angle < 0) angle += 360;\n\n        angle = (angle - startAngle + 360) % 360;\n\n        const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n\n        let percent = angle / totalAngle;\n        if (inverted) percent = 1 - percent;\n\n        return min + percent * (max - min);\n      },\n      getAngleFromValue: (value) => {\n        const { min, max, inverted, startAngle, endAngle } = stateRef.current;\n        let percent = (value - min) / (max - min);\n        if (inverted) percent = 1 - percent;\n\n        const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n        const angle = startAngle + percent * totalAngle;\n\n        return angle;\n      },\n      getPositionFromAngle: (angle) => {\n        const { size } = stateRef.current;\n        const radians = (angle * Math.PI) / 180;\n\n        return {\n          x: size * Math.cos(radians),\n          y: size * Math.sin(radians),\n        };\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, propsRef]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (value !== undefined) {\n      store.setState(\"values\", value);\n    }\n  }, [value, store]);\n\n  useIsomorphicLayoutEffect(() => {\n    const currentState = store.getState();\n\n    if (currentState.min !== min) {\n      store.setState(\"min\", min);\n    }\n    if (currentState.max !== max) {\n      store.setState(\"max\", max);\n    }\n    if (currentState.step !== step) {\n      store.setState(\"step\", step);\n    }\n    if (currentState.minStepsBetweenThumbs !== minStepsBetweenThumbs) {\n      store.setState(\"minStepsBetweenThumbs\", minStepsBetweenThumbs);\n    }\n    if (currentState.size !== size) {\n      store.setState(\"size\", size);\n    }\n    if (currentState.thickness !== thickness) {\n      store.setState(\"thickness\", thickness);\n    }\n    if (currentState.startAngle !== startAngle) {\n      store.setState(\"startAngle\", startAngle);\n    }\n    if (currentState.endAngle !== endAngle) {\n      store.setState(\"endAngle\", endAngle);\n    }\n    if (currentState.disabled !== disabled) {\n      store.setState(\"disabled\", disabled);\n    }\n    if (currentState.inverted !== inverted) {\n      store.setState(\"inverted\", inverted);\n    }\n  }, [\n    store,\n    min,\n    max,\n    step,\n    minStepsBetweenThumbs,\n    size,\n    thickness,\n    startAngle,\n    endAngle,\n    disabled,\n    inverted,\n  ]);\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  const [sliderElement, setSliderElement] = React.useState<RootElement | null>(\n    null,\n  );\n  const composedRef = useComposedRefs(ref, setSliderElement);\n  const valuesBeforeSlideStartRef = React.useRef(value ?? defaultValue);\n\n  const contextValue = React.useMemo<SliderContextValue>(\n    () => ({\n      dir,\n      name,\n      form,\n    }),\n    [dir, name, form],\n  );\n\n  const onSliderStart = React.useCallback(\n    (pointerValue: number) => {\n      if (disabled) return;\n\n      const values = store.getState().values;\n      const closestIndex = getClosestValueIndex(values, pointerValue);\n      store.setState(\"valueIndexToChange\", closestIndex);\n      store.updateValue(pointerValue, closestIndex);\n    },\n    [store, disabled],\n  );\n\n  const onSliderMove = React.useCallback(\n    (pointerValue: number) => {\n      if (disabled) return;\n\n      const valueIndexToChange = store.getState().valueIndexToChange;\n      store.updateValue(pointerValue, valueIndexToChange);\n    },\n    [store, disabled],\n  );\n\n  const onSliderEnd = React.useCallback(() => {\n    if (disabled) return;\n\n    const state = store.getState();\n    const prevValue =\n      valuesBeforeSlideStartRef.current[state.valueIndexToChange];\n    const nextValue = state.values[state.valueIndexToChange];\n    const hasChanged = nextValue !== prevValue;\n\n    if (hasChanged) {\n      onValueCommit?.(state.values);\n    }\n  }, [store, disabled, onValueCommit]);\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<RootElement>) => {\n      propsRef.current.onKeyDown?.(event);\n      if (event.defaultPrevented || disabled) return;\n\n      const state = store.getState();\n      const { values, valueIndexToChange, min, max, step } = state;\n      const currentValue = values[valueIndexToChange] ?? min;\n\n      if (event.key === \"Home\") {\n        event.preventDefault();\n        store.updateValue(min, 0, { commit: true });\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        store.updateValue(max, values.length - 1, { commit: true });\n      } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) {\n        event.preventDefault();\n\n        const isPageKey = PAGE_KEYS.includes(event.key);\n        const isSkipKey =\n          isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key));\n        const multiplier = isSkipKey ? 10 : 1;\n\n        let direction = 0;\n        const isDecreaseKey = [\"ArrowLeft\", \"ArrowUp\", \"PageUp\"].includes(\n          event.key,\n        );\n        direction = isDecreaseKey ? -1 : 1;\n        if (inverted) direction *= -1;\n\n        const stepInDirection = step * multiplier * direction;\n        store.updateValue(currentValue + stepInDirection, valueIndexToChange, {\n          commit: true,\n        });\n      }\n    },\n    [store, propsRef, disabled, inverted],\n  );\n\n  const onPointerDown = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      propsRef.current.onPointerDown?.(event);\n      if (event.defaultPrevented || disabled) return;\n\n      const target = event.target as HTMLElement;\n      target.setPointerCapture(event.pointerId);\n      event.preventDefault();\n\n      if (!disabled) {\n        valuesBeforeSlideStartRef.current = store.getState().values;\n\n        const thumbs = Array.from(store.getState().thumbs.values());\n        const clickedThumb = thumbs.find((thumb) =>\n          thumb.element.contains(target),\n        );\n\n        if (clickedThumb) {\n          clickedThumb.element.focus();\n          store.setState(\"valueIndexToChange\", clickedThumb.index);\n        } else if (sliderElement) {\n          const rect = sliderElement.getBoundingClientRect();\n          const pointerValue = store.getValueFromPointer(\n            event.clientX,\n            event.clientY,\n            rect,\n          );\n          onSliderStart(pointerValue);\n        }\n      }\n    },\n    [store, propsRef, disabled, sliderElement, onSliderStart],\n  );\n\n  const onPointerMove = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      propsRef.current.onPointerMove?.(event);\n      if (event.defaultPrevented || disabled) return;\n\n      const target = event.target as HTMLElement;\n      if (target.hasPointerCapture(event.pointerId) && sliderElement) {\n        const rect = sliderElement.getBoundingClientRect();\n        const pointerValue = store.getValueFromPointer(\n          event.clientX,\n          event.clientY,\n          rect,\n        );\n        onSliderMove(pointerValue);\n      }\n    },\n    [store, propsRef, disabled, sliderElement, onSliderMove],\n  );\n\n  const onPointerUp = React.useCallback(\n    (event: React.PointerEvent<RootElement>) => {\n      propsRef.current.onPointerUp?.(event);\n      if (event.defaultPrevented) return;\n\n      const target = event.target as RootElement;\n      if (target.hasPointerCapture(event.pointerId)) {\n        target.releasePointerCapture(event.pointerId);\n        onSliderEnd();\n      }\n    },\n    [propsRef, onSliderEnd],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <SliderContext.Provider value={contextValue}>\n        <RootPrimitive\n          data-disabled={disabled ? \"\" : undefined}\n          data-slot=\"angle-slider\"\n          dir={dir}\n          {...rootProps}\n          ref={composedRef}\n          className={cn(\n            \"relative touch-none select-none\",\n            disabled && \"opacity-50\",\n            className,\n          )}\n          style={{\n            width: `${size * 2 + 40}px`,\n            height: `${size * 2 + 40}px`,\n          }}\n          onKeyDown={onKeyDown}\n          onPointerDown={onPointerDown}\n          onPointerMove={onPointerMove}\n          onPointerUp={onPointerUp}\n        />\n      </SliderContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\nfunction AngleSliderTrack(props: React.ComponentProps<\"svg\">) {\n  const { className, children, ...trackProps } = props;\n\n  const disabled = useStore((state) => state.disabled);\n  const size = useStore((state) => state.size);\n  const thickness = useStore((state) => state.thickness);\n  const startAngle = useStore((state) => state.startAngle);\n  const endAngle = useStore((state) => state.endAngle);\n\n  const center = size + 20;\n  const trackRadius = size;\n\n  const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n  const isFullCircle = totalAngle >= 359;\n\n  const startRadians = (startAngle * Math.PI) / 180;\n  const endRadians = (endAngle * Math.PI) / 180;\n\n  const startX = center + trackRadius * Math.cos(startRadians);\n  const startY = center + trackRadius * Math.sin(startRadians);\n  const endX = center + trackRadius * Math.cos(endRadians);\n  const endY = center + trackRadius * Math.sin(endRadians);\n\n  const largeArcFlag = totalAngle > 180 ? 1 : 0;\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      data-disabled={disabled ? \"\" : undefined}\n      data-slot=\"angle-slider-track\"\n      width={center * 2}\n      height={center * 2}\n      {...trackProps}\n      className={cn(\"absolute inset-0\", className)}\n    >\n      {isFullCircle ? (\n        <circle\n          data-slot=\"angle-slider-track-rail\"\n          cx={center}\n          cy={center}\n          r={trackRadius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={thickness}\n          strokeLinecap=\"round\"\n          vectorEffect=\"non-scaling-stroke\"\n          className=\"stroke-muted\"\n        />\n      ) : (\n        <path\n          data-slot=\"angle-slider-track-rail\"\n          d={`M ${startX} ${startY} A ${trackRadius} ${trackRadius} 0 ${largeArcFlag} 1 ${endX} ${endY}`}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={thickness}\n          strokeLinecap=\"round\"\n          vectorEffect=\"non-scaling-stroke\"\n          className=\"stroke-muted\"\n        />\n      )}\n      {children}\n    </svg>\n  );\n}\n\nfunction AngleSliderRange(props: React.ComponentProps<\"path\">) {\n  const { className, ...rangeProps } = props;\n\n  const values = useStore((state) => state.values);\n  const min = useStore((state) => state.min);\n  const max = useStore((state) => state.max);\n  const disabled = useStore((state) => state.disabled);\n  const size = useStore((state) => state.size);\n  const thickness = useStore((state) => state.thickness);\n  const startAngle = useStore((state) => state.startAngle);\n  const endAngle = useStore((state) => state.endAngle);\n\n  const center = size + 20;\n  const trackRadius = size;\n\n  const sortedValues = [...values].sort((a, b) => a - b);\n\n  const rangeStart = values.length <= 1 ? min : (sortedValues[0] ?? min);\n  const rangeEnd =\n    values.length <= 1\n      ? (sortedValues[0] ?? min)\n      : (sortedValues[sortedValues.length - 1] ?? max);\n\n  const rangeStartPercent = (rangeStart - min) / (max - min);\n  const rangeEndPercent = (rangeEnd - min) / (max - min);\n\n  const totalAngle = (endAngle - startAngle + 360) % 360 || 360;\n  const rangeStartAngle = startAngle + rangeStartPercent * totalAngle;\n  const rangeEndAngle = startAngle + rangeEndPercent * totalAngle;\n\n  const rangeStartRadians = (rangeStartAngle * Math.PI) / 180;\n  const rangeEndRadians = (rangeEndAngle * Math.PI) / 180;\n\n  const startX = center + trackRadius * Math.cos(rangeStartRadians);\n  const startY = center + trackRadius * Math.sin(rangeStartRadians);\n  const endX = center + trackRadius * Math.cos(rangeEndRadians);\n  const endY = center + trackRadius * Math.sin(rangeEndRadians);\n\n  const rangeAngle = (rangeEndAngle - rangeStartAngle + 360) % 360;\n  const largeArcFlag = rangeAngle > 180 ? 1 : 0;\n\n  if (rangeStart === rangeEnd) return null;\n\n  return (\n    <path\n      data-disabled={disabled ? \"\" : undefined}\n      data-slot=\"angle-slider-range\"\n      d={`M ${startX} ${startY} A ${trackRadius} ${trackRadius} 0 ${largeArcFlag} 1 ${endX} ${endY}`}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={thickness}\n      strokeLinecap=\"round\"\n      vectorEffect=\"non-scaling-stroke\"\n      {...rangeProps}\n      className={cn(\"stroke-primary\", className)}\n    />\n  );\n}\n\ninterface AngleSliderThumbProps extends DivProps {\n  index?: number;\n}\n\nfunction AngleSliderThumb(props: AngleSliderThumbProps) {\n  const { index: indexProp, className, asChild, ref, ...thumbProps } = props;\n\n  const context = useSliderContext(THUMB_NAME);\n  const store = useStoreContext(THUMB_NAME);\n  const values = useStore((state) => state.values);\n  const min = useStore((state) => state.min);\n  const max = useStore((state) => state.max);\n  const step = useStore((state) => state.step);\n  const disabled = useStore((state) => state.disabled);\n  const size = useStore((state) => state.size);\n\n  const thumbId = React.useId();\n  const [thumbElement, setThumbElement] = React.useState<ThumbElement | null>(\n    null,\n  );\n  const composedRef = useComposedRefs(ref, setThumbElement);\n\n  const isFormControl = thumbElement\n    ? context.form || !!thumbElement.closest(\"form\")\n    : true;\n\n  const index = indexProp ?? 0;\n  const value = values[index];\n\n  React.useEffect(() => {\n    if (thumbElement && value !== undefined) {\n      store.addThumb(index, {\n        id: thumbId,\n        element: thumbElement,\n        index,\n        value,\n      });\n\n      return () => {\n        store.removeThumb(index);\n      };\n    }\n  }, [thumbElement, thumbId, index, value, store]);\n\n  const thumbStyle = React.useMemo<React.CSSProperties>(() => {\n    if (value === undefined) return {};\n\n    const angle = store.getAngleFromValue(value);\n    const position = store.getPositionFromAngle(angle);\n    const center = size + 20;\n\n    return {\n      position: \"absolute\",\n      left: `${center + position.x}px`,\n      top: `${center + position.y}px`,\n      transform: \"translate(-50%, -50%)\",\n    };\n  }, [value, store, size]);\n\n  const onFocus = React.useCallback(\n    (event: React.FocusEvent<ThumbElement>) => {\n      props.onFocus?.(event);\n      if (event.defaultPrevented) return;\n\n      store.setState(\"valueIndexToChange\", index);\n    },\n    [props.onFocus, store, index],\n  );\n\n  const ThumbPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  if (value === undefined) return null;\n\n  return (\n    <span style={thumbStyle}>\n      <ThumbPrimitive\n        id={thumbId}\n        role=\"slider\"\n        aria-valuemin={min}\n        aria-valuenow={value}\n        aria-valuemax={max}\n        aria-orientation=\"vertical\"\n        data-disabled={disabled ? \"\" : undefined}\n        data-slot=\"angle-slider-thumb\"\n        tabIndex={disabled ? undefined : 0}\n        {...thumbProps}\n        ref={composedRef}\n        className={cn(\n          \"block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\",\n          className,\n        )}\n        onFocus={onFocus}\n      />\n      {isFormControl && value !== undefined && (\n        <VisuallyHiddenInput\n          key={index}\n          control={thumbElement}\n          name={\n            context.name\n              ? context.name + (values.length > 1 ? \"[]\" : \"\")\n              : undefined\n          }\n          form={context.form}\n          value={value.toString()}\n          type=\"number\"\n          min={min}\n          max={max}\n          step={step}\n          disabled={disabled}\n        />\n      )}\n    </span>\n  );\n}\n\ninterface AngleSliderValueProps extends DivProps {\n  unit?: string;\n  formatValue?: (value: number | number[]) => string;\n}\n\nfunction AngleSliderValue(props: AngleSliderValueProps) {\n  const {\n    unit = \"°\",\n    formatValue,\n    className,\n    style,\n    asChild,\n    children,\n    ...valueProps\n  } = props;\n\n  const values = useStore((state) => state.values);\n  const size = useStore((state) => state.size);\n  const disabled = useStore((state) => state.disabled);\n\n  const center = size + 20;\n\n  const displayValue = React.useMemo(() => {\n    if (formatValue) {\n      return formatValue(values.length === 1 ? (values[0] ?? 0) : values);\n    }\n\n    if (values.length === 1) {\n      return `${values[0] ?? 0}${unit}`;\n    }\n\n    const sortedValues = [...values].sort((a, b) => a - b);\n    return `${sortedValues[0]}${unit} - ${sortedValues[sortedValues.length - 1]}${unit}`;\n  }, [values, formatValue, unit]);\n\n  const valueStyle = React.useMemo<React.CSSProperties>(\n    () => ({\n      position: \"absolute\",\n      left: `${center}px`,\n      top: `${center}px`,\n      transform: \"translate(-50%, -50%)\",\n    }),\n    [center],\n  );\n\n  const ValuePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ValuePrimitive\n      data-disabled={disabled ? \"\" : undefined}\n      data-slot=\"angle-slider-value\"\n      {...valueProps}\n      className={cn(\n        \"pointer-events-none flex items-center justify-center text-sm font-medium text-foreground select-none\",\n        className,\n      )}\n      style={{\n        ...valueStyle,\n        ...style,\n      }}\n    >\n      {children ?? displayValue}\n    </ValuePrimitive>\n  );\n}\n\nexport {\n  AngleSlider,\n  type AngleSliderProps,\n  AngleSliderRange,\n  AngleSliderThumb,\n  AngleSliderTrack,\n  AngleSliderValue,\n  useStore as useAngleSlider,\n};\n",
      "target": ""
    },
    {
      "path": "components/visually-hidden-input.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype InputValue = string[] | string;\n\ninterface VisuallyHiddenInputProps<T = InputValue> extends Omit<\n  React.InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"checked\" | \"onReset\"\n> {\n  value?: T;\n  checked?: boolean;\n  control: HTMLElement | null;\n  bubbles?: boolean;\n}\n\nfunction VisuallyHiddenInput<T = InputValue>(\n  props: VisuallyHiddenInputProps<T>,\n) {\n  const {\n    control,\n    value,\n    checked,\n    bubbles = true,\n    type = \"hidden\",\n    style,\n    ...inputProps\n  } = props;\n\n  const isCheckInput = React.useMemo(\n    () => type === \"checkbox\" || type === \"radio\" || type === \"switch\",\n    [type],\n  );\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const prevValueRef = React.useRef<{\n    value: T | boolean | undefined;\n    previous: T | boolean | undefined;\n  }>({\n    value: isCheckInput ? checked : value,\n    previous: isCheckInput ? checked : value,\n  });\n\n  const prevValue = React.useMemo(() => {\n    const currentValue = isCheckInput ? checked : value;\n    if (prevValueRef.current.value !== currentValue) {\n      prevValueRef.current.previous = prevValueRef.current.value;\n      prevValueRef.current.value = currentValue;\n    }\n    return prevValueRef.current.previous;\n  }, [isCheckInput, value, checked]);\n\n  const [controlSize, setControlSize] = React.useState<{\n    width?: number;\n    height?: number;\n  }>({});\n\n  React.useLayoutEffect(() => {\n    if (!control) {\n      setControlSize({});\n      return;\n    }\n\n    setControlSize({\n      width: control.offsetWidth,\n      height: control.offsetHeight,\n    });\n\n    if (typeof window === \"undefined\") return;\n\n    const resizeObserver = new ResizeObserver((entries) => {\n      if (!Array.isArray(entries) || !entries.length) return;\n\n      const entry = entries[0];\n      if (!entry) return;\n\n      let width: number;\n      let height: number;\n\n      if (\"borderBoxSize\" in entry) {\n        const borderSizeEntry = entry.borderBoxSize;\n        const borderSize = Array.isArray(borderSizeEntry)\n          ? borderSizeEntry[0]\n          : borderSizeEntry;\n        width = borderSize.inlineSize;\n        height = borderSize.blockSize;\n      } else {\n        width = control.offsetWidth;\n        height = control.offsetHeight;\n      }\n\n      setControlSize({ width, height });\n    });\n\n    resizeObserver.observe(control, { box: \"border-box\" });\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [control]);\n\n  React.useEffect(() => {\n    const input = inputRef.current;\n    if (!input) return;\n\n    const inputProto = window.HTMLInputElement.prototype;\n    const propertyKey = isCheckInput ? \"checked\" : \"value\";\n    const eventType = isCheckInput ? \"click\" : \"input\";\n    const currentValue = isCheckInput ? checked : value;\n\n    const serializedCurrentValue = isCheckInput\n      ? checked\n      : typeof value === \"object\" && value !== null\n        ? JSON.stringify(value)\n        : value;\n\n    const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);\n\n    const setter = descriptor?.set;\n\n    if (prevValue !== currentValue && setter) {\n      const event = new Event(eventType, { bubbles });\n      setter.call(input, serializedCurrentValue);\n      input.dispatchEvent(event);\n    }\n  }, [prevValue, value, checked, bubbles, isCheckInput]);\n\n  const composedStyle = React.useMemo<React.CSSProperties>(() => {\n    return {\n      ...style,\n      ...(controlSize.width !== undefined && controlSize.height !== undefined\n        ? controlSize\n        : {}),\n      border: 0,\n      clip: \"rect(0 0 0 0)\",\n      clipPath: \"inset(50%)\",\n      height: \"1px\",\n      margin: \"-1px\",\n      overflow: \"hidden\",\n      padding: 0,\n      position: \"absolute\",\n      whiteSpace: \"nowrap\",\n      width: \"1px\",\n    };\n  }, [style, controlSize]);\n\n  return (\n    <input\n      type={type}\n      {...inputProps}\n      ref={inputRef}\n      aria-hidden={isCheckInput}\n      tabIndex={-1}\n      defaultChecked={isCheckInput ? checked : undefined}\n      style={composedStyle}\n    />\n  );\n}\n\nexport { VisuallyHiddenInput };\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}