{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "realtime-cursor-tanstack", "type": "registry:component", "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
\n \n\n \n {name}\n
\n \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
\n {Object.keys(cursors).map((id) => (\n \n ))}\n
\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/tanstack/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 = (\n callback: (...args: Params) => Return,\n delay: number\n) => {\n const lastCall = useRef(0)\n const timeout = useRef(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>({})\n const cursorPayload = useRef(null)\n\n const channelRef = useRef(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/tanstack/lib/supabase/client.ts", "content": "/// \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/tanstack/lib/supabase/server.ts", "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\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`." }