Installation

Install a variant with the shadcn CLI, or copy the files by hand. What lands where, and why the provider is a separate item.

With the shadcn CLI

Register the namespace once in your components.json:

{
  "registries": {
    "@pointerui": "https://pointerui.dev/r/{name}.json"
  }
}

Then add any variant:

npx shadcn@latest add @pointerui/dot-ring

That one command writes three things: the variant, the provider it needs, and the engine the provider needs. GSAP and @gsap/react are installed as dependencies automatically. If you skip the namespace step, the full URL works on its own:

npx shadcn@latest add https://pointerui.dev/r/dot-ring.json

What lands where

ItemFilesLocation
engine8lib/pointerui/engine/
provider6components/pointerui/provider/
a variant2components/pointerui/<slug>/

Paths follow the aliases in your own components.json, so a project that puts components somewhere else gets them there.

Adding a second variant reuses what is already on disk — the engine and provider are separate registry items precisely so that six variants do not mean six copies of the engine.

Wire up the provider

CursorProvider creates the engine and the portal host. It goes once, high in the tree, and the variant goes inside it:

import { CursorProvider } from "@/components/pointerui/provider";
import { DotRing } from "@/components/pointerui/dot-ring";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <CursorProvider>
          <DotRing />
          {children}
        </CursorProvider>
      </body>
    </html>
  );
}

Children render in place — the provider does not portal your app anywhere. Each variant portals its own layers to document.body, which is what keeps mix-blend-mode working for blend and keeps every layer clear of your stacking contexts.

To compose variants, mount more than one inside the same provider. They share the engine:

<CursorProvider>
  <DotRing />
  <TrailDots count={6} />
</CursorProvider>

Hiding the native cursor

The provider sets cursor: none on the document — or on the scope element in scoped mode — while a variant is mounted, and restores it on unmount. You do not need to set it yourself, and you should not: a stray cursor: none that outlives the component leaves the visitor with no pointer at all.

Manual install

Copy src/lib/engine/, src/registry/provider/ and the variant folder you want out of the repo, then fix the two import paths the CLI would have rewritten:

  • @/lib/engine → wherever you put the engine
  • ../../provider → wherever you put the provider

Everything else is relative and survives the move.

Vanilla JavaScript

The engine under lib/engine/ is plain TypeScript with GSAP as its only dependency — no React anywhere in it, enforced by a lint rule. You can drive it directly:

import { createEngine } from "./lib/pointerui/engine";

const engine = createEngine();
const el = document.createElement("div");
document.body.append(el);

const layer = engine.registerLayer(el, { duration: 0.15, ease: "power3" });
const stop = engine.onTick((state) => {
  layer.setX(state.x);
  layer.setY(state.y);
});

Call stop() and layer.destroy() when you are done — the engine removes its gsap.ticker callback once the last layer unregisters.

A packaged vanilla build is not published yet. The React components are v1; the engine being framework-agnostic from the first commit is what makes that build a packaging job later rather than a rewrite.