{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slide-up-text",
  "title": "Slide Up Text",
  "description": "Text animation that slides up from bottom with stagger effect.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/slide-up-text.tsx",
      "content": "\"use client\";\n\nimport { AnimationOptions, motion } from \"motion/react\";\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface SlideUpTextProps {\n  children: React.ReactNode;\n  split?: \"words\" | \"characters\" | \"lines\";\n  delay?: number;\n  stagger?: number;\n  from?: \"first\" | \"last\" | \"center\";\n  transition?: AnimationOptions;\n  className?: string;\n  wordClass?: string;\n  charClass?: string;\n  autoStart?: boolean;\n  onStart?: () => void;\n  onComplete?: () => void;\n  inView?: boolean;\n  once?: boolean;\n}\n\nexport interface SlideUpTextRef {\n  startAnimation: () => void;\n  reset: () => void;\n}\n\ninterface WordObject {\n  characters: string[];\n  needsSpace: boolean;\n}\n\nconst SlideUpText = forwardRef<SlideUpTextRef, SlideUpTextProps>(\n  (\n    {\n      children,\n      split = \"words\",\n      delay = 0,\n      stagger = 0.1,\n      from = \"first\",\n      transition = {\n        type: \"tween\",\n        ease: [0.625, 0.05, 0, 1],\n        duration: 0.5,\n      },\n      className,\n      wordClass,\n      charClass,\n      autoStart = true,\n      onStart,\n      onComplete,\n      inView = false,\n      once = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const text = typeof children === \"string\"\n      ? children\n      : children?.toString() || \"\";\n    const [isAnimating, setIsAnimating] = useState(false);\n\n    const splitIntoCharacters = (text: string): string[] => {\n      if (typeof Intl !== \"undefined\" && \"Segmenter\" in Intl) {\n        const segmenter = new Intl.Segmenter(\"en\", { granularity: \"grapheme\" });\n        return Array.from(segmenter.segment(text), ({ segment }) => segment);\n      }\n      return Array.from(text);\n    };\n\n    const elements = useMemo(() => {\n      const words = text.split(\" \");\n      if (split === \"characters\") {\n        return words.map((word, i) => ({\n          characters: splitIntoCharacters(word),\n          needsSpace: i !== words.length - 1,\n        }));\n      }\n      return split === \"words\" ? text.split(\" \") : text.split(\"\\n\");\n    }, [text, split]);\n\n    const getStaggerDelay = useCallback(\n      (index: number) => {\n        const total = split === \"characters\"\n          ? elements.reduce(\n            (acc, word) =>\n              acc +\n              (typeof word === \"string\"\n                ? 1\n                : word.characters.length + (word.needsSpace ? 1 : 0)),\n            0,\n          )\n          : elements.length;\n\n        if (from === \"first\") return index * stagger;\n        if (from === \"last\") return (total - 1 - index) * stagger;\n        if (from === \"center\") {\n          const center = Math.floor(total / 2);\n          return Math.abs(center - index) * stagger;\n        }\n        return index * stagger;\n      },\n      [elements, from, stagger, split],\n    );\n\n    const startAnimation = useCallback(() => {\n      setIsAnimating(true);\n      onStart?.();\n    }, [onStart]);\n\n    useImperativeHandle(ref, () => ({\n      startAnimation,\n      reset: () => setIsAnimating(false),\n    }));\n\n    useEffect(() => {\n      if (autoStart && !inView) {\n        startAnimation();\n      }\n    }, [autoStart, inView, startAnimation]);\n\n    const variants = {\n      hidden: { y: \"100%\" },\n      visible: (i: number) => ({\n        y: 0,\n        transition: {\n          ...transition,\n          delay: delay + ((transition?.delay as number) || 0) + getStaggerDelay(i),\n        },\n      }),\n    };\n\n    return (\n      <motion.span\n        className={cn(\n          className,\n          \"flex flex-wrap whitespace-pre-wrap\",\n          split === \"lines\" && \"flex-col\",\n        )}\n        initial=\"hidden\"\n        whileInView={inView ? \"visible\" : undefined}\n        animate={inView ? undefined : isAnimating ? \"visible\" : \"hidden\"}\n        viewport={{ once }}\n        onAnimationStart={() => {\n          if (inView) {\n            setIsAnimating(true);\n            onStart?.();\n          }\n        }}\n        {...props}\n      >\n        <span className=\"sr-only\">{text}</span>\n\n        {(split === \"characters\"\n          ? (elements as WordObject[])\n          : (elements as string[]).map((el, i, arr) => ({\n            characters: [el],\n            needsSpace: split === \"words\" && i !== arr.length - 1,\n          }))).map((wordObj, wordIndex, array) => {\n            const previousCharsCount = array\n              .slice(0, wordIndex)\n              .reduce((sum, word) => sum + word.characters.length, 0);\n\n            return (\n              <span\n                key={wordIndex}\n                aria-hidden=\"true\"\n                className={cn(\"inline-flex overflow-hidden\", wordClass)}\n              >\n                {wordObj.characters.map((char, charIndex) => (\n                  <span\n                    className={cn(\n                      charClass,\n                      \"whitespace-pre-wrap relative overflow-hidden\",\n                    )}\n                    key={charIndex}\n                  >\n                    <motion.span\n                      custom={previousCharsCount + charIndex}\n                      initial=\"hidden\"\n                      animate={isAnimating ? \"visible\" : \"hidden\"}\n                      variants={variants}\n                      onAnimationComplete={wordIndex === array.length - 1 &&\n                          charIndex === wordObj.characters.length - 1\n                        ? onComplete\n                        : undefined}\n                      className=\"inline-block\"\n                    >\n                      {char}\n                    </motion.span>\n                  </span>\n                ))}\n                {wordObj.needsSpace && (\n                  <span className=\"relative overflow-hidden\">\n                    <motion.span\n                      custom={previousCharsCount + wordObj.characters.length}\n                      initial=\"hidden\"\n                      animate={isAnimating ? \"visible\" : \"hidden\"}\n                      variants={variants}\n                      className=\"inline-block\"\n                    >\n                      {\" \"}\n                    </motion.span>\n                  </span>\n                )}\n              </span>\n            );\n          })}\n      </motion.span>\n    );\n  },\n);\n\nSlideUpText.displayName = \"SlideUpText\";\n\nexport { SlideUpText };\nexport type { SlideUpTextProps };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}