{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "parameter-slider",
  "title": "Parameter Slider",
  "description": "A labelled slider with a numeric input, unit, marks, reset and a linear or log scale, for gain trims, delays, thresholds and frequencies.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "@audiocn/core",
    "@audiocn/use-audio-config"
  ],
  "files": [
    {
      "path": "components/ui/parameter-slider.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { NumberField as NumberFieldPrimitive } from \"@base-ui/react/number-field\";\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport type { ComponentProps, CSSProperties, KeyboardEvent } from \"react\";\n\nimport { useAudioConfig } from \"@/hooks/use-audio-config\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { linearTaper, logTaper } from \"@/lib/audio/taper\";\nimport type { Taper } from \"@/lib/audio/types\";\nimport { cn } from \"@/lib/utils\";\n\nconst POSITION_STEP = 0.0005;\nconst PRECISION = 1e6;\n\nexport type ParameterChangeReason = \"drag\" | \"keyboard\" | \"input\" | \"reset\";\n\nexport interface ParameterChangeDetails {\n  reason: ParameterChangeReason;\n  event?: Event;\n}\n\nexport interface ParameterMark {\n  value: number;\n  label?: string;\n}\n\ninterface ParameterSliderContextValue {\n  value: number;\n  min: number;\n  max: number;\n  step: number;\n  largeStep: number;\n  unit: string | undefined;\n  decimals: number;\n  resetValue: number;\n  position: number;\n  originPosition: number;\n  taper: Taper;\n  disabled: boolean;\n  marks: ParameterMark[] | undefined;\n  labelId: string;\n  descriptionId: string;\n  format: (value: number) => string;\n  change: (value: number, details: ParameterChangeDetails) => void;\n  commit: (value: number) => void;\n  commitLatest: () => void;\n  handleKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;\n}\n\nconst ParameterSliderContext =\n  createContext<ParameterSliderContextValue | null>(null);\n\nconst useParameterSlider = (part: string) => {\n  const context = useContext(ParameterSliderContext);\n  if (!context) {\n    throw new Error(`${part} must be used inside ParameterSlider.`);\n  }\n  return context;\n};\n\nconst decimalsOf = (step: number) => {\n  const text = String(step);\n  const dot = text.indexOf(\".\");\n  return dot === -1 ? 0 : text.length - dot - 1;\n};\n\nconst roundValue = (value: number) => Math.round(value * PRECISION) / PRECISION;\n\n/** Points along the range sampled to find the widest value text. */\nconst WIDTH_SAMPLES = 24;\n\n/** Characters in the widest value along the range, so the value keeps one width. */\nconst widestValue = (\n  format: (value: number) => string,\n  taper: Taper,\n  snap: (value: number) => number\n) => {\n  let widest = 0;\n  for (let index = 0; index <= WIDTH_SAMPLES; index += 1) {\n    const sample = snap(taper.toValue(index / WIDTH_SAMPLES));\n    widest = Math.max(widest, format(sample).length);\n  }\n  return widest;\n};\n\nexport interface ParameterSliderProps extends Omit<\n  ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> {\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number, details: ParameterChangeDetails) => void;\n  /** Fires when a drag ends, after keyboard input, typing and reset. */\n  onValueCommitted?: (value: number) => void;\n  /** Default 0. */\n  min?: number;\n  /** Default 100. */\n  max?: number;\n  /** Default 1. */\n  step?: number;\n  /** Shift+arrow and Page Up/Down. Default 10. */\n  largeStep?: number;\n  /** Suffix such as \"ms\", \"dB\" or \"Hz\". */\n  unit?: string;\n  /** Digits after the decimal point. Default: from `step`. */\n  decimals?: number;\n  /** `log` suits frequency and time. Default `linear`. */\n  scale?: \"linear\" | \"log\";\n  /** Where the fill starts; the middle of a bipolar range. Default `min`. */\n  origin?: number;\n  /** Value restored by reset and double-click. Default `defaultValue` or `min`. */\n  resetValue?: number;\n  marks?: ParameterMark[];\n  format?: (value: number) => string;\n  disabled?: boolean;\n}\n\nexport const ParameterSlider = ({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onValueCommitted,\n  min = 0,\n  max = 100,\n  step = 1,\n  largeStep = 10,\n  unit,\n  decimals: decimalsProp,\n  scale = \"linear\",\n  origin,\n  resetValue: resetValueProp,\n  marks,\n  format: formatProp,\n  disabled: disabledProp,\n  className,\n  children,\n  ...props\n}: ParameterSliderProps) => {\n  const config = useAudioConfig();\n  const disabled = disabledProp ?? config.disabled ?? false;\n  const resetValue = resetValueProp ?? defaultValue ?? min;\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    clamp(defaultValue ?? resetValue, min, max)\n  );\n  const value = valueProp ?? uncontrolled;\n  const latestRef = useRef(value);\n  // Every commit, not only when `value` changes: a controlled parent that\n  // rejects a change re-renders with the same value, and the ref must drop\n  // the rejected one.\n  useLayoutEffect(() => {\n    latestRef.current = value;\n  });\n  const decimals = decimalsProp ?? decimalsOf(step);\n  const labelId = useId();\n  const descriptionId = useId();\n\n  const taper = useMemo(\n    () => (scale === \"log\" ? logTaper(min, max) : linearTaper(min, max)),\n    [max, min, scale]\n  );\n\n  const format = useCallback(\n    (next: number) => {\n      if (formatProp) {\n        return formatProp(next);\n      }\n      const text = next.toFixed(decimals);\n      return unit ? `${text} ${unit}` : text;\n    },\n    [decimals, formatProp, unit]\n  );\n\n  const quantize = useCallback(\n    (next: number) => {\n      const stepped = min + Math.round((next - min) / step) * step;\n      return roundValue(clamp(stepped, min, max));\n    },\n    [max, min, step]\n  );\n\n  const change = useCallback(\n    (next: number, details: ParameterChangeDetails) => {\n      if (next === latestRef.current) {\n        return;\n      }\n      latestRef.current = next;\n      if (valueProp === undefined) {\n        setUncontrolled(next);\n      }\n      onValueChange?.(next, details);\n    },\n    [onValueChange, valueProp]\n  );\n\n  const commit = useCallback(\n    (next: number) => {\n      onValueCommitted?.(next);\n    },\n    [onValueCommitted]\n  );\n\n  const commitLatest = useCallback(() => {\n    onValueCommitted?.(latestRef.current);\n  }, [onValueCommitted]);\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLInputElement>) => {\n      const increment = event.shiftKey ? largeStep : step;\n      const { current } = latestRef;\n      let next: number | null = null;\n      switch (event.key) {\n        case \"ArrowUp\":\n        case \"ArrowRight\": {\n          next = current + increment;\n          break;\n        }\n        case \"ArrowDown\":\n        case \"ArrowLeft\": {\n          next = current - increment;\n          break;\n        }\n        case \"PageUp\": {\n          next = current + largeStep;\n          break;\n        }\n        case \"PageDown\": {\n          next = current - largeStep;\n          break;\n        }\n        case \"Home\": {\n          next = min;\n          break;\n        }\n        case \"End\": {\n          next = max;\n          break;\n        }\n        default: {\n          break;\n        }\n      }\n      if (next === null) {\n        return;\n      }\n      event.preventDefault();\n      const quantized = quantize(next);\n      change(quantized, { event: event.nativeEvent, reason: \"keyboard\" });\n      commit(quantized);\n    },\n    [change, commit, largeStep, max, min, quantize, step]\n  );\n\n  const position = taper.toPosition(value);\n  const originPosition = taper.toPosition(clamp(origin ?? min, min, max));\n\n  const contextValue = useMemo<ParameterSliderContextValue>(\n    () => ({\n      change,\n      commit,\n      commitLatest,\n      decimals,\n      descriptionId,\n      disabled,\n      format,\n      handleKeyDown,\n      labelId,\n      largeStep,\n      marks,\n      max,\n      min,\n      originPosition,\n      position,\n      resetValue,\n      step,\n      taper,\n      unit,\n      value,\n    }),\n    [\n      change,\n      commit,\n      commitLatest,\n      decimals,\n      descriptionId,\n      disabled,\n      format,\n      handleKeyDown,\n      labelId,\n      largeStep,\n      marks,\n      max,\n      min,\n      originPosition,\n      position,\n      resetValue,\n      step,\n      taper,\n      unit,\n      value,\n    ]\n  );\n\n  return (\n    <ParameterSliderContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          \"group/parameter-slider flex w-full flex-col gap-2 data-disabled:opacity-50\",\n          className\n        )}\n        data-disabled={disabled ? \"\" : undefined}\n        data-modified={value === resetValue ? undefined : \"\"}\n        data-slot=\"parameter-slider\"\n        role=\"group\"\n        aria-labelledby={labelId}\n        {...props}\n      >\n        {children}\n      </div>\n    </ParameterSliderContext.Provider>\n  );\n};\n\nexport const ParameterSliderHeader = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    className={cn(\"flex min-h-6 items-center gap-2\", className)}\n    data-slot=\"parameter-slider-header\"\n    {...props}\n  />\n);\n\nexport const ParameterSliderLabel = ({\n  className,\n  ...props\n}: ComponentProps<\"span\">) => {\n  const { labelId } = useParameterSlider(\"ParameterSliderLabel\");\n  return (\n    <span\n      className={cn(\"mr-auto text-sm font-medium\", className)}\n      data-slot=\"parameter-slider-label\"\n      id={labelId}\n      {...props}\n    />\n  );\n};\n\nexport interface ParameterSliderInputProps extends Omit<\n  NumberFieldPrimitive.Input.Props,\n  \"value\" | \"defaultValue\"\n> {\n  /** Drag the unit to change the value. Default true. */\n  scrub?: boolean;\n}\n\nexport const ParameterSliderInput = ({\n  scrub = true,\n  className,\n  ...props\n}: ParameterSliderInputProps) => {\n  const {\n    change,\n    commit,\n    decimals,\n    disabled,\n    labelId,\n    largeStep,\n    max,\n    min,\n    step,\n    unit,\n    value,\n  } = useParameterSlider(\"ParameterSliderInput\");\n\n  const suffix = unit ? (\n    <span\n      className=\"text-muted-foreground px-1.5 text-xs\"\n      data-slot=\"parameter-slider-unit\"\n    >\n      {unit}\n    </span>\n  ) : null;\n\n  return (\n    <NumberFieldPrimitive.Root\n      className=\"shrink-0\"\n      disabled={disabled}\n      format={{\n        maximumFractionDigits: decimals,\n        minimumFractionDigits: decimals,\n      }}\n      largeStep={largeStep}\n      max={max}\n      min={min}\n      onValueChange={(next, details) => {\n        if (next !== null) {\n          change(next, { event: details.event, reason: \"input\" });\n        }\n      }}\n      onValueCommitted={(next) => {\n        if (next !== null) {\n          commit(next);\n        }\n      }}\n      step={step}\n      value={value}\n    >\n      <NumberFieldPrimitive.Group\n        className=\"bg-input/50 focus-within:ring-ring/30 flex h-7 items-center rounded-lg focus-within:ring-3\"\n        data-slot=\"parameter-slider-input-group\"\n      >\n        <NumberFieldPrimitive.Input\n          aria-labelledby={labelId}\n          className={cn(\n            \"h-full w-16 bg-transparent px-2 text-end font-mono text-xs tabular-nums outline-hidden\",\n            className\n          )}\n          data-slot=\"parameter-slider-input\"\n          {...props}\n        />\n        {scrub && suffix ? (\n          <NumberFieldPrimitive.ScrubArea className=\"cursor-ew-resize\">\n            {suffix}\n          </NumberFieldPrimitive.ScrubArea>\n        ) : (\n          suffix\n        )}\n      </NumberFieldPrimitive.Group>\n    </NumberFieldPrimitive.Root>\n  );\n};\n\nexport const ParameterSliderValue = ({\n  className,\n  style,\n  ...props\n}: ComponentProps<\"span\">) => {\n  const { format, max, min, step, taper, value } = useParameterSlider(\n    \"ParameterSliderValue\"\n  );\n  const valueWidth = useMemo(\n    () =>\n      widestValue(format, taper, (next) =>\n        roundValue(\n          clamp(min + Math.round((next - min) / step) * step, min, max)\n        )\n      ),\n    [format, max, min, step, taper]\n  );\n  return (\n    <span\n      className={cn(\n        \"text-muted-foreground inline-block min-w-(--parameter-value-width) text-end font-mono text-xs whitespace-nowrap tabular-nums\",\n        className\n      )}\n      data-slot=\"parameter-slider-value\"\n      style={\n        {\n          \"--parameter-value-width\": `${valueWidth}ch`,\n          ...style,\n        } as CSSProperties\n      }\n      {...props}\n    >\n      {format(value)}\n    </span>\n  );\n};\n\nexport type ParameterSliderResetProps = useRender.ComponentProps<\"button\">;\n\nexport const ParameterSliderReset = ({\n  render,\n  className,\n  children,\n  ...props\n}: ParameterSliderResetProps) => {\n  const { change, commit, disabled, resetValue, value } = useParameterSlider(\n    \"ParameterSliderReset\"\n  );\n  const modified = value !== resetValue;\n\n  return useRender({\n    defaultTagName: \"button\",\n    props: mergeProps<\"button\">(\n      {\n        \"aria-label\": \"Reset\",\n        children: children ?? \"Reset\",\n        className: cn(\n          \"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring/30 inline-flex h-6 items-center rounded-md px-1.5 text-xs outline-none focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-0\",\n          className\n        ),\n        disabled: disabled || !modified,\n        onClick: () => {\n          change(resetValue, { reason: \"reset\" });\n          commit(resetValue);\n        },\n        type: \"button\",\n      },\n      props\n    ),\n    render,\n    state: { modified, slot: \"parameter-slider-reset\" },\n  });\n};\n\nconst useSliderBindings = () => {\n  const context = useParameterSlider(\"ParameterSliderControl\");\n  const { max, min, step, taper } = context;\n  const quantizeFromPosition = useCallback(\n    (position: number) => {\n      const raw = taper.toValue(position);\n      const stepped = min + Math.round((raw - min) / step) * step;\n      return roundValue(clamp(stepped, min, max));\n    },\n    [max, min, step, taper]\n  );\n  return { ...context, quantizeFromPosition };\n};\n\nexport const ParameterSliderControl = ({\n  className,\n  ...props\n}: Omit<\n  SliderPrimitive.Root.Props<number>,\n  | \"value\"\n  | \"defaultValue\"\n  | \"onValueChange\"\n  | \"onValueCommitted\"\n  | \"min\"\n  | \"max\"\n  | \"step\"\n>) => {\n  const {\n    change,\n    commit,\n    commitLatest,\n    descriptionId,\n    disabled,\n    format,\n    handleKeyDown,\n    labelId,\n    originPosition,\n    position,\n    quantizeFromPosition,\n    resetValue,\n    value,\n  } = useSliderBindings();\n  const start = Math.min(originPosition, position) * 100;\n  const end = Math.max(originPosition, position) * 100;\n  const range = {\n    \"--parameter-range-size\": `${end - start}%`,\n    \"--parameter-range-start\": `${start}%`,\n  } as CSSProperties;\n\n  return (\n    <SliderPrimitive.Root\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none\",\n        className\n      )}\n      data-slot=\"parameter-slider-control\"\n      disabled={disabled}\n      max={1}\n      min={0}\n      onValueChange={(next, details) => {\n        change(quantizeFromPosition(next), {\n          event: details.event,\n          reason: \"drag\",\n        });\n      }}\n      onValueCommitted={commitLatest}\n      step={POSITION_STEP}\n      value={position}\n      {...props}\n    >\n      <SliderPrimitive.Control className=\"relative flex h-4 w-full items-center px-2 before:absolute before:inset-x-0 before:-inset-y-1.5 pointer-coarse:before:-inset-y-3\">\n        <SliderPrimitive.Track\n          className=\"bg-input/90 relative h-1 w-full grow rounded-full\"\n          data-slot=\"parameter-slider-track\"\n        >\n          <div\n            className=\"bg-primary absolute inset-y-0 left-(--parameter-range-start) w-(--parameter-range-size) rounded-full\"\n            data-slot=\"parameter-slider-range\"\n            style={range}\n          />\n          <SliderPrimitive.Thumb\n            aria-describedby={descriptionId}\n            aria-labelledby={labelId}\n            className=\"bg-background ring-foreground/15 hover:ring-ring/30 focus-visible:ring-ring/40 data-dragging:ring-ring/30 block size-4 shrink-0 rounded-full shadow-sm ring-1 outline-hidden transition-[box-shadow] hover:ring-4 focus-visible:ring-4 data-dragging:ring-4\"\n            data-slot=\"parameter-slider-thumb\"\n            getAriaValueText={() => format(value)}\n            onDoubleClick={() => {\n              change(resetValue, { reason: \"reset\" });\n              commit(resetValue);\n            }}\n            onKeyDown={handleKeyDown}\n          />\n        </SliderPrimitive.Track>\n      </SliderPrimitive.Control>\n    </SliderPrimitive.Root>\n  );\n};\n\nexport const ParameterSliderMarks = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { format, marks, taper } = useParameterSlider(\"ParameterSliderMarks\");\n  if (!marks || marks.length === 0) {\n    return null;\n  }\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        \"text-muted-foreground relative mx-2 h-4 text-[0.625rem]\",\n        className\n      )}\n      data-slot=\"parameter-slider-marks\"\n      {...props}\n    >\n      {marks.map((mark) => (\n        <span\n          className=\"absolute top-0 left-(--mark-position) -translate-x-1/2 whitespace-nowrap tabular-nums\"\n          key={mark.value}\n          style={\n            {\n              \"--mark-position\": `${taper.toPosition(mark.value) * 100}%`,\n            } as CSSProperties\n          }\n        >\n          {mark.label ?? format(mark.value)}\n        </span>\n      ))}\n    </div>\n  );\n};\n\nexport const ParameterSliderDescription = ({\n  className,\n  ...props\n}: ComponentProps<\"p\">) => {\n  const { descriptionId } = useParameterSlider(\"ParameterSliderDescription\");\n  return (\n    <p\n      className={cn(\"text-muted-foreground text-xs\", className)}\n      data-slot=\"parameter-slider-description\"\n      id={descriptionId}\n      {...props}\n    />\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}