{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "announcement",
  "type": "registry:ui",
  "title": "Announcement",
  "description": "An announcement component that highlights key info with icons, effects, and expandable content.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": ["@scrollxui/badge", "@scrollxui/lustretext"],
  "dependencies": [
    "motion",
    "class-variance-authority",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/announcement.tsx",
      "content": "'use client';\nimport React, { useState, useRef, useEffect, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { ComponentProps, HTMLAttributes, ReactNode } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { cn } from '@/lib/utils';\nimport {\n  motion,\n  AnimatePresence,\n  useAnimationFrame,\n  useMotionTemplate,\n  useMotionValue,\n  useTransform,\n} from 'motion/react';\nimport { ChevronDown } from 'lucide-react';\nimport LustreText from '@/components/ui/lustretext';\n\nconst EXPANDABLE_CONTENT_SYMBOL = Symbol.for('AnnouncementExpandedContent');\n\nconst MovingBorder = ({\n  children,\n  duration = 3000,\n  rx,\n  ry,\n  ...otherProps\n}: {\n  children: React.ReactNode;\n  duration?: number;\n  rx?: string;\n  ry?: string;\n} & React.SVGProps<SVGSVGElement>) => {\n  const pathRef = useRef<SVGRectElement | null>(null);\n  const progress = useMotionValue(0);\n\n  useAnimationFrame((time) => {\n    const length = pathRef.current?.getTotalLength?.();\n    if (length) {\n      const pxPerMillisecond = length / duration;\n      progress.set((time * pxPerMillisecond) % length);\n    }\n  });\n\n  const x = useTransform(\n    progress,\n    (val) => pathRef.current?.getPointAtLength(val).x ?? 0,\n  );\n  const y = useTransform(\n    progress,\n    (val) => pathRef.current?.getPointAtLength(val).y ?? 0,\n  );\n  const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;\n\n  return (\n    <>\n      <svg\n        xmlns='http://www.w3.org/2000/svg'\n        preserveAspectRatio='none'\n        className='absolute h-full w-full'\n        width='100%'\n        height='100%'\n        {...otherProps}\n      >\n        <rect\n          fill='none'\n          width='100%'\n          height='100%'\n          rx={rx}\n          ry={ry}\n          ref={pathRef}\n        />\n      </svg>\n      <motion.div\n        style={{\n          position: 'absolute',\n          top: 0,\n          left: 0,\n          display: 'inline-block',\n          transform,\n        }}\n      >\n        {children}\n      </motion.div>\n    </>\n  );\n};\n\nexport type AnnouncementProps = Omit<ComponentProps<typeof Badge>, 'ref'> & {\n  styled?: boolean;\n  animation?: 'fade';\n  icon?: ReactNode;\n  iconPosition?: 'left' | 'right';\n  shiny?: boolean;\n  movingBorder?: boolean;\n  movingBorderDuration?: number;\n  movingBorderClassName?: string;\n};\n\nfunction AnnouncementComponent({\n  variant = 'outline',\n  styled = false,\n  animation = 'fade',\n  icon,\n  iconPosition = 'left',\n  shiny = false,\n  movingBorder = false,\n  movingBorderDuration = 3000,\n  movingBorderClassName,\n  className,\n  children,\n  ...props\n}: AnnouncementProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [dropdownPosition, setDropdownPosition] = useState({\n    top: 0,\n    left: 0,\n    width: 0,\n  });\n  const containerRef = useRef<HTMLDivElement>(null);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  const isMounted = typeof window !== 'undefined';\n\n  const { expandedContent, mainContent, hasExpandable } = React.useMemo(() => {\n    const childArray = React.Children.toArray(children);\n    const main: ReactNode[] = [];\n    let expanded: ReactNode = null;\n    let found = false;\n\n    childArray.forEach((child) => {\n      if (\n        React.isValidElement(child) &&\n        (child.type as unknown as { [key: symbol]: boolean })?.[\n          EXPANDABLE_CONTENT_SYMBOL\n        ]\n      ) {\n        expanded = (child.props as { children?: ReactNode }).children;\n        found = true;\n      } else {\n        main.push(child);\n      }\n    });\n\n    return {\n      expandedContent: expanded,\n      mainContent: main,\n      hasExpandable: found,\n    };\n  }, [children]);\n\n  const updatePosition = useCallback(() => {\n    if (containerRef.current) {\n      const rect = containerRef.current.getBoundingClientRect();\n      setDropdownPosition({\n        top: rect.bottom + window.scrollY,\n        left: rect.left + window.scrollX,\n        width: rect.width,\n      });\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!isOpen || !hasExpandable) return;\n\n    updatePosition();\n\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as Node;\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(target) &&\n        dropdownRef.current &&\n        !dropdownRef.current.contains(target)\n      ) {\n        setIsOpen(false);\n      }\n    };\n\n    const handleEscape = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        setIsOpen(false);\n      }\n    };\n\n    const handleScroll = () => {\n      updatePosition();\n    };\n\n    const handleResize = () => {\n      updatePosition();\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    document.addEventListener('keydown', handleEscape);\n    window.addEventListener('scroll', handleScroll, true);\n    window.addEventListener('resize', handleResize);\n\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n      document.removeEventListener('keydown', handleEscape);\n      window.removeEventListener('scroll', handleScroll, true);\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [isOpen, hasExpandable, updatePosition]);\n\n  const animations = {\n    fade: {\n      initial: { opacity: 0 },\n      animate: { opacity: 1 },\n      exit: { opacity: 0 },\n    },\n  };\n\n  const displayContent = shiny\n    ? React.Children.map(mainContent, (child) =>\n        typeof child === 'string' ? <LustreText text={child} /> : child,\n      )\n    : mainContent;\n\n  const handleToggle = useCallback((e: React.MouseEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsOpen((prev) => !prev);\n  }, []);\n\n  const badgeContent = (\n    <Badge\n      className={cn(\n        'group relative max-w-full overflow-hidden rounded-full bg-background px-4 py-1.5 font-medium shadow-xs',\n        styled && 'border-foreground/5',\n        className,\n      )}\n      variant={variant}\n      data-expandable={hasExpandable}\n      {...props}\n    >\n      <div className='relative flex items-center gap-2'>\n        {icon && iconPosition === 'left' && (\n          <span className='shrink-0'>{icon}</span>\n        )}\n        <div className='flex-1 flex items-center gap-2 truncate'>\n          {displayContent}\n        </div>\n        {icon && iconPosition === 'right' && (\n          <span className='shrink-0'>{icon}</span>\n        )}\n        {hasExpandable ? (\n          <button\n            type='button'\n            onClick={handleToggle}\n            data-state={isOpen ? 'open' : 'closed'}\n            className='flex shrink-0 items-center rounded p-1 hover:bg-foreground/10 transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ml-1'\n            aria-expanded={isOpen}\n            aria-haspopup='true'\n            aria-label={\n              isOpen ? 'Collapse announcement' : 'Expand announcement'\n            }\n          >\n            <ChevronDown\n              className='size-3 shrink-0 transition-transform duration-300 data-[state=open]:rotate-180'\n              aria-hidden='true'\n              data-state={isOpen ? 'open' : 'closed'}\n            />\n          </button>\n        ) : null}\n      </div>\n    </Badge>\n  );\n\n  return (\n    <>\n      <motion.div\n        ref={containerRef}\n        data-state={isOpen ? 'open' : 'closed'}\n        {...animations[animation]}\n        transition={{ duration: 0.3, ease: 'easeOut' }}\n        className='relative inline-block'\n      >\n        {movingBorder ? (\n          <div className='relative overflow-hidden rounded-full bg-transparent p-px'>\n            <div className='absolute inset-0 pointer-events-none'>\n              <MovingBorder duration={movingBorderDuration} rx='50%' ry='50%'>\n                <div\n                  className={cn(\n                    'h-20 w-20 bg-[radial-gradient(#0ea5e9_40%,transparent_60%)] opacity-[0.8]',\n                    movingBorderClassName,\n                  )}\n                />\n              </MovingBorder>\n            </div>\n            {badgeContent}\n          </div>\n        ) : (\n          badgeContent\n        )}\n      </motion.div>\n      {isMounted &&\n        hasExpandable &&\n        createPortal(\n          <AnimatePresence>\n            {isOpen && (\n              <motion.div\n                ref={dropdownRef}\n                initial={{ opacity: 0, scale: 0.95, y: -10 }}\n                animate={{ opacity: 1, scale: 1, y: 0 }}\n                exit={{ opacity: 0, scale: 0.95, y: -10 }}\n                transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}\n                style={{\n                  position: 'absolute',\n                  top: `${dropdownPosition.top + 8}px`,\n                  left: `${dropdownPosition.left}px`,\n                  width: `${Math.max(dropdownPosition.width, 200)}px`,\n                  zIndex: 50,\n                }}\n                className='rounded-lg border bg-popover text-popover-foreground p-4 text-sm shadow-lg'\n                role='dialog'\n                aria-modal='false'\n                aria-label='Expanded announcement content'\n              >\n                {expandedContent}\n              </motion.div>\n            )}\n          </AnimatePresence>,\n          document.body,\n        )}\n    </>\n  );\n}\n\nexport const Announcement = AnnouncementComponent;\n\nexport type AnnouncementTagProps = HTMLAttributes<HTMLSpanElement> & {\n  lustre?: boolean;\n  movingBorder?: boolean;\n  movingBorderDuration?: number;\n  movingBorderClassName?: string;\n};\n\nexport function AnnouncementTag({\n  className,\n  lustre = false,\n  movingBorder = false,\n  movingBorderDuration = 3000,\n  movingBorderClassName,\n  children,\n  ...props\n}: AnnouncementTagProps) {\n  const tagContent = (\n    <span\n      className={cn(\n        'relative inline-flex items-center gap-2 px-3 py-1 text-xs font-semibold rounded-full bg-background',\n        className,\n      )}\n      {...props}\n    >\n      <span className='absolute inset-0 rounded-full bg-foreground/5 opacity-70 pointer-events-none' />\n      <span className='relative z-10'>\n        {React.Children.map(children, (child) =>\n          lustre && typeof child === 'string' ? (\n            <LustreText text={child} />\n          ) : (\n            child\n          ),\n        )}\n      </span>\n    </span>\n  );\n\n  if (movingBorder) {\n    return (\n      <span className='relative inline-block overflow-hidden rounded-full p-px'>\n        <span className='absolute inset-0 pointer-events-none'>\n          <MovingBorder duration={movingBorderDuration} rx='50%' ry='50%'>\n            <div\n              className={cn(\n                'h-12 w-12 bg-[radial-gradient(#0ea5e9_40%,transparent_60%)] opacity-80',\n                movingBorderClassName,\n              )}\n            />\n          </MovingBorder>\n        </span>\n        <span className='relative'>{tagContent}</span>\n      </span>\n    );\n  }\n\n  return tagContent;\n}\n\nexport type AnnouncementTitleProps = HTMLAttributes<HTMLSpanElement> & {\n  multiTags?: boolean;\n  lustre?: boolean;\n};\n\nexport function AnnouncementTitle({\n  className,\n  multiTags = false,\n  lustre = false,\n  children,\n  ...props\n}: AnnouncementTitleProps) {\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center gap-1.5 py-1',\n        multiTags ? 'flex-wrap' : 'truncate',\n        className,\n      )}\n      {...props}\n    >\n      {React.Children.map(children, (child) =>\n        lustre && typeof child === 'string' ? (\n          <LustreText text={child} />\n        ) : (\n          child\n        ),\n      )}\n    </span>\n  );\n}\n\nexport type AnnouncementContainerProps = HTMLAttributes<HTMLDivElement>;\n\nexport function AnnouncementContainer({\n  className,\n  ...props\n}: AnnouncementContainerProps) {\n  return (\n    <div\n      className={cn('flex flex-wrap items-center gap-1.5', className)}\n      {...props}\n    />\n  );\n}\n\nexport function AnnouncementExpandedContent({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  return null;\n}\n\n(AnnouncementExpandedContent as unknown as { [key: symbol]: boolean })[\n  EXPANDABLE_CONTENT_SYMBOL\n] = true;\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:ui",
      "path": "components/ui/lustretext.tsx",
      "content": "import React from \"react\";\n\ninterface LustreTextProps {\n  text: string;\n  disabled?: boolean;\n  speed?: number;\n  className?: string;\n}\n\nconst LustreText: React.FC<LustreTextProps> = ({\n  text,\n  disabled = false,\n  speed = 5,\n  className = \"\",\n}) => {\n  const animationStyle = {\n    animationDuration: `${speed}s`,\n    animationTimingFunction: \"linear\",\n    animationIterationCount: \"infinite\",\n    animationFillMode: \"forwards\",\n  };\n\n  return (\n    <span\n      className={`\n    lustre-text\n    ${!disabled ? \"animate-shine\" : \"\"}\n    dark:lustre-dark lustre-light\n    ${className}\n  `}\n      style={!disabled ? animationStyle : undefined}\n    >\n      {text}\n    </span>\n  );\n};\n\nexport default LustreText;\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}"
    }
  ]
}
