{
  "name": "tooltip",
  "type": "registry:component",
  "files": [
    {
      "name": "tooltip.ts",
      "content": "import { Overlay, OverlayPositionBuilder, type OverlayRef } from '@angular/cdk/overlay';\nimport { ComponentPortal } from '@angular/cdk/portal';\nimport { isPlatformBrowser, DOCUMENT } from '@angular/common';\nimport {\n  ChangeDetectionStrategy,\n  Component,\n  type ComponentRef,\n  computed,\n  DestroyRef,\n  Directive,\n  effect,\n  ElementRef,\n  inject,\n  Injector,\n  input,\n  numberAttribute,\n  type OnDestroy,\n  type OnInit,\n  output,\n  PLATFORM_ID,\n  Renderer2,\n  runInInjectionContext,\n  signal,\n  type TemplateRef,\n  viewChild,\n} from '@angular/core';\nimport { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';\n\nimport { filter, map, of, Subject, switchMap, tap, timer } from 'rxjs';\n\nimport { TOOLTIP_POSITIONS_MAP } from '@/shared/components/tooltip/tooltip-positions';\nimport {\n  tooltipPositionVariants,\n  tooltipVariants,\n  type ZardTooltipPositionVariants,\n} from '@/shared/components/tooltip/tooltip.variants';\nimport { ZardIdDirective } from '@/shared/core';\nimport { ZardStringTemplateOutletDirective } from '@/shared/core/directives/string-template-outlet/string-template-outlet.directive';\nimport { mergeClasses } from '@/shared/utils/merge-classes';\n\nexport type ZardTooltipTriggers = 'click' | 'hover';\nexport type ZardTooltipType = string | TemplateRef<void> | null;\n\ninterface DelayConfig {\n  isShow: boolean;\n  delay: number;\n}\n\nconst throttle = (callback: () => void, wait: number) => {\n  let time = Date.now();\n  return function () {\n    if (time + wait - Date.now() < 0) {\n      callback();\n      time = Date.now();\n    }\n  };\n};\n\n@Directive({\n  selector: '[zTooltip]',\n  host: {\n    style: 'cursor: pointer',\n  },\n  exportAs: 'zTooltip',\n})\nexport class ZardTooltipDirective implements OnInit, OnDestroy {\n  private readonly destroyRef = inject(DestroyRef);\n  private readonly document = inject(DOCUMENT);\n  private readonly elementRef = inject(ElementRef<HTMLElement>);\n  private readonly injector = inject(Injector);\n  private readonly overlay = inject(Overlay);\n  private readonly overlayPositionBuilder = inject(OverlayPositionBuilder);\n  private readonly platformId = inject(PLATFORM_ID);\n  private readonly renderer = inject(Renderer2);\n\n  private ariaEffectRef?: ReturnType<typeof effect>;\n  private componentRef?: ComponentRef<ZardTooltipComponent>;\n  private delaySubject?: Subject<DelayConfig>;\n  private listenersRefs: (() => void)[] = [];\n  private overlayRef?: OverlayRef;\n\n  readonly zHideDelay = input(100, { transform: numberAttribute });\n  readonly zPosition = input<ZardTooltipPositionVariants>('top');\n  readonly zPositionOffset = input(4);\n  readonly zShowDelay = input(150, { transform: numberAttribute });\n  readonly zTrigger = input<ZardTooltipTriggers>('hover');\n  readonly zTooltip = input<ZardTooltipType>(null);\n\n  readonly zHide = output<void>();\n  readonly zShow = output<void>();\n\n  private readonly tooltipText = computed<string | TemplateRef<void>>(() => {\n    let tooltipText = this.zTooltip();\n    if (!tooltipText) {\n      return '';\n    } else if (typeof tooltipText === 'string') {\n      tooltipText = tooltipText.trim();\n    }\n    return tooltipText;\n  });\n\n  ngOnInit() {\n    if (isPlatformBrowser(this.platformId)) {\n      const position = TOOLTIP_POSITIONS_MAP[this.zPosition()];\n      const positionOffset = this.zPositionOffset();\n      if (this.zPosition() === 'top' || this.zPosition() === 'bottom') {\n        position.offsetY = this.zPosition() === 'top' ? -positionOffset : positionOffset;\n      } else {\n        position.offsetX = this.zPosition() === 'left' ? -positionOffset : positionOffset;\n      }\n      const positionStrategy = this.overlayPositionBuilder\n        .flexibleConnectedTo(this.elementRef)\n        .withPositions([position]);\n      this.overlayRef = this.overlay.create({ positionStrategy });\n\n      runInInjectionContext(this.injector, () => {\n        toObservable(this.zTrigger)\n          .pipe(\n            tap(() => {\n              this.setupDelayMechanism();\n              this.cleanupTriggerEvents();\n              this.initTriggers();\n            }),\n            filter(() => !!this.overlayRef),\n            switchMap(() => (this.overlayRef as OverlayRef).outsidePointerEvents()),\n            filter(event => !this.elementRef.nativeElement.contains(event.target)),\n            takeUntilDestroyed(this.destroyRef),\n          )\n          .subscribe(() => this.delay(false, 0));\n      });\n    }\n  }\n\n  ngOnDestroy(): void {\n    // Clean up any pending effect\n    if (this.ariaEffectRef) {\n      this.ariaEffectRef.destroy();\n      this.ariaEffectRef = undefined;\n    }\n\n    this.delaySubject?.complete();\n    this.cleanupTriggerEvents();\n    this.overlayRef?.dispose();\n  }\n\n  private initTriggers() {\n    this.initScrollListener();\n    this.initClickListeners();\n    this.initHoverListeners();\n  }\n\n  private initClickListeners(): void {\n    if (this.zTrigger() !== 'click') {\n      return;\n    }\n\n    this.listenersRefs = [\n      ...this.listenersRefs,\n      this.renderer.listen(this.elementRef.nativeElement, 'click', () => {\n        const shouldShowTooltip = !this.overlayRef?.hasAttached();\n        const delay = shouldShowTooltip ? this.zShowDelay() : this.zHideDelay();\n        this.delay(shouldShowTooltip, delay);\n      }),\n    ];\n  }\n\n  private initHoverListeners(): void {\n    if (this.zTrigger() !== 'hover') {\n      return;\n    }\n\n    this.listenersRefs = [\n      ...this.listenersRefs,\n      this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', () => this.delay(true, this.zShowDelay())),\n      this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', () => this.delay(false, this.zHideDelay())),\n      this.renderer.listen(this.elementRef.nativeElement, 'focus', () => this.delay(true, this.zShowDelay())),\n      this.renderer.listen(this.elementRef.nativeElement, 'blur', () => this.delay(false, this.zHideDelay())),\n    ];\n  }\n\n  private initScrollListener(): void {\n    this.listenersRefs = [\n      ...this.listenersRefs,\n      this.renderer.listen(\n        this.document.defaultView,\n        'scroll',\n        throttle(() => this.delay(false, 0), 100),\n      ),\n    ];\n  }\n\n  private cleanupTriggerEvents(): void {\n    for (const eventRef of this.listenersRefs) {\n      eventRef();\n    }\n    this.listenersRefs = [];\n  }\n\n  private delay(isShow: boolean, delay = -1): void {\n    this.delaySubject?.next({ isShow, delay });\n  }\n\n  private setupDelayMechanism(): void {\n    this.delaySubject?.complete();\n    this.delaySubject = new Subject<DelayConfig>();\n\n    this.delaySubject\n      .pipe(\n        switchMap(config => (config.delay < 0 ? of(config) : timer(config.delay).pipe(map(() => config)))),\n        takeUntilDestroyed(this.destroyRef),\n      )\n      .subscribe(config => {\n        if (config.isShow) {\n          this.show();\n        } else {\n          this.hide();\n        }\n      });\n  }\n\n  private show() {\n    if (this.componentRef || !this.tooltipText()) {\n      return;\n    }\n\n    const tooltipPortal = new ComponentPortal(ZardTooltipComponent);\n    this.componentRef = this.overlayRef?.attach(tooltipPortal);\n    this.componentRef?.onDestroy(() => {\n      this.componentRef = undefined;\n    });\n    this.componentRef?.instance.state.set('opened');\n    this.componentRef?.instance.setProps(this.tooltipText(), this.zPosition());\n    runInInjectionContext(this.injector, () => {\n      this.ariaEffectRef = effect(() => {\n        const tooltipId = this.componentRef?.instance.uniqueId()?.id();\n        if (tooltipId) {\n          this.renderer.setAttribute(this.elementRef.nativeElement, 'aria-describedby', tooltipId);\n          this.ariaEffectRef?.destroy();\n          this.ariaEffectRef = undefined;\n        }\n      });\n    });\n    this.zShow.emit();\n  }\n\n  private hide() {\n    if (!this.componentRef) {\n      return;\n    }\n\n    // Clean up any pending effect\n    if (this.ariaEffectRef) {\n      this.ariaEffectRef.destroy();\n      this.ariaEffectRef = undefined;\n    }\n\n    this.renderer.removeAttribute(this.elementRef.nativeElement, 'aria-describedby');\n    this.componentRef.instance.state.set('closed');\n    this.zHide.emit();\n    this.overlayRef?.detach();\n  }\n}\n\n@Component({\n  selector: 'z-tooltip',\n  imports: [ZardStringTemplateOutletDirective, ZardIdDirective],\n  template: `\n    <ng-container *zStringTemplateOutlet=\"tooltipText()\" zardId=\"tooltip\" #z=\"zardId\">{{ tooltipText() }}</ng-container>\n\n    <span [class]=\"arrowClasses()\">\n      <svg\n        class=\"bg-foreground fill-foreground z-50 block size-2.5 translate-y-[calc(-50%-2px)] 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\" />\n      </svg>\n    </span>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  host: {\n    '[class]': 'classes()',\n    '[attr.id]': 'tooltipId()',\n    '[attr.data-side]': 'position()',\n    '[attr.data-state]': 'state()',\n    'data-slot': 'tooltip-content',\n    role: 'tooltip',\n  },\n})\nexport class ZardTooltipComponent {\n  protected readonly arrowClasses = computed(() =>\n    mergeClasses(tooltipPositionVariants({ position: this.position() })),\n  );\n\n  protected readonly classes = computed(() => mergeClasses(tooltipVariants()));\n  protected readonly position = signal<ZardTooltipPositionVariants>('top');\n  readonly state = signal<'closed' | 'opened'>('closed');\n  readonly uniqueId = viewChild<ZardIdDirective>('z');\n  protected readonly tooltipText = signal<ZardTooltipType>(null);\n  protected readonly tooltipId = computed(() => this.uniqueId()?.id() ?? 'tooltip');\n\n  setProps(tooltipText: ZardTooltipType, position: ZardTooltipPositionVariants) {\n    if (tooltipText) {\n      this.tooltipText.set(tooltipText);\n    }\n    this.position.set(position);\n  }\n}\n"
    },
    {
      "name": "tooltip-positions.ts",
      "content": "import type { ConnectedPosition } from '@angular/cdk/overlay';\n\nexport const TOOLTIP_POSITIONS_MAP: { [key: string]: ConnectedPosition } = {\n  top: {\n    originX: 'center',\n    originY: 'top',\n    overlayX: 'center',\n    overlayY: 'bottom',\n    offsetY: -4,\n  },\n  bottom: {\n    originX: 'center',\n    originY: 'bottom',\n    overlayX: 'center',\n    overlayY: 'top',\n    offsetY: 4,\n  },\n  left: {\n    originX: 'start',\n    originY: 'center',\n    overlayX: 'end',\n    overlayY: 'center',\n    offsetX: -4,\n  },\n  right: {\n    originX: 'end',\n    originY: 'center',\n    overlayX: 'start',\n    overlayY: 'center',\n    offsetX: 4,\n  },\n};\n"
    },
    {
      "name": "tooltip.variants.ts",
      "content": "import { cva, type VariantProps } from 'class-variance-authority';\n\nexport const tooltipVariants = cva(\n  'z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\n);\nexport type ZardTooltipVariants = VariantProps<typeof tooltipVariants>;\n\nexport const tooltipPositionVariants = cva('absolute', {\n  variants: {\n    position: {\n      top: 'bottom-0 translate-y-full left-[calc(50%-5px)]',\n      bottom: '-top-2.5 translate-y-0 rotate-180 left-[calc(50%-5px)]',\n      left: 'top-[calc(50%-5px)] rotate-270 translate-y-0 -right-2.5',\n      right: 'top-[calc(50%-5px)] translate-y-0 rotate-90 -left-2.5',\n    },\n  },\n});\n\nexport type ZardTooltipPositionVariants = NonNullable<VariantProps<typeof tooltipPositionVariants>['position']>;\n"
    },
    {
      "name": "tooltip.imports.ts",
      "content": "import { OverlayModule } from '@angular/cdk/overlay';\n\nimport { ZardTooltipComponent, ZardTooltipDirective } from '@/shared/components/tooltip/tooltip';\n\nexport const ZardTooltipImports = [ZardTooltipComponent, ZardTooltipDirective, OverlayModule] as const;\n"
    },
    {
      "name": "index.ts",
      "content": "export * from './tooltip';\nexport * from './tooltip.variants';\nexport * from './tooltip-positions';\nexport * from './tooltip.imports';\n"
    }
  ],
  "demos": [
    {
      "name": "click.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardTooltipImports } from '@/shared/components/tooltip/tooltip.imports';\n\n@Component({\n  selector: 'z-demo-tooltip-click',\n  imports: [ZardButtonComponent, ZardTooltipImports],\n  template: `\n    <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" zTrigger=\"click\">Click</button>\n  `,\n})\nexport class ZardDemoTooltipClickComponent {}\n"
    },
    {
      "name": "disabled.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardTooltipImports } from '@/shared/components/tooltip/tooltip.imports';\n\n@Component({\n  selector: 'z-demo-tooltip-disabled-button',\n  imports: [ZardButtonComponent, ZardTooltipImports],\n  template: `\n    <span zTooltip=\"This feature is currently unavailable\" class=\"block\" tabindex=\"0\">\n      <button type=\"button\" z-button zType=\"outline\" [zDisabled]=\"true\">Disabled</button>\n    </span>\n  `,\n})\nexport class ZardDemoTooltipDisabledButtonComponent {}\n"
    },
    {
      "name": "events.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardTooltipImports } from '@/shared/components/tooltip/tooltip.imports';\n\n@Component({\n  selector: 'z-demo-tooltip-events',\n  imports: [ZardButtonComponent, ZardTooltipImports],\n  template: `\n    <div class=\"flex w-25 flex-col gap-4\">\n      <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" (zShow)=\"onShow()\" (zHide)=\"onHide()\">\n        Events\n      </button>\n\n      <span class=\"text-sm\">Event: {{ event }}</span>\n    </div>\n  `,\n})\nexport class ZardDemoTooltipEventsComponent {\n  protected event = 'none';\n\n  protected onShow() {\n    this.event = '(zShow)';\n  }\n\n  protected onHide() {\n    this.event = '(zHide)';\n  }\n}\n"
    },
    {
      "name": "hover.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardTooltipImports } from '@/shared/components/tooltip/tooltip.imports';\n\n@Component({\n  selector: 'z-demo-tooltip-hover',\n  imports: [ZardButtonComponent, ZardTooltipImports],\n  template: `\n    <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\">Hover</button>\n  `,\n})\nexport class ZardDemoTooltipHoverComponent {}\n"
    },
    {
      "name": "position.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardTooltipImports } from '@/shared/components/tooltip/tooltip.imports';\n\n@Component({\n  selector: 'z-demo-tooltip-position',\n  imports: [ZardButtonComponent, ZardTooltipImports],\n  template: `\n    <div class=\"flex flex-col space-y-2\">\n      <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" zPosition=\"top\">Top</button>\n\n      <div class=\"flex space-x-2\">\n        <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" zPosition=\"left\">Left</button>\n        <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" zPosition=\"right\">Right</button>\n      </div>\n\n      <button type=\"button\" z-button zType=\"outline\" zTooltip=\"Tooltip content\" zPosition=\"bottom\">Bottom</button>\n    </div>\n  `,\n})\nexport class ZardDemoTooltipPositionComponent {}\n"
    },
    {
      "name": "with-kbd.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { lucideSave } from '@ng-icons/lucide';\n\nimport { ZardButtonComponent } from '@/shared/components/button';\nimport { ZardKbdComponent } from '@/shared/components/kbd';\nimport { ZardTooltipDirective } from '@/shared/components/tooltip/tooltip';\n\n@Component({\n  selector: 'z-demo-kbd-tooltip',\n  imports: [NgIcon, ZardButtonComponent, ZardTooltipDirective, ZardKbdComponent],\n  template: `\n    <button type=\"button\" z-button [zTooltip]=\"shortcutTip\" zType=\"outline\" zSize=\"icon-sm\">\n      <ng-icon name=\"lucideSave\" />\n    </button>\n\n    <ng-template #shortcutTip>\n      Save changes\n      <z-kbd>S</z-kbd>\n    </ng-template>\n  `,\n  viewProviders: [provideIcons({ lucideSave })],\n})\nexport class ZardDemoTooltipWithKbdComponent {}\n"
    }
  ]
}
