Vendor 50 Aceternity UI components into components/ui from the official registry (ui.aceternity.com/registry/<slug>.json), adapted for this template's stack: Tailwind v4, motion/react 13, cn util, React 19. Add a live showcase gallery (/ui + /docs/... navigation is unchanged): app/ui/page.tsx renders <ShowcaseGallery> with 30+ demos — 3d-pin, aurora, background-beams, bento, card-stack, card-spotlight, direction-aware-hover, encrypted-text, evervault-card, flip-words, hero-highlight, hero-parallax, infinite-moving-cards, moving-border, sparkles, tracing-beam, animated-tooltip, typewriter-effect, placeholders-and-vanish-input and friends. Toolchain stays zero-warning: vendored animation code is scoped in eslint.config.mjs (react-hooks/react-compiler advisory rules, no-any, prefer-const, no-img-element) so app code keeps a 0-error gate. typecheck, build, 7 vitest tests and 2 test suites all green.
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useId } from "react";
|
|
import { motion, useTime, useTransform } from "motion/react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface SquigglyTextProps {
|
|
/**
|
|
* The text (or any node) to wrap with the squiggly effect.
|
|
*/
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
/**
|
|
* Number of distinct displacement frames to cycle through.
|
|
* Higher = smoother wobble, more SVG filters in the DOM.
|
|
* @default 5
|
|
*/
|
|
steps?: number;
|
|
/**
|
|
* Time between filter swaps, in milliseconds.
|
|
* Lower = more frantic; higher = lazier wave.
|
|
* @default 80
|
|
*/
|
|
stepDuration?: number;
|
|
/**
|
|
* Maximum displacement in px. Bigger = more squiggly.
|
|
* Pass a single number for a constant scale, or a tuple to alternate
|
|
* between two values per step (matches the original Lucas Bebber demo).
|
|
* @default [6, 8]
|
|
*/
|
|
scale?: number | [number, number];
|
|
/**
|
|
* Turbulence base frequency. Lower values produce longer, smoother waves;
|
|
* higher values produce tighter, jitterier noise.
|
|
* @default 0.02
|
|
*/
|
|
baseFrequency?: number;
|
|
/**
|
|
* Number of turbulence octaves. Higher = more detailed noise.
|
|
* @default 3
|
|
*/
|
|
numOctaves?: number;
|
|
/**
|
|
* Render the wrapper as this element type.
|
|
* @default "span"
|
|
*/
|
|
as?: "span" | "div";
|
|
}
|
|
|
|
export function SquigglyText({
|
|
children,
|
|
steps = 5,
|
|
stepDuration = 80,
|
|
scale = [6, 8],
|
|
baseFrequency = 0.02,
|
|
numOctaves = 3,
|
|
as = "span",
|
|
className,
|
|
style,
|
|
}: SquigglyTextProps) {
|
|
const reactId = useId();
|
|
// useId can produce ":" / "_" which aren't valid in CSS url(#…) refs.
|
|
const safeId = reactId.replace(/[:_]/g, "");
|
|
const filterId = (i: number) => `squiggly-${safeId}-${i}`;
|
|
|
|
const filters = React.useMemo(
|
|
() => Array.from({ length: steps }, (_, i) => `url(#${filterId(i)})`),
|
|
// filterId is stable per instance
|
|
[steps, safeId]
|
|
);
|
|
|
|
const time = useTime();
|
|
const filter = useTransform(time, (t) => filters[Math.floor(t / stepDuration) % filters.length]);
|
|
|
|
const scaleAt = (i: number) => (Array.isArray(scale) ? scale[i % scale.length] : scale);
|
|
|
|
const Wrapper = as === "div" ? motion.div : motion.span;
|
|
|
|
return (
|
|
<Wrapper style={{ filter, ...style }} className={cn("inline-block", className)}>
|
|
<svg
|
|
aria-hidden
|
|
className="pointer-events-none absolute h-0 w-0 overflow-hidden"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<defs>
|
|
{Array.from({ length: steps }).map((_, i) => (
|
|
<filter id={filterId(i)} key={i}>
|
|
<feTurbulence
|
|
baseFrequency={baseFrequency}
|
|
numOctaves={numOctaves}
|
|
result="noise"
|
|
seed={i}
|
|
/>
|
|
<feDisplacementMap in="SourceGraphic" in2="noise" scale={scaleAt(i)} />
|
|
</filter>
|
|
))}
|
|
</defs>
|
|
</svg>
|
|
{children}
|
|
</Wrapper>
|
|
);
|
|
}
|