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

rigged-macbook-3d

v0.2.0

Published

Rigged 3D MacBook Pro model with animatable hinge and screen

Readme

Animatable MacBook Pro 3D Model

Transform the MacBook in a 3D scene... animate the lid opening... play your product video on screen...

Live demo · Quick start · API · Changelog

npm License: MIT React 18+ three.js

Quick start

npm i rigged-macbook-3d three @react-three/fiber @react-three/drei
import { Macbook, MacbookStage } from 'rigged-macbook-3d';

export default function Hero() {
  return (
    <div style={{ height: 600 }}>
      <MacbookStage lighting="studio-dark">
        <Macbook open={1} screen="/demo.mp4" />
      </MacbookStage>
    </div>
  );
}

That's the whole integration: a lit stage, an open MacBook, your video on the screen. The package itself is 6.4 kB gzipped with zero runtime dependenciesthree and R3F stay peers, so it adds nothing you aren't already shipping.

Scroll to open it

<MacbookScroll> is the full pinned journey — the device rises in, the lid swings open onto your playing video, the journey holds there while the user keeps scrolling, then the device recedes and scroll hands off to the rest of the page. No GSAP, no Lenis, no scroll library at all.

import { MacbookScroll } from 'rigged-macbook-3d';

<MacbookScroll
  height="600vh"
  screen={{ src: '/demo.webm', fallbackSrc: '/demo.mp4' }}
/>

Every beat is a [start, end] pair on 0–1 scroll progress, and every one is overridable:

| Beat | Default | What happens | | --- | --- | --- | | deviceIn | 0 → 0.18 | Device fades and rises into frame | | lidOpen | 0.24 → 0.48 | Lid rotates open, unhurried; the screen wakes behind it | | dive | 0.42 → 0.64 | Camera eases in toward the display | | hold | 0.64 → 0.74 | The gap before recede — the open MacBook plays its video while the user scrolls through it | | recede | 0.74 → 0.87 | Pushes back so the whole laptop is visible again | | settle | 0.87 → 1 | The tail after recede — the device rests, fully composed, before the page scrolls on |

Make the hold longer or shorter by moving recede, or give the whole journey more scroll room with height. The settle is generous on purpose: the damped follow trails raw scroll, so the pushback needs that runway to finish and rest before the pin releases — trim it and a quick scroll will carry the exit into the page's own motion. Pass timeline, poses, or feel to override any of it; anything you leave out keeps the tuned default. demo/src/Journey.tsx is the complete example.

The four components

Layered so each one is useful on its own.

| Component | What it's for | | --- | --- | | <Macbook> | The rigged model. Headless and controlled — you drive open, brightness, and the screen content from whatever state you already have. | | <MacbookStage> | A ready-made <Canvas> with the tuned camera and lighting. Skip it if you already have an R3F scene. | | <MacbookLighting> | The three lighting presets on their own, for dropping into an existing scene. | | <MacbookScroll> | The whole scroll journey above, in one component. |

Because <Macbook> is controlled, it binds to anything: a useState, a scroll position, a spring, a Framer Motion value.

API reference

| Prop | Type | Default | Description | | --- | --- | --- | --- | | open | number | 1 | Lid amount: 0 = closed, 1 = fully open. Linear — apply your own easing. | | screen | ScreenInput | — | Screen content: a video URL, image URL, or ready THREE.Texture. | | brightness | number | 1 | Screen wake: 0 = black, 1 = full. | | autoPlay | boolean | true | Play/pause the screen video so it only decodes while visible (paused while the lid is shut). | | modelSrc | string | — | Self-hosting escape hatch — see The model below. | | onLoad | () => void | — | Fires once the model is rigged and ready. | | frameDriver | () => MacbookFrameState | — | Per-frame state source, called inside the render loop. Returned fields (open, brightness) override the matching props — drive the model without re-rendering React. |

A ScreenInput is a URL string, a THREE.Texture, or { src, type?, fallbackSrc? }. Video vs image is sniffed from the extension unless you set type.

<Macbook> also takes every prop an R3F <group> takes (position, rotation, scale, …) and forwards a ref to the underlying THREE.Group.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | lighting | LightingPreset | 'studio-dark' | Lighting preset for the stage. | | lightingIntensity | number | 1 | Scales the preset's lights. | | pauseWhenOffscreen | boolean | true | Park the frameloop (and the GPU) while the stage is scrolled out of view. | | className / style | — | — | Size the wrapper div with these; defaults to 100% × 100%. |

Ships a tuned camera (z 6, fov 32) and ACES filmic tone mapping. Accepts every <Canvas> prop, so you can override camera, dpr, gl, and the rest.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | preset | 'studio-dark' \| 'studio-light' \| 'soft' | 'studio-dark' | studio-dark is the tuned Space Black look — a dark body carried by a few crisp reflections. | | intensity | number | 1 | Scales every light in the preset. | | children | ReactNode | — | Escape hatch: children replace the preset entirely. |

