{
  "name": "avatar",
  "type": "registry:ui",
  "description": "Avatar with image and text fallback parts",
  "files": [
    {
      "path": "ui/avatar.tsx",
      "type": "registry:ui",
      "content": "import { fv, type VariantProps } from \"@facet-ui/react-variants\";\nimport { Avatar as AvatarPrimitive } from \"@lattice-ui/react-avatar\";\nimport { getPassthroughProps, type PassthroughProps, React, toSlotProps } from \"@lattice-ui/react-runtime\";\nimport { type ClassName, cn } from \"~/lib/utils\";\n\n/**\n * Lattice's `Avatar.Root` renders no instance — it only tracks whether the\n * image loaded — so the circle itself is drawn here. The image and the fallback\n * both fill it and carry their own `rounded-full`: Roblox clips to a rectangle,\n * so rounding the parent alone would leave square corners poking out of the\n * circle whenever a child paints.\n *\n * `size` is shadcn's three steps — `sm` 24px, `default` 32px, `lg` 40px — and it\n * has to be passed to the fallback as well as the root, because shadcn changes\n * the initials' size through a `group-data-[size=sm]` selector and nothing\n * cascades here. Same shape as `alert`'s `variant`.\n *\n * shadcn has three more parts this file does not: `AvatarBadge`, `AvatarGroup`\n * and `AvatarGroupCount`. All three are positioned by things Vela reports as\n * having no Roblox equivalent — `absolute` with `right-0 bottom-0` for the\n * badge, a negative `-space-x-2` for the overlap in the group. A stack of\n * avatars is expressible, but not by copying those classes over.\n *\n * One recipe object rather than three exports: every exported name costs a\n * Luau register once Vela inlines its runtime. See\n * docs/decisions/luau-register-limit.md.\n */\nexport const avatarVariants = {\n  root: fv(\"shrink-0 rounded-full overflow-hidden\", {\n    variants: {\n      size: { sm: \"size-6\", default: \"size-8\", lg: \"size-10\" },\n    },\n    defaultVariants: { size: \"default\" },\n  }),\n  image: fv(\"aspect-square size-full rounded-full\"),\n  // `bg-muted` belongs to the fallback, not the circle. shadcn puts it here so\n  // an avatar with a loaded image has no surface of its own to show through.\n  fallback: fv(\"size-full rounded-full bg-muted font-normal text-muted-foreground text-center\", {\n    variants: {\n      // shadcn shrinks the initials on the small avatar and leaves the other two\n      // at `text-sm`. Nothing cascades here, so `size` reaches this part too.\n      size: { sm: \"text-xs\", default: \"text-sm\", lg: \"text-sm\" },\n    },\n    defaultVariants: { size: \"default\" },\n  }),\n};\n\nconst NEUTRAL_PROPS = {\n  BackgroundTransparency: 1,\n  BorderSizePixel: 0,\n};\n\nconst ROOT_OWN_PROPS = [\"src\", \"delayMs\", \"size\", \"className\", \"children\"] as const;\nconst IMAGE_OWN_PROPS = [\"className\", \"children\"] as const;\nconst FALLBACK_OWN_PROPS = [\"size\", \"className\", \"Text\", \"children\"] as const;\n\nexport type AvatarProps = VariantProps<typeof avatarVariants.root> & {\n  /** The image source — an `rbxassetid://` or `rbxthumb://` URL. */\n  src?: string;\n  /** How long to hold the fallback back while the image loads, in milliseconds. */\n  delayMs?: number;\n  className?: ClassName;\n  children?: React.ReactNode;\n} & PassthroughProps<Frame>;\n\nexport type AvatarImageProps = { className?: ClassName } & PassthroughProps<ImageLabel>;\nexport type AvatarFallbackProps = VariantProps<typeof avatarVariants.fallback> & {\n  className?: ClassName;\n  Text?: string;\n} & PassthroughProps<TextLabel>;\n\n// The forwarded bag is widened by `toSlotProps` and then has `children`\n// dropped from its *type*: these parts type `children` as a single element\n// (what `asChild` merges onto), and the bag never actually carries one —\n// `children` is listed as an own prop — so only the type needs narrowing.\nfunction forwardProps(props: object, ownKeys: readonly string[]): { key?: React.Key } & { [index: string]: unknown } {\n  return toSlotProps(getPassthroughProps(props, ownKeys));\n}\n\nexport function Avatar(props: AvatarProps) {\n  return (\n    <AvatarPrimitive.Root delayMs={props.delayMs} src={props.src}>\n      <frame\n        className={cn(avatarVariants.root({ size: props.size, className: props.className }))}\n        {...NEUTRAL_PROPS}\n        {...getPassthroughProps<Frame>(props, ROOT_OWN_PROPS)}\n      >\n        {props.children}\n      </frame>\n    </AvatarPrimitive.Root>\n  );\n}\n\nexport function AvatarImage(props: AvatarImageProps) {\n  return (\n    <AvatarPrimitive.Image\n      className={cn(avatarVariants.image({ className: props.className }))}\n      {...forwardProps(props, IMAGE_OWN_PROPS)}\n    />\n  );\n}\n\nexport function AvatarFallback(props: AvatarFallbackProps) {\n  return (\n    <AvatarPrimitive.Fallback\n      className={cn(avatarVariants.fallback({ size: props.size, className: props.className }))}\n      Text={props.Text ?? \"\"}\n      {...forwardProps(props, FALLBACK_OWN_PROPS)}\n    />\n  );\n}\n"
    }
  ],
  "registryDependencies": [
    "utils"
  ],
  "dependencies": [
    "@facet-ui/react-variants@^0.1.1",
    "@lattice-ui/react-runtime@^0.8.0",
    "@lattice-ui/react-avatar@^0.8.0"
  ],
  "tokens": [
    "muted",
    "muted-foreground"
  ]
}
