{"slug":"not-found","name":"404 / Not Found","description":"Animated 404 pages in five styles: glitch scramble, magnetic digits, cursor spotlight, a fanning card stack and a typed terminal.","category":"blocks","source_url":"https://beui.dev/r/not-found/raw","detail_url":"https://beui.dev/r/not-found","raw_url":"https://beui.dev/r/not-found/raw","page_url":"https://beui.dev/components/blocks/not-found","dependencies":["clsx","motion","react","tailwind-merge"],"internal":["./glitch","./magnetic","./shared","./spotlight","./stacked","./terminal","@/components/motion/magnetic","@/components/motion/text-reveal","@/lib/ease","@/lib/hooks/use-hover-capable","@/lib/utils"],"files":[{"path":"components/motion/not-found/index.tsx","type":"component","content":"export { NotFoundGlitch } from \"./glitch\";\nexport { NotFoundMagnetic } from \"./magnetic\";\nexport { NotFoundSpotlight } from \"./spotlight\";\nexport { NotFoundStacked } from \"./stacked\";\nexport { NotFoundTerminal } from \"./terminal\";\nexport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n"},{"path":"components/motion/not-found/shared.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NotFoundProps {\n  className?: string;\n  /** The big status code. */\n  code?: string;\n  title?: string;\n  description?: string;\n  homeHref?: string;\n  homeLabel?: string;\n  browseHref?: string;\n  browseLabel?: string;\n}\n\nexport const NOT_FOUND_DEFAULTS = {\n  code: \"404\",\n  title: \"Page not found\",\n  description:\n    \"The page you are looking for moved, vanished, or never existed.\",\n  homeHref: \"/\",\n  homeLabel: \"Back home\",\n  browseHref: \"/components/motion\",\n  browseLabel: \"Browse components\",\n} as const;\n\ntype ActionsProps = Pick<\n  NotFoundProps,\n  \"homeHref\" | \"homeLabel\" | \"browseHref\" | \"browseLabel\" | \"className\"\n>;\n\n/** The shared dual CTA: a primary \"Back home\" and a secondary \"Browse\". */\nexport function NotFoundActions({\n  homeHref = NOT_FOUND_DEFAULTS.homeHref,\n  homeLabel = NOT_FOUND_DEFAULTS.homeLabel,\n  browseHref = NOT_FOUND_DEFAULTS.browseHref,\n  browseLabel = NOT_FOUND_DEFAULTS.browseLabel,\n  className,\n}: ActionsProps) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const whileTap = reduce ? undefined : { scale: 0.96 };\n  const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center justify-center gap-3\",\n        className,\n      )}\n    >\n      <motion.a\n        href={homeHref}\n        whileTap={whileTap}\n        whileHover={whileHover}\n        transition={SPRING_PRESS}\n        className=\"inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90\"\n      >\n        {homeLabel}\n      </motion.a>\n      <motion.a\n        href={browseHref}\n        whileTap={whileTap}\n        whileHover={whileHover}\n        transition={SPRING_PRESS}\n        className=\"inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5\"\n      >\n        {browseLabel}\n      </motion.a>\n    </div>\n  );\n}\n\n/** Centers a variant and gives it a consistent minimum stage height. */\nexport function NotFoundStage({\n  className,\n  children,\n}: {\n  className?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n"},{"path":"components/motion/not-found/glitch.tsx","type":"component","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nconst GLYPHS = \"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&@$?/\\\\\";\nconst SCRAMBLE_MS = 700;\nconst TICK_MS = 45;\n\n/**\n * Renders the code, scrambling each character on mount before it settles.\n * SSR and the first paint show the real code, so the scramble is a pure\n * client-side enhancement and reduced-motion users see the code immediately.\n */\nfunction Scramble({ text }: { text: string }) {\n  const reduce = useReducedMotion();\n  const [display, setDisplay] = useState(text);\n\n  useEffect(() => {\n    if (reduce) {\n      setDisplay(text);\n      return;\n    }\n    const chars = text.split(\"\");\n    const start = performance.now();\n    let raf = 0;\n    let last = 0;\n\n    const loop = (now: number) => {\n      if (now - last >= TICK_MS) {\n        last = now;\n        const progress = Math.min((now - start) / SCRAMBLE_MS, 1);\n        const settled = Math.floor(progress * chars.length);\n        setDisplay(\n          chars\n            .map((ch, i) =>\n              i < settled || ch === \" \"\n                ? ch\n                : GLYPHS[Math.floor(Math.random() * GLYPHS.length)],\n            )\n            .join(\"\"),\n        );\n      }\n      if (now - start < SCRAMBLE_MS) {\n        raf = requestAnimationFrame(loop);\n      } else {\n        setDisplay(text);\n      }\n    };\n    raf = requestAnimationFrame(loop);\n    return () => cancelAnimationFrame(raf);\n  }, [text, reduce]);\n\n  return <span className=\"tabular-nums\">{display}</span>;\n}\n\nexport function NotFoundGlitch({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  return (\n    <NotFoundStage className={className}>\n      <div className=\"group relative select-none font-mono font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,11rem)]\">\n        {/* Chromatic ghost layers, nudged apart on hover. */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 text-[#ff0040] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden\"\n        >\n          <Scramble text={code} />\n        </span>\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 text-[#00e5ff] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:-translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden\"\n        >\n          <Scramble text={code} />\n        </span>\n        <h1 className=\"relative\">\n          <Scramble text={code} />\n        </h1>\n      </div>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"components/motion/not-found/magnetic.tsx","type":"component","content":"\"use client\";\n\nimport { Magnetic } from \"@/components/motion/magnetic\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nexport function NotFoundMagnetic({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  const chars = code.split(\"\");\n\n  return (\n    <NotFoundStage className={className}>\n      <h1\n        aria-label={code}\n        className=\"flex select-none items-center justify-center font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,12rem)]\"\n      >\n        {chars.map((ch, i) => (\n          <Magnetic\n            // biome-ignore lint/suspicious/noArrayIndexKey: fixed positional glyphs\n            key={i}\n            strength={0.6}\n            className={cn(i > 0 && \"-ml-2\")}\n          >\n            <span aria-hidden className=\"inline-block px-1 tabular-nums\">\n              {ch}\n            </span>\n          </Magnetic>\n        ))}\n      </h1>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"components/motion/not-found/spotlight.tsx","type":"component","content":"\"use client\";\n\nimport { useRef } from \"react\";\nimport {\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nexport function NotFoundSpotlight({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const enabled = !reduce && canHover;\n\n  const mx = useMotionValue(50);\n  const my = useMotionValue(50);\n  const mask = useMotionTemplate`radial-gradient(220px circle at ${mx}% ${my}%, #000 25%, transparent 72%)`;\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    mx.set(((e.clientX - rect.left) / rect.width) * 100);\n    my.set(((e.clientY - rect.top) / rect.height) * 100);\n  };\n\n  return (\n    <NotFoundStage className={className}>\n      <motion.div\n        ref={ref}\n        onMouseMove={onMove}\n        className=\"relative isolate flex aspect-[16/9] w-full max-w-xl items-center justify-center overflow-hidden rounded-3xl border border-border bg-neutral-950\"\n      >\n        {/* Dim base layer. */}\n        <span\n          aria-hidden\n          className=\"select-none font-bold leading-none tracking-tighter text-white/10 [font-size:clamp(5rem,16vw,10rem)]\"\n        >\n          {code}\n        </span>\n        {/* Bright layer, revealed only under the spotlight. */}\n        <motion.h1\n          aria-label={code}\n          style={enabled ? { WebkitMaskImage: mask, maskImage: mask } : undefined}\n          className={cn(\n            \"absolute select-none font-bold leading-none tracking-tighter text-white [font-size:clamp(5rem,16vw,10rem)]\",\n            !enabled && \"text-white/90\",\n          )}\n        >\n          <span aria-hidden>{code}</span>\n        </motion.h1>\n      </motion.div>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"components/motion/not-found/stacked.tsx","type":"component","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { SPRING_PANEL } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nconst CARD =\n  \"absolute inset-0 rounded-3xl border border-border bg-card shadow-sm\";\n\nexport function NotFoundStacked({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const interactive = !reduce && canHover;\n\n  return (\n    <NotFoundStage className={className}>\n      <motion.div\n        initial=\"rest\"\n        animate=\"rest\"\n        whileHover={interactive ? \"hover\" : undefined}\n        className=\"relative h-44 w-64\"\n      >\n        <motion.div\n          aria-hidden\n          variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: -9, x: -28, y: 8 } }}\n          transition={SPRING_PANEL}\n          className={CARD}\n        />\n        <motion.div\n          aria-hidden\n          variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: 9, x: 28, y: 8 } }}\n          transition={SPRING_PANEL}\n          className={CARD}\n        />\n        <motion.div\n          variants={{ rest: { y: 0 }, hover: { y: -6 } }}\n          transition={SPRING_PANEL}\n          className=\"absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-3xl border border-border bg-card shadow-md\"\n        >\n          <h1 className=\"select-none font-bold leading-none tracking-tighter text-foreground [font-size:clamp(3.5rem,9vw,5rem)]\">\n            {code}\n          </h1>\n          <span className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n            out of the deck\n          </span>\n        </motion.div>\n      </motion.div>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"components/motion/not-found/terminal.tsx","type":"component","content":"\"use client\";\n\nimport { TextReveal } from \"@/components/motion/text-reveal\";\nimport {\n  NOT_FOUND_DEFAULTS,\n  NotFoundActions,\n  NotFoundStage,\n  type NotFoundProps,\n} from \"./shared\";\n\nconst TYPE_SPRING = { stiffness: 320, damping: 30, mass: 0.6 };\n\nexport function NotFoundTerminal({\n  className,\n  code = NOT_FOUND_DEFAULTS.code,\n  title = NOT_FOUND_DEFAULTS.title,\n  description = NOT_FOUND_DEFAULTS.description,\n  homeHref,\n  homeLabel,\n  browseHref,\n  browseLabel,\n}: NotFoundProps) {\n  return (\n    <NotFoundStage className={className}>\n      <div className=\"w-full max-w-md overflow-hidden rounded-xl border border-border bg-neutral-950 text-left shadow-lg\">\n        <div className=\"flex items-center gap-1.5 border-b border-white/10 px-4 py-3\">\n          <span className=\"h-3 w-3 rounded-full bg-[#ff5f57]\" />\n          <span className=\"h-3 w-3 rounded-full bg-[#febc2e]\" />\n          <span className=\"h-3 w-3 rounded-full bg-[#28c840]\" />\n          <span className=\"ml-2 text-xs text-white/40\">~/beui</span>\n        </div>\n        <div className=\"space-y-1.5 p-4 font-mono text-sm leading-relaxed\">\n          <TextReveal\n            as=\"p\"\n            split=\"char\"\n            stagger={0.018}\n            blur={6}\n            yOffset={0}\n            spring={TYPE_SPRING}\n            className=\"text-white/80\"\n            text=\"$ cd /page\"\n          />\n          <TextReveal\n            as=\"p\"\n            split=\"char\"\n            stagger={0.012}\n            delay={0.45}\n            blur={6}\n            yOffset={0}\n            spring={TYPE_SPRING}\n            className=\"text-[#ff5f57]\"\n            text=\"cd: no such file or directory: /page\"\n          />\n          <p className=\"flex items-center text-white/80\">\n            <TextReveal\n              as=\"span\"\n              split=\"char\"\n              stagger={0.018}\n              delay={1.1}\n              blur={6}\n              yOffset={0}\n              spring={TYPE_SPRING}\n              text={`$ status ${code}`}\n            />\n            <span className=\"ml-1 inline-block h-[1.1em] w-[0.55ch] translate-y-[0.12em] bg-white/80 motion-safe:animate-pulse\" />\n          </p>\n        </div>\n      </div>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <p className=\"text-lg font-semibold text-foreground\">{title}</p>\n        <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>\n      </div>\n\n      <NotFoundActions\n        homeHref={homeHref}\n        homeLabel={homeLabel}\n        browseHref={browseHref}\n        browseLabel={browseLabel}\n      />\n    </NotFoundStage>\n  );\n}\n"},{"path":"lib/ease.ts","type":"util","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"util","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\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"},{"path":"components/motion/magnetic.tsx","type":"util","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/text-reveal.tsx","type":"util","content":"\"use client\";\n\nimport { motion, type Transition, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef, type ElementType, type ReactNode } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\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  const lineCounts = new Map<string, number>();\n\n  return (\n    <Comp ref={ref} className={cn(\"block\", className)}>\n      {lines.map((line) => {\n        const units = split === \"word\" ? line.split(\" \") : Array.from(line);\n        const lineCount = lineCounts.get(line) ?? 0;\n        lineCounts.set(line, lineCount + 1);\n        const lineKey = `${line}-${lineCount}`;\n        const unitCounts = new Map<string, number>();\n\n        return (\n          <span key={lineKey} className=\"block\">\n            {units.map((unit, i) => {\n              const d = delay + unitIndex * stagger;\n              unitIndex += 1;\n              const unitCount = unitCounts.get(unit) ?? 0;\n              unitCounts.set(unit, unitCount + 1);\n              const unitKey = `${unit}-${unitCount}`;\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: Transition = reduce\n                ? { opacity: { duration: 0.25, ease: EASE_OUT, delay: d * 0.3 } }\n                : {\n                    y: { type: \"spring\" as const, ...s, delay: d },\n                    opacity: { duration: 0.7, ease: EASE_OUT, delay: d },\n                    filter: { duration: 0.9, ease: EASE_OUT, delay: d },\n                  };\n              return (\n                <motion.span\n                  key={unitKey}\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"}]}