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 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
@@ -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;
@@ -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';
@@ -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;
@@ -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,5 @@
1
+ /**
2
+ * Runs `update` inside a View Transition when supported, otherwise applies it
3
+ * synchronously. Skips the transition entirely under reduced motion.
4
+ */
5
+ export declare function withViewTransition(update: () => void): 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":"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"}
@@ -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
+ }