{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-cursor-vue",
  "title": "Realtime Cursor for Vue and Supabase",
  "description": "Component which renders realtime cursors from other users in a room.",
  "dependencies": [
    "@supabase/supabase-js@latest",
    "@vueuse/core",
    "lucide-vue-next"
  ],
  "files": [
    {
      "path": "registry/default/realtime-cursor/vue/components/cursor.vue",
      "content": "<script setup lang=\"ts\">\nimport { MousePointer2 } from 'lucide-vue-next'\nimport type { CSSProperties } from 'vue'\n// @ts-ignore\nimport { cn } from '@/lib/utils'\n\ndefineProps<{\n  className?: string\n  style?: CSSProperties\n  color: string\n  name: string\n}>()\n</script>\n\n<template>\n  <div\n    :class=\"cn('pointer-events-none', className)\"\n    :style=\"style\"\n  >\n    <MousePointer2\n      :color=\"color\"\n      :fill=\"color\"\n      :size=\"30\"\n    />\n\n    <div\n      class=\"mt-1 px-2 py-1 rounded text-xs font-bold text-white text-center\"\n      :style=\"{ backgroundColor: color }\"\n    >\n      {{ name }}\n    </div>\n  </div>\n</template>\n",
      "type": "registry:component",
      "target": "components/cursor.vue"
    },
    {
      "path": "registry/default/realtime-cursor/vue/components/realtime-cursors.vue",
      "content": "<script setup lang=\"ts\">\n  // @ts-ignore\nimport { Cursor } from '@/components/cursor'\n// @ts-ignore\nimport { useRealtimeCursors } from '@/composables/useRealtimeCursors'\n\nconst THROTTLE_MS = 50\n\nconst props = defineProps<{\n  roomName: string\n  username: string\n}>()\n\nconst { cursors } = useRealtimeCursors({\n  roomName: props.roomName,\n  username: props.username,\n  throttleMs: THROTTLE_MS,\n})\n</script>\n\n<template>\n  <div>\n    <Cursor\n      v-for=\"(cursor, id) in cursors\"\n      :key=\"id\"\n      className=\"fixed transition-transform ease-in-out z-50\"\n      :style=\"{\n        transitionDuration: '20ms',\n        top: '0px',\n        left: '0px',\n        transform: `translate(${cursor.position.x}px, ${cursor.position.y}px)`,\n      }\"\n      :color=\"cursor.color\"\n      :name=\"cursor.user.name\"\n    />\n  </div>\n</template>\n",
      "type": "registry:component",
      "target": "components/realtime-cursors.vue"
    },
    {
      "path": "registry/default/realtime-cursor/vue/composables/useRealtimeCursors.ts",
      "content": "import { REALTIME_SUBSCRIBE_STATES, type RealtimeChannel } from '@supabase/supabase-js'\nimport { onMounted, onUnmounted, reactive, ref } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\n/**\n * Throttle a callback to a certain delay.\n * It will only call the callback if the delay has passed,\n * using the arguments from the last call.\n */\nfunction useThrottleCallback<Params extends unknown[]>(\n  callback: (...args: Params) => void,\n  delay: number\n) {\n  let lastCall = 0\n  let timeout: ReturnType<typeof setTimeout> | null = null\n\n  const run = (...args: Params) => {\n    const now = Date.now()\n    const remainingTime = delay - (now - lastCall)\n\n    if (remainingTime <= 0) {\n      if (timeout) {\n        clearTimeout(timeout)\n        timeout = null\n      }\n      lastCall = now\n      callback(...args)\n    } else if (!timeout) {\n      timeout = setTimeout(() => {\n        lastCall = Date.now()\n        timeout = null\n        callback(...args)\n      }, remainingTime)\n    }\n  }\n\n  const cancel = () => {\n    if (timeout) {\n      clearTimeout(timeout)\n      timeout = null\n    }\n  }\n\n  return { run, cancel }\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\nexport type CursorEventPayload = {\n  position: { x: number; y: number }\n  user: { id: number; name: string }\n  color: string\n  timestamp: number\n}\n\nexport function useRealtimeCursors({\n  roomName,\n  username,\n  throttleMs,\n}: {\n  roomName: string\n  username: string\n  throttleMs: number\n}) {\n  const color = generateRandomColor()\n  const userId = generateRandomNumber()\n\n  const cursors = reactive<Record<string, CursorEventPayload>>({})\n  const cursorPayload = ref<CursorEventPayload | null>(null)\n  const channelRef = ref<RealtimeChannel | null>(null)\n\n  const sendCursor = (event: MouseEvent) => {\n    const payload: CursorEventPayload = {\n      position: {\n        x: event.clientX,\n        y: event.clientY,\n      },\n      user: {\n        id: userId,\n        name: username,\n      },\n      color,\n      timestamp: Date.now(),\n    }\n\n    cursorPayload.value = payload\n\n    channelRef.value?.send({\n      type: 'broadcast',\n      event: EVENT_NAME,\n      payload,\n    })\n  }\n\n  const { run: handleMouseMove, cancel: cancelThrottle } = useThrottleCallback(\n    sendCursor,\n    throttleMs\n  )\n\n  onMounted(() => {\n    const channel = supabase.channel(roomName)\n\n    channel\n      .on('system', {}, (payload: CursorEventPayload) => {\n        console.error('Realtime system error:', payload)\n\n        // Defensive cleanup\n        Object.keys(cursors).forEach((k) => delete cursors[k])\n        channelRef.value = null\n      })\n      .on(\n        'presence',\n        { event: 'leave' },\n        ({ leftPresences }: { leftPresences: Array<{ key: string }> }) => {\n          leftPresences.forEach(({ key }) => {\n            delete cursors[key]\n          })\n        }\n      )\n      .on('presence', { event: 'join' }, () => {\n        if (!cursorPayload.value) return\n\n        channelRef.value?.send({\n          type: 'broadcast',\n          event: EVENT_NAME,\n          payload: cursorPayload.value,\n        })\n      })\n      .on('broadcast', { event: EVENT_NAME }, ({ payload }: { payload: CursorEventPayload }) => {\n        if (payload.user.id === userId) return\n\n        cursors[payload.user.id] = payload\n      })\n      .subscribe(async (status: REALTIME_SUBSCRIBE_STATES) => {\n        if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {\n          try {\n            await channel.track({ key: userId })\n            channelRef.value = channel\n          } catch (err) {\n            console.error('Failed to track presence for current user:', err)\n            channelRef.value = null\n          }\n        } else {\n          Object.keys(cursors).forEach((k) => delete cursors[k])\n          channelRef.value = null\n        }\n      })\n\n    window.addEventListener('mousemove', handleMouseMove)\n  })\n\n  onUnmounted(() => {\n    window.removeEventListener('mousemove', handleMouseMove)\n\n    cancelThrottle()\n\n    if (channelRef.value) {\n      channelRef.value.unsubscribe()\n      channelRef.value = null\n    }\n\n    Object.keys(cursors).forEach((k) => delete cursors[k])\n  })\n\n  return { cursors }\n}\n",
      "type": "registry:component",
      "target": "composables/useRealtimeCursors.ts"
    }
  ],
  "type": "registry:component"
}