{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"avatar","type":"registry:ui","title":"Avatar","description":"An image element with a fallback for representing a user, and a group that stacks several.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/tooltip.json"],"files":[{"path":"registry/dashral/avatar.tsx","type":"registry:file","target":"components/dashral/avatar.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Avatar as AvatarPrimitive } from \"radix-ui\"\n\nimport { cn } from \"./lib/utils\"\nimport { Tooltip } from \"./tooltip\"\n\ntype AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\ntype AvatarStatus = \"online\" | \"away\" | \"busy\" | \"offline\"\n\nconst sizes: Record<AvatarSize, string> = {\n  xs: \"size-6 text-[10px]\",\n  sm: \"size-7 text-xs\",\n  md: \"size-8 text-xs\",\n  lg: \"size-10 text-sm\",\n  xl: \"size-16 text-lg\",\n}\n\nconst statusColors: Record<AvatarStatus, string> = {\n  online: \"bg-success\",\n  away: \"bg-warning\",\n  busy: \"bg-destructive\",\n  offline: \"bg-muted-foreground\",\n}\n\n/** \"Olivia Martin\" → \"OM\"; one name gives one letter. */\nconst initials = (name: string) =>\n  name\n    .trim()\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((part) => part[0]?.toUpperCase() ?? \"\")\n    .join(\"\")\n\ntype AvatarProps = Omit<React.ComponentProps<typeof AvatarPrimitive.Root>, \"children\"> & {\n  src?: string\n  /** Who it is: the image's alt text, and the initials shown until (or instead of) the image. */\n  name?: string\n  /** The image's alt text, name by default. \"\" when the name is written beside the avatar, so it isn't read twice. */\n  alt?: string\n  /** Replaces the initials, e.g. an icon. */\n  fallback?: React.ReactNode\n  size?: AvatarSize\n  shape?: \"circle\" | \"square\"\n  /** A dot in the corner; its meaning is read out too. */\n  status?: AvatarStatus\n  classNames?: { image?: string; fallback?: string; status?: string }\n}\n\n/** A person's picture in one tag, with their initials while it loads or when there's none. */\nfunction Avatar({ src, name, alt = name, fallback, size = \"md\", shape = \"circle\", status, className, classNames, ...props }: AvatarProps) {\n  const radius = shape === \"circle\" ? \"rounded-full\" : \"rounded-md\"\n  return (\n    // With alt=\"\", the name is written beside it: the avatar is decoration, so its initials aren't read out either.\n    <span data-slot=\"avatar\" aria-hidden={alt === \"\" || undefined} className={cn(\"relative inline-flex shrink-0\", radius, sizes[size], className)}>\n      <AvatarPrimitive.Root className={cn(\"flex size-full overflow-hidden\", radius)} {...props}>\n        {src && (\n          <AvatarPrimitive.Image\n            src={src}\n            alt={alt ?? \"\"}\n            // Fades in once loaded, over the fallback, instead of popping.\n            className={cn(\"aspect-square size-full animate-in object-cover fade-in-0 duration-(--duration-standard) ease-enter\", classNames?.image)}\n          />\n        )}\n        <AvatarPrimitive.Fallback\n          className={cn(\"flex size-full items-center justify-center bg-muted font-medium text-muted-foreground\", radius, classNames?.fallback)}\n        >\n          {fallback ?? (name ? initials(name) : null)}\n          {/* With no image, the initials alone don't say who it is. */}\n          {!src && alt && <span className=\"sr-only\">{alt}</span>}\n        </AvatarPrimitive.Fallback>\n      </AvatarPrimitive.Root>\n      {status && (\n        <span\n          className={cn(\n            \"absolute end-0 bottom-0 size-[28%] min-w-2 rounded-full ring-2 ring-card\",\n            statusColors[status],\n            classNames?.status\n          )}\n        >\n          <span className=\"sr-only\">{status}</span>\n        </span>\n      )}\n    </span>\n  )\n}\n\ntype AvatarGroupProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  items: (Pick<AvatarProps, \"src\" | \"name\" | \"alt\" | \"fallback\"> & { id?: string | number })[]\n  /** Avatars shown before the rest collapse into \"+N\". */\n  max?: number\n  size?: AvatarSize\n  /** Each avatar shows its name in a tooltip on hover or keyboard focus, and \"+N\" lists the rest. */\n  showNames?: boolean\n}\n\n/**\n * Avatars overlapping in a row, in one tag. Shows the first `max` and a \"+N\" for the rest; give it a label for screen\n * readers, e.g. aria-label=\"5 people on this board\".\n */\nfunction AvatarGroup({ items, max, size = \"md\", showNames = false, className, ...props }: AvatarGroupProps) {\n  const shown = max === undefined ? items : items.slice(0, max)\n  const rest = items.slice(shown.length)\n  // The one under the pointer (or focused) rises a little and comes to the front of the stack.\n  const lift =\n    \"relative rounded-full transition-transform duration-(--duration-fast) ease-standard outline-none hover:z-10 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring motion-safe:hover:-translate-y-0.5\"\n  const more = rest.length > 0 && (\n    <span\n      tabIndex={showNames ? 0 : undefined}\n      className={cn(\n        \"flex shrink-0 items-center justify-center bg-muted font-medium text-muted-foreground tabular-nums ring-2 ring-card\",\n        lift,\n        sizes[size]\n      )}\n    >\n      +{rest.length}\n    </span>\n  )\n  const restNames = rest.map((item) => item.name).filter(Boolean)\n  return (\n    <div data-slot=\"avatar-group\" role=\"group\" className={cn(\"flex items-center -space-x-2\", className)} {...props}>\n      {shown.map(({ id, ...item }, i) =>\n        showNames && item.name ? (\n          <Tooltip key={id ?? i} content={item.name}>\n            <span tabIndex={0} className={lift}>\n              <Avatar {...item} size={size} className=\"ring-2 ring-card\" />\n            </span>\n          </Tooltip>\n        ) : (\n          <Avatar key={id ?? i} {...item} size={size} className={cn(\"ring-2 ring-card\", lift)} />\n        )\n      )}\n      {more && showNames && restNames.length > 0 ? (\n        <Tooltip content={<span className=\"block max-w-56\">{restNames.join(\", \")}</span>}>{more}</Tooltip>\n      ) : (\n        more\n      )}\n    </div>\n  )\n}\n\nexport { Avatar, AvatarGroup, type AvatarProps, type AvatarGroupProps, type AvatarSize, type AvatarStatus }\n"}],"cssVars":{"theme":{"color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42"}}}