{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-spy",
  "title": "ScrollSpy",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/scroll-spy/scroll-spy.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnDestroy,\n  OnInit,\n  Output,\n  ViewChild,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { resolveScrollSpyColor } from './scroll-spy.types'\nimport type {\n  RegisteredItem,\n  ScrollSpyColor,\n  ScrollSpyIndicatorMode,\n  ScrollSpyItem,\n  ScrollSpyLineWidth,\n  ScrollSpyPosition,\n  ScrollSpyRailPosition,\n  ScrollSpyTurn,\n  ScrollSpyVariant,\n} from './scroll-spy.types'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-title, [ui-scroll-spy-title]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"scroll-spy-title\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiScrollSpyTitleComponent {\n  @Input('class') className?: string\n\n  readonly root: UiScrollSpyComponent | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n  }\n\n  get hostClass(): string {\n    const isRightRail = this.root?.position === 'left' && this.root?.resolvedRailPosition === 'right'\n    return cn(\n      'scroll-spy-title text-foreground mb-3 text-sm font-semibold tracking-tight',\n      isRightRail && 'pr-3 text-right',\n      this.className,\n    )\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-list, [ui-scroll-spy-list]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"scroll-spy-list\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n    '[style.borderLeftWidth]': 'leftBorderWidth',\n    '[style.borderRightWidth]': 'rightBorderWidth',\n  },\n  template: `<ng-content />`,\n})\nexport class UiScrollSpyListComponent {\n  @Input('class') className?: string\n\n  readonly root: UiScrollSpyComponent | null = null\n  readonly el: ElementRef<HTMLElement> | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n    try {\n      this.el = inject<ElementRef<HTMLElement>>(ElementRef, { optional: true })\n    } catch {}\n  }\n\n  get isStraight(): boolean {\n    return this.root?.resolvedTurn === 'straight'\n  }\n\n  get isRightRail(): boolean {\n    return this.root?.position === 'left' && this.root?.resolvedRailPosition === 'right'\n  }\n\n  get leftBorderWidth(): string | undefined {\n    return this.isStraight && !this.isRightRail ? `${this.root?.resolvedLineWidth ?? 2.5}px` : undefined\n  }\n\n  get rightBorderWidth(): string | undefined {\n    return this.isStraight && this.isRightRail ? `${this.root?.resolvedLineWidth ?? 2.5}px` : undefined\n  }\n\n  get hostClass(): string {\n    return cn(\n      'scroll-spy-list relative flex flex-col space-y-1 text-sm',\n      this.isStraight && (this.isRightRail ? 'border-border border-r' : 'border-border border-l'),\n      this.className,\n    )\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-item, [ui-scroll-spy-item]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"scroll-spy-item\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-depth]': 'depth',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiScrollSpyItemComponent implements OnInit, OnDestroy {\n  @Input() value = ''\n  @Input() title?: string\n  @Input() depth = 1\n  @Input('class') className?: string\n\n  readonly root: UiScrollSpyComponent | null = null\n  readonly el: ElementRef<HTMLElement> | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n    try {\n      this.el = inject<ElementRef<HTMLElement>>(ElementRef, { optional: true })\n    } catch {}\n  }\n\n  ngOnInit(): void {\n    if (this.root && this.value) {\n      this.root.registerItem({\n        value: this.value,\n        depth: this.depth,\n        el: this.el?.nativeElement ?? null,\n        title: this.title,\n      })\n    }\n  }\n\n  ngOnDestroy(): void {\n    if (this.root && this.value) {\n      this.root.unregisterItem(this.value)\n    }\n  }\n\n  get hostClass(): string {\n    return cn('scroll-spy-item relative flex flex-col', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-link, [ui-scroll-spy-link]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"scroll-spy-link\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-active]': 'resolvedActive ? \"true\" : \"false\"',\n    '[attr.data-parent-active]': 'resolvedParentActive ? \"true\" : \"false\"',\n    '[attr.data-scrolled]': 'resolvedScrolled ? \"true\" : \"false\"',\n    '[attr.data-depth]': 'depth',\n    '[class]': 'hostClass',\n    '[style.borderLeftWidth]': 'borderLeftWidth',\n    '[style.borderRightWidth]': 'borderRightWidth',\n    '[style.marginLeft]': 'marginLeft',\n    '[style.marginRight]': 'marginRight',\n  },\n  template: `\n    <a\n      [href]=\"href\"\n      (click)=\"onClick($event)\"\n      [attr.aria-current]=\"resolvedActive ? 'location' : null\"\n      class=\"focus-visible:ring-ring block truncate leading-snug no-underline transition-colors duration-150 focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\"\n    >\n      {{ title }}<ng-content />\n    </a>\n  `,\n})\nexport class UiScrollSpyLinkComponent {\n  @Input() href = ''\n  @Input() title?: string\n  @Input() depth = 1\n  @Input({ transform: booleanAttribute }) active = false\n  @Input({ transform: booleanAttribute }) scrolled = false\n  @Input('class') className?: string\n\n  readonly root: UiScrollSpyComponent | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n  }\n\n  get resolvedActive(): boolean {\n    return this.active || (this.root ? this.root.isItemActive(this.href) : false)\n  }\n\n  get resolvedParentActive(): boolean {\n    return this.root ? this.root.isItemParentActive(this.href) : false\n  }\n\n  get resolvedScrolled(): boolean {\n    return this.scrolled || (this.root ? this.root.isItemScrolled(this.href) : false)\n  }\n\n  get isLeftWithRightRail(): boolean {\n    return this.root?.position === 'left' && this.root?.resolvedRailPosition === 'right'\n  }\n\n  get isCircuit(): boolean {\n    return this.root?.resolvedTurn !== 'straight'\n  }\n\n  get hasIndicatorBar(): boolean {\n    return this.root?.resolvedTurn === 'straight' && (this.root?.indicator !== 'segment' || this.root?.keepScrolled)\n  }\n\n  get borderLeftWidth(): string | undefined {\n    if (\n      !this.isCircuit &&\n      !this.hasIndicatorBar &&\n      (this.resolvedActive || this.resolvedParentActive || this.resolvedScrolled)\n    ) {\n      return this.isLeftWithRightRail ? undefined : `${this.root?.resolvedLineWidth ?? 2.5}px`\n    }\n    return undefined\n  }\n\n  get borderRightWidth(): string | undefined {\n    if (\n      !this.isCircuit &&\n      !this.hasIndicatorBar &&\n      (this.resolvedActive || this.resolvedParentActive || this.resolvedScrolled)\n    ) {\n      return this.isLeftWithRightRail ? `${this.root?.resolvedLineWidth ?? 2.5}px` : undefined\n    }\n    return undefined\n  }\n\n  get marginLeft(): string | undefined {\n    if (\n      !this.isCircuit &&\n      !this.hasIndicatorBar &&\n      (this.resolvedActive || this.resolvedParentActive || this.resolvedScrolled)\n    ) {\n      return this.isLeftWithRightRail ? undefined : `-${this.root?.resolvedLineWidth ?? 2.5}px`\n    }\n    return undefined\n  }\n\n  get marginRight(): string | undefined {\n    if (\n      !this.isCircuit &&\n      !this.hasIndicatorBar &&\n      (this.resolvedActive || this.resolvedParentActive || this.resolvedScrolled)\n    ) {\n      return this.isLeftWithRightRail ? `-${this.root?.resolvedLineWidth ?? 2.5}px` : undefined\n    }\n    return undefined\n  }\n\n  get hostClass(): string {\n    const handleColor = resolveScrollSpyColor(this.root?.color ?? 'primary')\n    const borderActiveClass = this.hasIndicatorBar\n      ? this.resolvedActive || this.resolvedParentActive\n        ? 'text-foreground font-medium'\n        : this.resolvedScrolled\n          ? 'text-foreground/85'\n          : 'text-muted-foreground hover:text-foreground'\n      : this.resolvedActive\n        ? cn(handleColor.borderClass, 'text-foreground font-medium')\n        : this.resolvedParentActive\n          ? 'border-border/50 text-foreground font-medium'\n          : this.resolvedScrolled\n            ? 'border-border/70 text-foreground/85'\n            : 'text-muted-foreground hover:border-foreground/40 hover:text-foreground'\n\n    return cn(\n      'scroll-spy-link group block rounded-none leading-snug no-underline transition-[color,border-color,background-color,opacity,border-width,margin] duration-200 ease-out',\n      this.isCircuit\n        ? [\n            'py-1',\n            this.isLeftWithRightRail\n              ? [\n                  'text-right',\n                  this.depth <= 1 && 'pr-4 pl-2 text-sm',\n                  this.depth === 2 && 'pr-7 pl-2 text-xs',\n                  this.depth === 3 && 'pr-10 pl-2 text-xs',\n                  this.depth >= 4 && 'pr-12 pl-2 text-xs',\n                ]\n              : [\n                  this.depth <= 1 && 'pr-2 pl-4 text-sm',\n                  this.depth === 2 && 'pr-2 pl-7 text-xs',\n                  this.depth === 3 && 'pr-2 pl-10 text-xs',\n                  this.depth >= 4 && 'pr-2 pl-12 text-xs',\n                ],\n            this.resolvedActive || this.resolvedParentActive\n              ? 'text-foreground font-medium'\n              : this.resolvedScrolled\n                ? 'text-foreground/85'\n                : 'text-muted-foreground hover:text-foreground',\n          ]\n        : this.isLeftWithRightRail\n          ? [\n              '-mr-px border-r border-transparent py-0.5 pr-3 pl-2 text-right',\n              this.depth <= 1 && 'text-sm',\n              this.depth === 2 && 'pr-6 text-xs',\n              this.depth === 3 && 'pr-9 text-xs',\n              this.depth >= 4 && 'pr-12 text-xs',\n              borderActiveClass,\n            ]\n          : [\n              '-ml-px border-l border-transparent py-0.5 pr-2 pl-3 text-left',\n              this.depth <= 1 && 'text-sm',\n              this.depth === 2 && 'pl-6 text-xs',\n              this.depth === 3 && 'pl-9 text-xs',\n              this.depth >= 4 && 'pl-12 text-xs',\n              borderActiveClass,\n            ],\n      this.className,\n    )\n  }\n\n  onClick(e: MouseEvent): void {\n    if (this.root && this.href) {\n      e.preventDefault()\n      this.root.scrollToHref(this.href)\n    }\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-indicator, [ui-scroll-spy-indicator]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"scroll-spy-indicator\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (root && root.resolvedTurn !== 'straight') {\n      <svg class=\"absolute inset-0 size-full overflow-visible\" fill=\"none\">\n        <path\n          [attr.d]=\"trackPath\"\n          class=\"stroke-border\"\n          [attr.stroke-width]=\"root.resolvedLineWidth\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n        />\n        @if (trackPath && activeEnd > 0) {\n          <path\n            [attr.d]=\"trackPath\"\n            [class]=\"handleStroke\"\n            [attr.stroke-width]=\"root.resolvedLineWidth\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            [style.strokeDasharray]=\"activeEnd - activeStart + ' ' + pathLength\"\n            [style.strokeDashoffset]=\"-activeStart\"\n          />\n        }\n      </svg>\n    } @else if (root && root.resolvedTurn === 'straight' && (root.indicator !== 'segment' || root.keepScrolled)) {\n      <div\n        class=\"pointer-events-none absolute transition-[top,height] duration-200 ease-out\"\n        [class]=\"handleBg\"\n        [style.width.px]=\"root.resolvedLineWidth\"\n        [style.left.px]=\"isRightRail ? null : -root.resolvedLineWidth\"\n        [style.right.px]=\"isRightRail ? -root.resolvedLineWidth : null\"\n        [style.top.px]=\"highlightTop\"\n        [style.height.px]=\"highlightHeight\"\n      ></div>\n    }\n  `,\n})\nexport class UiScrollSpyIndicatorComponent {\n  @Input() color?: ScrollSpyColor\n  @Input('class') className?: string\n\n  trackPath = ''\n  pathLength = 0\n  activeStart = 0\n  activeEnd = 0\n  highlightTop = 0\n  highlightHeight = 0\n\n  readonly root: UiScrollSpyComponent | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n  }\n\n  get isRightRail(): boolean {\n    return this.root?.position === 'left' && this.root?.resolvedRailPosition === 'right'\n  }\n\n  get handleBg(): string {\n    return resolveScrollSpyColor(this.color ?? this.root?.color ?? 'primary').bgClass\n  }\n\n  get handleStroke(): string {\n    return resolveScrollSpyColor(this.color ?? this.root?.color ?? 'primary').strokeClass\n  }\n\n  get hostClass(): string {\n    return cn('scroll-spy-indicator pointer-events-none absolute inset-0 overflow-visible', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy-stepper, [ui-scroll-spy-stepper]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'slotName',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (root && root.position === 'bottom') {\n      <button\n        type=\"button\"\n        aria-label=\"Previous section\"\n        [disabled]=\"activeIndex <= 0\"\n        class=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground focus-visible:ring-ring flex size-7 items-center justify-center rounded-full transition-[color,background-color,box-shadow,opacity,scale] outline-none focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n        (click)=\"root.goToPrev()\"\n      >\n        <svg class=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 19.5L8.25 12l7.5-7.5\" />\n        </svg>\n      </button>\n\n      <div class=\"flex items-center gap-1.5 px-1\">\n        @for (item of root.items; track item.href ?? item.value; let idx = $index) {\n          <button\n            type=\"button\"\n            [attr.aria-label]=\"'Jump to section ' + (idx + 1)\"\n            class=\"focus-visible:ring-ring h-1.5 cursor-pointer rounded-full transition-[width,background-color,box-shadow] duration-200 outline-none focus-visible:ring-2 focus-visible:ring-offset-2\"\n            [class.bg-primary]=\"idx === activeIndex\"\n            [class.w-5]=\"idx === activeIndex\"\n            [class.bg-primary/40]=\"idx < activeIndex\"\n            [class.hover:bg-primary/60]=\"idx < activeIndex\"\n            [class.w-2]=\"idx !== activeIndex\"\n            [class.bg-muted-foreground/30]=\"idx > activeIndex\"\n            [class.hover:bg-muted-foreground/50]=\"idx > activeIndex\"\n            (click)=\"root.scrollToHref(item.href ?? item.value ?? '')\"\n          ></button>\n        }\n      </div>\n\n      <div class=\"border-border/60 flex items-center gap-2 border-l pl-2\">\n        <span class=\"text-foreground max-w-[130px] truncate text-xs font-medium tracking-tight\">\n          {{ activeTitle }}\n        </span>\n        <span class=\"bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 font-mono text-[10px] tabular-nums\">\n          {{ Math.round(rootProgress * 100) }}%\n        </span>\n      </div>\n\n      <button\n        type=\"button\"\n        aria-label=\"Next section\"\n        [disabled]=\"activeIndex >= root.items.length - 1\"\n        class=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground focus-visible:ring-ring flex size-7 items-center justify-center rounded-full transition-[color,background-color,box-shadow,opacity,scale] outline-none focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n        (click)=\"root.goToNext()\"\n      >\n        <svg class=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 4.5l7.5 7.5-7.5 7.5\" />\n        </svg>\n      </button>\n    } @else if (root && isScrollSpy) {\n      @for (item of root.items; track item.href ?? item.value; let idx = $index) {\n        <button\n          type=\"button\"\n          [attr.data-active]=\"idx === activeIndex ? 'true' : 'false'\"\n          class=\"group focus-visible:ring-ring relative flex shrink-0 items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium transition-[color,background-color,box-shadow,scale] duration-150 outline-none select-none focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-[0.98]\"\n          [class.bg-primary/10]=\"idx === activeIndex\"\n          [class.text-primary]=\"idx === activeIndex\"\n          [class.font-semibold]=\"idx === activeIndex\"\n          [class.shadow-2xs]=\"idx === activeIndex\"\n          [class.text-muted-foreground]=\"idx !== activeIndex\"\n          [class.hover:bg-muted/50]=\"idx !== activeIndex\"\n          [class.hover:text-foreground]=\"idx !== activeIndex\"\n          (click)=\"root.scrollToHref(item.href ?? item.value ?? '')\"\n        >\n          @if (idx === activeIndex) {\n            <span class=\"bg-primary animate-in fade-in zoom-in-75 size-1.5 shrink-0 rounded-full duration-150\"></span>\n          }\n          <span class=\"truncate\">{{ item.title || (item.href ?? item.value ?? '') }}</span>\n        </button>\n      }\n      <div class=\"bg-border/20 absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-xl\">\n        <div\n          class=\"bg-primary h-full transition-[width] duration-150 ease-out\"\n          [style.width.%]=\"Math.round(rootProgress * 100)\"\n        ></div>\n      </div>\n    } @else if (root) {\n      @for (item of root.items; track item.href ?? item.value; let idx = $index) {\n        <button\n          type=\"button\"\n          [attr.data-active]=\"idx === activeIndex ? 'true' : 'false'\"\n          class=\"group focus-visible:ring-ring flex shrink-0 items-center gap-2 rounded-md px-2.5 py-1.5 text-xs font-medium transition-[color,background-color,scale] duration-150 outline-none select-none focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-[0.98]\"\n          [class.bg-primary/10]=\"idx === activeIndex\"\n          [class.text-foreground]=\"idx === activeIndex\"\n          [class.font-medium]=\"idx === activeIndex\"\n          [class.shadow-2xs]=\"idx === activeIndex\"\n          [class.text-foreground/80]=\"idx < activeIndex\"\n          [class.hover:bg-muted/50]=\"idx < activeIndex\"\n          [class.text-muted-foreground]=\"idx > activeIndex\"\n          [class.hover:bg-muted/30]=\"idx > activeIndex\"\n          [class.hover:text-foreground]=\"idx > activeIndex\"\n          (click)=\"root.scrollToHref(item.href ?? item.value ?? '')\"\n        >\n          <span\n            class=\"flex size-5 shrink-0 items-center justify-center rounded-full font-mono text-[10px] transition-[color,background-color,box-shadow,scale] duration-200\"\n            [class.bg-primary]=\"idx === activeIndex\"\n            [class.text-primary-foreground]=\"idx === activeIndex\"\n            [class.scale-105]=\"idx === activeIndex\"\n            [class.font-semibold]=\"idx === activeIndex\"\n            [class.shadow-2xs]=\"idx === activeIndex\"\n            [class.bg-primary/15]=\"idx < activeIndex\"\n            [class.text-primary]=\"idx < activeIndex\"\n            [class.font-medium]=\"idx < activeIndex\"\n            [class.bg-muted]=\"idx > activeIndex\"\n            [class.text-muted-foreground/70]=\"idx > activeIndex\"\n          >\n            @if (idx < activeIndex) {\n              <svg class=\"size-3 stroke-[2.5]\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 12.75l6 6 9-13.5\" />\n              </svg>\n            } @else {\n              {{ idx + 1 }}\n            }\n          </span>\n          <span class=\"max-w-[120px] truncate\">{{ item.title || (item.href ?? item.value ?? '') }}</span>\n        </button>\n        @if (idx < root.items.length - 1) {\n          <div\n            class=\"bg-border/70 h-0.5 max-w-10 min-w-4 flex-1 rounded-full transition-colors duration-200\"\n            [class.bg-primary]=\"idx < activeIndex\"\n          ></div>\n        }\n      }\n    }\n  `,\n})\nexport class UiScrollSpyStepperComponent {\n  @Input('class') className?: string\n\n  Math = Math\n\n  readonly root: UiScrollSpyComponent | null = null\n\n  constructor() {\n    try {\n      this.root = inject(\n        forwardRef(() => UiScrollSpyComponent),\n        { optional: true },\n      )\n    } catch {}\n  }\n\n  get isScrollSpy(): boolean {\n    return (\n      this.root?.variant === 'scrollspy' ||\n      this.root?.variant === 'tabs' ||\n      this.root?.variant === 'pills' ||\n      this.root?.indicator === 'pill' ||\n      this.root?.indicator === 'dot'\n    )\n  }\n\n  get slotName(): string {\n    if (this.root?.position === 'bottom') return 'scroll-spy-stepper-bottom'\n    return this.isScrollSpy ? 'scroll-spy-top' : 'scroll-spy-stepper-top'\n  }\n\n  get activeIndex(): number {\n    if (!this.root || !this.root.items) return 0\n    const val = this.root.modelValue.replace(/^#/, '')\n    const idx = this.root.items.findIndex((i) => (i.href ?? i.value ?? '').replace(/^#/, '') === val)\n    return idx >= 0 ? idx : 0\n  }\n\n  get activeTitle(): string {\n    return this.root?.items?.[this.activeIndex]?.title ?? ''\n  }\n\n  get rootProgress(): number {\n    return this.root?.currentProgress ?? 0\n  }\n\n  get hostClass(): string {\n    if (this.root?.position === 'bottom') {\n      return cn(\n        'border-border/80 bg-background/95 sticky bottom-3 z-30 mx-auto flex items-center gap-2 rounded-full border px-3 py-1.5 shadow-md backdrop-blur-md select-none',\n        this.className,\n      )\n    }\n    if (this.isScrollSpy) {\n      return cn(\n        'border-border/70 bg-card/85 relative sticky top-0 z-20 flex w-full scrollbar-none items-center gap-1 overflow-x-auto rounded-xl border p-1.5 shadow-xs backdrop-blur-md',\n        this.className,\n      )\n    }\n    return cn(\n      'border-border/70 bg-card/85 sticky top-0 z-20 flex w-full scrollbar-none items-center gap-1.5 overflow-x-auto rounded-xl border p-2 shadow-xs backdrop-blur-md',\n      this.className,\n    )\n  }\n}\n\n/**\n * Angular port of UIPKGE ScrollSpy. In-page navigation list where the active\n * item highlights as the user scrolls through anchored sections. Root +\n * title/list/item/link/indicator/stepper parts mirror the Vue file splits;\n * intersection observation is plain inputs/outputs at the consumer level.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-spy, [ui-scroll-spy]',\n  standalone: true,\n  imports: [\n    UiScrollSpyTitleComponent,\n    UiScrollSpyListComponent,\n    UiScrollSpyItemComponent,\n    UiScrollSpyLinkComponent,\n    UiScrollSpyIndicatorComponent,\n    UiScrollSpyStepperComponent,\n  ],\n  host: {\n    '[attr.data-slot]': '\"scroll-spy\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-position]': 'resolvedPosition',\n    '[attr.data-rail-position]': 'resolvedRailPosition',\n    '[attr.data-variant]': 'resolvedVariant',\n    '[attr.data-turn]': 'resolvedTurn',\n    '[attr.data-indicator]': 'indicator',\n    '[attr.data-keep-scrolled]': 'keepScrolled ? \"true\" : null',\n    '[attr.data-highlight-parent]': 'highlightParent ? \"true\" : null',\n    '[attr.data-line-width]': 'lineWidth',\n    '[attr.data-color]': 'color',\n    '[class]': 'hostClass',\n    '[style.top.px]': 'affix && resolvedPosition !== \"bottom\" ? offsetTop : null',\n  },\n  template: `\n    @if (items && items.length > 0) {\n      @if (resolvedPosition === 'top') {\n        <ui-scroll-spy-stepper />\n      } @else if (resolvedPosition !== 'bottom') {\n        @if (title) {\n          <ui-scroll-spy-title>{{ title }}</ui-scroll-spy-title>\n        }\n        <ui-scroll-spy-list>\n          <ui-scroll-spy-indicator [color]=\"color\" />\n          @for (item of items; track item.href ?? item.value ?? $index; let itemIdx = $index) {\n            <ui-scroll-spy-item\n              [value]=\"item.href ?? item.value ?? ''\"\n              [depth]=\"item.depth ?? 1\"\n              [class]=\"itemIdx > 0 && items[itemIdx - 1]?.children?.length ? 'mt-2' : undefined\"\n            >\n              <ui-scroll-spy-link\n                [href]=\"item.href ?? item.value ?? ''\"\n                [title]=\"item.title\"\n                [depth]=\"item.depth ?? 1\"\n              />\n            </ui-scroll-spy-item>\n            @for (child of item.children ?? []; track child.href ?? child.value ?? $index; let childIdx = $index) {\n              <ui-scroll-spy-item\n                [value]=\"child.href ?? child.value ?? ''\"\n                [depth]=\"child.depth ?? 2\"\n                [class]=\"\n                  childIdx === 0 || (childIdx > 0 && item.children?.[childIdx - 1]?.children?.length)\n                    ? 'mt-2'\n                    : undefined\n                \"\n              >\n                <ui-scroll-spy-link\n                  [href]=\"child.href ?? child.value ?? ''\"\n                  [title]=\"child.title\"\n                  [depth]=\"child.depth ?? 2\"\n                />\n              </ui-scroll-spy-item>\n              @for (\n                grandchild of child.children ?? [];\n                track grandchild.href ?? grandchild.value ?? $index;\n                let gIdx = $index\n              ) {\n                <ui-scroll-spy-item\n                  [value]=\"grandchild.href ?? grandchild.value ?? ''\"\n                  [depth]=\"grandchild.depth ?? 3\"\n                  [class]=\"gIdx === 0 ? 'mt-2' : undefined\"\n                >\n                  <ui-scroll-spy-link\n                    [href]=\"grandchild.href ?? grandchild.value ?? ''\"\n                    [title]=\"grandchild.title\"\n                    [depth]=\"grandchild.depth ?? 3\"\n                  />\n                </ui-scroll-spy-item>\n              }\n            }\n          }\n        </ui-scroll-spy-list>\n      }\n      @if (resolvedPosition === 'bottom') {\n        <ui-scroll-spy-stepper />\n      }\n    } @else {\n      <ng-content />\n    }\n  `,\n})\nexport class UiScrollSpyComponent implements OnInit, OnDestroy {\n  @Input() modelValue = ''\n  @Output() modelValueChange = new EventEmitter<string>()\n  @Output() change = new EventEmitter<string>()\n  @Output() progress = new EventEmitter<number>()\n  @Input() items: ScrollSpyItem[] = []\n  @Input() title?: string\n  @Input() offsetTop = 0\n  @Input() bounds = 5\n  @Input() scrollContainer?: HTMLElement | string | null\n  @Input({ transform: booleanAttribute }) affix = false\n  @Input() variant?: ScrollSpyVariant\n  @Input() turn?: ScrollSpyTurn\n  @Input() indicator: ScrollSpyIndicatorMode = 'line'\n  @Input({ transform: booleanAttribute }) keepScrolled = false\n  @Input({ transform: booleanAttribute }) highlightParent = true\n  @Input() lineWidth: ScrollSpyLineWidth = 'default'\n  @Input() color: ScrollSpyColor = 'primary'\n  @Input() position: ScrollSpyPosition = 'right'\n  @Input() railPosition?: ScrollSpyRailPosition\n  private _className?: string\n  @Input('class') set classAttr(v: string | undefined) {\n    this._className = v\n  }\n  @Input() set className(v: string | undefined) {\n    this._className = v\n  }\n  get className(): string | undefined {\n    return this._className\n  }\n\n  registered: RegisteredItem[] = []\n  scrolledValues = new Set<string>()\n  currentProgress = 0\n\n  private scrollHandler = () => this.onScroll()\n  private containerEl: HTMLElement | Window | null = null\n\n  private readonly el: ElementRef<HTMLElement> | null = null\n\n  constructor() {\n    try {\n      this.el = inject<ElementRef<HTMLElement>>(ElementRef, { optional: true })\n    } catch {}\n  }\n\n  ngOnInit(): void {\n    if (typeof window !== 'undefined') {\n      this.initContainer()\n      if (this.items.length > 0 && !this.modelValue) {\n        this.modelValue = this.items[0]?.href ?? this.items[0]?.value ?? ''\n      }\n    }\n  }\n\n  ngOnDestroy(): void {\n    if (this.containerEl) {\n      this.containerEl.removeEventListener('scroll', this.scrollHandler)\n    }\n  }\n\n  private initContainer(): void {\n    if (typeof window === 'undefined') return\n    if (typeof this.scrollContainer === 'string') {\n      this.containerEl = document.querySelector(this.scrollContainer) as HTMLElement | null\n    } else if (this.scrollContainer instanceof HTMLElement) {\n      this.containerEl = this.scrollContainer\n    } else {\n      this.containerEl = window\n    }\n    if (this.containerEl) {\n      this.containerEl.addEventListener('scroll', this.scrollHandler, { passive: true })\n    }\n  }\n\n  private onScroll(): void {\n    const allItems = this.getAllItems()\n    if (allItems.length === 0) return\n\n    let current = allItems[0]?.href ?? allItems[0]?.value ?? ''\n    const offset = this.offsetTop + this.bounds + 20\n\n    for (const item of allItems) {\n      const id = (item.href ?? item.value ?? '').replace(/^#/, '')\n      const el = document.getElementById(id)\n      if (el) {\n        const rect = el.getBoundingClientRect()\n        if (rect.top <= offset) {\n          current = item.href ?? item.value ?? ''\n        }\n      }\n    }\n\n    if (current && current !== this.modelValue) {\n      this.setActive(current)\n    }\n  }\n\n  getAllItems(): ScrollSpyItem[] {\n    const list: ScrollSpyItem[] = []\n    const walk = (items: ScrollSpyItem[]) => {\n      for (const item of items) {\n        list.push(item)\n        if (item.children) walk(item.children)\n      }\n    }\n    walk(this.items)\n    return list\n  }\n\n  get resolvedPosition(): ScrollSpyPosition {\n    return this.position ?? 'right'\n  }\n\n  get resolvedRailPosition(): ScrollSpyRailPosition {\n    if (this.railPosition) return this.railPosition\n    return this.position === 'left' ? 'right' : 'left'\n  }\n\n  get resolvedTurn(): ScrollSpyTurn {\n    if (this.turn) return this.turn\n    if (this.variant === 'angle' || this.variant === 'rounded') return 'rounded'\n    if (this.variant === 'sharp') return 'sharp'\n    if (this.variant === 'line' || this.variant === 'default') return 'straight'\n    return 'straight'\n  }\n\n  get resolvedVariant(): ScrollSpyVariant {\n    if (this.variant) return this.variant\n    if (this.turn === 'sharp') return 'angle'\n    if (this.turn === 'rounded') return 'rounded'\n    if (this.position === 'top' || this.position === 'bottom') return 'stepper'\n    return 'line'\n  }\n\n  get resolvedLineWidth(): number {\n    if (typeof this.lineWidth === 'number') return this.lineWidth\n    return this.lineWidth === 'thin' ? 1.5 : this.lineWidth === 'thick' ? 3.5 : 2.5\n  }\n\n  get handleColor(): string {\n    return resolveScrollSpyColor(this.color).borderClass\n  }\n\n  get hostClass(): string {\n    const isTopOrBottom = this.resolvedPosition === 'top' || this.resolvedPosition === 'bottom'\n    return cn(\n      'relative flex text-sm',\n      isTopOrBottom ? 'w-full flex-col' : 'flex-col',\n      this.affix && (this.resolvedPosition === 'bottom' ? 'sticky bottom-4' : 'sticky'),\n      this.className,\n    )\n  }\n\n  registerItem(item: RegisteredItem): void {\n    if (!this.registered.some((i) => i.value === item.value)) this.registered.push(item)\n  }\n\n  unregisterItem(value: string): void {\n    this.registered = this.registered.filter((i) => i.value !== value)\n  }\n\n  normalize(href: string): string {\n    return href.replace(/^#/, '')\n  }\n\n  isItemActive(href: string): boolean {\n    return this.normalize(href) === this.normalize(this.modelValue)\n  }\n\n  isItemParentActive(href: string): boolean {\n    if (!this.highlightParent || this.isItemActive(href)) return this.isItemActive(href)\n    return this.registered.some((i) => this.isItemActive(i.value) && i.value.startsWith(this.normalize(href)))\n  }\n\n  isItemScrolled(href: string): boolean {\n    return this.scrolledValues.has(this.normalize(href))\n  }\n\n  setActive(value: string): void {\n    this.modelValue = value\n    this.modelValueChange.emit(value)\n    this.change.emit(value)\n  }\n\n  markScrolled(value: string, done = true): void {\n    const key = this.normalize(value)\n    if (done) this.scrolledValues.add(key)\n    else this.scrolledValues.delete(key)\n  }\n\n  reportProgress(value: number): void {\n    const p = Math.min(1, Math.max(0, value))\n    this.currentProgress = p\n    this.progress.emit(p)\n  }\n\n  scrollToHref(href: string): void {\n    this.setActive(href)\n    const id = this.normalize(href)\n    const target = document.getElementById(id)\n    if (target) {\n      target.scrollIntoView({ behavior: 'smooth' })\n    }\n  }\n\n  goToPrev(): void {\n    const items = this.getAllItems()\n    const idx = items.findIndex((i) => this.normalize(i.href ?? i.value ?? '') === this.normalize(this.modelValue))\n    if (idx > 0) {\n      this.scrollToHref(items[idx - 1]!.href ?? items[idx - 1]!.value ?? '')\n    }\n  }\n\n  goToNext(): void {\n    const items = this.getAllItems()\n    const idx = items.findIndex((i) => this.normalize(i.href ?? i.value ?? '') === this.normalize(this.modelValue))\n    if (idx >= 0 && idx < items.length - 1) {\n      this.scrollToHref(items[idx + 1]!.href ?? items[idx + 1]!.value ?? '')\n    }\n  }\n}\n\nexport * from './scroll-spy.types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/scroll-spy/scroll-spy.component.ts"
    },
    {
      "path": "packages/registry-angular/components/scroll-spy/scroll-spy.types.ts",
      "content": "/**\n * Angular port of the scroll-spy context types. Same exported names as the\n * Vue `context.ts` but framework-free (no vue/reka imports) so registry\n * consumers and vitest stay dependency-light.\n */\n\nexport type ScrollSpyTurn = 'straight' | 'sharp' | 'rounded'\nexport type ScrollSpyVariant =\n  'default' | 'line' | 'angle' | 'sharp' | 'rounded' | 'stepper' | 'scrollspy' | 'tabs' | 'pills'\nexport type ScrollSpyIndicatorMode = 'segment' | 'fill' | 'progress' | 'pill' | 'dot' | 'line'\nexport type ScrollSpyPosition = 'right' | 'left' | 'top' | 'bottom'\nexport type ScrollSpyRailPosition = 'left' | 'right'\n\nexport type ScrollSpyLineWidth = 'thin' | 'default' | 'thick' | number\nexport type ScrollSpyColor = 'primary' | 'foreground' | 'destructive' | 'secondary' | (string & {})\n\nexport interface ScrollSpyItem {\n  href?: string\n  value?: string\n  title: string\n  depth?: number\n  children?: ScrollSpyItem[]\n}\n\nexport interface RegisteredItem {\n  value: string\n  depth: number\n  el?: HTMLElement | null\n  title?: string\n}\n\nexport interface HandleColorResolved {\n  bgClass: string\n  strokeClass: string\n  borderClass: string\n  customColor?: string\n}\n\nexport function resolveScrollSpyColor(color: ScrollSpyColor = 'primary'): HandleColorResolved {\n  if (color === 'primary')\n    return {\n      bgClass: 'bg-primary',\n      strokeClass: 'stroke-primary',\n      borderClass: 'border-primary',\n      customColor: undefined,\n    }\n  if (color === 'foreground')\n    return {\n      bgClass: 'bg-foreground',\n      strokeClass: 'stroke-foreground',\n      borderClass: 'border-foreground',\n      customColor: undefined,\n    }\n  if (color === 'destructive')\n    return {\n      bgClass: 'bg-destructive',\n      strokeClass: 'stroke-destructive',\n      borderClass: 'border-destructive',\n      customColor: undefined,\n    }\n  if (color === 'secondary')\n    return {\n      bgClass: 'bg-secondary',\n      strokeClass: 'stroke-secondary',\n      borderClass: 'border-secondary',\n      customColor: undefined,\n    }\n  return { bgClass: '', strokeClass: '', borderClass: '', customColor: color }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/scroll-spy/scroll-spy.types.ts"
    },
    {
      "path": "packages/registry-angular/components/scroll-spy/index.ts",
      "content": "export {\n  UiScrollSpyComponent,\n  UiScrollSpyTitleComponent,\n  UiScrollSpyListComponent,\n  UiScrollSpyItemComponent,\n  UiScrollSpyLinkComponent,\n  UiScrollSpyIndicatorComponent,\n  UiScrollSpyStepperComponent,\n} from './scroll-spy.component'\nexport * from './scroll-spy.types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/scroll-spy/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "In-page navigation list with scroll-spy. Renders a vertical list of links; the active item highlights as the user scrolls through anchored sections.",
  "categories": [
    "navigation"
  ]
}