{"slug":"text-reveal","name":"Text Reveal","description":"Word or character reveal with spring slide-up and blur.","category":"motion","source_url":"https://beui.saura3h.xyz/r/text-reveal/raw","detail_url":"https://beui.saura3h.xyz/r/text-reveal","raw_url":"https://beui.saura3h.xyz/r/text-reveal/raw","page_url":"https://beui.saura3h.xyz/components/motion/text-reveal","dependencies":["motion","react"],"internal":["@/components/motion/text-reveal","@/lib/utils"],"files":[{"path":"components/motion/text-reveal.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef, type ElementType, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype SplitMode = \"word\" | \"char\";\n\nexport interface TextRevealProps {\n  text: string | string[];\n  as?: ElementType;\n  className?: string;\n  split?: SplitMode;\n  stagger?: number;\n  delay?: number;\n  blur?: number;\n  yOffset?: string | number;\n  spring?: { stiffness?: number; damping?: number; mass?: number };\n  once?: boolean;\n  whileInView?: boolean;\n  children?: ReactNode;\n}\n\nconst DEFAULT_SPRING = { stiffness: 140, damping: 26, mass: 1.2 };\n\nexport function TextReveal({\n  text,\n  as: Comp = \"span\",\n  className,\n  split = \"word\",\n  stagger = 0.09,\n  delay = 0,\n  blur = 12,\n  yOffset = \"40%\",\n  spring,\n  once = true,\n  whileInView = false,\n  children,\n}: TextRevealProps) {\n  const ref = useRef<HTMLElement>(null);\n  const inView = useInView(ref, { once, amount: 0.4 });\n  const reduce = useReducedMotion();\n  const shouldAnimate = whileInView ? inView : true;\n\n  const lines = Array.isArray(text) ? text : [text];\n  const s = { ...DEFAULT_SPRING, ...spring };\n\n  let unitIndex = 0;\n\n  return (\n    <Comp ref={ref} className={cn(\"block\", className)}>\n      {lines.map((line, li) => {\n        const units = split === \"word\" ? line.split(\" \") : Array.from(line);\n        return (\n          <span key={`${line}-${li}`} className=\"block\">\n            {units.map((unit, i) => {\n              const d = delay + unitIndex * stagger;\n              unitIndex += 1;\n              const initial = reduce\n                ? { opacity: 0 }\n                : { y: yOffset, opacity: 0, filter: `blur(${blur}px)` };\n              const animate = shouldAnimate\n                ? reduce\n                  ? { opacity: 1 }\n                  : { y: 0, opacity: 1, filter: \"blur(0px)\" }\n                : initial;\n              const transition = reduce\n                ? { opacity: { duration: 0.25, ease: [0.16, 1, 0.3, 1], delay: d * 0.3 } }\n                : {\n                    y: { type: \"spring\" as const, ...s, delay: d },\n                    opacity: { duration: 0.7, ease: [0.16, 1, 0.3, 1], delay: d },\n                    filter: { duration: 0.9, ease: [0.16, 1, 0.3, 1], delay: d },\n                  };\n              return (\n                <motion.span\n                  key={`${unit}-${i}`}\n                  initial={initial}\n                  animate={animate}\n                  transition={transition}\n                  className=\"inline-block will-change-transform\"\n                >\n                  {unit}\n                  {split === \"word\" && i < units.length - 1 ? (\n                    <span className=\"inline-block\">&nbsp;</span>\n                  ) : null}\n                </motion.span>\n              );\n            })}\n          </span>\n        );\n      })}\n      {children}\n    </Comp>\n  );\n}\n"},{"path":"components/previews/motion/text-reveal.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { TextReveal } from \"@/components/motion/text-reveal\";\n\nexport function TextRevealPreview() {\n  const [key, setKey] = useState(0);\n  return (\n    <div className=\"flex w-full flex-col items-center gap-8 text-center\">\n      <div key={key} className=\"flex flex-col gap-2\">\n        <TextReveal\n          as=\"h2\"\n          text={[\"Motion that feels\", \"considered.\"]}\n          className=\"text-balance text-4xl font-semibold leading-[0.95] tracking-[-0.04em] text-(--color-fg) sm:text-5xl\"\n        />\n        <TextReveal\n          text=\"Word by word, with a soft blur.\"\n          delay={0.9}\n          stagger={0.05}\n          blur={6}\n          yOffset=\"20%\"\n          className=\"text-sm text-(--color-fg-muted)\"\n        />\n      </div>\n\n      <button\n        type=\"button\"\n        onClick={() => setKey((k) => k + 1)}\n        className=\"inline-flex h-9 items-center rounded-full border border-(--color-border) bg-(--color-bg-elev) px-4 text-xs font-medium text-(--color-fg) press hover:border-(--color-border-strong)\"\n      >\n        Replay\n      </button>\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}