npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@spin-wheel/widget

v1.2.1

Published

Readme

@spin-wheel/widget

High-level spinning wheel widget for the web. Combines the @spin-wheel/core engine with a @spin-wheel/renderer (Canvas or SVG) into a single drop-in component. Mount it, spin it, collect the result.

Install

npm install @spin-wheel/widget

Peer dependencies @spin-wheel/core and @spin-wheel/renderer are installed automatically (npm 7+).

Quick Start

import { SpinWheelWidget } from '@spin-wheel/widget';

const widget = SpinWheelWidget.create('#wheel', {
  segments: [
    { id: '1', label: '🍕 Pizza', weight: 2 },
    { id: '2', label: '🍔 Burger', weight: 1 },
    { id: '3', label: '🌮 Tacos', weight: 3 },
  ],
});

const result = await widget.spin();
console.log(result.segment.label); // e.g. "🌮 Tacos"

When to Use This Package

  • Bundler-based apps (Vite, webpack, Rollup, etc.)
  • React / Vue / Svelte integration (see examples below)
  • You want a complete widget with animation out of the box

For CDN / <script> tag usage without a bundler, use @spin-wheel/embed. For headless / server-side logic only, use @spin-wheel/core.


API Reference

SpinWheelWidget.create(target, config): SpinWheelWidget

Static factory method (the constructor is private).

const widget = SpinWheelWidget.create('#wheel', {
  segments: [...],
  renderer: 'canvas',
  durationMs: 4000,
});

| Parameter | Type | Description | | --------- | ----------------------- | --------------------------------- | | target | HTMLElement \| string | Container element or CSS selector | | config | SpinWheelWidgetConfig | Configuration object |

Throws if the element is not found.


Configuration

interface SpinWheelWidgetConfig {
  readonly segments: readonly WheelSegment[];
  readonly renderer?: 'canvas' | 'svg';
  readonly durationMs?: number;
  readonly minSpins?: number;
  readonly maxSpins?: number;
  readonly seed?: string;
  readonly onFinish?: (result: SpinResult) => void;
  readonly onStateChange?: (state: WheelState) => void;
}

| Option | Type | Default | Description | | --------------- | ------------------- | ------------ | ------------------------------------------------------ | | segments | WheelSegment[] | required | At least one segment | | renderer | 'canvas' \| 'svg' | 'canvas' | Rendering backend | | durationMs | number | 4000 | Animation duration in milliseconds | | minSpins | number | 4 | Minimum full rotations before landing | | maxSpins | number | 8 | Maximum full rotations before landing | | seed | string | undefined | Seed for deterministic RNG (same seed → same sequence) | | onFinish | (result) => void | — | Callback when spin animation completes | | onStateChange | (state) => void | — | Callback on state transitions |

Segment

interface WheelSegment {
  readonly id: string;        // unique identifier
  readonly label: string;     // display text on the wheel
  readonly weight?: number;   // relative probability (default: 1)
  readonly data?: unknown;    // arbitrary payload, available on result.segment.data
}

Methods

widget.spin(): Promise<SpinResult>

Trigger a spin. The winning segment is determined before the animation starts (via the core engine). The returned promise resolves after the animation completes.

const result = await widget.spin();
console.log(result.index);          // 0-based winning index
console.log(result.segment.label);  // winning label
console.log(result.segment.data);   // your custom payload
console.log(result.finalAngle);     // final rotation in degrees

Throws if:

  • The widget is destroyed
  • A spin is already in progress

Lifecycle:

  1. State → spinning (fires onStateChange)
  2. Engine computes result (synchronous)
  3. Renderer animates to final angle (async, cubic ease-out)
  4. onFinish(result) callback fires
  5. State → finishedidle (fires onStateChange twice)
  6. Promise resolves with SpinResult

widget.setSegments(segments: WheelSegment[]): void

Replace segments at runtime. Resets the wheel angle to 0.

