{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "usage-meter-circle",
  "title": "Usage Meter Circle",
  "description": "A usage meter circle component",
  "dependencies": ["lucide-react", "motion"],
  "registryDependencies": ["card", "badge", "utils"],
  "files": [
    {
      "path": "src/registry/billingsdk/usage-meter.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { motion, useSpring, useMotionValue, useTransform } from \"motion/react\";\nimport { useEffect } from \"react\";\nimport type React from \"react\";\n\nexport interface Usage {\n  name: string;\n  usage: number;\n  limit: number;\n}\n\nexport interface UsageMeterProps {\n  usage: Usage[];\n  className?: string;\n  variant?: \"linear\" | \"circle\";\n  size?: \"sm\" | \"md\" | \"lg\";\n  title?: string;\n  description?: string;\n  progressColor?: \"default\" | \"usage\";\n}\n\nfunction CircleUsageItem({\n  item,\n  config,\n  radius,\n  circumference,\n  progressColor,\n  getUsageClasses,\n  getStatus,\n}: {\n  item: Usage;\n  config: { circle: number; stroke: number; text: string; label: string };\n  radius: number;\n  circumference: number;\n  progressColor: \"default\" | \"usage\";\n  getUsageClasses: (\n    percentage: number,\n    variant: \"circle\" | \"linear\",\n  ) => string[];\n  getStatus: (percentage: number) => React.ReactNode;\n}) {\n  const percentage = Math.min((item.usage / item.limit) * 100, 100);\n  const remaining = Math.max(item.limit - item.usage, 0);\n\n  const motionValue = useMotionValue(0);\n  const springValue = useSpring(motionValue, { stiffness: 100, damping: 20 });\n  const display = useTransform(\n    springValue,\n    (latest) => `${Math.round(latest)}%`,\n  );\n\n  useEffect(() => {\n    motionValue.set(percentage);\n  }, [percentage, motionValue]);\n\n  return (\n    <div className=\"bg-muted/20 flex flex-col items-center space-y-3 rounded-xl p-4 text-center\">\n      <span className=\"w-full truncate text-sm font-medium\">{item.name}</span>\n      <div className=\"relative\">\n        <svg\n          width={config.circle}\n          height={config.circle}\n          className=\"-rotate-90\"\n        >\n          <circle\n            cx={config.circle / 2}\n            cy={config.circle / 2}\n            r={radius}\n            strokeWidth={config.stroke}\n            className=\"text-muted stroke-current\"\n            fill=\"transparent\"\n          />\n          <motion.circle\n            cx={config.circle / 2}\n            cy={config.circle / 2}\n            r={radius}\n            strokeWidth={config.stroke}\n            fill=\"transparent\"\n            strokeDasharray={circumference}\n            strokeLinecap=\"round\"\n            className={cn(\n              \"stroke-current\",\n              progressColor === \"usage\"\n                ? getUsageClasses(percentage, \"circle\")\n                : \"text-primary\",\n            )}\n            initial={{ strokeDashoffset: circumference }}\n            animate={{\n              strokeDashoffset:\n                circumference - (percentage / 100) * circumference,\n            }}\n            transition={{ duration: 0.5, ease: \"easeOut\" }}\n          />\n        </svg>\n        <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n          <motion.span className={cn(\"font-semibold\", config.text)}>\n            {display}\n          </motion.span>\n          <span className={cn(\"text-muted-foreground\", config.label)}>\n            used\n          </span>\n        </div>\n      </div>\n      <span className=\"text-muted-foreground text-xs\">\n        {remaining.toLocaleString()} / {item.limit.toLocaleString()} left\n      </span>\n      {getStatus(percentage)}\n    </div>\n  );\n}\n\nfunction LinearUsageItem({\n  item,\n  config,\n  progressColor,\n  getUsageClasses,\n  getStatus,\n}: {\n  item: Usage;\n  config: { bar: string; text: string };\n  progressColor: \"default\" | \"usage\";\n  getUsageClasses: (\n    percentage: number,\n    variant: \"circle\" | \"linear\",\n  ) => string[];\n  getStatus: (percentage: number) => React.ReactNode;\n}) {\n  const percentage = Math.min((item.usage / item.limit) * 100, 100);\n  const remaining = Math.max(item.limit - item.usage, 0);\n\n  const motionValue = useMotionValue(0);\n  const springValue = useSpring(motionValue, { stiffness: 100, damping: 20 });\n  const display = useTransform(\n    springValue,\n    (latest) => `${Math.round(latest)}%`,\n  );\n\n  useEffect(() => {\n    motionValue.set(percentage);\n  }, [percentage, motionValue]);\n\n  return (\n    <div className=\"bg-muted/20 space-y-2 rounded-xl p-4\">\n      <div className=\"flex items-center justify-between\">\n        <span className=\"truncate text-sm font-medium\">{item.name}</span>\n        <motion.span className=\"text-muted-foreground text-xs\">\n          {display}\n        </motion.span>\n      </div>\n      <div\n        className={cn(\n          \"bg-muted w-full overflow-hidden rounded-full\",\n          config.bar,\n        )}\n      >\n        <motion.div\n          className={cn(\n            \"rounded-full bg-gradient-to-r\",\n            config.bar,\n            progressColor === \"usage\"\n              ? getUsageClasses(percentage, \"linear\")\n              : \"from-primary to-primary/70\",\n          )}\n          initial={{ width: 0 }}\n          animate={{ width: `${percentage}%` }}\n          transition={{ duration: 0.5, ease: \"easeOut\" }}\n        />\n      </div>\n      <div className=\"text-muted-foreground flex items-center justify-between text-xs\">\n        <span>\n          {remaining.toLocaleString()} / {item.limit.toLocaleString()} left\n        </span>\n        {getStatus(percentage)}\n      </div>\n    </div>\n  );\n}\n\nexport function UsageMeter({\n  usage,\n  className,\n  variant = \"linear\",\n  size = \"md\",\n  title,\n  description,\n  progressColor = \"default\",\n}: UsageMeterProps) {\n  if (!usage?.length) return null;\n\n  const getStatus = (percentage: number) => {\n    if (percentage >= 90) return <Badge variant=\"destructive\">Critical</Badge>;\n    if (percentage >= 75) return <Badge variant=\"secondary\">High</Badge>;\n    return null;\n  };\n  const getUsageClasses = (\n    percentage: number,\n    variant: \"circle\" | \"linear\",\n  ): string[] => {\n    const thresholds = [\n      {\n        min: 90,\n        circle: \"text-red-500\",\n        linear: [\"from-red-500\", \"to-red-400\"],\n      },\n      {\n        min: 75,\n        circle: \"text-yellow-500\",\n        linear: [\"from-yellow-500\", \"to-yellow-400\"],\n      },\n      {\n        min: 50,\n        circle: \"text-emerald-500\",\n        linear: [\"from-emerald-500\", \"to-emerald-400\"],\n      },\n      {\n        min: 25,\n        circle: \"text-blue-500\",\n        linear: [\"from-blue-500\", \"to-blue-400\"],\n      },\n      {\n        min: 0,\n        circle: \"text-gray-500\",\n        linear: [\"from-gray-500\", \"to-gray-400\"],\n      },\n    ];\n    const match = thresholds.find((t) => percentage >= t.min);\n\n    if (match) {\n      return variant === \"circle\" ? [match.circle] : match.linear;\n    }\n\n    return variant === \"circle\"\n      ? [\"text-gray-500\"]\n      : [\"from-gray-500\", \"to-gray-400\"];\n  };\n\n  if (variant === \"circle\") {\n    const sizeConfig = {\n      sm: { circle: 100, stroke: 6, text: \"text-lg\", label: \"text-xs\" },\n      md: { circle: 140, stroke: 10, text: \"text-xl\", label: \"text-sm\" },\n      lg: { circle: 180, stroke: 12, text: \"text-2xl\", label: \"text-base\" },\n    };\n\n    const config = sizeConfig[size];\n    const radius = (config.circle - config.stroke) / 2;\n    const circumference = radius * 2 * Math.PI;\n\n    return (\n      <Card className={cn(\"w-auto\", className)}>\n        {(title || description) && (\n          <CardHeader className=\"space-y-1\">\n            {title && (\n              <CardTitle className=\"truncate text-base leading-tight font-medium\">\n                {title}\n              </CardTitle>\n            )}\n            {description && (\n              <CardDescription className=\"text-muted-foreground text-sm\">\n                {description}\n              </CardDescription>\n            )}\n          </CardHeader>\n        )}\n        <CardContent\n          className={\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\"}\n        >\n          {usage.map((item, i) => (\n            <CircleUsageItem\n              key={item.name || i}\n              item={item}\n              config={config}\n              radius={radius}\n              circumference={circumference}\n              progressColor={progressColor}\n              getUsageClasses={getUsageClasses}\n              getStatus={getStatus}\n            />\n          ))}\n        </CardContent>\n      </Card>\n    );\n  }\n\n  // Linear variant\n  const sizeConfig = {\n    sm: { bar: \"h-2\", text: \"text-xs\" },\n    md: { bar: \"h-3\", text: \"text-sm\" },\n    lg: { bar: \"h-4\", text: \"text-base\" },\n  };\n\n  const config = sizeConfig[size];\n\n  return (\n    <Card className={cn(\"w-full max-w-md\", className)}>\n      {(title || description) && (\n        <CardHeader className=\"space-y-1\">\n          {title && (\n            <CardTitle className=\"truncate text-base leading-tight font-medium\">\n              {title}\n            </CardTitle>\n          )}\n          {description && (\n            <CardDescription className=\"text-muted-foreground text-sm\">\n              {description}\n            </CardDescription>\n          )}\n        </CardHeader>\n      )}\n      <CardContent className={\"grid grid-cols-1 gap-4\"}>\n        {usage.map((item, i) => (\n          <LinearUsageItem\n            key={item.name || i}\n            item={item}\n            config={config}\n            progressColor={progressColor}\n            getUsageClasses={getUsageClasses}\n            getStatus={getStatus}\n          />\n        ))}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/billingsdk/usage-meter.tsx"
    },
    {
      "path": "src/registry/billingsdk/demo/usage-meter-circle-demo.tsx",
      "content": "\"use client\";\n\nimport { UsageMeter } from \"@/components/billingsdk/usage-meter\";\n\nexport default function UsageMeterCircleDemo() {\n  return (\n    <div className=\"mx-auto flex w-full flex-col gap-4\">\n      <UsageMeter\n        usage={[\n          {\n            name: \"Claude Sonnet 4\",\n            usage: 75,\n            limit: 100,\n          },\n          {\n            name: \"ChatGPT 5\",\n            usage: 12,\n            limit: 100,\n          },\n          {\n            name: \"Grok 3\",\n            usage: 57,\n            limit: 100,\n          },\n          {\n            name: \"Gemini 2.5\",\n            usage: 95,\n            limit: 100,\n          },\n        ]}\n        title=\"LLM Usage\"\n        description=\"Your usage of the LLM models\"\n        variant=\"circle\"\n        size=\"md\"\n        className=\"mx-auto\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/usage-meter-circle-demo.tsx"
    }
  ],
  "type": "registry:block"
}
