{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cta-starfall",
  "type": "registry:block",
  "title": "CTA Starfall",
  "description": "A dark call-to-action section under a slow field of drifting stars lit by a violet bloom. The field is full on the first frame rather than spawning over time, and pauses off-screen and in background tabs.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [
    "https://tween-ui.vercel.app/r/shiny-button.json"
  ],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/cta-starfall.tsx",
      "type": "registry:component",
      "target": "components/tweenui/cta-starfall.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\nimport { cn } from '@/lib/utils';\nimport ShinyButton from './shiny-button';\n\ngsap.registerPlugin(useGSAP, ScrollTrigger);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n/** Seconds for one star to cross the section, top to bottom. */\nconst FALL = [15, 25] as const;\n/** How far a star slides sideways over that fall, in pixels. */\nconst DRIFT = 100;\n/** Stars start and end this far outside the box so they never pop in view. */\nconst MARGIN = 40;\n\nexport interface CtaStarfallAction {\n  /** Button label. */\n  label: string;\n  /** Runs when the button is pressed. */\n  onClick?: () => void;\n}\n\nexport interface CtaStarfallProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Section heading. */\n  title?: ReactNode;\n  /** Supporting line under the heading. */\n  description?: string;\n  /** Button below the copy. Pass `null` to hide it. */\n  action?: CtaStarfallAction | null;\n  /** How many stars fill the field. */\n  starCount?: number;\n}\n\nconst DEFAULT_TITLE = 'Turn content into videos the smarter way';\n\nconst DEFAULT_DESCRIPTION = 'No video editing skills? No problem — we handle the hard part.';\n\nexport default function CtaStarfall({\n  title = DEFAULT_TITLE,\n  description = DEFAULT_DESCRIPTION,\n  action = { label: 'Start creating' },\n  starCount = 160,\n  className,\n  ...props\n}: CtaStarfallProps) {\n  const rootRef = useRef<HTMLElement>(null);\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const field = fieldRef.current;\n      if (!field) return;\n\n      const stars = gsap.utils.toArray<HTMLElement>('[data-star]', field);\n      if (stars.length === 0) return;\n\n      const reduced = prefersReducedMotion();\n      let tweens: gsap.core.Tween[] = [];\n\n      const build = () => {\n        tweens.forEach((tween) => tween.kill());\n        const { width, height } = field.getBoundingClientRect();\n        if (width === 0 || height === 0) return;\n\n        tweens = stars.map((star) => {\n          const startX = gsap.utils.random(0, width);\n          gsap.set(star, {\n            opacity: gsap.utils.random(0.3, 1),\n            scale: gsap.utils.random(0.5, 1.2),\n          });\n\n          // Reduced motion still gets a sky — just a still one.\n          if (reduced) {\n            gsap.set(star, { x: startX, y: gsap.utils.random(0, height) });\n            return gsap.to(star, { duration: 0, paused: true });\n          }\n\n          const tween = gsap.fromTo(\n            star,\n            { x: startX, y: -MARGIN },\n            {\n              x: startX + gsap.utils.random(-DRIFT, DRIFT),\n              y: height + MARGIN,\n              duration: gsap.utils.random(FALL[0], FALL[1]),\n              ease: 'none',\n              repeat: -1,\n            }\n          );\n          // Seed each star part-way down so the field is full on the first\n          // frame, instead of filling over the length of one fall.\n          tween.progress(gsap.utils.random(0, 1));\n          return tween;\n        });\n      };\n\n      build();\n      if (reduced) return;\n\n      // Rebuild on resize so in-flight stars target the new height.\n      let pending: gsap.core.Tween | undefined;\n      const observer = new ResizeObserver(() => {\n        pending?.kill();\n        pending = gsap.delayedCall(0.2, build);\n      });\n      observer.observe(field);\n\n      const setPaused = (paused: boolean) =>\n        tweens.forEach((tween) => (paused ? tween.pause() : tween.play()));\n\n      const trigger = ScrollTrigger.create({\n        trigger: rootRef.current,\n        start: 'top bottom',\n        end: 'bottom top',\n        onEnter: () => setPaused(false),\n        onLeave: () => setPaused(true),\n        onEnterBack: () => setPaused(false),\n        onLeaveBack: () => setPaused(true),\n      });\n\n      // Don't burn frames in a background tab.\n      const onVisibility = () => setPaused(document.hidden);\n      document.addEventListener('visibilitychange', onVisibility);\n\n      return () => {\n        observer.disconnect();\n        pending?.kill();\n        trigger.kill();\n        tweens.forEach((tween) => tween.kill());\n        document.removeEventListener('visibilitychange', onVisibility);\n      };\n    },\n    { scope: rootRef, dependencies: [starCount] }\n  );\n\n  return (\n    <section\n      ref={rootRef}\n      data-cta-starfall\n      className={cn(\n        'relative grid min-h-[520px] w-full place-items-center overflow-hidden bg-[#0b0713] px-5 py-24 md:py-28',\n        className\n      )}\n      {...props}\n    >\n      {/* Starfield. Stars are React children so nothing mutates this subtree\n          behind React's back; GSAP only moves them. */}\n      <div\n        ref={fieldRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 motion-reduce:animate-none\"\n      >\n        {Array.from({ length: starCount }, (_, index) => (\n          <span\n            key={index}\n            data-star\n            className=\"absolute top-0 left-0 size-px bg-white will-change-[transform,opacity]\"\n          />\n        ))}\n      </div>\n\n      {/* Violet bloom at the top edge, plus the hairline it sits under. */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-1/2 h-[420px] w-[90%] -translate-x-1/2 bg-[radial-gradient(45%_46%_at_50%_0%,#ff59fc47_0%,transparent_100%)]\"\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-1/2 h-px w-[70%] -translate-x-1/2 bg-[radial-gradient(circle_at_center,#ffffff_0%,transparent_100%)] blur-[0.5px]\"\n      />\n\n      <div className=\"relative z-10 mx-auto max-w-[600px] space-y-10 text-center\">\n        <div className=\"space-y-3\">\n          <h2 className=\"text-3xl font-medium tracking-tight text-balance text-white/90 md:text-5xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-[460px] text-sm text-white/50 md:text-base\">{description}</p>\n        </div>\n\n        {action && (\n          <div className=\"flex justify-center\">\n            <ShinyButton onClick={action.onClick}>{action.label}</ShinyButton>\n          </div>\n        )}\n      </div>\n    </section>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/cta-starfall\nGSAP plugins (ScrollTrigger) are registered inside the component, so there is nothing to set up."
}