{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A DrawingCursor component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport type { ReactNode, Ref } from \"react\";\nimport { useEffect, useImperativeHandle, useRef, useState } from \"react\";\n\nexport type DrawingCursorHandle = {\n  clear: () => void;\n  toDataURL: (type?: string, quality?: number) => string;\n};\n\nexport type DrawingCursorProps = {\n  blend?: GlobalCompositeOperation;\n  children?: ReactNode;\n  className?: string;\n  clearOnLeave?: boolean;\n  color?: string;\n  decay?: number;\n  lineWidth?: number;\n  paused?: boolean;\n  ref?: Ref<DrawingCursorHandle>;\n  smoothing?: number;\n  taper?: boolean;\n};\n\ntype StrokePoint = { time: number; width: number; x: number; y: number };\ntype Stroke = { points: StrokePoint[] };\n\nconst DEFAULT_COLOR = \"var(--color-brand, #6366f1)\";\nconst DEFAULT_LINE_WIDTH = 3;\nconst DEFAULT_DECAY = 800;\nconst DEFAULT_SMOOTHING = 0.5;\nconst MAX_DEVICE_PIXEL_RATIO = 2;\nconst STROKE_BREAK_MS = 120;\nconst MIN_WIDTH_FACTOR = 0.35;\nconst SPEED_TO_WIDTH_SCALE = 0.12;\nconst MAX_POINTS_PER_STROKE = 600;\nconst MAX_STROKES = 40;\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\n\nconst getPointerCoordinates = (\n  event: PointerEvent,\n  rect: DOMRect\n): { x: number; y: number }[] => {\n  const coalesced =\n    typeof event.getCoalescedEvents === \"function\"\n      ? event.getCoalescedEvents()\n      : null;\n  const events = coalesced && coalesced.length > 0 ? coalesced : [event];\n  return events.map((coalescedEvent) => ({\n    x: coalescedEvent.clientX - rect.left,\n    y: coalescedEvent.clientY - rect.top,\n  }));\n};\n\nconst DrawingCursor = ({\n  blend = \"source-over\",\n  children,\n  className,\n  clearOnLeave = true,\n  color = DEFAULT_COLOR,\n  decay = DEFAULT_DECAY,\n  lineWidth = DEFAULT_LINE_WIDTH,\n  paused = false,\n  ref,\n  smoothing = DEFAULT_SMOOTHING,\n  taper = true,\n}: DrawingCursorProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isPointerCapable, setIsPointerCapable] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const strokesRef = useRef<Stroke[]>([]);\n  const activeStrokeRef = useRef<Stroke | null>(null);\n  const rafRef = useRef<number | null>(null);\n  const sizeRef = useRef({ height: 0, width: 0 });\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsPointerCapable(mediaQuery.matches);\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsPointerCapable(event.matches);\n    };\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const isMounted = isPointerCapable && !shouldReduceMotion;\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      clear: () => {\n        strokesRef.current = [];\n        activeStrokeRef.current = null;\n        const canvas = canvasRef.current;\n        const ctx = canvas?.getContext(\"2d\");\n        if (canvas && ctx) {\n          ctx.clearRect(0, 0, canvas.width, canvas.height);\n        }\n      },\n      toDataURL: (type?: string, quality?: number) =>\n        canvasRef.current?.toDataURL(type, quality) ?? \"\",\n    }),\n    []\n  );\n\n  useEffect(() => {\n    if (!isMounted) {\n      return;\n    }\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!(container && canvas)) {\n      return;\n    }\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) {\n      return;\n    }\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      const dpr = Math.min(\n        window.devicePixelRatio || 1,\n        MAX_DEVICE_PIXEL_RATIO\n      );\n      canvas.width = Math.max(1, Math.round(rect.width * dpr));\n      canvas.height = Math.max(1, Math.round(rect.height * dpr));\n      canvas.style.width = `${rect.width}px`;\n      canvas.style.height = `${rect.height}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      sizeRef.current = { height: rect.height, width: rect.width };\n    };\n\n    resize();\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n\n    let lastPointTime = 0;\n\n    const pushPoint = (x: number, y: number, time: number) => {\n      const isNewStroke =\n        !activeStrokeRef.current || time - lastPointTime > STROKE_BREAK_MS;\n      if (isNewStroke) {\n        const stroke: Stroke = { points: [] };\n        activeStrokeRef.current = stroke;\n        strokesRef.current.push(stroke);\n        if (strokesRef.current.length > MAX_STROKES) {\n          strokesRef.current.shift();\n        }\n      }\n      const stroke = activeStrokeRef.current;\n      if (!stroke) {\n        return;\n      }\n      const previous = stroke.points.at(-1);\n      let width = lineWidth;\n      if (taper && previous) {\n        const dt = Math.max(1, time - previous.time);\n        const distance = Math.hypot(x - previous.x, y - previous.y);\n        const speed = distance / dt;\n        const factor = clamp(\n          1 - speed * SPEED_TO_WIDTH_SCALE,\n          MIN_WIDTH_FACTOR,\n          1\n        );\n        width = lineWidth * factor;\n      }\n      stroke.points.push({ time, width, x, y });\n      if (stroke.points.length > MAX_POINTS_PER_STROKE) {\n        stroke.points.shift();\n      }\n      lastPointTime = time;\n    };\n\n    const handlePointerMove = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const coordinates = getPointerCoordinates(event, rect);\n      for (const point of coordinates) {\n        pushPoint(point.x, point.y, performance.now());\n      }\n    };\n\n    const handlePointerLeave = () => {\n      activeStrokeRef.current = null;\n      if (clearOnLeave) {\n        strokesRef.current = [];\n        ctx.clearRect(0, 0, canvas.width, canvas.height);\n      }\n    };\n\n    container.addEventListener(\"pointermove\", handlePointerMove, {\n      passive: true,\n    });\n    container.addEventListener(\"pointerleave\", handlePointerLeave, {\n      passive: true,\n    });\n\n    const drawStroke = (stroke: Stroke, now: number) => {\n      const { points } = stroke;\n      if (points.length < 2) {\n        return;\n      }\n      if (points.length === 2) {\n        const [start, end] = points;\n        const age = decay > 0 ? clamp(1 - (now - end.time) / decay, 0, 1) : 1;\n        if (age <= 0) {\n          return;\n        }\n        ctx.beginPath();\n        ctx.moveTo(start.x, start.y);\n        ctx.lineTo(end.x, end.y);\n        ctx.globalAlpha = age;\n        ctx.strokeStyle = color;\n        ctx.lineWidth = end.width;\n        ctx.stroke();\n        return;\n      }\n      for (let i = 1; i < points.length - 1; i++) {\n        const prev = points[i - 1];\n        const curr = points[i];\n        const next = points[i + 1];\n        const age = decay > 0 ? clamp(1 - (now - curr.time) / decay, 0, 1) : 1;\n        if (age <= 0) {\n          continue;\n        }\n        const midPrevX = (prev.x + curr.x) / 2;\n        const midPrevY = (prev.y + curr.y) / 2;\n        const midNextX = (curr.x + next.x) / 2;\n        const midNextY = (curr.y + next.y) / 2;\n        const startX = prev.x + (midPrevX - prev.x) * smoothing;\n        const startY = prev.y + (midPrevY - prev.y) * smoothing;\n        const endX = next.x + (midNextX - next.x) * smoothing;\n        const endY = next.y + (midNextY - next.y) * smoothing;\n        ctx.beginPath();\n        ctx.moveTo(startX, startY);\n        ctx.quadraticCurveTo(curr.x, curr.y, endX, endY);\n        ctx.globalAlpha = age;\n        ctx.strokeStyle = color;\n        ctx.lineWidth = curr.width;\n        ctx.stroke();\n      }\n    };\n\n    const draw = () => {\n      const now = performance.now();\n      const { width, height } = sizeRef.current;\n      ctx.clearRect(0, 0, width, height);\n      ctx.lineCap = \"round\";\n      ctx.lineJoin = \"round\";\n      ctx.globalCompositeOperation = blend;\n\n      for (const stroke of strokesRef.current) {\n        if (decay > 0) {\n          while (\n            stroke.points.length > 0 &&\n            now - stroke.points[0].time > decay\n          ) {\n            stroke.points.shift();\n          }\n        }\n        drawStroke(stroke, now);\n      }\n\n      strokesRef.current = strokesRef.current.filter(\n        (stroke) => stroke.points.length > 0\n      );\n\n      if (!paused) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    if (!paused) {\n      rafRef.current = requestAnimationFrame(draw);\n    }\n\n    return () => {\n      if (rafRef.current) {\n        cancelAnimationFrame(rafRef.current);\n      }\n      resizeObserver.disconnect();\n      container.removeEventListener(\"pointermove\", handlePointerMove);\n      container.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [\n    blend,\n    clearOnLeave,\n    color,\n    decay,\n    isMounted,\n    lineWidth,\n    paused,\n    smoothing,\n    taper,\n  ]);\n\n  return (\n    <div className={cn(\"relative\", className)} ref={containerRef}>\n      {children}\n      {isMounted ? (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n        >\n          <canvas className=\"h-full w-full\" ref={canvasRef} />\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\nexport default DrawingCursor;\n","path":"index.tsx","target":"components/smoothui/drawing-cursor/index.tsx","type":"registry:ui"}],"name":"drawing-cursor","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Drawing Cursor","type":"registry:ui"}