{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"colourful-text","type":"registry:ui","title":"ColourfulText","description":"Per-character text animation where each glyph transitions to a new color from a shuffled palette, staggered across characters and reshuffling automatically every 5 seconds using pure CSS transitions","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["text"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/colourful-text/ColourfulText.tsx","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\n\n/**\n * ColourfulText - Per-character color animation\n *\n * Each character is individually animated with shuffled colors.\n * Colors reshuffle every 5 seconds with staggered CSS transitions.\n * Pure CSS implementation (no motion library dependency).\n */\nexport interface ColourfulTextProps {\n\t/** Text to animate */\n\ttext: string;\n\t/** Array of colors to cycle through */\n\tcolors?: string[];\n\t/** Initial color before animation */\n\tstartColor?: string;\n\t/** Transition duration in seconds for each character */\n\tduration?: number;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Seed for the color shuffle. The Svelte source shuffles with\n\t * `Math.random()` at component init; here the initial shuffle must be\n\t * identical between the server render and its hydration, so it draws from\n\t * a deterministic stream instead. The same seed always deals the same\n\t * initial order — change it to make two instances start differently.\n\t */\n\tseed?: number;\n}\n\nconst DEFAULT_COLORS = [\n\t\"rgb(131, 179, 32)\",\n\t\"rgb(47, 195, 106)\",\n\t\"rgb(42, 169, 210)\",\n\t\"rgb(4, 112, 202)\",\n\t\"rgb(107, 10, 255)\",\n\t\"rgb(183, 0, 218)\",\n\t\"rgb(218, 0, 171)\",\n\t\"rgb(230, 64, 92)\",\n\t\"rgb(232, 98, 63)\",\n\t\"rgb(249, 129, 47)\",\n];\n\n/**\n * mulberry32 — a tiny deterministic PRNG. `Math.random()` cannot be used in\n * the initial shuffle: the `useState` initializer runs once on the server and\n * again during client hydration, and the two would disagree on every color,\n * which React reports as a hydration mismatch and may resolve by keeping the\n * SERVER attributes. A seed keeps both renders identical.\n */\nfunction mulberry32(seed: number): () => number {\n\tlet state = seed >>> 0;\n\treturn () => {\n\t\tstate = (state + 0x6d2b79f5) >>> 0;\n\t\tlet t = state;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\n/**\n * Fisher-Yates over the seeded stream.\n *\n * DIVERGENCE from the Svelte source, which shuffles with\n * `[...colors].sort(() => 0.5 - Math.random())`. That comparator is\n * inconsistent, so `Array.prototype.sort` consumes a different number of draws\n * in a different order depending on the engine's sort algorithm. Seeding the\n * stream would therefore still leave the RESULT engine-dependent: a server\n * render on Node and its hydration in another browser would deal different\n * colors and reintroduce exactly the mismatch the seed exists to remove.\n * Fisher-Yates consumes `length - 1` draws in a fixed order, so the same seed\n * yields the same permutation on every engine. The dealt order differs\n * slightly from the source's biased shuffle; the visual behaviour (a shuffled\n * palette, reshuffled every 5s) is unchanged.\n */\nfunction shuffleColors(colors: string[], random: () => number): string[] {\n\tconst out = [...colors];\n\tfor (let i = out.length - 1; i > 0; i--) {\n\t\tconst j = Math.floor(random() * (i + 1));\n\t\t[out[i], out[j]] = [out[j]!, out[i]!];\n\t}\n\treturn out;\n}\n\nexport function ColourfulText({\n\ttext,\n\tcolors = DEFAULT_COLORS,\n\tstartColor = \"rgb(255, 255, 255)\",\n\tduration = 0.5,\n\tclassName,\n\tseed = 1,\n}: ColourfulTextProps) {\n\t// Initial shuffle, once at init — the source shuffles synchronously before\n\t// the first render. A later `colors` change takes effect on the next\n\t// interval tick, exactly as in the source.\n\tconst [currentColors, setCurrentColors] = useState<string[]>(() =>\n\t\tshuffleColors(colors, mulberry32(seed)),\n\t);\n\n\t// The interval reads the CURRENT `colors` prop on every tick (the source's\n\t// closure reads the reactive prop) without restarting the timer.\n\tconst colorsRef = useLiveRef(colors);\n\n\tuseEffect(() => {\n\t\t// Client-only reshuffles. XORing the seed decorrelates this stream from\n\t\t// the initial shuffle's — replaying the same draws on the same array\n\t\t// would make the first 5s reshuffle a visual no-op.\n\t\tconst random = mulberry32(seed ^ 0x9e3779b9);\n\t\tconst intervalId = setInterval(() => {\n\t\t\tif (document.visibilityState === \"visible\") {\n\t\t\t\tsetCurrentColors(shuffleColors(colorsRef.current, random));\n\t\t\t}\n\t\t}, 5000);\n\n\t\treturn () => clearInterval(intervalId);\n\t}, [seed, colorsRef]);\n\n\tconst chars = text.split(\"\");\n\n\treturn (\n\t\t<span className={cn(\"colourful-text inline-flex\", className)}>\n\t\t\t{chars.map((char, i) => (\n\t\t\t\t<span\n\t\t\t\t\tkey={i}\n\t\t\t\t\tclassName=\"colourful-char inline-block\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tcolor: currentColors[i % currentColors.length] ?? startColor,\n\t\t\t\t\t\ttransition: `color ${duration}s ease ${i * 0.05}s,opacity ${duration}s ease ${\n\t\t\t\t\t\t\ti * 0.05\n\t\t\t\t\t\t}s,transform ${duration}s ease ${i * 0.05}s,filter ${duration}s ease ${i * 0.05}s`,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{char === \" \" ? \"\\u00A0\" : char}\n\t\t\t\t</span>\n\t\t\t))}\n\t\t</span>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/colourful-text/ColourfulText.tsx"},{"path":"react/src/components/colourful-text/index.ts","content":"\"use client\";\n\nexport { ColourfulText } from \"./ColourfulText\";\nexport type { ColourfulTextProps } from \"./ColourfulText\";\n","type":"registry:ui","target":"@ui/fancy/components/colourful-text/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/colourful-text?framework=react"}}