@moonarc/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/package.json +228 -0
  4. package/src/components/Accordion.astro +110 -0
  5. package/src/components/Accordion.meta.ts +33 -0
  6. package/src/components/AnimatedBeam.astro +217 -0
  7. package/src/components/AnimatedBeam.meta.ts +39 -0
  8. package/src/components/AnimatedIcons.astro +193 -0
  9. package/src/components/AnimatedIcons.meta.ts +34 -0
  10. package/src/components/AnimatedList.astro +305 -0
  11. package/src/components/AnimatedList.meta.ts +36 -0
  12. package/src/components/AnimatedSelect.astro +227 -0
  13. package/src/components/AnimatedSelect.meta.ts +38 -0
  14. package/src/components/ArrowButton.astro +133 -0
  15. package/src/components/ArrowButton.meta.ts +32 -0
  16. package/src/components/Aurora.astro +188 -0
  17. package/src/components/Aurora.meta.ts +36 -0
  18. package/src/components/AvatarStack.astro +205 -0
  19. package/src/components/AvatarStack.meta.ts +37 -0
  20. package/src/components/BarGrow.astro +123 -0
  21. package/src/components/BarGrow.meta.ts +42 -0
  22. package/src/components/BorderBeam.astro +75 -0
  23. package/src/components/BorderBeam.meta.ts +33 -0
  24. package/src/components/BrowserFrame.astro +172 -0
  25. package/src/components/BrowserFrame.meta.ts +36 -0
  26. package/src/components/BubbleText.astro +90 -0
  27. package/src/components/BubbleText.meta.ts +38 -0
  28. package/src/components/CardStack.astro +79 -0
  29. package/src/components/CardStack.meta.ts +33 -0
  30. package/src/components/Carousel.astro +147 -0
  31. package/src/components/Carousel.meta.ts +35 -0
  32. package/src/components/CheckDraw.astro +159 -0
  33. package/src/components/CheckDraw.meta.ts +37 -0
  34. package/src/components/CircularText.astro +103 -0
  35. package/src/components/CircularText.meta.ts +38 -0
  36. package/src/components/CodeTabs.astro +213 -0
  37. package/src/components/CodeTabs.meta.ts +36 -0
  38. package/src/components/CommandPalette.astro +363 -0
  39. package/src/components/CommandPalette.meta.ts +40 -0
  40. package/src/components/Confetti.astro +143 -0
  41. package/src/components/Confetti.meta.ts +40 -0
  42. package/src/components/CopyButton.astro +189 -0
  43. package/src/components/CopyButton.meta.ts +41 -0
  44. package/src/components/CountUp.astro +134 -0
  45. package/src/components/CountUp.meta.ts +40 -0
  46. package/src/components/CountdownFlip.astro +244 -0
  47. package/src/components/CountdownFlip.meta.ts +39 -0
  48. package/src/components/CounterPreloader.astro +122 -0
  49. package/src/components/CounterPreloader.meta.ts +34 -0
  50. package/src/components/Cursor.astro +177 -0
  51. package/src/components/Cursor.meta.ts +37 -0
  52. package/src/components/CursorGrid.astro +113 -0
  53. package/src/components/CursorGrid.meta.ts +37 -0
  54. package/src/components/Curtain.astro +305 -0
  55. package/src/components/Curtain.meta.ts +38 -0
  56. package/src/components/Dialog.astro +156 -0
  57. package/src/components/Dialog.meta.ts +37 -0
  58. package/src/components/Dock.astro +193 -0
  59. package/src/components/Dock.meta.ts +35 -0
  60. package/src/components/DotPattern.astro +58 -0
  61. package/src/components/DotPattern.meta.ts +32 -0
  62. package/src/components/DottedMap.astro +153 -0
  63. package/src/components/DottedMap.meta.ts +38 -0
  64. package/src/components/Drawer.astro +196 -0
  65. package/src/components/Drawer.meta.ts +35 -0
  66. package/src/components/DynamicIsland.astro +122 -0
  67. package/src/components/DynamicIsland.meta.ts +35 -0
  68. package/src/components/FileTree.astro +207 -0
  69. package/src/components/FileTree.meta.ts +34 -0
  70. package/src/components/FilterGrid.astro +170 -0
  71. package/src/components/FilterGrid.meta.ts +38 -0
  72. package/src/components/FooterReveal.astro +54 -0
  73. package/src/components/FooterReveal.meta.ts +31 -0
  74. package/src/components/GlitchText.astro +145 -0
  75. package/src/components/GlitchText.meta.ts +39 -0
  76. package/src/components/Globe.astro +304 -0
  77. package/src/components/Globe.meta.ts +41 -0
  78. package/src/components/GlyphField.astro +363 -0
  79. package/src/components/GlyphField.meta.ts +53 -0
  80. package/src/components/GradientText.astro +63 -0
  81. package/src/components/GradientText.meta.ts +35 -0
  82. package/src/components/GridPattern.astro +55 -0
  83. package/src/components/GridPattern.meta.ts +32 -0
  84. package/src/components/HighlightMarker.astro +68 -0
  85. package/src/components/HighlightMarker.meta.ts +37 -0
  86. package/src/components/HorizontalScroll.astro +112 -0
  87. package/src/components/HorizontalScroll.meta.ts +34 -0
  88. package/src/components/HoverImageLinks.astro +219 -0
  89. package/src/components/HoverImageLinks.meta.ts +34 -0
  90. package/src/components/HoverVideo.astro +179 -0
  91. package/src/components/HoverVideo.meta.ts +37 -0
  92. package/src/components/ImageCompare.astro +212 -0
  93. package/src/components/ImageCompare.meta.ts +39 -0
  94. package/src/components/ImageTrail.astro +169 -0
  95. package/src/components/ImageTrail.meta.ts +36 -0
  96. package/src/components/Lightbox.astro +236 -0
  97. package/src/components/Lightbox.meta.ts +40 -0
  98. package/src/components/LinkPreview.astro +234 -0
  99. package/src/components/LinkPreview.meta.ts +39 -0
  100. package/src/components/LiquidGlass.astro +148 -0
  101. package/src/components/LiquidGlass.meta.ts +37 -0
  102. package/src/components/Magnetic.astro +92 -0
  103. package/src/components/Magnetic.meta.ts +31 -0
  104. package/src/components/Marquee.astro +165 -0
  105. package/src/components/Marquee.meta.ts +38 -0
  106. package/src/components/Meteors.astro +156 -0
  107. package/src/components/Meteors.meta.ts +36 -0
  108. package/src/components/Noise.astro +61 -0
  109. package/src/components/Noise.meta.ts +33 -0
  110. package/src/components/NumberFlow.astro +143 -0
  111. package/src/components/NumberFlow.meta.ts +42 -0
  112. package/src/components/Orb.astro +130 -0
  113. package/src/components/Orb.meta.ts +39 -0
  114. package/src/components/OrbitingCircles.astro +119 -0
  115. package/src/components/OrbitingCircles.meta.ts +36 -0
  116. package/src/components/OtpInput.astro +215 -0
  117. package/src/components/OtpInput.meta.ts +42 -0
  118. package/src/components/PageTransition.astro +121 -0
  119. package/src/components/PageTransition.meta.ts +33 -0
  120. package/src/components/Parallax.astro +69 -0
  121. package/src/components/Parallax.meta.ts +32 -0
  122. package/src/components/Particles.astro +210 -0
  123. package/src/components/Particles.meta.ts +37 -0
  124. package/src/components/PersistPlayer.astro +126 -0
  125. package/src/components/PersistPlayer.meta.ts +35 -0
  126. package/src/components/PhoneFrame.astro +161 -0
  127. package/src/components/PhoneFrame.meta.ts +35 -0
  128. package/src/components/PillNav.astro +170 -0
  129. package/src/components/PillNav.meta.ts +37 -0
  130. package/src/components/Popover.astro +95 -0
  131. package/src/components/Popover.meta.ts +34 -0
  132. package/src/components/PriceSwitch.astro +251 -0
  133. package/src/components/PriceSwitch.meta.ts +41 -0
  134. package/src/components/Probe.astro +84 -0
  135. package/src/components/Progress.astro +165 -0
  136. package/src/components/Progress.meta.ts +40 -0
  137. package/src/components/ProgressiveBlur.astro +95 -0
  138. package/src/components/ProgressiveBlur.meta.ts +36 -0
  139. package/src/components/PromptInput.astro +273 -0
  140. package/src/components/PromptInput.meta.ts +46 -0
  141. package/src/components/ReadingToc.astro +153 -0
  142. package/src/components/ReadingToc.meta.ts +34 -0
  143. package/src/components/RetroGrid.astro +84 -0
  144. package/src/components/RetroGrid.meta.ts +36 -0
  145. package/src/components/Reveal.astro +292 -0
  146. package/src/components/Reveal.meta.ts +48 -0
  147. package/src/components/Ripple.astro +94 -0
  148. package/src/components/Ripple.meta.ts +34 -0
  149. package/src/components/RotatingText.astro +170 -0
  150. package/src/components/RotatingText.meta.ts +35 -0
  151. package/src/components/ScrollExpand.astro +90 -0
  152. package/src/components/ScrollExpand.meta.ts +35 -0
  153. package/src/components/ScrollProgress.astro +73 -0
  154. package/src/components/ScrollProgress.meta.ts +32 -0
  155. package/src/components/ScrollScrub.astro +96 -0
  156. package/src/components/ScrollScrub.meta.ts +37 -0
  157. package/src/components/ShimmerButton.astro +109 -0
  158. package/src/components/ShimmerButton.meta.ts +37 -0
  159. package/src/components/ShinyText.astro +72 -0
  160. package/src/components/ShinyText.meta.ts +35 -0
  161. package/src/components/Skeleton.astro +76 -0
  162. package/src/components/Skeleton.meta.ts +32 -0
  163. package/src/components/Sparkline.astro +168 -0
  164. package/src/components/Sparkline.meta.ts +43 -0
  165. package/src/components/Spinner.astro +79 -0
  166. package/src/components/Spinner.meta.ts +33 -0
  167. package/src/components/SplitFlap.astro +157 -0
  168. package/src/components/SplitFlap.meta.ts +39 -0
  169. package/src/components/SplitText.astro +208 -0
  170. package/src/components/SplitText.meta.ts +50 -0
  171. package/src/components/Spotlight.astro +92 -0
  172. package/src/components/Spotlight.meta.ts +34 -0
  173. package/src/components/StaggerMenu.astro +301 -0
  174. package/src/components/StaggerMenu.meta.ts +39 -0
  175. package/src/components/StatefulButton.astro +148 -0
  176. package/src/components/StatefulButton.meta.ts +35 -0
  177. package/src/components/StickyBanner.astro +168 -0
  178. package/src/components/StickyBanner.meta.ts +45 -0
  179. package/src/components/StickyReveal.astro +256 -0
  180. package/src/components/StickyReveal.meta.ts +33 -0
  181. package/src/components/StrokeDraw.astro +110 -0
  182. package/src/components/StrokeDraw.meta.ts +43 -0
  183. package/src/components/Switch.astro +165 -0
  184. package/src/components/Switch.meta.ts +37 -0
  185. package/src/components/Tabs.astro +194 -0
  186. package/src/components/Tabs.meta.ts +47 -0
  187. package/src/components/TextRoll.astro +78 -0
  188. package/src/components/TextRoll.meta.ts +36 -0
  189. package/src/components/TextScramble.astro +107 -0
  190. package/src/components/TextScramble.meta.ts +35 -0
  191. package/src/components/ThemeScript.astro +17 -0
  192. package/src/components/ThemeToggle.astro +182 -0
  193. package/src/components/ThemeToggle.meta.ts +34 -0
  194. package/src/components/Tilt.astro +97 -0
  195. package/src/components/Tilt.meta.ts +34 -0
  196. package/src/components/Toast.astro +184 -0
  197. package/src/components/Toast.meta.ts +33 -0
  198. package/src/components/TracingBeam.astro +113 -0
  199. package/src/components/TracingBeam.meta.ts +34 -0
  200. package/src/components/Typewriter.astro +124 -0
  201. package/src/components/Typewriter.meta.ts +39 -0
  202. package/src/components/UnderlineHover.astro +61 -0
  203. package/src/components/UnderlineHover.meta.ts +34 -0
  204. package/src/components/VanishInput.astro +240 -0
  205. package/src/components/VanishInput.meta.ts +41 -0
  206. package/src/components/VideoDialog.astro +230 -0
  207. package/src/components/VideoDialog.meta.ts +40 -0
  208. package/src/components/WeightWave.astro +70 -0
  209. package/src/components/WeightWave.meta.ts +36 -0
  210. package/src/components/internal/DialogMorph.astro +97 -0
  211. package/src/components/internal/PromptInputSubmitOnEnter.astro +32 -0
  212. package/src/index.d.ts +43 -0
  213. package/src/index.js +60 -0
  214. package/src/lib/canvas.ts +124 -0
  215. package/src/lib/confetti.ts +127 -0
  216. package/src/lib/contrast.ts +116 -0
  217. package/src/lib/form.ts +231 -0
  218. package/src/lib/geo.ts +77 -0
  219. package/src/lib/gsap.ts +58 -0
  220. package/src/lib/land.ts +11 -0
  221. package/src/lib/lenis.ts +112 -0
  222. package/src/lib/meta.ts +79 -0
  223. package/src/lib/morph.ts +74 -0
  224. package/src/lib/reduced-motion-plugin.js +299 -0
  225. package/src/lib/runtime.ts +215 -0
  226. package/src/lib/support.ts +102 -0
  227. package/src/lib/theme.ts +18 -0
  228. package/src/lib/toast.ts +17 -0
  229. package/src/styles/base.css +218 -0
  230. package/src/styles/motion.css +43 -0
  231. package/src/styles/theme.css +397 -0
  232. package/src/styles/theme.tailwind.css +75 -0
