{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-avatar-stack-react-router",
  "title": "Avatar Stack with Realtime Presence",
  "description": "Component which stack of avatars, tracked by realtime presence.",
  "dependencies": [
    "@supabase/ssr@latest",
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [
    "avatar",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/default/blocks/realtime-avatar-stack/components/avatar-stack.tsx",
      "content": "import { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/registry/default/components/ui/avatar'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'\n\nconst avatarStackVariants = cva('flex -space-x-4 -space-y-4', {\n  variants: {\n    orientation: {\n      vertical: 'flex-row',\n      horizontal: 'flex-col',\n    },\n  },\n  defaultVariants: {\n    orientation: 'vertical',\n  },\n})\n\nexport interface AvatarStackProps\n  extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof avatarStackVariants> {\n  avatars: { name: string; image: string }[]\n  maxAvatarsAmount?: number\n}\n\nconst AvatarStack = ({\n  className,\n  orientation,\n  avatars,\n  maxAvatarsAmount = 3,\n  ...props\n}: AvatarStackProps) => {\n  const shownAvatars = avatars.slice(0, maxAvatarsAmount)\n  const hiddenAvatars = avatars.slice(maxAvatarsAmount)\n\n  return (\n    <div\n      className={cn(\n        avatarStackVariants({ orientation }),\n        className,\n        orientation === 'horizontal' ? 'space-x-0' : 'space-y-0'\n      )}\n      {...props}\n    >\n      {shownAvatars.map(({ name, image }, index) => (\n        <Tooltip key={`${name}-${image}-${index}`}>\n          <TooltipTrigger asChild>\n            <Avatar className=\"hover:z-10\">\n              <AvatarImage src={image} />\n              <AvatarFallback>\n                {name\n                  ?.split(' ')\n                  ?.map((word) => word[0])\n                  ?.join('')\n                  ?.toUpperCase()}\n              </AvatarFallback>\n            </Avatar>\n          </TooltipTrigger>\n          <TooltipContent>\n            <p>{name}</p>\n          </TooltipContent>\n        </Tooltip>\n      ))}\n\n      {hiddenAvatars.length ? (\n        <Tooltip key=\"hidden-avatars\">\n          <TooltipTrigger asChild>\n            <Avatar>\n              <AvatarFallback>+{avatars.length - shownAvatars.length}</AvatarFallback>\n            </Avatar>\n          </TooltipTrigger>\n          <TooltipContent>\n            {hiddenAvatars.map(({ name }, index) => (\n              <p key={`${name}-${index}`}>{name}</p>\n            ))}\n          </TooltipContent>\n        </Tooltip>\n      ) : null}\n    </div>\n  )\n}\n\nexport { AvatarStack, avatarStackVariants }\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-avatar-stack/components/realtime-avatar-stack.tsx",
      "content": "'use client'\n\nimport { useMemo } from 'react'\n\nimport { AvatarStack } from '@/registry/default/blocks/realtime-avatar-stack/components/avatar-stack'\nimport { useRealtimePresenceRoom } from '@/registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room'\n\nexport const RealtimeAvatarStack = ({ roomName }: { roomName: string }) => {\n  const { users: usersMap } = useRealtimePresenceRoom(roomName)\n  const avatars = useMemo(() => {\n    return Object.values(usersMap).map((user) => ({\n      name: user.name,\n      image: user.image,\n    }))\n  }, [usersMap])\n\n  return <AvatarStack avatars={avatars} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-avatar-stack/hooks/use-realtime-presence-room.ts",
      "content": "'use client'\n\nimport { REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js'\nimport { useEffect, useState } from 'react'\n\nimport { useCurrentUserImage } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-image'\nimport { useCurrentUserName } from '@/registry/default/blocks/current-user-avatar/hooks/use-current-user-name'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\nconst supabase = createClient()\n\nexport type RealtimeUser = {\n  id: string\n  name: string\n  image: string\n}\n\nexport const useRealtimePresenceRoom = (roomName: string) => {\n  const currentUserImage = useCurrentUserImage()\n  const currentUserName = useCurrentUserName()\n\n  const [users, setUsers] = useState<Record<string, RealtimeUser>>({})\n\n  useEffect(() => {\n    const room = supabase.channel(roomName)\n\n    room\n      .on('presence', { event: 'sync' }, () => {\n        const newState = room.presenceState<{ image: string; name: string }>()\n\n        const newUsers = Object.fromEntries(\n          Object.entries(newState).map(([key, values]) => [\n            key,\n            { name: values[0].name, image: values[0].image },\n          ])\n        ) as Record<string, RealtimeUser>\n        setUsers(newUsers)\n      })\n      .subscribe(async (status) => {\n        if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n          await room.track({\n            name: currentUserName,\n            image: currentUserImage,\n          })\n        } else {\n          setUsers({})\n        }\n      })\n\n    return () => {\n      room.unsubscribe()\n    }\n  }, [roomName, currentUserName, currentUserImage])\n\n  return { users }\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-name.ts",
      "content": "import { useEffect, useState } from 'react'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\nexport const useCurrentUserName = () => {\n  const [name, setName] = useState<string | null>(null)\n\n  useEffect(() => {\n    const fetchProfileName = async () => {\n      const { data, error } = await createClient().auth.getSession()\n      if (error) {\n        console.error(error)\n      }\n\n      setName(data.session?.user.user_metadata.full_name ?? '?')\n    }\n\n    fetchProfileName()\n  }, [])\n\n  return name || '?'\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/blocks/current-user-avatar/hooks/use-current-user-image.ts",
      "content": "import { useEffect, useState } from 'react'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\nexport const useCurrentUserImage = () => {\n  const [image, setImage] = useState<string | null>(null)\n\n  useEffect(() => {\n    const fetchUserImage = async () => {\n      const { data, error } = await createClient().auth.getSession()\n      if (error) {\n        console.error(error)\n      }\n\n      setImage(data.session?.user.user_metadata.avatar_url ?? null)\n    }\n    fetchUserImage()\n  }, [])\n\n  return image\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/clients/react-router/lib/supabase/client.ts",
      "content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n  return createBrowserClient(\n    import.meta.env.VITE_SUPABASE_URL!,\n    import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/react-router/lib/supabase/server.ts",
      "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n  const headers = new Headers()\n\n  const supabase = createServerClient(\n    process.env.VITE_SUPABASE_URL!,\n    process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n            name: string\n            value: string\n          }[]\n        },\n        setAll(cookiesToSet) {\n          cookiesToSet.forEach(({ name, value, options }) =>\n            headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n          )\n        },\n      },\n    }\n  )\n\n  return { supabase, headers }\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "VITE_SUPABASE_URL": "",
    "VITE_SUPABASE_PUBLISHABLE_KEY": ""
  },
  "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
  "type": "registry:component"
}