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 1f6f6f07..04be73b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Animata is a free, open-source library of animated React components built with N - Use `cn()` from `@/lib/utils` for class merging — never raw string concatenation - Tailwind for layout, color, spacing, and transitions on the component itself - Co-located `.css` imported from the TSX for keyframes, pseudo-elements, and selectors Tailwind cannot express (see `roll-text.tsx`, `metis-text.tsx`) -- 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) => (
>; href: string; @@ -200,18 +202,6 @@ export default function FlowerMenu({ style={{ width: containerSize, height: containerSize, minHeight: containerSize }} {...props} > - - - -
- -
); }; 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 ( <> - -