@@ -0,0 +1,363 @@
1
+ ---
2
+ /**
3
+ * GlyphField — a field of braille dots that reads as a flowing contour map.
4
+ * Two waves cross it, the colour turns along a sunset arc, and the cursor
5
+ * carves a trail that decays one dot at a time. No canvas, no frame loop.
6
+ *
7
+ * Every cell is an element whose glyph is a CSS counter on a registered
8
+ * integer: change the integer, change the character. Because integers step,
9
+ * every transition looks like ASCII being rewritten, never a fade. The base
10
+ * pattern is value noise computed on the server (two octaves, seeded), so
11
+ * the contours are already in the HTML. The waves are one animation per
12
+ * row stepping an inherited integer; each cell derives its own crest with
13
+ * mod() and max() (an engine without mod() keeps the contours still). The
14
+ * trail is :hover released on a slow integer
15
+ * transition. `swish` inlines one pointer listener (measured, printed) that
16
+ * keeps the last three cells the cursor crossed and writes a halo around
17
+ * each — head widest, tail narrower — as one integer per touched cell,
18
+ * diffed, so a crossing restyles ~150 elements and never the field.
19
+ *
20
+ * Measured on the site's 48 × 20 hero (Chromium, M-series): the waves cost
21
+ * ~1.5 ms of style work per frame while the field is on screen and nothing
22
+ * once it is scrolled past (content-visibility: auto); a cursor crossing
23
+ * costs ~2 ms. Writing the cursor on the root and deriving the halo in
24
+ * calc() was tried first: every crossing then recalculated all 960 cells,
25
+ * ~7 ms — that is why the listener writes cells.
26
+ *
27
+ * The cost is HTML, not script: cols × rows elements at ~40 B each
28
+ * (48 × 20: 38.8 kB raw, 1.3 kB gzipped, measured 2026-09-28). Fills its
29
+ * positioned parent; put content above it.
30
+ */
31
+ import type { HTMLAttributes } from 'astro/types';
32
+
33
+ interface Props extends HTMLAttributes<'div'> {
34
+ /** Columns, max 96. */
35
+ cols?: number;
36
+ /** Rows, max 64. */
37
+ rows?: number;
38
+ /** Seed for the server-side noise; the same seed always draws the same contours. */
39
+ seed?: number;
40
+ /** Base hue in degrees (oklch) at the top-left cell. */
41
+ hue?: number;
42
+ /** Degrees the hue turns per cell along the diagonal; 3 sweeps ~200° across 48 × 20. Negative turns the other way. */
43
+ spread?: number;
44
+ /** Chroma of the glyphs, 0–0.3. Low reads as ink, high as neon. */
45
+ chroma?: number;
46
+ /** Seconds per wave cycle; the second wave takes 2.5× as long. */
47
+ speed?: number;
48
+ /** Wave height in glyph steps, 0–4. */
49
+ amp?: number;
50
+ /** Milliseconds a hovered cell takes to decay back, one glyph per step. */
51
+ fade?: number;
52
+ /** Swish radius in cells around the cursor. */
53
+ reach?: number;
54
+ /** Glyph set: braille (one to eight dots), ascii (· - = + / \ | #) or blocks. */
55
+ glyphs?: 'braille' | 'ascii' | 'blocks';
56
+ /** Run the two waves. */
57
+ wave?: boolean;
58
+ /** Soft colour bloom behind the glyphs. */
59
+ glow?: boolean;
60
+ /** Inline one pointermove listener so the field warps in two dimensions around the cursor, with a tail. */
61
+ swish?: boolean;
62
+ }
63
+
64
+ const {
65
+ cols = 48,
66
+ rows = 20,
67
+ seed = 7,
68
+ hue,
69
+ spread,
70
+ chroma,
71
+ speed,
72
+ amp,
73
+ fade,
74
+ reach,
75
+ glyphs = 'braille',
76
+ wave = true,
77
+ glow = true,
78
+ swish = false,
79
+ class: className,
80
+ style,
81
+ ...rest
82
+ } = Astro.props;
83
+
84
+ const C = Math.max(1, Math.min(Math.round(cols), 96));
85
+ const R = Math.max(1, Math.min(Math.round(rows), 64));
86
+
87
+ // Value noise, two octaves, seeded (mulberry32). Deterministic per seed, so the
88
+ // server HTML is stable across builds and the fixture is reproducible.
89
+ function mulberry32(a: number) {
90
+ return () => {
91
+ a |= 0;
92
+ a = (a + 0x6d2b79f5) | 0;
93
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
94
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
95
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
96
+ };
97
+ }
98
+ const rand = mulberry32(Math.floor(seed) || 1);
99
+ const lattice = (period: number) => Array.from({ length: Math.ceil(R / period) + 2 }, () => Array.from({ length: Math.ceil(C / period) + 2 }, rand));
100
+ const smooth = (t: number) => t * t * (3 - 2 * t);
101
+ const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
102
+ const sample = (lat: number[][], period: number, x: number, y: number) => {
103
+ const gx = x / period;
104
+ const gy = y / period;
105
+ const x0 = Math.floor(gx);
106
+ const y0 = Math.floor(gy);
107
+ const tx = smooth(gx - x0);
108
+ const ty = smooth(gy - y0);
109
+ return lerp(lerp(lat[y0]![x0]!, lat[y0]![x0 + 1]!, tx), lerp(lat[y0 + 1]![x0]!, lat[y0 + 1]![x0 + 1]!, tx), ty);
110
+ };
111
+ const l1 = lattice(7);
112
+ const l2 = lattice(3);
113
+ // 1..5 with contrast pulled up so the field is mostly quiet with bands of denser glyphs; the waves and the cursor add up to 8.
114
+ const cells = Array.from({ length: R }, (_, y) =>
115
+ Array.from({ length: C }, (_, x) => {
116
+ const n = 0.7 * sample(l1, 7, x, y) + 0.3 * sample(l2, 3, x, y);
117
+ const c = Math.min(1, Math.max(0, (n - 0.5) * 1.9 + 0.5));
118
+ return 1 + Math.floor(Math.pow(c, 1.5) * 4.99);
119
+ }),
120
+ );
121
+
122
+ const vars = [
123
+ hue !== undefined && `--ma-gf-hue:${hue}deg`,
124
+ spread !== undefined && `--ma-gf-spread:${spread}deg`,
125
+ chroma !== undefined && `--ma-gf-chroma:${chroma}`,
126
+ speed !== undefined && `--ma-gf-speed:${speed}s`,
127
+ amp !== undefined && `--ma-gf-amp:${amp}`,
128
+ fade !== undefined && `--ma-gf-fade:${fade}ms`,
129
+ reach !== undefined && `--ma-gf-reach:${reach}`,
130
+ typeof style === 'string' ? style : '',
131
+ ]
132
+ .filter(Boolean)
133
+ .join(';');
134
+
135
+ // The swish listener, inlined only when asked for so the default stays at 0 B.
136
+ // Self-contained: binds now and after every ClientRouter swap, once per field.
137
+ // Keeps the last three cells the cursor crossed; once per frame it writes the
138
+ // halo around each (reach, reach − 2, reach − 3 wide, Chebyshev falloff) as an
139
+ // inline --ma-gf-s on the touched cells, diffed against the previous frame so
140
+ // only cells whose value changed are written. After 140 ms of stillness the
141
+ // tail collapses one cell at a time; leaving the field clears it. Reduced
142
+ // motion (the query or the page's data-ma-motion switch) is read on every
143
+ // move, and Astro's page events fire on document, where the rebind listens.
144
+ // Every copy of the script calls the binder, so a second field on the first
145
+ // page is bound too.
146
+ const SWISH = `(window.__maGf||(window.__maGf=function(){document.querySelectorAll('.ma-gf[data-swish]').forEach(function(f){if(f.__ma)return;f.__ma=1;var rows=[].map.call(f.children,function(b){return b.children}),R=rows.length,C=rows[0].length,cur={},q=[],t,raf,rm=matchMedia('(prefers-reduced-motion: reduce)'),w=function(){var n={},k,r=+f.style.getPropertyValue('--ma-gf-reach')||5;q.forEach(function(p,i){var a=r-[0,2,3][i];for(var y=p[1]-a;y<=p[1]+a;y++)if(y>=0&&y<R)for(var x=p[0]-a;x<=p[0]+a;x++)if(x>=0&&x<C){var v=a-Math.max(Math.abs(x-p[0]),Math.abs(y-p[1]));k=y*C+x;if(v>0&&!(n[k]>=v))n[k]=v}});for(k in cur)if(!n[k])rows[k/C|0][k%C].style.removeProperty('--ma-gf-s');for(k in n)if(cur[k]!==n[k])rows[k/C|0][k%C].style.setProperty('--ma-gf-s',n[k]);cur=n},d=function(){if(q.length>1){q.pop();w();t=setTimeout(d,140)}};f.addEventListener('pointermove',function(e){var b=f.getBoundingClientRect(),x=Math.floor((e.clientX-b.left)/b.width*C),y=Math.floor((e.clientY-b.top)/b.height*R),h=q[0];if(h&&h[0]==x&&h[1]==y)return;q.unshift([x,y]);q.length=rm.matches||document.documentElement.dataset.maMotion=='reduce'?1:Math.min(q.length,3);clearTimeout(t);t=setTimeout(d,140);raf||(raf=requestAnimationFrame(function(){raf=0;w()}))},{passive:true});f.addEventListener('pointerleave',function(){clearTimeout(t);q=[];w()})})},document.addEventListener('astro:page-load',window.__maGf),document.addEventListener('astro:after-swap',window.__maGf),window.__maGf))()`;
147
+ ---
148
+
149
+ <div
150
+ class:list={['ma-gf', className]}
151
+ aria-hidden="true"
152
+ data-ma-gf
153
+ data-wave={wave ? '' : undefined}
154
+ data-glow={glow ? '' : undefined}
155
+ data-swish={swish ? '' : undefined}
156
+ data-glyphs={glyphs !== 'braille' ? glyphs : undefined}
157
+ style={vars || undefined}
158
+ {...rest}
159
+ >
160
+ {cells.map((row, y) => <b style={`--ma-gf-y:${y}`}>{row.map((g, x) => <i style={`--ma-gf-x:${x};--ma-gf-g:${g}`}></i>)}</b>)}
161
+ </div>
162
+ {swish && <script is:inline set:html={SWISH} />}
163
+
164
+ <style is:global>
165
+ /* wave phases, one per row, inherited by the cells */
166
+ @property --ma-gf-p {
167
+ syntax: '<integer>';
168
+ inherits: true;
169
+ initial-value: 0;
170
+ }
171
+ @property --ma-gf-q {
172
+ syntax: '<integer>';
173
+ inherits: true;
174
+ initial-value: 0;
175
+ }
176
+ /* per cell: hover lift (transitions), swish lift (written inline by the listener), and the total the counter reads */
177
+ @property --ma-gf-h {
178
+ syntax: '<integer>';
179
+ inherits: false;
180
+ initial-value: 0;
181
+ }
182
+ @property --ma-gf-s {
183
+ syntax: '<integer>';
184
+ inherits: false;
185
+ initial-value: 0;
186
+ }
187
+ @property --ma-gf-v {
188
+ syntax: '<integer>';
189
+ inherits: false;
190
+ initial-value: 1;
191
+ }
192
+
193
+ @layer components {
194
+ /* eight densities; the counter value 1–8 picks one. Braille is the default: one dot to eight, every font has them, and a dot reads as texture where a slash reads as noise */
195
+ @counter-style ma-glyphs-braille {
196
+ system: cyclic;
197
+ symbols: '⠁' '⠃' '⠇' '⠏' '⠟' '⠿' '⡿' '⣿';
198
+ }
199
+ @counter-style ma-glyphs-ascii {
200
+ system: cyclic;
201
+ symbols: '·' '-' '=' '+' '/' '\\' '|' '#';
202
+ }
203
+ @counter-style ma-glyphs-blocks {
204
+ system: cyclic;
205
+ symbols: '·' '░' '░' '▒' '▒' '▓' '▓' '█';
206
+ }
207
+
208
+ :where(.ma-gf) {
209
+ position: absolute;
210
+ inset: 0;
211
+ display: grid;
212
+ grid-auto-rows: 1fr;
213
+ overflow: hidden;
214
+ contain: strict;
215
+ /* the field is the query container the column rule below reads; each cell is its own, for the glyph size */
216
+ container-type: size;
217
+ /* off screen the cells are skipped entirely — no style, layout or paint — so a field scrolled past costs nothing */
218
+ content-visibility: auto;
219
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
220
+ font-weight: 500;
221
+ /* lightness follows the theme through the base scalar --ma-dark (0 light, 1 dark): dense glyphs go darker on a light ground, brighter on a dark one */
222
+ --ma-gf-l: calc(66% - 16% * var(--ma-dark, 0));
223
+ --ma-gf-lv: calc(-3.2% + 7.6% * var(--ma-dark, 0));
224
+ mask-image: radial-gradient(130% 110% at var(--ma-gf-focus, 50% 50%), #000 30%, transparent 100%);
225
+ }
226
+
227
+ /* bloom: three soft fields on the same hue arc as the glyphs — the second and third sit 40 and 20 cells along the spread — drifting on the compositor behind them */
228
+ :where(.ma-gf[data-glow])::before {
229
+ content: '';
230
+ position: absolute;
231
+ inset: -25%;
232
+ z-index: -1;
233
+ background:
234
+ radial-gradient(38% 42% at 28% 36%, oklch(62% 0.17 var(--ma-gf-hue, 68deg) / 0.42), transparent 70%),
235
+ radial-gradient(42% 46% at 72% 58%, oklch(50% 0.18 calc(var(--ma-gf-hue, 68deg) - var(--ma-gf-spread, 3deg) * 40) / 0.4), transparent 70%),
236
+ radial-gradient(30% 34% at 52% 82%, oklch(56% 0.16 calc(var(--ma-gf-hue, 68deg) - var(--ma-gf-spread, 3deg) * 20) / 0.34), transparent 70%);
237
+ will-change: translate;
238
+ animation: ma-gf-glow calc(var(--ma-gf-speed, 6s) * 4) ease-in-out infinite alternate;
239
+ }
240
+
241
+ :where(.ma-gf > b) {
242
+ display: flex;
243
+ min-height: 0;
244
+ }
245
+ /* the waves: an integer phase stepping 16 (and 24) times a cycle, one animation per row, offset per row so the crests run diagonally; the second wave is slower and shallower so the two never line up */
246
+ :where(.ma-gf[data-wave] > b) {
247
+ animation:
248
+ ma-gf-wave var(--ma-gf-speed, 6s) linear infinite,
249
+ ma-gf-wave-2 calc(var(--ma-gf-speed, 6s) * 2.5) linear infinite;
250
+ animation-delay: calc(var(--ma-gf-y) * var(--ma-gf-speed, 6s) / -16), calc(var(--ma-gf-y) * var(--ma-gf-speed, 6s) * 2.5 / -48);
251
+ }
252
+
253
+ :where(.ma-gf > b > i) {
254
+ flex: 1 1 0;
255
+ min-width: 0;
256
+ display: block;
257
+ contain: strict;
258
+ container-type: size;
259
+ font-style: normal;
260
+ text-align: center;
261
+ /* no crest where mod() is missing (the rule below): the server-drawn contours hold still */
262
+ --ma-gf-w: 0;
263
+ /* --ma-gf-s is the swish lift, written inline per cell by the listener; it transitions both ways so the halo blooms and releases in steps */
264
+ --ma-gf-v: clamp(1, var(--ma-gf-g) + var(--ma-gf-w) + var(--ma-gf-h) + var(--ma-gf-s), 8);
265
+ counter-set: ma-g var(--ma-gf-v); /* counter-set: WebKit repaints siblings' counters only through this (see SplitFlap) */
266
+ /* every cell has its own colour: hue turns along the diagonal, lightness and chroma rise with density */
267
+ color: oklch(
268
+ calc(var(--ma-gf-l) + var(--ma-gf-v) * var(--ma-gf-lv)) calc(var(--ma-gf-chroma, 0.14) + var(--ma-gf-v) * 0.012)
269
+ calc(var(--ma-gf-hue, 68deg) - (var(--ma-gf-x) + var(--ma-gf-y)) * var(--ma-gf-spread, 3deg))
270
+ );
271
+ transition:
272
+ --ma-gf-h var(--ma-gf-fade, 900ms) linear,
273
+ --ma-gf-s 180ms linear;
274
+ }
275
+ /* wave crest at this cell: distance from the phase, folded by mod(), its size max(d, -d) and clipped by max() — integer
276
+ maths, no per-cell animation. Not abs(): that arrived in Chrome 138, mod() in 125. Without mod() the declaration
277
+ would be kept and invalid at computed time, which took --ma-gf-v to its initial 1 and every cell to one dot */
278
+ @supports (z-index: mod(3, 2)) {
279
+ :where(.ma-gf > b > i) {
280
+ --ma-gf-d: calc(mod(var(--ma-gf-x) - var(--ma-gf-p) + 96, 16) - 8);
281
+ --ma-gf-e: calc(mod(var(--ma-gf-x) + var(--ma-gf-q) + 96, 24) - 12);
282
+ --ma-gf-w: calc(max(0, var(--ma-gf-amp, 3) - max(var(--ma-gf-d), -1 * var(--ma-gf-d))) + max(0, var(--ma-gf-amp, 3) - 1 - max(var(--ma-gf-e), -1 * var(--ma-gf-e))));
283
+ }
284
+ }
285
+ :where(.ma-gf > b > i)::before {
286
+ content: counter(ma-g, ma-glyphs-braille);
287
+ display: block;
288
+ font-size: calc(min(100cqw, 100cqh) * 0.82);
289
+ line-height: 100cqh;
290
+ }
291
+ :where(.ma-gf[data-glyphs='ascii'] > b > i)::before {
292
+ content: counter(ma-g, ma-glyphs-ascii);
293
+ }
294
+ :where(.ma-gf[data-glyphs='blocks'] > b > i)::before {
295
+ content: counter(ma-g, ma-glyphs-blocks);
296
+ }
297
+ /* narrow containers (a phone, a small tile) show every third column, so a glyph never drops below what the eye reads as a character */
298
+ @container (max-width: 40rem) {
299
+ :where(.ma-gf > b > i:not(:nth-child(3n + 1))) {
300
+ display: none;
301
+ }
302
+ }
303
+
304
+ /* the trail: lit at once, released one glyph at a time; two neighbours each side so a fast cursor never leaves single marks */
305
+ @media (hover: hover) {
306
+ :where(.ma-gf > b > i:hover) {
307
+ --ma-gf-h: 7;
308
+ transition-duration: 0ms;
309
+ }
310
+ :where(.ma-gf > b > i:hover + i),
311
+ :where(.ma-gf > b > i:has(+ i:hover)) {
312
+ --ma-gf-h: 4;
313
+ transition-duration: 60ms;
314
+ }
315
+ :where(.ma-gf > b > i:hover + i + i),
316
+ :where(.ma-gf > b > i:has(+ i + i:hover)) {
317
+ --ma-gf-h: 2;
318
+ transition-duration: 120ms;
319
+ }
320
+ }
321
+
322
+ @media (prefers-reduced-motion: reduce) {
323
+ :where(.ma-gf[data-wave] > b) {
324
+ animation: none;
325
+ }
326
+ :where(.ma-gf[data-glow])::before {
327
+ animation: none;
328
+ will-change: auto;
329
+ }
330
+ :where(.ma-gf > b > i) {
331
+ transition: none;
332
+ }
333
+ }
334
+ }
335
+
336
+ @keyframes ma-gf-wave {
337
+ from {
338
+ --ma-gf-p: 0;
339
+ }
340
+ to {
341
+ --ma-gf-p: 16;
342
+ }
343
+ }
344
+ @keyframes ma-gf-wave-2 {
345
+ from {
346
+ --ma-gf-q: 0;
347
+ }
348
+ to {
349
+ --ma-gf-q: 24;
350
+ }
351
+ }
352
+ @keyframes ma-gf-glow {
353
+ from {
354
+ translate: -6% -8%;
355
+ }
356
+ 50% {
357
+ translate: 8% 5%;
358
+ }
359
+ to {
360
+ translate: -3% 10%;
361
+ }
362
+ }
363
+ </style>
@@ -0,0 +1,53 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'GlyphField',
5
+ slug: 'glyph-field',
6
+ title: 'Glyph Field',
7
+ description:
8
+ 'A field of braille dots that reads as a flowing contour map: two waves cross it, the colour turns along a sunset arc, and the cursor carves a trail that decays one dot at a time. No canvas and no frame loop: every cell is an element whose glyph is a CSS counter on a registered integer, the contours are noise computed on the server, and the waves are one stepping animation per row. ASCII and block sets are one prop away. Fills its positioned parent.',
9
+ summary: 'A field of braille dots that reads as a flowing contour map, with a cursor trail that fades dot by dot.',
10
+ category: 'pointer',
11
+ tier: 'A',
12
+ trigger: ['pointer', 'always'],
13
+ support: {
14
+ baseline: 'newly',
15
+ needs: ['property', 'css-math', 'counter-style', 'counter-set', 'has'],
16
+ fallback: 'without mod() the waves stop and the field shows the server-drawn contours; without @property the hover lights and releases in one step instead of decaying glyph by glyph',
17
+ },
18
+ readout: '<GlyphField cols={48} rows={20}>',
19
+ usage: `<section class="relative min-h-[70vh]">\n <GlyphField swish />\n <h1 class="relative">Move your cursor. The field is CSS.</h1>\n</section>`,
20
+ props: [
21
+ { name: 'cols', type: 'number', default: '48', description: 'Columns, max 96. The cost is HTML: cols × rows elements at about 40 B raw each, 1.3 kB gzip at 48 × 20.' },
22
+ { name: 'rows', type: 'number', default: '20', description: 'Rows, max 64.' },
23
+ { name: 'seed', type: 'number', default: '7', description: 'Seed for the server-side value noise. The same seed always draws the same contours, so the HTML is stable across builds.' },
24
+ { name: 'hue', type: 'number', default: '68', description: 'Base hue in oklch degrees at the top-left cell. 68 is amber; 250 is indigo.' },
25
+ { name: 'spread', type: 'number', default: '3', description: 'Degrees the hue turns per cell along the diagonal. 3 sweeps amber → rose → violet across 48 × 20; 0 is a single hue; negative turns the other way.' },
26
+ { name: 'chroma', type: 'number', default: '0.14', description: 'Chroma of the glyphs. 0.02 reads as ink, 0.2 as neon. Dense glyphs get a little more.' },
27
+ { name: 'speed', type: 'number', default: '6', description: 'Seconds per wave cycle. The second wave takes 2.5× as long and is one step lower, so the crests never line up.' },
28
+ { name: 'amp', type: 'number', default: '3', description: 'Wave height in glyph steps, 0–4. A cell at rest is 1–5; a crest adds up to this; the cursor takes it to 8 (#).' },
29
+ { name: 'fade', type: 'number', default: '900', description: 'Milliseconds a hovered cell takes to decay back, one glyph per step, from eight dots down to one.' },
30
+ { name: 'reach', type: 'number', default: '5', description: 'Swish radius in cells around the cursor; the tail is one step narrower per older cell.' },
31
+ { name: 'glyphs', type: "'braille' | 'ascii' | 'blocks'", default: "'braille'", description: 'Glyph set, eight symbols in rising density each: braille ⠁…⣿, ascii · - = + / \\ | #, or shade blocks. Any font that has them works because a glyph is centred in its own cell.' },
32
+ { name: 'wave', type: 'boolean', default: 'true', description: 'Run the two waves.' },
33
+ { name: 'glow', type: 'boolean', default: 'true', description: 'Three soft colour fields drift behind the glyphs on the compositor.' },
34
+ { name: 'swish', type: 'boolean', default: 'false', description: 'Inline one pointermove listener so the field lifts in two dimensions around the cursor and a comet tail follows the last three cells it crossed. It writes one integer on each touched cell (about 150 a crossing, diffed) and never on the field. Without it the trail is :hover only: still a trail, one row deep.' },
35
+ ],
36
+ reducedMotion: 'The waves and the bloom stop; the field shows the server-drawn contours. Cells still light under the cursor, because that is direct manipulation, but release in one step, with no tail. With swish, the halo follows and the tail is not recorded; the setting and the page\'s data-ma-motion switch are read on every move.',
37
+ clientRouter: 'CSS-only by default; nothing to rebind. The swish listener binds itself again after every navigation, once per field.',
38
+ craft: [
39
+ 'The glyph is content: counter() on a registered <integer>. Integers interpolate in steps, so a 900 ms transition from 7 to 0 rewrites the character seven times. The decay is the browser stepping through the density ramp, not a fade.',
40
+ 'Braille by default: one dot to eight is a density ramp every system font carries, and dots read as texture where slashes and hashes read as noise. ASCII is there for the terminal look.',
41
+ 'The waves are twenty animations for 960 cells: each row steps an inherited integer phase; each cell folds its own column into that phase with mod(), and takes the distance as max(d, -d) (abs() arrived in Chrome 138, mod() in 125). Style work happens only when a row steps (48 cells at a time, about 1.5 ms a frame on the site hero), and content-visibility: auto makes a field that has scrolled past cost nothing.',
42
+ 'Swish writes each touched cell directly. The first build wrote the cursor cell on the root and let every cell derive its distance in calc(): elegant, and every crossing recalculated all 960 cells, ~7 ms. Writing the halo as one inline integer per touched cell, diffed frame to frame, restyles ~150 and costs ~2 ms.',
43
+ 'The contours are value noise on the server, two octaves with a contrast curve so the field is mostly quiet with bands of denser glyphs. Deterministic per seed: the fixture is reproducible and the HTML is stable.',
44
+ 'Colour is per cell, computed: hue = base − (x + y) × spread, lightness and chroma rise with density. A sunset arc (amber → rose → violet) at moderate chroma reads as expensive; a full rainbow at full chroma reads as a screensaver.',
45
+ 'Lit in 0 ms, released over 900 ms: asymmetry is what reads as a trail. Two neighbours each side at 60 and 120 ms so a fast cursor leaves a continuous stroke.',
46
+ 'The bloom is three radial fields on the same hue arc, translated on the compositor behind the glyphs, and a radial mask fades the field at its edges. That gives depth without a blur filter.',
47
+ 'contain: strict on the field and on every cell: a glyph change lays out one 30 px box. aria-hidden: 960 elements assistive tech never meets.',
48
+ ],
49
+ related: ['fluid-field', 'cursor-grid', 'aurora', 'spotlight'],
50
+ intents: ['braille dot field', 'dot matrix background', 'ascii hero background', 'glyph field', 'ascii art background reacts to cursor', 'motion.dev style ascii hero', 'character field animation', 'terminal texture background', 'contour map background'],
51
+ replaces: ['ASCII canvas hero (motion.dev)', 'ASCIIText / LetterGlitch (React Bits)', 'canvas character fields'],
52
+ fixture: { props: { cols: 8, rows: 4 }, expect: 'class="ma-gf"' },
53
+ } satisfies ComponentMeta;
@@ -0,0 +1,63 @@
1
+ ---
2
+ /**
3
+ * GradientText — a gradient fill that drifts through the text, zero JS.
4
+ *
5
+ * The gradient is three times wider than the text and only its position
6
+ * moves: no layout, though background-position is not composited, so each
7
+ * frame repaints the text's own box. The last stop repeats the first so the
8
+ * loop has no seam.
9
+ */
10
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
11
+
12
+ interface Props extends HTMLAttributes<'span'> {
13
+ /** Element to render. */
14
+ as?: HTMLTag;
15
+ /** Colours, 2–5, any CSS colour. */
16
+ colors?: string[];
17
+ /** Seconds per full drift. */
18
+ duration?: number;
19
+ /** Gradient angle in degrees. */
20
+ angle?: number;
21
+ }
22
+
23
+ const { as: Tag = 'span', colors = ['oklch(70% 0.19 300)', 'oklch(78% 0.15 200)', 'oklch(75% 0.17 350)'], duration = 6, angle = 90, class: className, style, ...rest } = Astro.props;
24
+ const stops = [...colors.slice(0, 5), colors[0]].join(', ');
25
+ // the caller's style joins the component's instead of replacing it: a style prop used to drop the stops, and the text,
26
+ // transparent over no gradient, disappeared
27
+ const vars = [`--ma-gt-stops:${stops}`, `--ma-gt-duration:${duration}s`, `--ma-gt-angle:${angle}deg`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
28
+ ---
29
+
30
+ <Tag class:list={['ma-gradient-text', className]} style={vars} {...rest}><slot /></Tag>
31
+
32
+ <style is:global>
33
+ @layer components {
34
+ :where(.ma-gradient-text) {
35
+ background-image: linear-gradient(var(--ma-gt-angle, 90deg), var(--ma-gt-stops));
36
+ background-size: 300% 100%;
37
+ -webkit-background-clip: text;
38
+ background-clip: text;
39
+ color: transparent;
40
+ animation: ma-gradient-text var(--ma-gt-duration, 6s) linear infinite;
41
+ }
42
+ @media (prefers-reduced-motion: reduce) {
43
+ :where(.ma-gradient-text) {
44
+ animation: none;
45
+ }
46
+ }
47
+ /* Print drops backgrounds by default, and with them the only paint the transparent text had: it prints in the
48
+ colour around it instead. */
49
+ @media print {
50
+ :where(.ma-gradient-text) {
51
+ color: inherit;
52
+ background: none;
53
+ animation: none;
54
+ }
55
+ }
56
+ }
57
+
58
+ @keyframes ma-gradient-text {
59
+ to {
60
+ background-position: -300% 0;
61
+ }
62
+ }
63
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'GradientText',
5
+ slug: 'gradient-text',
6
+ title: 'Gradient Text',
7
+ description: 'A gradient fill drifting slowly through the text with no seam at the loop point. Only the background position moves. Pure CSS; works on any inline or block text.',
8
+ summary: 'Gradient text whose colours drift slowly through the letters, with no visible seam where the loop restarts.',
9
+ category: 'text',
10
+ tier: 'A',
11
+ trigger: ['always'],
12
+ support: { baseline: 'widely', needs: ['bg-clip-text'] },
13
+ readout: '<GradientText duration={6}>',
14
+ usage: `<h1><GradientText>Motion for Astro.</GradientText></h1>\n<GradientText colors={['#f59e0b', '#ec4899']} angle={45} duration={4}>two colours</GradientText>`,
15
+ props: [
16
+ { name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
17
+ { name: 'colors', type: 'string[]', default: 'violet, cyan, rose (oklch)', description: '2–5 colours. The first is repeated at the end so the loop has no visible seam.' },
18
+ { name: 'duration', type: 'number', default: '6', description: 'Seconds per full drift. Under 3 flickers.' },
19
+ { name: 'angle', type: 'number', default: '90', description: 'Gradient angle in degrees.' },
20
+ ],
21
+ reducedMotion: 'The gradient stays put.',
22
+ clientRouter: 'CSS-only; nothing to rebind.',
23
+ craft: [
24
+ 'The first stop is appended as the last so -300% lands exactly where 0% began: no visible seam.',
25
+ 'Linear timing: continuous motion must not ease.',
26
+ 'Gradient text loses currentColor; keep it for headlines, not body copy.',
27
+ 'Only background-position moves: no layout, but it is not composited, so every frame repaints the text\'s own box.',
28
+ 'Print drops backgrounds, and with them the only paint the transparent text has, so in print the text takes the colour around it.',
29
+ ],
30
+ related: ['shiny-text', 'aurora'],
31
+ intents: ['gradient text', 'animated gradient text', 'rainbow text', 'color shifting headline'],
32
+ replaces: ['GradientText (React Bits)', 'AnimatedGradientText (Magic UI)'],
33
+ // a style prop joins the component's properties (it used to replace them, and the text disappeared)
34
+ fixture: { props: { style: 'letter-spacing:-0.01em' }, expect: '--ma-gt-angle:90deg;letter-spacing:-0.01em' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,55 @@
1
+ ---
2
+ /**
3
+ * GridPattern — a hairline grid behind content, faded at the edges, with an
4
+ * optional slow drift. Zero JS: two repeating gradients and a mask. Fills
5
+ * its positioned parent.
6
+ */
7
+ import type { HTMLAttributes } from 'astro/types';
8
+
9
+ interface Props extends HTMLAttributes<'div'> {
10
+ /** Cell size in px. */
11
+ size?: number;
12
+ /** Line colour. */
13
+ color?: string;
14
+ /** Fade the grid toward the edges with a radial mask. */
15
+ fade?: boolean;
16
+ /** Drift one cell every `drift` seconds; 0 keeps it still. */
17
+ drift?: number;
18
+ }
19
+
20
+ const { size = 32, color = 'var(--ma-edge)', fade = true, drift = 0, class: className, style, ...rest } = Astro.props;
21
+ const vars = [`--ma-gp-size:${size}px`, `--ma-gp-color:${color}`, `--ma-gp-drift:${drift}s`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
22
+ ---
23
+
24
+ <div class:list={['ma-grid-pattern', className]} aria-hidden="true" data-fade={fade ? '' : undefined} data-drift={drift > 0 ? '' : undefined} style={vars} {...rest}></div>
25
+
26
+ <style is:global>
27
+ @layer components {
28
+ :where(.ma-grid-pattern) {
29
+ position: absolute;
30
+ inset: 0;
31
+ pointer-events: none;
32
+ background-image:
33
+ linear-gradient(to right, var(--ma-gp-color, var(--ma-edge)) 1px, transparent 1px),
34
+ linear-gradient(to bottom, var(--ma-gp-color, var(--ma-edge)) 1px, transparent 1px);
35
+ background-size: var(--ma-gp-size, 32px) var(--ma-gp-size, 32px);
36
+ }
37
+ :where(.ma-grid-pattern[data-fade]) {
38
+ mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
39
+ }
40
+ :where(.ma-grid-pattern[data-drift]) {
41
+ animation: ma-grid-pattern var(--ma-gp-drift, 8s) linear infinite;
42
+ }
43
+ @media (prefers-reduced-motion: reduce) {
44
+ :where(.ma-grid-pattern) {
45
+ animation: none;
46
+ }
47
+ }
48
+ }
49
+
50
+ @keyframes ma-grid-pattern {
51
+ to {
52
+ background-position: var(--ma-gp-size, 32px) var(--ma-gp-size, 32px);
53
+ }
54
+ }
55
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'GridPattern',
5
+ slug: 'grid-pattern',
6
+ title: 'Grid Pattern',
7
+ description: 'A hairline grid behind content, faded toward the edges, optionally drifting one cell at a time. Two repeating gradients and a mask; pure CSS. Fills its positioned parent.',
8
+ summary: 'A hairline grid background that fades toward the edges and can drift one cell at a time.',
9
+ category: 'background',
10
+ tier: 'A',
11
+ trigger: ['always'],
12
+ support: { baseline: 'widely', needs: ['mask-image'] },
13
+ readout: '<GridPattern size={32}>',
14
+ usage: `<section class="relative">\n <GridPattern size={40} drift={12} />\n <h2 class="relative">Above the grid</h2>\n</section>`,
15
+ props: [
16
+ { name: 'size', type: 'number', default: '32', description: 'Cell size in px.' },
17
+ { name: 'color', type: 'string', default: 'var(--ma-edge)', description: 'Line colour; the token is theme-aware.' },
18
+ { name: 'fade', type: 'boolean', default: 'true', description: 'Radial mask so the grid dissolves toward the edges.' },
19
+ { name: 'drift', type: 'number', default: '0', description: 'Seconds per one-cell drift; 0 keeps it still. Linear, so the loop has no visible seam.' },
20
+ ],
21
+ reducedMotion: 'The grid holds still.',
22
+ clientRouter: 'CSS-only; nothing to rebind.',
23
+ craft: [
24
+ 'Drifting exactly one cell per loop makes the loop point invisible.',
25
+ 'The mask is on the pattern, not the parent, so content above keeps full contrast.',
26
+ 'background-position moves the tiled texture: no layout, but the box repaints every frame.',
27
+ ],
28
+ related: ['dot-pattern', 'cursor-grid', 'noise'],
29
+ intents: ['grid background', 'grid pattern background', 'blueprint grid', 'faded grid lines background'],
30
+ replaces: ['GridPattern (Magic UI)', 'GridBackground (Aceternity)'],
31
+ fixture: { expect: 'class="ma-grid-pattern"' },
32
+ } satisfies ComponentMeta;