@liiift-studio/magnettype 1.0.0 → 1.0.6

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/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const x=require("react"),I=require("react/jsx-runtime"),j={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},S={word:"mt-word",char:"mt-char",probe:"mt-probe"},N={axes:{wght:[300,500]},radius:120,falloff:"quadratic",magnetMode:"attract",wdthBoost:6};function F(e,n=[]){return e.nodeType===Node.TEXT_NODE?n.push(e):e.childNodes.forEach(o=>F(o,n)),n}function H(e,n,o){if(!e||e==="normal")return`"${n}" ${o}`;const r=new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`),t=`"${n}" ${o}`;return r.test(e)?e.replace(r,t):`${e}, ${t}`}function B(e,n){let o=e;for(const[r,t]of Object.entries(n))o=H(o,r,t);return o}function V(e){const n=e.cloneNode(!0),o=n.querySelectorAll(`.${S.word}, .${S.char}`);return Array.from(o).reverse().forEach(t=>{const l=t.parentNode;if(l){for(;t.firstChild;)l.insertBefore(t.firstChild,t);l.removeChild(t)}}),n.innerHTML}function X(e,n){e.innerHTML=n}function k(e,n,o={}){if(typeof window>"u")return;const r=window.scrollY,t=o.wdthBoost??N.wdthBoost;e.innerHTML=n;const l=F(e),a=getComputedStyle(e).fontVariationSettings;for(const h of l){const y=h.textContent??"";if(!y||!y.split("").some(m=>m in j))continue;const M=document.createDocumentFragment();for(const m of y){const p=j[m];if(p===void 0){const s=M.lastChild;s&&s.nodeType===Node.TEXT_NODE?s.textContent+=m:M.appendChild(document.createTextNode(m))}else{const s=t*(p/3),c=document.createElement("span");c.className=S.char;const f=a.match(/"wdth"\s+([\d.eE+-]+)/),E=(f?parseFloat(f[1]):100)+s;c.style.fontVariationSettings=H(a,"wdth",E),c.textContent=m,M.appendChild(c)}}h.parentNode.replaceChild(M,h)}requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-r)>2&&window.scrollTo({top:r,behavior:"instant"})})}function W(e,n,o={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return e.innerHTML=n,()=>{};const r=o.axes??N.axes,t=o.radius??N.radius,l=o.falloff??N.falloff,a=o.magnetMode??N.magnetMode,h=window.scrollY;e.innerHTML=n;const y=F(e),u=[];for(const i of y){const d=i.textContent??"";if(!d.trim())continue;const w=d.split(/(\S+)/),T=document.createDocumentFragment();for(let g=0;g<w.length;g+=2){const R=w[g],b=w[g+1];if(!b)continue;const q=w[g+3]===void 0?w[g+2]??"":"",v=document.createElement("span");v.className=S.word,v.textContent=R+b+q,T.appendChild(v),u.push(v)}i.parentNode.replaceChild(T,i)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-h)>2&&window.scrollTo({top:h,behavior:"instant"})}),u.length===0)return()=>{};const M=getComputedStyle(e).fontVariationSettings,m=B(M,Object.fromEntries(Object.entries(r).map(([i,[d]])=>[i,d])));u.forEach(i=>{i.style.fontVariationSettings=m});let p=-9999,s=-9999,c=!1,f=0,C=!0;function E(i,d){const[w,T]=r[i]??[300,500],g=a==="repel"?1-d:d;return w+(T-w)*g}function L(){if(!C)return;if(!c){u.forEach(d=>{d.style.fontVariationSettings=m}),f=0;return}const i=u.map(d=>d.getBoundingClientRect());u.forEach((d,w)=>{const T=i[w],g=T.left+T.width/2,R=T.top+T.height/2,b=Math.sqrt((p-g)**2+(s-R)**2),A=Math.max(0,1-b/t),q=l==="quadratic"?A*A:A,v={};for(const $ of Object.keys(r))v[$]=E($,q);d.style.fontVariationSettings=B(M,v)}),f=requestAnimationFrame(L)}function O(i){p=i.clientX,s=i.clientY,c||(c=!0),f===0&&(f=requestAnimationFrame(L))}function Y(){c=!1,f===0&&(f=requestAnimationFrame(L))}return e.addEventListener("mousemove",O),e.addEventListener("mouseleave",Y),()=>{C=!1,cancelAnimationFrame(f),e.removeEventListener("mousemove",O),e.removeEventListener("mouseleave",Y),e.innerHTML=n}}function _(e){const n=x.useRef(null),o=x.useRef(null),r=x.useRef(e);r.current=e;const t=x.useRef(null),l=e.mode??"field",{axes:a,radius:h,falloff:y,magnetMode:u,wdthBoost:M}=e,m=a?JSON.stringify(a):void 0,p=x.useCallback(()=>{const s=n.current;if(!s)return;o.current===null&&(o.current=V(s)),t.current&&(t.current(),t.current=null),(r.current.mode??"field")==="field"?t.current=W(s,o.current,r.current):k(s,o.current,r.current)},[l,m,h,y,u,M]);return x.useLayoutEffect(()=>{if(p(),typeof ResizeObserver>"u")return()=>{t.current&&(t.current(),t.current=null)};if((r.current.mode??"field")==="field")return()=>{t.current&&(t.current(),t.current=null)};let s=0,c=0;const f=new ResizeObserver(C=>{const E=Math.round(C[0].contentRect.width);E!==s&&(s=E,cancelAnimationFrame(c),c=requestAnimationFrame(p))});return f.observe(n.current),()=>{f.disconnect(),cancelAnimationFrame(c),t.current&&(t.current(),t.current=null)}},[p]),x.useEffect(()=>{document.fonts.ready.then(p)},[p]),n}const D=x.forwardRef(function({children:n,as:o="p",className:r,style:t,...l},a){const h=_(l),y=x.useCallback(u=>{h.current=u,typeof a=="function"?a(u):a&&(a.current=u)},[a]);return I.jsx(o,{ref:y,className:r,style:t,children:n})});D.displayName="MagnetTypeText";exports.MAGNET_TYPE_CLASSES=S;exports.MagnetTypeText=D;exports.applyMagnetType=k;exports.getCleanHTML=V;exports.removeMagnetType=X;exports.startMagnetType=W;exports.useMagnetType=_;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const C=require("react"),z=require("react/jsx-runtime"),_={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},B={word:"mt-word",char:"mt-char",probe:"mt-probe"},L={axes:{wght:[300,500]},radius:120,falloff:"quadratic",magnetMode:"attract",wdthBoost:6,scope:"document"};function k(e,t=[]){return e.nodeType===Node.TEXT_NODE?t.push(e):e.childNodes.forEach(n=>k(n,t)),t}function j(e,t,n){if(!e||e==="normal")return`"${t}" ${n}`;const c=new RegExp(`(["'])${t}\\1\\s+[\\d.eE+-]+`),o=`"${t}" ${n}`;return c.test(e)?e.replace(c,o):`${e}, ${o}`}function D(e,t){let n=e;for(const[c,o]of Object.entries(t))n=j(n,c,o);return n}function I(e,t,n){if(t.opacity!==void 0){const[c,o]=t.opacity;e.style.opacity=String(c+(o-c)*n)}t.italic===!0&&(e.style.fontStyle=n>.5?"italic":"")}function V(e,t){t.opacity!==void 0&&(e.style.opacity=String(t.opacity[0])),t.italic===!0&&(e.style.fontStyle="")}function P(e){const t=e.cloneNode(!0),n=t.querySelectorAll(`.${B.word}, .${B.char}`);return Array.from(n).reverse().forEach(o=>{const y=o.parentNode;if(y){for(;o.firstChild;)y.insertBefore(o.firstChild,o);y.removeChild(o)}}),t.innerHTML}function Q(e,t){e.innerHTML=t}function W(e,t,n={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return e.innerHTML=t,()=>{};const c=n.wdthBoost??L.wdthBoost,o=n.radius??L.radius,y=n.falloff??L.falloff,m=n.scope??L.scope,p=n.props,d=window.scrollY;e.innerHTML=t;const l=getComputedStyle(e).fontVariationSettings,b=l.match(/"wdth"\s+([\d.eE+-]+)/),h=b?parseFloat(b[1]):100,q=k(e),g=[];for(const r of q){const s=r.textContent??"";if(!s||!s.split("").some(i=>i in _))continue;const u=document.createDocumentFragment();for(const i of s){const N=_[i];if(N===void 0){const a=u.lastChild;a&&a.nodeType===Node.TEXT_NODE?a.textContent+=i:u.appendChild(document.createTextNode(i))}else{const a=document.createElement("span");a.className=B.char,a.style.fontVariationSettings=j(l,"wdth",h),a.textContent=i,u.appendChild(a),g.push({span:a,riskLevel:N})}}r.parentNode.replaceChild(u,r)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-d)>2&&window.scrollTo({top:d,behavior:"instant"})}),g.length===0)return()=>{};p&&g.forEach(({span:r})=>V(r,p));let w=-9999,v=-9999,M=!1,f=0,R=!0;function A(){if(!R)return;if(!M){g.forEach(({span:s})=>{s.style.fontVariationSettings=j(l,"wdth",h),p&&V(s,p)}),f=0;return}const r=g.map(({span:s})=>s.getBoundingClientRect());g.forEach(({span:s,riskLevel:T},u)=>{const i=r[u],N=i.left+i.width/2,a=i.top+i.height/2,F=Math.sqrt((w-N)**2+(v-a)**2),x=Math.max(0,1-F/o),S=y==="quadratic"?x*x:x,$=c*(T/3)*S;s.style.fontVariationSettings=j(l,"wdth",h+$),p&&I(s,p,S)}),f=requestAnimationFrame(A)}function O(r){w=r.clientX,v=r.clientY,M||(M=!0),f===0&&(f=requestAnimationFrame(A))}function Y(){M=!1,f===0&&(f=requestAnimationFrame(A))}const E=m==="document"?document:e;return E.addEventListener("mousemove",O),E.addEventListener("mouseleave",Y),()=>{R=!1,cancelAnimationFrame(f),E.removeEventListener("mousemove",O),E.removeEventListener("mouseleave",Y),e.innerHTML=t}}function G(e,t,n={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return e.innerHTML=t,()=>{};const c=n.axes??L.axes,o=n.radius??L.radius,y=n.falloff??L.falloff,m=n.magnetMode??L.magnetMode,p=n.scope??L.scope,d=n.props,l=window.scrollY;e.innerHTML=t;const b=k(e),h=[];for(const r of b){const s=r.textContent??"";if(!s.trim())continue;const T=s.split(/(\S+)/),u=document.createDocumentFragment();for(let i=0;i<T.length;i+=2){const N=T[i],a=T[i+1];if(!a)continue;const x=T[i+3]===void 0?T[i+2]??"":"",S=document.createElement("span");S.className=B.word,S.textContent=N+a+x,u.appendChild(S),h.push(S)}r.parentNode.replaceChild(u,r)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-l)>2&&window.scrollTo({top:l,behavior:"instant"})}),h.length===0)return()=>{};const q=getComputedStyle(e).fontVariationSettings,g=D(q,Object.fromEntries(Object.entries(c).map(([r,[s]])=>[r,s])));h.forEach(r=>{r.style.fontVariationSettings=g,d&&V(r,d)});let w=-9999,v=-9999,M=!1,f=0,R=!0;function A(){if(!R)return;if(!M){h.forEach(s=>{s.style.fontVariationSettings=g,d&&V(s,d)}),f=0;return}const r=h.map(s=>s.getBoundingClientRect());h.forEach((s,T)=>{const u=r[T],i=u.left+u.width/2,N=u.top+u.height/2,a=Math.sqrt((w-i)**2+(v-N)**2),F=Math.max(0,1-a/o),x=y==="quadratic"?F*F:F,S=m==="repel"?1-x:x,$={};for(const H of Object.keys(c)){const[X,U]=c[H]??[300,500];$[H]=X+(U-X)*S}s.style.fontVariationSettings=D(q,$),d&&I(s,d,x)}),f=requestAnimationFrame(A)}function O(r){w=r.clientX,v=r.clientY,M||(M=!0),f===0&&(f=requestAnimationFrame(A))}function Y(){M=!1,f===0&&(f=requestAnimationFrame(A))}const E=p==="document"?document:e;return E.addEventListener("mousemove",O),E.addEventListener("mouseleave",Y),()=>{R=!1,cancelAnimationFrame(f),E.removeEventListener("mousemove",O),E.removeEventListener("mouseleave",Y),e.innerHTML=t}}function J(e){const t=C.useRef(null),n=C.useRef(null),c=C.useRef(e);c.current=e;const o=C.useRef(null),y=e.mode??"field",{axes:m,radius:p,falloff:d,magnetMode:l,wdthBoost:b,scope:h}=e,q=m?JSON.stringify(m):void 0,g=e.props?JSON.stringify(e.props):void 0,w=C.useCallback(()=>{const v=t.current;if(!v)return;n.current===null&&(n.current=P(v)),o.current&&(o.current(),o.current=null),(c.current.mode??"field")==="field"?o.current=G(v,n.current,c.current):o.current=W(v,n.current,c.current)},[y,q,p,d,l,b,h,g]);return C.useLayoutEffect(()=>(w(),()=>{o.current&&(o.current(),o.current=null)}),[w]),C.useEffect(()=>{document.fonts.ready.then(w)},[w]),t}const K=C.forwardRef(function({children:t,as:n="p",className:c,style:o,...y},m){const p=J(y),d=C.useCallback(l=>{p.current=l,typeof m=="function"?m(l):m&&(m.current=l)},[m]);return z.jsx(n,{ref:d,className:c,style:o,children:t})});K.displayName="MagnetTypeText";exports.MAGNET_TYPE_CLASSES=B;exports.MagnetTypeText=K;exports.applyMagnetType=W;exports.getCleanHTML=P;exports.removeMagnetType=Q;exports.startMagnetType=G;exports.useMagnetType=J;
package/dist/index.d.ts CHANGED
@@ -2,16 +2,22 @@ import { default as default_2 } from 'react';
2
2
  import { RefObject } from 'react';
3
3
 
4
4
  /**
5
- * Apply per-character wdth boost to visually confusable characters (legibility mode).
5
+ * Start the legibility effect on an element.
6
6
  *
7
- * Wraps each character in a span with a boosted wdth axis value proportional to
8
- * the character's confusion risk level. Non-confusable characters are left unwrapped.
7
+ * Wraps visually confusable characters in spans, then listens for mousemove events
8
+ * to drive per-character wdth boost based on cursor distance. Characters near the
9
+ * cursor receive a wdth boost proportional to their confusion risk level.
10
+ * Resets to base wdth on mouseleave.
11
+ *
12
+ * Defaults to listening on document (scope: 'document'), enabling cross-paragraph
13
+ * effects when multiple elements are independently targeted.
9
14
  *
10
15
  * @param element - Target element (must be in the live DOM and visible)
11
16
  * @param originalHTML - Clean HTML snapshot from getCleanHTML()
12
- * @param options - MagnetTypeOptions; only wdthBoost is used here
17
+ * @param options - MagnetTypeOptions; wdthBoost, radius, falloff, scope, props used
18
+ * @returns - A stop function. Call it to cancel listeners and restore markup.
13
19
  */
14
- export declare function applyMagnetType(element: HTMLElement, originalHTML: string, options?: MagnetTypeOptions): void;
20
+ export declare function applyMagnetType(element: HTMLElement, originalHTML: string, options?: MagnetTypeOptions): () => void;
15
21
 
16
22
  /** Falloff curve for the cursor proximity field */
17
23
  export declare type FalloffType = 'linear' | 'quadratic';
@@ -43,25 +49,26 @@ export declare type MagnetTypeModeType = 'field' | 'legibility';
43
49
  *
44
50
  * Two modes share this options type:
45
51
  * - 'field' — cursor proximity drives per-word variable font axis values
46
- * - 'legibility' — per-character wdth boost for visually confusable characters
52
+ * - 'legibility' — cursor proximity drives per-character wdth boost for confusable characters
47
53
  */
48
54
  export declare interface MagnetTypeOptions {
49
55
  /**
50
56
  * Operating mode. Default: 'field'
51
57
  *
52
58
  * - **'field'** — cursor proximity drives per-word font-variation-settings.
53
- * - **'legibility'** — per-character wdth boost for confusable characters; no cursor needed.
59
+ * - **'legibility'** — cursor-driven wdth boost for confusable characters; both return a stop function.
54
60
  */
55
61
  mode?: MagnetTypeModeType;
56
62
  /**
57
- * Map of axis tag [restValue, peakValue].
58
- * restValue is applied at full distance; peakValue when cursor is directly over the word.
59
- * Default: { wght: [300, 500] }
63
+ * Scope for cursor event listeners. Default: 'document'
64
+ *
65
+ * - **'document'** listens on document; works across multiple elements and paragraphs.
66
+ * - **'element'** — listens only on the target element.
60
67
  */
61
- axes?: Record<string, [number, number]>;
68
+ scope?: ScopeType;
62
69
  /**
63
70
  * Pixel radius over which the field effect fades.
64
- * Words beyond this distance receive restValue. Default: 120
71
+ * Words/chars beyond this distance receive restValue. Default: 120
65
72
  */
66
73
  radius?: number;
67
74
  /**
@@ -71,6 +78,17 @@ export declare interface MagnetTypeOptions {
71
78
  * - **'quadratic'** — strength decreases as distance², giving a tighter hot zone
72
79
  */
73
80
  falloff?: FalloffType;
81
+ /**
82
+ * Additional CSS property effects driven by cursor proximity.
83
+ * Supports opacity [rest, peak] and italic toggle.
84
+ */
85
+ props?: MagnetTypeProps;
86
+ /**
87
+ * Map of axis tag → [restValue, peakValue].
88
+ * restValue is applied at full distance; peakValue when cursor is directly over the word.
89
+ * Default: { wght: [300, 500] }
90
+ */
91
+ axes?: Record<string, [number, number]>;
74
92
  /**
75
93
  * Attraction or repulsion mode. Default: 'attract'
76
94
  *
@@ -79,15 +97,32 @@ export declare interface MagnetTypeOptions {
79
97
  */
80
98
  magnetMode?: MagnetModeType;
81
99
  /**
82
- * wdth axis units to add to confusable characters. Default: 6
100
+ * wdth axis units to add to confusable characters at full cursor strength. Default: 6
83
101
  *
84
- * Risk 1 characters receive wdthBoost × (1/3).
85
- * Risk 2 characters receive wdthBoost × (2/3).
86
- * Risk 3 characters receive wdthBoost × (3/3) = full boost.
102
+ * Risk 1 characters receive wdthBoost × (1/3) at peak.
103
+ * Risk 2 characters receive wdthBoost × (2/3) at peak.
104
+ * Risk 3 characters receive wdthBoost × (3/3) = full boost at peak.
87
105
  */
88
106
  wdthBoost?: number;
89
107
  }
90
108
 
109
+ /**
110
+ * Additional CSS property effects driven by cursor proximity.
111
+ * Applied to word spans in field mode, or char spans in legibility mode.
112
+ */
113
+ declare interface MagnetTypeProps {
114
+ /**
115
+ * Opacity range [restValue, peakValue].
116
+ * e.g. [1, 0.6] fades words/chars near the cursor.
117
+ */
118
+ opacity?: [number, number];
119
+ /**
120
+ * When true, applies font-style:italic to spans where cursor strength > 0.5.
121
+ * For variable fonts with an 'ital' axis, use axes: { ital: [0, 1] } instead.
122
+ */
123
+ italic?: boolean;
124
+ }
125
+
91
126
  /**
92
127
  * Drop-in component that applies the magnetType effect to its children.
93
128
  * Forwards the ref to the root element while also attaching the internal hook ref.
@@ -107,10 +142,17 @@ declare interface MagnetTypeTextProps extends MagnetTypeOptions {
107
142
  * Remove magnetType markup and restore the element to its original HTML.
108
143
  *
109
144
  * @param element - The element that was previously modified
110
- * @param originalHTML - The snapshot passed to applyMagnetType
145
+ * @param originalHTML - The snapshot passed to applyMagnetType / startMagnetType
111
146
  */
112
147
  export declare function removeMagnetType(element: HTMLElement, originalHTML: string): void;
113
148
 
149
+ /**
150
+ * Scope for cursor event listeners.
151
+ * 'document' enables cross-element / cross-paragraph effects (default).
152
+ * 'element' restricts the field to the target element only.
153
+ */
154
+ declare type ScopeType = 'element' | 'document';
155
+
114
156
  /**
115
157
  * Start the cursor-field effect on an element.
116
158
  *
@@ -118,9 +160,12 @@ export declare function removeMagnetType(element: HTMLElement, originalHTML: str
118
160
  * font-variation-settings based on cursor distance. Uses a requestAnimationFrame
119
161
  * loop for smooth axis interpolation. Resets all words to restValue on mouseleave.
120
162
  *
163
+ * Defaults to listening on document (scope: 'document'), enabling cross-paragraph
164
+ * effects when multiple elements are independently targeted.
165
+ *
121
166
  * @param element - Target element (must be in the live DOM and visible)
122
167
  * @param originalHTML - Clean HTML snapshot from getCleanHTML()
123
- * @param options - MagnetTypeOptions; axes, radius, falloff, magnetMode are used
168
+ * @param options - MagnetTypeOptions; axes, radius, falloff, magnetMode, scope, props used
124
169
  * @returns - A stop function. Call it to cancel the rAF loop and restore markup.
125
170
  */
126
171
  export declare function startMagnetType(element: HTMLElement, originalHTML: string, options?: MagnetTypeOptions): () => void;
@@ -129,11 +174,10 @@ export declare function startMagnetType(element: HTMLElement, originalHTML: stri
129
174
  * React hook that applies the magnetType effect to a ref'd element.
130
175
  *
131
176
  * For mode: 'field' — starts the cursor proximity rAF loop via startMagnetType.
132
- * The stop function is called on unmount and when options change.
133
- * No ResizeObserver needed — the rAF loop reads live getBoundingClientRect each frame.
177
+ * For mode: 'legibility' starts the cursor-driven wdth boost via applyMagnetType.
134
178
  *
135
- * For mode: 'legibility' applies the static per-character wdth boost via applyMagnetType.
136
- * Re-runs on container width changes via ResizeObserver, and after fonts load.
179
+ * Both modes return a stop function on mount and restart when options change.
180
+ * No ResizeObserver needed the rAF loop reads live getBoundingClientRect each frame.
137
181
  *
138
182
  * Defaults to 'field' mode if mode is undefined.
139
183
  */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { useRef as A, useCallback as j, useLayoutEffect as W, useEffect as k, forwardRef as D } from "react";
2
- import { jsx as I } from "react/jsx-runtime";
3
- const B = {
1
+ import { useRef as B, useCallback as _, useLayoutEffect as J, useEffect as K, forwardRef as U } from "react";
2
+ import { jsx as G } from "react/jsx-runtime";
3
+ const D = {
4
4
  i: 3,
5
5
  l: 3,
6
6
  1: 3,
@@ -22,7 +22,7 @@ const B = {
22
22
  c: 1,
23
23
  e: 1
24
24
  // similar bowls
25
- }, L = {
25
+ }, V = {
26
26
  /** Applied to each word span in field mode */
27
27
  word: "mt-word",
28
28
  /** Applied to each character span in legibility mode */
@@ -34,175 +34,199 @@ const B = {
34
34
  radius: 120,
35
35
  falloff: "quadratic",
36
36
  magnetMode: "attract",
37
- wdthBoost: 6
37
+ wdthBoost: 6,
38
+ scope: "document"
38
39
  };
39
- function q(e, n = []) {
40
- return e.nodeType === Node.TEXT_NODE ? n.push(e) : e.childNodes.forEach((o) => q(o, n)), n;
40
+ function k(t, e = []) {
41
+ return t.nodeType === Node.TEXT_NODE ? e.push(t) : t.childNodes.forEach((n) => k(n, e)), e;
41
42
  }
42
- function H(e, n, o) {
43
- if (!e || e === "normal") return `"${n}" ${o}`;
44
- const r = new RegExp(`(["'])${n}\\1\\s+[\\d.eE+-]+`), t = `"${n}" ${o}`;
45
- return r.test(e) ? e.replace(r, t) : `${e}, ${t}`;
43
+ function $(t, e, n) {
44
+ if (!t || t === "normal") return `"${e}" ${n}`;
45
+ const c = new RegExp(`(["'])${e}\\1\\s+[\\d.eE+-]+`), o = `"${e}" ${n}`;
46
+ return c.test(t) ? t.replace(c, o) : `${t}, ${o}`;
46
47
  }
47
- function V(e, n) {
48
- let o = e;
49
- for (const [r, t] of Object.entries(n))
50
- o = H(o, r, t);
51
- return o;
48
+ function I(t, e) {
49
+ let n = t;
50
+ for (const [c, o] of Object.entries(e))
51
+ n = $(n, c, o);
52
+ return n;
52
53
  }
53
- function X(e) {
54
- const n = e.cloneNode(!0), o = n.querySelectorAll(
55
- `.${L.word}, .${L.char}`
54
+ function P(t, e, n) {
55
+ if (e.opacity !== void 0) {
56
+ const [c, o] = e.opacity;
57
+ t.style.opacity = String(c + (o - c) * n);
58
+ }
59
+ e.italic === !0 && (t.style.fontStyle = n > 0.5 ? "italic" : "");
60
+ }
61
+ function j(t, e) {
62
+ e.opacity !== void 0 && (t.style.opacity = String(e.opacity[0])), e.italic === !0 && (t.style.fontStyle = "");
63
+ }
64
+ function z(t) {
65
+ const e = t.cloneNode(!0), n = e.querySelectorAll(
66
+ `.${V.word}, .${V.char}`
56
67
  );
57
- return Array.from(o).reverse().forEach((t) => {
58
- const l = t.parentNode;
59
- if (l) {
60
- for (; t.firstChild; ) l.insertBefore(t.firstChild, t);
61
- l.removeChild(t);
68
+ return Array.from(n).reverse().forEach((o) => {
69
+ const y = o.parentNode;
70
+ if (y) {
71
+ for (; o.firstChild; ) y.insertBefore(o.firstChild, o);
72
+ y.removeChild(o);
62
73
  }
63
- }), n.innerHTML;
74
+ }), e.innerHTML;
64
75
  }
65
- function P(e, n) {
66
- e.innerHTML = n;
76
+ function rt(t, e) {
77
+ t.innerHTML = e;
67
78
  }
68
- function _(e, n, o = {}) {
69
- if (typeof window > "u") return;
70
- const r = window.scrollY, t = o.wdthBoost ?? C.wdthBoost;
71
- e.innerHTML = n;
72
- const l = q(e), a = getComputedStyle(e).fontVariationSettings;
73
- for (const h of l) {
74
- const y = h.textContent ?? "";
75
- if (!y || !y.split("").some((m) => m in B)) continue;
76
- const M = document.createDocumentFragment();
77
- for (const m of y) {
78
- const p = B[m];
79
- if (p === void 0) {
80
- const s = M.lastChild;
81
- s && s.nodeType === Node.TEXT_NODE ? s.textContent += m : M.appendChild(document.createTextNode(m));
79
+ function Q(t, e, n = {}) {
80
+ if (typeof window > "u") return () => {
81
+ };
82
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
83
+ return t.innerHTML = e, () => {
84
+ };
85
+ const c = n.wdthBoost ?? C.wdthBoost, o = n.radius ?? C.radius, y = n.falloff ?? C.falloff, m = n.scope ?? C.scope, p = n.props, d = window.scrollY;
86
+ t.innerHTML = e;
87
+ const l = getComputedStyle(t).fontVariationSettings, A = l.match(/"wdth"\s+([\d.eE+-]+)/), h = A ? parseFloat(A[1]) : 100, q = k(t), w = [];
88
+ for (const r of q) {
89
+ const s = r.textContent ?? "";
90
+ if (!s || !s.split("").some((i) => i in D)) continue;
91
+ const u = document.createDocumentFragment();
92
+ for (const i of s) {
93
+ const L = D[i];
94
+ if (L === void 0) {
95
+ const a = u.lastChild;
96
+ a && a.nodeType === Node.TEXT_NODE ? a.textContent += i : u.appendChild(document.createTextNode(i));
82
97
  } else {
83
- const s = t * (p / 3), c = document.createElement("span");
84
- c.className = L.char;
85
- const u = a.match(/"wdth"\s+([\d.eE+-]+)/), v = (u ? parseFloat(u[1]) : 100) + s;
86
- c.style.fontVariationSettings = H(a, "wdth", v), c.textContent = m, M.appendChild(c);
98
+ const a = document.createElement("span");
99
+ a.className = V.char, a.style.fontVariationSettings = $(l, "wdth", h), a.textContent = i, u.appendChild(a), w.push({ span: a, riskLevel: L });
87
100
  }
88
101
  }
89
- h.parentNode.replaceChild(M, h);
102
+ r.parentNode.replaceChild(u, r);
90
103
  }
91
- requestAnimationFrame(() => {
92
- typeof window < "u" && Math.abs(window.scrollY - r) > 2 && window.scrollTo({ top: r, behavior: "instant" });
93
- });
104
+ if (requestAnimationFrame(() => {
105
+ typeof window < "u" && Math.abs(window.scrollY - d) > 2 && window.scrollTo({ top: d, behavior: "instant" });
106
+ }), w.length === 0) return () => {
107
+ };
108
+ p && w.forEach(({ span: r }) => j(r, p));
109
+ let g = -9999, v = -9999, M = !1, f = 0, F = !0;
110
+ function N() {
111
+ if (!F) return;
112
+ if (!M) {
113
+ w.forEach(({ span: s }) => {
114
+ s.style.fontVariationSettings = $(l, "wdth", h), p && j(s, p);
115
+ }), f = 0;
116
+ return;
117
+ }
118
+ const r = w.map(({ span: s }) => s.getBoundingClientRect());
119
+ w.forEach(({ span: s, riskLevel: E }, u) => {
120
+ const i = r[u], L = i.left + i.width / 2, a = i.top + i.height / 2, b = Math.sqrt((g - L) ** 2 + (v - a) ** 2), T = Math.max(0, 1 - b / o), S = y === "quadratic" ? T * T : T, R = c * (E / 3) * S;
121
+ s.style.fontVariationSettings = $(l, "wdth", h + R), p && P(s, p, S);
122
+ }), f = requestAnimationFrame(N);
123
+ }
124
+ function O(r) {
125
+ g = r.clientX, v = r.clientY, M || (M = !0), f === 0 && (f = requestAnimationFrame(N));
126
+ }
127
+ function Y() {
128
+ M = !1, f === 0 && (f = requestAnimationFrame(N));
129
+ }
130
+ const x = m === "document" ? document : t;
131
+ return x.addEventListener("mousemove", O), x.addEventListener("mouseleave", Y), () => {
132
+ F = !1, cancelAnimationFrame(f), x.removeEventListener("mousemove", O), x.removeEventListener("mouseleave", Y), t.innerHTML = e;
133
+ };
94
134
  }
95
- function z(e, n, o = {}) {
135
+ function Z(t, e, n = {}) {
96
136
  if (typeof window > "u") return () => {
97
137
  };
98
138
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
99
- return e.innerHTML = n, () => {
139
+ return t.innerHTML = e, () => {
100
140
  };
101
- const r = o.axes ?? C.axes, t = o.radius ?? C.radius, l = o.falloff ?? C.falloff, a = o.magnetMode ?? C.magnetMode, h = window.scrollY;
102
- e.innerHTML = n;
103
- const y = q(e), f = [];
104
- for (const i of y) {
105
- const d = i.textContent ?? "";
106
- if (!d.trim()) continue;
107
- const w = d.split(/(\S+)/), x = document.createDocumentFragment();
108
- for (let g = 0; g < w.length; g += 2) {
109
- const F = w[g], N = w[g + 1];
110
- if (!N) continue;
111
- const R = w[g + 3] === void 0 ? w[g + 2] ?? "" : "", T = document.createElement("span");
112
- T.className = L.word, T.textContent = F + N + R, x.appendChild(T), f.push(T);
141
+ const c = n.axes ?? C.axes, o = n.radius ?? C.radius, y = n.falloff ?? C.falloff, m = n.magnetMode ?? C.magnetMode, p = n.scope ?? C.scope, d = n.props, l = window.scrollY;
142
+ t.innerHTML = e;
143
+ const A = k(t), h = [];
144
+ for (const r of A) {
145
+ const s = r.textContent ?? "";
146
+ if (!s.trim()) continue;
147
+ const E = s.split(/(\S+)/), u = document.createDocumentFragment();
148
+ for (let i = 0; i < E.length; i += 2) {
149
+ const L = E[i], a = E[i + 1];
150
+ if (!a) continue;
151
+ const T = E[i + 3] === void 0 ? E[i + 2] ?? "" : "", S = document.createElement("span");
152
+ S.className = V.word, S.textContent = L + a + T, u.appendChild(S), h.push(S);
113
153
  }
114
- i.parentNode.replaceChild(x, i);
154
+ r.parentNode.replaceChild(u, r);
115
155
  }
116
156
  if (requestAnimationFrame(() => {
117
- typeof window < "u" && Math.abs(window.scrollY - h) > 2 && window.scrollTo({ top: h, behavior: "instant" });
118
- }), f.length === 0) return () => {
157
+ typeof window < "u" && Math.abs(window.scrollY - l) > 2 && window.scrollTo({ top: l, behavior: "instant" });
158
+ }), h.length === 0) return () => {
119
159
  };
120
- const M = getComputedStyle(e).fontVariationSettings, m = V(
121
- M,
122
- Object.fromEntries(Object.entries(r).map(([i, [d]]) => [i, d]))
160
+ const q = getComputedStyle(t).fontVariationSettings, w = I(
161
+ q,
162
+ Object.fromEntries(Object.entries(c).map(([r, [s]]) => [r, s]))
123
163
  );
124
- f.forEach((i) => {
125
- i.style.fontVariationSettings = m;
164
+ h.forEach((r) => {
165
+ r.style.fontVariationSettings = w, d && j(r, d);
126
166
  });
127
- let p = -9999, s = -9999, c = !1, u = 0, E = !0;
128
- function v(i, d) {
129
- const [w, x] = r[i] ?? [300, 500], g = a === "repel" ? 1 - d : d;
130
- return w + (x - w) * g;
131
- }
132
- function S() {
133
- if (!E) return;
134
- if (!c) {
135
- f.forEach((d) => {
136
- d.style.fontVariationSettings = m;
137
- }), u = 0;
167
+ let g = -9999, v = -9999, M = !1, f = 0, F = !0;
168
+ function N() {
169
+ if (!F) return;
170
+ if (!M) {
171
+ h.forEach((s) => {
172
+ s.style.fontVariationSettings = w, d && j(s, d);
173
+ }), f = 0;
138
174
  return;
139
175
  }
140
- const i = f.map((d) => d.getBoundingClientRect());
141
- f.forEach((d, w) => {
142
- const x = i[w], g = x.left + x.width / 2, F = x.top + x.height / 2, N = Math.sqrt((p - g) ** 2 + (s - F) ** 2), b = Math.max(0, 1 - N / t), R = l === "quadratic" ? b * b : b, T = {};
143
- for (const $ of Object.keys(r))
144
- T[$] = v($, R);
145
- d.style.fontVariationSettings = V(M, T);
146
- }), u = requestAnimationFrame(S);
176
+ const r = h.map((s) => s.getBoundingClientRect());
177
+ h.forEach((s, E) => {
178
+ const u = r[E], i = u.left + u.width / 2, L = u.top + u.height / 2, a = Math.sqrt((g - i) ** 2 + (v - L) ** 2), b = Math.max(0, 1 - a / o), T = y === "quadratic" ? b * b : b, S = m === "repel" ? 1 - T : T, R = {};
179
+ for (const H of Object.keys(c)) {
180
+ const [X, W] = c[H] ?? [300, 500];
181
+ R[H] = X + (W - X) * S;
182
+ }
183
+ s.style.fontVariationSettings = I(q, R), d && P(s, d, T);
184
+ }), f = requestAnimationFrame(N);
147
185
  }
148
- function O(i) {
149
- p = i.clientX, s = i.clientY, c || (c = !0), u === 0 && (u = requestAnimationFrame(S));
186
+ function O(r) {
187
+ g = r.clientX, v = r.clientY, M || (M = !0), f === 0 && (f = requestAnimationFrame(N));
150
188
  }
151
189
  function Y() {
152
- c = !1, u === 0 && (u = requestAnimationFrame(S));
190
+ M = !1, f === 0 && (f = requestAnimationFrame(N));
153
191
  }
154
- return e.addEventListener("mousemove", O), e.addEventListener("mouseleave", Y), () => {
155
- E = !1, cancelAnimationFrame(u), e.removeEventListener("mousemove", O), e.removeEventListener("mouseleave", Y), e.innerHTML = n;
192
+ const x = p === "document" ? document : t;
193
+ return x.addEventListener("mousemove", O), x.addEventListener("mouseleave", Y), () => {
194
+ F = !1, cancelAnimationFrame(f), x.removeEventListener("mousemove", O), x.removeEventListener("mouseleave", Y), t.innerHTML = e;
156
195
  };
157
196
  }
158
- function U(e) {
159
- const n = A(null), o = A(null), r = A(e);
160
- r.current = e;
161
- const t = A(null), l = e.mode ?? "field", { axes: a, radius: h, falloff: y, magnetMode: f, wdthBoost: M } = e, m = a ? JSON.stringify(a) : void 0, p = j(() => {
162
- const s = n.current;
163
- if (!s) return;
164
- o.current === null && (o.current = X(s)), t.current && (t.current(), t.current = null), (r.current.mode ?? "field") === "field" ? t.current = z(s, o.current, r.current) : _(s, o.current, r.current);
165
- }, [l, m, h, y, f, M]);
166
- return W(() => {
167
- if (p(), typeof ResizeObserver > "u")
168
- return () => {
169
- t.current && (t.current(), t.current = null);
170
- };
171
- if ((r.current.mode ?? "field") === "field")
172
- return () => {
173
- t.current && (t.current(), t.current = null);
174
- };
175
- let s = 0, c = 0;
176
- const u = new ResizeObserver((E) => {
177
- const v = Math.round(E[0].contentRect.width);
178
- v !== s && (s = v, cancelAnimationFrame(c), c = requestAnimationFrame(p));
179
- });
180
- return u.observe(n.current), () => {
181
- u.disconnect(), cancelAnimationFrame(c), t.current && (t.current(), t.current = null);
182
- };
183
- }, [p]), k(() => {
184
- document.fonts.ready.then(p);
185
- }, [p]), n;
197
+ function tt(t) {
198
+ const e = B(null), n = B(null), c = B(t);
199
+ c.current = t;
200
+ const o = B(null), y = t.mode ?? "field", { axes: m, radius: p, falloff: d, magnetMode: l, wdthBoost: A, scope: h } = t, q = m ? JSON.stringify(m) : void 0, w = t.props ? JSON.stringify(t.props) : void 0, g = _(() => {
201
+ const v = e.current;
202
+ if (!v) return;
203
+ n.current === null && (n.current = z(v)), o.current && (o.current(), o.current = null), (c.current.mode ?? "field") === "field" ? o.current = Z(v, n.current, c.current) : o.current = Q(v, n.current, c.current);
204
+ }, [y, q, p, d, l, A, h, w]);
205
+ return J(() => (g(), () => {
206
+ o.current && (o.current(), o.current = null);
207
+ }), [g]), K(() => {
208
+ document.fonts.ready.then(g);
209
+ }, [g]), e;
186
210
  }
187
- const G = D(
188
- function({ children: n, as: o = "p", className: r, style: t, ...l }, a) {
189
- const h = U(l), y = j(
190
- (f) => {
191
- h.current = f, typeof a == "function" ? a(f) : a && (a.current = f);
211
+ const et = U(
212
+ function({ children: e, as: n = "p", className: c, style: o, ...y }, m) {
213
+ const p = tt(y), d = _(
214
+ (l) => {
215
+ p.current = l, typeof m == "function" ? m(l) : m && (m.current = l);
192
216
  },
193
217
  // eslint-disable-next-line react-hooks/exhaustive-deps
194
- [a]
218
+ [m]
195
219
  );
196
- return /* @__PURE__ */ I(o, { ref: y, className: r, style: t, children: n });
220
+ return /* @__PURE__ */ G(n, { ref: d, className: c, style: o, children: e });
197
221
  }
198
222
  );
199
- G.displayName = "MagnetTypeText";
223
+ et.displayName = "MagnetTypeText";
200
224
  export {
201
- L as MAGNET_TYPE_CLASSES,
202
- G as MagnetTypeText,
203
- _ as applyMagnetType,
204
- X as getCleanHTML,
205
- P as removeMagnetType,
206
- z as startMagnetType,
207
- U as useMagnetType
225
+ V as MAGNET_TYPE_CLASSES,
226
+ et as MagnetTypeText,
227
+ Q as applyMagnetType,
228
+ z as getCleanHTML,
229
+ rt as removeMagnetType,
230
+ Z as startMagnetType,
231
+ tt as useMagnetType
208
232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liiift-studio/magnettype",
3
- "version": "1.0.0",
3
+ "version": "1.0.6",
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",