{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-cursor-react-router",
  "title": "Realtime Cursor",
  "description": "Component which renders realtime cursors from other users in a room.",
  "dependencies": [
    "lucide-react",
    "@supabase/ssr@latest",
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/realtime-cursor/components/cursor.tsx",
      "content": "import { MousePointer2 } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nexport const Cursor = ({\n  className,\n  style,\n  color,\n  name,\n}: {\n  className?: string\n  style?: React.CSSProperties\n  color: string\n  name: string\n}) => {\n  return (\n    <div className={cn('pointer-events-none', className)} style={style}>\n      <MousePointer2 color={color} fill={color} size={30} />\n\n      <div\n        className=\"mt-1 px-2 py-1 rounded-sm text-xs font-bold text-white text-center\"\n        style={{ backgroundColor: color }}\n      >\n        {name}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx",
      "content": "'use client'\n\nimport { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'\nimport { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'\n\nconst THROTTLE_MS = 50\n\nexport const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {\n  const { cursors } = useRealtimeCursors({ roomName, username, throttleMs: THROTTLE_MS })\n\n  return (\n    <div>\n      {Object.keys(cursors).map((id) => (\n        <Cursor\n          key={id}\n          className=\"fixed transition-transform ease-in-out z-50\"\n          style={{\n            transitionDuration: '20ms',\n            top: 0,\n            left: 0,\n            transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,\n          }}\n          color={cursors[id].color}\n          name={cursors[id].user.name}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors.ts",
      "content": "import { REALTIME_SUBSCRIBE_STATES, RealtimeChannel } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay, It will only call the callback if the delay has passed, with the arguments\n * from the last call\n */\nconst useThrottleCallback = <Params extends unknown[], Return>(\n  callback: (...args: Params) => Return,\n  delay: number\n) => {\n  const lastCall = useRef(0)\n  const timeout = useRef<NodeJS.Timeout | null>(null)\n\n  return useCallback(\n    (...args: Params) => {\n      const now = Date.now()\n      const remainingTime = delay - (now - lastCall.current)\n\n      if (remainingTime <= 0) {\n        if (timeout.current) {\n          clearTimeout(timeout.current)\n          timeout.current = null\n        }\n        lastCall.current = now\n        callback(...args)\n      } else if (!timeout.current) {\n        timeout.current = setTimeout(() => {\n          lastCall.current = Date.now()\n          timeout.current = null\n          callback(...args)\n        }, remainingTime)\n      }\n    },\n    [callback, delay]\n  )\n}\n\nconst supabase = createClient()\n\nconst generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`\n\nconst generateRandomNumber = () => Math.floor(Math.random() * 100)\n\nconst EVENT_NAME = 'realtime-cursor-move'\n\ntype CursorEventPayload = {\n  position: {\n    x: number\n    y: number\n  }\n  user: {\n    id: number\n    name: string\n  }\n  color: string\n  timestamp: number\n}\n\nexport const useRealtimeCursors = ({\n  roomName,\n  username,\n  throttleMs,\n}: {\n  roomName: string\n  username: string\n  throttleMs: number\n}) => {\n  const [color] = useState(generateRandomColor())\n  const [userId] = useState(generateRandomNumber())\n  const [cursors, setCursors] = useState<Record<string, CursorEventPayload>>({})\n  const cursorPayload = useRef<CursorEventPayload | null>(null)\n\n  const channelRef = useRef<RealtimeChannel | null>(null)\n\n  const callback = useCallback(\n    (event: MouseEvent) => {\n      const { clientX, clientY } = event\n\n      const payload: CursorEventPayload = {\n        position: {\n          x: clientX,\n          y: clientY,\n        },\n        user: {\n          id: userId,\n          name: username,\n        },\n        color: color,\n        timestamp: new Date().getTime(),\n      }\n\n      cursorPayload.current = payload\n\n      channelRef.current?.send({\n        type: 'broadcast',\n        event: EVENT_NAME,\n        payload: payload,\n      })\n    },\n    [color, userId, username]\n  )\n\n  const handleMouseMove = useThrottleCallback(callback, throttleMs)\n\n  useEffect(() => {\n    const channel = supabase.channel(roomName)\n\n    channel\n      .on('presence', { event: 'leave' }, ({ leftPresences }) => {\n        leftPresences.forEach(function (element) {\n          // Remove cursor when user leaves\n          setCursors((prev) => {\n            if (prev[element.key]) {\n              delete prev[element.key]\n            }\n\n            return { ...prev }\n          })\n        })\n      })\n      .on('presence', { event: 'join' }, () => {\n        if (!cursorPayload.current) return\n\n        // All cursors broadcast their position when a new cursor joins\n        channelRef.current?.send({\n          type: 'broadcast',\n          event: EVENT_NAME,\n          payload: cursorPayload.current,\n        })\n      })\n      .on('broadcast', { event: EVENT_NAME }, (data: { payload: CursorEventPayload }) => {\n        const { user } = data.payload\n        // Don't render your own cursor\n        if (user.id === userId) return\n\n        setCursors((prev) => {\n          if (prev[userId]) {\n            delete prev[userId]\n          }\n\n          return {\n            ...prev,\n            [user.id]: data.payload,\n          }\n        })\n      })\n      .subscribe(async (status) => {\n        if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n          await channel.track({ key: userId })\n          channelRef.current = channel\n        } else {\n          setCursors({})\n          channelRef.current = null\n        }\n      })\n\n    return () => {\n      channel.unsubscribe()\n      channelRef.current = null\n    }\n  }, [])\n\n  useEffect(() => {\n    // Add event listener for mousemove\n    window.addEventListener('mousemove', handleMouseMove)\n\n    // Cleanup on unmount\n    return () => {\n      window.removeEventListener('mousemove', handleMouseMove)\n    }\n  }, [handleMouseMove])\n\n  return { cursors }\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"
}