UNPKG

@fseehawer/react-circular-slider

Version:

A customizable circular slider with no dependencies.

1 lines 67.2 kB
{"version":3,"sources":["../src/CircularSlider/index.tsx","../src/redux/reducer.ts","../src/hooks/useEventListener.ts","../src/hooks/useIsServer.ts","../src/Knob/index.tsx","../src/Labels/index.tsx","../src/Svg/index.tsx","../src/index.ts"],"sourcesContent":["\"use client\";\nimport React, {useCallback, useEffect, useReducer, useRef, forwardRef, useImperativeHandle} from 'react';\nimport reducer from '../redux/reducer';\nimport useEventListener from '../hooks/useEventListener';\nimport useIsServer from '../hooks/useIsServer';\nimport Knob from '../Knob';\nimport Labels from '../Labels';\nimport Svg from '../Svg';\n\nconst spreadDegrees = 360;\nconst knobOffsetConsts = {\n top: Math.PI / 2,\n right: 0,\n bottom: -Math.PI / 2,\n left: -Math.PI,\n} as const;\n\nexport type KnobPosition = keyof typeof knobOffsetConsts | number | string;\n\nexport type GradientStop = {\n offset?: string;\n stopColor: string;\n stopOpacity?: number;\n};\n\nexport interface CircularSliderProps {\n label?: string;\n width?: number;\n direction?: 1 | -1;\n min?: number;\n max?: number;\n initialValue?: number;\n value?: number;\n knobColor?: string;\n knobSize?: number;\n knobPosition?: KnobPosition;\n labelColor?: string;\n labelBottom?: boolean;\n labelFontSize?: string;\n valueFontSize?: string;\n appendToValue?: string;\n prependToValue?: string;\n verticalOffset?: string;\n hideLabelValue?: boolean;\n hideKnob?: boolean;\n hideKnobRing?: boolean;\n knobDraggable?: boolean;\n progressColorFrom?: string;\n progressColorTo?: string;\n progressGradient?: (string | GradientStop)[];\n useMouseAdditionalToTouch?: boolean;\n progressSize?: number;\n trackColor?: string;\n trackGradient?: (string | GradientStop)[];\n trackSize?: number;\n trackDraggable?: boolean;\n data?: (string | number)[];\n dataIndex?: number;\n progressLineCap?: 'round' | 'butt';\n renderLabelValue?: React.ReactNode;\n onChange?: (value: string | number) => void;\n isDragging?: (dragging: boolean) => void;\n children?: React.ReactNode;\n limitDragRange?: boolean;\n arcStart?: number;\n arcEnd?: number;\n}\n\n// Export the handle type for TypeScript users\nexport interface CircularSliderHandle {\n refresh: () => void;\n}\n\nconst getSliderRotation = (value: number) => (value < 0 ? -1 : 1);\nconst getRadians = (degrees: number) => (degrees * Math.PI) / 180;\nconst generateRange = (min: number, max: number) => Array.from({ length: max - min + 1 }, (_, i) => i + min);\nconst getKnobOffsetAmount = (knobPosition: KnobPosition): number => {\n if (typeof knobPosition === 'string' && knobPosition in knobOffsetConsts) {\n return knobOffsetConsts[knobPosition as keyof typeof knobOffsetConsts];\n }\n const parsed = typeof knobPosition === 'number' ? knobPosition : parseFloat(knobPosition);\n return getRadians(parsed);\n};\n\nconst CircularSlider = forwardRef<CircularSliderHandle, CircularSliderProps>((props, ref) => {\n const {\n label = 'ANGLE',\n width = 280,\n direction = 1,\n min = 0,\n max = 360,\n initialValue = 0,\n value = null,\n knobColor = '#4e63ea',\n knobSize = 36,\n knobPosition = 'top',\n labelColor = '#272b77',\n labelBottom = false,\n labelFontSize = '1rem',\n valueFontSize = '3rem',\n appendToValue = '',\n prependToValue = '',\n verticalOffset = '1.5rem',\n hideLabelValue = false,\n hideKnob = false,\n hideKnobRing = false,\n knobDraggable = true,\n progressColorFrom = '#80C3F3',\n progressColorTo = '#4990E2',\n progressGradient,\n useMouseAdditionalToTouch = false,\n progressSize = 8,\n trackColor = '#DDDEFB',\n trackGradient,\n trackSize = 8,\n trackDraggable = false,\n data = [],\n dataIndex = 0,\n progressLineCap = 'round',\n renderLabelValue = null,\n children,\n onChange = () => {},\n isDragging = () => {},\n limitDragRange = false,\n arcStart,\n arcEnd,\n } = props;\n\n const resizeObserverRef = useRef<ResizeObserver | null>(null);\n\n // Reference to track dragging state internally\n const draggingRef = useRef(false);\n\n // Prevent reset issues by tracking the current value position\n const currentPositionRef = useRef<{\n radians: number;\n label: string | number;\n knob: { x: number; y: number };\n dashOffset: number;\n } | null>(null);\n\n // Store the last data index to detect changes\n const lastDataIndexRef = useRef(dataIndex);\n\n // Store the last direction to detect changes\n const lastDirectionRef = useRef(direction);\n\n // Flag to track if a click operation is in progress\n const clickInProgressRef = useRef(false);\n\n // Flag to disable effects temporarily\n const disableEffectsRef = useRef(false);\n\n // Flag to prevent effect execution during specific operations\n const preventPositionResetRef = useRef(false);\n\n // Initialize state with proper data\n const dataArray = data.length > 0 ? [...data] : [...generateRange(min, max)];\n\n const initialState = {\n mounted: false,\n isDragging: false,\n width,\n radius: width / 2,\n knobOffset: getKnobOffsetAmount(knobPosition),\n label: initialValue,\n data: dataArray,\n radians: 0,\n offset: 0,\n knob: { x: 0, y: 0 },\n dashFullArray: 0,\n dashFullOffset: 0,\n };\n\n const isServer = useIsServer();\n const [state, dispatch] = useReducer(reducer, initialState);\n const circularSlider = useRef<HTMLDivElement | null>(null);\n const svgFullPath = useRef<SVGPathElement | null>(null);\n const touchSupported = !isServer && 'ontouchstart' in window;\n const useMouse = !touchSupported || (touchSupported && useMouseAdditionalToTouch);\n\n // Use ref for valueFromParent instead of state to prevent render cycles\n const valueFromParentRef = useRef<number | undefined>(undefined);\n const initCompletedRef = useRef(false);\n\n // Track if the component is currently mounted to prevent state updates after unmount\n const isMountedRef = useRef(false);\n\n // Track if the current position was set by dragging\n const positionSetByDragRef = useRef(false);\n\n // Add this to setKnobPosition to ensure the SVG path is correctly initialized at zero\n const setKnobPosition = useCallback((radians: number, fromDrag = false) => {\n // Skip updates if disabled\n if (disableEffectsRef.current && !fromDrag) return;\n\n // Track if this position was set by dragging\n if (fromDrag) {\n positionSetByDragRef.current = true;\n // Reset after a short delay\n setTimeout(() => {\n positionSetByDragRef.current = false;\n }, 300);\n }\n\n // Calculate position\n const radius = state.radius - trackSize / 2;\n const offsetRadians = radians + getKnobOffsetAmount(knobPosition);\n\n // Convert radians to degrees (0-360)\n let degrees = ((offsetRadians > 0 ? offsetRadians : (2 * Math.PI) + offsetRadians) * spreadDegrees) / (2 * Math.PI);\n\n // Apply direction\n degrees = getSliderRotation(direction) === -1 ? spreadDegrees - degrees : degrees;\n\n // Handle arc constraints if defined\n if (typeof arcStart === 'number' && typeof arcEnd === 'number') {\n const arcSpan = arcEnd - arcStart;\n const normalizedArcStart = (arcStart + 360) % 360;\n const normalizedArcEnd = (arcEnd + 360) % 360;\n \n // Normalize degrees to 0-360 range\n const normalizedDegrees = (degrees + 360) % 360;\n \n // Check if the arc crosses the 0/360 boundary\n const arcCrossesBoundary = normalizedArcEnd < normalizedArcStart;\n \n let constrainedDegrees = normalizedDegrees;\n \n if (!arcCrossesBoundary) {\n // Normal case: arc doesn't cross boundary\n if (normalizedDegrees < normalizedArcStart) {\n constrainedDegrees = normalizedArcStart;\n } else if (normalizedDegrees > normalizedArcEnd) {\n constrainedDegrees = normalizedArcEnd;\n }\n } else {\n // Arc crosses 0/360 boundary\n if (normalizedDegrees > normalizedArcEnd && normalizedDegrees < normalizedArcStart) {\n // Point is in the forbidden zone, snap to nearest boundary\n const distToStart = Math.min(Math.abs(normalizedDegrees - normalizedArcStart), 360 - Math.abs(normalizedDegrees - normalizedArcStart));\n const distToEnd = Math.min(Math.abs(normalizedDegrees - normalizedArcEnd), 360 - Math.abs(normalizedDegrees - normalizedArcEnd));\n constrainedDegrees = distToStart <= distToEnd ? normalizedArcStart : normalizedArcEnd;\n }\n }\n \n degrees = constrainedDegrees;\n \n // Recalculate radians based on constrained degrees\n const constrainedRadians = getRadians(degrees) - getKnobOffsetAmount(knobPosition);\n radians = constrainedRadians;\n }\n\n // Calculate dash offset and data index\n const dataArrayLength = state.data.length;\n const normalizedDegrees = (degrees + 360) % 360;\n let dashOffsetValue: number;\n let dataPointIndex: number;\n\n if (typeof arcStart === 'number' && typeof arcEnd === 'number') {\n // Arc mode: map position within the arc to data and dash\n const arcSpan = ((arcEnd - arcStart) + 360) % 360;\n const normArcStart = (arcStart + 360) % 360;\n const normArcEnd = (arcEnd + 360) % 360;\n const arcCrosses = normArcEnd < normArcStart;\n\n let degreesInArc: number;\n if (arcCrosses) {\n degreesInArc = normalizedDegrees >= normArcStart\n ? normalizedDegrees - normArcStart\n : (360 - normArcStart) + normalizedDegrees;\n } else {\n degreesInArc = normalizedDegrees - normArcStart;\n }\n\n const arcProgress = Math.max(0, Math.min(1, degreesInArc / arcSpan));\n dataPointIndex = Math.round(arcProgress * (dataArrayLength - 1));\n dashOffsetValue = state.dashFullArray - arcProgress * state.dashFullArray;\n } else {\n // Full circle mode\n const dashOffset = (degrees / spreadDegrees) * state.dashFullArray;\n dashOffsetValue = state.dashFullArray - dashOffset;\n dataPointIndex = Math.round((normalizedDegrees / 360) * (dataArrayLength - 1));\n }\n\n // Ensure the index is within bounds\n const safeIndex = Math.min(Math.max(0, dataPointIndex), dataArrayLength - 1);\n const labelValue = state.data[safeIndex];\n\n // Calculate the knob x,y position\n const knobXY = {\n x: radius * Math.cos(radians) + radius,\n y: radius * Math.sin(radians) + radius,\n };\n\n // Handle special case for zero value\n // Only apply the special case if we're at the true zero and not in the\n // middle of a drag operation\n const isZeroValue = (\n (!fromDrag && dataIndex === 0) ||\n (!fromDrag && safeIndex === 0 && dataArrayLength > 1) ||\n (typeof labelValue === 'number' && labelValue === 0 && !fromDrag) ||\n (typeof labelValue === 'string' && labelValue === '0' && !fromDrag)\n );\n\n // If it's a true zero value (not during dragging), set dashFullOffset appropriately\n const finalDashOffset = isZeroValue ? state.dashFullArray : dashOffsetValue;\n\n // Save this position as the current position\n currentPositionRef.current = {\n radians,\n label: labelValue,\n knob: knobXY,\n dashOffset: finalDashOffset\n };\n\n // Trigger onChange if needed and not in initial setup\n if (labelValue !== state.label && initCompletedRef.current) {\n onChange(labelValue);\n }\n\n // Update state with new position\n dispatch({\n type: 'setKnobPosition',\n payload: {\n dashFullOffset: finalDashOffset,\n label: labelValue,\n knob: knobXY,\n },\n });\n }, [state, trackSize, knobPosition, direction, onChange, dataIndex, arcStart, arcEnd]);\n\n // Position the dataIndex to the correct position in the circle\n const positionForDataIndex = useCallback(() => {\n if (!state.mounted || state.data.length === 0) return;\n\n // Don't reposition if this position was set by dragging\n if (positionSetByDragRef.current) return;\n\n // Don't reposition if we're trying to prevent reset\n if (preventPositionResetRef.current) return;\n\n // Ensure dataIndex is within bounds\n const dataIndexSafe = Math.min(Math.max(0, dataIndex), state.data.length - 1);\n\n let radians: number;\n\n if (typeof arcStart === 'number' && typeof arcEnd === 'number') {\n // Arc mode: map dataIndex to the arc span instead of full 360°\n const arcSpan = ((arcEnd - arcStart) + 360) % 360;\n const progress = dataIndexSafe / Math.max(state.data.length - 1, 1);\n const targetDegrees = ((arcStart + progress * arcSpan) + 360) % 360;\n // Convert to internal radians (setKnobPosition will add knobOffset back)\n radians = getRadians(targetDegrees) - state.knobOffset;\n } else {\n // Full circle: map dataIndex to 0-360°\n const degrees = (dataIndexSafe / (state.data.length - 1)) * 360 * getSliderRotation(direction);\n radians = getRadians(degrees) - state.knobOffset;\n }\n\n // Apply the position\n setKnobPosition(radians);\n\n // Update last known data index\n lastDataIndexRef.current = dataIndex;\n }, [dataIndex, state.mounted, state.data.length, state.knobOffset, direction, setKnobPosition, arcStart, arcEnd]);\n\n const onMouseDown = (event: React.MouseEvent | React.TouchEvent) => {\n // Prevent clicking during ongoing drag operation\n if (clickInProgressRef.current) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n\n // Set flags to track dragging state\n draggingRef.current = true;\n disableEffectsRef.current = true;\n clickInProgressRef.current = true;\n preventPositionResetRef.current = true;\n\n // Notify parent\n isDragging(true);\n\n // Update component state\n dispatch({ type: 'onMouseDown', payload: { isDragging: true } });\n\n // Reset click flag after a short delay\n setTimeout(() => {\n clickInProgressRef.current = false;\n }, 100);\n };\n\n const onMouseUp = () => {\n // Only process if we're actually dragging\n if (state.isDragging) {\n // Update dragging state\n isDragging(false);\n draggingRef.current = false;\n\n // Update component state\n dispatch({ type: 'onMouseUp', payload: { isDragging: false } });\n\n // Keep prevention active for a bit longer to avoid reset on release\n setTimeout(() => {\n if (isMountedRef.current) {\n disableEffectsRef.current = false;\n\n // Keep preventing position reset for longer\n setTimeout(() => {\n if (isMountedRef.current) {\n preventPositionResetRef.current = false;\n }\n }, 250);\n }\n }, 50);\n }\n };\n\n const onKeyDown = useCallback((event: KeyboardEvent) => {\n if (!knobDraggable && !trackDraggable) return;\n \n const step = state.data.length > 1 ? 1 : (max - min) / 100;\n let newIndex = dataIndex;\n \n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n event.preventDefault();\n newIndex = Math.min(dataIndex + step, state.data.length - 1);\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n event.preventDefault();\n newIndex = Math.max(dataIndex - step, 0);\n break;\n case 'Home':\n event.preventDefault();\n newIndex = 0;\n break;\n case 'End':\n event.preventDefault();\n newIndex = state.data.length - 1;\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n return;\n default:\n return;\n }\n \n if (newIndex !== dataIndex) {\n const degrees = (newIndex / (state.data.length - 1)) * 360 * getSliderRotation(direction);\n const radians = getRadians(degrees) - state.knobOffset;\n setKnobPosition(radians);\n }\n }, [knobDraggable, trackDraggable, state.data.length, dataIndex, max, min, direction, state.knobOffset, setKnobPosition]);\n\n const onMouseMove = useCallback((event: MouseEvent | TouchEvent) => {\n if (!state.isDragging || (!knobDraggable && !trackDraggable) || (event.type === 'mousemove' && !useMouse)) return;\n\n // Prevent default to avoid browser behaviors\n event.preventDefault();\n\n // Get mouse/touch position\n const touch = (event as TouchEvent).type === 'touchmove' ? (event as TouchEvent).changedTouches[0] : null;\n const getOffset = (ref: React.RefObject<HTMLElement | null>): { top: number; left: number } => {\n const element = ref.current;\n if (!element) {\n return { top: 0, left: 0 };\n }\n const rect: DOMRect = element.getBoundingClientRect();\n const scrollLeft = window.pageXOffset ?? document.documentElement.scrollLeft ?? 0;\n const scrollTop = window.pageYOffset ?? document.documentElement.scrollTop ?? 0;\n return {\n top: rect.top + scrollTop,\n left: rect.left + scrollLeft,\n };\n };\n\n // Calculate mouse position relative to component center\n const offset = getOffset(circularSlider);\n const pageX = touch ? touch.pageX : (event as MouseEvent).pageX;\n const pageY = touch ? touch.pageY : (event as MouseEvent).pageY;\n const mouseX = pageX - (offset.left + state.radius);\n const mouseY = pageY - (offset.top + state.radius);\n\n // Convert to radians\n let radians = Math.atan2(mouseY, mouseX);\n\n // If limitDragRange is enabled, constrain the radians to prevent multiple rounds\n if (limitDragRange) {\n // Normalize radians to 0 to 2π range\n let normalizedRadians = radians;\n while (normalizedRadians < 0) normalizedRadians += 2 * Math.PI;\n while (normalizedRadians >= 2 * Math.PI) normalizedRadians -= 2 * Math.PI;\n \n // Calculate the target data index based on the angle\n const degrees = (normalizedRadians * 180) / Math.PI;\n const adjustedDegrees = getSliderRotation(direction) === -1 ? 360 - degrees : degrees;\n const normalizedDegrees = (adjustedDegrees + 360) % 360;\n const dataArrayLength = state.data.length;\n const targetIndex = Math.round((normalizedDegrees / 360) * (dataArrayLength - 1));\n \n // Clamp the target index to valid range\n const clampedIndex = Math.min(Math.max(0, targetIndex), dataArrayLength - 1);\n \n // Convert back to radians for the clamped position\n const clampedDegrees = (clampedIndex / (dataArrayLength - 1)) * 360;\n const adjustedClampedDegrees = getSliderRotation(direction) === -1 ? 360 - clampedDegrees : clampedDegrees;\n radians = (adjustedClampedDegrees * Math.PI) / 180 - getKnobOffsetAmount(knobPosition);\n }\n\n // Apply the new position, specifying it comes from drag\n setKnobPosition(radians, true);\n }, [state.isDragging, state.radius, knobDraggable, trackDraggable, useMouse, setKnobPosition, limitDragRange, state.data.length, direction, knobPosition]);\n\n // Function to recalculate and update values when resized\n const refresh = useCallback(() => {\n if (!circularSlider.current || !svgFullPath.current || !isMountedRef.current) return;\n\n // Don't refresh during dragging\n if (draggingRef.current) return;\n\n // Get new measurements\n const newWidth = width;\n const newRadius = newWidth / 2;\n\n // Update state with new dimensions\n dispatch({\n type: 'updateDimensions',\n payload: {\n width: newWidth,\n radius: newRadius,\n dashFullArray: svgFullPath.current?.getTotalLength() || 0,\n },\n });\n\n // Reposition the knob based on the last position\n if (currentPositionRef.current) {\n // Small timeout to let the dimensions update first\n setTimeout(() => {\n if (!draggingRef.current && currentPositionRef.current && isMountedRef.current) {\n disableEffectsRef.current = true;\n\n // Use the current position's radians to preserve exact position\n setKnobPosition(currentPositionRef.current?.radians ?? 0);\n\n // Re-enable effects after a short delay\n setTimeout(() => {\n if (isMountedRef.current) {\n disableEffectsRef.current = false;\n }\n }, 50);\n }\n }, 10);\n }\n }, [width, setKnobPosition]);\n\n // Expose the refresh method via ref\n useImperativeHandle(ref, () => ({\n refresh,\n }));\n\n // Initialize the component\n useEffect(() => {\n isMountedRef.current = true;\n disableEffectsRef.current = true;\n\n dispatch({\n type: 'init',\n payload: {\n mounted: true,\n dashFullArray: svgFullPath.current?.getTotalLength?.() ?? 0,\n },\n });\n\n // Re-enable effects after initialization\n setTimeout(() => {\n if (isMountedRef.current) {\n disableEffectsRef.current = false;\n }\n }, 100);\n\n // Cleanup on unmount\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n // Setup initial position\n useEffect(() => {\n if (!state.mounted || state.dashFullArray === 0 || disableEffectsRef.current || initCompletedRef.current) return;\n\n // Initialize with offset configuration\n dispatch({\n type: 'setInitialKnobPosition',\n payload: {\n radians: Math.PI / 2 - state.knobOffset,\n offset: 0,\n },\n });\n\n // Initialize with correct data index position\n positionForDataIndex();\n\n // Mark initialization as complete\n initCompletedRef.current = true;\n }, [state.mounted, state.dashFullArray, state.knobOffset, positionForDataIndex]);\n\n // Handle dataIndex changes after initialization\n useEffect(() => {\n if (!state.mounted || !initCompletedRef.current || disableEffectsRef.current || draggingRef.current) return;\n\n // Only update if the dataIndex actually changed\n if (dataIndex !== lastDataIndexRef.current) {\n // Update position when dataIndex changes\n positionForDataIndex();\n }\n }, [dataIndex, positionForDataIndex, state.mounted]);\n\n // Handle direction changes after initialization\n useEffect(() => {\n if (!state.mounted || !initCompletedRef.current || disableEffectsRef.current || draggingRef.current) return;\n\n if (direction !== lastDirectionRef.current) {\n lastDirectionRef.current = direction;\n positionForDataIndex();\n }\n }, [direction, positionForDataIndex, state.mounted]);\n\n // Handle external value prop changes\n useEffect(() => {\n if (!state.mounted || disableEffectsRef.current || draggingRef.current || preventPositionResetRef.current) return;\n\n if (typeof value === 'number' && value !== valueFromParentRef.current) {\n valueFromParentRef.current = value;\n const radians = getRadians(value);\n const offsetRadians = -state.knobOffset + radians * getSliderRotation(direction);\n\n // Use a small delay to break the update cycle\n setTimeout(() => {\n if (!draggingRef.current && isMountedRef.current && !preventPositionResetRef.current) {\n setKnobPosition(offsetRadians);\n }\n }, 0);\n }\n }, [direction, state.knobOffset, value, state.mounted, setKnobPosition]);\n\n // Setup ResizeObserver to watch for container size changes\n useEffect(() => {\n if (typeof ResizeObserver === 'undefined') return;\n\n const observeResize = () => {\n if (circularSlider.current) {\n resizeObserverRef.current = new ResizeObserver(() => {\n if (!draggingRef.current && isMountedRef.current) {\n refresh();\n }\n });\n resizeObserverRef.current?.observe(circularSlider.current);\n }\n };\n\n observeResize();\n\n return () => {\n if (resizeObserverRef.current) {\n resizeObserverRef.current?.disconnect();\n }\n };\n }, [refresh]);\n\n // Window resize fallback\n useEffect(() => {\n const handleResize = () => {\n if (!draggingRef.current && isMountedRef.current) {\n refresh();\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [refresh]);\n\n useEventListener('touchend', onMouseUp);\n useEventListener('mouseup', onMouseUp);\n useEventListener('touchmove', onMouseMove);\n useEventListener('mousemove', onMouseMove);\n useEventListener('keydown', onKeyDown);\n\n const sanitizedLabel = label.replace(/[^a-zA-Z0-9-_]/g, '_');\n const sliderStyle: React.CSSProperties = {\n position: 'relative',\n display: 'inline-block',\n width: 'max-content',\n opacity: state.mounted ? 1 : 0,\n transition: 'opacity 1s ease-in',\n };\n\n // Prepare display value from either parent prop or internal state\n const displayValue = typeof valueFromParentRef.current !== 'undefined'\n ? `${valueFromParentRef.current}`\n : `${state.label}`;\n\n return (\n <div \n style={sliderStyle} \n ref={circularSlider}\n role=\"slider\"\n aria-label={`${label}: ${displayValue}`}\n aria-valuemin={data.length > 0 ? 0 : min}\n aria-valuemax={data.length > 0 ? data.length - 1 : max}\n aria-valuenow={data.length > 0 ? dataIndex : parseFloat(displayValue)}\n aria-valuetext={`${prependToValue}${displayValue}${appendToValue}`}\n tabIndex={0}\n >\n <Svg\n width={width}\n label={sanitizedLabel}\n direction={direction}\n strokeDasharray={state.dashFullArray}\n strokeDashoffset={state.dashFullOffset}\n svgFullPath={svgFullPath}\n progressSize={progressSize}\n progressColorFrom={progressColorFrom}\n progressColorTo={progressColorTo}\n progressGradient={progressGradient}\n progressLineCap={progressLineCap as 'round' | 'butt'}\n trackColor={trackColor}\n trackGradient={trackGradient}\n trackSize={trackSize}\n radiansOffset={state.radians}\n onMouseDown={trackDraggable ? onMouseDown : undefined}\n isDragging={state.isDragging}\n arcStart={arcStart}\n arcEnd={arcEnd}\n />\n <Knob\n isDragging={state.isDragging}\n knobPosition={state.knob}\n knobSize={knobSize}\n knobColor={knobColor}\n trackSize={trackSize}\n hideKnob={hideKnob}\n hideKnobRing={hideKnobRing}\n knobDraggable={knobDraggable}\n onMouseDown={onMouseDown}\n >\n {children}\n </Knob>\n {renderLabelValue ?? (\n <Labels\n label={label}\n labelColor={labelColor}\n labelBottom={labelBottom}\n labelFontSize={labelFontSize}\n verticalOffset={verticalOffset}\n valueFontSize={valueFontSize}\n appendToValue={appendToValue}\n prependToValue={prependToValue}\n hideLabelValue={hideLabelValue}\n value={displayValue}\n />\n )}\n </div>\n );\n});\n\nexport default CircularSlider;","// reducer.ts\nexport type ActionType =\n | { type: 'init'; payload: Partial<State> }\n | { type: 'setKnobPosition'; payload: Partial<State> }\n | { type: 'onMouseDown'; payload: Partial<State> }\n | { type: 'onMouseUp'; payload: Partial<State> }\n | { type: 'setInitialKnobPosition'; payload: Partial<State> }\n | { type: 'updateDimensions'; payload: Partial<State> };\n\nexport interface State {\n mounted: boolean;\n isDragging: boolean;\n width: number;\n radius: number;\n knobOffset: number;\n label: string | number;\n data: (string | number)[];\n radians: number;\n offset: number;\n knob: {\n x: number;\n y: number;\n };\n dashFullArray: number;\n dashFullOffset: number;\n}\n\nconst reducer = (state: State, action: ActionType): State => {\n switch (action.type) {\n case 'init':\n case 'setKnobPosition':\n case 'onMouseDown':\n case 'onMouseUp':\n case 'setInitialKnobPosition':\n case 'updateDimensions':\n return {\n ...state,\n ...action.payload,\n };\n default:\n throw new Error('Unknown action type');\n }\n};\n\nexport default reducer;","import { useEffect, useRef } from 'react';\n\nfunction useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void\n): void {\n const savedHandler = useRef<((event: WindowEventMap[K]) => void) | undefined>(undefined);\n\n useEffect(() => {\n savedHandler.current = handler;\n }, [handler]);\n\n useEffect(() => {\n if (typeof window !== 'undefined' && window.addEventListener) {\n const eventListener = (event: WindowEventMap[K]) => {\n if (savedHandler.current) savedHandler.current(event);\n };\n window.addEventListener(eventName, eventListener, { passive: false });\n return () => {\n window.removeEventListener(eventName, eventListener);\n };\n }\n }, [eventName]);\n}\n\nexport default useEventListener;\n","import { useEffect, useState } from 'react';\n\nconst useIsServer = (): boolean => {\n const [isServer, setIsServer] = useState(true);\n\n useEffect(() => {\n setIsServer(false);\n }, []);\n\n return isServer;\n};\n\nexport default useIsServer;","import React from 'react';\nimport './index.css';\n\nexport interface KnobProps {\n\tisDragging: boolean;\n\tknobPosition: { x: number; y: number };\n\tknobColor: string;\n\tknobSize: number;\n\thideKnob?: boolean;\n\thideKnobRing?: boolean;\n\tknobDraggable?: boolean;\n\ttrackSize: number;\n\tchildren?: React.ReactNode;\n\tonMouseDown?: (event: React.MouseEvent | React.TouchEvent) => void;\n}\n\nconst Knob: React.FC<KnobProps> = ({\n isDragging,\n knobPosition,\n knobColor,\n knobSize,\n hideKnob = false,\n hideKnobRing = false,\n knobDraggable = true,\n trackSize,\n children,\n onMouseDown,\n\t\t\t\t\t\t\t\t }) => {\n\tconst styles: { [key: string]: React.CSSProperties } = {\n\t\tknob: {\n\t\t\tposition: 'absolute',\n\t\t\tleft: `-${knobSize / 2 - trackSize / 2}px`,\n\t\t\ttop: `-${knobSize / 2 - trackSize / 2}px`,\n\t\t\tcursor: knobDraggable ? 'grab' : 'auto',\n\t\t\tzIndex: 3,\n\t\t},\n\t\tdragging: {\n\t\t\tcursor: 'grabbing',\n\t\t},\n\t\tpause: {\n\t\t\tanimationPlayState: 'paused',\n\t\t},\n\t\tanimation: {\n\t\t\ttransformOrigin: '50% 50%',\n\t\t\tanimationTimingFunction: 'ease-out',\n\t\t\tanimationDuration: '1500ms',\n\t\t\tanimationIterationCount: 'infinite',\n\t\t\tanimationName: 'pulse',\n\t\t},\n\t\thide: {\n\t\t\topacity: 0,\n\t\t},\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\ttransform: `translate(${knobPosition.x}px, ${knobPosition.y}px)`,\n\t\t\t\t...styles.knob,\n\t\t\t\t...(isDragging ? styles.dragging : {}),\n\t\t\t\t...(hideKnob ? styles.hide : {}),\n\t\t\t}}\n\t\t\tonMouseDown={onMouseDown}\n\t\t\tonTouchStart={onMouseDown}\n\t\t>\n\t\t\t<svg width={knobSize} height={knobSize} viewBox={`0 0 ${knobSize} ${knobSize}`}>\n\t\t\t\t{!hideKnobRing && (\n\t\t\t\t\t<circle\n\t\t\t\t\t\tstyle={{ ...styles.animation, ...(isDragging ? styles.pause : {}) }}\n\t\t\t\t\t\tfill={knobColor}\n\t\t\t\t\t\tfillOpacity={0.2}\n\t\t\t\t\t\tstroke='none'\n\t\t\t\t\t\tcx={knobSize / 2}\n\t\t\t\t\t\tcy={knobSize / 2}\n\t\t\t\t\t\tr={knobSize / 2}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t<circle\n\t\t\t\t\tfill={knobColor}\n\t\t\t\t\tstroke='none'\n\t\t\t\t\tcx={knobSize / 2}\n\t\t\t\t\tcy={knobSize / 2}\n\t\t\t\t\tr={(knobSize * 2) / 3 / 2}\n\t\t\t\t/>\n\t\t\t\t{children ?? (\n\t\t\t\t\t<svg width={knobSize} height={knobSize} viewBox='0 0 36 36'>\n\t\t\t\t\t\t<rect fill='#FFFFFF' x='14' y='14' width='8' height='1' />\n\t\t\t\t\t\t<rect fill='#FFFFFF' x='14' y='17' width='8' height='1' />\n\t\t\t\t\t\t<rect fill='#FFFFFF' x='14' y='20' width='8' height='1' />\n\t\t\t\t\t</svg>\n\t\t\t\t)}\n\t\t\t</svg>\n\t\t</div>\n\t);\n};\n\nexport default Knob;","import React from 'react';\n\nexport interface LabelsProps {\n\tlabel: string;\n\tvalue: string;\n\tlabelColor?: string;\n\tlabelBottom?: boolean;\n\tlabelFontSize?: string;\n\tvalueFontSize?: string;\n\tappendToValue?: string;\n\tprependToValue?: string;\n\tverticalOffset?: string;\n\thideLabelValue?: boolean;\n}\n\nconst Labels: React.FC<LabelsProps> = ({\n label,\n value,\n labelColor = '#000',\n labelBottom = false,\n labelFontSize = '1rem',\n valueFontSize = '3rem',\n appendToValue = '',\n prependToValue = '',\n verticalOffset = '1.5rem',\n hideLabelValue = false,\n}) => {\n\tconst styles: { [key: string]: React.CSSProperties } = {\n\t\tlabels: {\n\t\t\tposition: 'absolute',\n\t\t\ttop: '0',\n\t\t\tleft: '0',\n\t\t\twidth: '100%',\n\t\t\theight: '100%',\n\t\t\tdisplay: 'flex',\n\t\t\tflexDirection: 'column',\n\t\t\tjustifyContent: 'center',\n\t\t\talignItems: 'center',\n\t\t\tcolor: labelColor,\n\t\t\tuserSelect: 'none',\n\t\t\tzIndex: 1,\n\t\t},\n\t\tcodeTop: {\n\t\t\tverticalAlign: 'text-top',\n\t\t},\n\t\tvalue: {\n\t\t\tfontSize: valueFontSize,\n\t\t\tposition: 'relative',\n\t\t},\n\t\tbottomMargin: {\n\t\t\tmarginBottom: `calc(${verticalOffset})`,\n\t\t},\n\t\tappended: {\n\t\t\tposition: 'absolute',\n\t\t\tright: '0',\n\t\t\ttop: '0',\n\t\t\ttransform: 'translate(100%, 0)',\n\t\t},\n\t\tprepended: {\n\t\t\tposition: 'absolute',\n\t\t\tleft: '0',\n\t\t\ttop: '0',\n\t\t\ttransform: 'translate(-100%, 0)',\n\t\t},\n\t\thide: {\n\t\t\tdisplay: 'none',\n\t\t},\n\t};\n\n\treturn (\n\t\t<div style={{ ...styles.labels, ...(hideLabelValue ? styles.hide : {}) }}>\n\t\t\t{!labelBottom && <div style={{ fontSize: labelFontSize }}>{label}</div>}\n\t\t\t<div style={{ ...styles.value, ...(!labelBottom ? styles.bottomMargin : {}) }}>\n\t\t\t\t<code style={styles.codeTop}>\n\t\t\t\t\t<span style={styles.prepended}>{prependToValue}</span>\n\t\t\t\t\t{value}\n\t\t\t\t\t<span style={styles.appended}>{appendToValue}</span>\n\t\t\t\t</code>\n\t\t\t</div>\n\t\t\t{labelBottom && <div style={{ fontSize: labelFontSize, marginTop: '-0.5rem' }}>{label}</div>}\n\t\t</div>\n\t);\n};\n\nexport default Labels;\n","\"use client\";\nimport React, { useRef, useMemo } from 'react';\n\nexport type GradientStop = {\n offset?: string;\n stopColor: string;\n stopOpacity?: number;\n};\n\nexport interface SvgProps {\n width: number;\n label: string;\n direction: number;\n strokeDasharray: number;\n strokeDashoffset: number;\n progressColorFrom: string;\n progressColorTo: string;\n progressGradient?: (string | GradientStop)[];\n progressLineCap?: 'round' | 'butt';\n progressSize: number;\n trackColor: string;\n trackGradient?: (string | GradientStop)[];\n trackSize: number;\n radiansOffset: number;\n svgFullPath: React.RefObject<SVGPathElement | null>;\n onMouseDown?: (event: React.MouseEvent | React.TouchEvent) => void;\n isDragging?: boolean;\n arcStart?: number;\n arcEnd?: number;\n}\n\nconst Svg: React.FC<SvgProps> = ({\n width,\n label,\n direction,\n strokeDasharray,\n strokeDashoffset,\n progressColorFrom,\n progressColorTo,\n progressGradient,\n progressLineCap = 'round',\n progressSize,\n trackColor,\n trackGradient,\n trackSize,\n radiansOffset,\n svgFullPath,\n onMouseDown,\n isDragging,\n arcStart,\n arcEnd,\n }) => {\n const circleRef = useRef<SVGCircleElement | null>(null);\n\n const halfTrack = trackSize / 2;\n const radius = width / 2 - halfTrack;\n\n // Calculate arc path if arcStart and arcEnd are defined\n const isArcMode = typeof arcStart === 'number' && typeof arcEnd === 'number';\n\n // Keep styles simple with no transitions\n const styles: { [key: string]: React.CSSProperties } = {\n svg: {\n position: 'relative',\n zIndex: 2,\n userSelect: isDragging ? 'none' : 'auto',\n },\n path: {\n transform: isArcMode\n ? `${direction === -1 ? 'scale(-1, 1)' : 'scale(1, 1)'}`\n : `rotate(${radiansOffset}rad) ${direction === -1 ? 'scale(-1, 1)' : 'scale(1, 1)'}`,\n transformOrigin: 'center center',\n // No transition to make movement instant\n transition: 'none',\n },\n };\n\n const validatedLineCap: 'round' | 'butt' =\n progressLineCap === 'round' || progressLineCap === 'butt'\n ? progressLineCap\n : 'round';\n\n // Calculate arc path if arcStart and arcEnd are defined\n let trackPath = '';\n let progressPath = '';\n \n if (isArcMode) {\n const startAngle = (arcStart - 90) * Math.PI / 180; // Convert to radians, offset by -90° to start at top\n const endAngle = (arcEnd - 90) * Math.PI / 180;\n \n const startX = width / 2 + radius * Math.cos(startAngle);\n const startY = width / 2 + radius * Math.sin(startAngle);\n const endX = width / 2 + radius * Math.cos(endAngle);\n const endY = width / 2 + radius * Math.sin(endAngle);\n \n const arcSpan = ((arcEnd - arcStart) + 360) % 360;\n const largeArc = arcSpan > 180 ? 1 : 0;\n \n trackPath = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArc} 1 ${endX} ${endY}`;\n progressPath = trackPath;\n } else {\n // Full circle path for non-arc mode\n trackPath = `\n M ${width / 2}, ${width / 2}\n m 0, -${radius}\n a ${radius},${radius} 0 0,1 0,${radius * 2}\n a -${radius},-${radius} 0 0,1 0,-${radius * 2}\n `;\n progressPath = trackPath;\n }\n\n const handleClick = (event: React.MouseEvent | React.TouchEvent) => {\n if (!onMouseDown) return;\n const bounds = circleRef.current?.getBoundingClientRect();\n if (!bounds) return;\n\n const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;\n const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;\n const centerX = bounds.left + bounds.width / 2;\n const centerY = bounds.top + bounds.height / 2;\n const distance = Math.sqrt((clientX - centerX) ** 2 + (clientY - centerY) ** 2);\n const threshold = bounds.width / (isDragging ? 4 : 2) - trackSize;\n if (distance < threshold) return;\n onMouseDown(event);\n };\n\n // Create a stable unique gradient ID to avoid conflicts with multiple instances\n // Using useRef so the ID is generated once and remains stable across re-renders\n const gradientIdRef = useRef(`radial-${label}-${Math.random().toString(36).substr(2, 9)}`);\n const gradientId = gradientIdRef.current;\n const trackGradientId = `track-${gradientId}`;\n const progressGradientId = `progress-${gradientId}`;\n\n // Helper function to create color stops\n const createColorStops = (colors: (string | GradientStop)[]) => {\n return colors.map((color, index) => {\n let stopProps: GradientStop;\n \n if (typeof color === 'string') {\n stopProps = { stopColor: color };\n } else {\n stopProps = color;\n }\n \n let { offset, stopColor, stopOpacity } = stopProps;\n \n // Auto-calculate offset if not provided\n if (!offset) {\n if (index === 0) {\n offset = '0%';\n } else if (index === colors.length - 1) {\n offset = '100%';\n } else {\n offset = `${(100 / (colors.length - 1)) * index}%`;\n }\n }\n \n return (\n <stop\n key={index}\n offset={offset}\n stopColor={stopColor}\n stopOpacity={stopOpacity ?? 1}\n />\n );\n });\n };\n\n // Memoize gradient definitions to avoid unnecessary recalculations\n const gradientDefs = useMemo(() => {\n const defs = [];\n \n if (trackGradient && trackGradient.length > 0) {\n defs.push(\n <linearGradient key=\"track\" id={trackGradientId} x1=\"100%\" x2=\"0%\">\n {createColorStops(trackGradient)}\n </linearGradient>\n );\n }\n \n if (progressGradient && progressGradient.length > 0) {\n defs.push(\n <linearGradient key=\"progress\" id={progressGradientId} x1=\"100%\" x2=\"0%\">\n {createColorStops(progressGradient)}\n </linearGradient>\n );\n } else {\n // Default gradient for backward compatibility\n defs.push(\n <linearGradient key=\"progress-default\" id={gradientId} x1=\"100%\" x2=\"0%\">\n <stop offset=\"0%\" stopColor={progressColorFrom}/>\n <stop offset=\"100%\" stopColor={progressColorTo}/>\n </linearGradient>\n );\n }\n \n return defs;\n }, [trackGradient, progressGradient, trackGradientId, progressGradientId, gradientId, progressColorFrom, progressColorTo]);\n\n // Determine stroke colors\n const actualTrackStroke = trackGradient && trackGradient.length > 0 \n ? `url(#${trackGradientId})` \n : trackColor;\n \n const actualProgressStroke = progressGradient && progressGradient.length > 0 \n ? `url(#${progressGradientId})` \n : `url(#${gradientId})`;\n\n return (\n <svg\n width={`${width}px`}\n height={`${width}px`}\n viewBox={`0 0 ${width} ${width}`}\n overflow=\"visible\"\n style={styles.svg}\n onMouseDown={handleClick}\n onTouchStart={handleClick}\n >\n <defs>\n {gradientDefs}\n </defs>\n {isArcMode ? (\n <path\n ref={circleRef}\n strokeWidth={trackSize}\n fill=\"none\"\n stroke={actualTrackStroke}\n strokeLinecap={validatedLineCap}\n d={trackPath}\n />\n ) : (\n <circle\n ref={circleRef}\n strokeWidth={trackSize}\n fill=\"none\"\n stroke={actualTrackStroke}\n cx={width / 2}\n cy={width / 2}\n r={radius}\n />\n )}\n <path\n style={styles.path}\n ref={svgFullPath}\n strokeDasharray={strokeDasharray || 0}\n strokeDashoffset={strokeDashoffset || 0}\n strokeWidth={progressSize}\n strokeLinecap={validatedLineCap}\n fill=\"none\"\n stroke={actualProgressStroke}\n d={progressPath}\n />\n </svg>\n );\n};\n\nexport default Svg;","\"use client\"\n\nimport CircularSlider from './CircularSlider';\n\nexport default CircularSlider;\n"],"mappings":"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