{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "author": "@fma-ds/ds",
  "description": "A draggable single- or range-value slider, horizontal or vertical. Pass an array default/value to get one thumb per value.\n\n- **`label`**: names it above the track.\n- **`showValue`**: prints the value (or range) beside it, formatted by `formatValue` (\"200 pedidos\") or `formatOptions` (currency, percent — also what screen readers announce).\n- **`marks`**: labelled stops under the track (`number` or `{ value, label }`). Clicking one moves the nearest thumb there and fires `onChangeEnd`, while the keyboard keeps using the thumb.\n- **`limit`**: caps the value below `maxValue` (stock left, plan quota). The scale and marks still run to `maxValue`, the track above the limit is drawn as unavailable and its marks are disabled.\n- **`description`**: under the track, says why, and is read with the value.",
  "dependencies": [
    "cn@^0.5.0",
    "react-aria-components@^1.22.1"
  ],
  "registryDependencies": [
    "@fma-ds/label"
  ],
  "files": [
    {
      "path": "src/core/slider/slider.tsx",
      "content": "import { cn } from \"cn\";\nimport * as React from \"react\";\nimport {\n  SliderFill,\n  SliderOutput,\n  Slider as SliderPrimitive,\n  type SliderProps as SliderPrimitiveProps,\n  type SliderState,\n  SliderThumb,\n  SliderTrack,\n} from \"react-aria-components\";\nimport { Label } from \"@/core/label/label\";\n\ntype SliderValue = number | number[];\n\ntype SliderMark = number | { value: number; label?: React.ReactNode };\n\ntype SliderProps<T extends SliderValue = SliderValue> = Omit<\n  SliderPrimitiveProps<T>,\n  \"className\" | \"children\"\n> & {\n  className?: string;\n  /** Visible label above the track; names the slider. */\n  label?: React.ReactNode;\n  /** Shows the current value (or range) next to the label. */\n  showValue?: boolean;\n  /**\n   * Formats the value shown by `showValue` (\"200 pedidos\"). Without it, and for\n   * marks without a `label`, `formatOptions` applies; that is also what screen\n   * readers announce.\n   */\n  formatValue?: (value: number) => React.ReactNode;\n  /**\n   * Labelled stops under the track. Clicking one moves the nearest thumb\n   * there; the keyboard keeps using the thumb.\n   */\n  marks?: SliderMark[];\n  /**\n   * A cap below `maxValue`: the scale and the marks still run to `maxValue`,\n   * but the value stops here (dragging, keys and marks above it), and the part\n   * of the track above it is drawn as unavailable. Say why in `description`.\n   */\n  limit?: number;\n  /** Text under the track (\"Only 20 left\"); screen readers read it with the value. */\n  description?: React.ReactNode;\n};\n\nconst toMark = (mark: SliderMark) => (typeof mark === \"number\" ? { value: mark } : mark);\n\nfunction clampTo<T extends SliderValue>(value: T, limit: number): T {\n  return (\n    Array.isArray(value) ? value.map((item) => Math.min(item, limit)) : Math.min(value, limit)\n  ) as T;\n}\n\nconst sameValue = (a: SliderValue, b: SliderValue) =>\n  Array.isArray(a) && Array.isArray(b)\n    ? a.length === b.length && a.every((item, index) => item === b[index])\n    : a === b;\n\n/** Moves the thumb closest to `value` there, reporting it as a finished change; returns its index. */\nfunction moveNearestThumb(state: SliderState, value: number) {\n  let nearest = 0;\n  state.values.forEach((current, index) => {\n    const best = state.values[nearest] ?? current;\n    if (Math.abs(current - value) < Math.abs(best - value)) nearest = index;\n  });\n  state.setThumbDragging(nearest, true);\n  state.setThumbValue(nearest, value);\n  state.setThumbDragging(nearest, false);\n  return nearest;\n}\n\nfunction SliderMarks({\n  marks,\n  state,\n  limit,\n}: {\n  marks: SliderMark[];\n  state: SliderState;\n  limit?: number;\n}) {\n  return (\n    <div\n      data-slot=\"slider-marks\"\n      className=\"relative text-xs text-muted-foreground group-data-horizontal:h-5 group-data-horizontal:w-full group-data-vertical:min-w-8\"\n    >\n      {marks.map(toMark).map(({ value, label }) => {\n        const percent = state.getValuePercent(value) * 100;\n        return (\n          <button\n            key={value}\n            type=\"button\"\n            // The thumb is the keyboard path; marks are a pointer shortcut.\n            tabIndex={-1}\n            data-slot=\"slider-mark\"\n            disabled={state.isDisabled || (limit !== undefined && value > limit)}\n            // Hand focus to the moved thumb, so the arrow keys pick up from there.\n            onMouseDown={(event) => event.preventDefault()}\n            onClick={(event) => {\n              const index = moveNearestThumb(state, value);\n              event.currentTarget\n                .closest(\"[data-slot=slider]\")\n                ?.querySelectorAll<HTMLInputElement>(\"input[type=range]\")\n                [index]?.focus();\n            }}\n            style={\n              state.orientation === \"vertical\" ? { bottom: `${percent}%` } : { left: `${percent}%` }\n            }\n            className={cn(\n              \"absolute rounded-sm px-1 whitespace-nowrap tabular-nums transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\",\n              \"group-data-horizontal:top-0 group-data-vertical:left-0\",\n              percent <= 0\n                ? \"group-data-horizontal:-translate-x-1 group-data-vertical:translate-y-0\"\n                : percent >= 100\n                  ? \"group-data-horizontal:-translate-x-[calc(100%-0.25rem)] group-data-vertical:translate-y-full\"\n                  : \"group-data-horizontal:-translate-x-1/2 group-data-vertical:translate-y-1/2\",\n            )}\n          >\n            {label ?? state.getFormattedValue(value)}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction Slider<T extends SliderValue = SliderValue>({\n  className,\n  label,\n  showValue = false,\n  formatValue,\n  marks,\n  limit,\n  description,\n  ...props\n}: SliderProps<T>) {\n  const descriptionId = React.useId();\n  const [uncontrolled, setUncontrolled] = React.useState(props.defaultValue);\n\n  // With a limit the value is held here, so a change past it can be pulled back.\n  let limitProps: Partial<SliderPrimitiveProps<T>> = {};\n  if (limit !== undefined) {\n    const { value, onChange, onChangeEnd } = props;\n    const current = clampTo(value ?? uncontrolled ?? ((props.minValue ?? 0) as T), limit);\n    limitProps = {\n      defaultValue: undefined,\n      value: current,\n      onChange: (next) => {\n        const clamped = clampTo(next, limit);\n        if (sameValue(clamped, current)) return;\n        if (value === undefined) setUncontrolled(clamped);\n        onChange?.(clamped);\n      },\n      onChangeEnd: onChangeEnd && ((next) => onChangeEnd(clampTo(next, limit))),\n    };\n  }\n\n  return (\n    <SliderPrimitive\n      className={cn(\n        \"group flex w-full touch-none flex-col gap-2 select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto\",\n        className,\n      )}\n      data-slot=\"slider\"\n      {...props}\n      {...limitProps}\n      aria-describedby={\n        cn(props[\"aria-describedby\"], description != null && descriptionId) || undefined\n      }\n    >\n      {({ state }) => {\n        const format = formatValue ?? ((value: number) => state.getFormattedValue(value));\n        const limitPercent =\n          limit !== undefined && limit < (props.maxValue ?? 100)\n            ? state.getValuePercent(limit) * 100\n            : undefined;\n        return (\n          <>\n            {(label || showValue) && (\n              <div data-slot=\"slider-header\" className=\"flex items-center justify-between gap-3\">\n                {label && <Label>{label}</Label>}\n                {showValue && (\n                  <SliderOutput\n                    data-slot=\"slider-value\"\n                    className=\"ml-auto text-sm text-muted-foreground tabular-nums\"\n                  >\n                    {state.values.map((value, index) => (\n                      // biome-ignore lint/suspicious/noArrayIndexKey: one entry per thumb, in thumb order\n                      <span key={index}>\n                        {index > 0 && \" – \"}\n                        {format(value)}\n                      </span>\n                    ))}\n                  </SliderOutput>\n                )}\n              </div>\n            )}\n            <div className=\"flex flex-col gap-2 group-data-vertical:flex-1 group-data-vertical:flex-row\">\n              <div\n                data-slot=\"slider-control\"\n                className=\"relative flex w-full items-center group-data-vertical:h-full group-data-vertical:w-auto group-data-vertical:flex-col\"\n              >\n                <SliderTrack\n                  data-slot=\"slider-track\"\n                  className=\"relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1\"\n                >\n                  <SliderFill\n                    data-slot=\"slider-range\"\n                    className=\"absolute bg-primary select-none data-horizontal:h-full data-vertical:w-full\"\n                  />\n                  {limitPercent !== undefined && (\n                    <div\n                      aria-hidden=\"true\"\n                      data-slot=\"slider-unavailable\"\n                      style={\n                        state.orientation === \"vertical\"\n                          ? { bottom: `${limitPercent}%` }\n                          : { left: `${limitPercent}%` }\n                      }\n                      className=\"absolute bg-[repeating-linear-gradient(90deg,var(--muted-foreground)_0_2px,transparent_2px_5px)] opacity-40 group-data-horizontal:inset-y-0 group-data-horizontal:right-0 group-data-vertical:inset-x-0 group-data-vertical:top-0 group-data-vertical:bg-[repeating-linear-gradient(0deg,var(--muted-foreground)_0_2px,transparent_2px_5px)]\"\n                    />\n                  )}\n                </SliderTrack>\n                {limitPercent !== undefined && (\n                  <span\n                    aria-hidden=\"true\"\n                    data-slot=\"slider-limit\"\n                    style={\n                      state.orientation === \"vertical\"\n                        ? { bottom: `${limitPercent}%` }\n                        : { left: `${limitPercent}%` }\n                    }\n                    className=\"absolute rounded-full bg-muted-foreground/60 group-data-horizontal:h-3 group-data-horizontal:w-0.5 group-data-horizontal:-translate-x-1/2 group-data-vertical:h-0.5 group-data-vertical:w-3 group-data-vertical:translate-y-1/2\"\n                  />\n                )}\n                {state.values.map((_, index) => (\n                  <SliderThumb\n                    data-slot=\"slider-thumb\"\n                    // biome-ignore lint/suspicious/noArrayIndexKey: index IS the thumb's positional identity (also passed as the `index` prop below)\n                    key={index}\n                    index={index}\n                    className=\"relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none group-data-horizontal:top-[50%] group-data-vertical:left-[50%] after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50\"\n                  />\n                ))}\n              </div>\n              {marks && marks.length > 0 && (\n                <SliderMarks marks={marks} state={state} limit={limit} />\n              )}\n            </div>\n            {description != null && (\n              <p\n                id={descriptionId}\n                data-slot=\"slider-description\"\n                className=\"text-sm text-muted-foreground\"\n              >\n                {description}\n              </p>\n            )}\n          </>\n        );\n      }}\n    </SliderPrimitive>\n  );\n}\n\nexport type { SliderMark, SliderProps };\nexport { Slider };\n",
      "type": "registry:component",
      "target": "components/fma-ds/slider.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "customized",
    "license": "MIT",
    "origin": "shadcn/ui, style: aria-nova (base: react-aria-components)",
    "tags": [
      "forms",
      "input",
      "react-aria"
    ],
    "contentHash": "d2abc2cf276f"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}