{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-radial-chart",
  "title": "Animated radial chart",
  "description": "Animated radial chart is a production-ready charts component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/charts/animated-radial-chart.tsx",
      "content": "\"use client\"\n\nimport { motion, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform, animate } from \"framer-motion\"\nimport { useEffect, useId, useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface AnimatedRadialChartProps {\n  value?: number\n  size?: number\n  strokeWidth?: number\n  className?: string\n  showLabels?: boolean\n  duration?: number\n}\n\nexport function AnimatedRadialChart({ \n  value = 74, \n  size = 300,\n  strokeWidth: customStrokeWidth,\n  className,\n  showLabels = true,\n  duration = 2\n}: AnimatedRadialChartProps) {\n  const shouldReduceMotion = useReducedMotion()\n  const id = useId().replace(/:/g, \"\")\n  const [displayValue, setDisplayValue] = useState(0)\n  const [isLight, setIsLight] = useState(false)\n  const animationDuration = shouldReduceMotion ? 0.25 : duration\n  // Dynamic stroke width based on size if not provided\n  const strokeWidth = customStrokeWidth ?? Math.max(12, size * 0.06)\n  const radius = size * 0.35\n  const center = size / 2\n  const circumference = Math.PI * radius\n\n  // Calculate inner line radius (4px inside the main arc)\n  const innerLineRadius = radius - strokeWidth - 4\n\n  // Motion values for animation\n  const animatedValue = useMotionValue(0)\n  const offset = useTransform(animatedValue, [0, 100], [circumference, 0])\n\n  useMotionValueEvent(animatedValue, \"change\", (latest) => {\n    setDisplayValue(Math.round(latest))\n  })\n\n  useEffect(() => {\n    const updateTheme = () => {\n      setIsLight(document.documentElement.classList.contains(\"light\"))\n    }\n\n    updateTheme()\n\n    const observer = new MutationObserver(updateTheme)\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    })\n\n    return () => observer.disconnect()\n  }, [])\n\n  // Calculate animated positions\n  const progressAngle = useTransform(animatedValue, [0, 100], [-Math.PI, 0])\n  const innerRadius = radius - strokeWidth / 2\n\n  // Animate to the target value on mount or when value changes\n  useEffect(() => {\n    const controls = animate(animatedValue, value, {\n      duration: animationDuration,\n      ease: \"easeOut\",\n    })\n\n    return controls.stop\n  }, [value, animatedValue, animationDuration])\n\n  // Calculate responsive font size\n  const fontSize = Math.max(16, size * 0.1)\n  const labelFontSize = Math.max(12, size * 0.04)\n  const baseStops = isLight\n    ? {\n        start: \"#f7f5ef\",\n        mid: \"#c8c3b8\",\n        end: \"#7f858c\",\n        inner: \"#a8a298\",\n        label: \"#7c8794\",\n        textStart: \"#1d1d1d\",\n        textMid: \"#4b5563\",\n        textEnd: \"#9ca3af\",\n        shadow: \"#6f675d\",\n        shadowOpacity: \"0.18\",\n      }\n    : {\n        start: \"#ffffff\",\n        mid: \"#d1d5db\",\n        end: \"#6b7280\",\n        inner: \"#6b7280\",\n        label: \"#9ca3af\",\n        textStart: \"#ffffff\",\n        textMid: \"#d1d5db\",\n        textEnd: \"#6b7280\",\n        shadow: \"#000000\",\n        shadowOpacity: \"0.3\",\n      }\n\n  return (\n    <div className={cn(\"relative\", className)} style={{ width: size, height: size * 0.7 }}>\n      <svg width={size} height={size * 0.7} viewBox={`0 0 ${size} ${size * 0.7}`} className=\"overflow-visible\">\n        <defs>\n          {/* Base track gradient - white to silver/gray */}\n          <linearGradient id={`baseGradient-${id}`} x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\">\n            <stop offset=\"0%\" stopColor={baseStops.start} stopOpacity=\"0.9\" />\n            <stop offset=\"50%\" stopColor={baseStops.mid} stopOpacity=\"0.72\" />\n            <stop offset=\"100%\" stopColor={baseStops.end} stopOpacity=\"0.62\" />\n          </linearGradient>\n\n          {/* Progress gradient - orange */}\n          <linearGradient id={`progressGradient-${id}`} x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n            <stop offset=\"0%\" stopColor=\"#f97316\" />\n            <stop offset=\"50%\" stopColor=\"#ea580c\" />\n            <stop offset=\"100%\" stopColor=\"#dc2626\" />\n          </linearGradient>\n\n          {/* Text gradient - white to gray */}\n          <linearGradient id={`textGradient-${id}`} x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n            <stop offset=\"0%\" stopColor={baseStops.textStart} stopOpacity=\"0.78\" />\n            <stop offset=\"50%\" stopColor={baseStops.textMid} stopOpacity=\"0.62\" />\n            <stop offset=\"100%\" stopColor={baseStops.textEnd} stopOpacity=\"0.46\" />\n          </linearGradient>\n\n          {/* Drop shadow filter */}\n          <filter id={`dropshadow-${id}`} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n            <feDropShadow dx=\"0\" dy=\"2\" stdDeviation=\"3\" floodColor={baseStops.shadow} floodOpacity={baseStops.shadowOpacity} />\n          </filter>\n        </defs>\n\n        {/* Inner thin line (1px light gray) */}\n        <path\n          d={`M ${center - innerLineRadius} ${center} A ${innerLineRadius} ${innerLineRadius} 0 0 1 ${center + innerLineRadius} ${center}`}\n          fill=\"none\"\n          stroke={baseStops.inner}\n          strokeWidth=\"1\"\n          strokeLinecap=\"butt\"\n          opacity=\"0.6\"\n        />\n\n        {/* Base track */}\n        <path\n          d={`M ${center - radius} ${center} A ${radius} ${radius} 0 0 1 ${center + radius} ${center}`}\n          fill=\"none\"\n          stroke={`url(#baseGradient-${id})`}\n          strokeWidth={strokeWidth}\n          strokeLinecap=\"butt\"\n          filter={`url(#dropshadow-${id})`}\n        />\n\n        {/* Animated Progress track */}\n        <motion.path\n          d={`M ${center - radius} ${center} A ${radius} ${radius} 0 0 1 ${center + radius} ${center}`}\n          fill=\"none\"\n          stroke={`url(#progressGradient-${id})`}\n          strokeWidth={strokeWidth}\n          strokeLinecap=\"butt\"\n          strokeDasharray={circumference}\n          strokeDashoffset={offset}\n          filter={`url(#dropshadow-${id})`}\n        />\n\n        {/* Animated extending line */}\n        <motion.line\n          x1={useTransform(progressAngle, (angle) => center + Math.cos(angle) * innerRadius)}\n          y1={useTransform(progressAngle, (angle) => center + Math.sin(angle) * innerRadius)}\n          x2={useTransform(progressAngle, (angle) => center + Math.cos(angle) * innerRadius - Math.cos(angle) * 30)}\n          y2={useTransform(progressAngle, (angle) => center + Math.sin(angle) * innerRadius - Math.sin(angle) * 30)}\n          stroke={`url(#textGradient-${id})`}\n          strokeWidth=\"1\"\n          strokeLinecap=\"butt\"\n        />\n      </svg>\n\n      {/* Animated center percentage display with gradient text */}\n      <div className=\"pointer-events-none absolute inset-0 z-10 flex items-center justify-center\">\n        <motion.div\n          className=\"font-bold tracking-tight mt-10\"\n          style={{ fontSize: `${fontSize}px` }}\n          initial={false}\n          animate={{ scale: 1 }}\n          transition={{ duration: shouldReduceMotion ? 0.15 : 0.5, delay: shouldReduceMotion ? 0 : animationDuration * 0.75 }}\n        >\n          {isLight ? (\n            <span className=\"text-[#242424]\">\n              <span>{displayValue}</span>%\n            </span>\n          ) : (\n            <span\n              style={{\n                background: \"linear-gradient(to right, #ffffff, #9ca3af)\",\n                WebkitBackgroundClip: \"text\",\n                WebkitTextFillColor: \"transparent\",\n                backgroundClip: \"text\",\n              }}\n            >\n              <span>{displayValue}</span>%\n            </span>\n          )}\n        </motion.div>\n      </div>\n\n      {/* 0% and 100% labels */}\n      {showLabels && (\n        <>\n          <motion.div\n            className=\"absolute font-medium\"\n            style={{\n              fontSize: `${labelFontSize}px`,\n              left: center - radius - 5,\n              top: center + strokeWidth / 2,\n              color: baseStops.label,\n            }}\n            initial={{ opacity: 0, y: 10 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: shouldReduceMotion ? 0.15 : 0.5, delay: shouldReduceMotion ? 0 : animationDuration * 0.25 }}\n          >\n            0%\n          </motion.div>\n          <motion.div\n            className=\"absolute font-medium\"\n            style={{\n              fontSize: `${labelFontSize}px`,\n              left: center + radius - 20,\n              top: center + strokeWidth / 2,\n              color: baseStops.label,\n            }}\n            initial={{ opacity: 0, y: 10 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: shouldReduceMotion ? 0.15 : 0.5, delay: shouldReduceMotion ? 0 : animationDuration * 0.25 }}\n          >\n            100%\n          </motion.div>\n        </>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/charts/animated-radial-chart.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.72)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.52)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.36)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.52)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.36)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#ec5c13",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Charts",
    "registryUrl": "https://ui.isaiahbjork.com/animated-radial-chart.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/animated-radial-chart.json"
  },
  "type": "registry:component"
}