{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/slider/Slider.vue",
      "content": "<script setup lang=\"ts\">\nimport type { SliderRootProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport {\n  SliderRange,\n  SliderRoot,\n  SliderThumb,\n  SliderTrack,\n  TooltipArrow,\n  TooltipContent,\n  TooltipPortal,\n  TooltipProvider,\n  TooltipRoot,\n  TooltipTrigger,\n} from 'reka-ui'\nimport { cn } from '@/lib/utils'\n\nexport interface SliderMark {\n  label: string\n  style?: Record<string, string>\n}\n\ntype ModelValue = number | number[] | [number, number] | null | undefined\n\nconst props = withDefaults(\n  defineProps<\n    Omit<SliderRootProps, 'modelValue' | 'defaultValue' | 'inverted'> & {\n      class?: HTMLAttributes['class']\n      modelValue?: ModelValue\n      defaultValue?: ModelValue\n      /** Enable dual-thumb range selection */\n      range?: boolean\n      /** Vertical orientation */\n      vertical?: boolean\n      /** Height when vertical (px or css value) */\n      height?: string | number\n      /** Tick marks with labels */\n      marks?: Record<number, string | SliderMark>\n      /** Show tooltip on drag/focus. Boolean or formatter function */\n      tooltip?: boolean | ((value: number) => string)\n      /** Show dots at each step */\n      dots?: boolean\n      /** Reverse direction (right-to-left or bottom-to-top) */\n      reverse?: boolean\n      /** Highlight the track between thumbs/min (default true) */\n      included?: boolean\n      /** Size variant */\n      size?: 'small' | 'default'\n    }\n  >(),\n  {\n    min: 0,\n    max: 100,\n    step: 1,\n    included: true,\n    size: 'default',\n    tooltip: true,\n  },\n)\n\nconst emits = defineEmits<{\n  'update:modelValue': [value: number | number[] | [number, number]]\n  valueCommit: [payload: number | number[] | [number, number]]\n}>()\n\n/* ── normalize value to number[] for Reka UI ── */\nconst innerValue = ref<number[]>([])\n\nwatch(\n  () => props.modelValue,\n  (v) => {\n    const dflt = props.range ? [props.min, props.max] : [props.min]\n    if (v == null) {\n      innerValue.value = dflt\n      return\n    }\n    if (props.range) {\n      const arr = Array.isArray(v) ? v : [v, props.max]\n      innerValue.value = [arr[0] ?? props.min, arr[1] ?? props.max]\n    } else {\n      innerValue.value = Array.isArray(v) ? v : [v]\n    }\n  },\n  { immediate: true },\n)\n\nfunction handleUpdate(val: number[] | undefined) {\n  if (!val) return\n  innerValue.value = val\n  if (props.range) {\n    emits('update:modelValue', [val[0], val[1]] as [number, number])\n  } else if (Array.isArray(props.modelValue)) {\n    // backward-compat: consumer passed an array, emit an array\n    emits('update:modelValue', val)\n  } else {\n    emits('update:modelValue', val[0] ?? props.min)\n  }\n}\n\nfunction handleCommit(val: number[] | undefined) {\n  if (!val) return\n  if (props.range) {\n    emits('valueCommit', [val[0], val[1]] as [number, number])\n  } else if (Array.isArray(props.modelValue)) {\n    emits('valueCommit', val)\n  } else {\n    emits('valueCommit', val[0] ?? props.min)\n  }\n}\n\n/* ── layout helpers ── */\nconst orientation = computed(() => {\n  if (props.vertical) return 'vertical'\n  return props.orientation ?? 'horizontal'\n})\n\nconst isHorizontal = computed(() => orientation.value === 'horizontal')\n\nconst trackSize = computed(() =>\n  props.size === 'small' ? (isHorizontal.value ? 'h-1' : 'w-1') : isHorizontal.value ? 'h-1.5' : 'w-1.5',\n)\n\nconst thumbSize = computed(() => (props.size === 'small' ? 'size-3' : 'size-4'))\n\n/* ── marks ── */\nconst markList = computed(() => {\n  if (!props.marks) return []\n  const entries = Object.entries(props.marks).map(([key, val]) => {\n    const num = Number(key)\n    const label = typeof val === 'string' ? val : val.label\n    const style = typeof val === 'string' ? undefined : val.style\n    const pct = ((num - props.min) / (props.max - props.min)) * 100\n    return { value: num, label, style, pct }\n  })\n  entries.sort((a, b) => a.value - b.value)\n  return entries\n})\n\n/* ── step dots ── */\nconst dotList = computed(() => {\n  if (!props.dots) return []\n  const list: number[] = []\n  const count = Math.floor((props.max - props.min) / props.step)\n  for (let i = 0; i <= count; i++) {\n    list.push(props.min + i * props.step)\n  }\n  return list\n})\n\n/* ── tooltip ── */\nconst showTooltip = computed(() => props.tooltip !== false)\n\nfunction formatTooltip(v: number) {\n  if (typeof props.tooltip === 'function') return props.tooltip(v)\n  return String(v)\n}\n\n/* ── delegated reka props ── */\nconst delegated = reactiveOmit(\n  props,\n  'class',\n  'modelValue',\n  'defaultValue',\n  'range',\n  'vertical',\n  'height',\n  'marks',\n  'tooltip',\n  'dots',\n  'reverse',\n  'included',\n  'size',\n)\n\nconst rootProps = computed(() => ({\n  ...delegated,\n  modelValue: innerValue.value,\n  orientation: orientation.value,\n  inverted: props.reverse,\n}))\n</script>\n\n<template>\n  <TooltipProvider>\n    <div\n      :class=\"cn('relative w-full', !isHorizontal && 'flex flex-col items-center')\"\n      :style=\"!isHorizontal && height ? { height: typeof height === 'number' ? `${height}px` : height } : undefined\"\n    >\n      <SliderRoot\n        v-slot=\"{ modelValue: thumbs }\"\n        data-uipkge\n        data-slot=\"slider\"\n        :class=\"\n          cn(\n            'relative flex touch-none select-none data-[disabled]:opacity-50',\n            isHorizontal ? 'w-full items-center' : 'h-full min-h-44 flex-col justify-center',\n            props.class,\n          )\n        \"\n        v-bind=\"rootProps\"\n        @update:model-value=\"handleUpdate\"\n        @value-commit=\"handleCommit\"\n      >\n        <!-- Track -->\n        <SliderTrack\n          data-uipkge\n          data-slot=\"slider-track\"\n          :class=\"cn('bg-muted relative grow overflow-hidden rounded-full', trackSize)\"\n        >\n          <SliderRange\n            v-if=\"included\"\n            data-uipkge\n            data-slot=\"slider-range\"\n            :class=\"cn('bg-primary absolute', isHorizontal ? 'h-full' : 'w-full')\"\n          />\n        </SliderTrack>\n\n        <!-- Step dots -->\n        <template v-if=\"dotList.length\">\n          <div\n            v-for=\"dot in dotList\"\n            :key=\"dot\"\n            :class=\"\n              cn(\n                'border-primary/40 bg-background absolute rounded-full border',\n                isHorizontal ? 'top-1/2 size-1.5 -translate-y-1/2' : 'left-1/2 size-1.5 -translate-x-1/2',\n                props.size === 'small' && 'size-1',\n              )\n            \"\n            :style=\"\n              isHorizontal\n                ? {\n                    left: `${((dot - min) / (max - min)) * 100}%`,\n                    transform: 'translateX(-50%) translateY(-50%)',\n                  }\n                : {\n                    bottom: `${((dot - min) / (max - min)) * 100}%`,\n                    transform: 'translateX(-50%) translateY(50%)',\n                  }\n            \"\n          />\n        </template>\n\n        <!-- Marks -->\n        <div\n          v-if=\"markList.length\"\n          :class=\"\n            cn(\n              'pointer-events-none absolute',\n              isHorizontal ? 'top-full mt-2.5 h-5 w-full' : 'top-0 left-full ml-3 h-full w-20',\n            )\n          \"\n        >\n          <span\n            v-for=\"mark in markList\"\n            :key=\"mark.value\"\n            class=\"text-muted-foreground absolute text-xs whitespace-nowrap\"\n            :style=\"{\n              ...mark.style,\n              ...(isHorizontal\n                ? { left: `${mark.pct}%`, transform: 'translateX(-50%)' }\n                : { bottom: `${mark.pct}%`, transform: 'translateY(50%)' }),\n            }\"\n          >\n            {{ mark.label }}\n          </span>\n        </div>\n\n        <!-- Thumbs (with tooltips) -->\n        <template v-for=\"(_, idx) in thumbs\" :key=\"idx\">\n          <TooltipRoot v-if=\"showTooltip\" :delay-duration=\"0\">\n            <TooltipTrigger as-child>\n              <SliderThumb\n                :index=\"idx\"\n                data-uipkge\n                data-slot=\"slider-thumb\"\n                :class=\"\n                  cn(\n                    'border-primary bg-background ring-ring/50 block shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',\n                    thumbSize,\n                  )\n                \"\n              />\n            </TooltipTrigger>\n            <TooltipPortal>\n              <TooltipContent\n                :side=\"isHorizontal ? 'top' : 'right'\"\n                :side-offset=\"4\"\n                class=\"bg-foreground text-background z-50 w-fit rounded-md px-2 py-1 text-xs\"\n              >\n                {{ formatTooltip(thumbs?.[idx] ?? 0) }}\n                <TooltipArrow class=\"bg-foreground fill-foreground size-2.5 rotate-45 rounded-[2px]\" />\n              </TooltipContent>\n            </TooltipPortal>\n          </TooltipRoot>\n\n          <SliderThumb\n            v-else\n            :index=\"idx\"\n            data-uipkge\n            data-slot=\"slider-thumb\"\n            :class=\"\n              cn(\n                'border-primary bg-background ring-ring/50 block shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',\n                thumbSize,\n              )\n            \"\n          />\n        </template>\n      </SliderRoot>\n    </div>\n  </TooltipProvider>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/slider/Slider.vue"
    },
    {
      "path": "packages/registry-vue/components/slider/index.ts",
      "content": "export { default as Slider } from './Slider.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/slider/index.ts"
    }
  ],
  "dependencies": [
    "@vueuse/core",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Single-thumb slider — pick a value within a range. Optional tick marks, step size, and inline value display.",
  "categories": [
    "form"
  ]
}