{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "glyph-katakana", "title": "Glyph Katakana", "description": "Glyph katakana component with customizable animation. | カスタマイズ可能なアニメーションを備えたグリフ片仮名コンポーネント。", "dependencies": [ "motion" ], "files": [ { "path": "components/text/glyph-katakana.tsx", "content": "// KatakanaTitle.tsx\n\"use client\";\nimport React, { useEffect, useRef } from \"react\";\nimport { useReducedMotion, useSpring } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst glyphs = [\n \"ア\",\n \"イ\",\n \"ウ\",\n \"エ\",\n \"オ\",\n \"カ\",\n \"キ\",\n \"ク\",\n \"ケ\",\n \"コ\",\n \"サ\",\n \"シ\",\n \"ス\",\n \"セ\",\n \"ソ\",\n \"タ\",\n \"チ\",\n \"ツ\",\n \"テ\",\n \"ト\",\n \"ナ\",\n \"ニ\",\n \"ヌ\",\n \"ネ\",\n \"ノ\",\n \"ハ\",\n \"ヒ\",\n \"フ\",\n \"ヘ\",\n \"ホ\",\n \"マ\",\n \"ミ\",\n \"ム\",\n \"メ\",\n \"モ\",\n \"ヤ\",\n \"ユ\",\n \"ヨ\",\n \"ー\",\n \"ラ\",\n \"リ\",\n \"ル\",\n \"レ\",\n \"ロ\",\n \"ワ\",\n \"ヰ\",\n \"ヱ\",\n \"ヲ\",\n \"ン\",\n \"ガ\",\n \"ギ\",\n \"グ\",\n \"ゲ\",\n \"ゴ\",\n \"ザ\",\n \"ジ\",\n \"ズ\",\n \"ゼ\",\n \"ゾ\",\n \"ダ\",\n \"ヂ\",\n \"ヅ\",\n \"デ\",\n \"ド\",\n \"バ\",\n \"ビ\",\n \"ブ\",\n \"ベ\",\n \"ボ\",\n \"パ\",\n \"ピ\",\n \"プ\",\n \"ペ\",\n \"ポ\",\n];\n\n/** Module-level Tailwind classes for glyph/value states */\nconst CHAR_CLASSES: Record = {\n glyph: \"opacity-50 transition-all\",\n value: \"transition-all\",\n};\n\nenum CharType {\n Glyph = \"glyph\",\n Value = \"value\",\n}\n\ninterface CharItem {\n type: CharType;\n value: string;\n}\n\ninterface GlyphKatakanaProps extends React.HTMLAttributes {\n /** 目標文字,需為片假名 */\n text: string;\n /** 是否啟動動畫 (預設 true) */\n start?: boolean;\n /** 動畫開始前的延遲時間 (毫秒,預設 0) */\n delay?: number;\n}\n\n/**\n * 將輸入的文字陣列轉換為狀態陣列,\n * 到位的字元顯示真實文字,其他則隨機取自 glyphs\n */\nfunction shuffle(\n content: string[],\n current: CharItem[],\n position: number\n): CharItem[] {\n return content.map((char, index) => {\n if (index < position) {\n return { type: CharType.Value, value: char };\n }\n // 產生隨機的片假名\n const rand = Math.floor(Math.random() * glyphs.length);\n return { type: CharType.Glyph, value: glyphs[rand] };\n });\n}\n\n/**\n * KatakanaTitle - 組件載入時以解碼動畫效果,由隨機片假名逐步呈現正確文字\n */\nconst GlyphKatakana: React.FC = (\n ({ text, start = true, delay = 0, className, ...props }) => {\n const containerRef = useRef(null);\n const reduceMotion = useReducedMotion();\n const decoderSpring = useSpring(0, { stiffness: 8, damping: 4 });\n const content = text.split(\"\");\n const output = useRef(\n content.map(() => ({ type: CharType.Glyph, value: \"\" }))\n );\n const maxPositionRef = useRef(0);\n const cancelledRef = useRef(false);\n\n // 直接操作 innerHTML 將各字元以 span 包裹\n const renderOutput = () => {\n if (containerRef.current) {\n const html = output.current\n .map(\n (item) =>\n `${item.value}`\n )\n .join(\"\");\n containerRef.current.innerHTML = html;\n }\n };\n\n useEffect(() => {\n cancelledRef.current = false;\n maxPositionRef.current = 0;\n\n const unsubscribe = decoderSpring.on(\"change\", (value: number) => {\n const rawPos = Math.floor(value);\n const pos = Math.min(content.length, Math.max(0, rawPos));\n maxPositionRef.current = Math.max(maxPositionRef.current, pos);\n output.current = shuffle(content, output.current, maxPositionRef.current);\n renderOutput();\n });\n\n const startAnimation = async () => {\n await new Promise((resolve) => setTimeout(resolve, delay));\n if (cancelledRef.current) return;\n decoderSpring.set(content.length);\n };\n\n if (start && !reduceMotion) {\n decoderSpring.jump(0); // Reset for fresh animation (handles text change replay)\n startAnimation();\n } else {\n output.current = content.map((char) => ({\n type: CharType.Value,\n value: char,\n }));\n renderOutput();\n }\n\n return () => {\n cancelledRef.current = true;\n unsubscribe();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [decoderSpring, reduceMotion, start, delay, text]);\n\n return (\n \n \n \n );\n }\n);\n\nexport default GlyphKatakana;\n", "type": "registry:ui" } ], "type": "registry:ui" }