{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/slider/slider.component.ts",
      "content": "import {\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnDestroy,\n  AfterViewInit,\n  Output,\n  TemplateRef,\n  ViewChild,\n  ViewContainerRef,\n  ViewEncapsulation,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\nimport { BodyPortal, autoPlace, pushDismissableLayer, uniqueId, type Placement } from '@/ui/popper/popper'\n\n/** Mirrors React Slider.tsx motion styles (range ease, press scale, reduced-motion) — copied verbatim. */\nconst STYLE_CONTENT = `\n@media (prefers-reduced-motion: no-preference) {\n  [data-slot='slider-range'] {\n    transition-property: left, right, top, bottom;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  }\n  [data-slot='slider-thumb'] {\n    scale: 1;\n    transition-property: color, box-shadow, border-color, scale, left, right, top, bottom;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  }\n  [data-slot='slider-thumb']:active:not([data-disabled]) {\n    scale: 0.94;\n    transition-duration: 100ms;\n  }\n  [data-slot='slider']:active [data-slot='slider-range'],\n  [data-slot='slider']:active [data-slot='slider-thumb'] {\n    transition-property: color, box-shadow, border-color, scale;\n    transition-duration: 100ms;\n  }\n}\n@media (prefers-reduced-motion: reduce) {\n  [data-slot='slider-range'],\n  [data-slot='slider-thumb'] {\n    transition: none !important;\n    scale: 1 !important;\n  }\n}\n`\n\nexport interface SliderMark {\n  label: string\n  /** CSS properties applied to the label (React: `style`). */\n  style?: Record<string, string>\n}\n/** Accepted by the `value` input / form control; `valueChange` always emits `number[]` like React. */\nexport type SliderValue = number | number[]\nexport type SliderSize = 'small' | 'default'\nexport type SliderTooltip = boolean | ((value: number) => string)\ntype TooltipState = 'closed' | 'delayed-open' | 'instant-open'\n\nconst PAGE_KEYS = ['PageUp', 'PageDown']\nconst ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']\nconst BACK_KEYS: Record<string, string[]> = {\n  'from-left': ['Home', 'PageDown', 'ArrowDown', 'ArrowLeft'],\n  'from-right': ['Home', 'PageDown', 'ArrowDown', 'ArrowRight'],\n  'from-bottom': ['Home', 'PageDown', 'ArrowDown', 'ArrowLeft'],\n  'from-top': ['Home', 'PageDown', 'ArrowUp', 'ArrowLeft'],\n}\n\nconst clamp = (v: number, [lo, hi]: [number, number]) => Math.min(hi, Math.max(lo, v))\nfunction linearScale(input: [number, number], output: [number, number]) {\n  return (value: number) => {\n    if (input[0] === input[1] || output[0] === output[1]) return output[0]\n    const ratio = (output[1] - output[0]) / (input[1] - input[0])\n    return output[0] + ratio * (value - input[0])\n  }\n}\nfunction decimalCount(value: number): number {\n  if (!Number.isFinite(value)) return 0\n  const str = value.toString()\n  if (str.includes('e')) {\n    const [coefficient = '', exponent = '0'] = str.split('e')\n    return Math.max(0, (coefficient.split('.')[1] || '').length - Number(exponent))\n  }\n  return (str.split('.')[1] || '').length\n}\nconst roundValue = (value: number, decimals: number) => Math.round(value * 10 ** decimals) / 10 ** decimals\nexport function valueToPercent(value: number, min: number, max: number): number {\n  return clamp((100 / (max - min)) * (value - min), [0, 100])\n}\nfunction closestIndex(values: number[], next: number): number {\n  if (values.length === 1) return 0\n  const distances = values.map((v) => Math.abs(v - next))\n  return distances.indexOf(Math.min(...distances))\n}\n/** Radix: keeps the thumb inside the track at the ends (0% / 100%). */\nfunction thumbInBoundsOffset(size: number, percent: number, direction: number): number {\n  const half = size / 2\n  return (half - linearScale([0, 50], [0, half])(percent) * direction) * direction\n}\nfunction thumbLabel(index: number, total: number): string | null {\n  if (total > 2) return `Value ${index + 1} of ${total}`\n  if (total === 2) return ['Minimum', 'Maximum'][index] ?? null\n  return null\n}\n\n/** Radix popper arrow placement (same as tooltip): wrapper pinned to the edge facing the anchor. */\nfunction placeArrow(wrapper: HTMLElement | null, anchor: Element, floating: HTMLElement, p: Placement): void {\n  if (!wrapper) return\n  const a = anchor.getBoundingClientRect()\n  const f = floating.getBoundingClientRect()\n  const vertical = p.side === 'top' || p.side === 'bottom'\n  const base = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[p.side]\n  Object.assign(wrapper.style, {\n    position: 'absolute',\n    top: '',\n    bottom: '',\n    left: '',\n    right: '',\n    transformOrigin: { top: '', right: '0 0', bottom: 'center 0', left: '100% 0' }[p.side],\n    transform: {\n      top: 'translateY(100%)',\n      right: 'translateY(50%) rotate(90deg) translateX(-50%)',\n      bottom: 'rotate(180deg)',\n      left: 'translateY(50%) rotate(-90deg) translateX(50%)',\n    }[p.side],\n  })\n  wrapper.style.setProperty(base, '0px')\n  if (vertical) wrapper.style.left = `${Math.max(4, Math.min(f.width - 14, a.left + a.width / 2 - f.left - 5))}px`\n  else wrapper.style.top = `${Math.max(4, Math.min(f.height - 14, a.top + a.height / 2 - f.top - 5))}px`\n}\n\nlet lastTooltipClosedAt = 0\n\n/**\n * Angular port of UIPKGE Slider (React: Radix Slider + Radix Tooltip on each thumb).\n * Same DOM as React: wrapper div (this host) > root span[data-slot=slider] > track > range,\n * step dots, mark labels, and one span[role=slider] thumb per value. Radix behaviour:\n * pointer-down on the track jumps the closest thumb and drags it (pointer capture),\n * dragging a thumb moves it, Arrow / Shift+Arrow / PageUp / PageDown / Home / End step,\n * values snap to `step`, stay sorted and respect `minStepsBetweenThumbs`; `valueCommit`\n * fires at the end of a slide or a key step. Hover / focus shows the value tooltip.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-slider, [ui-slider]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  styles: [STYLE_CONTENT],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiSliderComponent), multi: true }],\n  host: {\n    // [attr.class], not [class]: [class] merges in the consumer's static class attribute, which\n    // belongs to the inner root only.\n    '[attr.class]': 'wrapperClass',\n    '[style.height]': 'wrapperHeight',\n  },\n  template: `\n    <span\n      #root\n      data-uipkge=\"\"\n      data-slot=\"slider\"\n      [attr.dir]=\"isHorizontal ? dir : null\"\n      [attr.data-orientation]=\"orientation\"\n      [attr.aria-disabled]=\"disabled\"\n      [attr.data-disabled]=\"disabled ? '' : null\"\n      [class]=\"rootClass\"\n      (keydown)=\"onKeydown($event)\"\n      (pointerdown)=\"onPointerDown($event)\"\n      (pointermove)=\"onPointerMove($event)\"\n      (pointerup)=\"onPointerUp($event)\"\n    >\n      <span\n        data-uipkge=\"\"\n        data-slot=\"slider-track\"\n        [attr.data-orientation]=\"orientation\"\n        [attr.data-disabled]=\"disabled ? '' : null\"\n        [class]=\"trackClass\"\n      >\n        @if (included) {\n          <span\n            data-uipkge=\"\"\n            data-slot=\"slider-range\"\n            [attr.data-orientation]=\"orientation\"\n            [attr.data-disabled]=\"disabled ? '' : null\"\n            [class]=\"rangeClass\"\n            [style]=\"rangeStyle\"\n          ></span>\n        }\n      </span>\n      @for (dot of dotList; track dot) {\n        <div [class]=\"dotClass\" [style]=\"edgeStyle(dot)\"></div>\n      }\n      @if (markList.length > 0) {\n        <div [class]=\"marksClass\">\n          @for (mark of markList; track mark.value) {\n            <span [class]=\"markLabelClass\" [style]=\"markStyle(mark)\">{{ mark.label }}</span>\n          }\n        </div>\n      }\n      @for (thumb of values; track $index) {\n        <span class=\"absolute [transform:var(--radix-slider-thumb-transform)]\" [style]=\"thumbStyle(thumb)\">\n          <span\n            role=\"slider\"\n            data-uipkge=\"\"\n            data-slot=\"slider-thumb\"\n            [attr.aria-label]=\"ariaLabel || thumbLabel($index, values.length)\"\n            [attr.aria-valuemin]=\"min\"\n            [attr.aria-valuenow]=\"thumb\"\n            [attr.aria-valuemax]=\"max\"\n            [attr.aria-orientation]=\"orientation\"\n            [attr.data-orientation]=\"orientation\"\n            [attr.data-disabled]=\"disabled ? '' : null\"\n            [attr.tabindex]=\"disabled ? null : 0\"\n            [attr.data-state]=\"showTooltip ? tooltipStateFor($index) : null\"\n            [attr.aria-describedby]=\"showTooltip && tooltipIndex() === $index ? tooltipId : null\"\n            [class]=\"thumbClass\"\n            (focus)=\"onThumbFocus($index)\"\n            (blur)=\"closeTooltip()\"\n            (pointermove)=\"onThumbPointerMove($index, $event)\"\n            (pointerleave)=\"onThumbPointerLeave()\"\n            (pointerdown)=\"onThumbPointerDown()\"\n            (click)=\"closeTooltip()\"\n          ></span>\n        </span>\n        @if (name) {\n          <input hidden [attr.name]=\"values.length > 1 ? name + '[]' : name\" [value]=\"thumb\" />\n        }\n      }\n    </span>\n    <ng-template #tooltipTpl>\n      <div\n        [id]=\"tooltipId\"\n        role=\"tooltip\"\n        [attr.data-state]=\"tooltipState()\"\n        class=\"bg-foreground text-background z-50 w-fit rounded-md px-2 py-1 text-xs\"\n      >\n        {{ tooltipText }}\n        <span data-tooltip-arrow=\"\"\n          ><svg\n            class=\"bg-foreground fill-foreground size-2.5 rotate-45 rounded-[2px]\"\n            width=\"10\"\n            height=\"5\"\n            viewBox=\"0 0 30 10\"\n            preserveAspectRatio=\"none\"\n          >\n            <polygon points=\"0,0 30,0 15,10\" /></svg\n        ></span>\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiSliderComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly portal = new BodyPortal(inject(ViewContainerRef))\n\n  /** Controlled value (React: number[]; a plain number is accepted for form controls / one-thumb bindings). */\n  @Input()\n  set value(v: SliderValue | null | undefined) {\n    this._value.set(v ?? undefined)\n  }\n  get value(): SliderValue | undefined {\n    return this._value()\n  }\n  /** Uncontrolled initial value. */\n  @Input() defaultValue?: number[]\n  @Output() valueChange = new EventEmitter<number[]>()\n  @Output() valueCommit = new EventEmitter<number[]>()\n  @Input() min = 0\n  @Input() max = 100\n  @Input() step = 1\n  @Input() minStepsBetweenThumbs = 0\n  @Input() name?: string\n  @Input() dir: 'ltr' | 'rtl' = 'ltr'\n  @Input('aria-label') ariaLabel?: string\n  @Input({ transform: booleanAttribute }) disabled = false\n  /** Dual-thumb range selection (affects the default value when uncontrolled). */\n  @Input({ transform: booleanAttribute }) range = false\n  @Input({ transform: booleanAttribute }) vertical = false\n  /** Height when vertical (px number or CSS value). */\n  @Input() height?: string | number\n  @Input() marks?: Record<number, string | SliderMark>\n  /** Show a tooltip on hover / focus. Boolean or formatter function. */\n  @Input() tooltip: SliderTooltip = true\n  @Input({ transform: booleanAttribute }) dots = false\n  @Input({ transform: booleanAttribute }) reverse = false\n  @Input({ transform: booleanAttribute }) included = true\n  @Input() size: SliderSize = 'default'\n  /** Classes for the Radix Root span (React `className`). */\n  @Input('class') className?: string\n\n  @ViewChild('root', { static: true }) rootRef!: ElementRef<HTMLElement>\n  @ViewChild('tooltipTpl', { static: true }) tooltipTpl!: TemplateRef<unknown>\n\n  readonly tooltipId = uniqueId('slider-tooltip')\n  readonly thumbLabel = thumbLabel\n  readonly tooltipIndex = signal<number | null>(null)\n  readonly tooltipState = signal<TooltipState>('closed')\n\n  private readonly _value = signal<SliderValue | undefined>(undefined)\n  private readonly _internal = signal<number[] | null>(null)\n  private readonly thumbSize = signal<{ width: number; height: number } | null>(null)\n  private valueIndexToChange = 0\n  private valuesBeforeSlide: number[] = []\n  private keyboard = false\n  private captureTarget: EventTarget | null = null\n  private formBound = false\n  private onChange: (v: SliderValue) => void = () => {}\n  private onTouched: () => void = () => {}\n  private ro?: ResizeObserver\n  private tooltipCleanups: (() => void)[] = []\n  private tooltipTimer?: ReturnType<typeof setTimeout>\n  private pointerMoveOpened = false\n  private pointerDownOnThumb = false\n\n  get orientation(): 'horizontal' | 'vertical' {\n    return this.vertical ? 'vertical' : 'horizontal'\n  }\n  get isHorizontal(): boolean {\n    return !this.vertical\n  }\n\n  /** Current thumb values (controlled value, else internal state, else default). */\n  get values(): number[] {\n    const v = this._value()\n    if (v !== undefined) return Array.isArray(v) ? v : [v]\n    return this._internal() ?? this.defaultValue ?? (this.range ? [this.min, this.max] : [this.min])\n  }\n\n  private get slidingFromStart(): boolean {\n    if (this.isHorizontal) return (this.dir === 'ltr') !== this.reverse\n    return !this.reverse\n  }\n  private get startEdge(): string {\n    if (this.isHorizontal) return this.slidingFromStart ? 'left' : 'right'\n    return this.slidingFromStart ? 'bottom' : 'top'\n  }\n  private get endEdge(): string {\n    const opposite: Record<string, string> = { left: 'right', right: 'left', top: 'bottom', bottom: 'top' }\n    return opposite[this.startEdge]!\n  }\n  private get direction(): number {\n    return this.slidingFromStart ? 1 : -1\n  }\n\n  // ── classes (verbatim from React) ──\n  get wrapperClass(): string {\n    return cn('block relative w-full', !this.isHorizontal && 'flex flex-col items-center')\n  }\n  get wrapperHeight(): string | null {\n    if (this.isHorizontal || this.height == null || this.height === '') return null\n    return typeof this.height === 'number' ? `${this.height}px` : this.height\n  }\n  get rootClass(): string {\n    return cn(\n      'relative flex touch-none select-none data-[disabled]:opacity-50',\n      this.isHorizontal ? 'w-full items-center' : 'h-full min-h-44 flex-col justify-center',\n      this.isHorizontal\n        ? '[--radix-slider-thumb-transform:translateX(-50%)]'\n        : '[--radix-slider-thumb-transform:translateY(50%)]',\n      this.className,\n    )\n  }\n  get trackClass(): string {\n    const trackSize =\n      this.size === 'small' ? (this.isHorizontal ? 'h-1' : 'w-1') : this.isHorizontal ? 'h-1.5' : 'w-1.5'\n    return cn('bg-muted relative grow overflow-hidden rounded-full', trackSize)\n  }\n  get rangeClass(): string {\n    return cn('bg-primary absolute', this.isHorizontal ? 'h-full' : 'w-full')\n  }\n  get thumbClass(): string {\n    return cn(\n      'border-primary bg-background ring-ring/50 block shrink-0 rounded-full border shadow-sm touch-manipulation hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none',\n      this.size === 'small' ? 'size-3' : 'size-4',\n    )\n  }\n  get dotClass(): string {\n    return cn(\n      'border-primary/40 bg-background absolute rounded-full border',\n      this.isHorizontal ? 'top-1/2 size-1.5 -translate-y-1/2' : 'left-1/2 size-1.5 -translate-x-1/2',\n      this.size === 'small' && 'size-1',\n      // React sets these transforms inline; same values as arbitrary-property utilities.\n      this.isHorizontal\n        ? '[transform:translateX(-50%)_translateY(-50%)]'\n        : '[transform:translateX(-50%)_translateY(50%)]',\n    )\n  }\n  get marksClass(): string {\n    return cn(\n      'pointer-events-none absolute',\n      this.isHorizontal ? 'top-full mt-2.5 h-5 w-full' : 'top-0 left-full ml-3 h-full w-20',\n    )\n  }\n  get markLabelClass(): string {\n    return cn(\n      'text-muted-foreground absolute text-xs whitespace-nowrap',\n      this.isHorizontal ? '[transform:translateX(-50%)]' : '[transform:translateY(50%)]',\n    )\n  }\n\n  // ── derived lists / styles ──\n  get markList(): { value: number; label: string; style?: Record<string, string>; pct: number }[] {\n    if (!this.marks) return []\n    return Object.entries(this.marks)\n      .map(([key, val]) => {\n        const num = Number(key)\n        return {\n          value: num,\n          label: typeof val === 'string' ? val : val.label,\n          style: typeof val === 'string' ? undefined : val.style,\n          pct: ((num - this.min) / (this.max - this.min)) * 100,\n        }\n      })\n      .sort((a, b) => a.value - b.value)\n  }\n  get dotList(): number[] {\n    if (!this.dots) return []\n    const list: number[] = []\n    const count = Math.floor((this.max - this.min) / this.step)\n    for (let i = 0; i <= count; i++) list.push(this.min + i * this.step)\n    return list\n  }\n  get rangeStyle(): Record<string, string> {\n    const pcts = this.values.map((v) => valueToPercent(v, this.min, this.max))\n    const start = pcts.length > 1 ? Math.min(...pcts) : 0\n    const end = 100 - Math.max(...pcts)\n    return { [this.startEdge]: `${start}%`, [this.endEdge]: `${end}%` }\n  }\n  edgeStyle(value: number): Record<string, string> {\n    const pct = `${((value - this.min) / (this.max - this.min)) * 100}%`\n    return this.isHorizontal ? { left: pct } : { bottom: pct }\n  }\n  markStyle(mark: { style?: Record<string, string>; pct: number }): Record<string, string> {\n    return { ...mark.style, ...(this.isHorizontal ? { left: `${mark.pct}%` } : { bottom: `${mark.pct}%` }) }\n  }\n  thumbStyle(value: number): Record<string, string> {\n    const pct = valueToPercent(value, this.min, this.max)\n    const size = this.thumbSize()?.[this.isHorizontal ? 'width' : 'height']\n    const offset = size ? thumbInBoundsOffset(size, pct, this.direction) : 0\n    return { [this.startEdge]: `calc(${pct}% + ${offset}px)` }\n  }\n\n  // ── tooltip ──\n  get showTooltip(): boolean {\n    return this.tooltip !== false\n  }\n  get tooltipText(): string {\n    const i = this.tooltipIndex()\n    const v = i === null ? undefined : this.values[i]\n    const n = v ?? 0\n    return typeof this.tooltip === 'function' ? this.tooltip(n) : String(n)\n  }\n  tooltipStateFor(index: number): TooltipState {\n    return this.tooltipIndex() === index ? this.tooltipState() : 'closed'\n  }\n\n  // ── value updates (Radix updateValues) ──\n  private updateValues(raw: number, atIndex: number, commit = false): void {\n    const snapped = roundValue(Math.round((raw - this.min) / this.step) * this.step + this.min, decimalCount(this.step))\n    const next = clamp(snapped, [this.min, this.max])\n    const prev = this.values\n    const nextValues = [...prev]\n    nextValues[atIndex] = next\n    nextValues.sort((a, b) => a - b)\n    const minGap = this.minStepsBetweenThumbs * this.step\n    if (minGap > 0) {\n      const gaps = nextValues.slice(0, -1).map((v, i) => nextValues[i + 1]! - v)\n      if (Math.min(...gaps) < minGap) return\n    }\n    this.valueIndexToChange = nextValues.indexOf(next)\n    if (String(nextValues) === String(prev)) return\n    this.setValues(nextValues)\n    if (commit) this.valueCommit.emit(nextValues)\n  }\n\n  private setValues(next: number[]): void {\n    const current = this._value()\n    if (current === undefined) this._internal.set(next)\n    else if (this.formBound) this._value.set(Array.isArray(current) ? next : next[0])\n    this.thumbs()[this.valueIndexToChange]?.focus({ preventScroll: true, focusVisible: this.keyboard } as FocusOptions)\n    this.keyboard = false\n    this.valueChange.emit(next)\n    this.onChange(Array.isArray(current) || current === undefined ? next : (next[0] ?? this.min))\n  }\n\n  private thumbs(): HTMLElement[] {\n    return [...this.rootRef.nativeElement.querySelectorAll<HTMLElement>('[data-slot=\"slider-thumb\"]')]\n  }\n\n  private valueFromPointer(event: PointerEvent): number {\n    const rect = this.rootRef.nativeElement.getBoundingClientRect()\n    if (this.isHorizontal) {\n      const out: [number, number] = this.slidingFromStart ? [this.min, this.max] : [this.max, this.min]\n      return linearScale([0, rect.width], out)(event.clientX - rect.left)\n    }\n    const out: [number, number] = this.slidingFromStart ? [this.max, this.min] : [this.min, this.max]\n    return linearScale([0, rect.height], out)(event.clientY - rect.top)\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (this.disabled) return\n    const key = event.key\n    if (key === 'Home') {\n      this.keyboard = true\n      this.updateValues(this.min, 0, true)\n      event.preventDefault()\n    } else if (key === 'End') {\n      this.keyboard = true\n      this.updateValues(this.max, this.values.length - 1, true)\n      event.preventDefault()\n    } else if (PAGE_KEYS.includes(key) || ARROW_KEYS.includes(key)) {\n      this.keyboard = true\n      const slide = this.isHorizontal\n        ? this.slidingFromStart\n          ? 'from-left'\n          : 'from-right'\n        : this.slidingFromStart\n          ? 'from-bottom'\n          : 'from-top'\n      const dir = BACK_KEYS[slide]!.includes(key) ? -1 : 1\n      const skip = PAGE_KEYS.includes(key) || (event.shiftKey && ARROW_KEYS.includes(key))\n      const atIndex = this.valueIndexToChange\n      const current = this.values[atIndex] ?? this.min\n      this.updateValues(current + this.step * (skip ? 10 : 1) * dir, atIndex, true)\n      event.preventDefault()\n    }\n  }\n\n  onPointerDown(event: PointerEvent): void {\n    if (this.disabled) return\n    this.valuesBeforeSlide = this.values\n    this.keyboard = false\n    const target = event.target as HTMLElement\n    target.setPointerCapture?.(event.pointerId)\n    this.captureTarget = target\n    event.preventDefault()\n    if (this.thumbs().includes(target)) {\n      target.focus({ preventScroll: true, focusVisible: false } as FocusOptions)\n    } else {\n      const v = this.valueFromPointer(event)\n      this.updateValues(v, closestIndex(this.values, v))\n    }\n  }\n\n  onPointerMove(event: PointerEvent): void {\n    if (this.disabled || !this.isCaptured(event)) return\n    this.updateValues(this.valueFromPointer(event), this.valueIndexToChange)\n  }\n\n  onPointerUp(event: PointerEvent): void {\n    if (this.disabled || !this.isCaptured(event)) return\n    const target = event.target as HTMLElement\n    target.releasePointerCapture?.(event.pointerId)\n    this.captureTarget = null\n    const i = this.valueIndexToChange\n    if (this.values[i] !== this.valuesBeforeSlide[i]) this.valueCommit.emit(this.values)\n    this.onTouched()\n  }\n\n  private isCaptured(event: PointerEvent): boolean {\n    const target = event.target as HTMLElement\n    return target.hasPointerCapture ? target.hasPointerCapture(event.pointerId) : this.captureTarget === target\n  }\n\n  // ── thumb tooltip (Radix Tooltip, delayDuration 0) ──\n  onThumbFocus(index: number): void {\n    this.valueIndexToChange = index\n    if (!this.pointerDownOnThumb) this.openTooltip(index, 'instant-open')\n  }\n\n  onThumbPointerMove(index: number, event: PointerEvent): void {\n    if (event.pointerType === 'touch' || this.pointerMoveOpened) return\n    this.pointerMoveOpened = true\n    clearTimeout(this.tooltipTimer)\n    if (Date.now() - lastTooltipClosedAt < 300) this.openTooltip(index, 'instant-open')\n    else this.tooltipTimer = setTimeout(() => this.openTooltip(index, 'delayed-open'), 0)\n  }\n\n  onThumbPointerLeave(): void {\n    this.pointerMoveOpened = false\n    this.closeTooltip()\n  }\n\n  onThumbPointerDown(): void {\n    this.closeTooltip()\n    this.pointerDownOnThumb = true\n    document.addEventListener('pointerup', () => (this.pointerDownOnThumb = false), { once: true })\n  }\n\n  private openTooltip(index: number, how: TooltipState): void {\n    if (!this.showTooltip || this.disabled) return\n    const thumb = this.thumbs()[index]\n    if (!thumb) return\n    this.tooltipCleanups.splice(0).forEach((fn) => fn())\n    this.tooltipIndex.set(index)\n    this.tooltipState.set(how)\n    if (!this.portal.attached) this.portal.attach(this.tooltipTpl)\n    const panel = document.getElementById(this.tooltipId)\n    if (!panel) return\n    const arrow = panel.querySelector<HTMLElement>('[data-tooltip-arrow]')\n    // Radix Popper offsets by sideOffset (4) + the arrow's layout height (size-2.5 = 10px, unrotated).\n    const svg = arrow?.firstElementChild\n    const arrowHeight = (svg && parseFloat(getComputedStyle(svg).height)) || 10\n    const place = () =>\n      autoPlace(\n        thumb,\n        panel,\n        () => ({\n          side: this.isHorizontal ? 'top' : 'right',\n          align: 'center',\n          sideOffset: 4 + arrowHeight,\n          alignOffset: 0,\n          collisionPadding: 0,\n          avoidCollisions: true,\n        }),\n        'tooltip',\n        (p) => placeArrow(arrow, thumb, panel, p),\n      )\n    let stop = place()\n    // Follow the thumb while its value changes (keyboard steps, track drags).\n    const follow = new MutationObserver(() => {\n      stop()\n      stop = place()\n    })\n    if (thumb.parentElement) follow.observe(thumb.parentElement, { attributes: true, attributeFilter: ['style'] })\n    this.tooltipCleanups.push(\n      () => stop(),\n      () => follow.disconnect(),\n      pushDismissableLayer({\n        contains: () => true,\n        onEscape: () => this.closeTooltip(),\n        onPointerDownOutside: () => {},\n      }),\n    )\n  }\n\n  closeTooltip(): void {\n    clearTimeout(this.tooltipTimer)\n    if (this.tooltipIndex() === null) return\n    lastTooltipClosedAt = Date.now()\n    this.tooltipCleanups.splice(0).forEach((fn) => fn())\n    this.tooltipIndex.set(null)\n    this.tooltipState.set('closed')\n    this.portal.detach()\n  }\n\n  ngAfterViewInit(): void {\n    if (typeof ResizeObserver === 'undefined') return\n    this.ro = new ResizeObserver(() => {\n      const t = this.thumbs()[0]\n      if (t) this.thumbSize.set({ width: t.offsetWidth, height: t.offsetHeight })\n    })\n    this.ro.observe(this.rootRef.nativeElement)\n  }\n\n  ngOnDestroy(): void {\n    this.ro?.disconnect()\n    clearTimeout(this.tooltipTimer)\n    this.tooltipCleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n\n  // ── ControlValueAccessor ──\n  writeValue(v: SliderValue | null): void {\n    this._value.set(v ?? undefined)\n    this.cdr.markForCheck()\n  }\n  registerOnChange(fn: (v: SliderValue) => void): void {\n    this.formBound = true\n    this.onChange = fn\n  }\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled = isDisabled\n    this.cdr.markForCheck()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/slider/slider.component.ts"
    },
    {
      "path": "packages/registry-angular/components/slider/index.ts",
      "content": "export {\n  UiSliderComponent,\n  valueToPercent,\n  type SliderMark,\n  type SliderValue,\n  type SliderSize,\n  type SliderTooltip,\n} from './slider.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/slider/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Slider with Radix semantics — single or dual-thumb range, vertical, reverse, step dots, labelled marks and a value tooltip on each thumb. Pointer drag, full keyboard support and a ControlValueAccessor for Angular forms.",
  "categories": [
    "form"
  ]
}