@liiift-studio/magnettype 1.1.3 → 1.1.5

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/%40liiift-studio%2Fmagnettype.svg)](https://www.npmjs.com/package/@liiift-studio/magnettype) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![part of liiift type-tools](https://img.shields.io/badge/liiift-type--tools-blueviolet)](https://github.com/Liiift-Studio/type-tools)
4
4
 
5
- CSS `font-variation-settings` applies a single value to the whole element — there is no native way to drive axis values per word from cursor proximity, or to selectively widen visually confusable characters for legibility. magnetType adds both.
5
+ CSS `font-variation-settings` applies a single value to the whole element — there is no native way to drive axis values per word from cursor proximity, to selectively widen visually confusable characters for legibility, or to vary weight per-character across a block element. magnetType adds all three.
6
6
 
7
7
  **[magnettype.com](https://magnettype.com)** · [npm](https://www.npmjs.com/package/@liiift-studio/magnettype) · [GitHub](https://github.com/Liiift-Studio/MagnetType)
8
8
 
@@ -16,15 +16,15 @@ TypeScript · Zero dependencies · React + Vanilla JS
16
16
  npm install @liiift-studio/magnettype
17
17
  ```
18
18
 
19
+ > **Variable font required:** magnetType sets `font-variation-settings` per word or per character. The target font must support the axes you specify (e.g. a font with a `wght` axis for weight-based effects, or a `wdth` axis for legibility mode). The effect is invisible with non-variable fonts.
20
+
19
21
  ---
20
22
 
21
23
  ## Usage
22
24
 
23
- > **Next.js App Router:** this library uses browser APIs. Add `"use client"` to any component file that imports from it.
24
-
25
- > **Variable font required:** magnetType sets `font-variation-settings` per word or per character. The target font must support the axes you specify (e.g. a font with a `wght` axis for weight-based field effects, or a `wdth` axis for legibility mode). The effect is invisible with fonts that do not have variable axis support.
25
+ ### React field mode (`MagnetTypeText`)
26
26
 
27
- ### React component field mode
27
+ Per-word cursor-proximity weight variation driven by a continuous rAF loop.
28
28
 
29
29
  ```tsx
30
30
  import { MagnetTypeText } from '@liiift-studio/magnettype'
@@ -40,19 +40,70 @@ import { MagnetTypeText } from '@liiift-studio/magnettype'
40
40
  </MagnetTypeText>
41
41
  ```
42
42
 
43
+ ### React — block mode (`MagnetBlock`)
44
+
45
+ Per-character cursor-proximity weight variation. Works with mixed content (inline elements, links, `<code>`, etc.) inside any block element. Characters are wrapped as React elements — no DOM mutation.
46
+
47
+ ```tsx
48
+ import { MagnetBlock } from '@liiift-studio/magnettype'
49
+
50
+ // Per-character spread — each character responds to cursor distance
51
+ <MagnetBlock
52
+ spreadRadius={200}
53
+ minWeight={300}
54
+ maxWeight={700}
55
+ >
56
+ Typography that responds to presence.
57
+ </MagnetBlock>
58
+
59
+ // Whole-element gate — the effect only activates when the cursor is within proximityRadius of the element edge
60
+ <MagnetBlock
61
+ proximityRadius={120}
62
+ minWeight={300}
63
+ maxWeight={700}
64
+ >
65
+ Weight rises when the cursor enters.
66
+ </MagnetBlock>
67
+
68
+ // Both combined — proximity gates the spread effect
69
+ <MagnetBlock
70
+ proximityRadius={200}
71
+ spreadRadius={120}
72
+ minWeight={300}
73
+ maxWeight={700}
74
+ >
75
+ Only spreads when the cursor is close.
76
+ </MagnetBlock>
77
+ ```
78
+
79
+ **`MagnetBlock` props:**
80
+
81
+ | Prop | Default | Description |
82
+ |------|---------|-------------|
83
+ | `as` | `'p'` | HTML element to render — `'h1'`, `'div'`, `'span'`, etc. |
84
+ | `minWeight` | `300` | `wght` axis value at rest (cursor beyond any radius) |
85
+ | `maxWeight` | `600` | `wght` axis value at peak (cursor directly over the character) |
86
+ | `spreadRadius` | — | Pixel distance from the cursor within which each **character's** weight rises to `maxWeight`. When omitted, per-character splitting is skipped |
87
+ | `proximityRadius` | — | Pixel distance from the element **edge** that gates the effect. Without `spreadRadius`, drives a whole-element weight ramp. With `spreadRadius`, acts as an outer gate — the spread only fires while the cursor is within this distance |
88
+ | `fixedAxes` | `{}` | Additional axis values to hold constant in every `font-variation-settings` string (e.g. `{ opsz: 144 }`) |
89
+ | `stabilizeLayout` | `true` | Apply compensating letter-spacing to prevent text reflow as weight rises. Measures the element's width at rest and peak weight via an off-screen probe and applies proportional negative letter-spacing per character. Disable if you want natural bold spacing, or if your font expands glyphs very unevenly (compensation is a per-element average) |
90
+ | `cachePositions` | `true` | Cache character centre positions in page-relative coordinates, eliminating `getBoundingClientRect` calls on every `mousemove`. Rebuilt on resize and after fonts load. Disable if the element is inside a custom scroll container (`overflow: scroll` on a non-window element) |
91
+ | `rafThrottle` | `true` | Throttle proximity updates to one per animation frame (≈ 60 fps on most displays). Disable for lowest input latency on 120 Hz displays or very fast-moving effects |
92
+ | `className` | — | Forwarded to the root element |
93
+ | `style` | — | Merged with the root element's style; `fontVariationSettings` at `minWeight` is set as the base |
94
+
43
95
  ### React hook — field mode
44
96
 
45
97
  ```tsx
46
98
  import { useMagnetType } from '@liiift-studio/magnettype'
47
99
 
48
- // Inside a React component:
49
100
  const ref = useMagnetType({ mode: 'field', axes: { wght: [300, 700] }, radius: 150 })
50
101
  return <p ref={ref}>{children}</p>
51
102
  ```
52
103
 
53
- The hook starts the cursor-proximity rAF loop on mount and tears it down cleanly on unmount. In field mode, no `ResizeObserver` is needed — the loop reads live `getBoundingClientRect` positions on every frame. After fonts load (`document.fonts.ready`), the hook re-runs to ensure measurements are taken on the loaded font.
104
+ The hook starts the cursor-proximity rAF loop on mount and tears it down cleanly on unmount. After fonts load (`document.fonts.ready`), the hook re-runs to ensure measurements are taken on the loaded font.
54
105
 
55
- ### React component — legibility mode
106
+ ### React — legibility mode
56
107
 
57
108
  ```tsx
58
109
  import { MagnetTypeText } from '@liiift-studio/magnettype'
@@ -112,7 +163,7 @@ ro.observe(el)
112
163
  ### TypeScript
113
164
 
114
165
  ```ts
115
- import type { MagnetTypeOptions, FalloffType, MagnetModeType } from '@liiift-studio/magnettype'
166
+ import type { MagnetTypeOptions, FalloffType, MagnetModeType, MagnetBlockProps } from '@liiift-studio/magnettype'
116
167
 
117
168
  const fieldOpts: MagnetTypeOptions = {
118
169
  mode: 'field',
@@ -130,18 +181,18 @@ const legibilityOpts: MagnetTypeOptions = {
130
181
 
131
182
  ---
132
183
 
133
- ## Options
184
+ ## Field mode options (`MagnetTypeText` / `useMagnetType` / vanilla JS)
134
185
 
135
186
  | Option | Default | Description |
136
187
  |--------|---------|-------------|
137
- | `mode` | `'field'` | `'field'` — cursor proximity drives per-word `font-variation-settings` via a continuous rAF loop. `'legibility'` — static per-character `wdth` boost applied to visually confusable characters; no cursor interaction needed |
138
- | `axes` | `{ wght: [300, 500] }` | *(field mode only)* Map of axis tag → `[restValue, peakValue]`. `restValue` is applied when the cursor is beyond the radius; `peakValue` when the cursor is directly over the word. Multiple axes are supported simultaneously |
139
- | `radius` | `120` | *(field mode only)* Pixel radius over which the field effect fades. Words with their centre beyond this distance from the cursor receive `restValue` |
140
- | `falloff` | `'quadratic'` | *(field mode only)* Falloff curve. `'linear'` strength decreases linearly with distance. `'quadratic'` strength decreases as distance², giving a tighter hot zone and a sharper peak feel |
141
- | `magnetMode` | `'attract'` | *(field mode only)* `'attract'` — words near the cursor approach `peakValue`. `'repel'` — words near the cursor stay at `restValue`; words farther away approach `peakValue` |
142
- | `wdthBoost` | `6` | *(legibility mode only)* `wdth` axis units added to confusable characters, scaled by risk level. Risk 3 characters (`i l 1 I`) receive the full boost; risk 2 characters (`r 0 O`) receive ⅔; risk 1 characters (`n m o b d p q c e`) receive ⅓ |
143
- | `transitionMs` | `0` | Duration in milliseconds for the CSS transition back to rest values when the cursor leaves (or a touch ends). `0` preserves the existing instant snap. When > 0, `font-variation-settings` animates back over the given duration using `ease` easing. The transition is cleared immediately on the next `mousemove`/`touchmove` so live tracking is not delayed |
144
- | `as` | `'p'` | HTML element to render, e.g. `'h1'`, `'div'`, `'span'`. Accepts any valid React element type. *(React component only)* |
188
+ | `mode` | `'field'` | `'field'` — cursor proximity drives per-word `font-variation-settings` via a continuous rAF loop. `'legibility'` — static per-character `wdth` boost on visually confusable characters |
189
+ | `axes` | `{ wght: [300, 500] }` | *(field mode)* Map of axis tag → `[restValue, peakValue]` |
190
+ | `radius` | `120` | *(field mode)* Pixel radius over which the field effect fades from each word's centre |
191
+ | `falloff` | `'quadratic'` | *(field mode)* `'linear'` or `'quadratic'` falloff curve |
192
+ | `magnetMode` | `'attract'` | *(field mode)* `'attract'` — near words approach `peakValue`. `'repel'` — near words stay at `restValue`, far words approach `peakValue` |
193
+ | `wdthBoost` | `6` | *(legibility mode)* `wdth` units added to confusable characters, scaled by risk: `il1I` (risk 3) get the full boost; `r 0 O` (risk 2) get ⅔; `n m o b d p q c e` (risk 1) get ⅓ |
194
+ | `transitionMs` | `0` | Duration in ms for CSS transition back to rest when cursor leaves. `0` = instant snap. Cleared on the next `mousemove` so live tracking is not delayed |
195
+ | `as` | `'p'` | HTML element to render. *(React component only)* |
145
196
 
146
197
  ---
147
198
 
@@ -149,30 +200,32 @@ const legibilityOpts: MagnetTypeOptions = {
149
200
 
150
201
  ### Field mode
151
202
 
152
- On activation, magnetType wraps each word in the element in an `mt-word` span. A `mousemove` listener records the cursor's `clientX`/`clientY` coordinates, and a `requestAnimationFrame` loop runs continuously while the cursor is inside the element. Each frame, the loop batch-reads every word span's `getBoundingClientRect`, computes the Euclidean distance from the cursor to each word's centre, and maps that distance through the falloff formula to a normalised strength value in `[0, 1]`:
203
+ On activation, magnetType wraps each word in an `mt-word` span. A `mousemove` listener records cursor coordinates, and a `requestAnimationFrame` loop runs while the cursor is inside the element. Each frame, the loop batch-reads every word span's `getBoundingClientRect`, computes Euclidean distance from cursor to word centre, and maps it through the falloff formula:
153
204
 
154
205
  ```
155
206
  normalised = max(0, 1 − distance / radius)
156
207
  strength = normalised² (quadratic) or normalised (linear)
157
208
  ```
158
209
 
159
- Each word's `font-variation-settings` is then set to the interpolated axis value between `restValue` and `peakValue`, with `attract` mode mapping `strength=1` to `peakValue` and `repel` mode inverting that relationship. Reads are batched before writes on every frame to avoid layout thrashing. When the cursor leaves the element, the loop fires one final frame to reset all words to `restValue`, then idles.
210
+ Each word's `font-variation-settings` interpolates between `restValue` and `peakValue` at that strength. Reads are batched before writes on every frame to avoid layout thrashing. When the cursor leaves, one final frame resets all words to `restValue`.
160
211
 
161
- The base `fontVariationSettings` string is read from the computed style of the element once at startup, and each per-word override patches only the affected axes — all parent-defined axes are preserved.
212
+ ### Block mode (`MagnetBlock`)
213
+
214
+ `MagnetBlock` splits string children into per-character `<span>` elements during the React render pass using `useMemo` — no DOM mutation. Callback refs collect each span element. On `mousemove` (and on `scroll`, using the stored last position), the component reads each span's `getBoundingClientRect`, computes cursor-to-character-centre distance, and sets `font-variation-settings` directly on the span's style. This is passive and batched per frame via the event handler.
215
+
216
+ `proximityRadius` measures cursor distance to the element **edge** (not its centre) — useful as an outer gate so the effect only fires when the cursor is actually near the block. `spreadRadius` measures cursor distance to each **character centre** — controls how wide the weight gradient spreads around the cursor within the block. Both are independent and combinable.
162
217
 
163
218
  ### Legibility mode
164
219
 
165
- magnetType scans all text nodes in the element using recursive `childNodes` traversal and checks each character against a built-in confusable character table. Confusable characters are grouped into three risk levels: `il1I` (risk 3, high confusion), `r 0 O` and related pairs (risk 2), and `n m o b d p q c e` (risk 1, low confusion). Each confusable character is wrapped in an `mt-char` span with a `wdth` axis boost proportional to its risk level — making similar-looking characters slightly wider and more distinct. Non-confusable characters pass through as plain text nodes, with adjacent non-confusable characters consolidated into single text nodes to keep the DOM lean.
220
+ magnetType scans text nodes recursively and checks each character against a built-in confusable character table. Confusable characters are wrapped in `mt-char` spans with a `wdth` boost proportional to risk level. Non-confusable characters pass through as plain text nodes.
166
221
 
167
222
  ### No layout shift
168
223
 
169
- In field mode, the rAF loop drives only `font-variation-settings` values on per-word spans it does not change element widths, margins, padding, or position. If you use only a `wght` axis, advance widths are not affected and no reflow occurs. If you include a `wdth` axis, character advance widths will change, which may cause lines to reflow. To prevent this, consider constraining axis ranges or combining with a `scaleX` transform on the container.
170
-
171
- In legibility mode, the `wdth` axis boost widens individual confusable characters, which shifts surrounding characters slightly. This is intentional — the point is to make the characters physically wider and more distinct. The shift is small by default (`wdthBoost: 6`) and does not cause line breaks to change.
224
+ Field mode and block mode drive only `font-variation-settings` on per-word or per-character spans. If you use only a `wght` axis, advance widths are unaffected and no reflow occurs. If you include a `wdth` axis, character advance widths change and lines may reflow consider constraining axis ranges or pairing with a `scaleX` transform.
172
225
 
173
226
  ### `prefers-reduced-motion`
174
227
 
175
- Field mode respects `prefers-reduced-motion: reduce`. If the media query matches at the time `startMagnetType` is called, the function returns immediately without wrapping words or starting the rAF loop, and returns a no-op stop function. Legibility mode is a static DOM transformation and is not affected by this preference.
228
+ Field mode respects `prefers-reduced-motion: reduce`. If the media query matches at activation time, the function returns immediately without wrapping words or starting the rAF loop. Legibility mode and block mode are not affected.
176
229
 
177
230
  ---
178
231
 
@@ -188,10 +241,10 @@ The package itself has zero runtime dependencies. Do not remove this entry.
188
241
 
189
242
  ## Future improvements
190
243
 
191
- - **Custom confusable table** — allow callers to pass their own `Record<string, number>` to override or extend the built-in character risk map for language- or font-specific tuning
192
- - **Axis clamping** — optional per-axis min/max clamp to prevent axis values from exceeding a font's supported range, avoiding undefined browser rendering behaviour
193
- - **SSR hydration** — pre-render legibility mode markup on the server so boosted characters are present from first paint without a client-side flash
244
+ - **Custom confusable table** — allow callers to pass their own `Record<string, number>` to override or extend the built-in character risk map
245
+ - **Axis clamping** — optional per-axis min/max clamp to prevent values from exceeding a font's supported range
246
+ - **SSR hydration** — pre-render legibility mode markup on the server so boosted characters are present from first paint
194
247
 
195
248
  ---
196
249
 
197
- Current version: 0.1.3
250
+ Current version: 1.1.5
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("react"),H=require("react/jsx-runtime"),G={i:3,l:3,1:3,I:3,r:2,n:1,m:1,0:2,O:2,o:1,b:1,d:1,p:1,q:1,c:1,e:1},R={word:"mt-word",char:"mt-char",probe:"mt-probe"},$={axes:{wght:[300,500]},radius:120,falloff:"quadratic",magnetMode:"attract",wdthBoost:6,scope:"document"};function D(t,n=[]){return t.nodeType===Node.TEXT_NODE?n.push(t):t.childNodes.forEach(o=>D(o,n)),n}function P(t,n,o){if(!t||t==="normal")return`"${n}" ${o}`;const u=new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`),c=`"${n}" ${o}`;return u.test(t)?t.replace(u,c):`${t}, ${c}`}function K(t,n){let o=t;for(const[u,c]of Object.entries(n))o=P(o,u,c);return o}function U(t,n,o){if(n.opacity!==void 0){const[u,c]=n.opacity;t.style.opacity=String(u+(c-u)*o)}n.italic===!0&&(t.style.fontStyle=o>.5?"italic":"")}function _(t,n){n.opacity!==void 0&&(t.style.opacity=String(n.opacity[0])),n.italic===!0&&(t.style.fontStyle="")}function z(t){const n=t.cloneNode(!0),o=n.querySelectorAll(`.${R.word}, .${R.char}`);return Array.from(o).reverse().forEach(c=>{const a=c.parentNode;if(a){for(;c.firstChild;)a.insertBefore(c.firstChild,c);a.removeChild(c)}}),n.innerHTML}function ot(t,n){t.innerHTML=n}function Q(t,n,o={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=n,()=>{};const u=o.wdthBoost??$.wdthBoost,c=o.radius??$.radius,a=o.falloff??$.falloff,T=o.scope??$.scope,y=o.props,l=o.transitionMs??0,C=window.scrollY;t.innerHTML=n;const N=getComputedStyle(t).fontVariationSettings,F=N.match(/"wdth"\s+([\d.eE+-]+)/),g=F?parseFloat(F[1]):100,q=D(t),w=[];for(const r of q){const e=r.textContent??"";if(!e||!e.split("").some(m=>m in G))continue;const A=document.createDocumentFragment();for(const m of e){const k=G[m];if(k===void 0){const L=A.lastChild;L&&L.nodeType===Node.TEXT_NODE?L.textContent+=m:A.appendChild(document.createTextNode(m))}else{const L=document.createElement("span");L.className=R.char,L.style.fontVariationSettings=P(N,"wdth",g),L.textContent=m,A.appendChild(L),w.push({span:L,riskLevel:k})}}r.parentNode.replaceChild(A,r)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-C)>2&&window.scrollTo({top:C,behavior:"instant"})}),w.length===0)return()=>{};y&&w.forEach(({span:r})=>_(r,y));let h=-9999,V=-9999,E=!1,i=0,B=!0,S=null;function s(){if(!B)return;if(!E){w.forEach(({span:e})=>{l>0&&(e.style.transition=`font-variation-settings ${l}ms ease`),e.style.fontVariationSettings=P(N,"wdth",g),y&&_(e,y)}),l>0&&(S!==null&&clearTimeout(S),S=setTimeout(()=>{w.forEach(({span:e})=>{e.style.transition=""}),S=null},l)),i=0;return}const r=w.map(({span:e})=>e.getBoundingClientRect());w.forEach(({span:e,riskLevel:x},A)=>{e.style.transition="";const m=r[A],k=m.left+m.width/2,L=m.top+m.height/2,j=Math.sqrt((h-k)**2+(V-L)**2),O=Math.max(0,1-j/c),Y=a==="quadratic"?O*O:O,X=u*(x/3)*Y;e.style.fontVariationSettings=P(N,"wdth",g+X),y&&U(e,y,Y)}),i=requestAnimationFrame(s)}function M(r){h=r.clientX,V=r.clientY,E||(E=!0),i===0&&(i=requestAnimationFrame(s))}function v(){E=!1,i===0&&(i=requestAnimationFrame(s))}function f(r){r.touches.length!==0&&(h=r.touches[0].clientX,V=r.touches[0].clientY,E||(E=!0),i===0&&(i=requestAnimationFrame(s)))}function b(){E=!1,i===0&&(i=requestAnimationFrame(s))}const d=T==="document"?document:t;return d.addEventListener("mousemove",M),d.addEventListener("mouseleave",v),d.addEventListener("touchmove",f,{passive:!0}),d.addEventListener("touchend",b),()=>{B=!1,cancelAnimationFrame(i),S!==null&&clearTimeout(S),d.removeEventListener("mousemove",M),d.removeEventListener("mouseleave",v),d.removeEventListener("touchmove",f),d.removeEventListener("touchend",b),t.innerHTML=n}}function Z(t,n,o={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=n,()=>{};const u=o.axes??$.axes,c=o.radius??$.radius,a=o.falloff??$.falloff,T=o.magnetMode??$.magnetMode,y=o.scope??$.scope,l=o.props,C=o.transitionMs??0,N=window.scrollY;t.innerHTML=n;const F=D(t),g=[];for(const r of F){const e=r.textContent??"";if(!e.trim())continue;const x=e.split(/(\S+)/),A=document.createDocumentFragment();for(let m=0;m<x.length;m+=2){const k=x[m],L=x[m+1];if(!L)continue;const O=x[m+3]===void 0?x[m+2]??"":"",Y=document.createElement("span");Y.className=R.word,Y.textContent=k+L+O,A.appendChild(Y),g.push(Y)}r.parentNode.replaceChild(A,r)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-N)>2&&window.scrollTo({top:N,behavior:"instant"})}),g.length===0)return()=>{};const q=getComputedStyle(t).fontVariationSettings,w=K(q,Object.fromEntries(Object.entries(u).map(([r,[e]])=>[r,e])));g.forEach(r=>{r.style.fontVariationSettings=w,l&&_(r,l)});let h=-9999,V=-9999,E=!1,i=0,B=!0,S=null;function s(){if(!B)return;if(!E){g.forEach(e=>{C>0&&(e.style.transition=`font-variation-settings ${C}ms ease`),e.style.fontVariationSettings=w,l&&_(e,l)}),C>0&&(S!==null&&clearTimeout(S),S=setTimeout(()=>{g.forEach(e=>{e.style.transition=""}),S=null},C)),i=0;return}const r=g.map(e=>e.getBoundingClientRect());g.forEach((e,x)=>{e.style.transition="";const A=r[x],m=A.left+A.width/2,k=A.top+A.height/2,L=Math.sqrt((h-m)**2+(V-k)**2),j=Math.max(0,1-L/c),O=a==="quadratic"?j*j:j,Y=T==="repel"?1-O:O,X={};for(const I of Object.keys(u)){const[J,nt]=u[I]??[300,500];X[I]=J+(nt-J)*Y}e.style.fontVariationSettings=K(q,X),l&&U(e,l,O)}),i=requestAnimationFrame(s)}function M(r){h=r.clientX,V=r.clientY,E||(E=!0),i===0&&(i=requestAnimationFrame(s))}function v(){E=!1,i===0&&(i=requestAnimationFrame(s))}function f(r){r.touches.length!==0&&(h=r.touches[0].clientX,V=r.touches[0].clientY,E||(E=!0),i===0&&(i=requestAnimationFrame(s)))}function b(){E=!1,i===0&&(i=requestAnimationFrame(s))}const d=y==="document"?document:t;return d.addEventListener("mousemove",M),d.addEventListener("mouseleave",v),d.addEventListener("touchmove",f,{passive:!0}),d.addEventListener("touchend",b),()=>{B=!1,cancelAnimationFrame(i),S!==null&&clearTimeout(S),d.removeEventListener("mousemove",M),d.removeEventListener("mouseleave",v),d.removeEventListener("touchmove",f),d.removeEventListener("touchend",b),t.innerHTML=n}}function W(t){const n=p.useRef(null),o=p.useRef(null),u=p.useRef(t);u.current=t;const c=p.useRef(null),a=t.mode??"field",{axes:T,radius:y,falloff:l,magnetMode:C,wdthBoost:N,scope:F}=t,g=T?JSON.stringify(T):void 0,q=t.props?JSON.stringify(t.props):void 0,w=p.useCallback(()=>{const h=n.current;if(!h)return;o.current===null&&(o.current=z(h)),c.current&&(c.current(),c.current=null),(u.current.mode??"field")==="field"?c.current=Z(h,o.current,u.current):c.current=Q(h,o.current,u.current)},[a,g,y,l,C,N,F,q]);return p.useLayoutEffect(()=>(w(),()=>{c.current&&(c.current(),c.current=null)}),[w]),p.useEffect(()=>{document.fonts.ready.then(w)},[w]),n}const tt=p.forwardRef(function({children:n,as:o="p",className:u,style:c,...a},T){const y=W(a),l=p.useCallback(C=>{y.current=C,typeof T=="function"?T(C):T&&(T.current=C)},[T]);return H.jsx(o,{ref:l,className:u,style:c,children:n})});tt.displayName="MagnetTypeText";const et=p.forwardRef(function({children:n,as:o="p",className:u,style:c,minWeight:a=300,maxWeight:T=600,proximityRadius:y,spreadRadius:l,fixedAxes:C={}},N){const F=p.useRef(null),g=p.useRef(null),q=p.useRef([]),w=p.useCallback(s=>{F.current=s,typeof N=="function"?N(s):N&&(N.current=s)},[N]);function h(s){const M=[`'wght' ${s.toFixed(0)}`];for(const[v,f]of Object.entries(C))M.push(`'${v}' ${f}`);return M.join(", ")}const V=p.useMemo(()=>{if(!l)return n;q.current=[];let s=0;function M(v){if(typeof v=="string")return[...v].map(f=>{if(/\s/.test(f))return f;const b=s++;return H.jsx("span",{ref:d=>{q.current[b]=d},style:{fontVariationSettings:h(a)},children:f},b)});if(Array.isArray(v))return v.map((f,b)=>H.jsx(p.Fragment,{children:M(f)},b));if(p.isValidElement(v)){const f=v;if(f.props.children!==void 0)return p.cloneElement(f,{},M(f.props.children))}return v}return M(n)},[n,l,a,JSON.stringify(C)]);function E(s,M){const v=F.current;if(!v)return;const f=v.getBoundingClientRect(),b=Math.max(f.left-s,0,s-f.right),d=Math.max(f.top-M,0,M-f.bottom),r=Math.sqrt(b*b+d*d);if(y!==void 0&&!l){const x=1-(1-Math.max(0,1-r/y))**2;v.style.fontVariationSettings=h(a+(T-a)*x);return}if(l){if(y!==void 0&&r>y){v.style.fontVariationSettings=h(a);for(const e of q.current)e&&(e.style.fontVariationSettings=h(a));return}for(const e of q.current){if(!e)continue;const x=e.getBoundingClientRect(),A=(x.left+x.right)/2,m=(x.top+x.bottom)/2,k=Math.sqrt((s-A)**2+(M-m)**2),j=1-(1-Math.max(0,1-k/l))**2;e.style.fontVariationSettings=h(a+(T-a)*j)}}}const i=p.useCallback(s=>{g.current={x:s.clientX,y:s.clientY},E(s.clientX,s.clientY)},[a,T,y,l]),B=p.useCallback(()=>{g.current&&E(g.current.x,g.current.y)},[a,T,y,l]),S=p.useCallback(()=>{g.current=null;const s=F.current;s&&(s.style.fontVariationSettings=h(a));for(const M of q.current)M&&(M.style.fontVariationSettings=h(a))},[a]);return p.useEffect(()=>(window.addEventListener("mousemove",i,{passive:!0}),window.addEventListener("scroll",B,{passive:!0,capture:!0}),document.documentElement.addEventListener("mouseleave",S),()=>{window.removeEventListener("mousemove",i),window.removeEventListener("scroll",B,{capture:!0}),document.documentElement.removeEventListener("mouseleave",S)}),[i,B,S]),H.jsx(o,{ref:w,className:u,style:{fontVariationSettings:h(a),...c},children:V})});et.displayName="MagnetBlock";exports.MAGNET_TYPE_CLASSES=R;exports.MagnetBlock=et;exports.MagnetTypeText=tt;exports.applyMagnetType=Q;exports.getCleanHTML=z;exports.removeMagnetType=ot;exports.startMagnetType=Z;exports.useMagnetType=W;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("react"),W=require("react/jsx-runtime"),rt={i:3,l:3,1:3,I:3,r:2,n:1,m:1,0:2,O:2,o:1,b:1,d:1,p:1,q:1,c:1,e:1},z={word:"mt-word",char:"mt-char",probe:"mt-probe"},K={axes:{wght:[300,500]},radius:120,falloff:"quadratic",magnetMode:"attract",wdthBoost:6,scope:"document"};function ot(t,n=[]){return t.nodeType===Node.TEXT_NODE?n.push(t):t.childNodes.forEach(s=>ot(s,n)),n}function tt(t,n,s){if(!t||t==="normal")return`"${n}" ${s}`;const M=new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`),y=`"${n}" ${s}`;return M.test(t)?t.replace(M,y):`${t}, ${y}`}function nt(t,n){let s=t;for(const[M,y]of Object.entries(n))s=tt(s,M,y);return s}function st(t,n,s){if(n.opacity!==void 0){const[M,y]=n.opacity;t.style.opacity=String(M+(y-M)*s)}n.italic===!0&&(t.style.fontStyle=s>.5?"italic":"")}function et(t,n){n.opacity!==void 0&&(t.style.opacity=String(n.opacity[0])),n.italic===!0&&(t.style.fontStyle="")}function ct(t){const n=t.cloneNode(!0),s=n.querySelectorAll(`.${z.word}, .${z.char}`);return Array.from(s).reverse().forEach(y=>{const u=y.parentNode;if(u){for(;y.firstChild;)u.insertBefore(y.firstChild,y);u.removeChild(y)}}),n.innerHTML}function pt(t,n){t.innerHTML=n}function it(t,n,s={}){var p,o;if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=n,()=>{};const M=s.wdthBoost??K.wdthBoost,y=s.radius??K.radius,u=s.falloff??K.falloff,L=s.scope??K.scope,F=s.props,d=s.transitionMs??0,S=s.cachePositions??!0,V=window.scrollY;t.innerHTML=n;const j=getComputedStyle(t).fontVariationSettings,N=j.match(/"wdth"\s+([\d.eE+-]+)/),k=N?parseFloat(N[1]):100,C=ot(t),g=[];for(const e of C){const E=e.textContent??"";if(!E||!E.split("").some(v=>v in rt))continue;const r=document.createDocumentFragment();for(const v of E){const l=rt[v];if(l===void 0){const f=r.lastChild;f&&f.nodeType===Node.TEXT_NODE?f.textContent+=v:r.appendChild(document.createTextNode(v))}else{const f=document.createElement("span");f.className=z.char,f.style.fontVariationSettings=tt(j,"wdth",k),f.textContent=v,r.appendChild(f),g.push({span:f,riskLevel:l})}}e.parentNode.replaceChild(r,e)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-V)>2&&window.scrollTo({top:V,behavior:"instant"})}),g.length===0)return()=>{};F&&g.forEach(({span:e})=>et(e,F));let A=[],H=!1;function _(){const e=window.scrollX,E=window.scrollY;A=g.map(({span:a})=>{const r=a.getBoundingClientRect();return{cx:(r.left+r.right)/2+e,cy:(r.top+r.bottom)/2+E}}),H=!0}let R=null;S&&(_(),R=new ResizeObserver(()=>{H=!1}),R.observe(t),(o=(p=document.fonts)==null?void 0:p.ready)==null||o.then(()=>{H=!1}));let q=-9999,B=-9999,$=!1,w=0,P=!0,T=null;function D(){if(!P)return;if(!$){g.forEach(({span:r})=>{d>0&&(r.style.transition=`font-variation-settings ${d}ms ease`),r.style.fontVariationSettings=tt(j,"wdth",k),F&&et(r,F)}),d>0&&(T!==null&&clearTimeout(T),T=setTimeout(()=>{g.forEach(({span:r})=>{r.style.transition=""}),T=null},d)),w=0;return}S&&!H&&_();const e=S?q+window.scrollX:q,E=S?B+window.scrollY:B,a=S?null:g.map(({span:r})=>r.getBoundingClientRect());g.forEach(({span:r,riskLevel:v},l)=>{r.style.transition="";let f,O;if(S)({cx:f,cy:O}=A[l]);else{const I=a[l];f=I.left+I.width/2,O=I.top+I.height/2}const x=Math.sqrt((e-f)**2+(E-O)**2),m=Math.max(0,1-x/y),b=u==="quadratic"?m*m:m,Y=M*(v/3)*b;r.style.fontVariationSettings=tt(j,"wdth",k+Y),F&&st(r,F,b)}),w=requestAnimationFrame(D)}function X(e){q=e.clientX,B=e.clientY,$||($=!0),w===0&&(w=requestAnimationFrame(D))}function J(){$=!1,w===0&&(w=requestAnimationFrame(D))}function Q(e){e.touches.length!==0&&(q=e.touches[0].clientX,B=e.touches[0].clientY,$||($=!0),w===0&&(w=requestAnimationFrame(D)))}function i(){$=!1,w===0&&(w=requestAnimationFrame(D))}const c=L==="document"?document:t;return c.addEventListener("mousemove",X),c.addEventListener("mouseleave",J),c.addEventListener("touchmove",Q,{passive:!0}),c.addEventListener("touchend",i),()=>{P=!1,cancelAnimationFrame(w),T!==null&&clearTimeout(T),R&&R.disconnect(),c.removeEventListener("mousemove",X),c.removeEventListener("mouseleave",J),c.removeEventListener("touchmove",Q),c.removeEventListener("touchend",i),t.innerHTML=n}}function lt(t,n,s={}){var e,E;if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=n,()=>{};const M=s.axes??K.axes,y=s.radius??K.radius,u=s.falloff??K.falloff,L=s.magnetMode??K.magnetMode,F=s.scope??K.scope,d=s.props,S=s.transitionMs??0,V=s.cachePositions??!0,j=s.stabilizeLayout??!0,N=window.scrollY;t.innerHTML=n;const k=ot(t),C=[];for(const a of k){const r=a.textContent??"";if(!r.trim())continue;const v=r.split(/(\S+)/),l=document.createDocumentFragment();for(let f=0;f<v.length;f+=2){const O=v[f],x=v[f+1];if(!x)continue;const b=v[f+3]===void 0?v[f+2]??"":"",Y=document.createElement("span");Y.className=z.word,Y.textContent=O+x+b,l.appendChild(Y),C.push(Y)}a.parentNode.replaceChild(l,a)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-N)>2&&window.scrollTo({top:N,behavior:"instant"})}),C.length===0)return()=>{};const g=getComputedStyle(t).fontVariationSettings,A=nt(g,Object.fromEntries(Object.entries(M).map(([a,[r]])=>[a,r])));let H=0;if(j){const a=nt(g,Object.fromEntries(Object.entries(M).map(([m,[,b]])=>[m,b]))),r=t.style.fontVariationSettings,v=t.style.whiteSpace,l=t.style.overflow;t.style.whiteSpace="nowrap",t.style.overflow="visible",t.style.fontVariationSettings=a;const f=t.scrollWidth;t.style.fontVariationSettings=A;const O=t.scrollWidth;t.style.fontVariationSettings=r,t.style.whiteSpace=v,t.style.overflow=l;const x=C.reduce((m,b)=>{var Y;return m+(((Y=b.textContent)==null?void 0:Y.replace(/\s+/g,"").length)??0)},0);x>0&&f>O&&(H=(f-O)/x)}C.forEach(a=>{a.style.fontVariationSettings=A,d&&et(a,d)});let _=[],R=!1;function q(){const a=window.scrollX,r=window.scrollY;_=C.map(v=>{const l=v.getBoundingClientRect();return{cx:(l.left+l.right)/2+a,cy:(l.top+l.bottom)/2+r}}),R=!0}let B=null;V&&(q(),B=new ResizeObserver(()=>{R=!1}),B.observe(t),(E=(e=document.fonts)==null?void 0:e.ready)==null||E.then(()=>{R=!1}));let $=-9999,w=-9999,P=!1,T=0,D=!0,X=null;function J(){if(!D)return;if(!P){C.forEach(l=>{S>0&&(l.style.transition=`font-variation-settings ${S}ms ease`),l.style.fontVariationSettings=A,j&&(l.style.letterSpacing=""),d&&et(l,d)}),S>0&&(X!==null&&clearTimeout(X),X=setTimeout(()=>{C.forEach(l=>{l.style.transition=""}),X=null},S)),T=0;return}V&&!R&&q();const a=V?$+window.scrollX:$,r=V?w+window.scrollY:w,v=V?null:C.map(l=>l.getBoundingClientRect());C.forEach((l,f)=>{l.style.transition="";let O,x;if(V)({cx:O,cy:x}=_[f]);else{const G=v[f];O=G.left+G.width/2,x=G.top+G.height/2}const m=Math.sqrt((a-O)**2+(r-x)**2),b=Math.max(0,1-m/y),Y=u==="quadratic"?b*b:b,I=L==="repel"?1-Y:Y,Z={};for(const G of Object.keys(M)){const[U,dt]=M[G]??[300,500];Z[G]=U+(dt-U)*I}l.style.fontVariationSettings=nt(g,Z),j&&H!==0&&(l.style.letterSpacing=`${(-H*I).toFixed(3)}px`),d&&st(l,d,Y)}),T=requestAnimationFrame(J)}function Q(a){$=a.clientX,w=a.clientY,P||(P=!0),T===0&&(T=requestAnimationFrame(J))}function i(){P=!1,T===0&&(T=requestAnimationFrame(J))}function c(a){a.touches.length!==0&&($=a.touches[0].clientX,w=a.touches[0].clientY,P||(P=!0),T===0&&(T=requestAnimationFrame(J)))}function p(){P=!1,T===0&&(T=requestAnimationFrame(J))}const o=F==="document"?document:t;return o.addEventListener("mousemove",Q),o.addEventListener("mouseleave",i),o.addEventListener("touchmove",c,{passive:!0}),o.addEventListener("touchend",p),()=>{D=!1,cancelAnimationFrame(T),X!==null&&clearTimeout(X),B&&B.disconnect(),o.removeEventListener("mousemove",Q),o.removeEventListener("mouseleave",i),o.removeEventListener("touchmove",c),o.removeEventListener("touchend",p),t.innerHTML=n}}function at(t){const n=h.useRef(null),s=h.useRef(null),M=h.useRef(t);M.current=t;const y=h.useRef(null),u=t.mode??"field",{axes:L,radius:F,falloff:d,magnetMode:S,wdthBoost:V,scope:j}=t,N=L?JSON.stringify(L):void 0,k=t.props?JSON.stringify(t.props):void 0,C=h.useCallback(()=>{const g=n.current;if(!g)return;s.current===null&&(s.current=ct(g)),y.current&&(y.current(),y.current=null),(M.current.mode??"field")==="field"?y.current=lt(g,s.current,M.current):y.current=it(g,s.current,M.current)},[u,N,F,d,S,V,j,k]);return h.useLayoutEffect(()=>(C(),()=>{y.current&&(y.current(),y.current=null)}),[C]),h.useEffect(()=>{var g,A;(A=(g=document.fonts)==null?void 0:g.ready)==null||A.then(C)},[C]),n}const ut=h.forwardRef(function({children:n,as:s="p",className:M,style:y,...u},L){const F=at(u),d=h.useCallback(S=>{F.current=S,typeof L=="function"?L(S):L&&(L.current=S)},[L]);return W.jsx(s,{ref:d,className:M,style:y,children:n})});ut.displayName="MagnetTypeText";const ft=h.forwardRef(function({children:n,as:s="p",className:M,style:y,minWeight:u=300,maxWeight:L=600,proximityRadius:F,spreadRadius:d,fixedAxes:S={},cachePositions:V=!0,rafThrottle:j=!0,stabilizeLayout:N=!0},k){const C=h.useRef(null),g=h.useRef(null),A=h.useRef([]),H=h.useRef([]),_=h.useRef(null),R=h.useRef(!1),q=h.useRef(null),B=h.useRef(0),$=h.useCallback(i=>{C.current=i,typeof k=="function"?k(i):k&&(k.current=i)},[k]);function w(i){const c=[`'wght' ${i.toFixed(0)}`];for(const[p,o]of Object.entries(S))c.push(`'${p}' ${o}`);return c.join(", ")}function P(){const i=C.current;if(!i)return;const c=window.scrollX,p=window.scrollY,o=i.getBoundingClientRect();_.current={top:o.top+p,left:o.left+c,right:o.right+c,bottom:o.bottom+p},H.current=A.current.map(e=>{if(!e)return{cx:0,cy:0};const E=e.getBoundingClientRect();return{cx:(E.left+E.right)/2+c,cy:(E.top+E.bottom)/2+p}}),R.current=!0}function T(i,c){const p=C.current;if(!p)return;V&&!R.current&&P();let o,e,E,a,r,v;if(V&&_.current)r=i+window.scrollX,v=c+window.scrollY,{top:o,left:e,right:E,bottom:a}=_.current;else{const x=p.getBoundingClientRect();r=i,v=c,o=x.top,e=x.left,E=x.right,a=x.bottom}const l=Math.max(e-r,0,r-E),f=Math.max(o-v,0,v-a),O=Math.sqrt(l*l+f*f);if(F!==void 0&&!d){const m=1-(1-Math.max(0,1-O/F))**2;p.style.fontVariationSettings=w(u+(L-u)*m);return}if(d){if(F!==void 0&&O>F){p.style.fontVariationSettings=w(u);for(const m of A.current)m&&(m.style.fontVariationSettings=w(u),N&&(m.style.letterSpacing=""));return}if(O>d){for(const m of A.current)m&&(m.style.fontVariationSettings=w(u),N&&(m.style.letterSpacing=""));return}const x=A.current;if(V&&H.current.length===x.length)for(let m=0;m<x.length;m++){const b=x[m];if(!b)continue;const{cx:Y,cy:I}=H.current[m],Z=Math.sqrt((r-Y)**2+(v-I)**2),U=1-(1-Math.max(0,1-Z/d))**2;b.style.fontVariationSettings=w(u+(L-u)*U),N&&B.current!==0&&(b.style.letterSpacing=`${(-B.current*U).toFixed(3)}px`)}else for(const m of x){if(!m)continue;const b=m.getBoundingClientRect(),Y=(b.left+b.right)/2,I=(b.top+b.bottom)/2,Z=Math.sqrt((i-Y)**2+(c-I)**2),U=1-(1-Math.max(0,1-Z/d))**2;m.style.fontVariationSettings=w(u+(L-u)*U),N&&B.current!==0&&(m.style.letterSpacing=`${(-B.current*U).toFixed(3)}px`)}}}const D=h.useCallback(i=>{g.current={x:i.clientX,y:i.clientY},j?q.current===null&&(q.current=requestAnimationFrame(()=>{q.current=null,g.current&&T(g.current.x,g.current.y)})):T(i.clientX,i.clientY)},[u,L,F,d,V,j,N,JSON.stringify(S)]),X=h.useCallback(()=>{g.current&&T(g.current.x,g.current.y)},[u,L,F,d,V,N,JSON.stringify(S)]),J=h.useCallback(()=>{g.current=null,q.current!==null&&(cancelAnimationFrame(q.current),q.current=null);const i=C.current;i&&(i.style.fontVariationSettings=w(u));for(const c of A.current)c&&(c.style.fontVariationSettings=w(u),N&&(c.style.letterSpacing=""))},[u,N,JSON.stringify(S)]);h.useEffect(()=>(window.addEventListener("mousemove",D,{passive:!0}),window.addEventListener("scroll",X,{passive:!0,capture:!0}),document.documentElement.addEventListener("mouseleave",J),()=>{window.removeEventListener("mousemove",D),window.removeEventListener("scroll",X,{capture:!0}),document.documentElement.removeEventListener("mouseleave",J),q.current!==null&&(cancelAnimationFrame(q.current),q.current=null)}),[D,X,J]),h.useEffect(()=>{var p,o;if(!V||!d)return;R.current=!1;const i=C.current;if(!i)return;const c=new ResizeObserver(()=>{R.current=!1});return c.observe(i),(o=(p=document.fonts)==null?void 0:p.ready)==null||o.then(()=>{R.current=!1}),()=>c.disconnect()},[V,d]),h.useEffect(()=>{R.current=!1},[n,d]),h.useEffect(()=>{if(!N||!d){B.current=0;return}const i=C.current;if(!i)return;const c=i.textContent??"",p=c.replace(/\s/g,"").length;if(p===0)return;const o=getComputedStyle(i),e=document.createElement("span");e.style.cssText="position:fixed;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;pointer-events:none;",e.style.fontFamily=o.fontFamily,e.style.fontSize=o.fontSize,e.style.fontWeight=o.fontWeight,e.style.lineHeight=o.lineHeight,e.style.letterSpacing=o.letterSpacing,e.textContent=c,document.body.appendChild(e);const E=v=>{const l=[`'wght' ${v.toFixed(0)}`];for(const[f,O]of Object.entries(S))l.push(`'${f}' ${O}`);return l.join(", ")};e.style.fontVariationSettings=E(L);const a=e.scrollWidth;e.style.fontVariationSettings=E(u);const r=e.scrollWidth;document.body.removeChild(e),B.current=a>r?(a-r)/p:0},[N,d,u,L,n,JSON.stringify(S)]);const Q=h.useMemo(()=>{if(!d)return n;A.current=[];let i=0;function c(p){if(typeof p=="string")return[...p].map(o=>{if(/\s/.test(o))return o;const e=i++;return W.jsx("span",{ref:E=>{A.current[e]=E},style:{fontVariationSettings:w(u)},children:o},e)});if(Array.isArray(p))return p.map((o,e)=>W.jsx(h.Fragment,{children:c(o)},e));if(h.isValidElement(p)){const o=p;if(o.props.children!==void 0)return h.cloneElement(o,{},c(o.props.children))}return p}return c(n)},[n,d,u,JSON.stringify(S)]);return W.jsx(s,{ref:$,className:M,style:{fontVariationSettings:w(u),...y},children:Q})});ft.displayName="MagnetBlock";exports.MAGNET_TYPE_CLASSES=z;exports.MagnetBlock=ft;exports.MagnetTypeText=ut;exports.applyMagnetType=it;exports.getCleanHTML=ct;exports.removeMagnetType=pt;exports.startMagnetType=lt;exports.useMagnetType=at;
package/dist/index.d.ts CHANGED
@@ -38,12 +38,6 @@ export declare const MAGNET_TYPE_CLASSES: {
38
38
  readonly probe: "mt-probe";
39
39
  };
40
40
 
41
- /**
42
- * Drop-in block element with cursor-proximity variable font weight variation.
43
- * Accepts any ReactNode. When spreadRadius is set, string children are split into
44
- * per-character spans rendered as React elements — no DOM manipulation required.
45
- * Use proximityRadius for a whole-element gate, or combine both.
46
- */
47
41
  export declare const MagnetBlock: default_2.ForwardRefExoticComponent<MagnetBlockProps & default_2.RefAttributes<HTMLElement>>;
48
42
 
49
43
  export declare interface MagnetBlockProps {
@@ -59,6 +53,34 @@ export declare interface MagnetBlockProps {
59
53
  /** Pixel distance from the cursor within which each character's weight rises to max */
60
54
  spreadRadius?: number;
61
55
  fixedAxes?: Record<string, number>;
56
+ /**
57
+ * Cache character centre positions in page-relative coordinates after first render,
58
+ * eliminating getBoundingClientRect calls on every mousemove. The cache is rebuilt on
59
+ * resize and after fonts load. Disable if the block lives inside a custom scroll
60
+ * container (overflow: scroll on a non-window element) — page coordinates are derived
61
+ * from window.scrollX/Y and will be incorrect when a nested element is the scroll parent.
62
+ * @default true
63
+ */
64
+ cachePositions?: boolean;
65
+ /**
66
+ * Throttle proximity updates to one per animation frame via requestAnimationFrame,
67
+ * capping the update rate at the display refresh rate. Disable if the ~16 ms rAF delay
68
+ * is perceptible — for example on 120 Hz displays or very tight, fast-moving effects.
69
+ * @default true
70
+ */
71
+ rafThrottle?: boolean;
72
+ /**
73
+ * Apply compensating letter-spacing to keep line lengths stable as font weight changes.
74
+ * Measures the element's text width at rest and peak weight via an off-screen probe span,
75
+ * then applies proportional negative letter-spacing per character as weight rises,
76
+ * cancelling the advance-width increase that would otherwise cause text to reflow.
77
+ *
78
+ * Disable if you prefer natural bold letter-spacing, or if the font expands characters
79
+ * very unevenly across the weight axis (the compensation is a per-element average and
80
+ * may not perfectly cancel highly variable per-character expansion).
81
+ * @default true
82
+ */
83
+ stabilizeLayout?: boolean;
62
84
  }
63
85
 
64
86
  /** Whether cursor proximity attracts toward peak or repels toward rest */
@@ -127,6 +149,28 @@ export declare interface MagnetTypeOptions {
127
149
  * Risk 3 characters receive wdthBoost × (3/3) = full boost at peak.
128
150
  */
129
151
  wdthBoost?: number;
152
+ /**
153
+ * Cache word/character centre positions in page-relative coordinates after setup,
154
+ * eliminating getBoundingClientRect calls during the active interaction loop.
155
+ * The cache is rebuilt on resize and after fonts load. Disable if the element lives
156
+ * inside a custom scroll container (overflow: scroll on a non-window element) —
157
+ * page coordinates are computed using window.scrollX/Y and will be incorrect when
158
+ * a nested element is the scroll parent.
159
+ * @default true
160
+ */
161
+ cachePositions?: boolean;
162
+ /**
163
+ * Apply compensating letter-spacing to keep line lengths stable as font weight
164
+ * changes. Measures the element's text width at rest and peak weight, then applies
165
+ * proportional negative letter-spacing per word/character as weight rises, cancelling
166
+ * the advance-width increase that would otherwise cause text to reflow.
167
+ *
168
+ * Disable if you prefer natural bold letter-spacing, or if the font expands characters
169
+ * very unevenly across the weight axis (the compensation is a per-element average and
170
+ * may not perfectly cancel highly variable per-character expansion).
171
+ * @default true
172
+ */
173
+ stabilizeLayout?: boolean;
130
174
  /**
131
175
  * Duration in milliseconds for the CSS transition back to rest values when the
132
176
  * cursor leaves (mouseleave / touchend). Default: 0 (instant snap, no transition).
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import J, { useRef as j, useCallback as X, useLayoutEffect as et, useEffect as Q, forwardRef as Z, useMemo as nt } from "react";
3
- import { jsx as D } from "react/jsx-runtime";
4
- const R = {
2
+ import rt, { useRef as I, useCallback as z, useLayoutEffect as ft, useEffect as W, forwardRef as lt, useMemo as dt } from "react";
3
+ import { jsx as tt } from "react/jsx-runtime";
4
+ const it = {
5
5
  i: 3,
6
6
  l: 3,
7
7
  1: 3,
@@ -23,14 +23,14 @@ const R = {
23
23
  c: 1,
24
24
  e: 1
25
25
  // similar bowls
26
- }, P = {
26
+ }, nt = {
27
27
  /** Applied to each word span in field mode */
28
28
  word: "mt-word",
29
29
  /** Applied to each character span in legibility mode */
30
30
  char: "mt-char",
31
31
  /** Applied to measurement probe spans (never in final output) */
32
32
  probe: "mt-probe"
33
- }, Y = {
33
+ }, U = {
34
34
  axes: { wght: [300, 500] },
35
35
  radius: 120,
36
36
  falloff: "quadratic",
@@ -38,329 +38,482 @@ const R = {
38
38
  wdthBoost: 6,
39
39
  scope: "document"
40
40
  };
41
- function K(t, n = []) {
42
- return t.nodeType === Node.TEXT_NODE ? n.push(t) : t.childNodes.forEach((o) => K(o, n)), n;
41
+ function ct(t, n = []) {
42
+ return t.nodeType === Node.TEXT_NODE ? n.push(t) : t.childNodes.forEach((s) => ct(s, n)), n;
43
43
  }
44
- function I(t, n, o) {
45
- if (!t || t === "normal") return `"${n}" ${o}`;
46
- const u = new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`), c = `"${n}" ${o}`;
47
- return u.test(t) ? t.replace(u, c) : `${t}, ${c}`;
44
+ function et(t, n, s) {
45
+ if (!t || t === "normal") return `"${n}" ${s}`;
46
+ const S = new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`), h = `"${n}" ${s}`;
47
+ return S.test(t) ? t.replace(S, h) : `${t}, ${h}`;
48
48
  }
49
- function z(t, n) {
50
- let o = t;
51
- for (const [u, c] of Object.entries(n))
52
- o = I(o, u, c);
53
- return o;
49
+ function st(t, n) {
50
+ let s = t;
51
+ for (const [S, h] of Object.entries(n))
52
+ s = et(s, S, h);
53
+ return s;
54
54
  }
55
- function W(t, n, o) {
55
+ function at(t, n, s) {
56
56
  if (n.opacity !== void 0) {
57
- const [u, c] = n.opacity;
58
- t.style.opacity = String(u + (c - u) * o);
57
+ const [S, h] = n.opacity;
58
+ t.style.opacity = String(S + (h - S) * s);
59
59
  }
60
- n.italic === !0 && (t.style.fontStyle = o > 0.5 ? "italic" : "");
60
+ n.italic === !0 && (t.style.fontStyle = s > 0.5 ? "italic" : "");
61
61
  }
62
- function _(t, n) {
62
+ function ot(t, n) {
63
63
  n.opacity !== void 0 && (t.style.opacity = String(n.opacity[0])), n.italic === !0 && (t.style.fontStyle = "");
64
64
  }
65
- function ot(t) {
66
- const n = t.cloneNode(!0), o = n.querySelectorAll(
67
- `.${P.word}, .${P.char}`
65
+ function mt(t) {
66
+ const n = t.cloneNode(!0), s = n.querySelectorAll(
67
+ `.${nt.word}, .${nt.char}`
68
68
  );
69
- return Array.from(o).reverse().forEach((c) => {
70
- const a = c.parentNode;
71
- if (a) {
72
- for (; c.firstChild; ) a.insertBefore(c.firstChild, c);
73
- a.removeChild(c);
69
+ return Array.from(s).reverse().forEach((h) => {
70
+ const u = h.parentNode;
71
+ if (u) {
72
+ for (; h.firstChild; ) u.insertBefore(h.firstChild, h);
73
+ u.removeChild(h);
74
74
  }
75
75
  }), n.innerHTML;
76
76
  }
77
- function lt(t, n) {
77
+ function Mt(t, n) {
78
78
  t.innerHTML = n;
79
79
  }
80
- function rt(t, n, o = {}) {
80
+ function pt(t, n, s = {}) {
81
+ var m, o;
81
82
  if (typeof window > "u") return () => {
82
83
  };
83
84
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
84
85
  return t.innerHTML = n, () => {
85
86
  };
86
- const u = o.wdthBoost ?? Y.wdthBoost, c = o.radius ?? Y.radius, a = o.falloff ?? Y.falloff, M = o.scope ?? Y.scope, v = o.props, f = o.transitionMs ?? 0, x = window.scrollY;
87
+ const S = s.wdthBoost ?? U.wdthBoost, h = s.radius ?? U.radius, u = s.falloff ?? U.falloff, b = s.scope ?? U.scope, F = s.props, d = s.transitionMs ?? 0, w = s.cachePositions ?? !0, L = window.scrollY;
87
88
  t.innerHTML = n;
88
- const N = getComputedStyle(t).fontVariationSettings, F = N.match(/"wdth"\s+([\d.eE+-]+)/), y = F ? parseFloat(F[1]) : 100, q = K(t), w = [];
89
- for (const r of q) {
90
- const e = r.textContent ?? "";
91
- if (!e || !e.split("").some((m) => m in R)) continue;
92
- const C = document.createDocumentFragment();
93
- for (const m of e) {
94
- const V = R[m];
95
- if (V === void 0) {
96
- const S = C.lastChild;
97
- S && S.nodeType === Node.TEXT_NODE ? S.textContent += m : C.appendChild(document.createTextNode(m));
89
+ const X = getComputedStyle(t).fontVariationSettings, V = X.match(/"wdth"\s+([\d.eE+-]+)/), j = V ? parseFloat(V[1]) : 100, E = ct(t), y = [];
90
+ for (const e of E) {
91
+ const M = e.textContent ?? "";
92
+ if (!M || !M.split("").some((g) => g in it)) continue;
93
+ const r = document.createDocumentFragment();
94
+ for (const g of M) {
95
+ const l = it[g];
96
+ if (l === void 0) {
97
+ const f = r.lastChild;
98
+ f && f.nodeType === Node.TEXT_NODE ? f.textContent += g : r.appendChild(document.createTextNode(g));
98
99
  } else {
99
- const S = document.createElement("span");
100
- S.className = P.char, S.style.fontVariationSettings = I(N, "wdth", y), S.textContent = m, C.appendChild(S), w.push({ span: S, riskLevel: V });
100
+ const f = document.createElement("span");
101
+ f.className = nt.char, f.style.fontVariationSettings = et(X, "wdth", j), f.textContent = g, r.appendChild(f), y.push({ span: f, riskLevel: l });
101
102
  }
102
103
  }
103
- r.parentNode.replaceChild(C, r);
104
+ e.parentNode.replaceChild(r, e);
104
105
  }
105
106
  if (requestAnimationFrame(() => {
106
- typeof window < "u" && Math.abs(window.scrollY - x) > 2 && window.scrollTo({ top: x, behavior: "instant" });
107
- }), w.length === 0) return () => {
107
+ typeof window < "u" && Math.abs(window.scrollY - L) > 2 && window.scrollTo({ top: L, behavior: "instant" });
108
+ }), y.length === 0) return () => {
108
109
  };
109
- v && w.forEach(({ span: r }) => _(r, v));
110
- let p = -9999, b = -9999, E = !1, i = 0, B = !0, T = null;
111
- function s() {
112
- if (!B) return;
113
- if (!E) {
114
- w.forEach(({ span: e }) => {
115
- f > 0 && (e.style.transition = `font-variation-settings ${f}ms ease`), e.style.fontVariationSettings = I(N, "wdth", y), v && _(e, v);
116
- }), f > 0 && (T !== null && clearTimeout(T), T = setTimeout(() => {
117
- w.forEach(({ span: e }) => {
118
- e.style.transition = "";
119
- }), T = null;
120
- }, f)), i = 0;
110
+ F && y.forEach(({ span: e }) => ot(e, F));
111
+ let N = [], H = !1;
112
+ function _() {
113
+ const e = window.scrollX, M = window.scrollY;
114
+ N = y.map(({ span: a }) => {
115
+ const r = a.getBoundingClientRect();
116
+ return { cx: (r.left + r.right) / 2 + e, cy: (r.top + r.bottom) / 2 + M };
117
+ }), H = !0;
118
+ }
119
+ let Y = null;
120
+ w && (_(), Y = new ResizeObserver(() => {
121
+ H = !1;
122
+ }), Y.observe(t), (o = (m = document.fonts) == null ? void 0 : m.ready) == null || o.then(() => {
123
+ H = !1;
124
+ }));
125
+ let A = -9999, $ = -9999, B = !1, v = 0, R = !0, x = null;
126
+ function D() {
127
+ if (!R) return;
128
+ if (!B) {
129
+ y.forEach(({ span: r }) => {
130
+ d > 0 && (r.style.transition = `font-variation-settings ${d}ms ease`), r.style.fontVariationSettings = et(X, "wdth", j), F && ot(r, F);
131
+ }), d > 0 && (x !== null && clearTimeout(x), x = setTimeout(() => {
132
+ y.forEach(({ span: r }) => {
133
+ r.style.transition = "";
134
+ }), x = null;
135
+ }, d)), v = 0;
121
136
  return;
122
137
  }
123
- const r = w.map(({ span: e }) => e.getBoundingClientRect());
124
- w.forEach(({ span: e, riskLevel: L }, C) => {
125
- e.style.transition = "";
126
- const m = r[C], V = m.left + m.width / 2, S = m.top + m.height / 2, k = Math.sqrt((p - V) ** 2 + (b - S) ** 2), O = Math.max(0, 1 - k / c), $ = a === "quadratic" ? O * O : O, H = u * (L / 3) * $;
127
- e.style.fontVariationSettings = I(N, "wdth", y + H), v && W(e, v, $);
128
- }), i = requestAnimationFrame(s);
138
+ w && !H && _();
139
+ const e = w ? A + window.scrollX : A, M = w ? $ + window.scrollY : $, a = w ? null : y.map(({ span: r }) => r.getBoundingClientRect());
140
+ y.forEach(({ span: r, riskLevel: g }, l) => {
141
+ r.style.transition = "";
142
+ let f, O;
143
+ if (w)
144
+ ({ cx: f, cy: O } = N[l]);
145
+ else {
146
+ const P = a[l];
147
+ f = P.left + P.width / 2, O = P.top + P.height / 2;
148
+ }
149
+ const C = Math.sqrt((e - f) ** 2 + (M - O) ** 2), p = Math.max(0, 1 - C / h), T = u === "quadratic" ? p * p : p, q = S * (g / 3) * T;
150
+ r.style.fontVariationSettings = et(X, "wdth", j + q), F && at(r, F, T);
151
+ }), v = requestAnimationFrame(D);
129
152
  }
130
- function g(r) {
131
- p = r.clientX, b = r.clientY, E || (E = !0), i === 0 && (i = requestAnimationFrame(s));
153
+ function k(e) {
154
+ A = e.clientX, $ = e.clientY, B || (B = !0), v === 0 && (v = requestAnimationFrame(D));
132
155
  }
133
- function h() {
134
- E = !1, i === 0 && (i = requestAnimationFrame(s));
156
+ function J() {
157
+ B = !1, v === 0 && (v = requestAnimationFrame(D));
135
158
  }
136
- function l(r) {
137
- r.touches.length !== 0 && (p = r.touches[0].clientX, b = r.touches[0].clientY, E || (E = !0), i === 0 && (i = requestAnimationFrame(s)));
159
+ function Q(e) {
160
+ e.touches.length !== 0 && (A = e.touches[0].clientX, $ = e.touches[0].clientY, B || (B = !0), v === 0 && (v = requestAnimationFrame(D)));
138
161
  }
139
- function A() {
140
- E = !1, i === 0 && (i = requestAnimationFrame(s));
162
+ function i() {
163
+ B = !1, v === 0 && (v = requestAnimationFrame(D));
141
164
  }
142
- const d = M === "document" ? document : t;
143
- return d.addEventListener("mousemove", g), d.addEventListener("mouseleave", h), d.addEventListener("touchmove", l, { passive: !0 }), d.addEventListener("touchend", A), () => {
144
- B = !1, cancelAnimationFrame(i), T !== null && clearTimeout(T), d.removeEventListener("mousemove", g), d.removeEventListener("mouseleave", h), d.removeEventListener("touchmove", l), d.removeEventListener("touchend", A), t.innerHTML = n;
165
+ const c = b === "document" ? document : t;
166
+ return c.addEventListener("mousemove", k), c.addEventListener("mouseleave", J), c.addEventListener("touchmove", Q, { passive: !0 }), c.addEventListener("touchend", i), () => {
167
+ R = !1, cancelAnimationFrame(v), x !== null && clearTimeout(x), Y && Y.disconnect(), c.removeEventListener("mousemove", k), c.removeEventListener("mouseleave", J), c.removeEventListener("touchmove", Q), c.removeEventListener("touchend", i), t.innerHTML = n;
145
168
  };
146
169
  }
147
- function st(t, n, o = {}) {
170
+ function ht(t, n, s = {}) {
171
+ var e, M;
148
172
  if (typeof window > "u") return () => {
149
173
  };
150
174
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
151
175
  return t.innerHTML = n, () => {
152
176
  };
153
- const u = o.axes ?? Y.axes, c = o.radius ?? Y.radius, a = o.falloff ?? Y.falloff, M = o.magnetMode ?? Y.magnetMode, v = o.scope ?? Y.scope, f = o.props, x = o.transitionMs ?? 0, N = window.scrollY;
177
+ const S = s.axes ?? U.axes, h = s.radius ?? U.radius, u = s.falloff ?? U.falloff, b = s.magnetMode ?? U.magnetMode, F = s.scope ?? U.scope, d = s.props, w = s.transitionMs ?? 0, L = s.cachePositions ?? !0, X = s.stabilizeLayout ?? !0, V = window.scrollY;
154
178
  t.innerHTML = n;
155
- const F = K(t), y = [];
156
- for (const r of F) {
157
- const e = r.textContent ?? "";
158
- if (!e.trim()) continue;
159
- const L = e.split(/(\S+)/), C = document.createDocumentFragment();
160
- for (let m = 0; m < L.length; m += 2) {
161
- const V = L[m], S = L[m + 1];
162
- if (!S) continue;
163
- const O = L[m + 3] === void 0 ? L[m + 2] ?? "" : "", $ = document.createElement("span");
164
- $.className = P.word, $.textContent = V + S + O, C.appendChild($), y.push($);
179
+ const j = ct(t), E = [];
180
+ for (const a of j) {
181
+ const r = a.textContent ?? "";
182
+ if (!r.trim()) continue;
183
+ const g = r.split(/(\S+)/), l = document.createDocumentFragment();
184
+ for (let f = 0; f < g.length; f += 2) {
185
+ const O = g[f], C = g[f + 1];
186
+ if (!C) continue;
187
+ const T = g[f + 3] === void 0 ? g[f + 2] ?? "" : "", q = document.createElement("span");
188
+ q.className = nt.word, q.textContent = O + C + T, l.appendChild(q), E.push(q);
165
189
  }
166
- r.parentNode.replaceChild(C, r);
190
+ a.parentNode.replaceChild(l, a);
167
191
  }
168
192
  if (requestAnimationFrame(() => {
169
- typeof window < "u" && Math.abs(window.scrollY - N) > 2 && window.scrollTo({ top: N, behavior: "instant" });
170
- }), y.length === 0) return () => {
193
+ typeof window < "u" && Math.abs(window.scrollY - V) > 2 && window.scrollTo({ top: V, behavior: "instant" });
194
+ }), E.length === 0) return () => {
171
195
  };
172
- const q = getComputedStyle(t).fontVariationSettings, w = z(
173
- q,
174
- Object.fromEntries(Object.entries(u).map(([r, [e]]) => [r, e]))
196
+ const y = getComputedStyle(t).fontVariationSettings, N = st(
197
+ y,
198
+ Object.fromEntries(Object.entries(S).map(([a, [r]]) => [a, r]))
175
199
  );
176
- y.forEach((r) => {
177
- r.style.fontVariationSettings = w, f && _(r, f);
200
+ let H = 0;
201
+ if (X) {
202
+ const a = st(
203
+ y,
204
+ Object.fromEntries(Object.entries(S).map(([p, [, T]]) => [p, T]))
205
+ ), r = t.style.fontVariationSettings, g = t.style.whiteSpace, l = t.style.overflow;
206
+ t.style.whiteSpace = "nowrap", t.style.overflow = "visible", t.style.fontVariationSettings = a;
207
+ const f = t.scrollWidth;
208
+ t.style.fontVariationSettings = N;
209
+ const O = t.scrollWidth;
210
+ t.style.fontVariationSettings = r, t.style.whiteSpace = g, t.style.overflow = l;
211
+ const C = E.reduce(
212
+ (p, T) => {
213
+ var q;
214
+ return p + (((q = T.textContent) == null ? void 0 : q.replace(/\s+/g, "").length) ?? 0);
215
+ },
216
+ 0
217
+ );
218
+ C > 0 && f > O && (H = (f - O) / C);
219
+ }
220
+ E.forEach((a) => {
221
+ a.style.fontVariationSettings = N, d && ot(a, d);
178
222
  });
179
- let p = -9999, b = -9999, E = !1, i = 0, B = !0, T = null;
180
- function s() {
181
- if (!B) return;
182
- if (!E) {
183
- y.forEach((e) => {
184
- x > 0 && (e.style.transition = `font-variation-settings ${x}ms ease`), e.style.fontVariationSettings = w, f && _(e, f);
185
- }), x > 0 && (T !== null && clearTimeout(T), T = setTimeout(() => {
186
- y.forEach((e) => {
187
- e.style.transition = "";
188
- }), T = null;
189
- }, x)), i = 0;
223
+ let _ = [], Y = !1;
224
+ function A() {
225
+ const a = window.scrollX, r = window.scrollY;
226
+ _ = E.map((g) => {
227
+ const l = g.getBoundingClientRect();
228
+ return { cx: (l.left + l.right) / 2 + a, cy: (l.top + l.bottom) / 2 + r };
229
+ }), Y = !0;
230
+ }
231
+ let $ = null;
232
+ L && (A(), $ = new ResizeObserver(() => {
233
+ Y = !1;
234
+ }), $.observe(t), (M = (e = document.fonts) == null ? void 0 : e.ready) == null || M.then(() => {
235
+ Y = !1;
236
+ }));
237
+ let B = -9999, v = -9999, R = !1, x = 0, D = !0, k = null;
238
+ function J() {
239
+ if (!D) return;
240
+ if (!R) {
241
+ E.forEach((l) => {
242
+ w > 0 && (l.style.transition = `font-variation-settings ${w}ms ease`), l.style.fontVariationSettings = N, X && (l.style.letterSpacing = ""), d && ot(l, d);
243
+ }), w > 0 && (k !== null && clearTimeout(k), k = setTimeout(() => {
244
+ E.forEach((l) => {
245
+ l.style.transition = "";
246
+ }), k = null;
247
+ }, w)), x = 0;
190
248
  return;
191
249
  }
192
- const r = y.map((e) => e.getBoundingClientRect());
193
- y.forEach((e, L) => {
194
- e.style.transition = "";
195
- const C = r[L], m = C.left + C.width / 2, V = C.top + C.height / 2, S = Math.sqrt((p - m) ** 2 + (b - V) ** 2), k = Math.max(0, 1 - S / c), O = a === "quadratic" ? k * k : k, $ = M === "repel" ? 1 - O : O, H = {};
196
- for (const U of Object.keys(u)) {
197
- const [G, tt] = u[U] ?? [300, 500];
198
- H[U] = G + (tt - G) * $;
250
+ L && !Y && A();
251
+ const a = L ? B + window.scrollX : B, r = L ? v + window.scrollY : v, g = L ? null : E.map((l) => l.getBoundingClientRect());
252
+ E.forEach((l, f) => {
253
+ l.style.transition = "";
254
+ let O, C;
255
+ if (L)
256
+ ({ cx: O, cy: C } = _[f]);
257
+ else {
258
+ const K = g[f];
259
+ O = K.left + K.width / 2, C = K.top + K.height / 2;
260
+ }
261
+ const p = Math.sqrt((a - O) ** 2 + (r - C) ** 2), T = Math.max(0, 1 - p / h), q = u === "quadratic" ? T * T : T, P = b === "repel" ? 1 - q : q, Z = {};
262
+ for (const K of Object.keys(S)) {
263
+ const [G, ut] = S[K] ?? [300, 500];
264
+ Z[K] = G + (ut - G) * P;
199
265
  }
200
- e.style.fontVariationSettings = z(q, H), f && W(e, f, O);
201
- }), i = requestAnimationFrame(s);
266
+ l.style.fontVariationSettings = st(y, Z), X && H !== 0 && (l.style.letterSpacing = `${(-H * P).toFixed(3)}px`), d && at(l, d, q);
267
+ }), x = requestAnimationFrame(J);
202
268
  }
203
- function g(r) {
204
- p = r.clientX, b = r.clientY, E || (E = !0), i === 0 && (i = requestAnimationFrame(s));
269
+ function Q(a) {
270
+ B = a.clientX, v = a.clientY, R || (R = !0), x === 0 && (x = requestAnimationFrame(J));
205
271
  }
206
- function h() {
207
- E = !1, i === 0 && (i = requestAnimationFrame(s));
272
+ function i() {
273
+ R = !1, x === 0 && (x = requestAnimationFrame(J));
208
274
  }
209
- function l(r) {
210
- r.touches.length !== 0 && (p = r.touches[0].clientX, b = r.touches[0].clientY, E || (E = !0), i === 0 && (i = requestAnimationFrame(s)));
275
+ function c(a) {
276
+ a.touches.length !== 0 && (B = a.touches[0].clientX, v = a.touches[0].clientY, R || (R = !0), x === 0 && (x = requestAnimationFrame(J)));
211
277
  }
212
- function A() {
213
- E = !1, i === 0 && (i = requestAnimationFrame(s));
278
+ function m() {
279
+ R = !1, x === 0 && (x = requestAnimationFrame(J));
214
280
  }
215
- const d = v === "document" ? document : t;
216
- return d.addEventListener("mousemove", g), d.addEventListener("mouseleave", h), d.addEventListener("touchmove", l, { passive: !0 }), d.addEventListener("touchend", A), () => {
217
- B = !1, cancelAnimationFrame(i), T !== null && clearTimeout(T), d.removeEventListener("mousemove", g), d.removeEventListener("mouseleave", h), d.removeEventListener("touchmove", l), d.removeEventListener("touchend", A), t.innerHTML = n;
281
+ const o = F === "document" ? document : t;
282
+ return o.addEventListener("mousemove", Q), o.addEventListener("mouseleave", i), o.addEventListener("touchmove", c, { passive: !0 }), o.addEventListener("touchend", m), () => {
283
+ D = !1, cancelAnimationFrame(x), k !== null && clearTimeout(k), $ && $.disconnect(), o.removeEventListener("mousemove", Q), o.removeEventListener("mouseleave", i), o.removeEventListener("touchmove", c), o.removeEventListener("touchend", m), t.innerHTML = n;
218
284
  };
219
285
  }
220
- function ct(t) {
221
- const n = j(null), o = j(null), u = j(t);
222
- u.current = t;
223
- const c = j(null), a = t.mode ?? "field", { axes: M, radius: v, falloff: f, magnetMode: x, wdthBoost: N, scope: F } = t, y = M ? JSON.stringify(M) : void 0, q = t.props ? JSON.stringify(t.props) : void 0, w = X(() => {
224
- const p = n.current;
225
- if (!p) return;
226
- o.current === null && (o.current = ot(p)), c.current && (c.current(), c.current = null), (u.current.mode ?? "field") === "field" ? c.current = st(p, o.current, u.current) : c.current = rt(p, o.current, u.current);
227
- }, [a, y, v, f, x, N, F, q]);
228
- return et(() => (w(), () => {
229
- c.current && (c.current(), c.current = null);
230
- }), [w]), Q(() => {
231
- document.fonts.ready.then(w);
232
- }, [w]), n;
286
+ function yt(t) {
287
+ const n = I(null), s = I(null), S = I(t);
288
+ S.current = t;
289
+ const h = I(null), u = t.mode ?? "field", { axes: b, radius: F, falloff: d, magnetMode: w, wdthBoost: L, scope: X } = t, V = b ? JSON.stringify(b) : void 0, j = t.props ? JSON.stringify(t.props) : void 0, E = z(() => {
290
+ const y = n.current;
291
+ if (!y) return;
292
+ s.current === null && (s.current = mt(y)), h.current && (h.current(), h.current = null), (S.current.mode ?? "field") === "field" ? h.current = ht(y, s.current, S.current) : h.current = pt(y, s.current, S.current);
293
+ }, [u, V, F, d, w, L, X, j]);
294
+ return ft(() => (E(), () => {
295
+ h.current && (h.current(), h.current = null);
296
+ }), [E]), W(() => {
297
+ var y, N;
298
+ (N = (y = document.fonts) == null ? void 0 : y.ready) == null || N.then(E);
299
+ }, [E]), n;
233
300
  }
234
- const it = Z(
235
- function({ children: n, as: o = "p", className: u, style: c, ...a }, M) {
236
- const v = ct(a), f = X(
237
- (x) => {
238
- v.current = x, typeof M == "function" ? M(x) : M && (M.current = x);
301
+ const gt = lt(
302
+ function({ children: n, as: s = "p", className: S, style: h, ...u }, b) {
303
+ const F = yt(u), d = z(
304
+ (w) => {
305
+ F.current = w, typeof b == "function" ? b(w) : b && (b.current = w);
239
306
  },
240
307
  // eslint-disable-next-line react-hooks/exhaustive-deps
241
- [M]
308
+ [b]
242
309
  );
243
- return /* @__PURE__ */ D(o, { ref: f, className: u, style: c, children: n });
310
+ return /* @__PURE__ */ tt(s, { ref: d, className: S, style: h, children: n });
244
311
  }
245
312
  );
246
- it.displayName = "MagnetTypeText";
247
- const at = Z(
313
+ gt.displayName = "MagnetTypeText";
314
+ const vt = lt(
248
315
  function({
249
316
  children: n,
250
- as: o = "p",
251
- className: u,
252
- style: c,
253
- minWeight: a = 300,
254
- maxWeight: M = 600,
255
- proximityRadius: v,
256
- spreadRadius: f,
257
- fixedAxes: x = {}
258
- }, N) {
259
- const F = j(null), y = j(null), q = j([]), w = X(
260
- (s) => {
261
- F.current = s, typeof N == "function" ? N(s) : N && (N.current = s);
317
+ as: s = "p",
318
+ className: S,
319
+ style: h,
320
+ minWeight: u = 300,
321
+ maxWeight: b = 600,
322
+ proximityRadius: F,
323
+ spreadRadius: d,
324
+ fixedAxes: w = {},
325
+ cachePositions: L = !0,
326
+ rafThrottle: X = !0,
327
+ stabilizeLayout: V = !0
328
+ }, j) {
329
+ const E = I(null), y = I(null), N = I([]), H = I([]), _ = I(null), Y = I(!1), A = I(null), $ = I(0), B = z(
330
+ (i) => {
331
+ E.current = i, typeof j == "function" ? j(i) : j && (j.current = i);
262
332
  },
263
333
  // eslint-disable-next-line react-hooks/exhaustive-deps
264
- [N]
334
+ [j]
265
335
  );
266
- function p(s) {
267
- const g = [`'wght' ${s.toFixed(0)}`];
268
- for (const [h, l] of Object.entries(x)) g.push(`'${h}' ${l}`);
269
- return g.join(", ");
336
+ function v(i) {
337
+ const c = [`'wght' ${i.toFixed(0)}`];
338
+ for (const [m, o] of Object.entries(w)) c.push(`'${m}' ${o}`);
339
+ return c.join(", ");
270
340
  }
271
- const b = nt(() => {
272
- if (!f) return n;
273
- q.current = [];
274
- let s = 0;
275
- function g(h) {
276
- if (typeof h == "string")
277
- return [...h].map((l) => {
278
- if (/\s/.test(l)) return l;
279
- const A = s++;
280
- return /* @__PURE__ */ D(
281
- "span",
282
- {
283
- ref: (d) => {
284
- q.current[A] = d;
285
- },
286
- style: { fontVariationSettings: p(a) },
287
- children: l
288
- },
289
- A
290
- );
291
- });
292
- if (Array.isArray(h)) return h.map((l, A) => /* @__PURE__ */ D(J.Fragment, { children: g(l) }, A));
293
- if (J.isValidElement(h)) {
294
- const l = h;
295
- if (l.props.children !== void 0)
296
- return J.cloneElement(l, {}, g(l.props.children));
297
- }
298
- return h;
341
+ function R() {
342
+ const i = E.current;
343
+ if (!i) return;
344
+ const c = window.scrollX, m = window.scrollY, o = i.getBoundingClientRect();
345
+ _.current = {
346
+ top: o.top + m,
347
+ left: o.left + c,
348
+ right: o.right + c,
349
+ bottom: o.bottom + m
350
+ }, H.current = N.current.map((e) => {
351
+ if (!e) return { cx: 0, cy: 0 };
352
+ const M = e.getBoundingClientRect();
353
+ return {
354
+ cx: (M.left + M.right) / 2 + c,
355
+ cy: (M.top + M.bottom) / 2 + m
356
+ };
357
+ }), Y.current = !0;
358
+ }
359
+ function x(i, c) {
360
+ const m = E.current;
361
+ if (!m) return;
362
+ L && !Y.current && R();
363
+ let o, e, M, a, r, g;
364
+ if (L && _.current)
365
+ r = i + window.scrollX, g = c + window.scrollY, { top: o, left: e, right: M, bottom: a } = _.current;
366
+ else {
367
+ const C = m.getBoundingClientRect();
368
+ r = i, g = c, o = C.top, e = C.left, M = C.right, a = C.bottom;
299
369
  }
300
- return g(n);
301
- }, [n, f, a, JSON.stringify(x)]);
302
- function E(s, g) {
303
- const h = F.current;
304
- if (!h) return;
305
- const l = h.getBoundingClientRect(), A = Math.max(l.left - s, 0, s - l.right), d = Math.max(l.top - g, 0, g - l.bottom), r = Math.sqrt(A * A + d * d);
306
- if (v !== void 0 && !f) {
307
- const L = 1 - (1 - Math.max(0, 1 - r / v)) ** 2;
308
- h.style.fontVariationSettings = p(a + (M - a) * L);
370
+ const l = Math.max(e - r, 0, r - M), f = Math.max(o - g, 0, g - a), O = Math.sqrt(l * l + f * f);
371
+ if (F !== void 0 && !d) {
372
+ const p = 1 - (1 - Math.max(0, 1 - O / F)) ** 2;
373
+ m.style.fontVariationSettings = v(u + (b - u) * p);
309
374
  return;
310
375
  }
311
- if (f) {
312
- if (v !== void 0 && r > v) {
313
- h.style.fontVariationSettings = p(a);
314
- for (const e of q.current) e && (e.style.fontVariationSettings = p(a));
376
+ if (d) {
377
+ if (F !== void 0 && O > F) {
378
+ m.style.fontVariationSettings = v(u);
379
+ for (const p of N.current)
380
+ p && (p.style.fontVariationSettings = v(u), V && (p.style.letterSpacing = ""));
315
381
  return;
316
382
  }
317
- for (const e of q.current) {
318
- if (!e) continue;
319
- const L = e.getBoundingClientRect(), C = (L.left + L.right) / 2, m = (L.top + L.bottom) / 2, V = Math.sqrt((s - C) ** 2 + (g - m) ** 2), k = 1 - (1 - Math.max(0, 1 - V / f)) ** 2;
320
- e.style.fontVariationSettings = p(a + (M - a) * k);
383
+ if (O > d) {
384
+ for (const p of N.current)
385
+ p && (p.style.fontVariationSettings = v(u), V && (p.style.letterSpacing = ""));
386
+ return;
321
387
  }
388
+ const C = N.current;
389
+ if (L && H.current.length === C.length)
390
+ for (let p = 0; p < C.length; p++) {
391
+ const T = C[p];
392
+ if (!T) continue;
393
+ const { cx: q, cy: P } = H.current[p], Z = Math.sqrt((r - q) ** 2 + (g - P) ** 2), G = 1 - (1 - Math.max(0, 1 - Z / d)) ** 2;
394
+ T.style.fontVariationSettings = v(u + (b - u) * G), V && $.current !== 0 && (T.style.letterSpacing = `${(-$.current * G).toFixed(3)}px`);
395
+ }
396
+ else
397
+ for (const p of C) {
398
+ if (!p) continue;
399
+ const T = p.getBoundingClientRect(), q = (T.left + T.right) / 2, P = (T.top + T.bottom) / 2, Z = Math.sqrt((i - q) ** 2 + (c - P) ** 2), G = 1 - (1 - Math.max(0, 1 - Z / d)) ** 2;
400
+ p.style.fontVariationSettings = v(u + (b - u) * G), V && $.current !== 0 && (p.style.letterSpacing = `${(-$.current * G).toFixed(3)}px`);
401
+ }
322
402
  }
323
403
  }
324
- const i = X(
325
- (s) => {
326
- y.current = { x: s.clientX, y: s.clientY }, E(s.clientX, s.clientY);
404
+ const D = z(
405
+ (i) => {
406
+ y.current = { x: i.clientX, y: i.clientY }, X ? A.current === null && (A.current = requestAnimationFrame(() => {
407
+ A.current = null, y.current && x(y.current.x, y.current.y);
408
+ })) : x(i.clientX, i.clientY);
327
409
  },
328
- [a, M, v, f]
329
- ), B = X(
410
+ // eslint-disable-next-line react-hooks/exhaustive-deps
411
+ [u, b, F, d, L, X, V, JSON.stringify(w)]
412
+ ), k = z(
330
413
  () => {
331
- y.current && E(y.current.x, y.current.y);
414
+ y.current && x(y.current.x, y.current.y);
332
415
  },
333
- [a, M, v, f]
334
- ), T = X(
416
+ // eslint-disable-next-line react-hooks/exhaustive-deps
417
+ [u, b, F, d, L, V, JSON.stringify(w)]
418
+ ), J = z(
335
419
  () => {
336
- y.current = null;
337
- const s = F.current;
338
- s && (s.style.fontVariationSettings = p(a));
339
- for (const g of q.current) g && (g.style.fontVariationSettings = p(a));
420
+ y.current = null, A.current !== null && (cancelAnimationFrame(A.current), A.current = null);
421
+ const i = E.current;
422
+ i && (i.style.fontVariationSettings = v(u));
423
+ for (const c of N.current)
424
+ c && (c.style.fontVariationSettings = v(u), V && (c.style.letterSpacing = ""));
340
425
  },
341
- [a]
426
+ // eslint-disable-next-line react-hooks/exhaustive-deps
427
+ [u, V, JSON.stringify(w)]
342
428
  );
343
- return Q(() => (window.addEventListener("mousemove", i, { passive: !0 }), window.addEventListener("scroll", B, { passive: !0, capture: !0 }), document.documentElement.addEventListener("mouseleave", T), () => {
344
- window.removeEventListener("mousemove", i), window.removeEventListener("scroll", B, { capture: !0 }), document.documentElement.removeEventListener("mouseleave", T);
345
- }), [i, B, T]), /* @__PURE__ */ D(
346
- o,
429
+ W(() => (window.addEventListener("mousemove", D, { passive: !0 }), window.addEventListener("scroll", k, { passive: !0, capture: !0 }), document.documentElement.addEventListener("mouseleave", J), () => {
430
+ window.removeEventListener("mousemove", D), window.removeEventListener("scroll", k, { capture: !0 }), document.documentElement.removeEventListener("mouseleave", J), A.current !== null && (cancelAnimationFrame(A.current), A.current = null);
431
+ }), [D, k, J]), W(() => {
432
+ var m, o;
433
+ if (!L || !d) return;
434
+ Y.current = !1;
435
+ const i = E.current;
436
+ if (!i) return;
437
+ const c = new ResizeObserver(() => {
438
+ Y.current = !1;
439
+ });
440
+ return c.observe(i), (o = (m = document.fonts) == null ? void 0 : m.ready) == null || o.then(() => {
441
+ Y.current = !1;
442
+ }), () => c.disconnect();
443
+ }, [L, d]), W(() => {
444
+ Y.current = !1;
445
+ }, [n, d]), W(() => {
446
+ if (!V || !d) {
447
+ $.current = 0;
448
+ return;
449
+ }
450
+ const i = E.current;
451
+ if (!i) return;
452
+ const c = i.textContent ?? "", m = c.replace(/\s/g, "").length;
453
+ if (m === 0) return;
454
+ const o = getComputedStyle(i), e = document.createElement("span");
455
+ e.style.cssText = "position:fixed;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;pointer-events:none;", e.style.fontFamily = o.fontFamily, e.style.fontSize = o.fontSize, e.style.fontWeight = o.fontWeight, e.style.lineHeight = o.lineHeight, e.style.letterSpacing = o.letterSpacing, e.textContent = c, document.body.appendChild(e);
456
+ const M = (g) => {
457
+ const l = [`'wght' ${g.toFixed(0)}`];
458
+ for (const [f, O] of Object.entries(w)) l.push(`'${f}' ${O}`);
459
+ return l.join(", ");
460
+ };
461
+ e.style.fontVariationSettings = M(b);
462
+ const a = e.scrollWidth;
463
+ e.style.fontVariationSettings = M(u);
464
+ const r = e.scrollWidth;
465
+ document.body.removeChild(e), $.current = a > r ? (a - r) / m : 0;
466
+ }, [V, d, u, b, n, JSON.stringify(w)]);
467
+ const Q = dt(() => {
468
+ if (!d) return n;
469
+ N.current = [];
470
+ let i = 0;
471
+ function c(m) {
472
+ if (typeof m == "string")
473
+ return [...m].map((o) => {
474
+ if (/\s/.test(o)) return o;
475
+ const e = i++;
476
+ return /* @__PURE__ */ tt(
477
+ "span",
478
+ {
479
+ ref: (M) => {
480
+ N.current[e] = M;
481
+ },
482
+ style: { fontVariationSettings: v(u) },
483
+ children: o
484
+ },
485
+ e
486
+ );
487
+ });
488
+ if (Array.isArray(m)) return m.map((o, e) => /* @__PURE__ */ tt(rt.Fragment, { children: c(o) }, e));
489
+ if (rt.isValidElement(m)) {
490
+ const o = m;
491
+ if (o.props.children !== void 0)
492
+ return rt.cloneElement(o, {}, c(o.props.children));
493
+ }
494
+ return m;
495
+ }
496
+ return c(n);
497
+ }, [n, d, u, JSON.stringify(w)]);
498
+ return /* @__PURE__ */ tt(
499
+ s,
347
500
  {
348
- ref: w,
349
- className: u,
350
- style: { fontVariationSettings: p(a), ...c },
351
- children: b
501
+ ref: B,
502
+ className: S,
503
+ style: { fontVariationSettings: v(u), ...h },
504
+ children: Q
352
505
  }
353
506
  );
354
507
  }
355
508
  );
356
- at.displayName = "MagnetBlock";
509
+ vt.displayName = "MagnetBlock";
357
510
  export {
358
- P as MAGNET_TYPE_CLASSES,
359
- at as MagnetBlock,
360
- it as MagnetTypeText,
361
- rt as applyMagnetType,
362
- ot as getCleanHTML,
363
- lt as removeMagnetType,
364
- st as startMagnetType,
365
- ct as useMagnetType
511
+ nt as MAGNET_TYPE_CLASSES,
512
+ vt as MagnetBlock,
513
+ gt as MagnetTypeText,
514
+ pt as applyMagnetType,
515
+ mt as getCleanHTML,
516
+ Mt as removeMagnetType,
517
+ ht as startMagnetType,
518
+ yt as useMagnetType
366
519
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/magnettype",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Cursor-field per-word variable font axis variation and per-character legibility mode",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",