{"name":"avatar","type":"registry:component","title":"Avatar","description":"An image element with a fallback for representing the user. Built with Lit and styled using Tailwind CSS.","categories":["ui","avatar","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","dependencies":[],"files":[{"path":"registry/ui/avatar/avatar.ts","type":"registry:ui","content":"import { cva } from \"class-variance-authority\";\nimport { html, LitElement, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { TW } from \"@/registry/lib/tailwindMixin\";\nimport { cn } from \"@/registry/lib/utils\";\n\nexport const avatarVariants = cva(\n  \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n);\n\nexport interface AvatarProperties {\n  src?: string;\n  alt?: string;\n  loading?: \"eager\" | \"lazy\";\n}\n\n@customElement(\"ui-avatar\")\nexport class Avatar extends TW(LitElement) implements AvatarProperties {\n  @property({ type: String }) src?: string;\n  @property({ type: String }) alt = \"\";\n  @property({ type: String }) loading: \"eager\" | \"lazy\" = \"lazy\";\n\n  @state() private _imageLoaded = false;\n  @state() private _imageError = false;\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (changedProperties.has(\"src\")) {\n      this._imageLoaded = false;\n      this._imageError = false;\n    }\n  }\n\n  private _handleImageLoad() {\n    this._imageLoaded = true;\n    this._imageError = false;\n  }\n\n  private _handleImageError() {\n    this._imageLoaded = false;\n    this._imageError = true;\n  }\n\n  private get _shouldShowImage(): boolean {\n    return Boolean(this.src && this._imageLoaded && !this._imageError);\n  }\n\n  private get _shouldShowFallback(): boolean {\n    return !this.src || this._imageError || !this._imageLoaded;\n  }\n\n  override render() {\n    const hasImageSlot = this.querySelector('[slot=\"image\"]');\n\n    return html`\n      <span class=${cn(avatarVariants(), this.className)}>\n        <slot name=\"image\"></slot>\n\n        ${\n          this.src && !hasImageSlot\n            ? html`\n              <img\n                class=\"aspect-square h-full w-full object-cover\"\n                src=${this.src}\n                alt=${this.alt}\n                loading=${this.loading}\n                @load=${this._handleImageLoad}\n                @error=${this._handleImageError}\n                style=${this._shouldShowImage ? \"\" : \"display: none;\"}\n              />\n            `\n            : \"\"\n        }\n        ${\n          this._shouldShowFallback\n            ? html`\n              <span\n                class=\"flex h-full w-full items-center justify-center rounded-full bg-muted\"\n              >\n                <slot></slot>\n              </span>\n            `\n            : \"\"\n        }\n      </span>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    \"ui-avatar\": Avatar;\n  }\n}\n"},{"path":"registry/ui/avatar/avatar.stories.ts","type":"registry:ui","content":"import \"./avatar\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\nimport type { AvatarProperties } from \"./avatar\";\n\ntype AvatarArgs = AvatarProperties & { fallback?: string };\n\nconst meta: Meta<AvatarArgs> = {\n  title: \"ui/Avatar\",\n  component: \"ui-avatar\",\n  tags: [\"autodocs\"],\n  argTypes: {\n    src: {\n      control: \"text\",\n      description: \"Image source URL\",\n    },\n    alt: {\n      control: \"text\",\n      description: \"Image alt text for accessibility\",\n    },\n    loading: {\n      control: \"select\",\n      options: [\"eager\", \"lazy\"],\n      description: \"Image loading strategy\",\n    },\n    fallback: {\n      control: \"text\",\n      description: \"Fallback content (e.g., initials)\",\n    },\n  },\n  args: {\n    src: \"https://github.com/shadcn.png\",\n    alt: \"@shadcn\",\n    loading: \"lazy\",\n    fallback: \"CN\",\n  },\n  parameters: {\n    layout: \"centered\",\n  },\n  render: (args) => html`\n    <ui-avatar\n      src=${args.src || \"\"}\n      alt=${args.alt || \"\"}\n      loading=${args.loading || \"lazy\"}\n    >\n      ${args.fallback || \"\"}\n    </ui-avatar>\n  `,\n};\n\nexport default meta;\ntype Story = StoryObj<AvatarArgs>;\n\nexport const Default: Story = {};\n\nexport const FallbackOnly: Story = {\n  args: {\n    src: \"\",\n    fallback: \"JD\",\n  },\n};\n\nexport const WithBrokenImage: Story = {\n  args: {\n    src: \"https://invalid-url-that-will-fail.example.com/image.jpg\",\n    fallback: \"AB\",\n  },\n};\n\nexport const CustomSize: Story = {\n  render: (args) => html`\n    <div class=\"flex items-center gap-4\">\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"} class=\"size-8\">\n        ${args.fallback}\n      </ui-avatar>\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"}>\n        ${args.fallback}\n      </ui-avatar>\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"} class=\"size-16\">\n        ${args.fallback}\n      </ui-avatar>\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"} class=\"size-24\">\n        ${args.fallback}\n      </ui-avatar>\n    </div>\n  `,\n};\n\nexport const CustomShape: Story = {\n  render: (args) => html`\n    <div class=\"flex items-center gap-4\">\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"}>\n        ${args.fallback}\n      </ui-avatar>\n      <ui-avatar src=${args.src || \"\"} alt=${args.alt || \"\"} class=\"rounded-lg\">\n        ${args.fallback}\n      </ui-avatar>\n      <ui-avatar\n        src=${args.src || \"\"}\n        alt=${args.alt || \"\"}\n        class=\"rounded-none\"\n      >\n        ${args.fallback}\n      </ui-avatar>\n    </div>\n  `,\n};\n\nexport const StackedAvatars: Story = {\n  render: () => html`\n    <div class=\"flex -space-x-2\">\n      <ui-avatar\n        class=\"ring-2 ring-background\"\n        src=\"https://github.com/shadcn.png\"\n        alt=\"User 1\"\n      >\n        U1\n      </ui-avatar>\n      <ui-avatar\n        class=\"ring-2 ring-background\"\n        src=\"https://github.com/vercel.png\"\n        alt=\"User 2\"\n      >\n        U2\n      </ui-avatar>\n      <ui-avatar\n        class=\"ring-2 ring-background\"\n        src=\"https://github.com/react.png\"\n        alt=\"User 3\"\n      >\n        U3\n      </ui-avatar>\n      <ui-avatar class=\"ring-2 ring-background\" alt=\"User 4\"> +2 </ui-avatar>\n    </div>\n  `,\n};\n\nexport const WithImageSlot: Story = {\n  render: (args) => html`\n    <ui-avatar alt=${args.alt || \"\"}>\n      <img slot=\"image\" src=${args.src || \"\"} alt=${args.alt || \"\"} />\n      ${args.fallback}\n    </ui-avatar>\n  `,\n};\n"}]}