{"name":"cursor-player","type":"registry:block","title":"Cursor Player","description":"A free compound component for starting, pausing, resuming, and stopping cursor.js demos.","dependencies":["@cursor.js/core","lucide-react"],"registryDependencies":["button"],"files":[{"path":"registry/default/cursor-player/cursor-player.tsx","type":"registry:component","target":"@components/cursor-player.tsx","content":"'use client';\n\nimport * as React from 'react';\nimport { Pause, Play, Square } from 'lucide-react';\nimport { Slot } from 'radix-ui';\n\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { type CursorPlayerRuntime, useCursorPlayer } from './use-cursor-player';\n\ninterface CursorPlayerRootProps<TCursor extends CursorPlayerRuntime> {\n  createCursor: (anchorElement: HTMLElement) => TCursor;\n  buildSequence: (cursor: TCursor, anchorElement: HTMLElement) => void;\n  onError?: (error: unknown) => void;\n  children: React.ReactNode;\n}\n\ninterface CursorPlayerPartProps extends React.HTMLAttributes<HTMLDivElement> {\n  asChild?: boolean;\n}\n\ntype CursorPlayerHotspot = 'top-left' | 'center' | { x: number; y: number };\n\ninterface CursorPlayerCursorProps extends CursorPlayerPartProps {\n  hotspot?: CursorPlayerHotspot;\n}\n\ninterface CursorPlayerButtonProps extends React.ComponentProps<typeof Button> {\n  children?: React.ReactNode;\n}\n\ninterface CursorPlayerIconProps extends React.HTMLAttributes<HTMLElement> {\n  asChild?: boolean;\n  children?: React.ReactNode;\n}\n\nconst CursorPlayerContext = React.createContext<CursorPlayerContextValue | null>(null);\n\nfunction useCursorPlayerContext() {\n  const value = React.useContext(CursorPlayerContext);\n\n  if (!value) {\n    throw new Error('CursorPlayer components must be used inside <CursorPlayer>.');\n  }\n\n  return value;\n}\n\nfunction isPlayVisible(state: CursorPlayerContextValue['state']) {\n  return state !== 'running';\n}\n\nfunction isPauseVisible(state: CursorPlayerContextValue['state']) {\n  return state === 'running';\n}\n\ntype CursorPlayerContextValue = ReturnType<typeof useCursorPlayer>;\ntype CursorPlayerRootComponent = <TCursor extends CursorPlayerRuntime>(\n  props: CursorPlayerRootProps<TCursor>,\n) => React.ReactElement;\n\nconst CursorPlayerRoot: CursorPlayerRootComponent = ({\n  createCursor,\n  buildSequence,\n  onError,\n  children,\n}) => {\n  const controls = useCursorPlayer({ createCursor, buildSequence, onError });\n\n  return <CursorPlayerContext.Provider value={controls}>{children}</CursorPlayerContext.Provider>;\n};\n\nfunction getHotspotPosition(hotspot: CursorPlayerHotspot | undefined): {\n  left: string;\n  top: string;\n} {\n  if (hotspot === 'center') {\n    return { left: '50%', top: '50%' };\n  }\n\n  if (hotspot && typeof hotspot === 'object') {\n    return { left: `${hotspot.x}px`, top: `${hotspot.y}px` };\n  }\n\n  return { left: '0px', top: '0px' };\n}\n\nfunction CursorPlayerCursor({\n  className,\n  asChild = false,\n  children,\n  hotspot = 'top-left',\n  ...props\n}: CursorPlayerCursorProps) {\n  const { setAnchorElement, setAnchorFrameElement, setPreviewElement } = useCursorPlayerContext();\n  const Comp = asChild ? Slot.Root : 'span';\n  const hotspotPosition = getHotspotPosition(hotspot);\n\n  return (\n    <Comp\n      ref={setAnchorFrameElement}\n      className={cn('relative inline-flex size-4', className)}\n      aria-hidden=\"true\"\n      {...props}\n    >\n      <span\n        ref={setAnchorElement}\n        className=\"absolute h-px w-px\"\n        style={{\n          left: hotspotPosition.left,\n          top: hotspotPosition.top,\n          transform: hotspot === 'center' ? 'translate(-50%, -50%)' : undefined,\n        }}\n      />\n      <span\n        ref={setPreviewElement}\n        className=\"pointer-events-none absolute\"\n        style={{\n          left: hotspotPosition.left,\n          top: hotspotPosition.top,\n          transform: hotspot === 'center' ? 'translate(-50%, -50%)' : undefined,\n        }}\n      />\n      {children}\n    </Comp>\n  );\n}\n\nfunction CursorPlayerPlayPause({ onClick, ...props }: CursorPlayerButtonProps) {\n  const { state, start, pause } = useCursorPlayerContext();\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      data-state={state}\n      onClick={(event) => {\n        onClick?.(event);\n\n        if (event.defaultPrevented) {\n          return;\n        }\n\n        if (state === 'running') {\n          pause();\n          return;\n        }\n\n        void start();\n      }}\n      {...props}\n    >\n      {props.children}\n    </Button>\n  );\n}\n\nfunction CursorPlayerPlayIcon({\n  className,\n  children,\n  asChild = false,\n  ...props\n}: CursorPlayerIconProps) {\n  const { state } = useCursorPlayerContext();\n  const Comp = asChild ? Slot.Root : 'span';\n\n  if (!isPlayVisible(state)) {\n    return null;\n  }\n\n  const content = children ?? <Play className=\"size-4\" />;\n\n  return (\n    <Comp\n      className={cn('pointer-events-none inline-flex items-center justify-center', className)}\n      aria-hidden=\"true\"\n      {...props}\n    >\n      {content}\n    </Comp>\n  );\n}\n\nfunction CursorPlayerPauseIcon({\n  className,\n  children,\n  asChild = false,\n  ...props\n}: CursorPlayerIconProps) {\n  const { state } = useCursorPlayerContext();\n  const Comp = asChild ? Slot.Root : 'span';\n\n  if (!isPauseVisible(state)) {\n    return null;\n  }\n\n  const content = children ?? <Pause className=\"size-4\" />;\n\n  return (\n    <Comp\n      className={cn('pointer-events-none inline-flex items-center justify-center', className)}\n      aria-hidden=\"true\"\n      {...props}\n    >\n      {content}\n    </Comp>\n  );\n}\n\nfunction CursorPlayerStopButton({\n  className,\n  children,\n  asChild = false,\n  onClick,\n  ...props\n}: CursorPlayerButtonProps) {\n  const { state, stop, canStop } = useCursorPlayerContext();\n  const content = children ?? (\n    <>\n      <Square className=\"size-4\" />\n      Stop\n    </>\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      className={className}\n      asChild={asChild}\n      data-state={state}\n      onClick={(event) => {\n        onClick?.(event);\n\n        if (event.defaultPrevented) {\n          return;\n        }\n\n        stop();\n      }}\n      disabled={!canStop}\n      {...props}\n    >\n      {content}\n    </Button>\n  );\n}\n\nfunction CursorPlayerStatus({\n  children,\n}: {\n  children: (controls: CursorPlayerContextValue) => React.ReactNode;\n}) {\n  const controls = useCursorPlayerContext();\n\n  return <>{children(controls)}</>;\n}\n\nexport const CursorPlayer = Object.assign(CursorPlayerRoot, {\n  Cursor: CursorPlayerCursor,\n  PlayPause: CursorPlayerPlayPause,\n  PlayIcon: CursorPlayerPlayIcon,\n  PauseIcon: CursorPlayerPauseIcon,\n  StopButton: CursorPlayerStopButton,\n  Status: CursorPlayerStatus,\n});\n"},{"path":"registry/default/cursor-player/use-cursor-player.ts","type":"registry:hook","target":"@hooks/use-cursor-player.ts","content":"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type CursorPlayerState = \"idle\" | \"running\" | \"paused\" | \"complete\" | \"error\";\n\nexport interface CursorPlayerRuntime extends PromiseLike<void> {\n  cursor: {\n    el: HTMLElement;\n    x: number;\n    y: number;\n    scale: number;\n    setSize: (scale: number) => void;\n    moveTo: (pageX: number, pageY: number) => void;\n  };\n  on(event: string, callback: () => void): this;\n  off(event: string, callback: () => void): this;\n  pause(): this;\n  play(): this;\n  destroy(): void;\n}\n\nexport type CursorPlayerInstance = CursorPlayerRuntime;\n\nexport interface UseCursorPlayerOptions<TCursor extends CursorPlayerRuntime> {\n  createCursor: (anchorElement: HTMLElement) => TCursor;\n  buildSequence: (cursor: TCursor, anchorElement: HTMLElement) => void;\n  onError?: (error: unknown) => void;\n}\n\nexport interface CursorPlayerControls {\n  state: CursorPlayerState;\n  canStart: boolean;\n  canPause: boolean;\n  canStop: boolean;\n  start: () => Promise<void>;\n  pause: () => void;\n  stop: () => void;\n  setAnchorElement: (element: HTMLElement | null) => void;\n  setAnchorFrameElement: (element: HTMLElement | null) => void;\n  setPreviewElement: (element: HTMLElement | null) => void;\n}\n\ninterface CursorBinding<TCursor extends CursorPlayerRuntime> {\n  cursor: TCursor;\n  anchorElement: HTMLElement;\n  anchorFrameElement: HTMLElement;\n  activeScale: number;\n  lastAnchorPosition: { x: number; y: number } | null;\n  previewCursorElement: HTMLElement | null;\n  onPause: () => void;\n  onPlay: () => void;\n  onDestroy: () => void;\n}\n\nfunction resolveVisualElement(cursorElement: HTMLElement) {\n  return (cursorElement.querySelector(\".cursor-theme-wrapper\") as HTMLElement | null) ?? cursorElement;\n}\n\nfunction parseNumericAttribute(value: string | null) {\n  if (!value) {\n    return null;\n  }\n\n  const parsed = Number.parseFloat(value);\n  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;\n}\n\nfunction resolveVisualBaseSize(visualElement: HTMLElement) {\n  const svgElement = visualElement.querySelector(\"svg\");\n\n  if (svgElement instanceof SVGSVGElement) {\n    const viewBox = svgElement.viewBox.baseVal;\n\n    if (viewBox && viewBox.width > 0 && viewBox.height > 0) {\n      return {\n        width: viewBox.width,\n        height: viewBox.height,\n      };\n    }\n\n    const width = parseNumericAttribute(svgElement.getAttribute(\"width\"));\n    const height = parseNumericAttribute(svgElement.getAttribute(\"height\"));\n\n    if (width && height) {\n      return { width, height };\n    }\n  }\n\n  const width = visualElement.offsetWidth;\n  const height = visualElement.offsetHeight;\n\n  if (width > 0 && height > 0) {\n    return { width, height };\n  }\n\n  return null;\n}\n\nfunction syncCursorScale(cursor: CursorPlayerRuntime, anchorFrameElement: HTMLElement) {\n  const anchorRect = anchorFrameElement.getBoundingClientRect();\n  const visualElement = resolveVisualElement(cursor.cursor.el);\n  const visualBaseSize = resolveVisualBaseSize(visualElement);\n\n  if (\n    anchorRect.width <= 0 ||\n    anchorRect.height <= 0 ||\n    !visualBaseSize\n  ) {\n    return false;\n  }\n\n  const scale = Math.min(\n    anchorRect.width / visualBaseSize.width,\n    anchorRect.height / visualBaseSize.height,\n  );\n  cursor.cursor.setSize(scale);\n  return scale;\n}\n\nfunction restoreCursorScale(cursor: CursorPlayerRuntime, scale: number) {\n  cursor.cursor.setSize(scale);\n}\n\nfunction setCursorVisibility(cursor: CursorPlayerRuntime, isVisible: boolean) {\n  cursor.cursor.el.style.visibility = isVisible ? \"visible\" : \"hidden\";\n  cursor.cursor.el.style.opacity = isVisible ? \"1\" : \"0\";\n}\n\nfunction createPreviewCursorElement(cursor: CursorPlayerRuntime) {\n  const previewCursorElement = cursor.cursor.el.cloneNode(true) as HTMLElement;\n\n  previewCursorElement.style.position = \"absolute\";\n  previewCursorElement.style.top = \"0\";\n  previewCursorElement.style.left = \"0\";\n  previewCursorElement.style.visibility = \"visible\";\n  previewCursorElement.style.opacity = \"1\";\n  previewCursorElement.style.pointerEvents = \"none\";\n  previewCursorElement.style.zIndex = \"0\";\n  previewCursorElement.style.transition = \"none\";\n  previewCursorElement.style.transform = `scale(${cursor.cursor.scale})`;\n\n  return previewCursorElement;\n}\n\nfunction syncPreviewCursorScale(previewCursorElement: HTMLElement | null, scale: number | false) {\n  if (!previewCursorElement || scale === false) {\n    return;\n  }\n\n  previewCursorElement.style.transform = `scale(${scale})`;\n}\n\nfunction resolveAnchorPosition(anchorElement: HTMLElement) {\n  const rect = anchorElement.getBoundingClientRect();\n  return {\n    x: rect.left + window.scrollX + rect.width / 2,\n    y: rect.top + window.scrollY + rect.height / 2,\n  };\n}\n\nfunction syncCursorPosition(binding: CursorBinding<CursorPlayerRuntime>) {\n  const nextAnchorPosition = resolveAnchorPosition(binding.anchorElement);\n  const previousAnchorPosition = binding.lastAnchorPosition;\n\n  binding.lastAnchorPosition = nextAnchorPosition;\n\n  if (!previousAnchorPosition) {\n    return false;\n  }\n\n  const deltaX = nextAnchorPosition.x - previousAnchorPosition.x;\n  const deltaY = nextAnchorPosition.y - previousAnchorPosition.y;\n\n  if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) {\n    return false;\n  }\n\n  binding.cursor.cursor.moveTo(binding.cursor.cursor.x + deltaX, binding.cursor.cursor.y + deltaY);\n  return true;\n}\n\nfunction isSameBinding<TCursor extends CursorPlayerRuntime>(\n  currentBinding: CursorBinding<TCursor> | null,\n  binding: CursorBinding<TCursor>,\n) {\n  return currentBinding?.cursor === binding.cursor;\n}\n\nfunction scheduleScaleSync(\n  syncScale: () => number | false,\n  isActive: () => boolean,\n  attempts = 3,\n) {\n  const run = (remaining: number) => {\n    if (!isActive()) {\n      return;\n    }\n\n    const didSync = syncScale();\n\n    if (typeof window === \"undefined\" || !window.requestAnimationFrame) {\n      return;\n    }\n\n    if (!didSync && remaining > 0) {\n      window.requestAnimationFrame(() => run(remaining - 1));\n    }\n  };\n\n  run(attempts);\n}\n\nexport function useCursorPlayer<TCursor extends CursorPlayerRuntime>({\n  createCursor,\n  buildSequence,\n  onError,\n}: UseCursorPlayerOptions<TCursor>): CursorPlayerControls {\n  const [state, setState] = useState<CursorPlayerState>(\"idle\");\n  const stateRef = useRef<CursorPlayerState>(\"idle\");\n  const anchorElementRef = useRef<HTMLElement | null>(null);\n  const anchorFrameElementRef = useRef<HTMLElement | null>(null);\n  const previewElementRef = useRef<HTMLElement | null>(null);\n  const bindingRef = useRef<CursorBinding<TCursor> | null>(null);\n  const runIdRef = useRef(0);\n  const resizeObserverRef = useRef<ResizeObserver | null>(null);\n  const animationFrameRef = useRef<number | null>(null);\n\n  const setPlayerState = useCallback((nextState: CursorPlayerState) => {\n    stateRef.current = nextState;\n    setState(nextState);\n  }, []);\n\n  const canApplyPreviewScale = useCallback(() => {\n    const currentState = stateRef.current;\n    return currentState === \"idle\" || currentState === \"complete\" || currentState === \"error\";\n  }, []);\n\n  const clearBinding = (destroyCursor: boolean) => {\n    const binding = bindingRef.current;\n    if (!binding) return;\n\n    binding.cursor.off(\"pause\", binding.onPause);\n    binding.cursor.off(\"play\", binding.onPlay);\n    binding.cursor.off(\"destroy\", binding.onDestroy);\n\n    if (destroyCursor) {\n      binding.cursor.destroy();\n    }\n\n    binding.previewCursorElement?.remove();\n\n    bindingRef.current = null;\n  };\n\n  const cleanupObservers = () => {\n    resizeObserverRef.current?.disconnect();\n    resizeObserverRef.current = null;\n\n    if (animationFrameRef.current !== null && typeof window !== \"undefined\" && window.cancelAnimationFrame) {\n      window.cancelAnimationFrame(animationFrameRef.current);\n      animationFrameRef.current = null;\n    }\n  };\n\n  const initializeCursor = useCallback((nextAnchorElement: HTMLElement, nextAnchorFrameElement: HTMLElement) => {\n    anchorElementRef.current = nextAnchorElement;\n    anchorFrameElementRef.current = nextAnchorFrameElement;\n    runIdRef.current += 1;\n    const runId = runIdRef.current;\n\n    cleanupObservers();\n    clearBinding(true);\n\n    const cursor = createCursor(nextAnchorElement);\n\n    const onPause = () => setPlayerState(\"paused\");\n    const onPlay = () => setPlayerState(\"running\");\n    const onDestroy = () => {\n      if (runIdRef.current === runId) {\n        setPlayerState(\"idle\");\n      }\n    };\n\n    cursor.on(\"pause\", onPause);\n    cursor.on(\"play\", onPlay);\n    cursor.on(\"destroy\", onDestroy);\n\n    bindingRef.current = {\n      cursor,\n      anchorElement: nextAnchorElement,\n      anchorFrameElement: nextAnchorFrameElement,\n      activeScale: cursor.cursor.scale,\n      lastAnchorPosition: resolveAnchorPosition(nextAnchorElement),\n      previewCursorElement: null,\n      onPause,\n      onPlay,\n      onDestroy,\n    };\n\n    const previewElement = previewElementRef.current;\n    if (previewElement) {\n      const previewCursorElement = createPreviewCursorElement(cursor);\n      previewElement.replaceChildren(previewCursorElement);\n      bindingRef.current.previewCursorElement = previewCursorElement;\n    }\n\n    const syncScale = () => {\n      const scale = syncCursorScale(cursor, nextAnchorFrameElement);\n\n      if (bindingRef.current?.cursor === cursor) {\n        syncPreviewCursorScale(bindingRef.current.previewCursorElement, scale);\n      }\n\n      return scale;\n    };\n\n    scheduleScaleSync(\n      syncScale,\n      () => bindingRef.current?.cursor === cursor && canApplyPreviewScale(),\n    );\n    syncScale();\n\n    if (typeof ResizeObserver !== \"undefined\") {\n      const observer = new ResizeObserver(() => {\n        if (bindingRef.current?.cursor === cursor && canApplyPreviewScale()) {\n          syncScale();\n        }\n      });\n\n      observer.observe(nextAnchorFrameElement);\n      resizeObserverRef.current = observer;\n    }\n\n    if (typeof window !== \"undefined\" && window.requestAnimationFrame) {\n      const trackAnchorPosition = () => {\n        const binding = bindingRef.current;\n\n        if (!binding || binding.cursor !== cursor) {\n          animationFrameRef.current = null;\n          return;\n        }\n\n        syncCursorPosition(binding);\n        animationFrameRef.current = window.requestAnimationFrame(trackAnchorPosition);\n      };\n\n      animationFrameRef.current = window.requestAnimationFrame(trackAnchorPosition);\n    }\n\n    setCursorVisibility(cursor, false);\n\n    setPlayerState(\"idle\");\n  }, [canApplyPreviewScale, createCursor, setPlayerState]);\n\n  useEffect(() => {\n    return () => {\n      cleanupObservers();\n      clearBinding(true);\n    };\n  }, []);\n\n  const syncBinding = useCallback(() => {\n    const anchorElement = anchorElementRef.current;\n    const anchorFrameElement = anchorFrameElementRef.current;\n\n    if (!anchorElement || !anchorFrameElement) {\n      return;\n    }\n\n    const currentBinding = bindingRef.current;\n    if (\n      currentBinding &&\n      currentBinding.anchorElement === anchorElement &&\n      currentBinding.anchorFrameElement === anchorFrameElement\n    ) {\n      if (canApplyPreviewScale()) {\n        const scale = syncCursorScale(currentBinding.cursor, anchorFrameElement);\n        syncPreviewCursorScale(currentBinding.previewCursorElement, scale);\n      }\n      return;\n    }\n\n    initializeCursor(anchorElement, anchorFrameElement);\n  }, [canApplyPreviewScale, initializeCursor]);\n\n  const setAnchorElement = useCallback((element: HTMLElement | null) => {\n    if (!element) {\n      anchorElementRef.current = null;\n      cleanupObservers();\n      clearBinding(true);\n      return;\n    }\n\n    anchorElementRef.current = element;\n    syncBinding();\n  }, [syncBinding]);\n\n  const setAnchorFrameElement = useCallback((element: HTMLElement | null) => {\n    if (!element) {\n      anchorFrameElementRef.current = null;\n      cleanupObservers();\n      clearBinding(true);\n      return;\n    }\n\n    anchorFrameElementRef.current = element;\n    syncBinding();\n  }, [syncBinding]);\n\n  const setPreviewElement = useCallback((element: HTMLElement | null) => {\n    previewElementRef.current = element;\n\n    if (!element) {\n      return;\n    }\n\n    const binding = bindingRef.current;\n    if (!binding) {\n      element.replaceChildren();\n      return;\n    }\n\n    const previewCursorElement = createPreviewCursorElement(binding.cursor);\n    element.replaceChildren(previewCursorElement);\n    binding.previewCursorElement = previewCursorElement;\n\n    if (canApplyPreviewScale()) {\n      const scale = syncCursorScale(binding.cursor, binding.anchorFrameElement);\n      syncPreviewCursorScale(previewCursorElement, scale);\n      setCursorVisibility(binding.cursor, false);\n    }\n  }, [canApplyPreviewScale]);\n\n  const pause = () => {\n    if (state !== \"running\") return;\n    bindingRef.current?.cursor.pause();\n  };\n\n  const stop = () => {\n    const binding = bindingRef.current;\n    if (!binding) return;\n\n    initializeCursor(binding.anchorElement, binding.anchorFrameElement);\n  };\n\n  const start = async () => {\n    const binding = bindingRef.current;\n    if (!binding) return;\n\n    if (state === \"running\") return;\n\n    if (state === \"paused\") {\n      binding.cursor.play();\n      return;\n    }\n\n    try {\n      setPlayerState(\"running\");\n      setCursorVisibility(binding.cursor, true);\n      restoreCursorScale(binding.cursor, binding.activeScale);\n      buildSequence(binding.cursor, binding.anchorElement);\n\n      await binding.cursor;\n\n      if (isSameBinding(bindingRef.current, binding)) {\n        initializeCursor(binding.anchorElement, binding.anchorFrameElement);\n      }\n    } catch (error) {\n      if (isSameBinding(bindingRef.current, binding)) {\n        clearBinding(true);\n        setPlayerState(\"error\");\n        initializeCursor(binding.anchorElement, binding.anchorFrameElement);\n      }\n\n      onError?.(error);\n    }\n  };\n\n  return {\n    state,\n    canStart: state !== \"running\",\n    canPause: state === \"running\",\n    canStop: state === \"running\" || state === \"paused\",\n    start,\n    pause,\n    stop,\n    setAnchorElement,\n    setAnchorFrameElement,\n    setPreviewElement,\n  };\n}\n"}]}