{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "avatar", "title": "Avatar", "description": "A hand-drawn circle border around a profile image or initials fallback.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json" ], "files": [ { "path": "registry/new-york/ui/avatar.tsx", "content": "\"use client\";\n\nimport { useCallback, useContext, useEffect } from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CrumbleContext,\n randomSeed,\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\n\nexport interface AvatarProps extends CrumbleColorProps {\n animateOnHover?: boolean;\n className?: string;\n fallback?: string;\n id?: string;\n size?: number;\n src?: string;\n theme?: CrumbleTheme;\n}\n\nexport interface AvatarGroupProps {\n avatars: AvatarProps[];\n className?: string;\n fill?: string;\n max?: number;\n size?: number;\n stroke?: string;\n strokeMuted?: string;\n theme?: CrumbleTheme;\n}\n\nfunction AvatarCircle({\n animateOnHover = true,\n className,\n fallback,\n fill,\n id,\n size = 40,\n src,\n stroke,\n strokeMuted,\n theme: themeProp,\n}: AvatarProps) {\n const stableId = id ?? `avatar-${fallback ?? \"user\"}`;\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const {\n animateOnHover: animateFromContext,\n drawCircle,\n svgRef,\n } = useRough({\n stableId,\n theme: themeProp,\n variant: \"border\",\n });\n\n const draw = useCallback(\n (reseed = false) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n svg.replaceChildren();\n svg.setAttribute(\"width\", String(size));\n svg.setAttribute(\"height\", String(size));\n svg.setAttribute(\"viewBox\", `0 0 ${size} ${size}`);\n\n const circle = drawCircle(size / 2, size / 2, size - 3, {\n fill: \"none\",\n seed: reseed ? randomSeed() : undefined,\n stroke: \"var(--cr-stroke)\",\n });\n if (circle) svg.appendChild(circle);\n },\n [drawCircle, size, svgRef],\n );\n\n useEffect(() => {\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw]);\n\n return (\n {\n if (animateOnHover && animateFromContext) draw(true);\n }}\n onMouseLeave={() => {\n if (animateOnHover && animateFromContext) draw(false);\n }}\n >\n {src ? (\n \n ) : (\n \n {fallback?.slice(0, 2).toUpperCase() ?? \"?\"}\n \n )}\n \n \n );\n}\n\nexport function Avatar(props: AvatarProps) {\n return ;\n}\n\nexport function AvatarGroup({\n avatars,\n className,\n fill,\n max = 4,\n size = 40,\n stroke,\n strokeMuted,\n theme: themeProp,\n}: AvatarGroupProps) {\n const { theme: contextTheme } = useContext(CrumbleContext);\n const theme = themeProp ?? contextTheme;\n const visible = avatars.slice(0, max);\n const overflow = avatars.length - max;\n const overlap = Math.round(size * 0.3);\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n\n return (\n \n {visible.map((avatar, i) => (\n
\n \n
\n ))}\n {overflow > 0 ? (\n \n \n +{overflow}\n \n \n ) : null}\n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/avatar.tsx" } ], "type": "registry:ui" }