From 3c21530efbc383c48c694e792d4a34884fd62c23 Mon Sep 17 00:00:00 2001 From: Hari Lamichhane Date: Thu, 11 Jun 2026 23:33:14 +0545 Subject: [PATCH 1/2] release: docs revamp (#502) Co-authored-by: Sudha Shrestha --- .gitignore | 1 + CLAUDE.md | 17 +- animata/background/animated-beam.css | 28 + animata/background/animated-beam.tsx | 14 +- animata/background/shooting-stars.css | 32 + animata/background/shooting-stars.tsx | 15 +- animata/button/algolia-blue-button.css | 27 + animata/button/algolia-blue-button.tsx | 16 +- animata/button/algolia-white-button.css | 27 + animata/button/algolia-white-button.tsx | 18 +- animata/button/ripple-button.css | 43 + animata/button/ripple-button.tsx | 28 +- animata/card/card-spread.css | 31 + animata/card/card-spread.tsx | 34 +- animata/card/collab-card.css | 129 +++ animata/card/collab-card.tsx | 63 +- animata/container/animated-border-trail.css | 17 + animata/container/animated-border-trail.tsx | 15 +- animata/container/marquee.css | 33 + animata/container/marquee.tsx | 21 +- .../container/sibling-focus-nav.stories.tsx | 5 +- animata/container/sibling-focus-nav.tsx | 41 +- animata/fabs/flower-menu.css | 12 + animata/fabs/flower-menu.tsx | 14 +- animata/fabs/speed-dial.css | 84 ++ animata/fabs/speed-dial.tsx | 41 +- animata/feature-cards/content-scan.css | 16 + animata/feature-cards/content-scan.tsx | 16 +- animata/preloader/split-reveal.css | 52 + animata/preloader/split-reveal.tsx | 58 +- animata/scroll/stacked-sections.css | 5 + animata/scroll/stacked-sections.tsx | 8 +- animata/skeleton/category-glyphs.css | 127 +++ animata/skeleton/category-glyphs.tsx | 965 ++++++++++++++++++ .../skeleton/category-skeleton.stories.tsx | 52 + animata/skeleton/category-skeleton.tsx | 45 + animata/text/blur-out-up.stories.tsx | 9 +- animata/text/bottom-up-letters.stories.tsx | 9 +- animata/text/fade-through.stories.tsx | 9 +- animata/text/focus-blur-resolve.stories.tsx | 9 +- animata/text/glitch-text.css | 111 ++ animata/text/glitch-text.stories.tsx | 15 +- animata/text/glitch-text.tsx | 104 +- animata/text/kinetic-center-build.stories.tsx | 9 +- animata/text/line-by-line-slide.stories.tsx | 9 +- animata/text/mask-reveal-up.stories.tsx | 9 +- animata/text/metis-text.css | 33 + animata/text/metis-text.stories.tsx | 20 + animata/text/metis-text.tsx | 45 + animata/text/micro-scale-fade.stories.tsx | 9 +- animata/text/per-character-rise.stories.tsx | 9 +- animata/text/per-word-crossfade.stories.tsx | 9 +- animata/text/roll-text.css | 86 ++ animata/text/roll-text.stories.tsx | 54 + animata/text/roll-text.tsx | 287 ++++++ animata/text/scale-down-fade.stories.tsx | 9 +- animata/text/shared-axis-y.stories.tsx | 9 +- animata/text/shared-axis-z.stories.tsx | 9 +- animata/text/shimmer-sweep.stories.tsx | 9 +- animata/text/short-slide-down.stories.tsx | 9 +- animata/text/short-slide-right.stories.tsx | 9 +- animata/text/soft-blur-in.stories.tsx | 9 +- animata/text/spring-scale-in.stories.tsx | 9 +- animata/text/text-animator-demo.css | 23 + animata/text/text-animator-demo.ts | 8 + .../text-animator-preset.stories-shared.ts | 8 + animata/text/text-animator.css | 103 +- animata/text/text-animator.tsx | 12 +- animata/text/top-down-letters.stories.tsx | 10 +- app/(main)/_landing/call-to-action.tsx | 2 +- app/(main)/_landing/newsletter.tsx | 92 +- app/(main)/_landing/open-source-section.tsx | 2 +- app/(main)/_landing/section-one.tsx | 31 - app/(main)/_landing/section-three.tsx | 64 -- app/(main)/_landing/section-two.tsx | 118 --- app/(main)/_landing/stats-bento.tsx | 2 +- app/(main)/_landing/testimonials.tsx | 2 +- app/(main)/blog/layout.tsx | 2 +- app/(main)/components/layout.tsx | 9 + app/(main)/components/page.tsx | 48 + app/(main)/docs/[[...slug]]/page.tsx | 56 +- app/(main)/docs/layout.tsx | 20 +- app/(main)/layout.tsx | 7 +- app/(main)/page.tsx | 4 +- app/(main)/resources/page.tsx | 16 + app/(main)/sitemap.ts | 4 + app/(main)/text-animations/page.tsx | 2 +- app/demo/demo-notes.tsx | 2 +- app/demo/generated/demo-sources.ts | 57 +- app/demo/library/browse/cinema-row.tsx | 2 +- .../library/footer/footer-wordmark-notes.tsx | 2 +- app/demo/library/footer/footer-wordmark.tsx | 4 +- app/layout.tsx | 12 +- components/callout.tsx | 10 +- components/category-index-grid.tsx | 35 + components/category-index.tsx | 22 + components/category-nav-accordion.tsx | 132 +++ components/command-menu.tsx | 2 +- components/component-list-item.tsx | 2 +- components/doc-json-ld.tsx | 14 + components/docs-shell-layout.tsx | 21 + components/footer-bottom.tsx | 85 ++ components/footer-category-grid.tsx | 102 ++ components/footer-stamp.tsx | 14 + components/footer-subscribe.tsx | 80 ++ components/icons.tsx | 1 + components/mdx-components.tsx | 24 +- components/mobile-nav.tsx | 113 +- components/resources/grid-backdrop.tsx | 23 + components/resources/resource-grid-item.tsx | 39 + components/resources/resource-grid.tsx | 12 + components/resources/resources-shell.tsx | 46 + components/sidebar-nav.tsx | 417 ++++++-- components/site-footer.tsx | 160 ++- components/site-header.tsx | 70 +- components/text-animator-list-demo.tsx | 28 + config/docs.ts | 65 +- config/site-stats.ts | 6 +- config/site.ts | 3 +- content/docs/accordion/index.mdx | 7 + content/docs/background/animated-beam.mdx | 13 +- content/docs/background/index.mdx | 7 + content/docs/background/shooting-stars.mdx | 13 +- content/docs/button/algolia-blue-button.mdx | 8 +- content/docs/button/algolia-white-button.mdx | 8 +- content/docs/button/ripple-button.mdx | 8 +- content/docs/card/card-spread.mdx | 8 +- content/docs/card/collab-card.mdx | 8 +- content/docs/card/index.mdx | 7 + content/docs/carousel/index.mdx | 7 + content/docs/changelog/2026-06.mdx | 4 + .../docs/container/animated-border-trail.mdx | 10 +- content/docs/container/index.mdx | 7 + content/docs/container/marquee.mdx | 24 +- content/docs/container/sibling-focus-nav.mdx | 51 +- .../docs/contributing/category-glyphs-spec.md | 193 ++++ content/docs/contributing/category-glyphs.mdx | 117 +++ content/docs/contributing/components.mdx | 6 + content/docs/contributing/guidelines.mdx | 8 + content/docs/contributing/index.mdx | 2 + content/docs/fabs/flower-menu.mdx | 8 +- content/docs/fabs/index.mdx | 7 + content/docs/fabs/speed-dial.mdx | 10 +- content/docs/feature-cards/index.mdx | 7 + content/docs/graphs/index.mdx | 12 +- content/docs/hero/index.mdx | 7 + content/docs/icon/index.mdx | 7 + content/docs/image/index.mdx | 7 + content/docs/list/index.mdx | 7 + content/docs/overlay/index.mdx | 7 + content/docs/preloader/index.mdx | 7 + content/docs/preloader/split-reveal.mdx | 8 +- content/docs/progress/index.mdx | 7 + content/docs/scroll/index.mdx | 7 + content/docs/scroll/stacked-sections.mdx | 6 +- content/docs/section/index.mdx | 7 + content/docs/skeleton/code.mdx | 51 + content/docs/skeleton/cookie-banner.mdx | 45 + content/docs/skeleton/index.mdx | 59 +- content/docs/skeleton/list.mdx | 45 + content/docs/skeleton/receipt.mdx | 45 + content/docs/skeleton/report.mdx | 51 + content/docs/skeleton/wide-card.mdx | 45 + content/docs/tabs/index.mdx | 7 + content/docs/text/blur-out-up.mdx | 17 +- content/docs/text/bottom-up-letters.mdx | 17 +- content/docs/text/fade-through.mdx | 17 +- content/docs/text/focus-blur-resolve.mdx | 17 +- content/docs/text/glitch-text.mdx | 139 ++- content/docs/text/index.mdx | 52 +- content/docs/text/kinetic-center-build.mdx | 17 +- content/docs/text/line-by-line-slide.mdx | 17 +- content/docs/text/mask-reveal-up.mdx | 17 +- content/docs/text/metis-text.mdx | 66 ++ content/docs/text/micro-scale-fade.mdx | 17 +- content/docs/text/per-character-rise.mdx | 17 +- content/docs/text/per-word-crossfade.mdx | 17 +- content/docs/text/roll-text.mdx | 141 +++ content/docs/text/scale-down-fade.mdx | 17 +- content/docs/text/shared-axis-y.mdx | 17 +- content/docs/text/shared-axis-z.mdx | 17 +- content/docs/text/shimmer-sweep.mdx | 17 +- content/docs/text/short-slide-down.mdx | 17 +- content/docs/text/short-slide-right.mdx | 17 +- content/docs/text/soft-blur-in.mdx | 17 +- content/docs/text/spring-scale-in.mdx | 17 +- content/docs/text/top-down-letters.mdx | 17 +- lib/brand-font.ts | 13 + lib/component-categories.ts | 30 + lib/docs.ts | 100 +- lib/footer-grid.ts | 49 + lib/footer-theme.ts | 23 + lib/metadata.ts | 187 ++++ lib/outbound-ref.ts | 19 + lib/published-docs.ts | 33 + lib/resources.ts | 486 +++++++++ package.json | 1 - pnpm-lock.yaml | 8 - .../instrument-sans-latin-wght-normal.woff2 | Bin 0 -> 30092 bytes scripts/build-registry.js | 18 +- scripts/lib/og-template.mjs | 10 +- scripts/sync-category-indexes.mjs | 122 +++ scripts/sync-og-fonts.mjs | 4 +- scripts/validate-registry-install.js | 15 +- styles/docs.css | 18 +- styles/fonts.css | 7 + styles/globals.css | 74 ++ 207 files changed, 6739 insertions(+), 1501 deletions(-) create mode 100644 animata/background/animated-beam.css create mode 100644 animata/background/shooting-stars.css create mode 100644 animata/button/algolia-blue-button.css create mode 100644 animata/button/algolia-white-button.css create mode 100644 animata/button/ripple-button.css create mode 100644 animata/card/card-spread.css create mode 100644 animata/card/collab-card.css create mode 100644 animata/container/animated-border-trail.css create mode 100644 animata/container/marquee.css create mode 100644 animata/fabs/flower-menu.css create mode 100644 animata/fabs/speed-dial.css create mode 100644 animata/feature-cards/content-scan.css create mode 100644 animata/preloader/split-reveal.css create mode 100644 animata/scroll/stacked-sections.css create mode 100644 animata/skeleton/category-glyphs.css create mode 100644 animata/skeleton/category-glyphs.tsx create mode 100644 animata/skeleton/category-skeleton.stories.tsx create mode 100644 animata/skeleton/category-skeleton.tsx create mode 100644 animata/text/glitch-text.css create mode 100644 animata/text/metis-text.css create mode 100644 animata/text/metis-text.stories.tsx create mode 100644 animata/text/metis-text.tsx create mode 100644 animata/text/roll-text.css create mode 100644 animata/text/roll-text.stories.tsx create mode 100644 animata/text/roll-text.tsx create mode 100644 animata/text/text-animator-demo.css create mode 100644 animata/text/text-animator-demo.ts create mode 100644 animata/text/text-animator-preset.stories-shared.ts delete mode 100644 app/(main)/_landing/section-one.tsx delete mode 100644 app/(main)/_landing/section-three.tsx delete mode 100644 app/(main)/_landing/section-two.tsx create mode 100644 app/(main)/components/layout.tsx create mode 100644 app/(main)/components/page.tsx create mode 100644 app/(main)/resources/page.tsx create mode 100644 components/category-index-grid.tsx create mode 100644 components/category-index.tsx create mode 100644 components/category-nav-accordion.tsx create mode 100644 components/doc-json-ld.tsx create mode 100644 components/docs-shell-layout.tsx create mode 100644 components/footer-bottom.tsx create mode 100644 components/footer-category-grid.tsx create mode 100644 components/footer-stamp.tsx create mode 100644 components/footer-subscribe.tsx create mode 100644 components/resources/grid-backdrop.tsx create mode 100644 components/resources/resource-grid-item.tsx create mode 100644 components/resources/resource-grid.tsx create mode 100644 components/resources/resources-shell.tsx create mode 100644 components/text-animator-list-demo.tsx create mode 100644 content/docs/accordion/index.mdx create mode 100644 content/docs/background/index.mdx create mode 100644 content/docs/card/index.mdx create mode 100644 content/docs/carousel/index.mdx create mode 100644 content/docs/container/index.mdx create mode 100644 content/docs/contributing/category-glyphs-spec.md create mode 100644 content/docs/contributing/category-glyphs.mdx create mode 100644 content/docs/fabs/index.mdx create mode 100644 content/docs/feature-cards/index.mdx create mode 100644 content/docs/hero/index.mdx create mode 100644 content/docs/icon/index.mdx create mode 100644 content/docs/image/index.mdx create mode 100644 content/docs/list/index.mdx create mode 100644 content/docs/overlay/index.mdx create mode 100644 content/docs/preloader/index.mdx create mode 100644 content/docs/progress/index.mdx create mode 100644 content/docs/scroll/index.mdx create mode 100644 content/docs/section/index.mdx create mode 100644 content/docs/skeleton/code.mdx create mode 100644 content/docs/skeleton/cookie-banner.mdx create mode 100644 content/docs/skeleton/list.mdx create mode 100644 content/docs/skeleton/receipt.mdx create mode 100644 content/docs/skeleton/report.mdx create mode 100644 content/docs/skeleton/wide-card.mdx create mode 100644 content/docs/tabs/index.mdx create mode 100644 content/docs/text/metis-text.mdx create mode 100644 content/docs/text/roll-text.mdx create mode 100644 lib/brand-font.ts create mode 100644 lib/component-categories.ts create mode 100644 lib/footer-grid.ts create mode 100644 lib/footer-theme.ts create mode 100644 lib/metadata.ts create mode 100644 lib/outbound-ref.ts create mode 100644 lib/published-docs.ts create mode 100644 lib/resources.ts create mode 100644 public/og-fonts/instrument-sans-latin-wght-normal.woff2 create mode 100644 scripts/sync-category-indexes.mjs create mode 100644 styles/fonts.css diff --git a/.gitignore b/.gitignore index 57cbf583..aa3aae8d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ # misc .DS_Store *.pem +.patterns/ # debug npm-debug.log* diff --git a/CLAUDE.md b/CLAUDE.md index 657274ca..04be73b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,13 +8,16 @@ Animata is a free, open-source library of animated React components built with N - Components go in `animata//.tsx` with a matching `.stories.tsx` - Use `cn()` from `@/lib/utils` for class merging — never raw string concatenation -- CSS animations belong in inline ` - {/* soft glow */}
- -
- Algolia Blue ); diff --git a/animata/button/algolia-white-button.css b/animata/button/algolia-white-button.css new file mode 100644 index 00000000..b4637944 --- /dev/null +++ b/animata/button/algolia-white-button.css @@ -0,0 +1,27 @@ +@layer components { + .algolia-white-btn { + box-shadow: + rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:hover { + box-shadow: + rgba(45, 35, 66, 0.4) 0 4px 8px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:focus { + box-shadow: + #d6d6e7 0 0 0 1.5px inset, + rgba(45, 35, 66, 0.4) 0 2px 4px, + rgba(45, 35, 66, 0.3) 0 7px 13px -3px, + #d6d6e7 0 -3px 0 inset; + } + + .algolia-white-btn:active { + box-shadow: #d6d6e7 0 3px 7px inset; + } +} diff --git a/animata/button/algolia-white-button.tsx b/animata/button/algolia-white-button.tsx index dfc8f424..bbc07d09 100644 --- a/animata/button/algolia-white-button.tsx +++ b/animata/button/algolia-white-button.tsx @@ -1,21 +1,9 @@ +import "./algolia-white-button.css"; + export default function AlgoliaWhiteButton() { return ( ); } diff --git a/animata/button/ripple-button.css b/animata/button/ripple-button.css new file mode 100644 index 00000000..2f599332 --- /dev/null +++ b/animata/button/ripple-button.css @@ -0,0 +1,43 @@ +@layer components { + .ripple { + position: absolute; + border-radius: 50%; + pointer-events: none; + background-color: #000000; + z-index: 1; + opacity: 0; + transition: transform 50ms linear; + } + + .ripple-enter { + animation: ripple-enter 250ms ease-out forwards; + } + + .ripple-leave { + animation: ripple-leave 250ms ease-out forwards; + } + + @keyframes ripple-enter { + from { + transform: scale(0); + opacity: 1; + } + + to { + transform: scale(1); + opacity: 1; + } + } + + @keyframes ripple-leave { + from { + transform: scale(1); + opacity: 1; + } + + to { + transform: scale(0); + opacity: 1; + } + } +} diff --git a/animata/button/ripple-button.tsx b/animata/button/ripple-button.tsx index 84955925..ebff66f7 100644 --- a/animata/button/ripple-button.tsx +++ b/animata/button/ripple-button.tsx @@ -1,6 +1,9 @@ "use client"; + import { useCallback, useRef, useState } from "react"; +import "./ripple-button.css"; + interface RippleButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; } @@ -97,31 +100,6 @@ export default function RippleButton({ children, ...props }: RippleButtonProps) > {children} - ); } diff --git a/animata/card/card-spread.css b/animata/card/card-spread.css new file mode 100644 index 00000000..5335bbf4 --- /dev/null +++ b/animata/card/card-spread.css @@ -0,0 +1,31 @@ +@layer components { + .card-spread-stage { + /* Quick throw, soft settle — expand on click only */ + --ease-throw: cubic-bezier(0.22, 1.18, 0.36, 1); + } + + .card-spread-motion--expand { + transition: transform 480ms var(--ease-throw); + } + + .card-spread-motion--collapse { + transition: transform 520ms cubic-bezier(0.33, 1, 0.68, 1); + } + + .card-spread-stage--expand { + transition: width 480ms var(--ease-throw); + } + + .card-spread-stage--collapse { + transition: width 520ms cubic-bezier(0.33, 1, 0.68, 1); + } + + @media (prefers-reduced-motion: reduce) { + .card-spread-motion--expand, + .card-spread-motion--collapse, + .card-spread-stage--expand, + .card-spread-stage--collapse { + transition-duration: 0.01ms !important; + } + } +} diff --git a/animata/card/card-spread.tsx b/animata/card/card-spread.tsx index 683fe13b..216571bc 100644 --- a/animata/card/card-spread.tsx +++ b/animata/card/card-spread.tsx @@ -6,6 +6,8 @@ import Notes, { NotesCard } from "@/animata/widget/notes"; import ShoppingList from "@/animata/widget/shopping-list"; import { cn } from "@/lib/utils"; +import "./card-spread.css"; + /** w-48 + gap-3 — horizontal step between spread slots */ const CARD_STEP = "12.75rem"; @@ -72,38 +74,6 @@ export default function CardSpread() { return (
- -
{trailing} : null}

- -
); } diff --git a/animata/container/animated-border-trail.css b/animata/container/animated-border-trail.css new file mode 100644 index 00000000..2ff63086 --- /dev/null +++ b/animata/container/animated-border-trail.css @@ -0,0 +1,17 @@ +@layer components { + @property --border-trail-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; + } + + @keyframes border-trail { + 0% { + --border-trail-angle: 0deg; + } + + 100% { + --border-trail-angle: 360deg; + } + } +} diff --git a/animata/container/animated-border-trail.tsx b/animata/container/animated-border-trail.tsx index 4b7c11f6..831011d0 100644 --- a/animata/container/animated-border-trail.tsx +++ b/animata/container/animated-border-trail.tsx @@ -1,5 +1,7 @@ import { cn } from "@/lib/utils"; +import "./animated-border-trail.css"; + interface AnimatedTrailProps extends React.HTMLAttributes { /** * The duration of the animation. @@ -33,19 +35,6 @@ export default function AnimatedBorderTrail({ {...props} className={cn("relative h-fit w-fit overflow-hidden rounded-2xl bg-gray-200 p-px", className)} > -
{ /** * Should the marquee scroll horizontally or vertically. @@ -54,25 +56,6 @@ export default function Marquee({ className, )} > - {Array.from({ length: repeat }).map((_, index) => (
; export const Primary: Story = { render: () => ( - + {FOOTER_LINKS.map((label) => ( {["Work", "Studio", "Journal", "Contact"].map((label) => ( ("opacity"); -/** Opacity mode: dim siblings on hover or when the nav has focus; active link stays at full opacity. */ +/** Opacity mode: dim siblings only while a link is hovered or focus-visible. */ export const siblingFocusNavOpacityGroupClassName = cn( - "[&:hover>a]:opacity-30 [&:focus-within>a]:opacity-30", - "[&>a:hover]:opacity-100 [&>a:focus-visible]:opacity-100", + "[&:has(>a:hover)>a:not(:hover)]:opacity-30", + "[&:has(>a:focus-visible)>a:not(:focus-visible)]:opacity-30", ); /** Blur mode: same sibling trick, but with blur-sm instead of opacity. */ export const siblingFocusNavBlurGroupClassName = cn( - "[&:hover>a]:blur-sm [&:focus-within>a]:blur-sm", - "[&>a:hover]:blur-none [&>a:focus-visible]:blur-none", - "motion-reduce:[&:hover>a]:blur-none motion-reduce:[&:focus-within>a]:blur-none", + "[&:has(>a:hover)>a:not(:hover)]:blur-sm", + "[&:has(>a:focus-visible)>a:not(:focus-visible)]:blur-sm", + "motion-reduce:[&:has(>a:hover)>a:not(:hover)]:blur-none", + "motion-reduce:[&:has(>a:focus-visible)>a:not(:focus-visible)]:blur-none", ); const siblingFocusNavLinkBaseClassName = cn( @@ -37,16 +38,38 @@ export function siblingFocusNavGroupClassName(mode: SiblingFocusNavMode = "opaci return mode === "blur" ? siblingFocusNavBlurGroupClassName : siblingFocusNavOpacityGroupClassName; } +/** Horizontal separation — padding on links so hover survives the space between items. */ +export const siblingFocusNavSpacingXClassName = "[&>a:not(:last-child)]:pe-6"; + +/** Vertical separation — same idea for stacked nav rows. */ +export const siblingFocusNavSpacingYClassName = "[&>a:not(:last-child)]:pb-2.5"; + +type SiblingFocusNavSpacingAxis = "x" | "y" | "none"; + type SiblingFocusNavRootProps = ComponentProps<"nav"> & { mode?: SiblingFocusNavMode; + /** Padding-based link separation — keeps hover active between items (no flex gap). */ + spacingAxis?: SiblingFocusNavSpacingAxis; }; -function SiblingFocusNavRoot({ mode = "opacity", className, ...props }: SiblingFocusNavRootProps) { +function siblingFocusNavSpacingClassName(axis: SiblingFocusNavSpacingAxis) { + if (axis === "y") return siblingFocusNavSpacingYClassName; + if (axis === "none") return undefined; + return siblingFocusNavSpacingXClassName; +} + +function SiblingFocusNavRoot({ + mode = "opacity", + spacingAxis = "x", + className, + ...props +}: SiblingFocusNavRootProps) { return (
- -
); }; diff --git a/animata/preloader/split-reveal.css b/animata/preloader/split-reveal.css new file mode 100644 index 00000000..7e349c34 --- /dev/null +++ b/animata/preloader/split-reveal.css @@ -0,0 +1,52 @@ +@layer components { + @keyframes split-reveal-shutter-top { + from { + transform: translate3d(0, 0, 0); + } + + to { + transform: translate3d(0, -100%, 0); + } + } + + @keyframes split-reveal-shutter-bottom { + from { + transform: translate3d(0, 0, 0); + } + + to { + transform: translate3d(0, 100%, 0); + } + } + + [data-split-reveal-overlay] [data-split-reveal-progress] { + opacity: 1; + transition: opacity var(--split-reveal-progress-fade) ease-out; + } + + [data-split-reveal-overlay][data-phase="fade-ui"] [data-split-reveal-progress], + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-progress] { + opacity: 0; + } + + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter="top"] { + animation: split-reveal-shutter-top var(--split-reveal-duration) cubic-bezier(0.76, 0, 0.24, 1) + forwards; + } + + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter="bottom"] { + animation: split-reveal-shutter-bottom var(--split-reveal-duration) + cubic-bezier(0.76, 0, 0.24, 1) forwards; + } + + @media (prefers-reduced-motion: reduce) { + [data-split-reveal-overlay][data-phase="reveal"] [data-split-reveal-shutter] { + animation: none; + opacity: 0; + } + + [data-split-reveal-overlay] [data-split-reveal-progress] { + transition: none; + } + } +} diff --git a/animata/preloader/split-reveal.tsx b/animata/preloader/split-reveal.tsx index bb6e4e56..ebe3b070 100644 --- a/animata/preloader/split-reveal.tsx +++ b/animata/preloader/split-reveal.tsx @@ -15,6 +15,8 @@ import { import { cn } from "@/lib/utils"; +import "./split-reveal.css"; + type PreloaderPhase = "loading" | "fade-ui" | "reveal" | "done"; export interface SplitRevealProgressState { @@ -35,8 +37,6 @@ interface SplitRevealContextValue extends SplitRevealProgressState { const SplitRevealContext = createContext(null); -const REVEAL_EASE = "cubic-bezier(0.76, 0, 0.24, 1)"; - export function useSplitReveal() { const context = use(SplitRevealContext); if (!context) { @@ -167,59 +167,6 @@ function useScrollLock(active: boolean) { }, [active]); } -function SplitRevealStyles() { - return ( - - ); -} - function SplitRevealOverlayFrame({ className, children, ...props }: ComponentProps<"div">) { const { phase, zIndex, revealDuration, progressFadeMs, isActive } = useSplitReveal(); @@ -248,7 +195,6 @@ function SplitRevealOverlayFrame({ className, children, ...props }: ComponentPro data-split-reveal-overlay="" {...props} > - {children} ); diff --git a/animata/scroll/stacked-sections.css b/animata/scroll/stacked-sections.css new file mode 100644 index 00000000..cc4db84a --- /dev/null +++ b/animata/scroll/stacked-sections.css @@ -0,0 +1,5 @@ +@layer components { + [data-stacked-content][data-stacked-covered] { + transform-origin: 50% 0%; + } +} diff --git a/animata/scroll/stacked-sections.tsx b/animata/scroll/stacked-sections.tsx index 5a0c8119..01073f3c 100644 --- a/animata/scroll/stacked-sections.tsx +++ b/animata/scroll/stacked-sections.tsx @@ -4,6 +4,8 @@ import * as React from "react"; import { cn } from "@/lib/utils"; +import "./stacked-sections.css"; + export type StackedSectionsProps = { /** * One pane per direct child — sticky card > inner content (scale only). @@ -165,12 +167,6 @@ export default function StackedSections({ return ( <> - -
+ ); +} + +const GLYPHS: Record = { + /** Animated type — capital "A" with a motion trail */ + text: ( + <> + + + + + + + ), + + /** Ambient field — symmetric waves + floating particles */ + background: ( + <> + {[ + [22, SOFT], + [32, INK], + [42, SOFT], + ].map(([y, stroke], i) => ( + + ))} + {[ + [20, 16], + [44, 16], + [20, 48], + [44, 48], + ].map(([cx, cy]) => ( + + ))} + + ), + + /** Photo — frame, sun, mountains */ + image: ( + <> + + + + + ), + + /** Avatar rows */ + list: ( + <> + {[ + [17, 24, INK], + [27, 18, SOFT], + [37, 24, SOFT], + [47, 18, FAINT], + ].map(([y, w, shade], i) => ( + + + + + ))} + + ), + + /** Wrapper frame clipping a marquee of content blocks */ + container: ( + <> + + + {/* concentric with the frame: inner rx = outer rx − inset */} + + + + + + {[ + [14, SOFT], + [28, INK], + [42, SOFT], + [56, INK], + ].map(([x, shade]) => ( + + ))} + + + ), + + /** FAQ — closed, open (focal, with content), closed */ + accordion: ( + <> + + + + + + + + + + + + + + + ), + + /** Fanned stack of cards */ + card: ( + <> + + + + + + + ), + + /** Segmented control + panel */ + tabs: ( + <> + + + + + + + + + + + ), + + /** Pricing — three tiers, featured center */ + section: ( + <> + + + + + + + + + + + + + + + + + + ), + + /** Touch ripple — concentric rings */ + icon: ( + <> + + + + + + ), + + /** Split reveal — panels parting outward */ + preloader: ( + <> + + + + + + + ), + + /** Ring progress at ~70% */ + progress: ( + <> + + + + + ), + + /** Ascending bar chart */ + graphs: ( + <> + {[ + [15, 10, SOFT], + [24, 16, SOFT], + [33, 22, INK], + [42, 28, INK], + ].map(([x, h, shade], i) => ( + + ))} + + + ), + + /** Modal floating above dimmed page content */ + overlay: ( + <> + + + + + + + + + + + + + ), + + /** CTA pill with a click cursor */ + button: ( + <> + + + + + + + ), + + /** Clock widget tile */ + widget: ( + <> + + + + + + + ), + + /** Bento composition */ + "bento-grid": ( + <> + + + + + + ), + + /** Headline + CTA + scroll cue */ + hero: ( + <> + + + + + + + + + ), + + /** Sections + scrollbar thumb */ + scroll: ( + <> + + + + + + + + + + ), + + /** Center slide + side peeks + pagination dots */ + carousel: ( + <> + + + + + + + + + + + + + + + ), + + /** Loading placeholder layout */ + skeleton: ( + <> + + + + + + + + + ), + + /** Tilted feature card + highlight sparkle */ + "feature-cards": ( + <> + + + + + + + + + + ), + + /** Speed dial — FAB with an upward fan of actions */ + fabs: ( + <> + {[ + [12, 40], + [18, 26], + [32, 20], + [46, 26], + [52, 40], + ].map(([cx, cy], i) => ( + + ))} + + + + ), +}; + +const DEFAULT_GLYPH = ( + <> + {[16, 32, 48].map((cx) => + [16, 32, 48].map((cy) => ( + + )), + )} + +); + +export function CategoryGlyph({ variant }: { variant: string }) { + return {GLYPHS[variant] ?? DEFAULT_GLYPH}; +} diff --git a/animata/skeleton/category-skeleton.stories.tsx b/animata/skeleton/category-skeleton.stories.tsx new file mode 100644 index 00000000..f06b4ff6 --- /dev/null +++ b/animata/skeleton/category-skeleton.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import CategorySkeleton from "@/animata/skeleton/category-skeleton"; + +const meta = { + title: "Skeleton/Category Skeleton", + component: CategorySkeleton, + parameters: { layout: "centered", backgrounds: { default: "dark" } }, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Text: Story = { + args: { variant: "text", className: "w-80" }, +}; + +export const List: Story = { + args: { variant: "list", className: "w-80" }, +}; + +export const Icon: Story = { + args: { variant: "icon", className: "w-80" }, +}; + +export const Graphs: Story = { + args: { variant: "graphs", className: "w-80" }, +}; + +export const Skeleton: Story = { + args: { variant: "skeleton", className: "w-80" }, +}; + +export const Background: Story = { + args: { variant: "background", className: "w-80" }, +}; + +export const Widget: Story = { + args: { variant: "widget", className: "w-80" }, +}; + +export const Carousel: Story = { + args: { variant: "carousel", className: "w-80" }, +}; diff --git a/animata/skeleton/category-skeleton.tsx b/animata/skeleton/category-skeleton.tsx new file mode 100644 index 00000000..29dff99d --- /dev/null +++ b/animata/skeleton/category-skeleton.tsx @@ -0,0 +1,45 @@ +import { CategoryGlyph } from "@/animata/skeleton/category-glyphs"; +import { cn } from "@/lib/utils"; + +export type CategorySkeletonVariant = string; + +type CategorySkeletonProps = { + variant: CategorySkeletonVariant; + className?: string; +}; + +const CARD_W = 358; +const CARD_H = 201; +/** Glyph viewBox is 64×64; scale so art fills ~78% of card height */ +const GLYPH_SCALE = 2.35; + +export default function CategorySkeleton({ variant, className }: CategorySkeletonProps) { + return ( + + ); +} diff --git a/animata/text/blur-out-up.stories.tsx b/animata/text/blur-out-up.stories.tsx index 6c85ed17..f83582ab 100644 --- a/animata/text/blur-out-up.stories.tsx +++ b/animata/text/blur-out-up.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import BlurOutUp from "@/animata/text/blur-out-up"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Blur Out Up", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/bottom-up-letters.stories.tsx b/animata/text/bottom-up-letters.stories.tsx index b70dbf7c..8059ecf8 100644 --- a/animata/text/bottom-up-letters.stories.tsx +++ b/animata/text/bottom-up-letters.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import BottomUpLetters from "@/animata/text/bottom-up-letters"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Bottom-Up Letters", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/fade-through.stories.tsx b/animata/text/fade-through.stories.tsx index 43fdb6cb..1b88cdf7 100644 --- a/animata/text/fade-through.stories.tsx +++ b/animata/text/fade-through.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import FadeThrough from "@/animata/text/fade-through"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Fade Through", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/focus-blur-resolve.stories.tsx b/animata/text/focus-blur-resolve.stories.tsx index 3d8776a6..54fe0827 100644 --- a/animata/text/focus-blur-resolve.stories.tsx +++ b/animata/text/focus-blur-resolve.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import FocusBlurResolve from "@/animata/text/focus-blur-resolve"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Focus Blur Resolve", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/glitch-text.css b/animata/text/glitch-text.css new file mode 100644 index 00000000..44156aca --- /dev/null +++ b/animata/text/glitch-text.css @@ -0,0 +1,111 @@ +@layer components { + .glitch-text { + --glitch-intensity: 5; + --glitch-step: 0.01em; + --glitch-duration: 2.5s; + --glitch-color-base: #ffffff; + --glitch-color-a: #ff00ff; + --glitch-color-b: #00ffff; + --glitch-blend-mode: screen; + + /* Derived offsets — em-relative so glitch scales with font-size. */ + --glitch-x: calc(var(--glitch-intensity) * var(--glitch-step)); + --glitch-y: max(var(--glitch-step), calc(var(--glitch-x) * 0.35)); + --glitch-x-1: calc((var(--glitch-intensity) - 1) * var(--glitch-step)); + --glitch-x-2: calc((var(--glitch-intensity) - 2) * var(--glitch-step)); + + position: relative; + display: inline-grid; + grid-template-areas: stack; + } + + .glitch-text__ghost-a, + .glitch-text__ghost-b, + .glitch-text__base { + grid-area: stack; + } + + .glitch-text__ghost-a { + color: var(--glitch-color-a); + mix-blend-mode: var(--glitch-blend-mode); + animation: glitch-a var(--glitch-duration) infinite; + } + + .glitch-text__ghost-b { + color: var(--glitch-color-b); + mix-blend-mode: var(--glitch-blend-mode); + animation: glitch-b var(--glitch-duration) infinite; + } + + .glitch-text__base { + position: relative; + z-index: 2; + color: var(--glitch-color-base); + } + + @keyframes glitch-a { + 0%, + 88%, + 100% { + transform: translate(0, 0); + opacity: 0.85; + } + + 90% { + transform: translate(calc(var(--glitch-x) * -1), calc(var(--glitch-y) * -1)); + opacity: 1; + } + + 92% { + transform: translate(var(--glitch-x-1), var(--glitch-y)); + opacity: 0.9; + } + + 94% { + transform: translate(calc(var(--glitch-x-2) * -1), 0); + opacity: 1; + } + + 96% { + transform: translate(var(--glitch-x), calc(var(--glitch-y) * -1)); + opacity: 0.85; + } + } + + @keyframes glitch-b { + 0%, + 86%, + 100% { + transform: translate(0, 0); + opacity: 0.85; + } + + 88% { + transform: translate(var(--glitch-x), var(--glitch-y)); + opacity: 1; + } + + 91% { + transform: translate(calc(var(--glitch-x) * -1), calc(var(--glitch-y) * -1)); + opacity: 0.9; + } + + 93% { + transform: translate(var(--glitch-x-1), 0); + opacity: 1; + } + + 95% { + transform: translate(calc(var(--glitch-x-2) * -1), var(--glitch-y)); + opacity: 0.85; + } + } + + @media (prefers-reduced-motion: reduce) { + .glitch-text__ghost-a, + .glitch-text__ghost-b { + animation: none; + opacity: 0.35; + } + } +} diff --git a/animata/text/glitch-text.stories.tsx b/animata/text/glitch-text.stories.tsx index a213d12c..534b7819 100644 --- a/animata/text/glitch-text.stories.tsx +++ b/animata/text/glitch-text.stories.tsx @@ -1,14 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react"; + import GlitchText from "@/animata/text/glitch-text"; const meta = { title: "Text/Glitch Text", component: GlitchText, - parameters: { - layout: "centered", - }, + parameters: { layout: "centered" }, tags: ["autodocs"], - argTypes: {}, } satisfies Meta; export default meta; @@ -16,7 +14,12 @@ type Story = StoryObj; export const Primary: Story = { args: { - text: "1000 Stars", - starCount: 50, + children: "404", + className: "font-(family-name:--font-mono) text-8xl font-bold tracking-tight text-white", }, + render: (args) => ( +
+ +
+ ), }; diff --git a/animata/text/glitch-text.tsx b/animata/text/glitch-text.tsx index f10e77ee..73e36db2 100644 --- a/animata/text/glitch-text.tsx +++ b/animata/text/glitch-text.tsx @@ -1,48 +1,74 @@ -import { Tomorrow } from "next/font/google"; +"use client"; import { cn } from "@/lib/utils"; -const tomorrow = Tomorrow({ - subsets: ["latin"], - weight: ["800", "900"], - variable: "--font-tomorrow", -}); +import "./glitch-text.css"; +export interface GlitchTextProps extends Omit, "children"> { + children: string; + /** Unitless multiplier on `--glitch-step`. @default 5 (via CSS) */ + intensity?: number; + /** Base offset unit — em scales with font size. @default "0.01em" */ + step?: string; + /** Glitch burst cycle. Numbers are seconds (`2.5` → `2.5s`). @default 2.5 */ + duration?: number | string; + /** Main layer color (on top). @default "#ffffff" */ + baseColor?: string; + /** Back ghost layer A. @default "#ff00ff" */ + colorA?: string; + /** Back ghost layer B. @default "#00ffff" */ + colorB?: string; + /** Ghost layer blend mode. @default "screen" */ + blendMode?: "screen" | "normal"; +} + +function glitchDuration(duration: number | string) { + return typeof duration === "number" ? `${duration}s` : duration; +} + +/** + * RGB glitch via stacked text layers — ghosts animate transform only (compositor), + * base stays full-color on top. Props map to CSS variables; defaults live in `glitch-text.css`. + */ export default function GlitchText({ - text = "1000 Stars", + children, className, - starCount = 50, -}: { - text: string; - className?: string; - starCount?: number; -}) { + intensity, + step, + duration, + baseColor, + colorA, + colorB, + blendMode, + style, + ...props +}: GlitchTextProps) { return ( -
-
- {[...Array(starCount)].map((_, i) => ( -
- ))} -

- {text} -

-
-
+ + + {children} + + + {children} + + {children} + ); } diff --git a/animata/text/kinetic-center-build.stories.tsx b/animata/text/kinetic-center-build.stories.tsx index 0bc66aaf..8c380ec9 100644 --- a/animata/text/kinetic-center-build.stories.tsx +++ b/animata/text/kinetic-center-build.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import KineticCenterBuild from "@/animata/text/kinetic-center-build"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Kinetic Center Build", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/line-by-line-slide.stories.tsx b/animata/text/line-by-line-slide.stories.tsx index 61f63a35..ebd5f41c 100644 --- a/animata/text/line-by-line-slide.stories.tsx +++ b/animata/text/line-by-line-slide.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import LineByLineSlide from "@/animata/text/line-by-line-slide"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Line-by-Line Slide", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/mask-reveal-up.stories.tsx b/animata/text/mask-reveal-up.stories.tsx index 97e89568..695572b1 100644 --- a/animata/text/mask-reveal-up.stories.tsx +++ b/animata/text/mask-reveal-up.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import MaskRevealUp from "@/animata/text/mask-reveal-up"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Mask Reveal Up", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/metis-text.css b/animata/text/metis-text.css new file mode 100644 index 00000000..06487d39 --- /dev/null +++ b/animata/text/metis-text.css @@ -0,0 +1,33 @@ +@layer components { + .metis-text::after { + content: ""; + position: absolute; + left: 0; + top: 100%; + width: 100%; + height: 1px; + background: currentcolor; + pointer-events: none; + transform-origin: 100% 50%; + transform: scale3d(0, 1, 1); + transition: transform 0.3s; + } + + .metis-text:hover::after, + .metis-text:focus-visible::after { + transform-origin: 0% 50%; + transform: scale3d(1, 1, 1); + } + + .group:hover .metis-text--group::after, + .group:focus-visible .metis-text--group::after { + transform-origin: 0% 50%; + transform: scale3d(1, 1, 1); + } + + @media (prefers-reduced-motion: reduce) { + .metis-text::after { + transition: none; + } + } +} diff --git a/animata/text/metis-text.stories.tsx b/animata/text/metis-text.stories.tsx new file mode 100644 index 00000000..e621e9d8 --- /dev/null +++ b/animata/text/metis-text.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import MetisText from "@/animata/text/metis-text"; + +const meta = { + title: "Text/Metis Text", + component: MetisText, + parameters: { layout: "centered" }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + children: "About us", + className: "font-(family-name:--font-display) text-2xl lowercase text-foreground", + }, +}; diff --git a/animata/text/metis-text.tsx b/animata/text/metis-text.tsx new file mode 100644 index 00000000..907e77f4 --- /dev/null +++ b/animata/text/metis-text.tsx @@ -0,0 +1,45 @@ +import type React from "react"; + +import { cn } from "@/lib/utils"; + +import "./metis-text.css"; + +export interface MetisTextProps extends React.HTMLAttributes { + children: React.ReactNode; + as?: "span" | "a" | "p"; + /** + * When true, the underline reacts to an ancestor with `.group` on + * `:hover` / `:focus-visible` — useful inside card links. + * @default false + */ + groupHover?: boolean; + /** Classes for the inner label wrapper — put `truncate` / `line-clamp-*` here. */ + labelClassName?: string; +} + +/** + * Codrops Metis underline — scaleX reveal with flipped transform-origin on enter/exit. + * Keep truncation on `labelClassName`, not the root, so the line is not clipped. + * @see https://github.com/codrops/LineHoverStyles/ + */ +export default function MetisText({ + children, + as: Tag = "span", + groupHover = false, + className, + labelClassName, + ...props +}: MetisTextProps) { + return ( + + {children} + + ); +} diff --git a/animata/text/micro-scale-fade.stories.tsx b/animata/text/micro-scale-fade.stories.tsx index 25eb76fe..b75f023f 100644 --- a/animata/text/micro-scale-fade.stories.tsx +++ b/animata/text/micro-scale-fade.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import MicroScaleFade from "@/animata/text/micro-scale-fade"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Micro Scale Fade", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/per-character-rise.stories.tsx b/animata/text/per-character-rise.stories.tsx index e7cc2505..a630426b 100644 --- a/animata/text/per-character-rise.stories.tsx +++ b/animata/text/per-character-rise.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import PerCharacterRise from "@/animata/text/per-character-rise"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Per-Character Rise", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/per-word-crossfade.stories.tsx b/animata/text/per-word-crossfade.stories.tsx index 0b96c72d..e3aa8a2c 100644 --- a/animata/text/per-word-crossfade.stories.tsx +++ b/animata/text/per-word-crossfade.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import PerWordCrossfade from "@/animata/text/per-word-crossfade"; +import { TEXT_ANIMATOR_PRESET_STORY_ARGS } from "@/animata/text/text-animator-preset.stories-shared"; const meta = { title: "Text/Per-Word Crossfade", @@ -12,11 +13,5 @@ export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - speed: 0.72, - holdMs: 550, - gapMs: 320, - className: - "h-80 w-[32rem] rounded-lg border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100", - }, + args: TEXT_ANIMATOR_PRESET_STORY_ARGS, }; diff --git a/animata/text/roll-text.css b/animata/text/roll-text.css new file mode 100644 index 00000000..c7dae398 --- /dev/null +++ b/animata/text/roll-text.css @@ -0,0 +1,86 @@ +@layer components { + .roll-text { + --roll-duration: 450ms; + --roll-duration-reduced: 150ms; + --roll-ease: cubic-bezier(0.77, 0, 0.175, 1); + --roll-line-height: 1; + font-kerning: none; + line-height: var(--roll-line-height); + } + + .roll-text:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 3px; + } + + .roll-text__track { + display: inline; + line-height: inherit; + white-space: pre; + } + + /* inline-grid + overflow:hidden keeps the text baseline (from the sizer in + row 1) while clipping the translated stack. */ + .roll-unit { + position: relative; + display: inline-grid; + overflow: hidden; + vertical-align: baseline; + font-kerning: none; + line-height: inherit; + } + + .roll-unit__sizer, + .roll-unit__stack { + grid-area: 1 / 1; + line-height: inherit; + } + + .roll-unit__sizer { + visibility: hidden; + pointer-events: none; + user-select: none; + white-space: pre; + } + + .roll-unit__stack { + position: absolute; + inset-inline: 0; + top: 0; + display: flex; + flex-direction: column; + white-space: pre; + transform: translateY(0); + } + + .roll-unit__line { + display: block; + line-height: inherit; + white-space: inherit; + } + + @keyframes roll-stack-open { + from { + transform: translateY(0); + } + + to { + transform: translateY(-50%); + } + } + + .roll-text--animating .roll-unit__stack { + animation: roll-stack-open var(--roll-unit-duration, var(--roll-duration)) var(--roll-ease) + var(--roll-delay, 0ms) forwards; + } + + .roll-text--open:not(.roll-text--animating) .roll-unit__stack { + transform: translateY(-50%); + } + + @media (prefers-reduced-motion: reduce) { + .roll-text--animating .roll-unit__stack { + animation-duration: var(--roll-duration-reduced); + } + } +} diff --git a/animata/text/roll-text.stories.tsx b/animata/text/roll-text.stories.tsx new file mode 100644 index 00000000..4cd455fc --- /dev/null +++ b/animata/text/roll-text.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import RollText from "@/animata/text/roll-text"; + +const meta = { + title: "Text/Roll Text", + component: RollText, + parameters: { layout: "centered" }, + tags: ["autodocs"], + argTypes: { + stagger: { + control: "select", + options: ["none", "word", "character"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + text: "Team member", + stagger: "character", + staggerMs: 32, + durationMs: 250, + className: "font-(family-name:--font-display) text-4xl tracking-tight text-foreground", + }, +}; + +export const InCardLink: Story = { + args: { + text: "Roll text", + stagger: "character", + staggerMs: 45, + }, + render: (args) => ( + + +

+ Hover the card — animation plays through even if the pointer leaves, stays revealed, and + replays on re-hover. +

+
+ ), +}; diff --git a/animata/text/roll-text.tsx b/animata/text/roll-text.tsx new file mode 100644 index 00000000..34769c90 --- /dev/null +++ b/animata/text/roll-text.tsx @@ -0,0 +1,287 @@ +"use client"; + +import type React from "react"; +import { + Fragment, + memo, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { cn } from "@/lib/utils"; + +import "./roll-text.css"; + +export type RollStagger = "none" | "word" | "character"; + +export interface RollTextProps extends Omit, "children"> { + /** Label duplicated across the two stacked roll layers. */ + text: string; + /** + * When true, the roll plays when the nearest `[data-roll-group]` or + * `group/roll` ancestor receives hover or focus. The span is not tabbable — + * put `href` / actions on the wrapping link or button. + * @default false + */ + groupHover?: boolean; + /** + * When true, hover and focus do not trigger the roll — use for active nav + * items or other states where motion should stay static. + * @default false + */ + disabled?: boolean; + /** + * Stagger the roll across words or characters. `none` animates the whole + * label at once. + * @default "none" + */ + stagger?: RollStagger; + /** Delay step between staggered units in ms. @default 32 */ + staggerMs?: number; + /** Per-unit travel duration in ms. @default 250 */ + durationMs?: number; +} + +type RollPhase = "closed" | "animating" | "open"; + +type RollSegment = { + key: string; + value: string; + delay: number; + duration: number; +}; + +function staggerDelay(index: number, count: number, staggerMs: number): number { + if (count <= 1) return 0; + return index * staggerMs; +} + +function segmentTiming( + index: number, + count: number, + staggerMs: number, + durationMs: number, +): { delay: number; duration: number } { + return { + delay: staggerDelay(index, count, staggerMs), + duration: durationMs, + }; +} + +function splitSegments( + text: string, + stagger: RollStagger, + staggerMs: number, + durationMs: number, +): RollSegment[] { + if (stagger === "none") { + return [{ key: "whole", value: text, delay: 0, duration: durationMs }]; + } + + if (stagger === "word") { + const words = text.trim().split(/\s+/); + return words.map((word, index) => { + const timing = segmentTiming(index, words.length, staggerMs, durationMs); + return { + key: `${index}-${word}`, + value: word, + ...timing, + }; + }); + } + + return [...text].map((char, index) => { + const timing = segmentTiming(index, text.length, staggerMs, durationMs); + return { + key: `${index}-${char}`, + value: char, + ...timing, + }; + }); +} + +const ROLL_GROUP_SELECTOR = "[data-roll-group], .group\\/roll"; + +function findRollGroup(node: HTMLElement | null) { + return node?.closest(ROLL_GROUP_SELECTOR) ?? null; +} + +const RollUnit = memo(function RollUnit({ + segment, + onStackAnimationEnd, +}: { + segment: RollSegment; + onStackAnimationEnd?: (event: React.AnimationEvent) => void; +}) { + return ( + + + {segment.value} + + + {segment.value} + {segment.value} + + + ); +}); + +/** + * Decorative roll label — always a `span`. Wrap in ``, ``, or ` -

+

{success ? ( Thank you for subscribing! ) : error ? ( @@ -49,23 +79,59 @@ function NewsletterInput(): React.JSX.Element { ); } -export default function NewsletterSection() { +type NewsletterSectionProps = { + compact?: boolean; + brand?: boolean; +}; + +export default function NewsletterSection({ + compact = false, + brand = false, +}: NewsletterSectionProps) { + if (brand) { + return ( +

+

+ Sign up for updates on new components and releases. +

+
+ +
+
+ ); + } + return ( - - - + + + Stay in the loop - + New components, tips, and updates. No spam. - - + + ); diff --git a/app/(main)/_landing/open-source-section.tsx b/app/(main)/_landing/open-source-section.tsx index fc30a4bf..f5d308d4 100644 --- a/app/(main)/_landing/open-source-section.tsx +++ b/app/(main)/_landing/open-source-section.tsx @@ -100,7 +100,7 @@ export default function OpenSourceSection() { return (
-
+

Open source. diff --git a/app/(main)/_landing/section-one.tsx b/app/(main)/_landing/section-one.tsx deleted file mode 100644 index ef4325a3..00000000 --- a/app/(main)/_landing/section-one.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import Expandable from "@/animata/carousel/expandable"; -import ComponentLinkWrapper from "@/components/component-link-wrapper"; -import { siteStats } from "@/config/site-stats"; - -export default function SectionOne() { - return ( -
-
-
-

- {siteStats.componentsFormatted} components -

-

- Every one is live. Every one is free. -

-
- - - -
-
- ); -} diff --git a/app/(main)/_landing/section-three.tsx b/app/(main)/_landing/section-three.tsx deleted file mode 100644 index 2a15aa8a..00000000 --- a/app/(main)/_landing/section-three.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import AnimatedGradientText from "@/animata/text/animated-gradient-text"; -import GlitchText from "@/animata/text/glitch-text"; -import SplitText from "@/animata/text/split-text"; -import ComponentLinkWrapper from "@/components/component-link-wrapper"; -import { cn } from "@/lib/utils"; - -function DarkCard({ - children, - href, - label, - className, -}: { - children: React.ReactNode; - href: string; - label: string; - className?: string; -}) { - return ( - -
{children}
-
- {label} - View → -
-
- ); -} - -export default function SectionThree() { - return ( -
-
- {/* GlitchText — the star. Full-width, un-carded. */} - - - - - {/* Supporting cast — in cards */} -
- - - HELLO - - - - - - -
-
-
- ); -} diff --git a/app/(main)/_landing/section-two.tsx b/app/(main)/_landing/section-two.tsx deleted file mode 100644 index a722cf72..00000000 --- a/app/(main)/_landing/section-two.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { Inbox, Landmark, PieChart } from "lucide-react"; -import Link from "next/link"; -import LEDBoard from "@/animata/card/led-board"; -import FluidTabs from "@/animata/tabs/fluid-tabs"; -import StaggeredLetter from "@/animata/text/staggered-letter"; -import ComponentLinkWrapper from "@/components/component-link-wrapper"; -import { siteStats } from "@/config/site-stats"; -import { cn } from "@/lib/utils"; - -function DemoCard({ - children, - href, - label, - dark, - className, -}: { - children: React.ReactNode; - href: string; - label: string; - dark?: boolean; - className?: string; -}) { - return ( - -
{children}
-
- - {label} - - - View → - -
-
- ); -} - -export default function SectionTwo() { - return ( -
-
-
- -
- -
-
- - -
- - - - - - - Accounts - - - - - - Deposits - - - - - - Funds - - - -
-
- - - - -
- -
- - Browse all {siteStats.componentsFormatted} components → - -
-
-
- ); -} diff --git a/app/(main)/_landing/stats-bento.tsx b/app/(main)/_landing/stats-bento.tsx index b391ade6..2b06bb6f 100644 --- a/app/(main)/_landing/stats-bento.tsx +++ b/app/(main)/_landing/stats-bento.tsx @@ -145,7 +145,7 @@ function BentoCard({ children, className }: { children: React.ReactNode; classNa export default function StatsBento() { return (
-
+
{/* 1 col mobile → 2 col tablet → 3 col desktop */}
{/* Card 1: Stars + Border Trail */} diff --git a/app/(main)/_landing/testimonials.tsx b/app/(main)/_landing/testimonials.tsx index 05578734..685c7bc3 100644 --- a/app/(main)/_landing/testimonials.tsx +++ b/app/(main)/_landing/testimonials.tsx @@ -61,7 +61,7 @@ function Testimonial({ comment, author }: { comment: string; author: string }) { export default function Testimonials() { return (
-
+

Loved by developers diff --git a/app/(main)/blog/layout.tsx b/app/(main)/blog/layout.tsx index f2b44bb1..32bf2e90 100644 --- a/app/(main)/blog/layout.tsx +++ b/app/(main)/blog/layout.tsx @@ -12,7 +12,7 @@ export default function BlogLayout({ children }: BlogLayoutProps) {
{children} diff --git a/app/(main)/components/layout.tsx b/app/(main)/components/layout.tsx new file mode 100644 index 00000000..368001ce --- /dev/null +++ b/app/(main)/components/layout.tsx @@ -0,0 +1,9 @@ +import { DocsShellLayout } from "@/components/docs-shell-layout"; + +interface ComponentsLayoutProps { + children: React.ReactNode; +} + +export default function ComponentsLayout({ children }: ComponentsLayoutProps) { + return {children}; +} diff --git a/app/(main)/components/page.tsx b/app/(main)/components/page.tsx new file mode 100644 index 00000000..8c38510f --- /dev/null +++ b/app/(main)/components/page.tsx @@ -0,0 +1,48 @@ +import Link from "next/link"; + +import CategorySkeleton from "@/animata/skeleton/category-skeleton"; +import { siteStats } from "@/config/site-stats"; +import { getComponentCategories } from "@/lib/component-categories"; +import { createPageMetadata } from "@/lib/metadata"; + +export const metadata = createPageMetadata({ + title: "Components", + description: `${siteStats.componentsFormatted} animated React components, filed by category. Text, backgrounds, cards, widgets, and the rest. Each links to a live preview and copy-paste docs for Next.js and Tailwind.`, + path: "/components", + keywords: ["component library", "React UI categories", "animated components index"], +}); + +export default function ComponentsPage() { + const categories = getComponentCategories(); + + return ( +
+
+

Components

+

+ Everything in Animata lives here, sorted by type. Open a category for live previews and + copy-paste docs. +

+
+ +
    + {categories.map((category) => ( +
  • + + +

    + + {category.title} + + · + + {category.count} + +

    + +
  • + ))} +
+
+ ); +} diff --git a/app/(main)/docs/[[...slug]]/page.tsx b/app/(main)/docs/[[...slug]]/page.tsx index ec2cdb6c..a397194e 100644 --- a/app/(main)/docs/[[...slug]]/page.tsx +++ b/app/(main)/docs/[[...slug]]/page.tsx @@ -2,19 +2,26 @@ import { ChevronRightIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { docs as allDocs } from "#site/content"; import NavMenu from "@/app/(main)/docs/[[...slug]]/nav-menu"; import CarbonAds from "@/components/ads"; import { DocDemoLinks } from "@/components/doc-demo-links"; +import { DocJsonLd } from "@/components/doc-json-ld"; import { Mdx } from "@/components/mdx-components"; import { DocsPager } from "@/components/pager"; import { DashboardTableOfContents } from "@/components/toc"; import { badgeVariants } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { siteConfig } from "@/config/site"; +import { buildDocMetadata } from "@/lib/metadata"; import ogManifest from "@/lib/og-manifest.json"; +import { + getPublishedDoc, + getPublishedDocs, + isCategoryIndexDoc, + type PublishedDoc, +} from "@/lib/published-docs"; import { getTableOfContents } from "@/lib/toc"; -import { absoluteUrl, cn } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import "@/styles/mdx.css"; interface DocPageProps { @@ -25,13 +32,15 @@ interface DocPageProps { async function getDocFromParams(params: { slug: string[] }) { const slug = params.slug?.join("/") || ""; - const doc = allDocs.find((doc) => doc.slugAsParams === slug); + return getPublishedDoc(slug); +} - if (!doc) { - return null; +function getMdxFilePath(doc: PublishedDoc) { + if (isCategoryIndexDoc(doc)) { + return `content/docs/${doc.slugAsParams}/index.mdx`; } - return doc; + return `content/${doc.path}.mdx`; } export async function generateMetadata({ params }: DocPageProps): Promise { @@ -47,35 +56,11 @@ export async function generateMetadata({ params }: DocPageProps): Promise)[doc.slug]?.url ?? siteConfig.ogImage; - return { - title: doc.title, - description: doc.description, - openGraph: { - title: doc.title, - description: doc.description, - type: "article", - url: absoluteUrl(doc.slug), - images: [ - { - url: ogImage, - width: 1200, - height: 630, - alt: doc.title, - }, - ], - }, - twitter: { - card: "summary_large_image", - title: doc.title, - description: doc.description, - images: [ogImage], - creator: doc.author ? `@${doc.author}` : "@AnimataDesign", - }, - }; + return buildDocMetadata(doc, ogImage); } export async function generateStaticParams() { - return allDocs.map((doc) => ({ + return getPublishedDocs().map((doc) => ({ slug: doc.slugAsParams.split("/"), })); } @@ -92,7 +77,8 @@ export default async function DocPage({ params }: DocPageProps) { return (
-
+ +
Docs
@@ -148,11 +134,11 @@ export default async function DocPage({ params }: DocPageProps) {
- +
-
- - {children} -
-
- ); + return {children}; } diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 91e4e396..b1a420f5 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,4 +1,3 @@ -import AnnouncementRibbon from "@/animata/container/announcement-ribbon"; import { CSPostHogProvider } from "@/app/(main)/providers"; import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; @@ -12,8 +11,10 @@ export default function MainLayout({ children }: MainLayoutProps) { return (
-
- +
{children}
diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 0203cc41..747179f1 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -46,7 +46,7 @@ function Hero() { return (
-

+

Ship faster.
Look better. @@ -109,7 +109,7 @@ function WhySection() { return (
-
+

Why teams
diff --git a/app/(main)/resources/page.tsx b/app/(main)/resources/page.tsx new file mode 100644 index 00000000..63df497e --- /dev/null +++ b/app/(main)/resources/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; + +import { ResourcesShell } from "@/components/resources/resources-shell"; + +export const metadata: Metadata = { + title: "Resources", + description: "Sites we use while building animata.", + openGraph: { + title: "Resources", + description: "Sites we use while building animata.", + }, +}; + +export default function ResourcesPage() { + return ; +} diff --git a/app/(main)/sitemap.ts b/app/(main)/sitemap.ts index 8acc7776..09a556fe 100644 --- a/app/(main)/sitemap.ts +++ b/app/(main)/sitemap.ts @@ -11,6 +11,10 @@ export default function sitemap(): MetadataRoute.Sitemap { url: `${domain}/`, lastModified: new Date(), }, + { + url: `${domain}/components`, + lastModified: new Date(), + }, ...allDocs .filter((doc) => doc.published) .map((doc) => ({ diff --git a/app/(main)/text-animations/page.tsx b/app/(main)/text-animations/page.tsx index 2306f03c..9ede71ce 100644 --- a/app/(main)/text-animations/page.tsx +++ b/app/(main)/text-animations/page.tsx @@ -198,7 +198,7 @@ function LazyTile({ preset }: { preset: Preset }) { export default function TextAnimationsPage() { return ( -
+

Text Animations

diff --git a/app/demo/demo-notes.tsx b/app/demo/demo-notes.tsx index 958c359c..e49e9a35 100644 --- a/app/demo/demo-notes.tsx +++ b/app/demo/demo-notes.tsx @@ -79,7 +79,7 @@ function DemoNotesRoot({ )} > -

+
{children}

diff --git a/app/demo/generated/demo-sources.ts b/app/demo/generated/demo-sources.ts index 281b78f9..b48d58cd 100644 --- a/app/demo/generated/demo-sources.ts +++ b/app/demo/generated/demo-sources.ts @@ -115,7 +115,7 @@ export default function FooterWordmarkDemo() { >