widget.setSegments([
  { id: 'new1', label: 'New A' },
  { id: 'new2', label: 'New B' },
]);

widget.reset(): void

Reset to initial state — engine back to idle, wheel angle to 0.

widget.destroy(): void

Tear down the widget. Removes DOM elements, cancels any running animation. Idempotent — safe to call multiple times.

Getters

| Getter | Type | Description | | -------------------- | --------- | -------------------------------------------- | | widget.isSpinning | boolean | true while a spin animation is in progress | | widget.isDestroyed | boolean | true after destroy() has been called |


Collecting Results

Four ways to get the spin result:

1. Await the promise (recommended)

const result = await widget.spin();

2. onFinish callback

SpinWheelWidget.create('#wheel', {
  segments,
  onFinish(result) {
    console.log('Winner:', result.segment.label);
  },
});

3. onStateChange callback

SpinWheelWidget.create('#wheel', {
  segments,
  onStateChange(state) {
    if (state === 'finished') {
      // spin just completed
    }
  },
});

4. Engine-level (via core)

Use @spin-wheel/core directly if you need synchronous, headless access.

SpinResult

interface SpinResult {
  readonly index: number;         // 0-based winning segment index
  readonly segment: WheelSegment; // frozen copy of the winning segment
  readonly finalAngle: number;    // total rotation in degrees
}

Framework Integration

React

import { SpinWheelWidget, type SpinResult } from '@spin-wheel/widget';
import { useEffect, useRef } from 'react';

interface Props {
  segments: { id: string; label: string; weight?: number }[];
  onResult?: (result: SpinResult) => void;
}

export function SpinWheel({ segments, onResult }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const widgetRef = useRef<SpinWheelWidget | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const w = SpinWheelWidget.create(containerRef.current, {
      segments,
      onFinish: onResult,
    });
    widgetRef.current = w;
    return () => w.destroy();
  }, [segments]);

  return (
    <div>
      <div ref={containerRef} style={{ width: 300, height: 300 }} />
      <button onClick={() => widgetRef.current?.spin()}>Spin!</button>
    </div>
  );
}

Vue

<script setup lang="ts">
import { SpinWheelWidget, type SpinResult } from '@spin-wheel/widget';
import { onMounted, onUnmounted, ref } from 'vue';

const props = defineProps<{
  segments: { id: string; label: string; weight?: number }[];
}>();
const emit = defineEmits<{ result: [result: SpinResult] }>();

const container = ref<HTMLElement>();
let widget: SpinWheelWidget | null = null;

onMounted(() => {
  if (!container.value) return;
  widget = SpinWheelWidget.create(container.value, {
    segments: props.segments,
    onFinish: (r) => emit('result', r),
  });
});
onUnmounted(() => widget?.destroy());
</script>

<template>
  <div>
    <div ref="container" style="width: 300px; height: 300px" />
    <button @click="widget?.spin()">Spin!</button>
  </div>
</template>

Svelte

<script lang="ts">
  import { SpinWheelWidget } from '@spin-wheel/widget';
  import { onMount, onDestroy, createEventDispatcher } from 'svelte';

  export let segments: { id: string; label: string; weight?: number }[];

  const dispatch = createEventDispatcher();
  let container: HTMLElement;
  let widget: SpinWheelWidget | null = null;

  onMount(() => {
    widget = SpinWheelWidget.create(container, {
      segments,
      onFinish: (result) => dispatch('result', result),
    });
  });
  onDestroy(() => widget?.destroy());
</script>

<div>
  <div bind:this={container} style="width: 300px; height: 300px" />
  <button on:click={() => widget?.spin()}>Spin!</button>
</div>

Styling

The widget auto-injects minimal CSS on creation:

.sw-container { position: relative; display: inline-block; }
.sw-container canvas,
.sw-container svg { display: block; }

The container element receives the sw-container CSS class. You can override styles normally:

.sw-container {
  border-radius: 50%;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}

License

MIT