{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shimmer-text",
  "type": "registry:ui",
  "title": "Shimmer Text",
  "description": "A working status whose sheen sweeps the letters and crossfades between steps.",
  "categories": [
    "ai-chat"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/shimmer-text.tsx",
      "content": "\"use client\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\n\nexport type ShimmerTextProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** The status to show: \"Thinking…\", \"Searching the web…\". Changing it crossfades to the new label. */\n  children: string;\n  /** Sweep a sheen across the text. Turn it off when the work is done and the label becomes a plain record. */\n  active?: boolean;\n  /** Seconds for one full sweep, pause included. */\n  duration?: number;\n};\n\n// currentColor is the resting tone; the soft band in the middle is the foreground.\n// At 250% size the band crosses the text in about a third of each cycle, which\n// leaves a beat of rest between sweeps. It runs the other way in right-to-left text.\n// Reduced motion drops the sweep and shows the plain label.\nconst sheen = cn(\n  \"bg-[linear-gradient(90deg,currentColor_0%,currentColor_40%,var(--fg)_50%,currentColor_60%,currentColor_100%)]\",\n  \"[background-size:250%_100%] bg-clip-text [-webkit-text-fill-color:transparent] animate-shine rtl:[animation-direction:reverse]\",\n  \"motion-reduce:animate-none motion-reduce:bg-none motion-reduce:[-webkit-text-fill-color:currentColor]\",\n);\n\n/**\n * A one-line status label with a sheen moving across the letters. The sheen is a\n * gradient clipped to the text: the resting colour is the element's own colour\n * (set it with a text class), the highlight is the foreground. Only the label\n * shimmers, never a paragraph.\n */\nexport function ShimmerText({ children, active = true, duration = 2, className, style, ref, ...rest }: ShimmerTextProps) {\n  const reduce = useReducedMotion();\n  const rootRef = useRef<HTMLSpanElement | null>(null);\n  const sizerRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const [visible, setVisible] = useState(true);\n\n  // The width follows the current label so whatever sits after it (a timer, a\n  // chevron) glides to its new place instead of jumping. An invisible copy that\n  // never wraps is measured, so the animation can't feed back into the reading.\n  useEffect(() => {\n    const el = sizerRef.current;\n    if (!el) return;\n    const ro = new ResizeObserver(([entry]) => setWidth(Math.ceil(entry.borderBoxSize?.[0]?.inlineSize ?? el.offsetWidth)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  // The sweep repaints every frame, so it stops while the label is off screen.\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el || !active) return;\n    const io = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting));\n    io.observe(el);\n    return () => io.disconnect();\n  }, [active]);\n\n  const enter = reduce ? { opacity: 0 } : { opacity: 0, y: 5, filter: \"blur(2px)\" };\n  const leave = reduce ? { opacity: 0 } : { opacity: 0, y: -5, filter: \"blur(2px)\" };\n\n  return (\n    <span\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      data-state={active ? \"active\" : \"idle\"}\n      className={cn(\n        \"relative inline-grid max-w-full overflow-x-clip whitespace-nowrap text-fg-2\",\n        \"transition-[width] duration-300 ease-in-out-quart motion-reduce:transition-none\",\n        className,\n      )}\n      style={{ width, ...style }}\n      {...rest}\n    >\n      <span ref={sizerRef} aria-hidden className=\"pointer-events-none invisible absolute left-0 top-0 w-max\">\n        {children}\n      </span>\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={children}\n          aria-hidden\n          className={cn(\n            \"col-start-1 row-start-1 min-w-0 truncate\",\n            active && sheen,\n          )}\n          style={active ? { animationDuration: `${duration}s`, animationPlayState: visible ? \"running\" : \"paused\" } : undefined}\n          initial={enter}\n          animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={{ ...leave, transition: { duration: reduce ? 0.12 : 0.16, ease: ease.in } }}\n          transition={{ duration: reduce ? 0.16 : 0.26, ease: ease.out }}\n        >\n          {children}\n        </motion.span>\n      </AnimatePresence>\n      {/* The visible labels are decorative copies; this is what assistive tech reads and announces. */}\n      <span className=\"sr-only\">{children}</span>\n    </span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/shimmer-text"
}
