{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drop-text",
  "title": "Drop Text",
  "description": "Text that drops into place under gravity and settles with a bounce, word by word.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/drop-text.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface DropTextProps {\n  ref?: React.Ref<DropTextRef>;\n  children: string;\n\n  splitBy?: \"words\" | \"characters\";\n\n  dropDistance?: number;\n\n  speed?: number;\n\n  duration?: number;\n\n  stagger?: number;\n\n  delay?: number;\n\n  bounce?: number;\n\n  landedClassName?: string;\n\n  className?: string;\n  wordClassName?: string;\n  charClassName?: string;\n\n  autoStart?: boolean;\n  inView?: boolean;\n  once?: boolean;\n\n  onStart?: () => void;\n  onComplete?: () => void;\n}\n\nexport interface DropTextRef {\n  play: () => void;\n  reset: () => void;\n}\n\nfunction DropText({\n  ref,\n  children,\n  splitBy = \"words\",\n  dropDistance = 40,\n  speed = 1,\n  duration = 0.5,\n  stagger = 0.05,\n  delay = 0,\n  bounce = 0.35,\n  landedClassName,\n  className,\n  wordClassName,\n  charClassName,\n  autoStart = true,\n  inView = false,\n  once = true,\n  onStart,\n  onComplete,\n}: DropTextProps) {\n  const reduceMotion = useReducedMotion();\n  const [played, setPlayed] = React.useState(!inView && autoStart);\n  const [landed, setLanded] = React.useState(false);\n  const frame = React.useRef<number | null>(null);\n  const cancelPlay = React.useCallback(() => {\n    if (frame.current !== null) cancelAnimationFrame(frame.current);\n    frame.current = null;\n  }, []);\n  React.useEffect(() => cancelPlay, [cancelPlay]);\n\n  const units = React.useMemo(() => {\n    if (splitBy === \"characters\") {\n      return Array.from(children);\n    }\n    return children.split(\" \");\n  }, [children, splitBy]);\n  const offset = reduceMotion ? 0 : dropDistance;\n  const hidden = { transform: `translateY(${-offset}px)`, opacity: 0 };\n  const shown = { transform: \"translateY(0px)\", opacity: 1 };\n  const effectiveDuration = duration / Math.max(speed, 0.01);\n  const effectiveStagger = stagger / Math.max(speed, 0.01);\n\n  const play = React.useCallback(() => {\n    cancelPlay();\n    setLanded(false);\n    setPlayed(false);\n    frame.current = requestAnimationFrame(() => {\n      frame.current = null;\n      setPlayed(true);\n    });\n    onStart?.();\n  }, [onStart, cancelPlay]);\n\n  const reset = React.useCallback(() => {\n    cancelPlay();\n    setPlayed(false);\n    setLanded(false);\n  }, [cancelPlay]);\n\n  React.useImperativeHandle(ref, () => ({ play, reset }), [play, reset]);\n\n  const handleViewportEnter = React.useCallback(() => {\n    if (once && played) return;\n    play();\n  }, [once, played, play]);\n\n  return (\n    <motion.span\n      className={cn(\"inline-block\", className)}\n      viewport={{ once, margin: \"-10% 0px -10% 0px\" }}\n      onViewportEnter={inView ? handleViewportEnter : undefined}\n    >\n      {units.map((unit, index) => (\n        <motion.span\n          key={`${unit}-${index}`}\n          className={cn(\n            \"inline-block\",\n            splitBy === \"words\" ? wordClassName : charClassName,\n            landed && landedClassName,\n          )}\n          style={{ willChange: landed ? undefined : \"transform, opacity\" }}\n          initial={hidden}\n          animate={played ? shown : hidden}\n          transition={{\n            type: bounce > 0 && !reduceMotion ? \"spring\" : \"tween\",\n            bounce,\n            duration: reduceMotion ? 0.2 : effectiveDuration,\n            ease: bounce > 0 && !reduceMotion ? undefined : \"easeIn\",\n            delay: reduceMotion ? 0 : delay + index * effectiveStagger,\n          }}\n          onAnimationComplete={() => {\n            if (played && index === units.length - 1) {\n              setLanded(true);\n              onComplete?.();\n            }\n          }}\n        >\n          {unit}\n          {splitBy === \"words\" && index < units.length - 1 ? \"\\u00A0\" : \"\"}\n        </motion.span>\n      ))}\n    </motion.span>\n  );\n}\n\nexport { DropText };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}