{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "Picks a value or a range by feel, as a thin line, a thick volume track, labelled steps or a value bubble, with full keyboard control.",
  "dependencies": [
    "radix-ui",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/slider.tsx",
      "content": "import type * as React from \"react\"\nimport { useState } from \"react\"\nimport { Slider as SliderPrimitive } from \"radix-ui\"\nimport {\n  composePointer,\n  resolveMarks,\n  SliderMarks,\n  toPercent,\n  useRubberStretch,\n  type SliderMark,\n} from \"@/registry/miseui/ui/slider-parts\"\nimport { HALF_THUMB_PX, SliderStage, type SliderVariant } from \"@/registry/miseui/ui/slider-stage\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype SliderEffect = \"rubber\"\n\ntype SliderProps = Omit<React.ComponentProps<typeof SliderPrimitive.Root>, \"orientation\" | \"inverted\"> & {\n  /** `plain` thin line, `filled` thick track with the thumb inside, `stepped` ticks and labels, `label` value bubble. */\n  variant?: SliderVariant\n  /** `rubber` stretches the slider with resistance when dragged past an end. */\n  effect?: SliderEffect\n  /** Ticks for `stepped`. Omitted, they are worked out from `step`. */\n  marks?: readonly SliderMark[]\n  /** Text for the bubble, the tick labels and assistive tech. */\n  formatValue?: (value: number) => string\n}\n\nconst ROOT_CLASS = \"relative flex w-full touch-none flex-col select-none data-disabled:opacity-50\"\n\n/** Mirrors Radix's own state, because fill, ticks and bubble have to know the value in React. */\nfunction useTrackedValues({ value, defaultValue, onValueChange }: SliderProps, min: number) {\n  const [uncontrolled, setUncontrolled] = useState(defaultValue ?? [min])\n  return {\n    values: value ?? uncontrolled,\n    onValueChange: (next: number[]) => {\n      setUncontrolled(next)\n      onValueChange?.(next)\n    },\n  }\n}\n\nfunction Slider({\n  variant = \"plain\",\n  effect,\n  marks,\n  formatValue = String,\n  className,\n  min = 0,\n  max = 100,\n  step = 1,\n  \"aria-label\": label,\n  \"aria-labelledby\": labelledBy,\n  \"aria-describedby\": describedBy,\n  ...props\n}: SliderProps) {\n  const { values, onValueChange } = useTrackedValues(props, min)\n  const percents = values.map((entry) => toPercent(entry, min, max))\n  const rubber = useRubberStretch(effect === \"rubber\" && !props.disabled)\n  const stage = { variant, values, percents, formatValue, rubber, thumbAria: { label, labelledBy, describedBy } }\n  const scale = { min, max, halfThumb: HALF_THUMB_PX[variant] }\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      data-variant={variant}\n      data-effect={effect}\n      className={cn(ROOT_CLASS, variant === \"label\" && \"pt-8\", className)}\n      min={min}\n      max={max}\n      step={step}\n      {...props}\n      {...composePointer(props, rubber.handlers)}\n      onValueChange={onValueChange}\n    >\n      <SliderStage {...stage} />\n      {variant === \"stepped\" ? (\n        <SliderMarks\n          {...scale}\n          marks={resolveMarks(marks, { min, max, step, format: formatValue })}\n          low={values.length > 1 ? values[0] : min}\n          high={values[values.length - 1]}\n          values={values}\n        />\n      ) : null}\n    </SliderPrimitive.Root>\n  )\n}\n\nexport { Slider, type SliderProps, type SliderVariant, type SliderEffect, type SliderMark }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/slider-stage.tsx",
      "content": "import type * as React from \"react\"\nimport { Slider as SliderPrimitive } from \"radix-ui\"\nimport { motion } from \"motion/react\"\nimport { thumbCentreOffset, type useRubberStretch } from \"@/registry/miseui/ui/slider-parts\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype SliderVariant = \"plain\" | \"filled\" | \"stepped\" | \"label\"\n\n/** Half the rendered thumb width. Radix measures the thumb, so these must match THUMB_CLASS or the fill drifts. */\nconst HALF_THUMB_PX: Record<SliderVariant, number> = { plain: 10, filled: 12, stepped: 10, label: 10 }\n\nconst ROUND_THUMB_CLASS = \"block size-5 rounded-full border border-input bg-background shadow-sm dark:bg-foreground\"\n\nconst THUMB_CLASS: Record<SliderVariant, string> = {\n  plain: ROUND_THUMB_CLASS,\n  filled: \"flex h-8 w-6 items-center justify-center rounded-lg\",\n  stepped: ROUND_THUMB_CLASS,\n  label: ROUND_THUMB_CLASS,\n}\n\nconst TRACK_CLASS: Record<SliderVariant, string> = {\n  plain: \"h-1 rounded-full bg-input\",\n  filled: \"h-8 overflow-hidden rounded-lg bg-input/70\",\n  stepped: \"h-1 rounded-full bg-input\",\n  label: \"h-1 rounded-full bg-input\",\n}\n\nfunction thumbName(label: string | undefined, index: number, total: number) {\n  if (!label || total < 2) return label\n  if (index === 0) return `${label}, minimum`\n  return index === total - 1 ? `${label}, maximum` : `${label}, ${index + 1} of ${total}`\n}\n\nfunction fillStyle(percents: readonly number[], variant: SliderVariant): React.CSSProperties {\n  const halfThumb = HALF_THUMB_PX[variant]\n  // A filled track wraps the whole thumb, so its grip always sits on the fill and a stub shows even at zero.\n  const pad = variant === \"filled\" ? halfThumb : 0\n  const first = percents[0]\n  const last = percents[percents.length - 1]\n  const end = thumbCentreOffset(last, halfThumb) + pad\n  if (percents.length < 2) return { insetInlineStart: 0, width: `calc(${last}% + ${end}px)` }\n  const start = thumbCentreOffset(first, halfThumb) - pad\n  return {\n    insetInlineStart: `calc(${first}% + ${start}px)`,\n    width: `calc(${last - first}% + ${end - start}px)`,\n  }\n}\n\ntype SliderThumbProps = React.ComponentProps<typeof SliderPrimitive.Thumb> & {\n  variant: SliderVariant\n  percent: number\n  text: string\n}\n\nfunction SliderThumb({ variant, percent, text, ...props }: SliderThumbProps) {\n  return (\n    <SliderPrimitive.Thumb\n      data-slot=\"slider-thumb\"\n      aria-valuetext={text}\n      className={cn(\n        THUMB_CLASS[variant],\n        \"relative outline-none transition-[scale,box-shadow] duration-(--duration-fast) ease-(--ease-mise) active:duration-100\",\n        \"focus-visible:ring-[3px] focus-visible:ring-ring/50 data-disabled:cursor-not-allowed\",\n        // The target grows without the thumb growing, so Radix's measurement and the fill stay in step.\n        \"before:absolute before:-inset-2.5 before:content-['']\",\n        variant !== \"filled\" && \"active:scale-110\",\n      )}\n      {...props}\n    >\n      {variant === \"filled\" ? (\n        <span data-slot=\"slider-grip\" className=\"h-4 w-1 rounded-full bg-mise-foreground/70\" />\n      ) : null}\n      {variant === \"label\" ? (\n        // Anchored by percent, so the bubble ends flush with the track at both ends instead of overflowing it.\n        <span\n          data-slot=\"slider-bubble\"\n          className=\"absolute bottom-full mb-2 flex h-6 items-center rounded-md bg-foreground px-2 text-[13px] whitespace-nowrap text-background tabular-nums shadow-sm\"\n          style={{ left: `${percent}%`, transform: `translateX(-${percent}%)` }}\n        >\n          {text}\n        </span>\n      ) : null}\n    </SliderPrimitive.Thumb>\n  )\n}\n\n/** Focus lands on a thumb, not on the root, so name and description have to reach it to be heard. */\ninterface ThumbAria {\n  label: string | undefined\n  labelledBy: string | undefined\n  describedBy: string | undefined\n}\n\ninterface SliderStageProps {\n  variant: SliderVariant\n  values: readonly number[]\n  percents: readonly number[]\n  formatValue: (value: number) => string\n  rubber: ReturnType<typeof useRubberStretch>\n  thumbAria: ThumbAria\n}\n\n/** Transforms live here, not on the root, whose rect Radix reads to turn a pointer into a value. */\nfunction SliderStage({ variant, values, percents, formatValue, rubber, thumbAria }: SliderStageProps) {\n  return (\n    <motion.span\n      data-slot=\"slider-stage\"\n      data-stretch={rubber.side ?? undefined}\n      className={cn(\n        \"relative flex w-full items-center\",\n        variant === \"filled\" ? \"h-8 pointer-coarse:h-10\" : \"h-5 pointer-coarse:h-10\",\n      )}\n      style={rubber.active ? rubber.style : undefined}\n    >\n      <SliderPrimitive.Track data-slot=\"slider-track\" className={cn(\"relative w-full grow\", TRACK_CLASS[variant])}>\n        <span\n          data-slot=\"slider-fill\"\n          className=\"absolute inset-y-0 rounded-[inherit] bg-mise\"\n          style={fillStyle(percents, variant)}\n        />\n      </SliderPrimitive.Track>\n      {values.map((entry, index) => (\n        <SliderThumb\n          key={index}\n          variant={variant}\n          percent={percents[index]}\n          text={formatValue(entry)}\n          aria-label={thumbName(thumbAria.label, index, values.length)}\n          aria-labelledby={thumbAria.labelledBy}\n          aria-describedby={thumbAria.describedBy}\n        />\n      ))}\n    </motion.span>\n  )\n}\n\nexport { HALF_THUMB_PX, SliderStage, type SliderVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/slider-parts.tsx",
      "content": "import type * as React from \"react\"\nimport { useRef, useState } from \"react\"\nimport { animate, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\nimport { rubberband, SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface SliderMark {\n  value: number\n  label?: React.ReactNode\n}\n\ntype StretchSide = \"left\" | \"right\"\n\nconst MAX_AUTO_TICKS = 11\nconst MAX_AUTO_LABELS = 6\n/** The track gets thinner as it stretches, the way a rubber band does; a quarter of the stretch keeps it subtle. */\nconst SQUASH_FACTOR = 0.25\n\n/**\n * Radix keeps a thumb inside the track by pulling it inward as it nears an end, so its centre is not at\n * `percent`. Fill and ticks use the same formula to meet the thumb's centre exactly. In px past `percent`.\n */\nfunction thumbCentreOffset(percent: number, halfThumb: number) {\n  return halfThumb * (1 - percent / 50)\n}\n\nfunction thumbCentre(percent: number, halfThumb: number) {\n  return `calc(${percent}% + ${thumbCentreOffset(percent, halfThumb)}px)`\n}\n\nfunction toPercent(value: number, min: number, max: number) {\n  if (max === min) return 0\n  return ((value - min) / (max - min)) * 100\n}\n\n/** Evenly spaced ticks from the step, thinned to a countable number; labels only where they can all fit. */\nfunction resolveMarks(\n  marks: readonly SliderMark[] | undefined,\n  { min, max, step, format }: { min: number; max: number; step: number; format: (value: number) => React.ReactNode },\n): readonly SliderMark[] {\n  if (marks) return marks\n  const intervals = Math.max(1, Math.round((max - min) / step))\n  const stride = Math.ceil(intervals / (MAX_AUTO_TICKS - 1))\n  const count = Math.floor(intervals / stride) + 1\n  const labelAll = count <= MAX_AUTO_LABELS\n  return Array.from({ length: count }, (_, index) => {\n    const value = min + index * stride * step\n    const showLabel = labelAll || index === 0 || index === count - 1\n    return { value, label: showLabel ? format(value) : undefined }\n  })\n}\n\ninterface SliderMarksProps {\n  marks: readonly SliderMark[]\n  min: number\n  max: number\n  halfThumb: number\n  /** Everything between these is inside the selected range. */\n  low: number\n  high: number\n  values: readonly number[]\n}\n\nfunction SliderMarks({ marks, min, max, halfThumb, low, high, values }: SliderMarksProps) {\n  return (\n    // The touch stage is taller than the track; pull the marks back up so they still read as part of it.\n    <span data-slot=\"slider-marks\" className=\"relative block h-8 w-full pointer-coarse:-mt-2\">\n      {marks.map((mark) => {\n        const percent = toPercent(mark.value, min, max)\n        const inRange = mark.value >= low && mark.value <= high\n        return (\n          <span key={mark.value}>\n            <span\n              data-slot=\"slider-tick\"\n              className={cn(\"absolute top-0 h-2 w-px -translate-x-1/2\", inRange ? \"bg-foreground/60\" : \"bg-border\")}\n              style={{ insetInlineStart: thumbCentre(percent, halfThumb) }}\n            />\n            {mark.label === undefined ? null : (\n              // Anchored by percent, so the first and last labels end flush with the track instead of overflowing it.\n              <span\n                data-current={values.includes(mark.value) ? \"true\" : \"false\"}\n                className=\"absolute top-3 text-[13px] leading-5 whitespace-nowrap text-muted-foreground tabular-nums data-[current=true]:font-medium data-[current=true]:text-foreground\"\n                style={{ insetInlineStart: `${percent}%`, transform: `translateX(-${percent}%)` }}\n              >\n                {mark.label}\n              </span>\n            )}\n          </span>\n        )\n      })}\n    </span>\n  )\n}\n\n/** Stretches the slider past its ends while dragging. Pointer moves arrive here because Radix captures the pointer. */\nfunction useRubberStretch(enabled: boolean) {\n  const reduceMotion = useReducedMotion() ?? false\n  const active = enabled && !reduceMotion\n  const overshoot = useMotionValue(0)\n  const scaleX = useTransform(overshoot, (ratio) => 1 + Math.abs(ratio))\n  const scaleY = useTransform(overshoot, (ratio) => 1 - Math.abs(ratio) * SQUASH_FACTOR)\n  // Keyed on the sign, not on state, so the origin holds still while the release spring settles back.\n  const originX = useTransform(overshoot, (ratio) => (ratio < 0 ? 1 : 0))\n  const [side, setSide] = useState<StretchSide | null>(null)\n  const dragRect = useRef<DOMRect | null>(null)\n\n  const handlers = {\n    onPointerDown: (event: React.PointerEvent<HTMLElement>) => {\n      if (active) dragRect.current = event.currentTarget.getBoundingClientRect()\n    },\n    onPointerMove: (event: React.PointerEvent<HTMLElement>) => {\n      const rect = dragRect.current\n      if (!rect || rect.width === 0) return\n      const past =\n        event.clientX < rect.left ? event.clientX - rect.left : event.clientX > rect.right ? event.clientX - rect.right : 0\n      overshoot.set(rubberband(past, rect.width) / rect.width)\n      setSide(past === 0 ? null : past < 0 ? \"left\" : \"right\")\n    },\n    onPointerEnd: () => {\n      if (!dragRect.current) return\n      dragRect.current = null\n      setSide(null)\n      animate(overshoot, 0, SPRING_DEFAULT)\n    },\n  }\n  return { active, side, style: { scaleX, scaleY, originX }, handlers }\n}\n\ntype RubberHandlers = ReturnType<typeof useRubberStretch>[\"handlers\"]\ntype PointerProps = Pick<\n  React.ComponentProps<\"span\">,\n  \"onPointerDown\" | \"onPointerMove\" | \"onPointerUp\" | \"onPointerCancel\"\n>\n\n/** The owner's handlers run first, like Radix's own composition. */\nfunction composePointer(owner: PointerProps, rubber: RubberHandlers): PointerProps {\n  return {\n    onPointerDown: (event) => {\n      owner.onPointerDown?.(event)\n      rubber.onPointerDown(event)\n    },\n    onPointerMove: (event) => {\n      owner.onPointerMove?.(event)\n      rubber.onPointerMove(event)\n    },\n    onPointerUp: (event) => {\n      owner.onPointerUp?.(event)\n      rubber.onPointerEnd()\n    },\n    onPointerCancel: (event) => {\n      owner.onPointerCancel?.(event)\n      rubber.onPointerEnd()\n    },\n  }\n}\n\nexport {\n  composePointer,\n  resolveMarks,\n  SliderMarks,\n  thumbCentre,\n  thumbCentreOffset,\n  toPercent,\n  useRubberStretch,\n  type SliderMark,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}