{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "description": "A user's picture with a fallback, a status badge, and groups that overlap.",
  "dependencies": [
    "@base-ui/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/avatar.tsx",
      "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { ComponentProps } from \"react\";\n\nimport type { Sx } from \"@/lib/constella/styles\";\nimport {\n  avatarVars,\n  badgeVars,\n  colors,\n  durations,\n  easings,\n  fontSizes,\n  fontWeights,\n  lineHeights,\n  radii,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\nconst avatarSizes = [\"sm\", \"default\", \"lg\"] as const;\nconst avatarCorners = [\"pill\", \"rounded\"] as const;\n\ntype AvatarSize = (typeof avatarSizes)[number];\ntype AvatarCorners = (typeof avatarCorners)[number];\n\ntype Styled<T> = Omit<T, \"className\" | \"style\"> & {\n  sx?: Sx;\n};\n\ntype AvatarProps = Styled<AvatarPrimitive.Root.Props> & {\n  /** `rounded` for a logo; a group's cut-outs assume a pill. */\n  corners?: AvatarCorners;\n  size?: AvatarSize;\n};\n\nconst avatarPresences = [\"online\", \"away\", \"busy\", \"offline\"] as const;\n\ntype AvatarPresence = (typeof avatarPresences)[number];\n\ntype AvatarBadgeProps = Styled<ComponentProps<\"span\">> & {\n  presence?: AvatarPresence;\n};\n\nconst inGroup = \":is([data-slot='avatar-group'] > *)\";\nconst groupOf = (size: AvatarSize) =>\n  `:is([data-slot='avatar-group']:has(> [data-size='${size}']) > *)`;\n\n// Leading a badge, it takes the size the badge sets for it.\nconst inBadge = (size: string) => ({\n  default: size,\n  \":is([data-slot='badge'] > :first-child)\": badgeVars.avatar,\n});\n\nconst gap = `calc(2 * ${strokes.border})`;\nconst overlap = space.xs;\n\n// A hole one gap wider than a circle of `size` centered at `x y`, so whatever surface is\n// behind shows through. Chromium rejects a radius mixing `%` and `px`, so all are lengths.\nconst hole = (size: string, x: string, y: string) =>\n  `radial-gradient(circle calc(${size} / 2 + ${gap}) at ${x} ${y}, transparent calc(100% - 0.5px), black calc(100% + 0.5px))`;\n\n// Each avatar but the last is cut where the next one overlaps it.\nconst overlapped = `${inGroup}:not(:last-child)`;\nconst hasBadge = \":has(> [data-slot='avatar-badge'])\";\n// Not `:dir(rtl)`: Lightning CSS lowers it to a `:lang()` list that ignores `dir`.\nconst rtl = \":is([dir='rtl'], [dir='rtl'] *)\";\n\nconst styles = stylex.create({\n  avatar: {\n    // The fallback leaves as the photo arrives, so the photo fades in over its fill.\n    backgroundColor: colors.fillOpaque,\n    borderStartStartRadius: avatarVars.corner,\n    borderStartEndRadius: avatarVars.corner,\n    borderEndStartRadius: avatarVars.corner,\n    borderEndEndRadius: avatarVars.corner,\n    display: \"flex\",\n    flexShrink: 0,\n    height: avatarVars.size,\n    marginInlineStart: {\n      default: null,\n      [`${inGroup}:not(:first-child)`]: `calc(-1 * ${overlap})`,\n    },\n    maskImage: {\n      default: null,\n      [overlapped]: {\n        default: hole(\n          avatarVars.size,\n          `calc(${avatarVars.size} * 1.5 - ${overlap})`,\n          `calc(${avatarVars.size} / 2)`\n        ),\n        [rtl]: hole(\n          avatarVars.size,\n          `calc(${overlap} - ${avatarVars.size} / 2)`,\n          `calc(${avatarVars.size} / 2)`\n        ),\n      },\n    },\n    position: \"relative\",\n    // The photo, fallback and edge read this; the badge itself stays whole.\n    [avatarVars.badgeCut]: {\n      default: null,\n      [hasBadge]: {\n        default: hole(\n          avatarVars.badge,\n          `calc(${avatarVars.size} - ${avatarVars.badge} / 2)`,\n          `calc(${avatarVars.size} - ${avatarVars.badge} / 2)`\n        ),\n        [rtl]: hole(\n          avatarVars.badge,\n          `calc(${avatarVars.badge} / 2)`,\n          `calc(${avatarVars.size} - ${avatarVars.badge} / 2)`\n        ),\n      },\n    },\n    userSelect: \"none\",\n    width: avatarVars.size,\n    // A photo's edge is drawn inside it, so a light or dark image keeps its shape.\n    \"::after\": {\n      borderBlockColor: colors.border,\n      borderInlineColor: colors.border,\n      borderStartStartRadius: avatarVars.corner,\n      borderStartEndRadius: avatarVars.corner,\n      borderEndStartRadius: avatarVars.corner,\n      borderEndEndRadius: avatarVars.corner,\n      borderBlockStyle: \"solid\",\n      borderInlineStyle: \"solid\",\n      borderBlockWidth: strokes.border,\n      borderInlineWidth: strokes.border,\n      content: \"''\",\n      insetBlockEnd: 0,\n      insetBlockStart: 0,\n      insetInlineEnd: 0,\n      insetInlineStart: 0,\n      maskImage: avatarVars.badgeCut,\n      pointerEvents: \"none\",\n      position: \"absolute\",\n    },\n  },\n  sm: {\n    [avatarVars.size]: inBadge(sizes.controlXs),\n    [avatarVars.badge]: space.xs,\n  },\n  default: {\n    [avatarVars.size]: inBadge(sizes.controlMd),\n    [avatarVars.badge]: `calc(${space.xs} + ${space.xxxs})`,\n  },\n  lg: {\n    [avatarVars.size]: inBadge(sizes.controlLg),\n    [avatarVars.badge]: space.sm,\n  },\n  // A ladder step under the badge's or a control's corner, so a logo tile reads as one.\n  rounded: {\n    [avatarVars.corner]: {\n      default: radii.sm,\n      \":is([data-size='sm'])\": radii.xs,\n      \":is([data-slot='badge'] > :first-child)\": radii.xs,\n    },\n  },\n  image: {\n    aspectRatio: \"1\",\n    borderStartStartRadius: avatarVars.corner,\n    borderStartEndRadius: avatarVars.corner,\n    borderEndStartRadius: avatarVars.corner,\n    borderEndEndRadius: avatarVars.corner,\n    height: \"100%\",\n    maskImage: avatarVars.badgeCut,\n    objectFit: \"cover\",\n    opacity: { default: 1, \":is([data-starting-style])\": 0 },\n    transitionDuration: durations.crossfade,\n    transitionProperty: \"opacity\",\n    transitionTimingFunction: easings.crossfade,\n    width: \"100%\",\n  },\n  fallback: {\n    alignItems: \"center\",\n    backgroundColor: colors.fillOpaque,\n    borderStartStartRadius: avatarVars.corner,\n    borderStartEndRadius: avatarVars.corner,\n    borderEndStartRadius: avatarVars.corner,\n    borderEndEndRadius: avatarVars.corner,\n    color: colors.textSecondary,\n    display: \"flex\",\n    fontSize: {\n      default: fontSizes.textSm,\n      \":is([data-size='sm'] *)\": fontSizes.captionLg,\n      \":is([data-slot='badge'] > :first-child > *)\": fontSizes.captionLg,\n    },\n    lineHeight: {\n      default: lineHeights.textSm,\n      \":is([data-size='sm'] *)\": lineHeights.captionLg,\n      \":is([data-slot='badge'] > :first-child > *)\": lineHeights.captionLg,\n    },\n    fontWeight: fontWeights.medium,\n    height: \"100%\",\n    justifyContent: \"center\",\n    maskImage: avatarVars.badgeCut,\n    width: \"100%\",\n  },\n  badge: {\n    alignItems: \"center\",\n    backgroundColor: colors.accentSolid,\n    borderStartStartRadius: radii.full,\n    borderStartEndRadius: radii.full,\n    borderEndStartRadius: radii.full,\n    borderEndEndRadius: radii.full,\n    color: colors.accentOnSolid,\n    display: \"inline-flex\",\n    height: avatarVars.badge,\n    insetBlockEnd: 0,\n    insetInlineEnd: 0,\n    justifyContent: \"center\",\n    position: \"absolute\",\n    transitionDuration: durations.hover,\n    transitionProperty: \"background-color\",\n    transitionTimingFunction: \"ease\",\n    width: avatarVars.badge,\n    zIndex: 1,\n  },\n  online: { backgroundColor: colors.successSolid },\n  away: { backgroundColor: colors.warningSolid },\n  busy: { backgroundColor: colors.dangerSolid },\n  offline: { backgroundColor: colors.textMuted },\n  group: {\n    display: \"flex\",\n  },\n  count: {\n    alignItems: \"center\",\n    backgroundColor: colors.fillOpaque,\n    borderStartStartRadius: radii.full,\n    borderStartEndRadius: radii.full,\n    borderEndStartRadius: radii.full,\n    borderEndEndRadius: radii.full,\n    color: colors.textSecondary,\n    display: \"flex\",\n    flexShrink: 0,\n    fontSize: {\n      default: fontSizes.textSm,\n      [groupOf(\"sm\")]: fontSizes.captionLg,\n    },\n    fontWeight: fontWeights.medium,\n    height: {\n      default: sizes.controlMd,\n      [groupOf(\"sm\")]: sizes.controlXs,\n      [groupOf(\"lg\")]: sizes.controlLg,\n    },\n    justifyContent: \"center\",\n    marginInlineStart: `calc(-1 * ${space.xs})`,\n    position: \"relative\",\n    width: {\n      default: sizes.controlMd,\n      [groupOf(\"sm\")]: sizes.controlXs,\n      [groupOf(\"lg\")]: sizes.controlLg,\n    },\n    // The avatars' edge, so the count reads as one of them.\n    \"::after\": {\n      borderBlockColor: colors.border,\n      borderInlineColor: colors.border,\n      borderStartStartRadius: radii.full,\n      borderStartEndRadius: radii.full,\n      borderEndStartRadius: radii.full,\n      borderEndEndRadius: radii.full,\n      borderBlockStyle: \"solid\",\n      borderInlineStyle: \"solid\",\n      borderBlockWidth: strokes.border,\n      borderInlineWidth: strokes.border,\n      content: \"''\",\n      insetBlockEnd: 0,\n      insetBlockStart: 0,\n      insetInlineEnd: 0,\n      insetInlineStart: 0,\n      pointerEvents: \"none\",\n      position: \"absolute\",\n    },\n  },\n});\n\nconst sizeStyles = {\n  default: styles.default,\n  lg: styles.lg,\n  sm: styles.sm,\n} satisfies Record<AvatarSize, stylex.StyleXStyles>;\n\nfunction Avatar({\n  corners = \"pill\",\n  size = \"default\",\n  sx,\n  ...props\n}: AvatarProps) {\n  return (\n    <AvatarPrimitive.Root\n      data-corners={corners}\n      data-size={size}\n      data-slot=\"avatar\"\n      {...props}\n      {...stylex.props(\n        styles.avatar,\n        sizeStyles[size],\n        corners === \"rounded\" && styles.rounded,\n        sx\n      )}\n    />\n  );\n}\n\nfunction AvatarImage({ sx, ...props }: Styled<AvatarPrimitive.Image.Props>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      {...props}\n      {...stylex.props(styles.image, sx)}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  sx,\n  ...props\n}: Styled<AvatarPrimitive.Fallback.Props>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      {...props}\n      {...stylex.props(styles.fallback, sx)}\n    />\n  );\n}\n\nconst presenceStyles = {\n  away: styles.away,\n  busy: styles.busy,\n  offline: styles.offline,\n  online: styles.online,\n} satisfies Record<AvatarPresence, stylex.StyleXStyles>;\n\nconst presenceLabels = {\n  away: \"Away\",\n  busy: \"Busy\",\n  offline: \"Offline\",\n  online: \"Online\",\n} satisfies Record<AvatarPresence, string>;\n\nfunction AvatarBadge({ presence, sx, ...props }: AvatarBadgeProps) {\n  return (\n    <span\n      aria-label={presence && presenceLabels[presence]}\n      data-slot=\"avatar-badge\"\n      data-presence={presence}\n      role={presence && \"img\"}\n      {...props}\n      {...stylex.props(styles.badge, presence && presenceStyles[presence], sx)}\n    />\n  );\n}\n\nfunction AvatarGroup({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"avatar-group\"\n      {...props}\n      {...stylex.props(styles.group, sx)}\n    />\n  );\n}\n\nfunction AvatarGroupCount({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"avatar-group-count\"\n      {...props}\n      {...stylex.props(styles.count, sx)}\n    />\n  );\n}\n\nexport {\n  Avatar,\n  AvatarBadge,\n  AvatarFallback,\n  AvatarGroup,\n  AvatarGroupCount,\n  AvatarImage,\n  avatarCorners,\n  avatarSizes,\n  avatarPresences,\n};\nexport type {\n  AvatarBadgeProps,\n  AvatarProps,\n  AvatarCorners,\n  AvatarSize,\n  AvatarPresence,\n};\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/avatar/index.md",
  "type": "registry:ui"
}