@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,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'RotatingText',
5
+ slug: 'rotating-text',
6
+ title: 'Rotating Text',
7
+ description:
8
+ 'Cycles through a list of words in place. Each word rises in, holds, and leaves upward, and the box is sized to the widest word so the surrounding text never shifts. Pure CSS.',
9
+ summary: 'Rotating words in a sentence: each word rises in, holds and leaves, in a box sized so nothing around it shifts.',
10
+ category: 'text',
11
+ tier: 'A',
12
+ trigger: ['always'],
13
+ support: { baseline: 'newly', needs: ['property'], fallback: 'words crossfade in place without the vertical travel' },
14
+ readout: '<RotatingText period={1600}>',
15
+ usage: `<p>Build <RotatingText words={['faster', 'lighter', 'calmer']} /> sites.</p>\n<p>Ship <RotatingText words={['once', 'everywhere', 'today']} fluid />. The sentence follows.</p>`,
16
+ props: [
17
+ { name: 'words', type: 'string[]', description: 'Words to cycle through, 2–8. A single word stays in place.' },
18
+ { name: 'as', type: 'HTMLTag', default: "'span'", description: 'Element to render.' },
19
+ { name: 'period', type: 'number', default: '2200', description: 'Time each word is shown in ms, including its entrance. Under 1500 reads as nervous.' },
20
+ { name: 'fluid', type: 'boolean', default: 'false', description: 'The box follows the current word\'s width, so the text around it shifts smoothly as words change instead of being sized to the widest word.' },
21
+ ],
22
+ reducedMotion: 'Words crossfade in place; the vertical travel is removed.',
23
+ clientRouter: 'CSS-only; fresh elements restart their own animation after every navigation.',
24
+ craft: [
25
+ 'inline-grid with every word in one cell sizes the box to the widest word, so the sentence around it never reflows.',
26
+ 'Entrance is ease-out, exit is ease-in, hold is linear: the same asymmetry as any enter/exit pair.',
27
+ 'The travel lives in a registered custom property so reduced motion neutralises it with a single declaration.',
28
+ 'A visually hidden span (.ma-sr) lists every word once; the animated spans are aria-hidden. Not aria-label: RotatingText usually sits inside a sentence as a span, where a label is not allowed and is not read.',
29
+ 'fluid is a second animation on max-width, not a measurement: each word contributes 0 while it is away and its own width while it is the word, so the grid cell (the widest contribution) travels from one width to the next. The growth bound is 0.75 em per character, an upper limit that only sets how fast the width reaches the word\'s real size.',
30
+ ],
31
+ related: ['typewriter', 'split-text'],
32
+ intents: ['rotating words', 'word cycle animation', 'flip words', 'animated headline words', 'text loop astro'],
33
+ replaces: ['RotatingText (React Bits)', 'FlipWords (Aceternity)', 'WordRotate (Magic UI)'],
34
+ fixture: { props: { words: ['Fixture content for RotatingText', 'second', 'third'] }, expect: 'Fixture content for RotatingText' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,90 @@
1
+ ---
2
+ /**
3
+ * ScrollExpand — an image that starts narrow and rounded and opens to the
4
+ * full width as it scrolls into view, zero JS. Nothing about the layout
5
+ * changes: the element is full width from the start and a clip-path inset
6
+ * (with its rounding) shrinks it, animated on the element's own view()
7
+ * timeline; the content underneath scales up a touch for depth. Without
8
+ * scroll timelines, and under reduced motion, it is simply full width.
9
+ */
10
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
11
+
12
+ interface Props extends HTMLAttributes<'div'> {
13
+ as?: HTMLTag;
14
+ /** Starting width as a fraction of the full width, 0.2–1. */
15
+ from?: number;
16
+ /** Starting corner radius, any CSS length; it eases to `radiusEnd`. */
17
+ radius?: string;
18
+ radiusEnd?: string;
19
+ /** Starting scale of the content inside. */
20
+ scale?: number;
21
+ /** animation-range on the element's view timeline. */
22
+ range?: string;
23
+ }
24
+
25
+ const { as: Tag = 'div', from = 0.6, radius = '2rem', radiusEnd = '0px', scale = 0.92, range = 'entry 0% cover 45%', class: className, style, ...rest } = Astro.props;
26
+ const inset = ((1 - Math.max(0.2, Math.min(from, 1))) / 2) * 100;
27
+ const vars = [`--ma-expand-inset:${inset}%`, `--ma-expand-r:${radius}`, `--ma-expand-r-end:${radiusEnd}`, `--ma-expand-scale:${scale}`, `--ma-expand-range:${range}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
28
+ ---
29
+
30
+ <Tag class:list={['ma-expand', className]} style={vars} {...rest}>
31
+ <div class="ma-expand__inner"><slot /></div>
32
+ </Tag>
33
+
34
+ <style is:global>
35
+ @layer components {
36
+ :where(.ma-expand) {
37
+ display: block;
38
+ overflow: clip;
39
+ }
40
+ :where(.ma-expand__inner) {
41
+ display: block;
42
+ transform-origin: 50% 50%;
43
+ }
44
+ :where(.ma-expand__inner > img),
45
+ :where(.ma-expand__inner > picture > img),
46
+ :where(.ma-expand__inner > video) {
47
+ display: block;
48
+ width: 100%;
49
+ height: auto;
50
+ }
51
+ @supports (animation-timeline: view()) {
52
+ :where(.ma-expand) {
53
+ animation: ma-expand linear both;
54
+ animation-range: var(--ma-expand-range, entry 0% cover 45%);
55
+ }
56
+ :where(.ma-expand__inner) {
57
+ animation: ma-expand-inner linear both;
58
+ animation-range: var(--ma-expand-range, entry 0% cover 45%);
59
+ }
60
+ /* the timeline in its own rule: a minifier folds it into the animation shorthand otherwise, and the shorthand cannot carry it */
61
+ :where(html) :where(.ma-expand),
62
+ :where(html) :where(.ma-expand__inner) {
63
+ animation-timeline: view();
64
+ }
65
+ }
66
+ @media (prefers-reduced-motion: reduce) {
67
+ :where(.ma-expand),
68
+ :where(.ma-expand__inner) {
69
+ animation: none;
70
+ }
71
+ }
72
+ }
73
+
74
+ @keyframes ma-expand {
75
+ from {
76
+ clip-path: inset(0 var(--ma-expand-inset, 20%) round var(--ma-expand-r, 2rem));
77
+ }
78
+ to {
79
+ clip-path: inset(0 0 round var(--ma-expand-r-end, 0px));
80
+ }
81
+ }
82
+ @keyframes ma-expand-inner {
83
+ from {
84
+ scale: var(--ma-expand-scale, 0.92);
85
+ }
86
+ to {
87
+ scale: 1;
88
+ }
89
+ }
90
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'ScrollExpand',
5
+ slug: 'scroll-expand',
6
+ title: 'Scroll Expand',
7
+ description: 'An image that starts narrow and rounded and opens to the full width as it scrolls into view. The layout never changes: the element is full width from the start and a clip-path inset with its rounding shrinks it, animated on the element\'s own view() timeline; the content inside scales up a touch for depth. Without scroll timelines it is simply full width. Zero JavaScript.',
8
+ summary: 'An image that opens from narrow and rounded to the full width as it scrolls into view.',
9
+ category: 'scroll',
10
+ tier: 'A',
11
+ trigger: ['scroll'],
12
+ support: { baseline: 'limited', needs: ['scroll-timeline', 'clip-path'], fallback: 'full width, static (Firefox)' },
13
+ readout: '<ScrollExpand from={0.6} radius="2rem">',
14
+ usage: `<ScrollExpand from={0.6} radius="2rem">\n <img src="/case-study.jpg" alt="The studio" width="1600" height="900" />\n</ScrollExpand>`,
15
+ props: [
16
+ { name: 'as', type: 'HTMLTag', default: "'div'", description: 'Element to render.' },
17
+ { name: 'from', type: 'number', default: '0.6', description: 'Starting width as a fraction of the full width, 0.2–1.' },
18
+ { name: 'radius', type: 'string', default: "'2rem'", description: 'Starting corner radius.' },
19
+ { name: 'radiusEnd', type: 'string', default: "'0px'", description: 'Corner radius at full width.' },
20
+ { name: 'scale', type: 'number', default: '0.92', description: 'Starting scale of the content inside.' },
21
+ { name: 'range', type: 'string', default: "'entry 0% cover 45%'", description: 'animation-range on the element\'s view timeline.' },
22
+ ],
23
+ reducedMotion: 'Full width from the start; no clip, no scale.',
24
+ clientRouter: 'CSS-only; nothing to rebind.',
25
+ craft: [
26
+ 'clip-path, not width: a width change reflows the page on every scroll frame; a clip is a compositor property and the neighbours never move.',
27
+ 'The corner radius rides on the same inset (inset(… round r)), so the rounding eases away as the edges reach the viewport: one keyframe, no second animation.',
28
+ 'The inner scale runs the opposite way to the clip (content grows as the frame opens), which reads as a camera pulling back rather than a curtain.',
29
+ 'The default range ends at cover 45 %: full width is reached while the image is still in the lower half of the viewport, so the reader never scrolls past a half-open frame.',
30
+ ],
31
+ related: ['scroll-scrub', 'parallax', 'browser-frame'],
32
+ intents: ['image expands on scroll', 'scroll zoom image full width', 'container scroll expand', 'scroll expand media', 'hero image grows on scroll'],
33
+ replaces: ['ScrollExpandMedia (Motion Primitives)', 'ContainerScroll (Aceternity)', 'scroll zoom hero (Codrops)'],
34
+ fixture: { slot: 'Fixture content for ScrollExpand', expect: 'Fixture content for ScrollExpand' },
35
+ } satisfies ComponentMeta;
@@ -0,0 +1,73 @@
1
+ ---
2
+ /**
3
+ * ScrollProgress — a hairline that fills as the page is read, zero JS.
4
+ *
5
+ * scale-x is bound to the document scroll timeline; nothing listens to
6
+ * scroll. Browsers without scroll timelines do not show the bar at all,
7
+ * which is better than a bar that is always full.
8
+ */
9
+ import type { HTMLAttributes } from 'astro/types';
10
+
11
+ interface Props extends HTMLAttributes<'div'> {
12
+ /** Thickness in px. */
13
+ height?: number;
14
+ /** Bar colour. */
15
+ color?: string;
16
+ /** Edge of the viewport. */
17
+ position?: 'top' | 'bottom';
18
+ /** Which scroller to track: the document, or the nearest scrolling ancestor (then the bar is sticky inside it). */
19
+ scope?: 'root' | 'nearest';
20
+ }
21
+
22
+ const { height = 2, color = 'currentColor', position = 'top', scope = 'root', class: className, ...rest } = Astro.props;
23
+ ---
24
+
25
+ <div class:list={['ma-scroll-progress', className]} aria-hidden="true" data-position={position === 'bottom' ? 'bottom' : undefined} data-scope={scope === 'nearest' ? 'nearest' : undefined} style={`--ma-sp-h:${height}px;--ma-sp-color:${color}`} {...rest}></div>
26
+
27
+ <style is:global>
28
+ @layer components {
29
+ :where(.ma-scroll-progress) {
30
+ display: none;
31
+ position: fixed;
32
+ inset: 0 0 auto;
33
+ z-index: 50;
34
+ height: var(--ma-sp-h, 2px);
35
+ background: var(--ma-sp-color, currentColor);
36
+ transform-origin: 0 50%;
37
+ scale: 0 1;
38
+ pointer-events: none;
39
+ }
40
+ :where(.ma-scroll-progress[data-position='bottom']) {
41
+ inset: auto 0 0;
42
+ }
43
+ :where(.ma-scroll-progress[data-scope='nearest']) {
44
+ position: sticky;
45
+ inset: 0 0 auto;
46
+ margin-bottom: calc(var(--ma-sp-h, 2px) * -1);
47
+ }
48
+ @supports (animation-timeline: scroll()) {
49
+ :where(.ma-scroll-progress) {
50
+ display: block;
51
+ animation: ma-scroll-progress linear both;
52
+ }
53
+ :where(html) :where(.ma-scroll-progress) {
54
+ animation-timeline: scroll(root block);
55
+ }
56
+ :where(html) :where(.ma-scroll-progress[data-scope='nearest']) {
57
+ animation-timeline: scroll(nearest block);
58
+ }
59
+ }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ /* progress is information, not motion: it stays */
62
+ :where(.ma-scroll-progress) {
63
+ transition: none;
64
+ }
65
+ }
66
+ }
67
+
68
+ @keyframes ma-scroll-progress {
69
+ to {
70
+ scale: 1 1;
71
+ }
72
+ }
73
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'ScrollProgress',
5
+ slug: 'scroll-progress',
6
+ title: 'Scroll Progress',
7
+ description: 'A hairline along the top of the viewport that fills as the page is read. Zero JavaScript: its scale is bound to the document\'s scroll timeline, so nothing listens to scroll and it is never a frame behind.',
8
+ summary: 'A reading progress bar along the top of the page, bound to the scroll so it is never a frame behind.',
9
+ category: 'scroll',
10
+ tier: 'A',
11
+ trigger: ['scroll'],
12
+ support: { baseline: 'limited', needs: ['scroll-timeline'], fallback: 'the bar is not rendered (Firefox)' },
13
+ readout: '<ScrollProgress height={2}>',
14
+ usage: `<ScrollProgress color="var(--accent)" />`,
15
+ props: [
16
+ { name: 'height', type: 'number', default: '2', description: 'Thickness in px.' },
17
+ { name: 'color', type: 'string', default: "'currentColor'", description: 'Bar colour.' },
18
+ { name: 'position', type: "'top' | 'bottom'", default: "'top'", description: 'Edge of the viewport.' },
19
+ { name: 'scope', type: "'root' | 'nearest'", default: "'root'", description: 'Track the document, or the nearest scrolling ancestor; with nearest, the bar sticks to the top of that scroller.' },
20
+ ],
21
+ reducedMotion: 'Unchanged: the bar reports position and does not animate on its own.',
22
+ clientRouter: 'CSS-only; the new page\'s bar starts at its own scroll position.',
23
+ craft: [
24
+ 'scale on a transform-origin at the left: one compositor property, no width recalculation.',
25
+ 'Hidden where scroll timelines are missing, because a full bar on load is worse than no bar.',
26
+ 'aria-hidden: the reading position is already in the scrollbar for assistive tech.',
27
+ ],
28
+ related: ['parallax', 'reveal'],
29
+ intents: ['scroll progress bar', 'reading progress indicator', 'page scroll indicator', 'progress bar on scroll astro'],
30
+ replaces: ['ScrollProgress (Magic UI)', 'framer-motion useScroll progress bar'],
31
+ fixture: { expect: 'class="ma-scroll-progress"' },
32
+ } satisfies ComponentMeta;
@@ -0,0 +1,96 @@
1
+ ---
2
+ /**
3
+ * ScrollScrub — bind any transform to scroll position, zero JS.
4
+ *
5
+ * Reveal answers "appear when seen"; this answers "move exactly as I
6
+ * scroll": translate, scale, rotate and opacity go from the given start
7
+ * values to neutral across a range of the element's own view timeline.
8
+ * Without scroll timelines the element shows its end state.
9
+ */
10
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
11
+
12
+ interface Props extends HTMLAttributes<'div'> {
13
+ /** Element to render. */
14
+ as?: HTMLTag;
15
+ /** Start x offset in px. */
16
+ x?: number;
17
+ /** Start y offset in px. */
18
+ y?: number;
19
+ /** Start scale. */
20
+ scale?: number;
21
+ /** Start rotation in degrees. */
22
+ rotate?: number;
23
+ /** Start opacity. */
24
+ opacity?: number;
25
+ /** Start tilt on the X axis in degrees — the "container scroll" look. Adds perspective to the element itself. */
26
+ rotateX?: number;
27
+ /** Perspective in px, used with rotateX. */
28
+ perspective?: number;
29
+ /** animation-range of the element's view timeline. */
30
+ range?: string;
31
+ }
32
+
33
+ const { as: Tag = 'div', x = 0, y = 0, scale = 1, rotate = 0, opacity = 1, rotateX, perspective = 1200, range = 'entry 0% cover 40%', class: className, style, ...rest } = Astro.props;
34
+ const inline = [`--ma-ss-x:${x}px`, `--ma-ss-y:${y}px`, `--ma-ss-scale:${scale}`, `--ma-ss-rotate:${rotate}deg`, `--ma-ss-opacity:${opacity}`, `--ma-ss-range:${range}`, rotateX !== undefined && `--ma-ss-rx:${rotateX}deg`, rotateX !== undefined && `--ma-ss-p:${perspective}px`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
35
+ ---
36
+
37
+ <Tag class:list={['ma-scrub', className]} data-tilt={rotateX !== undefined ? '' : undefined} style={inline} {...rest}>
38
+ <slot />
39
+ </Tag>
40
+
41
+ <style is:global>
42
+ @layer components {
43
+ @supports (animation-timeline: view()) {
44
+ :where(.ma-scrub) {
45
+ will-change: translate, scale, rotate, opacity;
46
+ animation: ma-scrub linear both;
47
+ animation-range: var(--ma-ss-range, entry 0% cover 40%);
48
+ }
49
+ :where(html) :where(.ma-scrub) {
50
+ animation-timeline: view();
51
+ }
52
+ /* rotateX: the 3D pair on the same element — perspective() and rotateX() in one transform, no wrapper */
53
+ :where(.ma-scrub[data-tilt]) {
54
+ animation-name: ma-scrub-tilt;
55
+ will-change: translate, scale, rotate, opacity, transform;
56
+ }
57
+ }
58
+ @media (prefers-reduced-motion: reduce) {
59
+ :where(.ma-scrub) {
60
+ animation: none;
61
+ will-change: auto;
62
+ }
63
+ }
64
+ }
65
+
66
+ @keyframes ma-scrub-tilt {
67
+ from {
68
+ translate: var(--ma-ss-x, 0px) var(--ma-ss-y, 0px);
69
+ scale: var(--ma-ss-scale, 1);
70
+ rotate: var(--ma-ss-rotate, 0deg);
71
+ opacity: var(--ma-ss-opacity, 1);
72
+ transform: perspective(var(--ma-ss-p, 1200px)) rotateX(var(--ma-ss-rx, 0deg));
73
+ }
74
+ to {
75
+ translate: 0 0;
76
+ scale: 1;
77
+ rotate: 0deg;
78
+ opacity: 1;
79
+ transform: perspective(var(--ma-ss-p, 1200px)) rotateX(0deg);
80
+ }
81
+ }
82
+ @keyframes ma-scrub {
83
+ from {
84
+ translate: var(--ma-ss-x, 0px) var(--ma-ss-y, 0px);
85
+ scale: var(--ma-ss-scale, 1);
86
+ rotate: var(--ma-ss-rotate, 0deg);
87
+ opacity: var(--ma-ss-opacity, 1);
88
+ }
89
+ to {
90
+ translate: 0 0;
91
+ scale: 1;
92
+ rotate: 0deg;
93
+ opacity: 1;
94
+ }
95
+ }
96
+ </style>
@@ -0,0 +1,37 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'ScrollScrub',
5
+ slug: 'scroll-scrub',
6
+ title: 'Scroll Scrub',
7
+ description: 'Bind translate, scale, rotate and opacity to the scroll position: the element goes from the start values you give to neutral across a range of its own passage through the viewport, exactly in step. Zero JavaScript.',
8
+ summary: "Scroll-linked animation: bind translate, scale, rotate and opacity to an element's passage through the viewport.",
9
+ category: 'scroll',
10
+ tier: 'A',
11
+ trigger: ['scroll'],
12
+ support: { baseline: 'limited', needs: ['scroll-timeline'], fallback: 'the end state, static (Firefox)' },
13
+ readout: '<ScrollScrub y={80} scale={0.9}>',
14
+ usage: `<ScrollScrub y={80} scale={0.9} opacity={0}>\n <img src="/screenshot.png" alt="" />\n</ScrollScrub>\n<ScrollScrub rotate={-6} range="entry 0% entry 100%">tilted in</ScrollScrub>\n<ScrollScrub rotateX={24} scale={0.94} range="entry 0% cover 55%">\n <BrowserFrame url="https://example.com" src={shot} alt="" />\n</ScrollScrub>`,
15
+ props: [
16
+ { name: 'as', type: 'HTMLTag', default: "'div'", description: 'Element to render.' },
17
+ { name: 'x', type: 'number', default: '0', description: 'Start x offset in px.' },
18
+ { name: 'y', type: 'number', default: '0', description: 'Start y offset in px.' },
19
+ { name: 'scale', type: 'number', default: '1', description: 'Start scale.' },
20
+ { name: 'rotate', type: 'number', default: '0', description: 'Start rotation in degrees.' },
21
+ { name: 'opacity', type: 'number', default: '1', description: 'Start opacity.' },
22
+ { name: 'rotateX', type: 'number', description: 'Start tilt on the X axis in degrees (the "container scroll" look); the element straightens across the range. Perspective is applied on the element itself, no wrapper.' },
23
+ { name: 'perspective', type: 'number', default: '1200', description: 'Perspective in px, with rotateX.' },
24
+ { name: 'range', type: 'string', default: "'entry 0% cover 40%'", description: 'animation-range on the element\'s view timeline: which part of its passage the change spans.' },
25
+ ],
26
+ reducedMotion: 'No scrubbing; the element shows its end state.',
27
+ clientRouter: 'CSS-only; nothing to rebind.',
28
+ craft: [
29
+ 'Default range entry 0% → cover 40%: the change completes while the element is still in the lower half of the viewport, so it is settled by the time the reader reaches it.',
30
+ 'Linear timing; a scrubbed animation with easing feels detached from the hand.',
31
+ 'Reveal is a trigger, this is a binding. Use Reveal for entrances; use ScrollScrub when the motion must track the scroll.',
32
+ ],
33
+ related: ['reveal', 'parallax', 'card-stack'],
34
+ intents: ['scroll linked animation', 'scrub animation with scroll', 'animate on scroll position', 'scroll-driven transform', 'scrolltrigger scrub alternative'],
35
+ replaces: ['GSAP ScrollTrigger scrub: true', 'framer-motion useScroll + useTransform'],
36
+ fixture: { expect: 'class="ma-scrub"' },
37
+ } satisfies ComponentMeta;
@@ -0,0 +1,109 @@
1
+ ---
2
+ /**
3
+ * ShimmerButton — a highlight sweeps across the body of a button on a loop,
4
+ * zero JS. The sheen is one pseudo-element gradient whose background-position
5
+ * animates: no layout, though every frame repaints the button's own box; the
6
+ * label sits above it. Hover lifts the button on the preset's hover travel and adds a
7
+ * ring; :focus-visible draws the same. Renders a <button> or an <a>.
8
+ */
9
+ import type { HTMLAttributes } from 'astro/types';
10
+
11
+ interface Props extends HTMLAttributes<'button'> {
12
+ /** Element to render. */
13
+ as?: 'button' | 'a';
14
+ /** For `as="a"`. */
15
+ href?: string;
16
+ /** Sheen colour. */
17
+ color?: string;
18
+ /** Button background. */
19
+ background?: string;
20
+ /** Label colour. */
21
+ text?: string;
22
+ /** Seconds per sweep, pause included. */
23
+ duration?: number;
24
+ /** Corner radius, any CSS length. */
25
+ radius?: string;
26
+ }
27
+
28
+ const { as: Tag = 'button', href, color = 'var(--ma-shine)', background = 'var(--ma-ink)', text = 'var(--ma-panel)', duration = 3, radius = '999px', class: className, style, ...rest } = Astro.props;
29
+ const vars = [`--ma-shimmer-c:${color}`, `--ma-shimmer-bg:${background}`, `--ma-shimmer-fg:${text}`, `--ma-shimmer-dur:${duration}s`, `--ma-shimmer-r:${radius}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
30
+ ---
31
+
32
+ <Tag class:list={['ma-shimmer', className]} href={Tag === 'a' ? href : undefined} type={Tag === 'button' ? 'button' : undefined} style={vars} {...rest}>
33
+ <slot />
34
+ </Tag>
35
+
36
+ <style is:global>
37
+ @layer components {
38
+ :where(.ma-shimmer) {
39
+ position: relative;
40
+ isolation: isolate;
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ gap: 0.5em;
45
+ padding: 0.7em 1.4em;
46
+ border: 1px solid color-mix(in srgb, var(--ma-shimmer-fg, var(--ma-panel)) 18%, transparent);
47
+ border-radius: var(--ma-shimmer-r, 999px);
48
+ background: var(--ma-shimmer-bg, var(--ma-ink));
49
+ color: var(--ma-shimmer-fg, var(--ma-panel));
50
+ font: inherit;
51
+ font-weight: 500;
52
+ line-height: 1.2;
53
+ text-decoration: none;
54
+ white-space: nowrap;
55
+ cursor: pointer;
56
+ overflow: clip;
57
+ transition:
58
+ translate var(--ma-duration-fast) var(--ma-ease-out),
59
+ box-shadow var(--ma-duration-fast) var(--ma-ease-out);
60
+ }
61
+ :where(.ma-shimmer)::before {
62
+ content: '';
63
+ position: absolute;
64
+ inset: -1px;
65
+ z-index: -1;
66
+ pointer-events: none;
67
+ background: linear-gradient(110deg, transparent 36%, var(--ma-shimmer-c, var(--ma-shine)) 50%, transparent 64%);
68
+ background-size: 250% 100%;
69
+ background-position: 120% 0;
70
+ animation: ma-shimmer var(--ma-shimmer-dur, 3s) var(--ma-ease-in-out) infinite;
71
+ }
72
+ @media (hover: hover) {
73
+ :where(.ma-shimmer:hover) {
74
+ translate: 0 calc(-0.5 * var(--ma-travel-hover));
75
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ma-shimmer-c, var(--ma-shine)) 55%, transparent);
76
+ }
77
+ }
78
+ :where(.ma-shimmer:focus-visible) {
79
+ /* --ma-shimmer-ring: the label colour is the ring by default; a section whose button inverts on its ground sets
80
+ the ground's own text colour here, since a ring in the label colour is the ground's colour (HeroWaitlist, 7G) */
81
+ outline: 2px solid var(--ma-shimmer-ring, currentColor);
82
+ outline-offset: 3px;
83
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ma-shimmer-c, var(--ma-shine)) 55%, transparent);
84
+ }
85
+ :where(.ma-shimmer:active) {
86
+ translate: 0 0;
87
+ scale: 0.98;
88
+ }
89
+ @media (prefers-reduced-motion: reduce) {
90
+ :where(.ma-shimmer)::before {
91
+ animation: none;
92
+ display: none;
93
+ }
94
+ :where(.ma-shimmer:hover) {
95
+ translate: 0 0;
96
+ }
97
+ }
98
+ }
99
+
100
+ @keyframes ma-shimmer {
101
+ 0% {
102
+ background-position: 120% 0;
103
+ }
104
+ 55%,
105
+ 100% {
106
+ background-position: -30% 0;
107
+ }
108
+ }
109
+ </style>
@@ -0,0 +1,37 @@
1
+ import type { ComponentMeta } from '../lib/meta';
2
+
3
+ export default {
4
+ name: 'ShimmerButton',
5
+ slug: 'shimmer-button',
6
+ title: 'Shimmer Button',
7
+ description: 'A highlight sweeps across the body of a button on a loop; hover lifts it on the preset\'s hover travel and adds a ring, and focus draws the same. One pseudo-element gradient whose position animates: no layout, though every frame repaints the button\'s own box. Renders a <button> or an <a>. Zero JavaScript.',
8
+ summary: 'A button with a shimmer that sweeps across it on a loop. It lifts on hover and draws a ring on focus.',
9
+ category: 'ui',
10
+ tier: 'A',
11
+ trigger: ['always', 'hover'],
12
+ support: { baseline: 'widely', needs: [] },
13
+ readout: '<ShimmerButton duration={3}>',
14
+ usage: `<ShimmerButton>Get started</ShimmerButton>\n<ShimmerButton as="a" href="/pricing/" color="oklch(90% 0.12 80)" background="oklch(45% 0.15 55)" text="#fff">See pricing →</ShimmerButton>`,
15
+ props: [
16
+ { name: 'as', type: "'button' | 'a'", default: "'button'", description: 'Element to render.' },
17
+ { name: 'href', type: 'string', description: 'For as="a".' },
18
+ { name: 'color', type: 'string', default: 'var(--ma-shine)', description: 'Sheen colour; the token is theme-aware.' },
19
+ { name: 'background', type: 'string', default: 'var(--ma-ink)', description: 'Button background.' },
20
+ { name: 'text', type: 'string', default: 'var(--ma-panel)', description: 'Label colour.' },
21
+ { name: 'duration', type: 'number', default: '3', description: 'Seconds per sweep, pause included.' },
22
+ { name: 'radius', type: 'string', default: "'999px'", description: 'Corner radius.' },
23
+ { name: '--ma-shimmer-ring', type: 'CSS colour (custom property)', default: 'currentColor', description: 'The focus ring\'s colour, the label colour by default. Set it in a rule or style on the button or any ancestor when the button inverts on its ground. The label colour is then the ground\'s colour and the ring vanishes; the ground\'s own text colour is the ring that reads.' },
24
+ ],
25
+ reducedMotion: 'The sheen is removed; a plain button that still lifts nothing on hover.',
26
+ clientRouter: 'CSS-only; nothing to rebind.',
27
+ craft: [
28
+ 'The sheen runs 55 % of the period and rests the other 45 %: a loop that never pauses reads as a spinner.',
29
+ 'The gradient is a pseudo-element behind the label (z-index −1 inside an isolated stacking context), so the text is never tinted.',
30
+ 'Hover lifts by half the preset\'s hover travel: a button is heavier than a link.',
31
+ 'Ring on hover and focus-visible come from the same declaration, so keyboard users see exactly what pointer users see.',
32
+ ],
33
+ related: ['arrow-button', 'shiny-text', 'border-beam'],
34
+ intents: ['shimmer button', 'shiny button', 'animated cta button', 'button with light sweep', 'glowing button astro'],
35
+ replaces: ['ShimmerButton (Magic UI)', 'ShinyButton (Magic UI)', 'HoverBorderGradient (Aceternity)'],
36
+ fixture: { slot: 'Fixture content for ShimmerButton', expect: 'Fixture content for ShimmerButton' },
37
+ } satisfies ComponentMeta;
@@ -0,0 +1,72 @@
1
+ ---
2
+ /**
3
+ * ShinyText — a highlight sweeps across the text on a loop, zero JS.
4
+ *
5
+ * The text is painted once in its own colour; a pseudo-element repeats it
6
+ * with a moving gradient clipped to the glyphs on top. Only background-
7
+ * position animates: no layout, but it is not composited either, so each
8
+ * frame repaints the text's own box.
9
+ */
10
+ import type { HTMLAttributes, HTMLTag } from 'astro/types';
11
+
12
+ interface Props extends HTMLAttributes<'span'> {
13
+ /** The text. */
14
+ text: string;
15
+ /** Element to render. */
16
+ as?: HTMLTag;
17
+ /** Seconds per sweep, including the pause. */
18
+ duration?: number;
19
+ /** Highlight colour. */
20
+ color?: string;
21
+ /** Width of the highlight as a fraction of the text, 0–1. */
22
+ width?: number;
23
+ }
24
+
25
+ const { text, as: Tag = 'span', duration = 3, color = 'var(--ma-shine)', width = 0.25, class: className, style, ...rest } = Astro.props;
26
+ const half = Math.max(0.05, Math.min(width, 0.6)) * 50;
27
+ // the caller's style joins the component's properties instead of replacing them (duration, colour and width were lost)
28
+ const vars = [`--ma-shiny-duration:${duration}s`, `--ma-shiny-color:${color}`, `--ma-shiny-a:${50 - half}%`, `--ma-shiny-b:${50 + half}%`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
29
+ ---
30
+
31
+ <Tag class:list={['ma-shiny', className]} data-text={text} style={vars} {...rest}>{text}</Tag>
32
+
33
+ <style is:global>
34
+ @layer components {
35
+ :where(.ma-shiny) {
36
+ position: relative;
37
+ display: inline-block;
38
+ }
39
+ :where(.ma-shiny)::after {
40
+ /* the copy is paint only: the alternative text after the slash is empty, so assistive tech reads the sentence
41
+ once (generated content is part of an element's name). The first line is for engines without that syntax. */
42
+ content: attr(data-text);
43
+ content: attr(data-text) / '';
44
+ position: absolute;
45
+ inset: 0;
46
+ pointer-events: none;
47
+ color: transparent;
48
+ background: linear-gradient(110deg, transparent var(--ma-shiny-a, 37.5%), var(--ma-shiny-color, var(--ma-shine)) 50%, transparent var(--ma-shiny-b, 62.5%));
49
+ background-size: 250% 100%;
50
+ background-position: 120% 0;
51
+ -webkit-background-clip: text;
52
+ background-clip: text;
53
+ animation: ma-shiny var(--ma-shiny-duration, 3s) var(--ma-ease-in-out) infinite;
54
+ }
55
+ @media (prefers-reduced-motion: reduce) {
56
+ :where(.ma-shiny)::after {
57
+ animation: none;
58
+ opacity: 0;
59
+ }
60
+ }
61
+ }
62
+
63
+ @keyframes ma-shiny {
64
+ 0% {
65
+ background-position: 120% 0;
66
+ }
67
+ 60%,
68
+ 100% {
69
+ background-position: -20% 0;
70
+ }
71
+ }
72
+ </style>