{"version":1,"id":"liquid-metal-avatar","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/liquid-metal-avatar.tsx","content":"\"use client\";\n// GFA liquid metal avatar (fab-ui / Paper LiquidMetal)\n\nimport { LiquidMetal, type LiquidMetalProps } from \"@paper-design/shaders-react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  createContext,\n  type ComponentProps,\n  type ImgHTMLAttributes,\n  type ReactNode,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst avatarVariants = cva(\n  \"relative flex shrink-0 items-center justify-center overflow-visible rounded-full bg-transparent\",\n  {\n    variants: {\n      size: {\n        sm: \"size-8\",\n        md: \"size-12\",\n        lg: \"size-16\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  },\n);\n\nconst innerVariants = cva(\n  \"absolute z-[1] overflow-hidden rounded-full bg-background shadow-[inset_0_1px_2px_rgba(0,0,0,0.18)]\",\n  {\n    variants: {\n      size: {\n        sm: \"inset-px\",\n        md: \"inset-0.5\",\n        lg: \"inset-[3px]\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  },\n);\n\ntype ShaderProps = Omit<LiquidMetalProps, \"className\" | \"style\" | \"shape\">;\n\nexport type LiquidMetalAvatarProps = ComponentProps<\"div\"> &\n  Partial<ShaderProps> &\n  VariantProps<typeof avatarVariants>;\n\ntype AvatarCtx = {\n  imageStatus: \"idle\" | \"loaded\" | \"error\";\n  setImageStatus: (status: \"idle\" | \"loaded\" | \"error\") => void;\n};\n\nconst AvatarContext = createContext<AvatarCtx | null>(null);\n\nexport function LiquidMetalAvatar({\n  size = \"md\",\n  className,\n  children,\n  speed = 0.28,\n  repetition = 4,\n  softness = 0.62,\n  shiftRed = 0.3,\n  shiftBlue = 0.3,\n  distortion = 0,\n  contour = 0,\n  angle = 45,\n  scale = 8,\n  offsetX = 0.1,\n  offsetY = -0.1,\n  colorBack = \"#c8c8ca\",\n  colorTint = \"#ffffff\",\n  ...props\n}: LiquidMetalAvatarProps) {\n  const [imageStatus, setImageStatus] = useState<\"idle\" | \"loaded\" | \"error\">(\n    \"idle\",\n  );\n  const ctx = useMemo(\n    () => ({ imageStatus, setImageStatus }),\n    [imageStatus],\n  );\n\n  return (\n    <div\n      data-slot=\"liquid-metal-avatar\"\n      className={cn(avatarVariants({ size }), className)}\n      {...props}\n    >\n      <div className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-full\">\n        <LiquidMetal\n          className=\"absolute inset-0 size-full\"\n          shape=\"none\"\n          speed={speed}\n          repetition={repetition}\n          softness={softness}\n          shiftRed={shiftRed}\n          shiftBlue={shiftBlue}\n          distortion={distortion}\n          contour={contour}\n          angle={angle}\n          scale={scale}\n          offsetX={offsetX}\n          offsetY={offsetY}\n          colorBack={colorBack}\n          colorTint={colorTint}\n        />\n      </div>\n      <AvatarContext.Provider value={ctx}>\n        <div\n          data-slot=\"avatar\"\n          className={cn(\n            innerVariants({ size }),\n            \"flex items-center justify-center bg-muted\",\n          )}\n        >\n          {children}\n        </div>\n      </AvatarContext.Provider>\n    </div>\n  );\n}\n\nexport function AvatarImage({\n  className,\n  alt = \"\",\n  onError,\n  onLoad,\n  ...props\n}: ImgHTMLAttributes<HTMLImageElement>) {\n  const ctx = useContext(AvatarContext);\n  const [failed, setFailed] = useState(false);\n\n  if (failed) return null;\n\n  return (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      data-slot=\"avatar-image\"\n      alt={alt}\n      className={cn(\n        \"pointer-events-none absolute inset-0 aspect-square size-full rounded-full object-cover select-none\",\n        className,\n      )}\n      onError={(event) => {\n        setFailed(true);\n        ctx?.setImageStatus(\"error\");\n        onError?.(event);\n      }}\n      onLoad={(event) => {\n        ctx?.setImageStatus(\"loaded\");\n        onLoad?.(event);\n      }}\n      {...props}\n    />\n  );\n}\n\nexport function AvatarFallback({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"span\"> & { children?: ReactNode }) {\n  const ctx = useContext(AvatarContext);\n  const visible = !ctx || ctx.imageStatus !== \"loaded\";\n\n  if (!visible) return null;\n\n  return (\n    <span\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"flex size-full items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport { LiquidMetalAvatar as LiquidMetalAvatarBlock };\nexport type { LiquidMetalAvatarProps as LiquidMetalAvatarBlockProps };\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}