warm-motion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/Reveal.d.ts +14 -0
- package/dist/index.d.ts +5 -0
- package/dist/motion.d.ts +6 -0
- package/dist/styles.css +40 -0
- package/dist/useMagnetic.d.ts +21 -0
- package/dist/useSpotlight.d.ts +18 -0
- package/dist/viewTransition.d.ts +5 -0
- package/dist/warm-motion.cjs +2 -0
- package/dist/warm-motion.cjs.map +1 -0
- package/dist/warm-motion.js +138 -0
- package/dist/warm-motion.js.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# warm-motion
|
|
2
|
+
|
|
3
|
+
Tasteful, **reduced-motion-aware** React interaction primitives — a cursor
|
|
4
|
+
spotlight, magnetic hover, scroll reveals, and view-transition helpers. Extracted
|
|
5
|
+
from [marekzska.space](https://marekzska.space) — the site runs on it.
|
|
6
|
+
|
|
7
|
+
> **Pre-release:** packaged and publish-ready, but not yet on the npm registry.
|
|
8
|
+
> The API below is stable.
|
|
9
|
+
|
|
10
|
+
- **Accessible by default** — every effect honours `prefers-reduced-motion` and
|
|
11
|
+
bails on coarse (touch) pointers, so you never ship a dead hover affordance.
|
|
12
|
+
- **Owns its physics** — the magnetic pull is clamped and spring-smoothed in JS
|
|
13
|
+
(no reliance on a CSS transition), and its rAF loop idles when settled.
|
|
14
|
+
- **Tiny & tree-shakeable** — ~1.5 kB gzipped, ESM + CJS, `react`/`react-dom`/
|
|
15
|
+
`framer-motion` are peer deps.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm i warm-motion
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// once, near your app root:
|
|
25
|
+
import 'warm-motion/styles.css'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Peer dependencies: `react >=18`, `react-dom >=18`, `framer-motion >=11`.
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `useSpotlight(options?)`
|
|
33
|
+
Tracks the cursor into the `--mx` / `--my` CSS variables on `:root` and toggles a
|
|
34
|
+
`spotlight-live` class. Combined with the stylesheet, it lights up any element
|
|
35
|
+
with the `.lit` class.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
useSpotlight() // options: { lerp?, staticX?, staticY? }
|
|
39
|
+
|
|
40
|
+
<h1 className="lit">Lit by the cursor</h1>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Map your text colours onto the spotlight:
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
:root {
|
|
47
|
+
--wm-spotlight-from: #ede6e0; /* bright */
|
|
48
|
+
--wm-spotlight-to: #8a7e7e; /* dim */
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `useMagnetic(options?) → ref`
|
|
53
|
+
Attracts an element toward the cursor once it enters an activation radius. The
|
|
54
|
+
pull is clamped (`maxOffset`) and both follow and snap-back are spring-smoothed.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
const ref = useMagnetic<HTMLAnchorElement>({ strength: 0.35, radius: 140, maxOffset: 12 })
|
|
58
|
+
<a ref={ref} href="…">Contact</a>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `<Reveal>`
|
|
62
|
+
Fades (and optionally rises) children into view once, when scrolled near. Renders
|
|
63
|
+
a plain wrapper with no animation under reduced motion.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Reveal delay={0.08} rise>
|
|
67
|
+
<p>…</p>
|
|
68
|
+
</Reveal>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `withViewTransition(update)`
|
|
72
|
+
Runs `update` inside a View Transition when supported, synchronously otherwise,
|
|
73
|
+
and skips the transition entirely under reduced motion.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
withViewTransition(() => flushSync(() => setTheme('dark')))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Constants
|
|
80
|
+
`EASE`, `REVEAL_DURATION`, `REVEAL_RISE`, `REVEAL_STAGGER` — the shared timing the
|
|
81
|
+
primitives use, exported so your own motion can match.
|
|
82
|
+
|
|
83
|
+
## Behaviour matrix
|
|
84
|
+
|
|
85
|
+
| Condition | useSpotlight | useMagnetic | Reveal |
|
|
86
|
+
| --- | --- | --- | --- |
|
|
87
|
+
| reduced motion | static fallback vars | inert | no animation |
|
|
88
|
+
| coarse pointer | static fallback vars | inert | animates on scroll |
|
|
89
|
+
|
|
90
|
+
## Versioning
|
|
91
|
+
|
|
92
|
+
Semantic versioning. The public surface is:
|
|
93
|
+
|
|
94
|
+
- the named JS exports above;
|
|
95
|
+
- the CSS custom properties `--wm-spotlight-from`, `--wm-spotlight-to`, and
|
|
96
|
+
`--spotlight-radius` (spotlight size, default `30rem`);
|
|
97
|
+
- the `--mx` / `--my` cursor variables written by `useSpotlight` (read them for
|
|
98
|
+
your own cursor-driven effects);
|
|
99
|
+
- the `.lit` and `.spotlight-live` class names.
|
|
100
|
+
|
|
101
|
+
These names are intentionally generic — if they collide with your app, scope the
|
|
102
|
+
stylesheet or override them.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/dist/Reveal.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type RevealProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/** Delay (s) before the reveal begins. */
|
|
5
|
+
delay?: number;
|
|
6
|
+
/** Whether the content rises as it fades in. */
|
|
7
|
+
rise?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fades (and optionally rises) its children into view once, when scrolled near.
|
|
12
|
+
* Under reduced motion it renders a plain wrapper with no animation at all.
|
|
13
|
+
*/
|
|
14
|
+
export declare function Reveal({ children, delay, rise, className }: RevealProps): import("react").JSX.Element;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useSpotlight, type SpotlightOptions } from './useSpotlight';
|
|
2
|
+
export { useMagnetic, type MagneticOptions } from './useMagnetic';
|
|
3
|
+
export { Reveal, type RevealProps } from './Reveal';
|
|
4
|
+
export { withViewTransition } from './viewTransition';
|
|
5
|
+
export { EASE, REVEAL_DURATION, REVEAL_RISE, REVEAL_STAGGER } from './motion';
|
package/dist/motion.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared easing curve — a soft expo-out. */
|
|
2
|
+
export declare const EASE: [number, number, number, number];
|
|
3
|
+
/** Default scroll-reveal timing. */
|
|
4
|
+
export declare const REVEAL_DURATION = 0.6;
|
|
5
|
+
export declare const REVEAL_RISE = 16;
|
|
6
|
+
export declare const REVEAL_STAGGER = 0.08;
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* warm-motion — stylesheet for the cursor spotlight + view transitions.
|
|
3
|
+
*
|
|
4
|
+
* The spotlight lights up any element with the `.lit` class while `useSpotlight`
|
|
5
|
+
* is active. Map your own text colors onto the two custom properties:
|
|
6
|
+
*
|
|
7
|
+
* :root {
|
|
8
|
+
* --wm-spotlight-from: <bright text color>;
|
|
9
|
+
* --wm-spotlight-to: <dim text color>;
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--mx: 50%;
|
|
15
|
+
--my: 30%;
|
|
16
|
+
--spotlight-radius: 30rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.lit {
|
|
20
|
+
color: var(--wm-spotlight-from, currentColor);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
|
|
24
|
+
.spotlight-live .lit {
|
|
25
|
+
background: radial-gradient(
|
|
26
|
+
circle var(--spotlight-radius) at var(--mx) var(--my),
|
|
27
|
+
var(--wm-spotlight-from, currentColor),
|
|
28
|
+
var(--wm-spotlight-to, currentColor)
|
|
29
|
+
)
|
|
30
|
+
fixed;
|
|
31
|
+
-webkit-background-clip: text;
|
|
32
|
+
background-clip: text;
|
|
33
|
+
color: transparent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
::view-transition-old(root),
|
|
38
|
+
::view-transition-new(root) {
|
|
39
|
+
animation-duration: 0.35s;
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MagneticOptions = {
|
|
2
|
+
/** Fraction of cursor distance applied as pull. */
|
|
3
|
+
strength?: number;
|
|
4
|
+
/** Activation radius (px) measured from the element's edge. */
|
|
5
|
+
radius?: number;
|
|
6
|
+
/** Maximum translation (px) per axis — clamps the pull so the hit target never flies away. */
|
|
7
|
+
maxOffset?: number;
|
|
8
|
+
/** Per-frame smoothing factor (0–1) for both follow and snap-back. */
|
|
9
|
+
smoothing?: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Returns a ref for an element that is magnetically attracted toward the cursor
|
|
13
|
+
* once it enters an activation radius. The pull is clamped, and the follow +
|
|
14
|
+
* snap-back are both driven by an in-hook spring (no reliance on a CSS
|
|
15
|
+
* transition). The rAF loop stops when the element settles and restarts on the
|
|
16
|
+
* next pointer change — no perpetual frame loop. Scroll/resize re-measure the
|
|
17
|
+
* element so a parked cursor never leaves a stale offset.
|
|
18
|
+
*
|
|
19
|
+
* No-ops under reduced motion or a coarse pointer.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useMagnetic<T extends HTMLElement>({ strength, radius, maxOffset, smoothing, }?: MagneticOptions): import("react").RefObject<T | null>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type SpotlightOptions = {
|
|
2
|
+
/** Easing factor per frame toward the cursor (0–1). Lower = laggier. */
|
|
3
|
+
lerp?: number;
|
|
4
|
+
/** Fallback `--mx` when the pointer is coarse or motion is reduced. */
|
|
5
|
+
staticX?: string;
|
|
6
|
+
/** Fallback `--my` when the pointer is coarse or motion is reduced. */
|
|
7
|
+
staticY?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Tracks the cursor into two CSS custom properties (`--mx`, `--my`) on the
|
|
11
|
+
* document root via an rAF lerp loop, and toggles a `spotlight-live` class. The
|
|
12
|
+
* loop idles when the position settles and restarts on the next pointer move, so
|
|
13
|
+
* a stationary cursor costs nothing. Pair with the package stylesheet to light up
|
|
14
|
+
* `.lit` headings.
|
|
15
|
+
*
|
|
16
|
+
* No-ops (writes static fallbacks) under reduced motion or a coarse pointer.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useSpotlight({ lerp, staticX, staticY, }?: SpotlightOptions): void;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("framer-motion"),n=require("react/jsx-runtime");function r({lerp:n=.1,staticX:r=`82%`,staticY:i=`16%`}={}){let a=(0,t.useReducedMotion)();(0,e.useEffect)(()=>{let e=document.documentElement,t=window.matchMedia(`(pointer: fine)`).matches;if(a||!t){e.style.setProperty(`--mx`,r),e.style.setProperty(`--my`,i);return}let o={x:window.innerWidth*.5,y:window.innerHeight*.3},s={...o},c=0,l=()=>{s.x+=(o.x-s.x)*n,s.y+=(o.y-s.y)*n,Math.abs(o.x-s.x)<.5&&Math.abs(o.y-s.y)<.5&&(s.x=o.x,s.y=o.y),e.style.setProperty(`--mx`,`${s.x}px`),e.style.setProperty(`--my`,`${s.y}px`),c=s.x!==o.x||s.y!==o.y?requestAnimationFrame(l):0},u=()=>{c||=requestAnimationFrame(l)},d=e=>{o.x=e.clientX,o.y=e.clientY,u()};return e.classList.add(`spotlight-live`),e.style.setProperty(`--mx`,`${s.x}px`),e.style.setProperty(`--my`,`${s.y}px`),window.addEventListener(`pointermove`,d,{passive:!0}),()=>{c&&cancelAnimationFrame(c),window.removeEventListener(`pointermove`,d),e.classList.remove(`spotlight-live`)}},[a,n,r,i])}function i({strength:t=.35,radius:n=140,maxOffset:r=12,smoothing:i=.15}={}){let a=(0,e.useRef)(null);return(0,e.useEffect)(()=>{let e=a.current;if(!e)return;let o=window.matchMedia(`(pointer: fine)`).matches,s=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;if(!o||s)return;let c={x:0,y:0},l={x:0,y:0},u={x:0,y:0},d=!1,f=0,p=e=>Math.max(-r,Math.min(r,e)),m=()=>{if(d){d=!1;let r=e.getBoundingClientRect(),i=c.x-(r.left+r.width/2),a=c.y-(r.top+r.height/2),o=n+Math.max(r.width,r.height)/2,s=i*i+a*a<o*o;l.x=s?p(i*t):0,l.y=s?p(a*t):0}u.x+=(l.x-u.x)*i,u.y+=(l.y-u.y)*i,Math.abs(l.x-u.x)<.05&&Math.abs(l.y-u.y)<.05&&(u.x=l.x,u.y=l.y),e.style.transform=u.x===0&&u.y===0?``:`translate(${u.x.toFixed(2)}px, ${u.y.toFixed(2)}px)`,f=d||u.x!==l.x||u.y!==l.y?requestAnimationFrame(m):0},h=()=>{f||=requestAnimationFrame(m)},g=e=>{c.x=e.clientX,c.y=e.clientY,d=!0,h()},_=()=>{d=!0,h()},v=()=>{c.x=-1/0,c.y=-1/0,d=!0,h()};return window.addEventListener(`pointermove`,g,{passive:!0}),window.addEventListener(`scroll`,_,{passive:!0,capture:!0}),window.addEventListener(`resize`,_,{passive:!0}),window.addEventListener(`blur`,v),()=>{f&&cancelAnimationFrame(f),window.removeEventListener(`pointermove`,g),window.removeEventListener(`scroll`,_,{capture:!0}),window.removeEventListener(`resize`,_),window.removeEventListener(`blur`,v),e.style.transform=``}},[t,n,r,i]),a}var a=[.22,1,.36,1],o=.6,s=16,c=.08;function l({children:e,delay:r=0,rise:i=!0,className:s}){if((0,t.useReducedMotion)())return(0,n.jsx)(`div`,{className:s,children:e});let c=i?{hidden:{opacity:0,y:16},visible:{opacity:1,y:0}}:{hidden:{opacity:0},visible:{opacity:1}};return(0,n.jsx)(t.motion.div,{className:s,variants:c,initial:`hidden`,whileInView:`visible`,viewport:{once:!0,margin:`-15%`},transition:{duration:o,ease:a,delay:r},children:e})}function u(e){let t=document;if(window.matchMedia(`(prefers-reduced-motion: reduce)`).matches||typeof t.startViewTransition!=`function`){e();return}t.startViewTransition(e)}exports.EASE=a,exports.REVEAL_DURATION=o,exports.REVEAL_RISE=s,exports.REVEAL_STAGGER=c,exports.Reveal=l,exports.useMagnetic=i,exports.useSpotlight=r,exports.withViewTransition=u;
|
|
2
|
+
//# sourceMappingURL=warm-motion.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warm-motion.cjs","names":[],"sources":["../src/useSpotlight.ts","../src/useMagnetic.ts","../src/motion.ts","../src/Reveal.tsx","../src/viewTransition.ts"],"sourcesContent":["import { useEffect } from 'react'\nimport { useReducedMotion } from 'framer-motion'\n\nexport type SpotlightOptions = {\n /** Easing factor per frame toward the cursor (0–1). Lower = laggier. */\n lerp?: number\n /** Fallback `--mx` when the pointer is coarse or motion is reduced. */\n staticX?: string\n /** Fallback `--my` when the pointer is coarse or motion is reduced. */\n staticY?: string\n}\n\n/**\n * Tracks the cursor into two CSS custom properties (`--mx`, `--my`) on the\n * document root via an rAF lerp loop, and toggles a `spotlight-live` class. The\n * loop idles when the position settles and restarts on the next pointer move, so\n * a stationary cursor costs nothing. Pair with the package stylesheet to light up\n * `.lit` headings.\n *\n * No-ops (writes static fallbacks) under reduced motion or a coarse pointer.\n */\nexport function useSpotlight({\n lerp = 0.1,\n staticX = '82%',\n staticY = '16%',\n}: SpotlightOptions = {}) {\n const reduced = useReducedMotion()\n\n useEffect(() => {\n const root = document.documentElement\n const fine = window.matchMedia('(pointer: fine)').matches\n\n if (reduced || !fine) {\n root.style.setProperty('--mx', staticX)\n root.style.setProperty('--my', staticY)\n return\n }\n\n const target = { x: window.innerWidth * 0.5, y: window.innerHeight * 0.3 }\n const pos = { ...target }\n let raf = 0\n\n const tick = () => {\n pos.x += (target.x - pos.x) * lerp\n pos.y += (target.y - pos.y) * lerp\n if (Math.abs(target.x - pos.x) < 0.5 && Math.abs(target.y - pos.y) < 0.5) {\n pos.x = target.x\n pos.y = target.y\n }\n root.style.setProperty('--mx', `${pos.x}px`)\n root.style.setProperty('--my', `${pos.y}px`)\n raf = pos.x !== target.x || pos.y !== target.y ? requestAnimationFrame(tick) : 0\n }\n\n const ensureLoop = () => {\n if (!raf) raf = requestAnimationFrame(tick)\n }\n\n const onMove = (event: PointerEvent) => {\n target.x = event.clientX\n target.y = event.clientY\n ensureLoop()\n }\n\n root.classList.add('spotlight-live')\n root.style.setProperty('--mx', `${pos.x}px`)\n root.style.setProperty('--my', `${pos.y}px`)\n window.addEventListener('pointermove', onMove, { passive: true })\n\n return () => {\n if (raf) cancelAnimationFrame(raf)\n window.removeEventListener('pointermove', onMove)\n root.classList.remove('spotlight-live')\n }\n }, [reduced, lerp, staticX, staticY])\n}\n","import { useEffect, useRef } from 'react'\n\nexport type MagneticOptions = {\n /** Fraction of cursor distance applied as pull. */\n strength?: number\n /** Activation radius (px) measured from the element's edge. */\n radius?: number\n /** Maximum translation (px) per axis — clamps the pull so the hit target never flies away. */\n maxOffset?: number\n /** Per-frame smoothing factor (0–1) for both follow and snap-back. */\n smoothing?: number\n}\n\n/**\n * Returns a ref for an element that is magnetically attracted toward the cursor\n * once it enters an activation radius. The pull is clamped, and the follow +\n * snap-back are both driven by an in-hook spring (no reliance on a CSS\n * transition). The rAF loop stops when the element settles and restarts on the\n * next pointer change — no perpetual frame loop. Scroll/resize re-measure the\n * element so a parked cursor never leaves a stale offset.\n *\n * No-ops under reduced motion or a coarse pointer.\n */\nexport function useMagnetic<T extends HTMLElement>({\n strength = 0.35,\n radius = 140,\n maxOffset = 12,\n smoothing = 0.15,\n}: MagneticOptions = {}) {\n const ref = useRef<T>(null)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const fine = window.matchMedia('(pointer: fine)').matches\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n if (!fine || reduced) return\n\n const pointer = { x: 0, y: 0 }\n const target = { x: 0, y: 0 }\n const current = { x: 0, y: 0 }\n let dirty = false\n let raf = 0\n\n const clamp = (v: number) => Math.max(-maxOffset, Math.min(maxOffset, v))\n\n const tick = () => {\n if (dirty) {\n dirty = false\n const rect = el.getBoundingClientRect()\n const dx = pointer.x - (rect.left + rect.width / 2)\n const dy = pointer.y - (rect.top + rect.height / 2)\n const range = radius + Math.max(rect.width, rect.height) / 2\n const inRange = dx * dx + dy * dy < range * range\n target.x = inRange ? clamp(dx * strength) : 0\n target.y = inRange ? clamp(dy * strength) : 0\n }\n\n current.x += (target.x - current.x) * smoothing\n current.y += (target.y - current.y) * smoothing\n if (Math.abs(target.x - current.x) < 0.05 && Math.abs(target.y - current.y) < 0.05) {\n current.x = target.x\n current.y = target.y\n }\n\n el.style.transform =\n current.x === 0 && current.y === 0\n ? ''\n : `translate(${current.x.toFixed(2)}px, ${current.y.toFixed(2)}px)`\n\n if (dirty || current.x !== target.x || current.y !== target.y) {\n raf = requestAnimationFrame(tick)\n } else {\n raf = 0 // settled — idle until the next pointer change\n }\n }\n\n const ensureLoop = () => {\n if (!raf) raf = requestAnimationFrame(tick)\n }\n\n const onMove = (event: PointerEvent) => {\n pointer.x = event.clientX\n pointer.y = event.clientY\n dirty = true\n ensureLoop()\n }\n\n // The element can move under a stationary cursor; re-measure on reflow.\n const onReflow = () => {\n dirty = true\n ensureLoop()\n }\n\n const reset = () => {\n pointer.x = Number.NEGATIVE_INFINITY\n pointer.y = Number.NEGATIVE_INFINITY\n dirty = true\n ensureLoop()\n }\n\n window.addEventListener('pointermove', onMove, { passive: true })\n window.addEventListener('scroll', onReflow, { passive: true, capture: true })\n window.addEventListener('resize', onReflow, { passive: true })\n window.addEventListener('blur', reset)\n\n return () => {\n if (raf) cancelAnimationFrame(raf)\n window.removeEventListener('pointermove', onMove)\n window.removeEventListener('scroll', onReflow, { capture: true } as EventListenerOptions)\n window.removeEventListener('resize', onReflow)\n window.removeEventListener('blur', reset)\n el.style.transform = ''\n }\n }, [strength, radius, maxOffset, smoothing])\n\n return ref\n}\n","/** Shared easing curve — a soft expo-out. */\nexport const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]\n\n/** Default scroll-reveal timing. */\nexport const REVEAL_DURATION = 0.6\nexport const REVEAL_RISE = 16\nexport const REVEAL_STAGGER = 0.08\n","import { motion, useReducedMotion, type Variants } from 'framer-motion'\nimport type { ReactNode } from 'react'\nimport { EASE, REVEAL_DURATION, REVEAL_RISE } from './motion'\n\nexport type RevealProps = {\n children: ReactNode\n /** Delay (s) before the reveal begins. */\n delay?: number\n /** Whether the content rises as it fades in. */\n rise?: boolean\n className?: string\n}\n\n/**\n * Fades (and optionally rises) its children into view once, when scrolled near.\n * Under reduced motion it renders a plain wrapper with no animation at all.\n */\nexport function Reveal({ children, delay = 0, rise = true, className }: RevealProps) {\n const reduced = useReducedMotion()\n\n if (reduced) {\n return <div className={className}>{children}</div>\n }\n\n const variants: Variants = rise\n ? { hidden: { opacity: 0, y: REVEAL_RISE }, visible: { opacity: 1, y: 0 } }\n : { hidden: { opacity: 0 }, visible: { opacity: 1 } }\n\n return (\n <motion.div\n className={className}\n variants={variants}\n initial=\"hidden\"\n whileInView=\"visible\"\n viewport={{ once: true, margin: '-15%' }}\n transition={{ duration: REVEAL_DURATION, ease: EASE, delay }}\n >\n {children}\n </motion.div>\n )\n}\n","type ViewTransitionDocument = Document & {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/**\n * Runs `update` inside a View Transition when supported, otherwise applies it\n * synchronously. Skips the transition entirely under reduced motion.\n */\nexport function withViewTransition(update: () => void) {\n const doc = document as ViewTransitionDocument\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n if (reduced || typeof doc.startViewTransition !== 'function') {\n update()\n return\n }\n\n doc.startViewTransition(update)\n}\n"],"mappings":"oJAqBA,SAAgB,EAAa,CAC3B,OAAO,GACP,UAAU,MACV,UAAU,OACU,CAAC,EAAG,CACxB,IAAM,GAAA,EAAA,EAAA,kBAA2B,GAEjC,EAAA,EAAA,eAAgB,CACd,IAAM,EAAO,SAAS,gBAChB,EAAO,OAAO,WAAW,iBAAiB,EAAE,QAElD,GAAI,GAAW,CAAC,EAAM,CACpB,EAAK,MAAM,YAAY,OAAQ,CAAO,EACtC,EAAK,MAAM,YAAY,OAAQ,CAAO,EACtC,MACF,CAEA,IAAM,EAAS,CAAE,EAAG,OAAO,WAAa,GAAK,EAAG,OAAO,YAAc,EAAI,EACnE,EAAM,CAAE,GAAG,CAAO,EACpB,EAAM,EAEJ,MAAa,CACjB,EAAI,IAAM,EAAO,EAAI,EAAI,GAAK,EAC9B,EAAI,IAAM,EAAO,EAAI,EAAI,GAAK,EAC1B,KAAK,IAAI,EAAO,EAAI,EAAI,CAAC,EAAI,IAAO,KAAK,IAAI,EAAO,EAAI,EAAI,CAAC,EAAI,KACnE,EAAI,EAAI,EAAO,EACf,EAAI,EAAI,EAAO,GAEjB,EAAK,MAAM,YAAY,OAAQ,GAAG,EAAI,EAAE,GAAG,EAC3C,EAAK,MAAM,YAAY,OAAQ,GAAG,EAAI,EAAE,GAAG,EAC3C,EAAM,EAAI,IAAM,EAAO,GAAK,EAAI,IAAM,EAAO,EAAI,sBAAsB,CAAI,EAAI,CACjF,EAEM,MAAmB,CACvB,AAAU,IAAM,sBAAsB,CAAI,CAC5C,EAEM,EAAU,GAAwB,CACtC,EAAO,EAAI,EAAM,QACjB,EAAO,EAAI,EAAM,QACjB,EAAW,CACb,EAOA,OALA,EAAK,UAAU,IAAI,gBAAgB,EACnC,EAAK,MAAM,YAAY,OAAQ,GAAG,EAAI,EAAE,GAAG,EAC3C,EAAK,MAAM,YAAY,OAAQ,GAAG,EAAI,EAAE,GAAG,EAC3C,OAAO,iBAAiB,cAAe,EAAQ,CAAE,QAAS,EAAK,CAAC,MAEnD,CACP,GAAK,qBAAqB,CAAG,EACjC,OAAO,oBAAoB,cAAe,CAAM,EAChD,EAAK,UAAU,OAAO,gBAAgB,CACxC,CACF,EAAG,CAAC,EAAS,EAAM,EAAS,CAAO,CAAC,CACtC,CCpDA,SAAgB,EAAmC,CACjD,WAAW,IACX,SAAS,IACT,YAAY,GACZ,YAAY,KACO,CAAC,EAAG,CACvB,IAAM,GAAA,EAAA,EAAA,QAAgB,IAAI,EAwF1B,OAtFA,EAAA,EAAA,eAAgB,CACd,IAAM,EAAK,EAAI,QACf,GAAI,CAAC,EAAI,OAET,IAAM,EAAO,OAAO,WAAW,iBAAiB,EAAE,QAC5C,EAAU,OAAO,WAAW,kCAAkC,EAAE,QACtE,GAAI,CAAC,GAAQ,EAAS,OAEtB,IAAM,EAAU,CAAE,EAAG,EAAG,EAAG,CAAE,EACvB,EAAS,CAAE,EAAG,EAAG,EAAG,CAAE,EACtB,EAAU,CAAE,EAAG,EAAG,EAAG,CAAE,EACzB,EAAQ,GACR,EAAM,EAEJ,EAAS,GAAc,KAAK,IAAI,CAAC,EAAW,KAAK,IAAI,EAAW,CAAC,CAAC,EAElE,MAAa,CACjB,GAAI,EAAO,CACT,EAAQ,GACR,IAAM,EAAO,EAAG,sBAAsB,EAChC,EAAK,EAAQ,GAAK,EAAK,KAAO,EAAK,MAAQ,GAC3C,EAAK,EAAQ,GAAK,EAAK,IAAM,EAAK,OAAS,GAC3C,EAAQ,EAAS,KAAK,IAAI,EAAK,MAAO,EAAK,MAAM,EAAI,EACrD,EAAU,EAAK,EAAK,EAAK,EAAK,EAAQ,EAC5C,EAAO,EAAI,EAAU,EAAM,EAAK,CAAQ,EAAI,EAC5C,EAAO,EAAI,EAAU,EAAM,EAAK,CAAQ,EAAI,CAC9C,CAEA,EAAQ,IAAM,EAAO,EAAI,EAAQ,GAAK,EACtC,EAAQ,IAAM,EAAO,EAAI,EAAQ,GAAK,EAClC,KAAK,IAAI,EAAO,EAAI,EAAQ,CAAC,EAAI,KAAQ,KAAK,IAAI,EAAO,EAAI,EAAQ,CAAC,EAAI,MAC5E,EAAQ,EAAI,EAAO,EACnB,EAAQ,EAAI,EAAO,GAGrB,EAAG,MAAM,UACP,EAAQ,IAAM,GAAK,EAAQ,IAAM,EAC7B,GACA,aAAa,EAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAQ,EAAE,QAAQ,CAAC,EAAE,KAEnE,AAGE,EAHE,GAAS,EAAQ,IAAM,EAAO,GAAK,EAAQ,IAAM,EAAO,EACpD,sBAAsB,CAAI,EAE1B,CAEV,EAEM,MAAmB,CACvB,AAAU,IAAM,sBAAsB,CAAI,CAC5C,EAEM,EAAU,GAAwB,CACtC,EAAQ,EAAI,EAAM,QAClB,EAAQ,EAAI,EAAM,QAClB,EAAQ,GACR,EAAW,CACb,EAGM,MAAiB,CACrB,EAAQ,GACR,EAAW,CACb,EAEM,MAAc,CAClB,EAAQ,EAAI,KACZ,EAAQ,EAAI,KACZ,EAAQ,GACR,EAAW,CACb,EAOA,OALA,OAAO,iBAAiB,cAAe,EAAQ,CAAE,QAAS,EAAK,CAAC,EAChE,OAAO,iBAAiB,SAAU,EAAU,CAAE,QAAS,GAAM,QAAS,EAAK,CAAC,EAC5E,OAAO,iBAAiB,SAAU,EAAU,CAAE,QAAS,EAAK,CAAC,EAC7D,OAAO,iBAAiB,OAAQ,CAAK,MAExB,CACP,GAAK,qBAAqB,CAAG,EACjC,OAAO,oBAAoB,cAAe,CAAM,EAChD,OAAO,oBAAoB,SAAU,EAAU,CAAE,QAAS,EAAK,CAAyB,EACxF,OAAO,oBAAoB,SAAU,CAAQ,EAC7C,OAAO,oBAAoB,OAAQ,CAAK,EACxC,EAAG,MAAM,UAAY,EACvB,CACF,EAAG,CAAC,EAAU,EAAQ,EAAW,CAAS,CAAC,EAEpC,CACT,CCrHA,IAAa,EAAyC,CAAC,IAAM,EAAG,IAAM,CAAC,EAG1D,EAAkB,GAClB,EAAc,GACd,EAAiB,ICW9B,SAAgB,EAAO,CAAE,WAAU,QAAQ,EAAG,OAAO,GAAM,aAA0B,CAGnF,IAAA,EAAA,EAAA,kBAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAgB,YAAY,UAAc,CAAA,EAGnD,IAAM,EAAqB,EACvB,CAAE,OAAQ,CAAE,QAAS,EAAG,EAAA,EAAe,EAAG,QAAS,CAAE,QAAS,EAAG,EAAG,CAAE,CAAE,EACxE,CAAE,OAAQ,CAAE,QAAS,CAAE,EAAG,QAAS,CAAE,QAAS,CAAE,CAAE,EAEtD,OACE,EAAA,EAAA,KAAC,EAAA,OAAO,IAAR,CACa,YACD,WACV,QAAQ,SACR,YAAY,UACZ,SAAU,CAAE,KAAM,GAAM,OAAQ,MAAO,EACvC,WAAY,CAAE,SAAU,EAAiB,KAAM,EAAM,OAAM,EAE1D,UACS,CAAA,CAEhB,CChCA,SAAgB,EAAmB,EAAoB,CACrD,IAAM,EAAM,SAGZ,GAFgB,OAAO,WAAW,kCAAkC,EAAE,SAEvD,OAAO,EAAI,qBAAwB,WAAY,CAC5D,EAAO,EACP,MACF,CAEA,EAAI,oBAAoB,CAAM,CAChC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useEffect as e, useRef as t } from "react";
|
|
2
|
+
import { motion as n, useReducedMotion as r } from "framer-motion";
|
|
3
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
4
|
+
//#region src/useSpotlight.ts
|
|
5
|
+
function a({ lerp: t = .1, staticX: n = "82%", staticY: i = "16%" } = {}) {
|
|
6
|
+
let a = r();
|
|
7
|
+
e(() => {
|
|
8
|
+
let e = document.documentElement, r = window.matchMedia("(pointer: fine)").matches;
|
|
9
|
+
if (a || !r) {
|
|
10
|
+
e.style.setProperty("--mx", n), e.style.setProperty("--my", i);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let o = {
|
|
14
|
+
x: window.innerWidth * .5,
|
|
15
|
+
y: window.innerHeight * .3
|
|
16
|
+
}, s = { ...o }, c = 0, l = () => {
|
|
17
|
+
s.x += (o.x - s.x) * t, s.y += (o.y - s.y) * t, Math.abs(o.x - s.x) < .5 && Math.abs(o.y - s.y) < .5 && (s.x = o.x, s.y = o.y), e.style.setProperty("--mx", `${s.x}px`), e.style.setProperty("--my", `${s.y}px`), c = s.x !== o.x || s.y !== o.y ? requestAnimationFrame(l) : 0;
|
|
18
|
+
}, u = () => {
|
|
19
|
+
c ||= requestAnimationFrame(l);
|
|
20
|
+
}, d = (e) => {
|
|
21
|
+
o.x = e.clientX, o.y = e.clientY, u();
|
|
22
|
+
};
|
|
23
|
+
return e.classList.add("spotlight-live"), e.style.setProperty("--mx", `${s.x}px`), e.style.setProperty("--my", `${s.y}px`), window.addEventListener("pointermove", d, { passive: !0 }), () => {
|
|
24
|
+
c && cancelAnimationFrame(c), window.removeEventListener("pointermove", d), e.classList.remove("spotlight-live");
|
|
25
|
+
};
|
|
26
|
+
}, [
|
|
27
|
+
a,
|
|
28
|
+
t,
|
|
29
|
+
n,
|
|
30
|
+
i
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/useMagnetic.ts
|
|
35
|
+
function o({ strength: n = .35, radius: r = 140, maxOffset: i = 12, smoothing: a = .15 } = {}) {
|
|
36
|
+
let o = t(null);
|
|
37
|
+
return e(() => {
|
|
38
|
+
let e = o.current;
|
|
39
|
+
if (!e) return;
|
|
40
|
+
let t = window.matchMedia("(pointer: fine)").matches, s = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
41
|
+
if (!t || s) return;
|
|
42
|
+
let c = {
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0
|
|
45
|
+
}, l = {
|
|
46
|
+
x: 0,
|
|
47
|
+
y: 0
|
|
48
|
+
}, u = {
|
|
49
|
+
x: 0,
|
|
50
|
+
y: 0
|
|
51
|
+
}, d = !1, f = 0, p = (e) => Math.max(-i, Math.min(i, e)), m = () => {
|
|
52
|
+
if (d) {
|
|
53
|
+
d = !1;
|
|
54
|
+
let t = e.getBoundingClientRect(), i = c.x - (t.left + t.width / 2), a = c.y - (t.top + t.height / 2), o = r + Math.max(t.width, t.height) / 2, s = i * i + a * a < o * o;
|
|
55
|
+
l.x = s ? p(i * n) : 0, l.y = s ? p(a * n) : 0;
|
|
56
|
+
}
|
|
57
|
+
u.x += (l.x - u.x) * a, u.y += (l.y - u.y) * a, Math.abs(l.x - u.x) < .05 && Math.abs(l.y - u.y) < .05 && (u.x = l.x, u.y = l.y), e.style.transform = u.x === 0 && u.y === 0 ? "" : `translate(${u.x.toFixed(2)}px, ${u.y.toFixed(2)}px)`, f = d || u.x !== l.x || u.y !== l.y ? requestAnimationFrame(m) : 0;
|
|
58
|
+
}, h = () => {
|
|
59
|
+
f ||= requestAnimationFrame(m);
|
|
60
|
+
}, g = (e) => {
|
|
61
|
+
c.x = e.clientX, c.y = e.clientY, d = !0, h();
|
|
62
|
+
}, _ = () => {
|
|
63
|
+
d = !0, h();
|
|
64
|
+
}, v = () => {
|
|
65
|
+
c.x = -Infinity, c.y = -Infinity, d = !0, h();
|
|
66
|
+
};
|
|
67
|
+
return window.addEventListener("pointermove", g, { passive: !0 }), window.addEventListener("scroll", _, {
|
|
68
|
+
passive: !0,
|
|
69
|
+
capture: !0
|
|
70
|
+
}), window.addEventListener("resize", _, { passive: !0 }), window.addEventListener("blur", v), () => {
|
|
71
|
+
f && cancelAnimationFrame(f), window.removeEventListener("pointermove", g), window.removeEventListener("scroll", _, { capture: !0 }), window.removeEventListener("resize", _), window.removeEventListener("blur", v), e.style.transform = "";
|
|
72
|
+
};
|
|
73
|
+
}, [
|
|
74
|
+
n,
|
|
75
|
+
r,
|
|
76
|
+
i,
|
|
77
|
+
a
|
|
78
|
+
]), o;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/motion.ts
|
|
82
|
+
var s = [
|
|
83
|
+
.22,
|
|
84
|
+
1,
|
|
85
|
+
.36,
|
|
86
|
+
1
|
|
87
|
+
], c = .6, l = 16, u = .08;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/Reveal.tsx
|
|
90
|
+
function d({ children: e, delay: t = 0, rise: a = !0, className: o }) {
|
|
91
|
+
if (r()) return /* @__PURE__ */ i("div", {
|
|
92
|
+
className: o,
|
|
93
|
+
children: e
|
|
94
|
+
});
|
|
95
|
+
let l = a ? {
|
|
96
|
+
hidden: {
|
|
97
|
+
opacity: 0,
|
|
98
|
+
y: 16
|
|
99
|
+
},
|
|
100
|
+
visible: {
|
|
101
|
+
opacity: 1,
|
|
102
|
+
y: 0
|
|
103
|
+
}
|
|
104
|
+
} : {
|
|
105
|
+
hidden: { opacity: 0 },
|
|
106
|
+
visible: { opacity: 1 }
|
|
107
|
+
};
|
|
108
|
+
return /* @__PURE__ */ i(n.div, {
|
|
109
|
+
className: o,
|
|
110
|
+
variants: l,
|
|
111
|
+
initial: "hidden",
|
|
112
|
+
whileInView: "visible",
|
|
113
|
+
viewport: {
|
|
114
|
+
once: !0,
|
|
115
|
+
margin: "-15%"
|
|
116
|
+
},
|
|
117
|
+
transition: {
|
|
118
|
+
duration: c,
|
|
119
|
+
ease: s,
|
|
120
|
+
delay: t
|
|
121
|
+
},
|
|
122
|
+
children: e
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/viewTransition.ts
|
|
127
|
+
function f(e) {
|
|
128
|
+
let t = document;
|
|
129
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || typeof t.startViewTransition != "function") {
|
|
130
|
+
e();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
t.startViewTransition(e);
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { s as EASE, c as REVEAL_DURATION, l as REVEAL_RISE, u as REVEAL_STAGGER, d as Reveal, o as useMagnetic, a as useSpotlight, f as withViewTransition };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=warm-motion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warm-motion.js","names":[],"sources":["../src/useSpotlight.ts","../src/useMagnetic.ts","../src/motion.ts","../src/Reveal.tsx","../src/viewTransition.ts"],"sourcesContent":["import { useEffect } from 'react'\nimport { useReducedMotion } from 'framer-motion'\n\nexport type SpotlightOptions = {\n /** Easing factor per frame toward the cursor (0–1). Lower = laggier. */\n lerp?: number\n /** Fallback `--mx` when the pointer is coarse or motion is reduced. */\n staticX?: string\n /** Fallback `--my` when the pointer is coarse or motion is reduced. */\n staticY?: string\n}\n\n/**\n * Tracks the cursor into two CSS custom properties (`--mx`, `--my`) on the\n * document root via an rAF lerp loop, and toggles a `spotlight-live` class. The\n * loop idles when the position settles and restarts on the next pointer move, so\n * a stationary cursor costs nothing. Pair with the package stylesheet to light up\n * `.lit` headings.\n *\n * No-ops (writes static fallbacks) under reduced motion or a coarse pointer.\n */\nexport function useSpotlight({\n lerp = 0.1,\n staticX = '82%',\n staticY = '16%',\n}: SpotlightOptions = {}) {\n const reduced = useReducedMotion()\n\n useEffect(() => {\n const root = document.documentElement\n const fine = window.matchMedia('(pointer: fine)').matches\n\n if (reduced || !fine) {\n root.style.setProperty('--mx', staticX)\n root.style.setProperty('--my', staticY)\n return\n }\n\n const target = { x: window.innerWidth * 0.5, y: window.innerHeight * 0.3 }\n const pos = { ...target }\n let raf = 0\n\n const tick = () => {\n pos.x += (target.x - pos.x) * lerp\n pos.y += (target.y - pos.y) * lerp\n if (Math.abs(target.x - pos.x) < 0.5 && Math.abs(target.y - pos.y) < 0.5) {\n pos.x = target.x\n pos.y = target.y\n }\n root.style.setProperty('--mx', `${pos.x}px`)\n root.style.setProperty('--my', `${pos.y}px`)\n raf = pos.x !== target.x || pos.y !== target.y ? requestAnimationFrame(tick) : 0\n }\n\n const ensureLoop = () => {\n if (!raf) raf = requestAnimationFrame(tick)\n }\n\n const onMove = (event: PointerEvent) => {\n target.x = event.clientX\n target.y = event.clientY\n ensureLoop()\n }\n\n root.classList.add('spotlight-live')\n root.style.setProperty('--mx', `${pos.x}px`)\n root.style.setProperty('--my', `${pos.y}px`)\n window.addEventListener('pointermove', onMove, { passive: true })\n\n return () => {\n if (raf) cancelAnimationFrame(raf)\n window.removeEventListener('pointermove', onMove)\n root.classList.remove('spotlight-live')\n }\n }, [reduced, lerp, staticX, staticY])\n}\n","import { useEffect, useRef } from 'react'\n\nexport type MagneticOptions = {\n /** Fraction of cursor distance applied as pull. */\n strength?: number\n /** Activation radius (px) measured from the element's edge. */\n radius?: number\n /** Maximum translation (px) per axis — clamps the pull so the hit target never flies away. */\n maxOffset?: number\n /** Per-frame smoothing factor (0–1) for both follow and snap-back. */\n smoothing?: number\n}\n\n/**\n * Returns a ref for an element that is magnetically attracted toward the cursor\n * once it enters an activation radius. The pull is clamped, and the follow +\n * snap-back are both driven by an in-hook spring (no reliance on a CSS\n * transition). The rAF loop stops when the element settles and restarts on the\n * next pointer change — no perpetual frame loop. Scroll/resize re-measure the\n * element so a parked cursor never leaves a stale offset.\n *\n * No-ops under reduced motion or a coarse pointer.\n */\nexport function useMagnetic<T extends HTMLElement>({\n strength = 0.35,\n radius = 140,\n maxOffset = 12,\n smoothing = 0.15,\n}: MagneticOptions = {}) {\n const ref = useRef<T>(null)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const fine = window.matchMedia('(pointer: fine)').matches\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n if (!fine || reduced) return\n\n const pointer = { x: 0, y: 0 }\n const target = { x: 0, y: 0 }\n const current = { x: 0, y: 0 }\n let dirty = false\n let raf = 0\n\n const clamp = (v: number) => Math.max(-maxOffset, Math.min(maxOffset, v))\n\n const tick = () => {\n if (dirty) {\n dirty = false\n const rect = el.getBoundingClientRect()\n const dx = pointer.x - (rect.left + rect.width / 2)\n const dy = pointer.y - (rect.top + rect.height / 2)\n const range = radius + Math.max(rect.width, rect.height) / 2\n const inRange = dx * dx + dy * dy < range * range\n target.x = inRange ? clamp(dx * strength) : 0\n target.y = inRange ? clamp(dy * strength) : 0\n }\n\n current.x += (target.x - current.x) * smoothing\n current.y += (target.y - current.y) * smoothing\n if (Math.abs(target.x - current.x) < 0.05 && Math.abs(target.y - current.y) < 0.05) {\n current.x = target.x\n current.y = target.y\n }\n\n el.style.transform =\n current.x === 0 && current.y === 0\n ? ''\n : `translate(${current.x.toFixed(2)}px, ${current.y.toFixed(2)}px)`\n\n if (dirty || current.x !== target.x || current.y !== target.y) {\n raf = requestAnimationFrame(tick)\n } else {\n raf = 0 // settled — idle until the next pointer change\n }\n }\n\n const ensureLoop = () => {\n if (!raf) raf = requestAnimationFrame(tick)\n }\n\n const onMove = (event: PointerEvent) => {\n pointer.x = event.clientX\n pointer.y = event.clientY\n dirty = true\n ensureLoop()\n }\n\n // The element can move under a stationary cursor; re-measure on reflow.\n const onReflow = () => {\n dirty = true\n ensureLoop()\n }\n\n const reset = () => {\n pointer.x = Number.NEGATIVE_INFINITY\n pointer.y = Number.NEGATIVE_INFINITY\n dirty = true\n ensureLoop()\n }\n\n window.addEventListener('pointermove', onMove, { passive: true })\n window.addEventListener('scroll', onReflow, { passive: true, capture: true })\n window.addEventListener('resize', onReflow, { passive: true })\n window.addEventListener('blur', reset)\n\n return () => {\n if (raf) cancelAnimationFrame(raf)\n window.removeEventListener('pointermove', onMove)\n window.removeEventListener('scroll', onReflow, { capture: true } as EventListenerOptions)\n window.removeEventListener('resize', onReflow)\n window.removeEventListener('blur', reset)\n el.style.transform = ''\n }\n }, [strength, radius, maxOffset, smoothing])\n\n return ref\n}\n","/** Shared easing curve — a soft expo-out. */\nexport const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]\n\n/** Default scroll-reveal timing. */\nexport const REVEAL_DURATION = 0.6\nexport const REVEAL_RISE = 16\nexport const REVEAL_STAGGER = 0.08\n","import { motion, useReducedMotion, type Variants } from 'framer-motion'\nimport type { ReactNode } from 'react'\nimport { EASE, REVEAL_DURATION, REVEAL_RISE } from './motion'\n\nexport type RevealProps = {\n children: ReactNode\n /** Delay (s) before the reveal begins. */\n delay?: number\n /** Whether the content rises as it fades in. */\n rise?: boolean\n className?: string\n}\n\n/**\n * Fades (and optionally rises) its children into view once, when scrolled near.\n * Under reduced motion it renders a plain wrapper with no animation at all.\n */\nexport function Reveal({ children, delay = 0, rise = true, className }: RevealProps) {\n const reduced = useReducedMotion()\n\n if (reduced) {\n return <div className={className}>{children}</div>\n }\n\n const variants: Variants = rise\n ? { hidden: { opacity: 0, y: REVEAL_RISE }, visible: { opacity: 1, y: 0 } }\n : { hidden: { opacity: 0 }, visible: { opacity: 1 } }\n\n return (\n <motion.div\n className={className}\n variants={variants}\n initial=\"hidden\"\n whileInView=\"visible\"\n viewport={{ once: true, margin: '-15%' }}\n transition={{ duration: REVEAL_DURATION, ease: EASE, delay }}\n >\n {children}\n </motion.div>\n )\n}\n","type ViewTransitionDocument = Document & {\n startViewTransition?: (callback: () => void) => unknown\n}\n\n/**\n * Runs `update` inside a View Transition when supported, otherwise applies it\n * synchronously. Skips the transition entirely under reduced motion.\n */\nexport function withViewTransition(update: () => void) {\n const doc = document as ViewTransitionDocument\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n if (reduced || typeof doc.startViewTransition !== 'function') {\n update()\n return\n }\n\n doc.startViewTransition(update)\n}\n"],"mappings":";;;;AAqBA,SAAgB,EAAa,EAC3B,UAAO,IACP,aAAU,OACV,aAAU,UACU,CAAC,GAAG;CACxB,IAAM,IAAU,EAAiB;CAEjC,QAAgB;EACd,IAAM,IAAO,SAAS,iBAChB,IAAO,OAAO,WAAW,iBAAiB,EAAE;EAElD,IAAI,KAAW,CAAC,GAAM;GAEpB,AADA,EAAK,MAAM,YAAY,QAAQ,CAAO,GACtC,EAAK,MAAM,YAAY,QAAQ,CAAO;GACtC;EACF;EAEA,IAAM,IAAS;GAAE,GAAG,OAAO,aAAa;GAAK,GAAG,OAAO,cAAc;EAAI,GACnE,IAAM,EAAE,GAAG,EAAO,GACpB,IAAM,GAEJ,UAAa;GASjB,AARA,EAAI,MAAM,EAAO,IAAI,EAAI,KAAK,GAC9B,EAAI,MAAM,EAAO,IAAI,EAAI,KAAK,GAC1B,KAAK,IAAI,EAAO,IAAI,EAAI,CAAC,IAAI,MAAO,KAAK,IAAI,EAAO,IAAI,EAAI,CAAC,IAAI,OACnE,EAAI,IAAI,EAAO,GACf,EAAI,IAAI,EAAO,IAEjB,EAAK,MAAM,YAAY,QAAQ,GAAG,EAAI,EAAE,GAAG,GAC3C,EAAK,MAAM,YAAY,QAAQ,GAAG,EAAI,EAAE,GAAG,GAC3C,IAAM,EAAI,MAAM,EAAO,KAAK,EAAI,MAAM,EAAO,IAAI,sBAAsB,CAAI,IAAI;EACjF,GAEM,UAAmB;GACvB,AAAU,MAAM,sBAAsB,CAAI;EAC5C,GAEM,KAAU,MAAwB;GAGtC,AAFA,EAAO,IAAI,EAAM,SACjB,EAAO,IAAI,EAAM,SACjB,EAAW;EACb;EAOA,OALA,EAAK,UAAU,IAAI,gBAAgB,GACnC,EAAK,MAAM,YAAY,QAAQ,GAAG,EAAI,EAAE,GAAG,GAC3C,EAAK,MAAM,YAAY,QAAQ,GAAG,EAAI,EAAE,GAAG,GAC3C,OAAO,iBAAiB,eAAe,GAAQ,EAAE,SAAS,GAAK,CAAC,SAEnD;GAGX,AAFI,KAAK,qBAAqB,CAAG,GACjC,OAAO,oBAAoB,eAAe,CAAM,GAChD,EAAK,UAAU,OAAO,gBAAgB;EACxC;CACF,GAAG;EAAC;EAAS;EAAM;EAAS;CAAO,CAAC;AACtC;;;ACpDA,SAAgB,EAAmC,EACjD,cAAW,KACX,YAAS,KACT,eAAY,IACZ,eAAY,QACO,CAAC,GAAG;CACvB,IAAM,IAAM,EAAU,IAAI;CAwF1B,OAtFA,QAAgB;EACd,IAAM,IAAK,EAAI;EACf,IAAI,CAAC,GAAI;EAET,IAAM,IAAO,OAAO,WAAW,iBAAiB,EAAE,SAC5C,IAAU,OAAO,WAAW,kCAAkC,EAAE;EACtE,IAAI,CAAC,KAAQ,GAAS;EAEtB,IAAM,IAAU;GAAE,GAAG;GAAG,GAAG;EAAE,GACvB,IAAS;GAAE,GAAG;GAAG,GAAG;EAAE,GACtB,IAAU;GAAE,GAAG;GAAG,GAAG;EAAE,GACzB,IAAQ,IACR,IAAM,GAEJ,KAAS,MAAc,KAAK,IAAI,CAAC,GAAW,KAAK,IAAI,GAAW,CAAC,CAAC,GAElE,UAAa;GACjB,IAAI,GAAO;IACT,IAAQ;IACR,IAAM,IAAO,EAAG,sBAAsB,GAChC,IAAK,EAAQ,KAAK,EAAK,OAAO,EAAK,QAAQ,IAC3C,IAAK,EAAQ,KAAK,EAAK,MAAM,EAAK,SAAS,IAC3C,IAAQ,IAAS,KAAK,IAAI,EAAK,OAAO,EAAK,MAAM,IAAI,GACrD,IAAU,IAAK,IAAK,IAAK,IAAK,IAAQ;IAE5C,AADA,EAAO,IAAI,IAAU,EAAM,IAAK,CAAQ,IAAI,GAC5C,EAAO,IAAI,IAAU,EAAM,IAAK,CAAQ,IAAI;GAC9C;GAcA,AAZA,EAAQ,MAAM,EAAO,IAAI,EAAQ,KAAK,GACtC,EAAQ,MAAM,EAAO,IAAI,EAAQ,KAAK,GAClC,KAAK,IAAI,EAAO,IAAI,EAAQ,CAAC,IAAI,OAAQ,KAAK,IAAI,EAAO,IAAI,EAAQ,CAAC,IAAI,QAC5E,EAAQ,IAAI,EAAO,GACnB,EAAQ,IAAI,EAAO,IAGrB,EAAG,MAAM,YACP,EAAQ,MAAM,KAAK,EAAQ,MAAM,IAC7B,KACA,aAAa,EAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAQ,EAAE,QAAQ,CAAC,EAAE,MAEnE,AAGE,IAHE,KAAS,EAAQ,MAAM,EAAO,KAAK,EAAQ,MAAM,EAAO,IACpD,sBAAsB,CAAI,IAE1B;EAEV,GAEM,UAAmB;GACvB,AAAU,MAAM,sBAAsB,CAAI;EAC5C,GAEM,KAAU,MAAwB;GAItC,AAHA,EAAQ,IAAI,EAAM,SAClB,EAAQ,IAAI,EAAM,SAClB,IAAQ,IACR,EAAW;EACb,GAGM,UAAiB;GAErB,AADA,IAAQ,IACR,EAAW;EACb,GAEM,UAAc;GAIlB,AAHA,EAAQ,IAAI,WACZ,EAAQ,IAAI,WACZ,IAAQ,IACR,EAAW;EACb;EAOA,OALA,OAAO,iBAAiB,eAAe,GAAQ,EAAE,SAAS,GAAK,CAAC,GAChE,OAAO,iBAAiB,UAAU,GAAU;GAAE,SAAS;GAAM,SAAS;EAAK,CAAC,GAC5E,OAAO,iBAAiB,UAAU,GAAU,EAAE,SAAS,GAAK,CAAC,GAC7D,OAAO,iBAAiB,QAAQ,CAAK,SAExB;GAMX,AALI,KAAK,qBAAqB,CAAG,GACjC,OAAO,oBAAoB,eAAe,CAAM,GAChD,OAAO,oBAAoB,UAAU,GAAU,EAAE,SAAS,GAAK,CAAyB,GACxF,OAAO,oBAAoB,UAAU,CAAQ,GAC7C,OAAO,oBAAoB,QAAQ,CAAK,GACxC,EAAG,MAAM,YAAY;EACvB;CACF,GAAG;EAAC;EAAU;EAAQ;EAAW;CAAS,CAAC,GAEpC;AACT;;;ACrHA,IAAa,IAAyC;CAAC;CAAM;CAAG;CAAM;AAAC,GAG1D,IAAkB,IAClB,IAAc,IACd,IAAiB;;;ACW9B,SAAgB,EAAO,EAAE,aAAU,WAAQ,GAAG,UAAO,IAAM,gBAA0B;CAGnF,IAFgB,EAEZ,GACF,OAAO,kBAAC,OAAD;EAAgB;EAAY;CAAc,CAAA;CAGnD,IAAM,IAAqB,IACvB;EAAE,QAAQ;GAAE,SAAS;GAAG,GAAA;EAAe;EAAG,SAAS;GAAE,SAAS;GAAG,GAAG;EAAE;CAAE,IACxE;EAAE,QAAQ,EAAE,SAAS,EAAE;EAAG,SAAS,EAAE,SAAS,EAAE;CAAE;CAEtD,OACE,kBAAC,EAAO,KAAR;EACa;EACD;EACV,SAAQ;EACR,aAAY;EACZ,UAAU;GAAE,MAAM;GAAM,QAAQ;EAAO;EACvC,YAAY;GAAE,UAAU;GAAiB,MAAM;GAAM;EAAM;EAE1D;CACS,CAAA;AAEhB;;;AChCA,SAAgB,EAAmB,GAAoB;CACrD,IAAM,IAAM;CAGZ,IAFgB,OAAO,WAAW,kCAAkC,EAAE,WAEvD,OAAO,EAAI,uBAAwB,YAAY;EAC5D,EAAO;EACP;CACF;CAEA,EAAI,oBAAoB,CAAM;AAChC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "warm-motion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tasteful, reduced-motion-aware React interaction primitives — cursor spotlight, magnetic hover, scroll reveals, view transitions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Marek Žiška",
|
|
8
|
+
"homepage": "https://github.com/marekzska/resume/tree/main/packages/warm-motion",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/marekzska/resume.git",
|
|
12
|
+
"directory": "packages/warm-motion"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"animation",
|
|
17
|
+
"framer-motion",
|
|
18
|
+
"reduced-motion",
|
|
19
|
+
"accessibility",
|
|
20
|
+
"hooks",
|
|
21
|
+
"spotlight",
|
|
22
|
+
"magnetic"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"*.css"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/warm-motion.cjs",
|
|
31
|
+
"module": "./dist/warm-motion.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/warm-motion.js",
|
|
37
|
+
"require": "./dist/warm-motion.cjs"
|
|
38
|
+
},
|
|
39
|
+
"./styles.css": "./dist/styles.css"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "vite build && tsc -p tsconfig.build.json && node -e \"require('fs').copyFileSync('src/styles.css','dist/styles.css')\"",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"lint": "eslint ."
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"framer-motion": ">=11 <13",
|
|
50
|
+
"react": ">=18 <20"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
54
|
+
"@testing-library/react": "^16.1.0",
|
|
55
|
+
"@types/react": "^19.2.14",
|
|
56
|
+
"@types/react-dom": "^19.2.3",
|
|
57
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
58
|
+
"framer-motion": "^12.40.0",
|
|
59
|
+
"jsdom": "^25.0.1",
|
|
60
|
+
"react": "^19.2.6",
|
|
61
|
+
"react-dom": "^19.2.6",
|
|
62
|
+
"typescript": "~6.0.2",
|
|
63
|
+
"vite": "^8.0.12",
|
|
64
|
+
"vitest": "^3.2.0"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|