{
  "name": "slider",
  "type": "registry:component",
  "files": [
    {
      "name": "slider.component.ts",
      "content": "import { DOCUMENT } from '@angular/common';\nimport {\n  type AfterViewInit,\n  booleanAttribute,\n  ChangeDetectionStrategy,\n  Component,\n  computed,\n  DestroyRef,\n  ElementRef,\n  forwardRef,\n  inject,\n  input,\n  linkedSignal,\n  numberAttribute,\n  output,\n  signal,\n  viewChild,\n  viewChildren,\n  ViewEncapsulation,\n} from '@angular/core';\nimport { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\nimport { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\nimport type { ClassValue } from 'clsx';\nimport { filter, fromEvent, map, switchMap, takeUntil, tap } from 'rxjs';\n\nimport { mergeClasses, noopFn } from '@/shared/utils/merge-classes';\nimport { clamp, convertValueToPercentage, roundToStep } from '@/shared/utils/number';\n\nimport {\n  sliderOrientationVariants,\n  sliderRangeVariants,\n  sliderThumbVariants,\n  sliderTrackVariants,\n  sliderVariants,\n} from './slider.variants';\n\ntype OnTouchedType = () => void;\ntype OnChangeType = (value: number[]) => void;\n\n@Component({\n  selector: 'z-slider-track',\n  template: `\n    <span #track data-slot=\"slider-track\" [attr.data-orientation]=\"orientation()\" [class]=\"classes()\">\n      <ng-content />\n    </span>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    '[style.width]': '\"inherit\"',\n    '[style.height]': '\"100%\"',\n    '[attr.data-orientation]': 'orientation()',\n  },\n})\nexport class ZSliderTrackComponent {\n  readonly orientation = input<'horizontal' | 'vertical'>('horizontal');\n  readonly class = input<ClassValue>('');\n\n  protected readonly classes = computed(() => mergeClasses(sliderTrackVariants(), 'flex', this.class()));\n\n  private readonly trackEl = viewChild.required<ElementRef<HTMLElement>>('track');\n\n  get nativeElement(): HTMLElement {\n    return this.trackEl().nativeElement;\n  }\n}\n\n@Component({\n  selector: 'z-slider-range',\n  template: `\n    @for (seg of segments(); track $index) {\n      <span\n        data-slot=\"slider-range\"\n        [attr.data-orientation]=\"orientation()\"\n        [class]=\"classes()\"\n        [style.left]=\"seg.left\"\n        [style.right]=\"seg.right\"\n        [style.bottom]=\"seg.bottom\"\n        [style.top]=\"seg.top\"\n      ></span>\n    }\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n})\nexport class ZSliderRangeComponent {\n  readonly percent = input<number[]>([0]);\n\n  readonly orientation = input<'horizontal' | 'vertical'>('horizontal');\n  readonly class = input<ClassValue>('');\n\n  protected readonly classes = computed(() => mergeClasses(sliderRangeVariants(), this.class()));\n\n  protected readonly segments = computed(() => {\n    const p = this.percent();\n    const isHorizontal = this.orientation() === 'horizontal';\n\n    const make = (left: string, right: string) =>\n      isHorizontal ? { left, right, bottom: null, top: null } : { left: null, right: null, bottom: left, top: right };\n\n    if (p.length === 0) {\n      return [make('0', '100%')];\n    }\n\n    if (p.length === 1) {\n      return [make('0', 100 - p[0] + '%')];\n    }\n\n    const segs: ReturnType<typeof make>[] = [];\n    for (let i = 0; i < p.length - 1; i++) {\n      segs.push(make(p[i] + '%', 100 - p[i + 1] + '%'));\n    }\n    return segs;\n  });\n}\n\n@Component({\n  selector: 'z-slider-thumb',\n  template: `\n    <span\n      #thumb\n      data-slot=\"slider-thumb\"\n      [attr.role]=\"'slider'\"\n      [attr.aria-valuemin]=\"min()\"\n      [attr.aria-valuemax]=\"max()\"\n      [attr.aria-valuenow]=\"value()\"\n      [attr.aria-disabled]=\"disabled() ? true : null\"\n      [class]=\"classes()\"\n      tabindex=\"0\"\n    ></span>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    '[class]': 'orientationClasses()',\n    '[style.left]': 'orientation() === \"horizontal\" ? \"calc(\" + percent() + \"% + \" + offset() + \"px)\" : null',\n    '[style.bottom]': 'orientation() === \"vertical\" ? \"calc(\" + percent() + \"% + \" + offset() + \"px)\" : null',\n  },\n})\nexport class ZSliderThumbComponent {\n  readonly value = input(0);\n  readonly min = input(0);\n  readonly max = input(100);\n  readonly disabled = input(false);\n  readonly percent = input(0);\n  readonly offset = input(0);\n\n  readonly orientation = input<'horizontal' | 'vertical'>('horizontal');\n  readonly class = input<ClassValue>('');\n\n  protected readonly classes = computed(() => mergeClasses(sliderThumbVariants(), this.class()));\n\n  protected readonly orientationClasses = computed(() =>\n    mergeClasses(sliderOrientationVariants({ zOrientation: this.orientation() })),\n  );\n\n  private readonly thumbEl = viewChild.required<ElementRef<HTMLElement>>('thumb');\n\n  get nativeElement(): HTMLElement {\n    return this.thumbEl().nativeElement;\n  }\n}\n\n@Component({\n  selector: 'z-slider',\n  imports: [ZSliderTrackComponent, ZSliderRangeComponent, ZSliderThumbComponent],\n  template: `\n    <span\n      data-slot=\"slider\"\n      [attr.data-disabled]=\"disabled()\"\n      [attr.data-orientation]=\"zOrientation()\"\n      [class]=\"classes()\"\n    >\n      <z-slider-track [orientation]=\"zOrientation()\">\n        <z-slider-range [orientation]=\"zOrientation()\" [percent]=\"percentages()\" />\n      </z-slider-track>\n\n      @for (value of values(); track $index) {\n        <z-slider-thumb\n          [orientation]=\"zOrientation()\"\n          [percent]=\"percentages()[$index]\"\n          [offset]=\"thumbOffset()\"\n          [value]=\"value\"\n          [min]=\"zMin()\"\n          [max]=\"zMax()\"\n          [disabled]=\"disabled()\"\n          (keydown.{home,end,arrowleft,arrowright,arrowdown,arrowup}.prevent)=\"handleKeydown($event, $index)\"\n        />\n      }\n    </span>\n  `,\n  providers: [\n    {\n      provide: NG_VALUE_ACCESSOR,\n      useExisting: forwardRef(() => ZardSliderComponent),\n      multi: true,\n    },\n  ],\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    '[attr.data-orientation]': 'zOrientation()',\n    '[attr.aria-disabled]': 'disabled() ? true : null',\n    '[attr.data-disabled]': 'disabled() ? true : null',\n  },\n  exportAs: 'zSlider',\n})\nexport class ZardSliderComponent implements ControlValueAccessor, AfterViewInit {\n  private readonly destroyRef = inject(DestroyRef);\n  private readonly document = inject(DOCUMENT);\n  private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);\n\n  readonly zMin = input(0, { transform: numberAttribute });\n  readonly zMax = input(100, { transform: numberAttribute });\n  readonly zDefault = input<number[]>([0]);\n  readonly zValue = input<number[]>([]);\n  readonly zStep = input(1, { transform: numberAttribute });\n  readonly zDisabled = input(false, { transform: booleanAttribute });\n\n  readonly zOrientation = input<'horizontal' | 'vertical'>('horizontal');\n  readonly class = input<ClassValue>('');\n\n  readonly zSlideIndexChange = output<number[]>();\n\n  readonly thumbRefs = viewChildren(ZSliderThumbComponent);\n  readonly trackRef = viewChild.required(ZSliderTrackComponent);\n\n  protected readonly classes = computed(() => mergeClasses(sliderVariants(), this.class()));\n\n  protected readonly disabled = linkedSignal(() => this.zDisabled());\n  readonly activeThumbIndex = signal(0);\n  readonly values = linkedSignal(() => {\n    const v = this.zValue();\n    if (Array.isArray(v) && v.length) {\n      return v;\n    }\n    const d = this.zDefault();\n    if (Array.isArray(d) && d.length) {\n      return d;\n    }\n    return this.getMinMax();\n  });\n\n  protected readonly percentages = computed(() => {\n    if (this.zMax() > 1) {\n      return this.values();\n    }\n    const [min, max] = [this.zMin(), this.zMax()];\n    return this.values().map(v => convertValueToPercentage(v, min, max));\n  });\n\n  readonly lastEmittedValue = signal<number[]>([]);\n  readonly thumbOffset = signal(0);\n\n  private onTouched: OnTouchedType = noopFn;\n  private onChange: OnChangeType = noopFn;\n\n  constructor() {\n    toObservable(this.zValue)\n      .pipe(\n        filter(values => values.toString() !== this.lastEmittedValue().toString()),\n        takeUntilDestroyed(this.destroyRef),\n      )\n      .subscribe(() => this.setInitialValue());\n  }\n\n  ngAfterViewInit() {\n    const pointerDown$ = fromEvent<PointerEvent>(this.elementRef.nativeElement, 'pointerdown').pipe(\n      filter(() => !this.disabled()),\n      tap(event => {\n        const target = event.target as HTMLElement;\n        const thumbs = this.thumbRefs();\n\n        const clickedIndex = thumbs.findIndex(t => t.nativeElement.contains(target));\n        if (clickedIndex !== -1) {\n          this.activeThumbIndex.set(clickedIndex);\n          return;\n        }\n\n        const isTrack = this.trackRef().nativeElement.contains(target);\n        if (isTrack) {\n          const coord = this.zOrientation() === 'vertical' ? event.clientY : event.clientX;\n          const clickPercentage = this.calculatePercentage(coord);\n          let clickValue: number;\n          if (this.zMax() <= 1) {\n            const [userMin, userMax] = [this.zMin(), this.zMax()];\n            clickValue = userMin + (userMax - userMin) * clickPercentage;\n          } else {\n            clickValue = clamp(clickPercentage * 100, this.getMinMax());\n          }\n\n          const currentValues = this.values();\n          const closestIndex = currentValues.reduce(\n            (prev, curr, i) => (Math.abs(curr - clickValue) < Math.abs(currentValues[prev] - clickValue) ? i : prev),\n            0,\n          );\n\n          this.activeThumbIndex.set(closestIndex);\n          this.updateThumbFromPercentage(clickPercentage, closestIndex);\n          this.onTouched();\n          requestAnimationFrame(() => {\n            thumbs[closestIndex]?.nativeElement.focus();\n          });\n        }\n      }),\n    );\n\n    const pointerMove$ = fromEvent<PointerEvent>(this.document, 'pointermove');\n    const pointerUp$ = fromEvent<PointerEvent>(this.document, 'pointerup');\n\n    pointerDown$\n      .pipe(\n        switchMap(() =>\n          pointerMove$.pipe(\n            takeUntil(pointerUp$),\n            map(event => {\n              const coord = this.zOrientation() === 'vertical' ? event.clientY : event.clientX;\n              return this.calculatePercentage(coord);\n            }),\n          ),\n        ),\n        takeUntilDestroyed(this.destroyRef),\n      )\n      .subscribe(percentage => {\n        this.updateThumbFromPercentage(percentage, this.activeThumbIndex());\n        this.onTouched();\n      });\n\n    this.setInitialValue();\n  }\n\n  writeValue(value: number | number[]): void {\n    if (value == null) {\n      this.setInitialValue();\n      return;\n    }\n\n    const [min, max] = this.getMinMax();\n    const step = this.zStep();\n\n    const values = Array.isArray(value)\n      ? value.map(v => roundToStep(clamp(v, [min, max]), min, step))\n      : [roundToStep(clamp(value, [min, max]), min, step)];\n\n    if (values.toString() === this.lastEmittedValue().toString()) {\n      return;\n    }\n\n    this.lastEmittedValue.set(values);\n    this.values.set(values);\n  }\n\n  registerOnChange(fn: OnChangeType): void {\n    this.onChange = fn;\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn;\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled.set(isDisabled);\n  }\n\n  handleKeydown(event: Event, thumbIndex: number): void {\n    if (this.disabled()) {\n      return;\n    }\n\n    const [min, max] = this.getMinMax();\n    const currentValues = [...this.values()];\n    const currentValue = currentValues[thumbIndex];\n    let newValue = currentValue;\n\n    const { key } = event as KeyboardEvent;\n\n    switch (key) {\n      case 'Home':\n        newValue = min;\n        break;\n      case 'End':\n        newValue = max;\n        break;\n      case 'ArrowLeft':\n      case 'ArrowDown':\n        newValue = Math.max(currentValue - this.zStep(), min);\n        break;\n      case 'ArrowRight':\n      case 'ArrowUp':\n        newValue = Math.min(currentValue + this.zStep(), max);\n        break;\n      default:\n        return;\n    }\n\n    if (currentValues.length > 1) {\n      if (thumbIndex > 0) {\n        newValue = Math.max(newValue, currentValues[thumbIndex - 1]);\n      }\n      if (thumbIndex < currentValues.length - 1) {\n        newValue = Math.min(newValue, currentValues[thumbIndex + 1]);\n      }\n    }\n\n    if (newValue !== currentValue) {\n      currentValues[thumbIndex] = newValue;\n      this.zSlideIndexChange.emit(currentValues);\n      this.lastEmittedValue.set(currentValues);\n      this.values.set(currentValues);\n      this.onChange(currentValues);\n    }\n  }\n\n  private updateThumbFromPercentage(percentage: number, thumbIndex: number): void {\n    const [min, max] = this.getMinMax();\n    let value: number;\n\n    if (this.zMax() <= 1) {\n      const [userMin, userMax] = [this.zMin(), this.zMax()];\n      value = roundToStep(userMin + (userMax - userMin) * clamp(percentage, [0, 1]), userMin, this.zStep());\n      value = clamp(value, [min, max]);\n    } else {\n      value = roundToStep(clamp(percentage * 100, [min, max]), min, this.zStep());\n    }\n\n    const currentValues = [...this.values()];\n\n    if (currentValues.length > 1) {\n      if (thumbIndex > 0) {\n        value = Math.max(value, currentValues[thumbIndex - 1]);\n      }\n      if (thumbIndex < currentValues.length - 1) {\n        value = Math.min(value, currentValues[thumbIndex + 1]);\n      }\n    }\n\n    currentValues[thumbIndex] = value;\n\n    if (currentValues.toString() !== this.lastEmittedValue().toString()) {\n      this.zSlideIndexChange.emit(currentValues);\n      this.lastEmittedValue.set(currentValues);\n      this.values.set(currentValues);\n      this.onChange(currentValues);\n    }\n  }\n\n  private calculatePercentage(clientCoord: number): number {\n    const rect = this.trackRef().nativeElement.getBoundingClientRect();\n    if (this.zOrientation() === 'vertical') {\n      const relativeY = (clientCoord - rect.top) / rect.height;\n      return clamp(1 - relativeY, [0, 1]);\n    }\n    const relativeX = (clientCoord - rect.left) / rect.width;\n    return clamp(relativeX, [0, 1]);\n  }\n\n  private setInitialValue(): void {\n    const [min, max] = this.getMinMax();\n    const step = this.zStep();\n\n    const rawValues = this.zValue();\n    const defaults = this.zDefault();\n\n    const count = Math.max(rawValues.length, defaults.length, 1);\n    const values: number[] = [];\n\n    for (let i = 0; i < count; i++) {\n      const def = clamp(defaults[i] ?? min, [min, max]);\n      const raw = rawValues[i];\n      const value = raw !== undefined && raw >= min && raw <= max ? raw : def;\n      values.push(roundToStep(value, min, step));\n    }\n\n    this.lastEmittedValue.set(values);\n    this.values.set(values);\n    this.thumbOffset.set(0);\n  }\n\n  private getMinMax(): [number, number] {\n    return [Math.max(this.zMin(), 0), Math.min(this.zMax(), 100)];\n  }\n}\n"
    },
    {
      "name": "slider.variants.ts",
      "content": "import { cva } from 'class-variance-authority';\n\nexport const sliderVariants = cva(\n  'relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col',\n);\n\nexport const sliderTrackVariants = cva(\n  'bg-muted relative grow overflow-hidden rounded-full data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1',\n);\n\nexport const sliderRangeVariants = cva('bg-primary absolute select-none data-horizontal:h-full data-vertical:w-full');\n\nexport const sliderThumbVariants = cva(\n  'relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none 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\nexport const sliderOrientationVariants = cva('absolute', {\n  variants: {\n    zOrientation: {\n      horizontal: 'translate-x-[-50%]',\n      vertical: 'translate-y-[50%]',\n    },\n  },\n  defaultVariants: {\n    zOrientation: 'horizontal',\n  },\n});\n"
    },
    {
      "name": "index.ts",
      "content": "export * from './slider.component';\nexport * from './slider.variants';\n"
    }
  ],
  "demos": [
    {
      "name": "controlled.ts",
      "content": "import { Component, signal } from '@angular/core';\n\nimport { ZardFieldImports } from '@/shared/components/field';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-controlled',\n  imports: [ZardSliderComponent, ...ZardFieldImports],\n  template: `\n    <div class=\"flex min-h-50 w-full flex-col items-center justify-center gap-2 p-10\">\n      <div class=\"mx-auto flex w-full max-w-xs justify-between\">\n        <label z-field-label for=\"slider-demo-temperature\">Temperature</label>\n        <span class=\"text-muted-foreground text-sm\">{{ value().join(', ') }}</span>\n      </div>\n      <z-slider\n        id=\"slider-demo-temperature\"\n        class=\"mx-auto w-full max-w-xs\"\n        zMin=\"0\"\n        zMax=\"1\"\n        zStep=\"0.1\"\n        [zValue]=\"value()\"\n        (zSlideIndexChange)=\"onSlide($event)\"\n      />\n    </div>\n  `,\n})\nexport class ZardDemoSliderControlledComponent {\n  readonly value = signal([0.3, 0.7]);\n\n  onSlide(value: number[]) {\n    this.value.set(value.map(v => Math.round(v * 10) / 10));\n  }\n}\n"
    },
    {
      "name": "default.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-default',\n  imports: [ZardSliderComponent],\n  template: `\n    <div class=\"flex min-h-50 w-full items-center p-10\">\n      <z-slider class=\"mx-auto w-full max-w-xs\" [zDefault]=\"[75]\" />\n    </div>\n  `,\n})\nexport class ZardDemoSliderDefaultComponent {}\n"
    },
    {
      "name": "disabled.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-disabled',\n  imports: [ZardSliderComponent],\n  template: `\n    <div class=\"flex min-h-50 w-full items-center p-10\">\n      <z-slider class=\"mx-auto w-full max-w-xs\" [zDefault]=\"[50]\" zDisabled />\n    </div>\n  `,\n})\nexport class ZardDemoSliderDisabledComponent {}\n"
    },
    {
      "name": "multiple.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-multiple',\n  imports: [ZardSliderComponent],\n  template: `\n    <div class=\"flex min-h-50 w-full items-center justify-center p-10\">\n      <z-slider class=\"mx-auto w-full max-w-xs\" [zDefault]=\"[10, 20, 70]\" />\n    </div>\n  `,\n})\nexport class ZardDemoSliderMultipleComponent {}\n"
    },
    {
      "name": "range.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-range',\n  imports: [ZardSliderComponent],\n  template: `\n    <div class=\"flex min-h-50 w-full items-center p-10\">\n      <z-slider class=\"mx-auto w-full max-w-xs\" [zDefault]=\"[25, 50]\" />\n    </div>\n  `,\n})\nexport class ZardDemoSliderRangeComponent {}\n"
    },
    {
      "name": "vertical.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardSliderComponent } from '../slider.component';\n\n@Component({\n  selector: 'z-demo-slider-vertical',\n  imports: [ZardSliderComponent],\n  template: `\n    <div class=\"flex h-50 w-full items-center justify-center\">\n      <div class=\"flex h-full w-20 justify-center gap-6\">\n        <z-slider [zDefault]=\"[50]\" zOrientation=\"vertical\" />\n        <z-slider [zDefault]=\"[25]\" zOrientation=\"vertical\" />\n      </div>\n    </div>\n  `,\n})\nexport class ZardDemoSliderVerticalComponent {}\n"
    }
  ]
}
