@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 +83 -30
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +50 -6
- package/dist/index.js +394 -241
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@liiift-studio/magnettype) [](https://opensource.org/licenses/MIT) [](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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
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
|
|
138
|
-
| `axes` | `{ wght: [300, 500] }` | *(field mode
|
|
139
|
-
| `radius` | `120` | *(field mode
|
|
140
|
-
| `falloff` | `'quadratic'` | *(field mode
|
|
141
|
-
| `magnetMode` | `'attract'` | *(field mode
|
|
142
|
-
| `wdthBoost` | `6` | *(legibility mode
|
|
143
|
-
| `transitionMs` | `0` | Duration in
|
|
144
|
-
| `as` | `'p'` | HTML element to render
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
192
|
-
- **Axis clamping** — optional per-axis min/max clamp to prevent
|
|
193
|
-
- **SSR hydration** — pre-render legibility mode markup on the server so boosted characters are present from first paint
|
|
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:
|
|
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
|
|
3
|
-
import { jsx as
|
|
4
|
-
const
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
|
42
|
-
return t.nodeType === Node.TEXT_NODE ? n.push(t) : t.childNodes.forEach((
|
|
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
|
|
45
|
-
if (!t || t === "normal") return `"${n}" ${
|
|
46
|
-
const
|
|
47
|
-
return
|
|
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
|
|
50
|
-
let
|
|
51
|
-
for (const [
|
|
52
|
-
|
|
53
|
-
return
|
|
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
|
|
55
|
+
function at(t, n, s) {
|
|
56
56
|
if (n.opacity !== void 0) {
|
|
57
|
-
const [
|
|
58
|
-
t.style.opacity = String(
|
|
57
|
+
const [S, h] = n.opacity;
|
|
58
|
+
t.style.opacity = String(S + (h - S) * s);
|
|
59
59
|
}
|
|
60
|
-
n.italic === !0 && (t.style.fontStyle =
|
|
60
|
+
n.italic === !0 && (t.style.fontStyle = s > 0.5 ? "italic" : "");
|
|
61
61
|
}
|
|
62
|
-
function
|
|
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
|
|
66
|
-
const n = t.cloneNode(!0),
|
|
67
|
-
`.${
|
|
65
|
+
function mt(t) {
|
|
66
|
+
const n = t.cloneNode(!0), s = n.querySelectorAll(
|
|
67
|
+
`.${nt.word}, .${nt.char}`
|
|
68
68
|
);
|
|
69
|
-
return Array.from(
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
for (;
|
|
73
|
-
|
|
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
|
|
77
|
+
function Mt(t, n) {
|
|
78
78
|
t.innerHTML = n;
|
|
79
79
|
}
|
|
80
|
-
function
|
|
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
|
|
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
|
|
89
|
-
for (const
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
92
|
-
const
|
|
93
|
-
for (const
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
const
|
|
97
|
-
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
104
|
+
e.parentNode.replaceChild(r, e);
|
|
104
105
|
}
|
|
105
106
|
if (requestAnimationFrame(() => {
|
|
106
|
-
typeof window < "u" && Math.abs(window.scrollY -
|
|
107
|
-
}),
|
|
107
|
+
typeof window < "u" && Math.abs(window.scrollY - L) > 2 && window.scrollTo({ top: L, behavior: "instant" });
|
|
108
|
+
}), y.length === 0) return () => {
|
|
108
109
|
};
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
w.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
131
|
-
|
|
153
|
+
function k(e) {
|
|
154
|
+
A = e.clientX, $ = e.clientY, B || (B = !0), v === 0 && (v = requestAnimationFrame(D));
|
|
132
155
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
156
|
+
function J() {
|
|
157
|
+
B = !1, v === 0 && (v = requestAnimationFrame(D));
|
|
135
158
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
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
|
|
140
|
-
|
|
162
|
+
function i() {
|
|
163
|
+
B = !1, v === 0 && (v = requestAnimationFrame(D));
|
|
141
164
|
}
|
|
142
|
-
const
|
|
143
|
-
return
|
|
144
|
-
|
|
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
|
|
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
|
|
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
|
|
156
|
-
for (const
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
const
|
|
160
|
-
for (let
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
-
|
|
190
|
+
a.parentNode.replaceChild(l, a);
|
|
167
191
|
}
|
|
168
192
|
if (requestAnimationFrame(() => {
|
|
169
|
-
typeof window < "u" && Math.abs(window.scrollY -
|
|
170
|
-
}),
|
|
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
|
|
173
|
-
|
|
174
|
-
Object.fromEntries(Object.entries(
|
|
196
|
+
const y = getComputedStyle(t).fontVariationSettings, N = st(
|
|
197
|
+
y,
|
|
198
|
+
Object.fromEntries(Object.entries(S).map(([a, [r]]) => [a, r]))
|
|
175
199
|
);
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
}),
|
|
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
|
|
204
|
-
|
|
269
|
+
function Q(a) {
|
|
270
|
+
B = a.clientX, v = a.clientY, R || (R = !0), x === 0 && (x = requestAnimationFrame(J));
|
|
205
271
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
272
|
+
function i() {
|
|
273
|
+
R = !1, x === 0 && (x = requestAnimationFrame(J));
|
|
208
274
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
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
|
|
213
|
-
|
|
278
|
+
function m() {
|
|
279
|
+
R = !1, x === 0 && (x = requestAnimationFrame(J));
|
|
214
280
|
}
|
|
215
|
-
const
|
|
216
|
-
return
|
|
217
|
-
|
|
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
|
|
221
|
-
const n =
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
|
|
227
|
-
}, [
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
}), [
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
235
|
-
function({ children: n, as:
|
|
236
|
-
const
|
|
237
|
-
(
|
|
238
|
-
|
|
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
|
-
[
|
|
308
|
+
[b]
|
|
242
309
|
);
|
|
243
|
-
return /* @__PURE__ */
|
|
310
|
+
return /* @__PURE__ */ tt(s, { ref: d, className: S, style: h, children: n });
|
|
244
311
|
}
|
|
245
312
|
);
|
|
246
|
-
|
|
247
|
-
const
|
|
313
|
+
gt.displayName = "MagnetTypeText";
|
|
314
|
+
const vt = lt(
|
|
248
315
|
function({
|
|
249
316
|
children: n,
|
|
250
|
-
as:
|
|
251
|
-
className:
|
|
252
|
-
style:
|
|
253
|
-
minWeight:
|
|
254
|
-
maxWeight:
|
|
255
|
-
proximityRadius:
|
|
256
|
-
spreadRadius:
|
|
257
|
-
fixedAxes:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
[
|
|
334
|
+
[j]
|
|
265
335
|
);
|
|
266
|
-
function
|
|
267
|
-
const
|
|
268
|
-
for (const [
|
|
269
|
-
return
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 (
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
for (const
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
325
|
-
(
|
|
326
|
-
y.current = { x:
|
|
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
|
-
|
|
329
|
-
|
|
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 &&
|
|
414
|
+
y.current && x(y.current.x, y.current.y);
|
|
332
415
|
},
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
338
|
-
|
|
339
|
-
for (const
|
|
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
|
-
|
|
426
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
427
|
+
[u, V, JSON.stringify(w)]
|
|
342
428
|
);
|
|
343
|
-
|
|
344
|
-
window.removeEventListener("mousemove",
|
|
345
|
-
}), [
|
|
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:
|
|
349
|
-
className:
|
|
350
|
-
style: { fontVariationSettings:
|
|
351
|
-
children:
|
|
501
|
+
ref: B,
|
|
502
|
+
className: S,
|
|
503
|
+
style: { fontVariationSettings: v(u), ...h },
|
|
504
|
+
children: Q
|
|
352
505
|
}
|
|
353
506
|
);
|
|
354
507
|
}
|
|
355
508
|
);
|
|
356
|
-
|
|
509
|
+
vt.displayName = "MagnetBlock";
|
|
357
510
|
export {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
};
|