{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fader",
  "title": "Fader",
  "description": "A composable decibel fader with tapering, detents, keyboard stepping, reset, editable values, and optional wheel control.",
  "dependencies": [
    "solid-js@^1.9.15",
    "@kobalte/core@https://github.com/jhomra21/kobalte/releases/download/kobalte-solid1-audiocn-e0e3bf095f05c7e61230a251b79181c6d834d408/kobalte-core-0.13.14-audiocn.0.e0e3bf09.tgz",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/use-audio-config",
    "@audiocn-solid/db-scale"
  ],
  "files": [
    {
      "path": "components/ui/fader.tsx",
      "content": "import * as SliderPrimitive from \"@kobalte/core/slider\";\nimport { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport {\n  createContext,\n  createMemo,\n  createSignal,\n  Show,\n  useContext,\n} from \"solid-js\";\n\nimport { DbScale } from \"@/components/ui/db-scale\";\nimport type { DbScaleProps } from \"@/components/ui/db-scale\";\nimport { useAudioConfig } from \"@/hooks/use-audio-config\";\nimport type { AudioSize } from \"@/hooks/use-audio-config\";\nimport { clamp, formatDb, SILENCE_DB } from \"@/lib/audio/decibels\";\nimport { resolveTaper } from \"@/lib/audio/taper\";\nimport type { TaperInput } from \"@/lib/audio/taper\";\nimport type { Orientation, Taper } from \"@/lib/audio/types\";\nimport { roundValue } from \"@/lib/number\";\nimport { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type {\n  ButtonDOMProps,\n  DivDOMProps,\n  SpanDOMProps,\n} from \"@/lib/solid/jsx-types\";\nimport { useKobalteSliderCompat } from \"@/lib/solid/kobalte-slider\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { setRefValue } from \"@/lib/solid/ref\";\nimport type { RefTarget } from \"@/lib/solid/ref\";\nimport { mergeStyleVars } from \"@/lib/solid/style\";\nimport type { StyleValue } from \"@/lib/solid/style\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_MIN_DB = -60;\n\nconst DEFAULT_MAX_DB = 6;\n\nconst POSITION_STEP = 0.0005;\n\nconst DETENT_SNAP = 0.012;\n\nconst DEFAULT_DETENTS = [0] as const;\n\nconst DB_SUFFIX = /db/iu;\n\nconst INFINITY_TEXT = /^-?(?:inf|infinity|∞)$/iu;\n\nexport type FaderChangeReason =\n  | \"drag\"\n  | \"track-press\"\n  | \"keyboard\"\n  | \"wheel\"\n  | \"reset\"\n  | \"input\";\n\nexport interface FaderChangeDetails {\n  reason: FaderChangeReason;\n  event?: Event;\n}\n\ninterface FaderContextValue {\n  ariaLabel: () => string | undefined;\n  ariaLabelledBy: () => string | undefined;\n  change: (db: number, details: FaderChangeDetails) => void;\n  commit: (db: number) => void;\n  disabled: () => boolean;\n  format: () => (db: number) => string;\n  handleKeyDown: (event: KeyboardEvent) => void;\n  max: () => number;\n  min: () => number;\n  orientation: () => Orientation;\n  originPosition: () => number;\n  pointer: (reason: \"drag\" | \"track-press\", event: PointerEvent) => void;\n  position: () => number;\n  resetValue: () => number;\n  size: () => AudioSize;\n  taper: () => Taper;\n  value: () => number;\n  variant: () => \"console\" | \"default\";\n}\n\nconst FaderContext = createContext<FaderContextValue | null>(null);\n\nconst useFader = (part: string): FaderContextValue => {\n  const context = useContext(FaderContext);\n\n  if (!context) {\n    throw new Error(`${part} must be used inside Fader.`);\n  }\n\n  return context;\n};\n\nconst defaultFormat = (db: number): string =>\n  db === SILENCE_DB ? \"Silent\" : formatDb(db);\n\nconst parseDb = (text: string): number | null => {\n  const normalized = text.replaceAll(\"−\", \"-\").replace(DB_SUFFIX, \"\").trim();\n\n  if (INFINITY_TEXT.test(normalized)) {\n    return SILENCE_DB;\n  }\n\n  if (normalized === \"\") {\n    return null;\n  }\n\n  const parsed = Number(normalized);\n\n  return Number.isNaN(parsed) ? null : parsed;\n};\n\nconst incrementFor = (\n  event: KeyboardEvent,\n  step: number,\n  fineStep: number,\n  largeStep: number\n): number => {\n  if (event.altKey) {\n    return fineStep;\n  }\n\n  return event.shiftKey ? largeStep : step;\n};\n\nconst SliderCompat = (props: { onChangeEnd: () => void }) => {\n  const context = SliderPrimitive.useSliderContext();\n  useKobalteSliderCompat(context, {\n    onChangeEnd: () => props.onChangeEnd(),\n    suppressStep: true,\n  });\n\n  return null;\n};\n\ntype SliderLabelProps = Parameters<typeof SliderPrimitive.Label>[0];\n\ntype FaderLabelProps = Omit<\n  SliderLabelProps,\n  \"children\" | \"class\" | \"className\"\n> & {\n  class?: string;\n  className?: string;\n  children?: SpanDOMProps[\"children\"];\n};\n\nconst LABEL_OWN = [\"children\", \"class\", \"className\"] as const;\n\nexport const FaderLabel = (props: FaderLabelProps) => {\n  const rest = omitProps(props, LABEL_OWN);\n\n  return (\n    <SliderPrimitive.Label\n      class={cn(\"text-sm font-medium\", props.class, props.className)}\n      data-slot=\"fader-label\"\n      {...rest}\n    >\n      {props.children}\n    </SliderPrimitive.Label>\n  );\n};\n\ntype SliderTrackProps = Parameters<typeof SliderPrimitive.Track>[0];\n\ntype FaderTrackProps = Omit<\n  SliderTrackProps,\n  | \"children\"\n  | \"class\"\n  | \"className\"\n  | \"onPointerDown\"\n  | \"onPointerMove\"\n  | \"onPointerUp\"\n> & {\n  class?: string;\n  className?: string;\n  children?: DivDOMProps[\"children\"];\n};\n\nconst TRACK_OWN = [\"children\", \"class\", \"className\"] as const;\n\nexport const FaderTrack = (props: FaderTrackProps) => {\n  const context = useFader(\"FaderTrack\");\n  const rest = omitProps(props, TRACK_OWN);\n  const horizontal = () => context.orientation() === \"horizontal\";\n\n  return (\n    <div\n      class={cn(\n        \"relative flex min-h-0 min-w-0 items-center\",\n        horizontal()\n          ? \"h-(--fader-thumb-size) w-full px-[calc(var(--fader-thumb-size)/2)]\"\n          : \"h-full w-(--fader-thumb-size) flex-col py-[calc(var(--fader-thumb-size)/2)]\"\n      )}\n      data-slot=\"fader-control\"\n    >\n      <SliderPrimitive.Track\n        class={cn(\n          // Kobalte owns pointer capture on Track, not the outer Control.\n          // Expand its pseudo-element to the same hit region without changing\n          // the track bounds used to map pointer positions to dB.\n          \"bg-input/90 relative grow rounded-full before:absolute\",\n          horizontal()\n            ? \"h-(--fader-track-size) w-full before:-inset-x-[calc(var(--fader-thumb-size)/2)] before:inset-y-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2-0.375rem)] pointer-coarse:before:inset-y-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2-0.75rem)]\"\n            : \"h-full w-(--fader-track-size) before:inset-x-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2-0.375rem)] before:-inset-y-[calc(var(--fader-thumb-size)/2)] pointer-coarse:before:inset-x-[calc((var(--fader-track-size)-var(--fader-thumb-size))/2-0.75rem)]\",\n          props.class,\n          props.className\n        )}\n        data-slot=\"fader-track\"\n        onPointerDown={(event: PointerEvent) => {\n          context.pointer(\"track-press\", event);\n        }}\n        onPointerMove={(event: PointerEvent) => {\n          context.pointer(\"track-press\", event);\n        }}\n        {...rest}\n      >\n        {props.children}\n      </SliderPrimitive.Track>\n    </div>\n  );\n};\n\ntype FaderRangeProps = Omit<\n  DivDOMProps,\n  \"children\" | \"class\" | \"className\" | \"ref\" | \"style\"\n> & {\n  class?: string;\n  className?: string;\n  style?: StyleValue;\n};\n\nconst RANGE_OWN = [\"class\", \"className\", \"style\"] as const;\n\nexport const FaderRange = (props: FaderRangeProps) => {\n  const context = useFader(\"FaderRange\");\n  const rest = omitProps(props, RANGE_OWN);\n\n  const start = () =>\n    Math.min(context.originPosition(), context.position()) * 100;\n\n  const end = () =>\n    Math.max(context.originPosition(), context.position()) * 100;\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      class={cn(\n        \"bg-primary pointer-events-none absolute rounded-full\",\n        context.orientation() === \"horizontal\"\n          ? \"inset-y-0 left-(--fader-range-start) w-(--fader-range-size)\"\n          : \"inset-x-0 bottom-(--fader-range-start) h-(--fader-range-size)\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"fader-range\"\n      style={mergeStyleVars(props.style, {\n        \"--fader-range-size\": `${end() - start()}%`,\n        \"--fader-range-start\": `${start()}%`,\n      })}\n      {...rest}\n    />\n  );\n};\n\nconst thumbVariants = cva(\n  \"bg-background ring-foreground/15 hover:ring-ring/30 focus-visible:ring-ring/40 data-dragging:ring-ring/30 block shrink-0 shadow-sm ring-1 outline-hidden transition-[box-shadow] hover:ring-4 focus-visible:ring-4 data-disabled:pointer-events-none data-dragging:ring-4\",\n  {\n    compoundVariants: [\n      {\n        className:\n          \"h-[calc(var(--fader-thumb-size)*1.6)] w-[calc(var(--fader-thumb-size)*0.7)] bg-[linear-gradient(to_right,transparent_calc(50%-0.5px),var(--foreground)_calc(50%-0.5px),var(--foreground)_calc(50%+0.5px),transparent_calc(50%+0.5px))]\",\n        orientation: \"horizontal\",\n        variant: \"console\",\n      },\n      {\n        className:\n          \"h-[calc(var(--fader-thumb-size)*0.7)] w-[calc(var(--fader-thumb-size)*1.8)] bg-[linear-gradient(to_bottom,transparent_calc(50%-0.5px),var(--foreground)_calc(50%-0.5px),var(--foreground)_calc(50%+0.5px),transparent_calc(50%+0.5px))]\",\n        orientation: \"vertical\",\n        variant: \"console\",\n      },\n    ],\n    defaultVariants: {\n      orientation: \"horizontal\",\n      variant: \"default\",\n    },\n    variants: {\n      orientation: {\n        horizontal: \"\",\n        vertical: \"\",\n      },\n      variant: {\n        console: \"border-border rounded-sm border\",\n        default: \"size-(--fader-thumb-size) rounded-full\",\n      },\n    },\n  }\n);\n\ntype SliderThumbProps = Parameters<typeof SliderPrimitive.Thumb>[0];\n\nexport type FaderThumbProps = Omit<\n  SliderThumbProps,\n  | \"class\"\n  | \"className\"\n  | \"onDoubleClick\"\n  | \"onKeyDown\"\n  | \"onPointerDown\"\n  | \"onPointerMove\"\n> & {\n  class?: string;\n  className?: string;\n  onDoubleClick?: (event: MouseEvent) => void;\n  onKeyDown?: (event: KeyboardEvent) => void;\n  onPointerDown?: (event: PointerEvent) => void;\n  onPointerMove?: (event: PointerEvent) => void;\n};\n\nconst THUMB_OWN = [\n  \"class\",\n  \"className\",\n  \"onDoubleClick\",\n  \"onKeyDown\",\n  \"onPointerDown\",\n  \"onPointerMove\",\n] as const;\n\nexport const FaderThumb = (props: FaderThumbProps) => {\n  const context = useFader(\"FaderThumb\");\n  const rest = omitProps(props, THUMB_OWN);\n\n  return (\n    <SliderPrimitive.Thumb\n      aria-label={context.ariaLabel()}\n      aria-labelledby={context.ariaLabelledBy()}\n      aria-valuetext={context.format()(context.value())}\n      class={cn(\n        context.orientation() === \"vertical\"\n          ? \"left-1/2 -translate-x-1/2\"\n          : \"top-1/2 -translate-y-1/2\",\n        thumbVariants({\n          orientation: context.orientation(),\n          variant: context.variant(),\n        }),\n        props.class,\n        props.className\n      )}\n      data-slot=\"fader-thumb\"\n      onDblClick={(event: MouseEvent) => {\n        props.onDoubleClick?.(event);\n        context.change(context.resetValue(), {\n          event,\n          reason: \"reset\",\n        });\n        context.commit(context.resetValue());\n      }}\n      onKeyDown={(event: KeyboardEvent) => {\n        props.onKeyDown?.(event);\n\n        if (!event.defaultPrevented) {\n          context.handleKeyDown(event);\n        }\n      }}\n      onPointerDown={(event: PointerEvent) => {\n        props.onPointerDown?.(event);\n        context.pointer(\"drag\", event);\n      }}\n      onPointerMove={(event: PointerEvent) => {\n        props.onPointerMove?.(event);\n        context.pointer(\"drag\", event);\n      }}\n      {...rest}\n    >\n      <SliderPrimitive.Input />\n    </SliderPrimitive.Thumb>\n  );\n};\n\nexport type FaderScaleProps = Omit<\n  DbScaleProps,\n  \"maxDb\" | \"minDb\" | \"orientation\" | \"taper\"\n>;\n\nexport const FaderScale = (props: FaderScaleProps) => {\n  const context = useFader(\"FaderScale\");\n  const rest = omitProps(props, [\"class\", \"className\"] as const);\n  const horizontal = () => context.orientation() === \"horizontal\";\n\n  return (\n    <div\n      class={cn(\n        horizontal()\n          ? \"px-[calc(var(--fader-thumb-size)/2)]\"\n          : \"py-[calc(var(--fader-thumb-size)/2)]\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"fader-scale\"\n    >\n      <DbScale\n        maxDb={context.max()}\n        minDb={context.min()}\n        orientation={context.orientation()}\n        taper={context.taper()}\n        {...rest}\n      />\n    </div>\n  );\n};\n\nexport interface FaderValueProps extends Omit<\n  SpanDOMProps,\n  \"children\" | \"class\" | \"className\" | \"ref\" | \"style\"\n> {\n  class?: string;\n  className?: string;\n  editable?: boolean;\n  style?: StyleValue;\n}\n\nconst VALUE_OWN = [\"class\", \"className\", \"editable\", \"style\"] as const;\n\nconst WIDTH_SAMPLES_DB = [-88.8, -8.8, 8.8, 88.8];\n\nconst widestValue = (\n  format: (db: number) => string,\n  min: number,\n  max: number\n): number =>\n  Math.max(\n    ...[\n      SILENCE_DB,\n      min,\n      min + 0.1,\n      max,\n      max - 0.1,\n      ...WIDTH_SAMPLES_DB.filter((db) => db > min && db < max),\n    ].map((db) => format(db).length)\n  );\n\nexport const FaderValue = (props: FaderValueProps) => {\n  const context = useFader(\"FaderValue\");\n  const rest = omitProps(props, VALUE_OWN);\n  const [editing, setEditing] = createSignal(false);\n  const [draft, setDraft] = createSignal(\"\");\n\n  const width = () =>\n    widestValue(context.format(), context.min(), context.max());\n\n  const valueStyle = () =>\n    mergeStyleVars(props.style, {\n      \"--fader-value-width\": `${width()}ch`,\n    });\n\n  const finish = (apply: boolean) => {\n    setEditing(false);\n\n    if (!apply) {\n      return;\n    }\n\n    const parsed = parseDb(draft());\n\n    if (parsed === null) {\n      return;\n    }\n\n    const next =\n      parsed === SILENCE_DB\n        ? context.min()\n        : clamp(parsed, context.min(), context.max());\n\n    context.change(next, { reason: \"input\" });\n    context.commit(next);\n  };\n\n  const text = () => context.format()(context.value());\n\n  const readout = () => (\n    <Show\n      fallback={\n        <span\n          class={cn(\n            \"text-muted-foreground inline-block w-(--fader-value-width) shrink-0 text-end font-mono text-xs whitespace-nowrap tabular-nums\",\n            props.class,\n            props.className\n          )}\n          data-slot=\"fader-value\"\n          style={valueStyle()}\n          {...rest}\n        >\n          {text()}\n        </span>\n      }\n      when={props.editable ?? false}\n    >\n      <button\n        class={cn(\n          \"text-muted-foreground hover:bg-muted focus-visible:ring-ring/30 h-6 w-[calc(var(--fader-value-width)+0.75rem)] shrink-0 rounded-md px-1.5 text-end font-mono text-xs tabular-nums outline-none focus-visible:ring-3\",\n          props.class,\n          props.className\n        )}\n        data-slot=\"fader-value\"\n        disabled={context.disabled()}\n        onClick={() => {\n          setDraft(\n            context.value() === SILENCE_DB ? \"-inf\" : String(context.value())\n          );\n          setEditing(true);\n        }}\n        style={valueStyle()}\n        type=\"button\"\n      >\n        {text()}\n      </button>\n    </Show>\n  );\n\n  return (\n    <Show fallback={readout()} when={editing()}>\n      <input\n        aria-label=\"Value in dB\"\n        class={cn(\n          \"bg-background focus-visible:ring-ring/30 h-6 w-[calc(var(--fader-value-width)+0.75rem)] rounded-md border px-[calc(0.375rem-1px)] text-end font-mono text-xs tabular-nums outline-none focus-visible:ring-3\",\n          props.class,\n          props.className\n        )}\n        data-slot=\"fader-value-input\"\n        onBlur={() => finish(true)}\n        onInput={(event) => setDraft(event.currentTarget.value)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            finish(true);\n          } else if (event.key === \"Escape\") {\n            finish(false);\n          }\n        }}\n        ref={(node) => {\n          queueMicrotask(() => {\n            node.focus();\n            node.select();\n          });\n        }}\n        style={valueStyle()}\n        value={draft()}\n      />\n    </Show>\n  );\n};\n\nexport interface FaderResetProps extends Omit<\n  ButtonDOMProps,\n  \"children\" | \"class\" | \"className\" | \"onClick\" | \"ref\" | \"type\"\n> {\n  class?: string;\n  className?: string;\n  children?: ButtonDOMProps[\"children\"];\n  onClick?: (event: MouseEvent) => void;\n}\n\nconst RESET_OWN = [\"children\", \"class\", \"className\", \"onClick\"] as const;\n\nexport const FaderReset = (props: FaderResetProps) => {\n  const context = useFader(\"FaderReset\");\n  const rest = omitProps(props, RESET_OWN);\n  const modified = () => context.value() !== context.resetValue();\n\n  return (\n    <button\n      aria-label=\"Reset\"\n      class={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        props.class,\n        props.className\n      )}\n      data-modified={modified() ? \"\" : undefined}\n      data-slot=\"fader-reset\"\n      disabled={context.disabled() || !modified()}\n      onClick={(event) => {\n        props.onClick?.(event);\n        context.change(context.resetValue(), { event, reason: \"reset\" });\n        context.commit(context.resetValue());\n      }}\n      type=\"button\"\n      {...rest}\n    >\n      {props.children ?? \"Reset\"}\n    </button>\n  );\n};\n\nexport const faderVariants = cva(\n  \"group/fader relative flex touch-none gap-2 select-none data-disabled:opacity-50\",\n  {\n    defaultVariants: {\n      orientation: \"horizontal\",\n      size: \"default\",\n    },\n    variants: {\n      orientation: {\n        horizontal: \"w-full flex-col\",\n        vertical: \"h-full min-h-32 flex-row justify-center\",\n      },\n      size: {\n        default: \"[--fader-thumb-size:1rem] [--fader-track-size:0.25rem]\",\n        lg: \"[--fader-thumb-size:1.25rem] [--fader-track-size:0.375rem]\",\n        sm: \"[--fader-thumb-size:0.75rem] [--fader-track-size:0.1875rem]\",\n      },\n    },\n  }\n);\n\ntype SliderRootProps = Parameters<typeof SliderPrimitive.Root>[0];\n\ntype FaderDOMProps = Omit<\n  SliderRootProps,\n  | \"aria-label\"\n  | \"aria-labelledby\"\n  | \"children\"\n  | \"class\"\n  | \"className\"\n  | \"defaultValue\"\n  | \"getValueLabel\"\n  | \"maxValue\"\n  | \"minValue\"\n  | \"onChange\"\n  | \"onChangeEnd\"\n  | \"orientation\"\n  | \"ref\"\n  | \"step\"\n  | \"value\"\n>;\n\nexport interface FaderProps\n  extends\n    FaderDOMProps,\n    Omit<VariantProps<typeof faderVariants>, \"orientation\"> {\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  allowWheel?: boolean;\n  defaultValue?: number;\n  detents?: number[];\n  fineStep?: number;\n  format?: (db: number) => string;\n  largeStep?: number;\n  max?: number;\n  min?: number;\n  name?: string;\n  onValueChange?: (value: number, details: FaderChangeDetails) => void;\n  onValueCommitted?: (value: number) => void;\n  orientation?: Orientation;\n  origin?: number;\n  readOnly?: boolean;\n  ref?: RefTarget<HTMLDivElement>;\n  required?: boolean;\n  resetValue?: number;\n  silenceAtMin?: boolean;\n  step?: number;\n  taper?: TaperInput;\n  value?: number;\n  variant?: \"console\" | \"default\";\n  children?: DivDOMProps[\"children\"];\n  class?: string;\n  className?: string;\n}\n\nconst FADER_OWN = [\n  \"allowWheel\",\n  \"aria-label\",\n  \"aria-labelledby\",\n  \"children\",\n  \"class\",\n  \"className\",\n  \"defaultValue\",\n  \"detents\",\n  \"disabled\",\n  \"fineStep\",\n  \"format\",\n  \"largeStep\",\n  \"max\",\n  \"min\",\n  \"name\",\n  \"onValueChange\",\n  \"onValueCommitted\",\n  \"orientation\",\n  \"origin\",\n  \"readOnly\",\n  \"ref\",\n  \"required\",\n  \"resetValue\",\n  \"silenceAtMin\",\n  \"size\",\n  \"step\",\n  \"taper\",\n  \"value\",\n  \"variant\",\n] as const;\n\nexport const Fader = (props: FaderProps) => {\n  const rest = omitProps(props, FADER_OWN);\n  const config = useAudioConfig();\n\n  const min = () => props.min ?? DEFAULT_MIN_DB;\n  const max = () => props.max ?? DEFAULT_MAX_DB;\n  const resetValue = () => props.resetValue ?? 0;\n  const step = () => props.step ?? 0.5;\n  const largeStep = () => props.largeStep ?? 6;\n  const fineStep = () => props.fineStep ?? 0.1;\n\n  const detents = (): readonly number[] => props.detents ?? DEFAULT_DETENTS;\n\n  const silenceAtMin = () => props.silenceAtMin ?? false;\n  const disabled = () => props.disabled ?? config.disabled ?? false;\n\n  const orientation = () =>\n    props.orientation ?? config.orientation ?? \"horizontal\";\n\n  const size = () => props.size ?? config.size ?? \"default\";\n  const variant = () => props.variant ?? \"default\";\n  const format = () => props.format ?? defaultFormat;\n  const taperInput = () => props.taper ?? \"linear\";\n\n  const [uncontrolled, setUncontrolled] = createSignal(\n    props.defaultValue ?? clamp(resetValue(), min(), max())\n  );\n\n  const value = () => props.value ?? uncontrolled();\n  let latestValue = value();\n  let pointerReason: \"drag\" | \"track-press\" = \"drag\";\n  let pointerEvent: PointerEvent | undefined;\n\n  createCompatEffect(value, (next) => {\n    latestValue = next;\n  });\n\n  const change = (db: number, details: FaderChangeDetails) => {\n    if (db === latestValue) {\n      return;\n    }\n\n    latestValue = db;\n\n    if (props.value === undefined) {\n      setUncontrolled(db);\n    }\n\n    props.onValueChange?.(db, details);\n\n    if (props.value !== undefined) {\n      queueMicrotask(() => {\n        latestValue = value();\n      });\n    }\n  };\n\n  const commit = (db: number) => {\n    props.onValueCommitted?.(db);\n  };\n\n  const taper = createMemo(() => resolveTaper(taperInput(), min(), max()));\n\n  const toPosition = (db: number): number =>\n    db === SILENCE_DB ? 0 : taper().toPosition(db);\n\n  const quantize = (db: number, increment: number): number => {\n    if (db === SILENCE_DB) {\n      return silenceAtMin() ? SILENCE_DB : min();\n    }\n\n    const stepped = min() + Math.round((db - min()) / increment) * increment;\n\n    return roundValue(clamp(stepped, min(), max()));\n  };\n\n  const fromPosition = (position: number, fine: boolean): number => {\n    if (silenceAtMin() && position <= 0) {\n      return SILENCE_DB;\n    }\n\n    for (const detent of detents()) {\n      if (Math.abs(position - toPosition(detent)) < DETENT_SNAP) {\n        return detent;\n      }\n    }\n\n    return quantize(taper().toValue(position), fine ? fineStep() : step());\n  };\n\n  const nudge = (direction: number, increment: number): number => {\n    const current = latestValue;\n\n    if (current === SILENCE_DB) {\n      return direction > 0 ? min() : SILENCE_DB;\n    }\n\n    if (silenceAtMin() && direction < 0 && current <= min()) {\n      return SILENCE_DB;\n    }\n\n    return quantize(\n      current + direction * increment,\n      Math.min(increment, step())\n    );\n  };\n\n  const handleKeyDown = (event: KeyboardEvent) => {\n    const increment = incrementFor(event, step(), fineStep(), largeStep());\n\n    let next: number | undefined;\n\n    switch (event.key) {\n      case \"ArrowDown\":\n      case \"ArrowLeft\": {\n        next = nudge(-1, increment);\n\n        break;\n      }\n\n      case \"ArrowRight\":\n      case \"ArrowUp\": {\n        next = nudge(1, increment);\n\n        break;\n      }\n\n      case \"End\": {\n        next = max();\n\n        break;\n      }\n\n      case \"Home\": {\n        next = silenceAtMin() ? SILENCE_DB : min();\n\n        break;\n      }\n\n      case \"PageDown\": {\n        next = nudge(-1, largeStep());\n\n        break;\n      }\n\n      case \"PageUp\": {\n        next = nudge(1, largeStep());\n\n        break;\n      }\n\n      default: {\n        return;\n      }\n    }\n\n    event.preventDefault();\n\n    change(next, { event, reason: \"keyboard\" });\n    commit(next);\n  };\n\n  const position = () => toPosition(value());\n\n  const originPosition = () =>\n    toPosition(clamp(props.origin ?? min(), min(), max()));\n\n  const [rootElement, setRootElement] = createSignal<HTMLDivElement | null>(\n    null\n  );\n\n  createCompatEffect(\n    () => [rootElement(), props.allowWheel ?? false] as const,\n    ([root, enabled]) => {\n      if (!(root && enabled)) {\n        return;\n      }\n\n      const listener = (event: WheelEvent) => {\n        const focused = root.contains(document.activeElement);\n\n        if (disabled() || !focused || event.deltaY === 0) {\n          return;\n        }\n\n        event.preventDefault();\n\n        const next = nudge(\n          event.deltaY < 0 ? 1 : -1,\n          event.altKey ? fineStep() : step()\n        );\n\n        change(next, { event, reason: \"wheel\" });\n        commit(next);\n      };\n\n      root.addEventListener(\"wheel\", listener, { passive: false });\n\n      return () => {\n        root.removeEventListener(\"wheel\", listener);\n      };\n    }\n  );\n\n  const context: FaderContextValue = {\n    ariaLabel: () => props[\"aria-label\"],\n    ariaLabelledBy: () => props[\"aria-labelledby\"],\n    change,\n    commit,\n    disabled,\n    format,\n    handleKeyDown,\n    max,\n    min,\n    orientation,\n    originPosition,\n    pointer: (reason, event) => {\n      pointerReason = reason;\n      pointerEvent = event;\n    },\n    position,\n    resetValue,\n    size,\n    taper,\n    value,\n    variant,\n  };\n\n  const sliderValue = createMemo(() => [position()]);\n\n  return provideContext(FaderContext, context, () => (\n    <SliderPrimitive.Root\n      class={cn(\n        faderVariants({\n          orientation: orientation(),\n          size: size(),\n        }),\n        props.class,\n        props.className\n      )}\n      data-at-detent={detents().includes(value()) ? \"\" : undefined}\n      data-silent={value() === SILENCE_DB ? \"\" : undefined}\n      data-size={size()}\n      data-slot=\"fader\"\n      data-variant={variant()}\n      disabled={disabled()}\n      getValueLabel={() => format()(value())}\n      maxValue={1}\n      minValue={0}\n      name={props.name}\n      onChange={(next) => {\n        const nextPosition = next[0];\n\n        if (nextPosition === undefined) {\n          return;\n        }\n\n        change(fromPosition(nextPosition, pointerEvent?.altKey ?? false), {\n          event: pointerEvent,\n          reason: pointerReason,\n        });\n      }}\n      onChangeEnd={() => {\n        commit(latestValue);\n      }}\n      orientation={orientation()}\n      readOnly={props.readOnly}\n      ref={(node) => {\n        if (node instanceof HTMLDivElement) {\n          setRootElement(node);\n          setRefValue(props.ref, node);\n        }\n      }}\n      required={props.required}\n      step={POSITION_STEP}\n      value={sliderValue()}\n      {...rest}\n    >\n      <SliderCompat onChangeEnd={() => commit(latestValue)} />\n\n      {props.children ?? (\n        <FaderTrack>\n          <FaderRange />\n          <FaderThumb />\n        </FaderTrack>\n      )}\n    </SliderPrimitive.Root>\n  ));\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/solid/kobalte-slider.ts",
      "content": "import type { SliderContextValue } from \"@kobalte/core/slider\";\nimport * as Solid from \"solid-js\";\n\nimport { createCompatEffect } from \"@/lib/solid/effect\";\n\ninterface SliderCallbacks {\n  onChangeEnd?: () => void;\n  onSlideEnd?: (original: SliderContextValue[\"onSlideEnd\"]) => void;\n  onSlideMove?: (\n    original: SliderContextValue[\"onSlideMove\"],\n    delta: { deltaX: number; deltaY: number }\n  ) => void;\n  onSlideStart?: (\n    original: SliderContextValue[\"onSlideStart\"],\n    index: number,\n    value: number\n  ) => void;\n  suppressStep?: boolean;\n}\n\n/**\n * Kobalte 0.13.14 and 2.0.0-alpha.2 call these context methods dynamically.\n * SliderContextValue exposes them, but method replacement is not a documented\n * extension point, so keep the Solid 2 thumb-editability workaround and all\n * required pointer remapping in one adapter. Alpha.2's move handler calls\n * onChange twice: setThumbPercent emits the new value, then onChange(values())\n * replays the previous controlled value before Solid 2's queued update settles.\n * Its end handler likewise reads the old dragging array after writing it and\n * misses onChangeEnd. Adapt those two synchronous-read assumptions here, while\n * leaving vendor pointer capture, focus, keyboard and input handling intact.\n */\nexport const useKobalteSliderCompat = (\n  context: SliderContextValue,\n  callbacks: SliderCallbacks = {}\n): void => {\n  const originals = {\n    onSlideEnd: context.onSlideEnd,\n    onSlideMove: context.onSlideMove,\n    onSlideStart: context.onSlideStart,\n    onStepKeyDown: context.onStepKeyDown,\n  };\n\n  const queuedUpdates = \"onSettled\" in Solid;\n  let activeIndex: number | undefined;\n  let currentPosition: number | null = null;\n\n  const move: SliderContextValue[\"onSlideMove\"] = (delta) => {\n    if (!queuedUpdates) {\n      // 0.13.14 uses inversion alone for horizontal deltas, although thumb\n      // placement and track presses also use locale. Correct RTL before delegation.\n      const rtl = context.isSlidingFromLeft() === context.inverted();\n      originals.onSlideMove?.({\n        deltaX: rtl ? -delta.deltaX : delta.deltaX,\n        deltaY: delta.deltaY,\n      });\n\n      return;\n    }\n\n    if (activeIndex === undefined || context.state.isDisabled()) return;\n    const rect = context.trackRef()!.getBoundingClientRect();\n    const vertical = context.state.orientation() === \"vertical\";\n    const size = vertical ? rect.height : rect.width;\n\n    if (size <= 0) return;\n    currentPosition ??= context.state.getThumbPercent(activeIndex) * size;\n\n    const direction = vertical\n      ? context.isSlidingFromBottom()\n        ? -1\n        : 1\n      : context.isSlidingFromLeft()\n        ? 1\n        : -1;\n\n    currentPosition += (vertical ? delta.deltaY : delta.deltaX) * direction;\n    context.state.setThumbPercent(\n      activeIndex,\n      Math.min(1, Math.max(0, currentPosition / size))\n    );\n  };\n\n  context.onSlideStart = (index, value) => {\n    activeIndex = index;\n    currentPosition = null;\n\n    if (callbacks.onSlideStart)\n      callbacks.onSlideStart(originals.onSlideStart, index, value);\n    else originals.onSlideStart?.(index, value);\n  };\n\n  context.onSlideMove = (delta) => {\n    if (callbacks.onSlideMove) callbacks.onSlideMove(move, delta);\n    else move(delta);\n  };\n\n  context.onSlideEnd = () => {\n    if (callbacks.onSlideEnd) callbacks.onSlideEnd(originals.onSlideEnd);\n    else {\n      originals.onSlideEnd?.();\n\n      if (\n        queuedUpdates &&\n        activeIndex !== undefined &&\n        !context.state.isDisabled()\n      )\n        callbacks.onChangeEnd?.();\n    }\n\n    activeIndex = undefined;\n  };\n\n  if (callbacks.suppressStep) {\n    context.onStepKeyDown = () => undefined;\n  }\n\n  Solid.onCleanup(() => {\n    context.onSlideEnd = originals.onSlideEnd;\n    context.onSlideMove = originals.onSlideMove;\n    context.onSlideStart = originals.onSlideStart;\n    context.onStepKeyDown = originals.onStepKeyDown;\n  });\n\n  createCompatEffect(\n    () => [context.thumbs().length, context.state.isDisabled()] as const,\n    ([thumbCount, isDisabled]) => {\n      for (let index = 0; index < thumbCount; index += 1) {\n        context.state.setThumbEditable(index, !isDisabled);\n      }\n    }\n  );\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
