{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pan-control",
  "title": "Pan Control",
  "description": "Left and right balance with a fill from the centre, a centre detent and reset.",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn/core",
    "@audiocn/use-audio-config"
  ],
  "files": [
    {
      "path": "components/ui/pan-control.tsx",
      "content": "\"use client\";\n\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { cva } from \"class-variance-authority\";\nimport { useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\nimport { useAudioConfig } from \"@/hooks/use-audio-config\";\nimport type { AudioSize } from \"@/hooks/use-audio-config\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { cn } from \"@/lib/utils\";\n\nconst PERCENT = 100;\nconst DETENT_RANGE = 0.08;\n\n/** \"L30\", \"C\", \"R30\". */\nexport const formatPan = (value: number): string => {\n  const amount = Math.round(Math.abs(value) * PERCENT);\n  if (amount === 0) {\n    return \"C\";\n  }\n  return `${value < 0 ? \"L\" : \"R\"}${amount}`;\n};\n\nconst CENTER_TEXT = /^c(?:enter|entre)?$/iu;\nconst SIDE_PREFIX = /^[LR]/iu;\n\n/** Reads \"L30\", \"R15\", \"C\" or a number from −100 to 100; the inverse of formatPan. */\nexport const parsePan = (text: string): number | null => {\n  const trimmed = text.trim().replaceAll(\"\\u2212\", \"-\");\n  if (CENTER_TEXT.test(trimmed)) {\n    return 0;\n  }\n  const side = SIDE_PREFIX.test(trimmed) ? trimmed[0]?.toUpperCase() : null;\n  const digits = (side ? trimmed.slice(1) : trimmed).trim();\n  const amount = Number(digits) / PERCENT;\n  if (digits === \"\" || Number.isNaN(amount)) {\n    return null;\n  }\n  return side === \"L\" ? -Math.abs(amount) : amount;\n};\n\nexport const describePan = (value: number): string => {\n  const amount = Math.round(Math.abs(value) * PERCENT);\n  if (amount === 0) {\n    return \"Center\";\n  }\n  return `${amount}% ${value < 0 ? \"left\" : \"right\"}`;\n};\n\nconst panControlVariants = cva(\n  \"group/pan-control relative flex w-full touch-none items-center select-none data-disabled:opacity-50\",\n  {\n    defaultVariants: { size: \"default\" },\n    variants: {\n      size: {\n        default: \"[--pan-thumb-size:0.875rem] [--pan-track-size:0.25rem]\",\n        lg: \"[--pan-thumb-size:1rem] [--pan-track-size:0.375rem]\",\n        sm: \"[--pan-thumb-size:0.75rem] [--pan-track-size:0.1875rem]\",\n      },\n    },\n  }\n);\n\nexport interface PanControlProps extends Omit<\n  SliderPrimitive.Root.Props<number>,\n  | \"value\"\n  | \"defaultValue\"\n  | \"onValueChange\"\n  | \"onValueCommitted\"\n  | \"min\"\n  | \"max\"\n  | \"orientation\"\n  | \"format\"\n> {\n  /** −1 (left) to 1 (right). */\n  value?: number;\n  /** Default 0. */\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n  onValueCommitted?: (value: number) => void;\n  /** Default 0.05. */\n  step?: number;\n  /** Snap to the centre while dragging near it. Default true. */\n  detent?: boolean;\n  /** Default: \"L30\", \"C\", \"R30\". */\n  format?: (value: number) => string;\n  size?: AudioSize;\n}\n\nexport const PanControl = ({\n  value: valueProp,\n  defaultValue = 0,\n  onValueChange,\n  onValueCommitted,\n  step = 0.05,\n  largeStep = 0.25,\n  detent = true,\n  format = formatPan,\n  size: sizeProp,\n  disabled: disabledProp,\n  className,\n  ...props\n}: PanControlProps) => {\n  const config = useAudioConfig();\n  const size = sizeProp ?? config.size ?? \"default\";\n  const disabled = disabledProp ?? config.disabled ?? false;\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    clamp(defaultValue, -1, 1)\n  );\n  const value = valueProp ?? uncontrolled;\n\n  const setValue = (next: number) => {\n    if (valueProp === undefined) {\n      setUncontrolled(next);\n    }\n    onValueChange?.(next);\n  };\n\n  const start = Math.min(0, value);\n  const end = Math.max(0, value);\n\n  return (\n    <SliderPrimitive.Root\n      className={cn(panControlVariants({ size }), className)}\n      data-centered={value === 0 ? \"\" : undefined}\n      data-size={size}\n      data-slot=\"pan-control\"\n      disabled={disabled}\n      largeStep={largeStep}\n      max={1}\n      min={-1}\n      onValueChange={(next, details) => {\n        const snapped =\n          detent && details.reason === \"drag\" && Math.abs(next) < DETENT_RANGE\n            ? 0\n            : next;\n        setValue(snapped);\n      }}\n      onValueCommitted={(next) => {\n        onValueCommitted?.(detent && Math.abs(next) < DETENT_RANGE ? 0 : next);\n      }}\n      step={step}\n      value={value}\n      {...props}\n    >\n      <SliderPrimitive.Control className=\"relative flex h-(--pan-thumb-size) w-full items-center px-[calc(var(--pan-thumb-size)/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-(--pan-track-size) w-full grow rounded-full\"\n          data-slot=\"pan-control-track\"\n        >\n          <span\n            aria-hidden\n            className=\"bg-border absolute top-1/2 left-1/2 h-[calc(var(--pan-track-size)*3)] w-px -translate-x-1/2 -translate-y-1/2\"\n            data-slot=\"pan-control-center\"\n          />\n          <div\n            className=\"bg-primary absolute inset-y-0 left-(--pan-range-start) w-(--pan-range-size) rounded-full\"\n            data-slot=\"pan-control-range\"\n            style={\n              {\n                \"--pan-range-size\": `${((end - start) / 2) * PERCENT}%`,\n                \"--pan-range-start\": `${((start + 1) / 2) * PERCENT}%`,\n              } as CSSProperties\n            }\n          />\n          <SliderPrimitive.Thumb\n            aria-label=\"Pan\"\n            className=\"bg-background ring-foreground/15 hover:ring-ring/30 focus-visible:ring-ring/40 data-dragging:ring-ring/30 block size-(--pan-thumb-size) 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=\"pan-control-thumb\"\n            getAriaValueText={() => describePan(value)}\n            onDoubleClick={() => {\n              setValue(0);\n              onValueCommitted?.(0);\n            }}\n          />\n        </SliderPrimitive.Track>\n      </SliderPrimitive.Control>\n      <span className=\"sr-only\">{format(value)}</span>\n    </SliderPrimitive.Root>\n  );\n};\n\nexport { panControlVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}