{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "type": "registry:ui",
  "description": "Round or rounded-square user image with a fallback that shows initials or an icon when the image is missing or fails to load. Sizes from xs to 2xl, optional status dot, and a group composition for stacked avatar lists.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/avatar/avatar.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { avatarVariants, avatarFallbackVariants, type AvatarVariants } from './avatar.variants'\n\ntype Size = 'xs' | 'sm' | 'default' | 'lg' | 'xl' | '2xl'\ntype Rounded = NonNullable<AvatarVariants['rounded']>\ntype Color = NonNullable<AvatarVariants['color']>\ntype Variant = 'default' | 'outlined' | 'soft'\nexport type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error'\n\n/**\n * <uip-avatar> — the registry Avatar root (Radix Avatar.Root).\n *\n * Children: <uip-avatar-image> and/or <uip-avatar-fallback>. As in Radix, the\n * image only renders once it has loaded; until then (or on error) the\n * fallback renders. The parts need no id references, so they are separate\n * elements coordinated through the root.\n *\n * Class strings are React's verbatim, on the inner `part=\"base\"` span. React's\n * `className` overrides → style the part from outside, e.g.\n * `class=\"[&::part(base)]:size-10 [&::part(base)]:ring-2\"`.\n */\nexport class UipAvatar extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: inline-flex; }`]\n\n  static properties = {\n    size: { reflect: true },\n    rounded: { reflect: true },\n    color: { reflect: true },\n    variant: { reflect: true },\n    tile: { type: Boolean, reflect: true },\n    disabled: { type: Boolean, reflect: true },\n    loading: { type: Boolean, reflect: true },\n  }\n\n  size?: Size\n  rounded?: Rounded\n  color?: Color\n  variant?: Variant\n  tile = false\n  disabled = false\n  loading = false\n\n  /** Loading status of the child <uip-avatar-image> (Radix context). */\n  imageStatus: ImageLoadingStatus = 'idle'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'avatar')\n  }\n\n  /** Called by <uip-avatar-image>; re-renders this avatar's fallbacks. */\n  setImageStatus(status: ImageLoadingStatus) {\n    this.imageStatus = status\n    this.querySelectorAll('uip-avatar-fallback').forEach((f) => {\n      if (f.closest('uip-avatar') === this) f.requestUpdate()\n    })\n  }\n\n  render() {\n    return html`<span\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"avatar\"\n      class=${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      )}\n      ><slot></slot\n    ></span>`\n  }\n}\n\n/**\n * <uip-avatar-image> — Radix Avatar.Image. Preloads `src`; the <img> renders\n * only once loaded.\n *\n * Events: `loading-status-change` (detail: { status: 'idle' | 'loading' |\n * 'loaded' | 'error' }) — React's `onLoadingStatusChange`.\n */\nexport class UipAvatarImage extends LitElement {\n  // display: contents so the <img> is laid out directly inside the avatar.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    src: {},\n    alt: {},\n    status: { state: true },\n  }\n\n  src?: string\n  alt = ''\n  private status: ImageLoadingStatus = 'idle'\n  private probe?: HTMLImageElement\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'avatar-image')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.probe = undefined\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('src')) this.load()\n  }\n\n  private load() {\n    if (isServer) return\n    if (!this.src) return this.setStatus('idle')\n    const img = new Image()\n    this.probe = img\n    const settle = (status: ImageLoadingStatus) => () => {\n      if (this.probe === img) this.setStatus(status)\n    }\n    this.setStatus('loading')\n    img.onload = settle('loaded')\n    img.onerror = settle('error')\n    img.src = this.src\n  }\n\n  private setStatus(status: ImageLoadingStatus) {\n    if (this.status === status) return\n    this.status = status\n    this.closest('uip-avatar')?.setImageStatus(status)\n    this.dispatchEvent(new CustomEvent('loading-status-change', { detail: { status }, bubbles: true, composed: true }))\n  }\n\n  render() {\n    if (this.status !== 'loaded') return nothing\n    return html`<img\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"avatar-image\"\n      class=\"aspect-square size-full object-cover\"\n      src=${this.src ?? nothing}\n      alt=${this.alt}\n    />`\n  }\n}\n\n/**\n * <uip-avatar-fallback> — Radix Avatar.Fallback. Renders while the sibling\n * image is not loaded (after `delay-ms`, Radix's `delayMs`). Content: the\n * `text` attribute, else the default slot.\n */\nexport class UipAvatarFallback extends LitElement {\n  // display: contents so the fallback span fills the avatar directly.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    size: { reflect: true },\n    color: { reflect: true },\n    text: {},\n    delayMs: { type: Number, attribute: 'delay-ms' },\n    delayed: { state: true },\n  }\n\n  size: Size = 'default'\n  color: Color = 'default'\n  text?: string\n  delayMs?: number\n  private delayed = false\n  private timer?: ReturnType<typeof setTimeout>\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'avatar-fallback')\n    if (!isServer && this.delayMs) {\n      this.delayed = true\n      this.timer = setTimeout(() => (this.delayed = false), this.delayMs)\n    }\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    clearTimeout(this.timer)\n  }\n\n  render() {\n    // The server DOM shim has no tree: render the fallback (Radix does too).\n    const status = isServer ? 'idle' : (this.closest('uip-avatar')?.imageStatus ?? 'idle')\n    if (this.delayed || status === 'loaded') return nothing\n    return html`<span\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"avatar-fallback\"\n      class=${avatarFallbackVariants({ size: this.size, color: this.color })}\n      >${this.text ?? html`<slot></slot>`}</span\n    >`\n  }\n}\n\nconst overflowSizeClasses: Record<Size, 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 * <uip-avatar-group> — the registry AvatarGroup (custom layout wrapper).\n *\n * Children are <uip-avatar>s. With `max`, only the first `max - 1` render\n * (manual slot assignment — the rest stay in the DOM, unrendered) plus a `+N`\n * chip. React's `overflow` render prop → an element with `slot=\"overflow\"`\n * replaces the chip; the hidden count is `.overflowCount` and the host's\n * `data-overflow-count`.\n *\n * Overlap: React's `-space-x-2` can't reach slotted children through the\n * <slot>, so the same negative margin is applied with `::slotted(…)`.\n */\nexport class UipAvatarGroup extends LitElement {\n  static shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, slotAssignment: 'manual' }\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    max: { type: Number },\n    overlap: { type: Boolean, converter: { fromAttribute: (v: string | null) => v !== 'false' } },\n    size: { reflect: true },\n    total: { type: Number },\n    items: { state: true },\n  }\n\n  max?: number\n  overlap = true\n  size: Size = 'default'\n  total?: number\n  private items: Element[] = []\n  private observer?: MutationObserver\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'avatar-group')\n    this.readItems()\n    if (isServer) return\n    this.observer = new MutationObserver(() => this.readItems())\n    this.observer.observe(this, { childList: true })\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.observer?.disconnect()\n  }\n\n  private readItems() {\n    this.items = [...this.children].filter((el) => el.slot !== 'overflow')\n  }\n\n  private get layout() {\n    const count = this.total ?? this.items.length\n    // When overflowing, reserve one slot for the +N chip so max includes the badge.\n    const showOverflow = this.max != null && count > this.max\n    const visibleLimit = showOverflow ? Math.max(this.max! - 1, 0) : this.items.length\n    return { showOverflow, visibleLimit, overflowCount: showOverflow ? count - visibleLimit : 0 }\n  }\n\n  /** Number of avatars hidden behind the overflow indicator. */\n  get overflowCount() {\n    return this.layout.overflowCount\n  }\n\n  protected updated() {\n    const { visibleLimit, overflowCount } = this.layout\n    this.setAttribute('data-overflow-count', String(overflowCount))\n    const root = this.renderRoot as ShadowRoot\n    root.querySelector<HTMLSlotElement>('slot:not([name])')?.assign(...this.items.slice(0, visibleLimit))\n    root\n      .querySelector<HTMLSlotElement>('slot[name=overflow]')\n      ?.assign(...[...this.children].filter((el) => el.slot === 'overflow'))\n  }\n\n  render() {\n    const { showOverflow, visibleLimit, overflowCount } = this.layout\n    return html`<div\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"avatar-group\"\n      class=${cn(\n        'flex items-center',\n        this.overlap ? '-space-x-2 [&::slotted(:not(:first-child))]:-ms-2' : 'gap-1',\n      )}\n    >\n      <slot></slot>\n      ${showOverflow\n        ? html`<div\n            class=${cn(\n              'bg-muted ring-background relative flex shrink-0 overflow-hidden rounded-full ring-2',\n              overflowSizeClasses[this.size],\n              this.overlap && visibleLimit > 0 ? '-ms-2' : '',\n            )}\n          >\n            <slot name=\"overflow\"\n              ><span class=\"flex size-full items-center justify-center font-medium\">+${overflowCount}</span></slot\n            >\n          </div>`\n        : nothing}\n    </div>`\n  }\n}\n\ncustomElements.get('uip-avatar') || customElements.define('uip-avatar', UipAvatar)\ncustomElements.get('uip-avatar-image') || customElements.define('uip-avatar-image', UipAvatarImage)\ncustomElements.get('uip-avatar-fallback') || customElements.define('uip-avatar-fallback', UipAvatarFallback)\ncustomElements.get('uip-avatar-group') || customElements.define('uip-avatar-group', UipAvatarGroup)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-avatar': UipAvatar\n    'uip-avatar-image': UipAvatarImage\n    'uip-avatar-fallback': UipAvatarFallback\n    'uip-avatar-group': UipAvatarGroup\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/avatar/avatar.ts"
    },
    {
      "path": "packages/elements-lit/src/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:file",
      "target": "src/uip/components/avatar/avatar.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
