{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"slider","title":"Slider","description":"Scalar, range, discrete-label and form slider.","dependencies":["@base-ui/react@1.7.0","class-variance-authority@0.7.1"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/slider/slider.tsx","content":"\"use client\";\n\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  siteplaneUIDefaults,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\n\nconst sliderControlVariants = cva(\n  \"relative flex cursor-pointer touch-none select-none active:cursor-grabbing before:absolute before:content-[''] data-disabled:cursor-not-allowed data-[orientation=horizontal]:w-full data-[orientation=horizontal]:items-center data-[orientation=horizontal]:before:inset-x-0 data-[orientation=vertical]:h-full data-[orientation=vertical]:items-center data-[orientation=vertical]:flex-col data-[orientation=vertical]:before:inset-y-0\",\n  {\n    defaultVariants: { size: siteplaneUIDefaults.controlSize },\n    variants: {\n      size: {\n        md: \"data-[orientation=horizontal]:min-h-6 data-[orientation=horizontal]:min-w-44 data-[orientation=horizontal]:before:inset-y-[-10px] data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:min-w-6 data-[orientation=vertical]:before:inset-x-[-10px]\",\n        lg: \"data-[orientation=horizontal]:min-h-7 data-[orientation=horizontal]:min-w-48 data-[orientation=horizontal]:before:inset-y-[-12px] data-[orientation=vertical]:min-h-48 data-[orientation=vertical]:min-w-7 data-[orientation=vertical]:before:inset-x-[-12px]\",\n        sm: \"data-[orientation=horizontal]:min-h-5 data-[orientation=horizontal]:min-w-40 data-[orientation=horizontal]:before:inset-y-[-8px] data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:min-w-5 data-[orientation=vertical]:before:inset-x-[-8px]\",\n        xl: \"data-[orientation=horizontal]:min-h-8 data-[orientation=horizontal]:min-w-52 data-[orientation=horizontal]:before:inset-y-[-14px] data-[orientation=vertical]:min-h-52 data-[orientation=vertical]:min-w-8 data-[orientation=vertical]:before:inset-x-[-14px]\",\n      },\n    },\n  },\n);\n\nconst sliderTrackVariants = cva(\n  \"relative grow cursor-inherit select-none active:cursor-grabbing before:absolute data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=horizontal]:before:inset-x-0.5 data-[orientation=vertical]:before:inset-x-0 data-[orientation=horizontal]:before:inset-y-0 data-[orientation=vertical]:before:inset-y-0.5\",\n  {\n    defaultVariants: { size: siteplaneUIDefaults.controlSize },\n    variants: {\n      size: {\n        md: \"\",\n        lg: \"\",\n        sm: \"\",\n        xl: \"\",\n      },\n    },\n  },\n);\n\nconst sliderThumbVariants = cva(\n  \"block shrink-0 cursor-inherit select-none active:cursor-grabbing before:absolute before:inset-0 before:rounded-[inherit] has-focus-visible:ring-ring/24 data-dragging:scale-120 data-dragging:cursor-grabbing dark:has-focus-visible:ring-ring/48\",\n  {\n    defaultVariants: { size: siteplaneUIDefaults.controlSize },\n    variants: {\n      size: {\n        md: \"\",\n        lg: \"\",\n        sm: \"\",\n        xl: \"\",\n      },\n    },\n  },\n);\n\n// The value's type is the kit rule's at every size, with the scale labels\n// (design-rework.css): 12 px on an 18 px line, as the lg slider shows it.\nconst sliderValueVariants = cva(\"flex justify-end\", {\n  defaultVariants: { size: siteplaneUIDefaults.controlSize },\n  variants: {\n    size: {\n      md: \"\",\n      lg: \"\",\n      sm: \"\",\n      xl: \"\",\n    },\n  },\n});\n\n// The inset lines labels up with the thumb centers; end labels may use it to\n// stay inside the slider box. The labels tuck 4 px under the control's hit\n// area, across the 8 px gap of the Slider's stack.\nconst sliderLabelsVariants = cva(\n  \"@container relative -mt-3 mx-(--slider-labels-inset) h-4\",\n  {\n    defaultVariants: { size: siteplaneUIDefaults.controlSize },\n    variants: {\n      size: {\n        md: \"[--slider-labels-inset:--spacing(2.5)] sm:[--slider-labels-inset:--spacing(2)]\",\n        lg: \"[--slider-labels-inset:--spacing(3)] sm:[--slider-labels-inset:--spacing(2.5)]\",\n        sm: \"[--slider-labels-inset:--spacing(2.25)] sm:[--slider-labels-inset:--spacing(2)]\",\n        xl: \"[--slider-labels-inset:--spacing(3.5)] sm:[--slider-labels-inset:--spacing(3)]\",\n      },\n    },\n  },\n);\n\nconst SliderSizeContext = React.createContext<SiteplaneControlSize | null>(\n  null,\n);\n\nexport interface SliderProps\n  extends\n    Omit<WithStringClassName<SliderPrimitive.Root.Props>, \"size\">,\n    VariantProps<typeof sliderControlVariants> {\n  labels?: readonly { label: React.ReactNode; value: number }[];\n}\n\n// Base UI measures an edge-aligned thumb and the indicator from the control\n// when they mount, and their resize observer finds no control at that point.\n// Mounted inside a hidden container (display: none), they keep a 0 px\n// measurement and stay hidden after it shows. The kit remounts them each time\n// the control is rendered again.\nfunction useRenderedKey(\n  control: React.RefObject<HTMLDivElement | null>,\n): number {\n  const [renderedKey, setRenderedKey] = React.useState(0);\n\n  React.useEffect(() => {\n    const node = control.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return undefined;\n    let rendered = node.getClientRects().length > 0;\n    const observer = new ResizeObserver(() => {\n      const next = node.getClientRects().length > 0;\n      if (next && !rendered) setRenderedKey((key) => key + 1);\n      rendered = next;\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [control]);\n\n  return renderedKey;\n}\n\nfunction toSliderValues(value: number | readonly number[]) {\n  return typeof value === \"number\" ? [value] : [...value];\n}\n\nexport function Slider({\n  \"aria-label\": ariaLabel,\n  className,\n  children,\n  defaultValue,\n  labels,\n  value,\n  min = 0,\n  max = 100,\n  onValueChange,\n  orientation = \"horizontal\",\n  size,\n  ...props\n}: SliderProps): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  const controlRef = React.useRef<HTMLDivElement>(null);\n  const renderedKey = useRenderedKey(controlRef);\n\n  const values = React.useMemo(() => {\n    if (value !== undefined) return toSliderValues(value);\n    if (defaultValue !== undefined) return toSliderValues(defaultValue);\n    return [min];\n  }, [value, defaultValue, min]);\n  return (\n    <SliderSizeContext.Provider value={resolvedSize}>\n      <SliderPrimitive.Root\n        className={cn(\n          slotClass(\"slider\", props),\n          \"data-[orientation=horizontal]:w-full\",\n          className,\n        )}\n        data-size={resolvedSize}\n        data-slot=\"slider\"\n        defaultValue={defaultValue}\n        max={max}\n        min={min}\n        onValueChange={onValueChange}\n        orientation={orientation}\n        thumbAlignment=\"edge\"\n        value={value}\n        {...props}\n      >\n        {children}\n        <SliderPrimitive.Control\n          className={cn(\n            \"sp-slider-control\",\n            sliderControlVariants({ size: resolvedSize }),\n            \"data-dragging:cursor-grabbing\",\n          )}\n          data-size={resolvedSize}\n          data-slot=\"slider-control\"\n          ref={controlRef}\n        >\n          <SliderPrimitive.Track\n            className={cn(\n              \"sp-slider-track\",\n              sliderTrackVariants({ size: resolvedSize }),\n              \"data-dragging:cursor-grabbing\",\n            )}\n            data-size={resolvedSize}\n            data-slot=\"slider-track\"\n          >\n            <SliderPrimitive.Indicator\n              // Its motion, and the reduced-motion switch, are the kit's\n              // Slider rules (design-rework.css).\n              className=\"sp-slider-indicator select-none data-[orientation=horizontal]:ms-0.5 data-[orientation=vertical]:mb-0.5\"\n              data-slot=\"slider-indicator\"\n              key={`indicator-${renderedKey}`}\n            />\n            {Array.from({ length: values.length }, (_, index) => (\n              <SliderPrimitive.Thumb\n                aria-label={ariaLabel}\n                className={cn(\n                  \"sp-slider-thumb\",\n                  sliderThumbVariants({ size: resolvedSize }),\n                  \"data-dragging:cursor-grabbing\",\n                )}\n                data-size={resolvedSize}\n                data-slot=\"slider-thumb\"\n                index={index}\n                key={`${index}-${renderedKey}`}\n              />\n            ))}\n          </SliderPrimitive.Track>\n        </SliderPrimitive.Control>\n        {labels?.length ? (\n          <SliderLabels\n            labels={labels}\n            max={max}\n            min={min}\n            orientation={orientation}\n            size={resolvedSize}\n          />\n        ) : null}\n      </SliderPrimitive.Root>\n    </SliderSizeContext.Provider>\n  );\n}\n\nfunction SliderLabels({\n  labels,\n  max,\n  min,\n  orientation,\n  size,\n}: {\n  labels: NonNullable<SliderProps[\"labels\"]>;\n  max: number;\n  min: number;\n  orientation: \"horizontal\" | \"vertical\";\n  size: SiteplaneControlSize;\n}) {\n  return (\n    <SliderPrimitive.Value\n      render={(_, state) => (\n        <div\n          className={cn(\n            \"sp-slider-labels\",\n            sliderLabelsVariants({ size }),\n            \"data-[orientation=vertical]:hidden\",\n          )}\n          data-orientation={orientation}\n          data-slot=\"slider-labels\"\n        >\n          {labels.map(({ label, value: labelValue }) => {\n            const position =\n              max === min\n                ? 0\n                : Math.min(\n                    100,\n                    Math.max(0, ((labelValue - min) / (max - min)) * 100),\n                  );\n            const fraction = position / 100;\n\n            return (\n              <span\n                className={cn(\n                  \"absolute top-0 whitespace-nowrap text-muted-foreground\",\n                  state.values.includes(labelValue) &&\n                    \"font-(--sp-rw-font-weight-strong) text-foreground\",\n                )}\n                data-active={state.values.includes(labelValue) || undefined}\n                key={labelValue}\n                style={{\n                  left: `${position}%`,\n                  // Centered under its value, clamped to the slider box.\n                  translate: `clamp(${-fraction} * 100cqw - var(--slider-labels-inset), -50%, ${1 - fraction} * 100cqw + var(--slider-labels-inset) - 100%)`,\n                }}\n              >\n                {label}\n              </span>\n            );\n          })}\n        </div>\n      )}\n    />\n  );\n}\n\nexport interface SliderValueProps\n  extends\n    Omit<WithStringClassName<SliderPrimitive.Value.Props>, \"size\">,\n    VariantProps<typeof sliderValueVariants> {}\n\nexport function SliderValue({\n  className,\n  size,\n  ...props\n}: SliderValueProps): React.ReactElement {\n  const groupSize = React.useContext(SliderSizeContext);\n  const resolvedSize = useResolvedControlSize(size ?? groupSize);\n\n  return (\n    <SliderPrimitive.Value\n      className={cn(\n        slotClass(\"slider-value\", props),\n        sliderValueVariants({ size: resolvedSize }),\n        className,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"slider-value\"\n      {...props}\n    />\n  );\n}\n\nexport { SliderPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/slider/slider.tsx"},{"path":"packages/ui/src/components/slider/index.ts","content":"export * from \"./slider\";\n","type":"registry:ui","target":"~/src/siteplane/components/slider/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/slider.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid when people need to enter a precise value; use Number Field.","Avoid for a choice among a few named options; use Radio Group or Toggle Group."],"description":"Scalar, range, discrete-label and form slider.","knownLimitations":["Chromium may report the hidden native range input as missing an accessible label."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["slider"],"useWhen":["Use to choose a number or a range by moving one or more thumbs when an approximate value is enough."]}}