| Prop | Type | Default | Description | | --- | --- | --- | --- | | screen | string \| ScreenSource | — | The video (or image) that plays on the screen once the lid opens. | | height | string | '600vh' | Total scroll length of the pinned journey. | | lighting | LightingPreset | 'studio-dark' | Lighting preset. | | timeline | Partial<Timeline> | — | Override any journey beats. | | poses | PosesPartial | — | Override the intro / dive / outro poses. | | feel | Partial<Feel> | — | Tune the damped follow: smooth time, max speed. | | pointerParallax | boolean | true | Cursor-follow tilt once dived in. | | fallback | ReactNode | null | Rendered instead of the journey without WebGL2, or under reduced motion. | | modelSrc | string | — | Self-hosting escape hatch. | | onProgress | (p: number) => void | — | Fires with the smoothed 0–1 progress. | | children | ReactNode | — | Overlay content, rendered inside the sticky viewport above the canvas. |

The forwarded ref exposes a read-only progress getter.

| Export | What it does | | --- | --- | | useCapabilityGate() | boolean \| null — should this client get 3D? (WebGL2 + not reduced-motion). null on first render, so it's SSR-safe. | | useScreenTexture(source) | Turns a URL or texture into a THREE.Texture, with setPlaying to control video decode. | | journeyState(...) | The pure progress → frame-state mapping. Call it yourself to keep DOM overlays in exact sync with the 3D scene. | | ramp, easeInOut, lerp, clamp01, smoothDamp | The math the journey is built from. | | DEFAULT_TIMELINE, DEFAULT_POSES, DEFAULT_FEEL, … | The tuned defaults, plus resolveTimeline/resolvePoses/resolveFeel to merge over them. |

Recipes

const target = useRef(0);
const now = useRef(0);

<div onPointerEnter={() => (target.current = 1)} onPointerLeave={() => (target.current = 0)}>
  <MacbookStage>
    <Macbook
      screen="/demo.mp4"
      frameDriver={() => {
        now.current += (target.current - now.current) * 0.08;
        return { open: now.current };
      }}
    />
  </MacbookStage>
</div>

frameDriver reads the value every frame, so nothing re-renders:

const openMV = useMotionValue(0);

<Macbook screen="/demo.mp4" frameDriver={() => ({ open: openMV.get() })} />
// elsewhere: animate(openMV, 1, { type: 'spring', stiffness: 120, damping: 20 });
const groupRef = useRef<THREE.Group>(null);

useFrame((state) => {
  if (!groupRef.current) return;
  groupRef.current.rotation.y = (state.pointer.x * Math.PI) / 8;
  groupRef.current.rotation.x = (state.pointer.y * Math.PI) / 16;
});

<group ref={groupRef}>
  <Macbook open={1} screen="/demo.mp4" />
</group>
<MacbookStage>
  <MacbookLighting>
    <ambientLight intensity={0.4} />
    <directionalLight position={[5, 8, 6]} intensity={1.2} />
  </MacbookLighting>
  <Macbook open={1} screen="/demo.mp4" />
</MacbookStage>

Good to know

Everything here is a client component. With the App Router, load the stage through next/dynamic with SSR off:

'use client';
import dynamic from 'next/dynamic';

const MacbookStage = dynamic(() => import('rigged-macbook-3d').then((m) => m.MacbookStage), { ssr: false });

By default the GLB loads from https://unpkg.com/rigged-macbook-3d@<version>/assets/macbook-rigged.glb, pinned to the version you installed so it never changes underneath you. To self-host, copy node_modules/rigged-macbook-3d/assets/macbook-rigged.glb into your static assets and pass its URL as modelSrc.

No other model works. The rig — the hinge split, the LidPivot/Screen node names, the seat-lift math that keeps the closing screen out of the keyboard — is welded to this specific file, and a different GLB throws. You customise through props and lighting, not by bringing your own geometry.

Multiple <Macbook> instances on one page are fine: each clones the node hierarchy on mount while geometries and materials stay shared, so extra instances are cheap.

<MacbookScroll> gates itself on useCapabilityGate() — WebGL2 present, prefers-reduced-motion not set. It renders a height-holding placeholder on first render (SSR-safe), then either the journey or your fallback. Once it downgrades it never reverts, since remounting a 3D scene under someone is its own kind of motion.

<MacbookStage> and <Macbook> do not gate themselves. Using them bare, call useCapabilityGate() yourself and branch on it. Make the fallback meaningful — a screenshot, a video, a description — not an empty div. A real share of your visitors will only ever see it.

Screen renders black with a cross-origin video. Every <video> is created with crossOrigin="anonymous", so the host must send CORS headers or the browser won't let WebGL sample the frames — and it fails silently. Serving the video from your own origin sidesteps it.

Model won't load / unpkg is blocked. Self-host the GLB and pass modelSrc (see The model, above).

Duplicate three version warnings. Two copies of three in one bundle silently break materials and textures. Run npm ls three, then pin a single version with overrides (npm) or resolutions (Yarn/pnpm).

Scroll feels rough in dev. StrictMode's double-mounting is handled. It's usually the host page forcing synchronous layout — reading getBoundingClientRect/scrollY in an unthrottled scroll listener, or a ResizeObserver loop — on the same frame as the damped follow.


License MIT © William Laverty

Model: "MacBook Pro M3 16-inch 2024" by jackbaeten, CC-BY 4.0, rigged and recoloured — full attribution in CREDITS.md.

Not affiliated with or endorsed by Apple Inc. "MacBook" is a trademark of Apple Inc., used nominatively.