{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rolling-text",
  "title": "Rolling Text",
  "description": "Rolling text component",
  "files": [
    {
      "path": "components/ui/rolling-text.tsx",
      "content": "import { cx } from 'class-variance-authority'\nimport { type ComponentProps, useState } from 'react'\n\nimport { cn } from '@/lib/cn'\n\nimport { prefersReducedMotion } from '@/utils/prefers-reduced-motion'\n\nexport type RollingTextProps = {\n  children: string | number\n} & ComponentProps<'span'>\n\nconst rollLine = cx('col-start-1 row-start-1 whitespace-nowrap')\nconst rollEnter = cx(\n  rollLine,\n  'animate-in fade-in slide-in-from-bottom duration-300 [--tw-ease:cubic-bezier(0.22,1,0.36,1)]'\n)\n// fill-mode-forwards holds the exit's end state so the outgoing line can't flash back before it unmounts.\nconst rollExit = cx(\n  rollLine,\n  'animate-out fade-out slide-out-to-top fill-mode-forwards duration-300 [--tw-ease:cubic-bezier(0.22,1,0.36,1)]'\n)\n\n/** A single line of text that vertically rolls—old line up and out, new in from below—whenever the value changes. */\nfunction RollingText({ children, className, ...props }: RollingTextProps) {\n  const reduced = prefersReducedMotion()\n  const [current, setCurrent] = useState(children)\n  const [previous, setPrevious] = useState<string | number | null>(null)\n\n  // Adjust during render (guarded) so the outgoing/incoming lines mount together, flash-free.\n  if (children !== current) {\n    if (!reduced) setPrevious(current)\n    setCurrent(children)\n  }\n\n  return (\n    <span data-slot='rolling-text' className={cn('grid overflow-hidden', className)} {...props}>\n      {previous !== null && (\n        <span key={`prev-${previous}`} className={rollExit} onAnimationEnd={() => setPrevious(null)}>\n          {previous}\n        </span>\n      )}\n      <span key={`cur-${current}`} className={reduced ? rollLine : rollEnter}>\n        {current}\n      </span>\n    </span>\n  )\n}\n\nexport { RollingText }\n",
      "type": "registry:component"
    },
    {
      "path": "lib/cn.ts",
      "content": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "utils/prefers-reduced-motion.ts",
      "content": "/**\n * Shared OS \"Reduce Motion\" check for all WAAPI animations in the app.\n * The `MediaQueryList` is cached lazily but `.matches` stays live, so toggling the\n * OS setting mid-session is picked up on the next call; SSR/no-`matchMedia` defaults to `false`.\n */\nlet reducedMotionMql: MediaQueryList | null | undefined\n\nexport function prefersReducedMotion(): boolean {\n  if (reducedMotionMql === undefined) {\n    reducedMotionMql =\n      typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n        ? window.matchMedia('(prefers-reduced-motion: reduce)')\n        : null\n  }\n  return reducedMotionMql?.matches ?? false\n}\n",
      "type": "registry:file",
      "target": "utils/prefers-reduced-motion.ts"
    }
  ],
  "type": "registry:component"
}