{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-plan-switch",
  "type": "registry:block",
  "title": "Pricing Plan Switch",
  "description": "A pricing section where picking a plan morphs the name badge, crossfades the description and spins the price with Number Flow, while the checklist lights up its included rows. A monthly/yearly toggle re-spins the price.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "@number-flow/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/pricing-plan-switch.tsx",
      "type": "registry:component",
      "target": "components/tweenui/pricing-plan-switch.tsx",
      "content": "'use client';\n\nimport { useRef, useState, type ComponentPropsWithoutRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport NumberFlow from '@number-flow/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface PricingPlan {\n  /** Stable id used as the animation key. */\n  id: string;\n  /** Plan name, shown in the left selector and the morphing card badge. */\n  name: string;\n  /** Supporting line under the name in the selector. */\n  subtitle: string;\n  /** Body copy that crossfades in when the plan is selected. */\n  description: string;\n  /** Price billed monthly. */\n  monthly: number;\n  /** Price billed yearly. */\n  yearly: number;\n  /** How many of `features` are included (the rest render dimmed). */\n  includedCount: number;\n}\n\nexport interface PricingPlanSwitchProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Plans shown in the selector. Defaults to a 3-tier sample. */\n  plans?: PricingPlan[];\n  /** Feature checklist; each plan lights up its first `includedCount` rows. */\n  features?: string[];\n  /** Eyebrow badge above the heading. */\n  eyebrow?: string;\n  /** Section heading. */\n  heading?: string;\n  /** Section sub-heading. */\n  description?: string;\n  /** Currency symbol shown before the price. */\n  currency?: string;\n  /** Primary CTA label. */\n  ctaText?: string;\n  /** Primary CTA link. */\n  ctaHref?: string;\n}\n\nconst DEFAULT_PLANS: PricingPlan[] = [\n  {\n    id: 'starter',\n    name: 'Starter',\n    subtitle: 'For early-stage teams',\n    description:\n      'Everything you need to get moving — core automation, a shared workspace, and email support to keep momentum.',\n    monthly: 2500,\n    yearly: 25000,\n    includedCount: 5,\n  },\n  {\n    id: 'pro',\n    name: 'Pro',\n    subtitle: 'For growing teams',\n    description:\n      'Built for growing teams with advanced automation, priority workflows, and deeper analytics to keep everything moving.',\n    monthly: 4190,\n    yearly: 41900,\n    includedCount: 7,\n  },\n  {\n    id: 'business',\n    name: 'Business',\n    subtitle: 'For established businesses',\n    description:\n      'Enterprise-grade controls with custom workflows, team-wide governance, and dedicated support for large-scale operations.',\n    monthly: 8290,\n    yearly: 82900,\n    includedCount: 9,\n  },\n];\n\nconst DEFAULT_FEATURES: string[] = [\n  'Unlimited workflows',\n  'Team workspace',\n  'Email support',\n  'Advanced automation',\n  'Priority queue',\n  'Usage analytics',\n  'Role-based access',\n  'Custom integrations',\n  'Dedicated success manager',\n];\n\nconst ArrowIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className={className}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M5 12h14M13 6l6 6-6 6\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={2}\n      />\n    </svg>\n  );\n};\n\nconst CheckIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className={className}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M5 13l4 4L19 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} />\n    </svg>\n  );\n};\n\nconst DotIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg viewBox=\"0 0 12 12\" className={className} aria-hidden=\"true\">\n      <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"#c6f56f\" stroke=\"#045f64\" strokeWidth={1} />\n    </svg>\n  );\n};\n\nconst PricingPlanSwitch = ({\n  plans = DEFAULT_PLANS,\n  features = DEFAULT_FEATURES,\n  eyebrow = 'Pricing',\n  heading = 'Simple pricing that scales with you',\n  description = 'Upgrade anytime as your needs evolve, and stay focused on building and expanding your product with confidence.',\n  currency = '$',\n  ctaText = 'Get started',\n  ctaHref = '#',\n  className,\n  ...props\n}: PricingPlanSwitchProps) => {\n  const firstId = plans[0]?.id ?? '';\n  const [selectedId, setSelectedId] = useState(firstId);\n  const [isYearly, setIsYearly] = useState(false);\n\n  const cardRef = useRef<HTMLDivElement>(null);\n  const badgeWrapperRef = useRef<HTMLSpanElement>(null);\n  const prevIdRef = useRef(firstId);\n  const badgeRefs = useRef<Record<string, HTMLSpanElement | undefined>>({});\n  const descriptionRefs = useRef<Record<string, HTMLParagraphElement | undefined>>({});\n\n  const currentPlan = plans.find((p) => p.id === selectedId) ?? plans[0];\n  const price = isYearly ? (currentPlan?.yearly ?? 0) : (currentPlan?.monthly ?? 0);\n\n  // Establish the starting state: only the first badge/description is visible,\n  // and the wrapper is sized to that badge so its width can morph later.\n  useGSAP(\n    () => {\n      const wrapper = badgeWrapperRef.current;\n      if (!wrapper) return;\n\n      plans.forEach((plan, index) => {\n        const badge = badgeRefs.current[plan.id];\n        if (badge) gsap.set(badge, { opacity: index === 0 ? 1 : 0, y: 0 });\n      });\n\n      const first = badgeRefs.current[firstId];\n      if (first) wrapper.style.width = `${first.offsetWidth}px`;\n    },\n    { scope: cardRef }\n  );\n\n  // Crossfade the badge + description and morph the badge width on selection.\n  useGSAP(\n    () => {\n      const prev = prevIdRef.current;\n      const next = selectedId;\n      if (prev === next) return;\n\n      const fromBadge = badgeRefs.current[prev];\n      const toBadge = badgeRefs.current[next];\n      const fromDescription = descriptionRefs.current[prev];\n      const toDescription = descriptionRefs.current[next];\n      const wrapper = badgeWrapperRef.current;\n\n      if (prefersReducedMotion()) {\n        plans.forEach((plan) => {\n          const badge = badgeRefs.current[plan.id];\n          const desc = descriptionRefs.current[plan.id];\n          if (badge) gsap.set(badge, { opacity: plan.id === next ? 1 : 0, y: 0 });\n          if (desc) gsap.set(desc, { opacity: plan.id === next ? 1 : 0, y: 0 });\n        });\n        if (wrapper && toBadge) wrapper.style.width = `${toBadge.offsetWidth}px`;\n        prevIdRef.current = next;\n        return;\n      }\n\n      const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });\n\n      if (fromBadge && toBadge) {\n        const fromWidth = wrapper?.offsetWidth ?? fromBadge.offsetWidth;\n        const toWidth = toBadge.offsetWidth;\n\n        tl.set(toBadge, { y: 12, opacity: 0 }, 0);\n        tl.to(fromBadge, { y: -12, opacity: 0, duration: 0.22 }, 0);\n        tl.to(toBadge, { y: 0, opacity: 1, duration: 0.28 }, 0.12);\n        tl.set(fromBadge, { y: 0 }, 0.3);\n\n        if (wrapper) {\n          tl.to(wrapper, { width: toWidth, duration: 0.28 }, toWidth > fromWidth ? 0 : 0.12);\n        }\n      }\n\n      if (fromDescription && toDescription) {\n        tl.set(toDescription, { y: 12, opacity: 0 }, 0);\n        tl.to(fromDescription, { y: -12, opacity: 0, duration: 0.22 }, 0);\n        tl.to(toDescription, { y: 0, opacity: 1, duration: 0.28 }, 0.12);\n        tl.set(fromDescription, { y: 0 }, 0.3);\n      }\n\n      prevIdRef.current = next;\n    },\n    { scope: cardRef, dependencies: [selectedId] }\n  );\n\n  return (\n    <section className={cn('w-full', className)} {...props}>\n      <div className=\"mx-auto max-w-[980px] space-y-10 px-4 lg:space-y-14\">\n        {/* Header */}\n        <div className=\"space-y-6\">\n          <div className=\"space-y-4 text-center\">\n            <span className=\"inline-block rounded-full border border-[#045f64]/20 px-3 py-1 text-xs font-medium tracking-wide text-[#045f64] uppercase dark:border-[#045f64]/40 dark:text-[#9fd4d6]\">\n              {eyebrow}\n            </span>\n            <div className=\"space-y-2\">\n              <h2 className=\"mx-auto max-w-[520px] text-2xl font-semibold text-[#12161F] sm:text-3xl dark:text-white\">\n                {heading}\n              </h2>\n              <p className=\"mx-auto max-w-[560px] text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n                {description}\n              </p>\n            </div>\n          </div>\n\n          {/* Billing toggle */}\n          <div className=\"flex items-center justify-center\">\n            <label className=\"inline-flex cursor-pointer items-center\">\n              <span className=\"me-2.5 text-sm font-medium text-[#12161F] select-none dark:text-white\">\n                Monthly\n              </span>\n              <input\n                type=\"checkbox\"\n                checked={isYearly}\n                onChange={(e) => setIsYearly(e.target.checked)}\n                className=\"peer sr-only\"\n                aria-label=\"Toggle yearly billing\"\n              />\n              <span className=\"relative h-6 w-11 rounded-full bg-[#12161F]/15 transition-colors peer-checked:bg-[#c6f56f] peer-focus-visible:ring-2 peer-focus-visible:ring-[#045f64] peer-focus-visible:ring-offset-2 after:absolute after:top-1/2 after:left-[3px] after:size-5 after:-translate-y-1/2 after:rounded-full after:bg-[#045f64] after:transition-transform after:content-[''] peer-checked:after:translate-x-5 dark:bg-white/15 dark:ring-offset-[#12161F]\" />\n              <span className=\"ms-2.5 text-sm font-medium text-[#12161F] select-none dark:text-white\">\n                Yearly\n              </span>\n            </label>\n          </div>\n        </div>\n\n        <div className=\"grid grid-cols-1 items-start gap-6 md:grid-cols-12 md:gap-8 lg:gap-12\">\n          {/* Plan selector */}\n          <div className=\"space-y-6 md:col-span-5\">\n            <div className=\"space-y-2 rounded-[20px] bg-[#045f64]/[0.06] p-2 dark:bg-[#045f64]/20\">\n              {plans.map((plan) => {\n                const isActive = selectedId === plan.id;\n                return (\n                  <button\n                    key={plan.id}\n                    type=\"button\"\n                    onClick={() => setSelectedId(plan.id)}\n                    data-active={isActive ? 'true' : 'false'}\n                    className=\"group flex w-full cursor-pointer items-center justify-between gap-x-6 rounded-xl px-5 py-3 text-left transition-colors hover:bg-white data-[active=true]:bg-white data-[active=true]:shadow-sm dark:hover:bg-white/10 dark:data-[active=true]:bg-white/10\"\n                  >\n                    <span className=\"space-y-1\">\n                      <span className=\"block text-sm font-medium text-[#12161F] dark:text-white\">\n                        {plan.name}\n                      </span>\n                      <span className=\"block text-xs text-[#045f64]/60 dark:text-[#9fd4d6]/80\">\n                        {plan.subtitle}\n                      </span>\n                    </span>\n                    <span\n                      className={cn(\n                        'flex size-8 shrink-0 items-center justify-center rounded-full bg-[#045f64] transition-transform duration-500 ease-in-out',\n                        isActive ? 'scale-100' : 'scale-0'\n                      )}\n                    >\n                      <ArrowIcon className=\"size-4 stroke-white\" />\n                    </span>\n                  </button>\n                );\n              })}\n            </div>\n\n            <div className=\"flex items-center justify-center gap-x-6\">\n              <span className=\"flex items-center gap-x-1.5 text-sm text-[#045f64] italic dark:text-[#9fd4d6]\">\n                <DotIcon className=\"size-3\" />\n                Free trial\n              </span>\n              <span className=\"flex items-center gap-x-1.5 text-sm text-[#045f64] italic dark:text-[#9fd4d6]\">\n                <DotIcon className=\"size-3\" />\n                Cancel anytime\n              </span>\n            </div>\n          </div>\n\n          {/* Card */}\n          <div\n            ref={cardRef}\n            className=\"space-y-10 rounded-2xl border border-[#045f64]/15 bg-white px-6 py-8 md:col-span-7 dark:border-[#045f64]/40 dark:bg-[#12161F]\"\n          >\n            <div className=\"space-y-6\">\n              <span\n                ref={badgeWrapperRef}\n                className=\"relative inline-flex h-7 items-center overflow-hidden rounded-full border border-[#045f64]/20 dark:border-[#045f64]/40\"\n              >\n                {plans.map((plan) => (\n                  <span\n                    key={plan.id}\n                    ref={(el) => {\n                      badgeRefs.current[plan.id] = el ?? undefined;\n                    }}\n                    className=\"absolute inset-y-0 left-0 flex w-max items-center px-3 text-xs font-medium whitespace-nowrap text-[#045f64] dark:text-[#9fd4d6]\"\n                  >\n                    {plan.name}\n                  </span>\n                ))}\n              </span>\n\n              <div className=\"space-y-4\">\n                <div className=\"flex items-center justify-between gap-4\">\n                  <h3 className=\"text-base font-medium text-[#12161F] dark:text-white\">\n                    Subscription\n                  </h3>\n                  <p className=\"text-3xl font-medium text-[#12161F] dark:text-white\">\n                    {currency}\n                    <NumberFlow\n                      value={price}\n                      format={{\n                        useGrouping: true,\n                        minimumIntegerDigits: 2,\n                        maximumFractionDigits: 0,\n                        minimumFractionDigits: 0,\n                      }}\n                      transformTiming={{ duration: 700, easing: 'ease-out' }}\n                      spinTiming={{ duration: 700, easing: 'ease-out' }}\n                      opacityTiming={{ duration: 315, easing: 'ease-out' }}\n                    />\n                    <span className=\"text-sm font-normal text-[#045f64]/60 dark:text-[#9fd4d6]/80\">\n                      {isYearly ? '/year' : '/month'}\n                    </span>\n                  </p>\n                </div>\n\n                <span className=\"relative grid overflow-hidden\">\n                  {plans.map((plan, index) => (\n                    <p\n                      key={plan.id}\n                      ref={(el) => {\n                        descriptionRefs.current[plan.id] = el ?? undefined;\n                      }}\n                      className={cn(\n                        'col-start-1 row-start-1 text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80',\n                        index !== 0 && 'opacity-0'\n                      )}\n                    >\n                      {plan.description}\n                    </p>\n                  ))}\n                </span>\n              </div>\n\n              <a\n                href={ctaHref}\n                className=\"flex w-full items-center justify-center gap-2 rounded-xl bg-[#045f64] px-5 py-3 text-sm font-medium text-white transition-colors hover:bg-[#12161F] motion-reduce:transition-none dark:hover:bg-[#0d1117]\"\n              >\n                {ctaText}\n                <ArrowIcon className=\"size-4 stroke-white\" />\n              </a>\n            </div>\n\n            <div className=\"space-y-4\">\n              <h4 className=\"text-base font-medium text-[#12161F] dark:text-white\">\n                What&apos;s included:\n              </h4>\n              <ul className=\"space-y-4\">\n                {features.map((feature, index) => {\n                  const isActive = index < (currentPlan?.includedCount ?? 0);\n                  return (\n                    <li key={feature} className=\"flex items-center gap-x-2\">\n                      <span\n                        className={cn(\n                          'flex size-6 shrink-0 items-center justify-center rounded-full border p-1 transition-colors duration-500 ease-in-out',\n                          isActive\n                            ? 'border-[#045f64]/30 dark:border-[#045f64]/50'\n                            : 'border-[#045f64]/15 dark:border-[#045f64]/40'\n                        )}\n                      >\n                        <CheckIcon\n                          className={cn(\n                            'size-4 transition-colors duration-500 ease-in-out',\n                            isActive\n                              ? 'stroke-[#045f64] dark:stroke-[#9fd4d6]'\n                              : 'stroke-[#045f64]/30'\n                          )}\n                        />\n                      </span>\n                      <span\n                        className={cn(\n                          'text-sm transition-colors duration-500 ease-in-out',\n                          isActive\n                            ? 'text-[#12161F] dark:text-white'\n                            : 'text-[#045f64]/30 dark:text-[#9fd4d6]/40'\n                        )}\n                      >\n                        {feature}\n                      </span>\n                    </li>\n                  );\n                })}\n              </ul>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default PricingPlanSwitch;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/pricing-plan-switch"
}