import { createElement, type ReactNode, type MouseEvent, type TouchEvent, type CSSProperties, } from 'react'; import { type InstantReactRoom } from '@instantdb/react-common'; import type { RoomSchemaShape } from '@instantdb/core'; export function Cursors< RoomSchema extends RoomSchemaShape, RoomType extends string & keyof RoomSchema, >({ as = 'div', spaceId: _spaceId, room, className, style, userCursorColor, children, renderCursor, propagate, zIndex, }: { spaceId?: string; room: InstantReactRoom; style?: React.CSSProperties; userCursorColor?: string; as?: any; className?: string; children?: ReactNode; renderCursor?: (props: { color: string; presence: RoomSchema[RoomType]['presence']; }) => ReactNode; propagate?: boolean; zIndex?: number; }) { const spaceId = _spaceId || `cursors-space-default--${String(room.type)}-${room.id}`; const cursorsPresence = room.usePresence({ keys: [spaceId] as (keyof RoomSchema[RoomType]['presence'])[], }); const fullPresence = room._core._reactor.getPresence(room.type, room.id); function publishCursor( rect: DOMRect, touch: { clientX: number; clientY: number }, ) { const x = touch.clientX; const y = touch.clientY; const xPercent = ((x - rect.left) / rect.width) * 100; const yPercent = ((y - rect.top) / rect.height) * 100; cursorsPresence.publishPresence({ [spaceId]: { x, y, xPercent, yPercent, color: userCursorColor, }, } as RoomSchema[RoomType]['presence']); } function onMouseMove(e: MouseEvent) { if (!propagate) { e.stopPropagation(); } const rect = e.currentTarget.getBoundingClientRect(); publishCursor(rect, e); } function onMouseOut(e: MouseEvent) { cursorsPresence.publishPresence({ [spaceId]: undefined, } as RoomSchema[RoomType]['presence']); } function onTouchMove(e: TouchEvent) { if (e.touches.length !== 1) { return; } const touch = e.touches[0]; if (touch.target instanceof Element) { if (!propagate) { e.stopPropagation(); } const rect = touch.target.getBoundingClientRect(); publishCursor(rect, touch); } } function onTouchEnd(e: TouchEvent) { cursorsPresence.publishPresence({ [spaceId]: undefined, } as RoomSchema[RoomType]['presence']); } return createElement( as, { onMouseMove, onMouseOut, onTouchMove, onTouchEnd, className, style: { position: 'relative', ...style, }, }, [ children,
{Object.entries(cursorsPresence.peers).map(([id, presence]) => { const cursor = presence[spaceId]; if (!cursor) return null; return (
{renderCursor ? ( renderCursor({ color: cursor.color, presence: fullPresence?.peers[id], }) ) : ( )}
); })}
, ], ); } function Cursor({ color }: { color: string }) { const size = 35; const fill = color || 'black'; return ( ); } const absStyles: CSSProperties = { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, }; const inertStyles: CSSProperties = { overflow: 'hidden', pointerEvents: 'none', userSelect: 'none', }; const defaultZ = 99999;