{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/avatar/avatar.component.ts",
      "content": "import {\n  AfterContentInit,\n  Component,\n  DestroyRef,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  TemplateRef,\n  ViewContainerRef,\n  booleanAttribute,\n  computed,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  avatarVariants,\n  avatarFallbackVariants,\n  type AvatarVariants,\n  type AvatarFallbackVariants,\n} from './avatar.variants'\n\nexport type AvatarSize = NonNullable<AvatarVariants['size']>\nexport type AvatarRounded = NonNullable<AvatarVariants['rounded']>\nexport type AvatarColor = NonNullable<AvatarVariants['color']>\nexport type AvatarVariant = NonNullable<AvatarVariants['variant']>\n/** Radix `ImageLoadingStatus`. */\nexport type AvatarImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error'\n\n/**\n * Angular port of UIPKGE Avatar (React `Avatar` on Radix Avatar). Radix semantics: the root\n * holds the image loading status; `ui-avatar-image` preloads its `src` off-DOM and only\n * renders the `<img>` once it has loaded, while `ui-avatar-fallback` renders until then\n * (optionally after `delayMs`) and disappears once the image is shown. A broken or missing\n * src therefore shows just the fallback, never both side by side.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-avatar, [ui-avatar]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"avatar\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiAvatarComponent {\n  @Input() size?: AvatarSize\n  @Input() rounded?: AvatarRounded\n  @Input() color?: AvatarColor\n  @Input() variant?: AvatarVariant\n  @Input({ transform: booleanAttribute }) tile = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input('class') className?: string\n\n  /** Shared with the image / fallback parts (signal: set from load callbacks, zoneless-safe). */\n  readonly imageStatus = signal<AvatarImageLoadingStatus>('idle')\n\n  get hostClass(): string {\n    return cn(\n      avatarVariants({ size: this.size, rounded: this.rounded, color: this.color, variant: this.variant }),\n      this.tile ? 'rounded-none' : '',\n      this.disabled ? 'cursor-not-allowed opacity-50' : '',\n      this.loading ? 'animate-pulse' : '',\n      this.className,\n    )\n  }\n}\n\n/**\n * Radix `AvatarImage`: preloads `src` with `new Image()` (honouring `referrerPolicy` /\n * `crossOrigin`) and renders the `<img>` only when it loaded. Emits `loadingStatusChange`\n * (React `onLoadingStatusChange`) for every status after `idle`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-avatar-image, [ui-avatar-image]',\n  standalone: true,\n  host: { class: 'contents' },\n  template: `@if (status() === 'loaded') {\n    <img\n      [src]=\"src\"\n      [attr.alt]=\"alt ?? null\"\n      [attr.loading]=\"loading ?? null\"\n      [attr.referrerpolicy]=\"referrerPolicy ?? null\"\n      [attr.crossorigin]=\"crossOrigin ?? null\"\n      data-uipkge=\"\"\n      data-slot=\"avatar-image\"\n      [class]=\"imgClass\"\n    />\n  }`,\n})\nexport class UiAvatarImageComponent implements OnChanges, OnDestroy {\n  private readonly avatar = inject(UiAvatarComponent, { optional: true })\n  private readonly ownStatus = signal<AvatarImageLoadingStatus>('idle')\n  private probe?: HTMLImageElement\n\n  @Input() src?: string\n  @Input() alt?: string\n  @Input() loading?: 'eager' | 'lazy'\n  @Input() referrerPolicy?: ReferrerPolicy\n  @Input() crossOrigin?: 'anonymous' | 'use-credentials' | ''\n  @Input('class') className?: string\n  @Output() loadingStatusChange = new EventEmitter<AvatarImageLoadingStatus>()\n\n  readonly status = computed(() => (this.avatar ? this.avatar.imageStatus() : this.ownStatus()))\n\n  get imgClass(): string {\n    return cn('aspect-square size-full object-cover', this.className)\n  }\n\n  ngOnChanges(): void {\n    this.load()\n  }\n\n  ngOnDestroy(): void {\n    this.detach()\n    if (this.avatar) this.avatar.imageStatus.set('idle')\n  }\n\n  private setStatus(s: AvatarImageLoadingStatus): void {\n    if (this.avatar) this.avatar.imageStatus.set(s)\n    this.ownStatus.set(s)\n    if (s !== 'idle') this.loadingStatusChange.emit(s)\n  }\n\n  private detach(): void {\n    if (!this.probe) return\n    this.probe.onload = this.probe.onerror = null\n    this.probe = undefined\n  }\n\n  private load(): void {\n    this.detach()\n    if (!this.src || typeof window === 'undefined') {\n      this.setStatus('error')\n      return\n    }\n    const img = new window.Image()\n    this.probe = img\n    img.onload = () => this.probe === img && this.setStatus('loaded')\n    img.onerror = () => this.probe === img && this.setStatus('error')\n    if (this.referrerPolicy) img.referrerPolicy = this.referrerPolicy\n    if (this.crossOrigin !== undefined) img.crossOrigin = this.crossOrigin\n    this.setStatus('loading')\n    img.src = this.src\n    if (img.complete && img.naturalWidth > 0) this.setStatus('loaded')\n  }\n}\n\n/**\n * Radix `AvatarFallback`: rendered while the image is not loaded; with `delayMs` it waits\n * that long first (avoids a flash of initials for fast-loading images). `text` wins over\n * projected content, like React `text ?? children`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-avatar-fallback, [ui-avatar-fallback]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"avatar-fallback\"',\n    '[class]': 'hostClass',\n  },\n  template: `@if (visible()) {\n    @if (text !== undefined) {\n      {{ text }}\n    } @else {\n      <ng-content />\n    }\n  }`,\n})\nexport class UiAvatarFallbackComponent implements OnInit {\n  private readonly avatar = inject(UiAvatarComponent, { optional: true })\n  private readonly destroyRef = inject(DestroyRef)\n  private readonly canRender = signal(true)\n\n  @Input() size: NonNullable<AvatarFallbackVariants['size']> = 'default'\n  @Input() color: NonNullable<AvatarFallbackVariants['color']> = 'default'\n  @Input() text?: string\n  @Input() delayMs?: number\n  @Input('class') className?: string\n\n  readonly visible = computed(() => this.canRender() && (this.avatar?.imageStatus() ?? 'idle') !== 'loaded')\n\n  ngOnInit(): void {\n    if (this.delayMs === undefined) return\n    this.canRender.set(false)\n    const id = setTimeout(() => this.canRender.set(true), this.delayMs)\n    this.destroyRef.onDestroy(() => clearTimeout(id))\n  }\n\n  get hostClass(): string {\n    return this.visible()\n      ? cn(avatarFallbackVariants({ size: this.size, color: this.color }), this.className)\n      : 'hidden'\n  }\n}\n\n/** Renders an `overflow` template with the hidden count (React `overflow(count)`). */\n@Directive({ selector: '[uiAvatarOverflowOutlet]', standalone: true })\nexport class UiAvatarOverflowOutletDirective implements OnChanges, OnDestroy {\n  @Input('uiAvatarOverflowOutlet') template: TemplateRef<AvatarOverflowContext> | null = null\n  @Input() uiAvatarOverflowOutletCount = 0\n  private readonly vcr = inject(ViewContainerRef)\n  private readonly ctx: AvatarOverflowContext = { $implicit: 0, count: 0 }\n\n  ngOnChanges(): void {\n    this.ctx.$implicit = this.ctx.count = this.uiAvatarOverflowOutletCount\n    if (this.vcr.length === 0 && this.template) this.vcr.createEmbeddedView(this.template, this.ctx)\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\nexport interface AvatarOverflowContext {\n  $implicit: number\n  count: number\n}\n\nconst overflowSizeClasses: Record<AvatarSize, string> = {\n  xs: 'size-4 text-xs',\n  sm: 'size-6 text-xs',\n  default: 'size-8 text-sm',\n  lg: 'size-12 text-base',\n  xl: 'size-16 text-lg',\n  '2xl': 'size-20 text-xl',\n}\n\n/**\n * React `AvatarGroup` (custom layout, not Radix): overlaps its children and, when there are\n * more than `max`, shows only `max - 1` of them plus a `+N` chip (so `max` includes the chip).\n * Children are counted from the DOM (and re-counted when they change); `total` overrides the\n * count when only a subset is projected. `overflow` replaces the `+N` chip with a template\n * that receives the hidden count (`let-count`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-avatar-group, [ui-avatar-group]',\n  standalone: true,\n  imports: [UiAvatarOverflowOutletDirective],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"avatar-group\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />\n    @if (showOverflow()) {\n      <div data-avatar-group-overflow=\"\" [class]=\"overflowClass\">\n        @if (overflow) {\n          <ng-container [uiAvatarOverflowOutlet]=\"overflow\" [uiAvatarOverflowOutletCount]=\"overflowCount()\" />\n        } @else {\n          <span class=\"flex size-full items-center justify-center font-medium\">+{{ overflowCount() }}</span>\n        }\n      </div>\n    }`,\n})\nexport class UiAvatarGroupComponent implements AfterContentInit, OnChanges, OnDestroy {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  private observer?: MutationObserver\n  private readonly itemCount = signal(0)\n  private readonly inputs = signal<{ max?: number; total?: number }>({})\n\n  /** Max visible avatars including the +N chip when overflowing. */\n  @Input() max?: number\n  @Input({ transform: booleanAttribute }) overlap = true\n  @Input() size: AvatarSize = 'default'\n  /** Override the total used for +N when only a subset of avatars is projected. */\n  @Input() total?: number\n  @Input() overflow?: TemplateRef<AvatarOverflowContext>\n  @Input('class') className?: string\n\n  private readonly count = computed(() => this.inputs().total ?? this.itemCount())\n  readonly showOverflow = computed(() => {\n    const max = this.inputs().max\n    return max != null && this.count() > max\n  })\n  private readonly visibleLimit = computed(() =>\n    this.showOverflow() ? Math.max((this.inputs().max ?? 0) - 1, 0) : Number.POSITIVE_INFINITY,\n  )\n  readonly overflowCount = computed(() => (this.showOverflow() ? this.count() - this.visibleLimit() : 0))\n\n  get overflowClass(): string {\n    return cn(\n      'bg-muted ring-background relative flex shrink-0 overflow-hidden rounded-full ring-2',\n      overflowSizeClasses[this.size] ?? overflowSizeClasses.default,\n    )\n  }\n\n  get hostClass(): string {\n    return cn('flex items-center', this.overlap ? '-space-x-2' : 'gap-1', this.className)\n  }\n\n  ngOnChanges(): void {\n    this.inputs.set({ max: this.max, total: this.total })\n    this.apply()\n  }\n\n  ngAfterContentInit(): void {\n    this.apply()\n    if (typeof MutationObserver === 'undefined') return\n    this.observer = new MutationObserver(() => this.apply())\n    this.observer.observe(this.el, { childList: true })\n  }\n\n  ngOnDestroy(): void {\n    this.observer?.disconnect()\n  }\n\n  private items(): HTMLElement[] {\n    return [...this.el.children].filter(\n      (c): c is HTMLElement => c instanceof HTMLElement && !c.hasAttribute('data-avatar-group-overflow'),\n    )\n  }\n\n  /** Hide children beyond the visible limit (React simply does not render them). */\n  private apply(): void {\n    const items = this.items()\n    this.itemCount.set(items.length)\n    const limit = this.visibleLimit()\n    items.forEach((item, i) => {\n      const hide = i >= limit\n      item.toggleAttribute('hidden', hide)\n      // The `hidden` attribute's UA display:none loses to the avatar's `flex` utility, so hide inline too.\n      item.style.display = hide ? 'none' : ''\n    })\n  }\n}\n\nexport { avatarVariants, avatarFallbackVariants, type AvatarVariants, type AvatarFallbackVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/avatar/avatar.component.ts"
    },
    {
      "path": "packages/registry-angular/components/avatar/avatar.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts` or inline in the SFC) so `Avatar.vue` / `AvatarFallback.vue`\n * can `import { avatarVariants } from './avatar.variants'` without\n * creating a circular dependency back through the index. The circular\n * form caused intermittent `$setup.avatarVariants is not a function`\n * errors during dev SSR.\n */\nexport const avatarVariants = cva('relative flex shrink-0 overflow-hidden', {\n  variants: {\n    size: {\n      xs: 'size-4',\n      sm: 'size-6',\n      default: 'size-8',\n      lg: 'size-12',\n      xl: 'size-16',\n      '2xl': 'size-20',\n    },\n    rounded: {\n      none: 'rounded-none',\n      sm: 'rounded-sm',\n      default: 'rounded-full',\n      md: 'rounded-md',\n      lg: 'rounded-lg',\n      xl: 'rounded-xl',\n      '2xl': 'rounded-2xl',\n      '3xl': 'rounded-3xl',\n      full: 'rounded-full',\n    },\n    color: {\n      default: '',\n      primary: 'bg-primary text-primary-foreground',\n      secondary: 'bg-secondary text-secondary-foreground',\n      destructive: 'bg-destructive text-destructive-foreground',\n      success: 'bg-success text-white dark:text-black',\n      warning: 'bg-warning text-black',\n      info: 'bg-info text-white dark:text-black',\n      error: 'bg-destructive text-white dark:text-black',\n      muted: 'bg-muted text-muted-foreground',\n    },\n    variant: {\n      default: '',\n      outlined: 'border-2 border-current',\n      soft: 'bg-opacity-20',\n    },\n  },\n  compoundVariants: [\n    { color: 'primary', variant: 'soft', class: 'bg-primary/20 text-primary' },\n    { color: 'secondary', variant: 'soft', class: 'bg-secondary/20 text-secondary-foreground' },\n    { color: 'destructive', variant: 'soft', class: 'bg-destructive/20 text-destructive' },\n    { color: 'success', variant: 'soft', class: 'bg-success/20 text-success' },\n    { color: 'warning', variant: 'soft', class: 'bg-warning/20 text-warning' },\n    { color: 'info', variant: 'soft', class: 'bg-info/20 text-info' },\n    { color: 'error', variant: 'soft', class: 'bg-destructive/20 text-destructive' },\n  ],\n  defaultVariants: {\n    size: 'default',\n    rounded: 'default',\n    color: 'default',\n  },\n})\n\nexport type AvatarVariants = VariantProps<typeof avatarVariants>\n\nexport const avatarFallbackVariants = cva(\n  'flex size-full items-center justify-center rounded-full bg-muted font-medium',\n  {\n    variants: {\n      size: {\n        xs: 'text-xs',\n        sm: 'text-xs',\n        default: 'text-sm',\n        lg: 'text-base',\n        xl: 'text-lg',\n        '2xl': 'text-xl',\n      },\n      color: {\n        default: '',\n        primary: 'bg-primary text-primary-foreground',\n        secondary: 'bg-secondary text-secondary-foreground',\n        destructive: 'bg-destructive text-destructive-foreground',\n        success: 'bg-success text-white dark:text-black',\n        warning: 'bg-warning text-black',\n        info: 'bg-info text-white dark:text-black',\n        error: 'bg-destructive text-white dark:text-black',\n        muted: 'bg-muted text-muted-foreground',\n      },\n    },\n    defaultVariants: {\n      size: 'default',\n      color: 'default',\n    },\n  },\n)\n\nexport type AvatarFallbackVariants = VariantProps<typeof avatarFallbackVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/avatar/avatar.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/avatar/index.ts",
      "content": "export {\n  UiAvatarComponent,\n  UiAvatarFallbackComponent,\n  UiAvatarImageComponent,\n  UiAvatarGroupComponent,\n  UiAvatarOverflowOutletDirective,\n  avatarVariants,\n  avatarFallbackVariants,\n  type AvatarVariants,\n  type AvatarFallbackVariants,\n  type AvatarSize,\n  type AvatarRounded,\n  type AvatarColor,\n  type AvatarVariant,\n  type AvatarImageLoadingStatus,\n  type AvatarOverflowContext,\n} from './avatar.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/avatar/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "User avatar with Radix image loading: the image renders only once it has loaded, the fallback (optionally delayed) shows until then. AvatarGroup overlaps avatars and collapses extras into a +N chip.",
  "categories": [
    "data-display"
  ]
}