{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "blueprint-tiers",
  "title": "Blueprint Tiers",
  "description": "A Swiss-style pricing table with crop marks, mono labels, and a royal-blue accent.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/spectrumui/blocks/pricing/blueprint-tiers.tsx",
      "content": "'use client';\n\nimport { useState } from 'react';\nimport { Check } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { PriceFigure } from './price-figure';\nimport { REVEAL_KEYFRAMES, revealClass, revealDelay, useReveal } from './reveal';\nimport type { Plan } from './types';\n\nexport type BlueprintTiersVariant = 'Marks' | 'Clean';\n\nexport interface BlueprintTiersProps {\n  plans: Plan[];\n  eyebrow?: string;\n  heading: string;\n  subheading?: string;\n  freeNote?: string;\n  yearlyNote?: string;\n  variant?: BlueprintTiersVariant;\n  className?: string;\n}\n\nfunction CropMark({ position }: { position: string }) {\n  return (\n    <span aria-hidden className={cn('pointer-events-none absolute z-10 size-2.5', position)}>\n      <span className=\"absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-[#a1a1aa] dark:bg-[#4a4a52]\" />\n      <span className=\"absolute top-1/2 left-0 h-px w-full -translate-y-1/2 bg-[#a1a1aa] dark:bg-[#4a4a52]\" />\n    </span>\n  );\n}\n\nfunction CornerTicks() {\n  return (\n    <>\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute -left-1 -top-1 size-2 border-l border-t border-[#18181b] dark:border-[#d4d4d8]\"\n      />\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute -right-1 -top-1 size-2 border-r border-t border-[#18181b] dark:border-[#d4d4d8]\"\n      />\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute -bottom-1 -left-1 size-2 border-b border-l border-[#18181b] dark:border-[#d4d4d8]\"\n      />\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute -bottom-1 -right-1 size-2 border-b border-r border-[#18181b] dark:border-[#d4d4d8]\"\n      />\n    </>\n  );\n}\n\nfunction Toggle({ on, onChange, label }: { on: boolean; onChange: () => void; label: string }) {\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      aria-label={label}\n      onClick={onChange}\n      className={cn(\n        'relative h-6 w-11 shrink-0 rounded-full transition-colors duration-200',\n        'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[#2941ff] focus-visible:ring-offset-2',\n        on ? 'bg-[#2941ff]' : 'bg-[#c9c9cf] dark:bg-[#3f3f46]',\n      )}\n    >\n      <span\n        aria-hidden\n        className={cn(\n          'absolute left-0.5 top-0.5 size-5 rounded-full bg-white shadow-sm',\n          'transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none',\n          on && 'translate-x-5',\n        )}\n      />\n    </button>\n  );\n}\n\nexport function BlueprintTiers({\n  plans,\n  eyebrow = 'Simple and transparent',\n  heading,\n  subheading,\n  freeNote = 'No credit card required',\n  yearlyNote = '2 months free',\n  variant = 'Marks',\n  className,\n}: BlueprintTiersProps) {\n  const [yearly, setYearly] = useState<Record<string, boolean>>(() =>\n    Object.fromEntries(plans.map((plan) => [plan.id, true])),\n  );\n  const { ref, shown } = useReveal<HTMLElement>();\n  const marks = variant === 'Marks';\n\n  return (\n    <section\n      ref={ref}\n      className={cn('@container w-full bg-[#f4f4f6] pt-14 dark:bg-[#0f0f12]', className)}\n    >\n      <style dangerouslySetInnerHTML={{ __html: REVEAL_KEYFRAMES }} />\n\n      <div className={cn('flex flex-col items-center px-6 text-center', revealClass(shown))}>\n        <span className=\"flex items-center gap-2.5 border border-[#d4d4d8] dark:border-[#2d2d33] bg-white px-3.5 py-2 dark:bg-[#17171b]\">\n          <span aria-hidden className=\"size-2 bg-[#2941ff]\" />\n          <span className=\"font-mono text-[12px] font-medium uppercase tracking-[0.1em] text-[#18181b] dark:text-[#f4f4f5]\">\n            {eyebrow}\n          </span>\n        </span>\n        <h2 className=\"mt-7 max-w-[22ch] text-balance text-[28px] font-semibold leading-[1.05] tracking-[-2px] text-[#0a0a0a] dark:text-white @sm:text-[34px] @lg:text-[46px]\">\n          {heading}\n        </h2>\n        {subheading && (\n          <p className=\"mt-5 max-w-[46ch] text-pretty text-[16px] leading-[1.55] text-[#52525b] dark:text-[#a1a1a8]\">\n            {subheading}\n          </p>\n        )}\n      </div>\n\n      <div className=\"relative mt-12 border-t border-[#d4d4d8] dark:border-[#2d2d33]\">\n        {marks && (\n          <>\n            <CropMark position=\"left-0 top-0 -translate-x-1/2 -translate-y-1/2\" />\n            <CropMark position=\"right-0 top-0 translate-x-1/2 -translate-y-1/2\" />\n          </>\n        )}\n\n        <div className=\"grid grid-cols-1 @3xl:grid-cols-3\">\n          {plans.map((plan, position) => {\n            const isYearly = yearly[plan.id] ?? true;\n            const amount = plan.price ? (isYearly ? plan.price.annual : plan.price.monthly) : 0;\n            const free = amount === 0;\n\n            return (\n              <div\n                key={plan.id}\n                style={revealDelay(shown, position + 1)}\n                className={cn(\n                  'relative flex flex-col border-t border-[#d4d4d8] dark:border-[#2d2d33] @3xl:border-l @3xl:border-t-0 @3xl:first:border-l-0',\n                  revealClass(shown),\n                )}\n              >\n                {marks && position > 0 && (\n                  <CropMark position=\"left-0 top-0 -translate-x-1/2 -translate-y-1/2 hidden @3xl:block\" />\n                )}\n\n                <div className=\"px-5 pb-8 pt-9 @sm:px-8 @lg:px-10\">\n                  <div className=\"flex items-center gap-3\">\n                    <h3 className=\"text-[17px] font-medium text-[#18181b] dark:text-[#f4f4f5]\">\n                      {plan.name}\n                    </h3>\n                    {plan.badge && (\n                      <span className=\"bg-[#e4e8ff] px-2.5 py-1 dark:bg-[#1b2147] font-mono text-[10.5px] font-medium uppercase tracking-[0.08em] text-[#2941ff] dark:text-[#93a3ff]\">\n                        {plan.badge}\n                      </span>\n                    )}\n                  </div>\n                  <p className=\"mt-3 min-h-[52px] text-pretty text-[15px] leading-[1.55] text-[#52525b] dark:text-[#a1a1a8]\">\n                    {plan.tagline}\n                  </p>\n                </div>\n\n                <div className=\"border-t border-[#d4d4d8] dark:border-[#2d2d33] px-5 pb-9 pt-8 @sm:px-8 @lg:px-10\">\n                  <div className=\"flex items-end gap-1.5\">\n                    <PriceFigure\n                      value={amount}\n                      className=\"text-[36px] font-semibold tracking-[-2.5px] text-[#0a0a0a] dark:text-white @sm:text-[44px] @lg:text-[52px]\"\n                    />\n                    <span className=\"pb-1.5 text-[14px] text-[#71717a] dark:text-[#8f8f96]\">\n                      /{free ? 'forever' : 'monthly'}\n                    </span>\n                  </div>\n\n                  <div className=\"mt-5 flex min-h-[28px] items-center gap-3\">\n                    {free ? (\n                      <span className=\"text-[14.5px] text-[#52525b] dark:text-[#a1a1a8]\">\n                        {freeNote}\n                      </span>\n                    ) : (\n                      <>\n                        <Toggle\n                          on={isYearly}\n                          onChange={() =>\n                            setYearly((prior) => ({ ...prior, [plan.id]: !prior[plan.id] }))\n                          }\n                          label={`Bill ${plan.name} yearly`}\n                        />\n                        <span className=\"text-[14.5px] text-[#18181b] dark:text-[#f4f4f5]\">\n                          Billed yearly\n                        </span>\n                        <span className=\"border border-[#d4d4d8] dark:border-[#2d2d33] bg-[#ececf0] px-2 py-1 dark:bg-[#1d1d22] font-mono text-[10px] font-medium uppercase tracking-[0.08em] text-[#3f3f46] dark:text-[#b3b3bb]\">\n                          {yearlyNote}\n                        </span>\n                      </>\n                    )}\n                  </div>\n\n                  <div className=\"relative mt-7\">\n                    {marks && plan.featured && <CornerTicks />}\n                    <button\n                      type=\"button\"\n                      className={cn(\n                        'h-[52px] w-full font-mono text-[14.5px]',\n                        'transition-[transform,background-color] duration-150 active:scale-[0.98]',\n                        'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[#2941ff] focus-visible:ring-offset-2',\n                        plan.featured\n                          ? 'bg-[#2941ff] text-white hover:bg-[#1f33d6]'\n                          : 'bg-[#e4e4e7] text-[#18181b] hover:bg-[#d9d9de] dark:bg-[#232329] dark:text-[#f4f4f5] dark:hover:bg-[#2c2c33]',\n                      )}\n                    >\n                      {plan.cta}\n                    </button>\n                  </div>\n                </div>\n\n                <ul className=\"flex-1 border-t border-[#d4d4d8] dark:border-[#2d2d33]\">\n                  {plan.features.map((feature) => (\n                    <li\n                      key={feature}\n                      className=\"flex items-center gap-4 border-t border-[#e2e2e6] px-5 py-[18px] first:border-t-0 dark:border-[#232328] @sm:px-8 @lg:px-10\"\n                    >\n                      <span\n                        aria-hidden\n                        className=\"grid size-5 shrink-0 place-items-center rounded-full border border-[#a1a1aa] dark:border-[#4a4a52]\"\n                      >\n                        <Check\n                          className=\"size-2.5 text-[#3f3f46] dark:text-[#b3b3bb]\"\n                          strokeWidth={2.4}\n                        />\n                      </span>\n                      <span className=\"text-[15px] text-[#3f3f46] dark:text-[#c6c6cd]\">\n                        {feature}\n                      </span>\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            );\n          })}\n        </div>\n\n        <div\n          aria-hidden\n          className=\"h-20 border-t border-[#d4d4d8] dark:border-[#2d2d33] [background-image:repeating-linear-gradient(45deg,transparent,transparent_8px,#dcdce1_8px,#dcdce1_9px)] dark:[background-image:repeating-linear-gradient(45deg,transparent,transparent_8px,#232329_8px,#232329_9px)]\"\n        />\n      </div>\n    </section>\n  );\n}\n\nexport default BlueprintTiers;\n",
      "type": "registry:block",
      "target": "components/spectrumui/blocks/pricing/blueprint-tiers.tsx"
    },
    {
      "path": "components/spectrumui/blocks/pricing/price-figure.tsx",
      "content": "'use client';\n\nimport { cn } from '@/lib/utils';\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\nexport interface PriceFigureProps {\n  value: number;\n  currency?: string;\n  decimals?: number;\n  className?: string;\n}\n\nfunction Digit({ digit }: { digit: number }) {\n  return (\n    <span className=\"relative block h-[1em] w-[1ch] overflow-hidden\">\n      <span\n        className=\"absolute inset-x-0 top-0 flex flex-col transition-transform duration-[420ms] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\"\n        style={{ transform: `translateY(-${digit * 10}%)` }}\n      >\n        {DIGITS.map((d) => (\n          <span key={d} className=\"flex h-[1em] items-center justify-center leading-none\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\nexport function PriceFigure({ value, currency = '$', decimals = 0, className }: PriceFigureProps) {\n  const negative = value < 0;\n  const body = Math.abs(value).toFixed(decimals);\n  const grouped = body.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n  const label = `${negative ? '−' : ''}${currency}${grouped}`;\n\n  return (\n    <span className={cn('inline-flex leading-none tabular-nums', className)}>\n      <span className=\"sr-only\">{label}</span>\n      <span aria-hidden className=\"inline-flex\">\n        {negative && <span className=\"flex h-[1em] items-center leading-none\">−</span>}\n        {currency && <span className=\"flex h-[1em] items-center leading-none\">{currency}</span>}\n        {grouped.split('').map((character, index) =>\n          /\\d/.test(character) ? (\n            <Digit key={`${index}-digit`} digit={Number(character)} />\n          ) : (\n            <span key={`${index}-${character}`} className=\"flex h-[1em] items-center leading-none\">\n              {character}\n            </span>\n          ),\n        )}\n      </span>\n    </span>\n  );\n}\n\nexport default PriceFigure;\n",
      "type": "registry:file",
      "target": "components/spectrumui/blocks/pricing/price-figure.tsx"
    },
    {
      "path": "components/spectrumui/blocks/pricing/types.ts",
      "content": "export type BillingPeriod = 'monthly' | 'annual';\n\nexport interface PlanPrice {\n  monthly: number;\n  annual: number;\n}\n\nexport interface Plan {\n  id: string;\n  name: string;\n  tagline: string;\n  price: PlanPrice | null;\n  unit?: string;\n  cta: string;\n  featured?: boolean;\n  badge?: string;\n  inherits?: string;\n  features: string[];\n  footnote?: string;\n}\n\nexport type FeatureValue = string | boolean;\n\nexport interface FeatureRow {\n  label: string;\n  hint?: string;\n  values: FeatureValue[];\n}\n\nexport interface FeatureGroup {\n  label: string;\n  rows: FeatureRow[];\n}\n\nexport interface RateTier {\n  from: number;\n  to?: number;\n  unitPrice: number;\n}\n\nexport interface MeteredProduct {\n  id: string;\n  name: string;\n  description?: string;\n  unit: string;\n  unitPlural: string;\n  included: number;\n  tiers: RateTier[];\n}\n\nexport interface CreditPack {\n  id: string;\n  name: string;\n  credits: number;\n  bonus: number;\n  price: number;\n  featured?: boolean;\n  note?: string;\n}\n\nexport interface CreditRate {\n  id: string;\n  label: string;\n  credits: number;\n  per: number;\n  unitLabel: string;\n}\n\nexport interface AddOn {\n  id: string;\n  name: string;\n  description: string;\n  price: number;\n  unit?: string;\n  perSeat?: boolean;\n  defaultOn?: boolean;\n}\n\nexport interface ModelRate {\n  id: string;\n  name: string;\n  context: string;\n  input: number;\n  output: number;\n  cacheWrite: number;\n  cacheRead: number;\n  badge?: string;\n}\n\nexport interface UsageDimension {\n  id: string;\n  label: string;\n  unit: string;\n  min: number;\n  max: number;\n  step: number;\n  defaultValue: number;\n}\n\nexport interface ComplianceItem {\n  id: string;\n  label: string;\n  detail: string;\n}\n\nexport interface RecommenderOption {\n  id: string;\n  label: string;\n  scores: Record<string, number>;\n  reason?: string;\n}\n\nexport interface RecommenderQuestion {\n  id: string;\n  label: string;\n  hint?: string;\n  options: RecommenderOption[];\n}\n\nexport interface PlanFamily {\n  id: string;\n  label: string;\n  description: string;\n  plans: Plan[];\n  footnote?: string;\n}\n",
      "type": "registry:file",
      "target": "components/spectrumui/blocks/pricing/types.ts"
    },
    {
      "path": "components/spectrumui/blocks/pricing/reveal.ts",
      "content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nexport const REVEAL_KEYFRAMES = `\n@keyframes su-reveal { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }\n@keyframes su-draw { from { stroke-dashoffset: 1 } to { stroke-dashoffset: 0 } }\n@keyframes su-sheen { 0%, 62% { transform: translateX(-110%) } 100% { transform: translateX(110%) } }\n`;\n\nexport function useReveal<T extends HTMLElement>() {\n  const ref = useRef<T>(null);\n  const [shown, setShown] = useState(false);\n\n  useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    if (typeof IntersectionObserver === 'undefined') {\n      const frame = requestAnimationFrame(() => setShown(true));\n      return () => cancelAnimationFrame(frame);\n    }\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((entry) => entry.isIntersecting)) {\n          setShown(true);\n          observer.disconnect();\n        }\n      },\n      { threshold: 0.18, rootMargin: '0px 0px -8% 0px' },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  return { ref, shown };\n}\n\nexport function revealClass(shown: boolean) {\n  return shown\n    ? '[animation:su-reveal_480ms_cubic-bezier(0.23,1,0.32,1)_both] motion-reduce:animate-none'\n    : '';\n}\n\nexport function revealDelay(shown: boolean, index: number) {\n  return shown ? { animationDelay: `${index * 70}ms` } : undefined;\n}\n",
      "type": "registry:file",
      "target": "components/spectrumui/blocks/pricing/reveal.ts"
    }
  ],
  "type": "registry:block"
}
