{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "type": "registry:ui",
  "title": "Avatar",
  "description": "A compact identity marker for people and accounts.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/avatar/avatar.tsx",
      "type": "registry:component",
      "target": "@components/xzero/avatar/avatar.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type HTMLAttributes } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { XzeroImage } from \"../lib/xzero-provider\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./avatar.module.css\";\nexport interface AvatarProps extends HTMLAttributes<HTMLSpanElement> { name: string; src?: string; size?: \"sm\" | \"md\" | \"lg\" | \"xl\"; status?: \"online\" | \"offline\" }\nexport function Avatar({ name, src, size = \"md\", status, className, ...props }: AvatarProps) {\n  const initials = name.trim().split(/\\s+/).slice(0, 2).map(part => part[0]?.toUpperCase()).join(\"\");\n  const reduceMotion = !!useReducedMotion();\n  const image = useRef<HTMLImageElement>(null);\n  const [failedSrc, setFailedSrc] = useState<string>();\n  // A photo that is already decoded shows at once. One that is still loading waits, then fades in from a soft blur.\n  useLayoutEffect(() => { const node = image.current; if (node && !node.complete) node.dataset.loading = \"\"; }, [src]);\n  const showImage = src && failedSrc !== src;\n  return <span {...props} className={[styles.avatar, styles[size], className].filter(Boolean).join(\" \")} role=\"img\" aria-label={`${name}${status ? `, ${status}` : \"\"}`}>\n    {showImage ? <XzeroImage key={src} ref={image} src={src} alt=\"\" fill sizes={size === \"xl\" ? \"88px\" : size === \"lg\" ? \"48px\" : size === \"md\" ? \"36px\" : \"28px\"} onLoad={event => { delete event.currentTarget.dataset.loading; }} onError={() => setFailedSrc(src)} /> : <span className={src ? styles.fallback : undefined} aria-hidden=\"true\">{initials}</span>}\n    <AnimatePresence initial={false}>{status && <motion.i key={status} className={[styles.status, styles[status]].join(\" \")} aria-hidden=\"true\" initial={{ opacity: 0, scale: .6 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: .6, transition: { duration: reduceMotion ? 0 : motionTokens.duration.fast } }} transition={reduceMotion ? { duration: 0 } : motionTokens.spring.snappy} />}</AnimatePresence>\n  </span>;\n}\n"
    },
    {
      "path": "registry/components/avatar/avatar.module.css",
      "type": "registry:component",
      "target": "@components/xzero/avatar/avatar.module.css",
      "content": ".avatar {\n  position: relative;\n  display: inline-grid;\n  flex: 0 0 auto;\n  place-items: center;\n  overflow: visible;\n  border: 1px solid var(--border);\n  border-radius: var(--radius-pill);\n  background: var(--surface-muted);\n  color: var(--foreground);\n  font-size: var(--text-xs);\n  font-weight: 500;\n}\n\n.avatar img {\n  pointer-events: none;\n  border-radius: inherit;\n  object-fit: cover;\n  transition: opacity var(--duration-standard) var(--ease-enter), filter var(--duration-standard) var(--ease-enter);\n}\n\n/* Set only while a photo is still downloading after mount, so cached photos never fade. */\n.avatar img[data-loading] {\n  opacity: 0;\n  filter: blur(4px);\n  transition: none;\n}\n\n.fallback { animation: avatar-fallback-in var(--duration-standard) var(--ease-enter) both; }\n\n.sm { width: 28px; height: 28px; font-size: 10px; }\n.md { width: 36px; height: 36px; }\n.lg { width: 48px; height: 48px; font-size: var(--text-sm); }\n.xl { width: 88px; height: 88px; font-size: var(--text-xl); }\n\n.status {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  z-index: 2;\n  width: 10px;\n  height: 10px;\n  border: 2px solid var(--surface);\n  border-radius: var(--radius-pill);\n  box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 70%, transparent);\n}\n\n.sm .status { width: 8px; height: 8px; border-width: 1.5px; }\n.lg .status { width: 12px; height: 12px; }\n.xl .status { width: 14px; height: 14px; right: 4px; bottom: 4px; }\n.online { background: var(--success); }\n.offline { background: var(--text-muted); }\n\n@keyframes avatar-fallback-in { from { opacity: 0; } to { opacity: 1; } }\n\n@media (prefers-reduced-motion: reduce) {\n  .avatar img { transition: none; }\n  .avatar img[data-loading] { filter: none; }\n  .fallback { animation: none; }\n}\n"
    },
    {
      "path": "lib/xzero-provider.tsx",
      "type": "registry:lib",
      "target": "@components/xzero/lib/xzero-provider.tsx",
      "content": "\"use client\";\n\n/**\n * Lets an app swap the link and image elements Xzero components render (kuratlielia/arc-library#13).\n *\n * Without a provider, Xzero renders a plain `<a>` and `<img>`, so components work in any React app (Vite, Remix, Astro,\n * plain React). Wrap the app once to use your framework's or your own components instead:\n *\n *   import Link from \"next/link\";\n *   import Image from \"next/image\";\n *   import { XzeroProvider } from \"@/components/xzero/lib/xzero-provider\";\n *\n *   <XzeroProvider link={Link} image={Image}>{children}</XzeroProvider>\n *\n * Any component that accepts these props works, such as a locale-aware Link from next-intl or a React Router Link\n * wrapped to map `href` to `to`.\n */\nimport { createContext, createElement, useContext, useMemo, type AnchorHTMLAttributes, type ComponentType, type ImgHTMLAttributes, type ReactNode, type Ref } from \"react\";\n\n/** The props Xzero passes to a link: an href plus ordinary anchor attributes. */\nexport interface XzeroLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n  href: string;\n  ref?: Ref<HTMLAnchorElement>;\n}\n\n/**\n * The props Xzero passes to an image. A subset of next/image's props, so next/image can be passed as is: either `fill`\n * (cover the positioned parent) or `width` and `height`, plus `sizes` and `priority` as hints.\n */\nexport interface XzeroImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"alt\" | \"width\" | \"height\" | \"loading\" | \"srcSet\"> {\n  src: string;\n  alt: string;\n  width?: number;\n  height?: number;\n  /** Fills the nearest positioned parent, like next/image's `fill`. */\n  fill?: boolean;\n  /** Loads the image eagerly with high priority, like next/image's `priority`. */\n  priority?: boolean;\n  loading?: \"eager\" | \"lazy\";\n  ref?: Ref<HTMLImageElement>;\n}\n\nexport type XzeroLinkComponent = ComponentType<XzeroLinkProps>;\nexport type XzeroImageComponent = ComponentType<XzeroImageProps>;\n\n/** The default link: a plain anchor. */\nexport function XzeroAnchor(props: XzeroLinkProps) {\n  return <a {...props} />;\n}\n\nconst fillStyle = { position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" } as const;\n\n/** The default image: a plain, lazily decoded `<img>` that understands `fill` and `priority`. */\nexport function XzeroImg({ alt, fill, priority, loading, style, ...props }: XzeroImageProps) {\n  // eslint-disable-next-line @next/next/no-img-element -- the framework-free default; pass next/image through XzeroProvider instead.\n  return <img alt={alt} decoding=\"async\" {...props} loading={loading ?? (priority ? \"eager\" : \"lazy\")} fetchPriority={priority ? \"high\" : props.fetchPriority} style={fill ? { ...fillStyle, ...style } : style} />;\n}\n\ninterface XzeroComponents {\n  link: XzeroLinkComponent;\n  image: XzeroImageComponent;\n}\n\nconst XzeroContext = createContext<XzeroComponents>({ link: XzeroAnchor, image: XzeroImg });\n\nexport interface XzeroProviderProps {\n  /** The link component Xzero renders for navigation, such as next/link or your own wrapped Link. Defaults to `<a>`. */\n  link?: XzeroLinkComponent;\n  /** The image component Xzero renders for photos, such as next/image. Defaults to `<img>`. */\n  image?: XzeroImageComponent;\n  children: ReactNode;\n}\n\n/** Sets the link and image components for every Xzero component inside it. Nested providers inherit what they do not set. */\nexport function XzeroProvider({ link, image, children }: XzeroProviderProps) {\n  const parent = useContext(XzeroContext);\n  const value = useMemo(() => ({ link: link ?? parent.link, image: image ?? parent.image }), [link, image, parent]);\n  return <XzeroContext.Provider value={value}>{children}</XzeroContext.Provider>;\n}\n\n/** The link component from the nearest XzeroProvider, or a plain anchor. */\nexport const useArcLink = () => useContext(XzeroContext).link;\n/** The image component from the nearest XzeroProvider, or a plain img. */\nexport const useArcImage = () => useContext(XzeroContext).image;\n\n/** What Xzero components render for a link: the provider's link component, or a plain anchor. */\nexport function XzeroLink(props: XzeroLinkProps) {\n  return createElement(useArcLink(), props);\n}\n\n/** What Xzero components render for an image: the provider's image component, or a plain img. */\nexport function XzeroImage(props: XzeroImageProps) {\n  return createElement(useArcImage(), props);\n}\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/avatar",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "identity",
      "people"
    ],
    "docs": "https://ui.xzero.co.uk/components/avatar",
    "markdown": "https://ui.xzero.co.uk/components/avatar/markdown"
  }
}