{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "icon-transition",
  "title": "Icon Transition",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/icon-transition/icon-transition.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  TemplateRef,\n  ViewContainerRef,\n  ViewEncapsulation,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\nimport { cn } from '@/lib/utils'\n\n// Copied verbatim from the React icon-transition (injected there into <head>). Unscoped\n// (ViewEncapsulation.None) so it reaches the slots and the consumer's icons.\nconst STYLES = `\n.icon-transition .icon-transition-slot {\n  grid-area: 1 / 1;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n.icon-transition .icon-transition-enter-active,\n.icon-transition .icon-transition-leave-active {\n  transform-origin: center;\n}\n.icon-transition .icon-transition-enter-active {\n  animation: icon-transition-pop var(--it-duration, 240ms) cubic-bezier(0.34, 1.56, 0.64, 1) both;\n}\n.icon-transition .icon-transition-leave-active {\n  animation: icon-transition-fade calc(var(--it-duration, 240ms) * 0.66) ease-in both;\n}\n@keyframes icon-transition-pop {\n  0% {\n    opacity: 0;\n    transform: scale(0.5) rotate(-12deg);\n  }\n  60% {\n    opacity: 1;\n    transform: scale(1.18) rotate(2deg);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1) rotate(0deg);\n  }\n}\n@keyframes icon-transition-fade {\n  to {\n    opacity: 0;\n    transform: scale(0.85);\n  }\n}\n@media (prefers-reduced-motion: reduce) {\n  .icon-transition .icon-transition-enter-active,\n  .icon-transition .icon-transition-leave-active {\n    animation-duration: 0ms !important;\n  }\n}\n`\n\n/**\n * Stamps an icon template and styles its root element(s) the way React styles the icon\n * component it receives (`className={iconClass}` + aria-hidden), so consumers pass a bare svg.\n */\n@Directive({ selector: '[uiIconTransitionIcon]', standalone: true })\nexport class UiIconTransitionIconDirective implements OnChanges, OnDestroy {\n  @Input('uiIconTransitionIcon') template: TemplateRef<unknown> | null | undefined = null\n  @Input() iconClass?: string\n  private readonly vcr = inject(ViewContainerRef)\n\n  ngOnChanges(): void {\n    this.vcr.clear()\n    if (!this.template) return\n    const view = this.vcr.createEmbeddedView(this.template)\n    view.detectChanges()\n    for (const node of view.rootNodes) {\n      if (!(node instanceof Element)) continue\n      node.setAttribute('class', cn(node.getAttribute('class') ?? '', this.iconClass))\n      node.setAttribute('aria-hidden', 'true')\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\n/**\n * Self-contained icon-swap control (React `IconTransition`). Click runs an optional async\n * `action`, then flips from `defaultIcon` to `activeIcon` with a spring pop (the outgoing icon\n * fades out underneath). Auto-reverts after `resetAfter` ms; `resetAfter=0` keeps it active\n * until `reset()` is called. Pass `active` to drive it externally. Icons are templates:\n * `<ng-template #copy><svg …/></ng-template>` + `[defaultIcon]=\"copy\"`.\n *\n * Renders a real `<button type=\"button\">` (or a passive `<span>` with `as=\"span\"`); the\n * `<ui-icon-transition>` host itself is `display: contents`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-icon-transition',\n  standalone: true,\n  exportAs: 'uiIconTransition',\n  encapsulation: ViewEncapsulation.None,\n  styles: [STYLES],\n  imports: [UiRenderTemplateDirective, UiIconTransitionIconDirective],\n  host: { class: 'contents' },\n  template: `\n    <ng-template #slots>\n      @if (leaving() !== null && leaving() !== isActive) {\n        <span class=\"icon-transition-slot icon-transition-leave-active\">\n          <ng-container [uiIconTransitionIcon]=\"leaving() ? activeIcon : defaultIcon\" [iconClass]=\"iconClass\" />\n        </span>\n      }\n      @if (isActive) {\n        <span class=\"icon-transition-slot icon-transition-enter-active\">\n          <ng-container [uiIconTransitionIcon]=\"activeIcon\" [iconClass]=\"iconClass\" />\n        </span>\n      } @else {\n        <span class=\"icon-transition-slot icon-transition-enter-active\">\n          <ng-container [uiIconTransitionIcon]=\"defaultIcon\" [iconClass]=\"iconClass\" />\n        </span>\n      }\n    </ng-template>\n    @if (tag === 'button') {\n      <button\n        data-uipkge=\"\"\n        data-slot=\"icon-transition\"\n        type=\"button\"\n        [attr.aria-label]=\"ariaLabel\"\n        [attr.aria-live]=\"isActive ? 'polite' : null\"\n        [class]=\"rootClass\"\n        [style.--it-duration]=\"duration + 'ms'\"\n        (click)=\"trigger()\"\n      >\n        <ng-container [uiRenderTemplate]=\"slots\" />\n      </button>\n    } @else {\n      <span\n        data-uipkge=\"\"\n        data-slot=\"icon-transition\"\n        [attr.aria-label]=\"ariaLabel\"\n        [attr.aria-live]=\"isActive ? 'polite' : null\"\n        [class]=\"rootClass\"\n        [style.--it-duration]=\"duration + 'ms'\"\n      >\n        <ng-container [uiRenderTemplate]=\"slots\" />\n      </span>\n    }\n  `,\n})\nexport class UiIconTransitionComponent implements OnChanges, OnDestroy {\n  @Input() defaultIcon?: TemplateRef<unknown> | null\n  @Input() activeIcon?: TemplateRef<unknown> | null\n  /** Tailwind size/color applied to the icons. */\n  @Input() iconClass = 'size-4'\n  /** Async work executed on click before the icon flips. Return `false` to skip the flip. */\n  @Input() action?: () => boolean | void | Promise<boolean | void>\n  /** ms before reverting to defaultIcon. Set to `0` (or null) to stay active. */\n  @Input() resetAfter: number | null = 1500\n  /** Pop animation duration in ms (also scales the leave fade). */\n  @Input() duration = 240\n  /** aria-label shown in default state. */\n  @Input() label?: string\n  /** aria-label shown after activation; falls back to `label`. */\n  @Input() activeLabel?: string\n  /** Tailwind class applied while active (e.g. \"text-success\"). */\n  @Input() activeClass = 'text-success'\n  /** Render element. `button` adds click handler + focus styling; `span` is purely visual. */\n  @Input('as') tag: 'button' | 'span' = 'button'\n  /** External control. When provided, this input wins over internal state. */\n  @Input() active?: boolean\n  @Input('class') className?: string\n\n  @Output() activate = new EventEmitter<void>()\n  /** React `onReset`. (The class member `reset()` is the imperative handle.) */\n  @Output('reset') resetEvent = new EventEmitter<void>()\n\n  // Signals: flipped from timers and awaited actions, outside any template event.\n  private readonly internalActive = signal(false)\n  /** The outgoing icon's state while its leave animation runs (React `prevActive`). */\n  readonly leaving = signal<boolean | null>(null)\n  private readonly activeInput = signal<boolean | undefined>(undefined)\n  private lastActive = false\n  private initialized = false\n  private resetTimer?: ReturnType<typeof setTimeout>\n  private leaveTimer?: ReturnType<typeof setTimeout>\n\n  get isActive(): boolean {\n    const external = this.activeInput()\n    return external !== undefined ? external : this.internalActive()\n  }\n\n  get ariaLabel(): string | null {\n    return (this.isActive ? (this.activeLabel ?? this.label) : this.label) ?? null\n  }\n\n  get rootClass(): string {\n    return [\n      'icon-transition focus-visible:ring-ring inline-grid place-items-center transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      this.isActive ? this.activeClass : '',\n      this.className ?? '',\n    ]\n      .filter(Boolean)\n      .join(' ')\n  }\n\n  ngOnChanges(): void {\n    this.activeInput.set(this.active)\n    if (this.initialized) this.syncLeaving()\n    else this.lastActive = this.isActive\n    this.initialized = true\n  }\n\n  ngOnDestroy(): void {\n    this.clearTimer()\n    clearTimeout(this.leaveTimer)\n  }\n\n  async trigger(): Promise<void> {\n    if (this.action) {\n      const result = await this.action()\n      if (result === false) return\n    }\n    this.internalActive.set(true)\n    this.syncLeaving()\n    this.activate.emit()\n    this.clearTimer()\n    if (this.resetAfter && this.resetAfter > 0) this.resetTimer = setTimeout(() => this.reset(), this.resetAfter)\n  }\n\n  reset(): void {\n    this.internalActive.set(false)\n    this.syncLeaving()\n    this.resetEvent.emit()\n    this.clearTimer()\n  }\n\n  /** Keep the outgoing icon mounted for `duration` ms after a flip, for the cross-fade. */\n  private syncLeaving(): void {\n    const current = this.isActive\n    if (current === this.lastActive) return\n    this.leaving.set(this.lastActive)\n    this.lastActive = current\n    clearTimeout(this.leaveTimer)\n    this.leaveTimer = setTimeout(() => this.leaving.set(null), this.duration)\n  }\n\n  private clearTimer(): void {\n    if (this.resetTimer) clearTimeout(this.resetTimer)\n    this.resetTimer = undefined\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/icon-transition/icon-transition.component.ts"
    },
    {
      "path": "packages/registry-angular/components/icon-transition/index.ts",
      "content": "export { UiIconTransitionComponent, UiIconTransitionIconDirective } from './icon-transition.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/icon-transition/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Animated icon swap for click acknowledgements — copy → check, bookmark → bookmark-filled, follow → follower. Pass two icon components and an async `action`; clicking runs the action then springs the active icon in. Auto-reverts after `resetAfter` ms (default 1500), or pass `0` to keep the active icon and call the exposed `reset()` method to flip it back. Renders as a `<button>` by default; use `as=\"span\"` plus the `:active` prop when you want an externally controlled, non-interactive icon swap inside another control. Honors `prefers-reduced-motion`.",
  "categories": [
    "feedback"
  ]
}