Menu

Creative Pricing Card

A premium pricing card with smooth smoky fog animation on price change.

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest add creative-pricing-card

Source Code

creative-pricing-card.tsx
'use client'

import React, { useState } from 'react'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
import { illustrationPlans, type BillingRegion } from '@/lib/payments/plans'

const EngineIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 122.88 122.878" {...props}>
    <path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M101.589,14.7l8.818,8.819c2.321,2.321,2.321,6.118,0,8.439l-7.101,7.101 c1.959,3.658,3.454,7.601,4.405,11.752h9.199c3.283,0,5.969,2.686,5.969,5.968V69.25c0,3.283-2.686,5.969-5.969,5.969h-10.039 c-1.231,4.063-2.992,7.896-5.204,11.418l6.512,6.51c2.321,2.323,2.321,6.12,0,8.44l-8.818,8.819c-2.321,2.32-6.119,2.32-8.439,0 l-7.102-7.102c-3.657,1.96-7.601,3.456-11.753,4.406v9.199c0,3.282-2.685,5.968-5.968,5.968H53.629 c-3.283,0-5.969-2.686-5.969-5.968v-10.039c-4.063-1.232-7.896-2.993-11.417-5.205l-6.511,6.512c-2.323,2.321-6.12,2.321-8.441,0 l-8.818-8.818c-2.321-2.321-2.321-6.118,0-8.439l7.102-7.102c-1.96-3.657-3.456-7.6-4.405-11.751H5.968 C2.686,72.067,0,69.382,0,66.099V53.628c0-3.283,2.686-5.968,5.968-5.968h10.039c1.232-4.063,2.993-7.896,5.204-11.418l-6.511-6.51 c-2.321-2.322-2.321-6.12,0-8.44l8.819-8.819c2.321-2.321,6.118-2.321,8.439,0l7.101,7.101c3.658-1.96,7.601-3.456,11.753-4.406 V5.969C50.812,2.686,53.498,0,56.78,0h12.471c3.282,0,5.968,2.686,5.968,5.969v10.036c4.064,1.231,7.898,2.992,11.422,5.204 l6.507-6.509C95.471,12.379,99.268,12.379,101.589,14.7L101.589,14.7z M61.44,36.92c13.54,0,24.519,10.98,24.519,24.519 c0,13.538-10.979,24.519-24.519,24.519c-13.539,0-24.519-10.98-24.519-24.519C36.921,47.9,47.901,36.92,61.44,36.92L61.44,36.92z" />
  </svg>
)

type IllustrationPass = 'sixMonth' | 'twelveMonth'

type CreativePricingCardProps = {
  passType?: IllustrationPass
  onPassTypeChange?: (pass: IllustrationPass) => void
  region?: BillingRegion
  onRegionChange?: (region: BillingRegion) => void
  onCheckout?: () => void
  hasAccess?: boolean
}

