{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radialflow",
  "type": "registry:ui",
  "title": "Radial Flow",
  "description": "Dynamic radial graph for visualizing networks & hierarchies with animated nodes.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": ["@scrollxui/badge"],
  "dependencies": [
    "motion",
    "class-variance-authority",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/radialflow.tsx",
      "content": "'use client';\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { motion } from 'motion/react';\nimport { Badge } from '@/components/ui/badge';\n\nexport interface Topic {\n  id: string;\n  name: string;\n  position: { x: number; y: number };\n  color: string;\n  highlighted: boolean;\n}\n\ninterface RadialFlowProps {\n  topics: Topic[];\n  badgeName: string;\n  centralDotColor?: string;\n}\n\nexport function RadialFlow({\n  topics,\n  badgeName,\n  centralDotColor = '#FFFFFF',\n}: RadialFlowProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n  const [isMobile, setIsMobile] = useState(false);\n\n  useEffect(() => {\n    const checkMobile = () => setIsMobile(window.innerWidth < 768);\n    checkMobile();\n    window.addEventListener('resize', checkMobile);\n    return () => window.removeEventListener('resize', checkMobile);\n  }, []);\n\n  const updateDimensions = useCallback(() => {\n    if (containerRef.current) {\n      setDimensions({\n        width: containerRef.current.offsetWidth,\n        height: containerRef.current.offsetHeight,\n      });\n    }\n  }, []);\n\n  useEffect(() => {\n    updateDimensions();\n    const resizeObserver = new ResizeObserver(() =>\n      requestAnimationFrame(updateDimensions),\n    );\n    if (containerRef.current) resizeObserver.observe(containerRef.current);\n    return () => resizeObserver.disconnect();\n  }, [updateDimensions]);\n\n  const getLabelPosition = useCallback(\n    (position: { x: number; y: number }) => {\n      if (!isMobile) return position;\n      return {\n        x:\n          position.x < 50\n            ? Math.min(position.x + 5, 20)\n            : Math.max(position.x - 5, 80),\n        y: position.y,\n      };\n    },\n    [isMobile],\n  );\n\n  const getPathData = useCallback(\n    (topic: Topic) => {\n      if (!dimensions.width || !dimensions.height) return '';\n\n      const centerX = dimensions.width / 2;\n      const centerY = dimensions.height / 2;\n      const pos = getLabelPosition(topic.position);\n      const x = (pos.x / 100) * dimensions.width;\n      const y = (pos.y / 100) * dimensions.height;\n\n      const controlX =\n        pos.x < 50 ? x + (centerX - x) * 0.75 : x - (x - centerX) * 0.75;\n\n      return `M ${x} ${y} Q ${controlX} ${y} ${centerX} ${centerY}`;\n    },\n    [dimensions, getLabelPosition],\n  );\n\n  const generateParticles = useCallback(\n    (topic: Topic) => {\n      if (!topic.highlighted) return null;\n\n      const pathData = getPathData(topic);\n      const eggWidth = 16;\n      const eggHeight = 10;\n\n      return (\n        <motion.g key={`particle-${topic.id}`}>\n          <motion.path\n            d={`M -${eggWidth / 2} 0 \n             a ${eggWidth / 2} ${eggHeight / 2} 0 1 0 ${eggWidth} 0 \n             a ${eggWidth / 2} ${eggHeight / 2} 0 1 0 -${eggWidth} 0`}\n            fill={topic.color}\n            initial={{ opacity: 0, scale: 0.8 }}\n            animate={{\n              opacity: [0, 0.8, 0],\n              scale: [0.8, 1.2, 0.8],\n            }}\n            transition={{\n              duration: 2.5,\n              repeat: Infinity,\n              repeatDelay: 1,\n              times: [0, 0.5, 1],\n            }}\n          >\n            <animateMotion\n              dur='3.5s'\n              repeatCount='indefinite'\n              path={pathData}\n              rotate='auto'\n              calcMode='spline'\n              keyPoints='0;1'\n              keyTimes='0;1'\n              keySplines='0.42 0 0.58 1'\n            />\n          </motion.path>\n        </motion.g>\n      );\n    },\n    [getPathData],\n  );\n\n  const getTopicLabelClasses = useCallback(\n    (topic: Topic) =>\n      `absolute transform -translate-x-1/2 -translate-y-1/2 bg-gray-900/90 text-white rounded-md \n     backdrop-blur-xs border transition-all duration-300 px-3 py-2 text-xs sm:text-sm\n     ${\n       topic.highlighted\n         ? 'border-yellow-400/30 shadow-glow'\n         : 'border-gray-800'\n     }\n     whitespace-normal wrap-break-word text-center leading-tight`,\n    [],\n  );\n\n  const getLabelStyle = useCallback(\n    (topic: Topic) => ({\n      left: `${getLabelPosition(topic.position).x}%`,\n      top: `${getLabelPosition(topic.position).y}%`,\n      color: topic.color,\n      maxWidth: isMobile ? '140px' : '200px',\n      minWidth: '80px',\n      lineHeight: '1.2',\n    }),\n    [isMobile, getLabelPosition],\n  );\n\n  return (\n    <div\n      ref={containerRef}\n      className='w-full h-full  relative overflow-hidden min-h-75'\n    >\n      <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-20 mb-8'>\n        <Badge variant='default' shiny={true}>\n          {badgeName}\n        </Badge>\n      </div>\n\n      {dimensions.width > 0 && (\n        <svg\n          className='absolute inset-0 w-full h-full'\n          viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}\n        >\n          <defs>\n            <filter id='glow' x='-50%' y='-50%' width='200%' height='200%'>\n              <feGaussianBlur stdDeviation='2' result='coloredBlur' />\n              <feMerge>\n                <feMergeNode in='coloredBlur' />\n                <feMergeNode in='SourceGraphic' />\n              </feMerge>\n            </filter>\n          </defs>\n\n          {topics.map((topic) => (\n            <path\n              key={`path-${topic.id}`}\n              d={getPathData(topic)}\n              stroke={topic.highlighted ? topic.color : '#374151'}\n              strokeWidth='1'\n              strokeOpacity={topic.highlighted ? 0.4 : 0.2}\n              fill='none'\n            />\n          ))}\n\n          {topics.map((topic) => generateParticles(topic))}\n\n          <motion.circle\n            cx={dimensions.width / 2}\n            cy={dimensions.height / 2}\n            r='4'\n            fill={centralDotColor}\n            initial={{ scale: 0 }}\n            animate={{ scale: 1 }}\n            transition={{ duration: 0.5 }}\n          />\n        </svg>\n      )}\n\n      {topics.map((topic) => (\n        <div\n          key={`label-${topic.id}`}\n          className={getTopicLabelClasses(topic)}\n          style={getLabelStyle(topic)}\n        >\n          {topic.name}\n        </div>\n      ))}\n    </div>\n  );\n}\n"
    },
    {
      "type": "registry:ui",
      "path": "components/ui/badge.tsx",
      "content": "import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\n\nconst badgeVariants = cva(\n  'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2',\n  {\n    variants: {\n      variant: {\n        default:\n          'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        destructive:\n          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',\n        outline: 'text-foreground',\n      },\n      shiny: {\n        true: 'relative overflow-hidden',\n        false: '',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      shiny: false,\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends\n    React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {\n  shiny?: boolean;\n  shinySpeed?: number;\n}\n\nfunction Badge({\n  className,\n  variant,\n  shiny = false,\n  shinySpeed = 5,\n  children,\n  ...props\n}: BadgeProps) {\n  const animationDuration = `${shinySpeed}s`;\n\n  return (\n    <div\n      className={cn(badgeVariants({ variant, shiny }), className)}\n      {...props}\n    >\n      <span className={shiny ? 'relative z-10' : ''}>{children}</span>\n\n      {shiny && (\n        <span\n          className='absolute inset-0 pointer-events-none animate-shine dark:hidden'\n          style={{\n            background:\n              'linear-gradient(120deg, transparent 40%, rgba(255,255,255,0.6) 50%, transparent 60%)',\n            backgroundSize: '200% 100%',\n            animationDuration,\n            mixBlendMode: 'screen',\n          }}\n        />\n      )}\n\n      {shiny && (\n        <span\n          className='absolute inset-0 pointer-events-none animate-shine hidden dark:block'\n          style={{\n            background:\n              'linear-gradient(120deg, transparent 40%, rgba(0,0,150,0.25) 50%, transparent 60%)',\n            backgroundSize: '200% 100%',\n            animationDuration,\n            mixBlendMode: 'multiply',\n          }}\n        />\n      )}\n    </div>\n  );\n}\n\nexport { Badge, badgeVariants };\n"
    },
    {
      "type": "registry:lib",
      "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}"
    }
  ]
}
