{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "limit-reached",
  "type": "registry:component",
  "title": "Limit Reached",
  "description": "An in-app quota state for when a team runs out of AI runs: a full meter, a countdown to the reset with rolling digits, what still works and what's paused, and an upgrade that rescales the meter in place.",
  "categories": [
    "component",
    "Empty & Error"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/limit-reached.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, CirclePauseIcon, ClockIcon, GaugeIcon, SparklesIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LimitReachedUpgrade {\n  plan: string;\n  limit: number;\n}\n\nexport interface LimitReachedProps {\n  used?: number;\n  limit?: number;\n  unit?: string;\n  shortUnit?: string;\n  period?: string;\n  planName?: string;\n  secondsUntilReset?: number;\n  resetDate?: string;\n  upgrade?: LimitReachedUpgrade | null;\n  stillWorks?: string[];\n  paused?: string[];\n  upgrading?: boolean;\n  secondaryLabel?: string;\n  secondaryDoneLabel?: string;\n  onUpgrade?: () => void;\n  onSecondary?: () => void;\n  onReset?: () => void;\n  className?: string;\n}\n\nconst meterSpring = { type: \"spring\", stiffness: 110, damping: 17 } as const;\nconst roll = { type: \"spring\", stiffness: 320, damping: 32 } as const;\nconst glide = { type: \"spring\", stiffness: 380, damping: 34 } as const;\n\n// Stays false until mounted so the server render and the first client render match.\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nconst format = (n: number) => n.toLocaleString(\"en-US\");\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n\nfunction split(seconds: number) {\n  const s = Math.max(0, Math.floor(seconds));\n  return { d: Math.floor(s / 86400), h: Math.floor((s % 86400) / 3600), m: Math.floor((s % 3600) / 60), s: s % 60 };\n}\n\nfunction spoken(seconds: number) {\n  const t = split(seconds);\n  const parts = [\n    t.d && `${t.d} ${t.d === 1 ? \"day\" : \"days\"}`,\n    t.h && `${t.h} ${t.h === 1 ? \"hour\" : \"hours\"}`,\n    !t.d && t.m && `${t.m} min`,\n  ].filter(Boolean);\n  return parts.length ? parts.join(\" \") : \"less than a minute\";\n}\n\nfunction Roll({ text, reduce, className }: { text: string; reduce: boolean; className?: string }) {\n  const chars = text.split(\"\");\n  return (\n    <span aria-hidden className={cn(\"tabular-nums leading-[1.2em]\", className)}>\n      {chars.map((char, i) => {\n        const key = chars.length - i;\n        if (!/\\d/.test(char)) return <span key={`s${key}`}>{char}</span>;\n        return (\n          <span className=\"relative inline-block [clip-path:inset(0)]\" key={`d${key}`}>\n            <span className=\"invisible\">{char}</span>\n            <motion.span\n              animate={{ y: `${-Number(char) * 1.2}em` }}\n              className=\"absolute inset-x-0 top-0 flex flex-col\"\n              initial={false}\n              transition={reduce ? { duration: 0 } : roll}\n            >\n              {Array.from({ length: 10 }, (_, n) => (\n                <span className=\"block h-[1.2em] text-center\" key={n}>\n                  {n}\n                </span>\n              ))}\n            </motion.span>\n          </span>\n        );\n      })}\n    </span>\n  );\n}\n\nfunction useCountdown(start: number, onZero?: () => void) {\n  const [left, setLeft] = useState(start);\n  const onZeroRef = useRef(onZero);\n  useEffect(() => {\n    onZeroRef.current = onZero;\n  }, [onZero]);\n  useEffect(() => {\n    setLeft(start);\n    if (start <= 0) return;\n    const began = Date.now();\n    const id = window.setInterval(() => {\n      const next = Math.max(0, start - Math.floor((Date.now() - began) / 1000));\n      setLeft(next);\n      if (next === 0) {\n        window.clearInterval(id);\n        onZeroRef.current?.();\n      }\n    }, 250);\n    return () => window.clearInterval(id);\n  }, [start]);\n  return left;\n}\n\nexport function LimitReached({\n  used = 500,\n  limit = 500,\n  unit = \"AI runs\",\n  shortUnit = \"runs\",\n  period = \"this month\",\n  planName = \"Team\",\n  secondsUntilReset = 3 * 86400 + 4 * 3600 + 12 * 60 + 8,\n  resetDate = \"Nov 1\",\n  upgrade = { plan: \"Pro\", limit: 2000 },\n  stillWorks = [\"Editing and comments\", \"Search across projects\", \"Exports and the API\"],\n  paused = [\"AI summaries\", \"Auto-tagging uploads\", \"Ask Ridgeline\"],\n  upgrading = false,\n  secondaryLabel = \"Ask your admin\",\n  secondaryDoneLabel = \"Admin notified\",\n  onUpgrade,\n  onSecondary,\n  onReset,\n  className,\n}: LimitReachedProps) {\n  const reduce = useSafeReducedMotion();\n  const left = useCountdown(secondsUntilReset, onReset);\n  const [asked, setAsked] = useState(false);\n  const atLimit = used >= limit;\n  const fraction = limit > 0 ? Math.min(1, used / limit) : 1;\n  const percent = Math.round(fraction * 100);\n  const t = split(left);\n  const canUpgrade = !!upgrade && upgrade.limit > limit;\n\n  useEffect(() => {\n    if (!atLimit) setAsked(false);\n  }, [atLimit]);\n\n  return (\n    <section\n      aria-label={`${unit} usage`}\n      className={cn(\"@container w-full max-w-md overflow-hidden rounded-2xl border bg-card shadow-xs/5\", className)}\n    >\n      <div className=\"grid gap-5 p-5\">\n        <div className=\"flex items-start gap-3\">\n          <span\n            className={cn(\n              \"flex size-10 shrink-0 items-center justify-center rounded-xl border bg-card shadow-xs/5 transition-colors duration-300 [&_svg]:size-5\",\n              atLimit ? \"text-warning-foreground\" : \"text-success-foreground\",\n            )}\n          >\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              <motion.span\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.8 }}\n                key={atLimit ? \"limit\" : \"ok\"}\n                transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n              >\n                {atLimit ? <GaugeIcon /> : <SparklesIcon />}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n          <div className=\"grid min-w-0 gap-1\">\n            <h3 aria-live=\"polite\" className=\"text-balance font-semibold text-base tabular-nums\">\n              {atLimit ? `You've used all ${format(limit)} ${unit} ${period}` : `${format(limit - used)} ${unit} left ${period}`}\n            </h3>\n            <p className=\"text-pretty text-muted-foreground text-sm\">\n              {atLimit\n                ? `AI features are paused until your ${planName} plan resets. Everything else works as usual.`\n                : \"Paused features are back on.\"}\n            </p>\n          </div>\n        </div>\n\n        <div className=\"grid gap-2\">\n          <div className=\"flex items-baseline justify-between gap-3 text-sm\">\n            <span className=\"font-medium\">\n              <span className=\"sr-only\">{`${format(used)} of ${format(limit)} ${shortUnit} used`}</span>\n              <Roll reduce={reduce} text={format(used)} />\n              <span aria-hidden className=\"text-muted-foreground\">\n                {\" of \"}\n                <Roll reduce={reduce} text={format(limit)} /> {shortUnit}\n              </span>\n            </span>\n            <span className={cn(\"font-medium transition-colors duration-300\", atLimit ? \"text-warning-foreground\" : \"text-muted-foreground\")}>\n              <span className=\"sr-only\">{`${percent}%`}</span>\n              <Roll reduce={reduce} text={`${percent}%`} />\n            </span>\n          </div>\n          <div\n            aria-label={`${unit} used`}\n            aria-valuemax={limit}\n            aria-valuemin={0}\n            aria-valuenow={used}\n            className=\"h-2 overflow-hidden rounded-full bg-muted\"\n            role=\"meter\"\n          >\n            <motion.div\n              animate={{ scaleX: fraction }}\n              className={cn(\"h-full origin-left rounded-full transition-colors duration-500\", atLimit ? \"bg-warning\" : \"bg-primary\")}\n              initial={false}\n              transition={reduce ? { duration: 0 } : meterSpring}\n            />\n          </div>\n          <p className=\"flex items-center gap-1.5 text-muted-foreground text-xs\">\n            <ClockIcon aria-hidden className=\"size-3.5 shrink-0\" />\n            <span className=\"sr-only\">{`Resets in ${spoken(left)}, on ${resetDate}`}</span>\n            <span aria-hidden className=\"inline-flex items-baseline gap-1\">\n              Resets in\n              <span className=\"inline-flex items-baseline gap-1 font-medium text-foreground\">\n                <span>\n                  <Roll reduce={reduce} text={String(t.d)} />\n                  <span className=\"text-muted-foreground\">d</span>\n                </span>\n                <span>\n                  <Roll reduce={reduce} text={pad(t.h)} />\n                  <span className=\"text-muted-foreground\">h</span>\n                </span>\n                <span>\n                  <Roll reduce={reduce} text={pad(t.m)} />\n                  <span className=\"text-muted-foreground\">m</span>\n                </span>\n                <span>\n                  <Roll reduce={reduce} text={pad(t.s)} />\n                  <span className=\"text-muted-foreground\">s</span>\n                </span>\n              </span>\n              <span className=\"hidden @xs:inline\">· {resetDate}</span>\n            </span>\n          </p>\n        </div>\n\n        <div className=\"grid gap-4 @sm:grid-cols-2\">\n          <FeatureList items={stillWorks} on reduce={reduce} title=\"Still works\" />\n          <FeatureList items={paused} on={!atLimit} reduce={reduce} title={atLimit ? \"Paused\" : \"Back on\"} />\n        </div>\n      </div>\n\n      <div className=\"flex min-h-15 flex-wrap items-center gap-2 border-t bg-muted/40 px-5 py-3\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {atLimit ? (\n            <motion.div\n              animate={{ opacity: 1 }}\n              className=\"flex w-full flex-wrap gap-2 @max-sm:*:w-full\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0 }}\n              key=\"actions\"\n              transition={{ duration: 0.18 }}\n            >\n              {canUpgrade && upgrade && (\n                <Button loading={upgrading} onClick={onUpgrade}>\n                  Upgrade to {upgrade.plan}\n                  <span className=\"font-normal text-primary-foreground/70 tabular-nums in-data-loading:text-transparent\">\n                    {format(upgrade.limit)} {shortUnit}\n                  </span>\n                </Button>\n              )}\n              <Button\n                disabled={asked}\n                onClick={() => {\n                  onSecondary?.();\n                  if (secondaryDoneLabel) setAsked(true);\n                }}\n                variant=\"outline\"\n              >\n                {asked && <CheckIcon />}\n                {asked ? secondaryDoneLabel : secondaryLabel}\n              </Button>\n            </motion.div>\n          ) : (\n            <motion.p\n              animate={{ opacity: 1 }}\n              className=\"flex items-center gap-2 text-sm\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0 }}\n              key=\"ok\"\n              transition={{ duration: 0.18 }}\n            >\n              <span className=\"flex size-5 items-center justify-center rounded-full bg-success/12 text-success-foreground\">\n                <svg aria-hidden className=\"size-3\" fill=\"none\" viewBox=\"0 0 16 16\">\n                  <motion.path\n                    animate={{ pathLength: 1 }}\n                    d=\"M3.5 8.5l3 3 6-7\"\n                    initial={reduce ? false : { pathLength: 0 }}\n                    stroke=\"currentColor\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2.2}\n                    transition={{ duration: 0.32, delay: 0.15, ease: [0.65, 0, 0.35, 1] }}\n                  />\n                </svg>\n              </span>\n              {`You're on ${planName} · ${format(limit)} ${unit} a month`}\n            </motion.p>\n          )}\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n\nfunction FeatureList({ title, items, on, reduce }: { title: string; items: string[]; on: boolean; reduce: boolean }) {\n  return (\n    <div className=\"grid content-start gap-2\">\n      <h4 className=\"font-medium text-muted-foreground text-xs\">{title}</h4>\n      <ul className=\"grid gap-1.5\">\n        {items.map((item, i) => (\n          <li\n            className={cn(\"flex items-center gap-2 text-sm transition-colors duration-300\", on ? \"text-foreground\" : \"text-muted-foreground\")}\n            key={item}\n          >\n            <span className=\"relative flex size-4 shrink-0 items-center justify-center [&_svg]:size-4\">\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  animate={{ opacity: 1, scale: 1 }}\n                  className={on ? \"text-success-foreground\" : \"text-muted-foreground\"}\n                  exit={{ opacity: 0, scale: 0.6 }}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}\n                  key={on ? \"on\" : \"off\"}\n                  transition={{ ...glide, delay: reduce ? 0 : 0.12 + i * 0.04 }}\n                >\n                  {on ? <CheckIcon aria-hidden /> : <CirclePauseIcon aria-hidden />}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n            {item}\n            {!on && <span className=\"sr-only\">(paused)</span>}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
    }
  ]
}