export default function CreativePricingCard({
  passType: controlledPassType,
  onPassTypeChange,
  region: controlledRegion,
  onRegionChange,
  onCheckout,
  hasAccess = false,
}: CreativePricingCardProps) {
  const [localPassType, setLocalPassType] = useState<IllustrationPass>('twelveMonth')
  const [localRegion, setLocalRegion] = useState<BillingRegion>('GLOBAL')
  const passType = controlledPassType ?? localPassType
  const region = controlledRegion ?? localRegion

  const setPassType = (next: IllustrationPass) => {
    setLocalPassType(next)
    onPassTypeChange?.(next)
  }

  const setRegion = (next: BillingRegion) => {
    setLocalRegion(next)
    onRegionChange?.(next)
  }

  const activePlan = passType === 'sixMonth' ? illustrationPlans.sixMonth : illustrationPlans.twelveMonth

  return (
    <div className="w-full max-w-lg mx-auto p-4 flex items-center justify-center min-h-[400px]">
      <div className="rounded-[24px] border border-foreground/10 bg-background dark:bg-[#1A1A1A] p-6 shadow-sm relative overflow-hidden flex flex-col items-center w-full">
        <div className="absolute top-4 right-4 flex items-center gap-1">
          <div className="inline-flex items-center rounded-lg border border-foreground/10 bg-foreground/[0.03] dark:bg-[#121212] p-1 shadow-inner relative hidden sm:flex">
            <motion.div layout className="absolute top-1 bottom-1 rounded-md bg-background dark:bg-[#2A2A2A] shadow-sm border border-foreground/5 dark:border-[#333333]" style={{ width: 'calc(50% - 4px)' }} animate={{ left: region === 'GLOBAL' ? '4px' : 'calc(50%)' }} transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }} />
            {(['GLOBAL', 'IN'] as const).map((r) => (
              <button key={r} type="button" onClick={() => setRegion(r)} className={cn('relative px-3 py-1 text-[10px] font-semibold transition-colors duration-300 z-10', region === r ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>
                {r === 'IN' ? 'Rs. IN' : '$ INTL'}
              </button>
            ))}
          </div>
        </div>

        <div className="flex items-center gap-2 mb-2">
          <EngineIcon className="w-5 h-5 text-indigo-500" />
          <h3 className="text-lg font-semibold tracking-tight text-foreground">Illustration Pass</h3>
        </div>
        <p className="text-sm text-muted-foreground mb-6 text-center max-w-sm">Unlock every premium illustration snippet with commercial usage.</p>

        <div className="inline-flex items-center rounded-lg border border-foreground/10 bg-foreground/[0.03] dark:bg-[#121212] p-1 shadow-inner relative mb-6">
          <motion.div layout className="absolute top-1 bottom-1 rounded-md bg-background dark:bg-[#2A2A2A] shadow-sm border border-foreground/5 dark:border-[#333333]" style={{ width: 'calc(50% - 4px)' }} animate={{ left: passType === 'sixMonth' ? '4px' : 'calc(50%)' }} transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }} />
          <button onClick={() => setPassType('sixMonth')} className={cn('relative px-3 py-1 text-[11px] font-medium transition-colors duration-300 z-10 w-24 text-center', passType === 'sixMonth' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>6 Months</button>
          <button onClick={() => setPassType('twelveMonth')} className={cn('relative px-3 py-1 text-[11px] font-medium transition-colors duration-300 z-10 w-24 text-center flex items-center justify-center gap-1', passType === 'twelveMonth' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}>12 Months <span className="text-[9px] uppercase tracking-wider font-bold text-indigo-500">Best</span></button>
        </div>

        <div className="flex flex-col items-center justify-center h-[70px] mb-6 relative w-full">
          <div className={cn('text-sm text-muted-foreground line-through decoration-destructive/50 transition-all duration-300 absolute top-0', passType === 'twelveMonth' ? 'opacity-100 transform-none' : 'opacity-0 -translate-y-2 pointer-events-none')}>
            {region === 'IN' ? `Rs. ${illustrationPlans.twelveMonth.originalInr}` : `$${illustrationPlans.twelveMonth.originalUsd}`}
          </div>
          <div className="flex items-baseline gap-2 mt-5">
            <span className="text-3xl font-bold tracking-tight text-foreground transition-all duration-300">{region === 'IN' ? `Rs. ${activePlan.inr}` : `$${activePlan.usd}`}</span>
            <span className="text-xs text-muted-foreground">{region === 'IN' ? 'incl. GST' : 'USD'}</span>
          </div>
        </div>

        <button onClick={hasAccess ? undefined : onCheckout} className={cn('w-full max-w-[280px] py-2.5 rounded-xl text-sm font-semibold transition-opacity shadow-sm', hasAccess ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' : 'bg-foreground text-background hover:opacity-90')}>
          {hasAccess ? 'Access Active' : 'Get Access'}
        </button>
      </div>
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest

Props

Component property reference.

NameTypeDefaultDescription
plansObject-Configuration object containing monthly and yearly pricing data.
featuresArray<string>[]List of features included in the pricing plan.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.