{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","popmotion"],"description":"A AppleInvites component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Crown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { wrap } from \"popmotion\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\nexport interface ResponsiveSize {\n  \"2xl\"?: number | string;\n  base?: number | string;\n  lg?: number | string;\n  md?: number | string;\n  sm?: number | string;\n  xl?: number | string;\n}\n\nconst breakpoints = {\n  \"2xl\": 1536,\n  lg: 1024,\n  md: 768,\n  sm: 640,\n  xl: 1280,\n} as const;\n\nconst DEFAULT_CARD_WIDTH = 240;\nconst DEFAULT_ASPECT_RATIO = 1.5625; // 5:8 ratio (500/320)\n\n// Base sizes for responsive scaling (based on DEFAULT_CARD_WIDTH = 240)\nconst BASE_BADGE_FONT_SIZE = 12;\nconst BASE_BADGE_PADDING_X = 12;\nconst BASE_BADGE_PADDING_Y = 3;\nconst BASE_BADGE_ICON_SIZE = 14;\nconst BASE_TITLE_FONT_SIZE = 18;\nconst BASE_SUBTITLE_FONT_SIZE = 12;\nconst BASE_LOCATION_FONT_SIZE = 12;\nconst BASE_AVATAR_SIZE = 24;\nconst BASE_CONTENT_PADDING = 24;\nconst BASE_BADGE_TOP = 16;\nconst BASE_BADGE_LEFT = 16;\nconst BASE_BADGE_GAP = 8;\nconst BASE_AVATAR_GAP = 8;\nconst BASE_AVATAR_MARGIN_BOTTOM = 8;\nconst BASE_TITLE_MARGIN_BOTTOM = 4;\nconst BASE_LINE_HEIGHT = 1.4;\nconst BADGE_PADDING_Y_SCALE_FACTOR = 0.7; // Reduce vertical padding scaling for more compact badges\n\n// Minimum sizes to ensure readability\nconst MIN_BADGE_FONT_SIZE = 10;\nconst MIN_BADGE_PADDING_X = 8;\nconst MIN_BADGE_PADDING_Y = 1;\nconst MIN_BADGE_ICON_SIZE = 12;\nconst MIN_TITLE_FONT_SIZE = 14;\nconst MIN_SUBTITLE_FONT_SIZE = 10;\nconst MIN_LOCATION_FONT_SIZE = 10;\nconst MIN_AVATAR_SIZE = 20;\nconst MIN_CONTENT_PADDING = 12;\nconst MIN_BADGE_TOP = 8;\nconst MIN_BADGE_LEFT = 8;\nconst MIN_BADGE_GAP = 4;\nconst MIN_AVATAR_GAP = 4;\nconst MIN_AVATAR_MARGIN_BOTTOM = 4;\nconst MIN_TITLE_MARGIN_BOTTOM = 2;\n\nfunction formatSize(size: number | string): string {\n  return typeof size === \"number\" ? `${size}px` : size;\n}\n\nfunction getInitialSize(\n  size: number | string | ResponsiveSize | undefined,\n  defaultValue: number | string\n): string {\n  if (!size) {\n    return formatSize(defaultValue);\n  }\n  if (typeof size === \"number\" || typeof size === \"string\") {\n    return formatSize(size);\n  }\n  // Responsive object - start with base or first available value\n  if (size.base !== undefined) {\n    return formatSize(size.base);\n  }\n  return formatSize(defaultValue);\n}\n\nfunction getSizeForBreakpoint(\n  size: ResponsiveSize,\n  width: number\n): number | string | undefined {\n  if (width >= breakpoints[\"2xl\"]) {\n    return size[\"2xl\"] ?? size.xl ?? size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.xl) {\n    return size.xl ?? size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.lg) {\n    return size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.md) {\n    return size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.sm) {\n    return size.sm ?? size.base;\n  }\n  return size.base;\n}\n\nfunction useResponsiveSize(\n  size: number | string | ResponsiveSize | undefined,\n  defaultValue: number | string\n): string {\n  const [currentSize, setCurrentSize] = useState<string>(() =>\n    getInitialSize(size, defaultValue)\n  );\n\n  useEffect(() => {\n    if (!size || typeof size === \"number\" || typeof size === \"string\") {\n      return;\n    }\n\n    const updateSize = () => {\n      const width = window.innerWidth;\n      const selectedSize = getSizeForBreakpoint(size, width);\n\n      if (selectedSize !== undefined) {\n        const newSize = formatSize(selectedSize);\n        setCurrentSize(newSize);\n      }\n    };\n\n    updateSize();\n    window.addEventListener(\"resize\", updateSize);\n    return () => window.removeEventListener(\"resize\", updateSize);\n  }, [size]);\n\n  return currentSize;\n}\n\nfunction parseSize(size: string): number {\n  const num = Number.parseFloat(size);\n  return Number.isNaN(num) ? 0 : num;\n}\n\nfunction calculateHeightFromWidth(width: string, aspectRatio: number): string {\n  const widthNum = parseSize(width);\n  if (widthNum === 0) {\n    return width;\n  }\n  const heightNum = widthNum * aspectRatio;\n  return `${heightNum}px`;\n}\n\nexport interface Participant {\n  avatar: string;\n}\n\nexport interface Event {\n  backgroundClassName?: string;\n  badge?: string;\n  id: number;\n  image?: string;\n  location: string;\n  participants?: Participant[];\n  subtitle?: string;\n  title?: string;\n}\n\nexport interface AppleInvitesProps {\n  activeIndex?: number;\n  aspectRatio?: number;\n  cardClassName?: string;\n  cardHeight?: number | string | ResponsiveSize;\n  cardWidth?: number | string | ResponsiveSize;\n  className?: string;\n  events: Event[];\n  interval?: number;\n  onChange?: (index: number) => void;\n}\n\nexport default function AppleInvites({\n  events,\n  interval = 3000,\n  className = \"\",\n  cardClassName = \"\",\n  activeIndex: controlledIndex,\n  onChange,\n  cardWidth = DEFAULT_CARD_WIDTH,\n  cardHeight,\n  aspectRatio = DEFAULT_ASPECT_RATIO,\n}: AppleInvitesProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalPage, setInternalPage] = useState(0);\n  const [direction, setDirection] = useState(0);\n  const responsiveWidth = useResponsiveSize(cardWidth, DEFAULT_CARD_WIDTH);\n\n  const variants = useMemo(\n    () => ({\n      center: {\n        opacity: 1,\n        rotate: 0,\n        scale: 1,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"-50%\",\n        zIndex: 3,\n      },\n      hidden: {\n        opacity: 0,\n        transition: shouldReduceMotion ? { duration: 0 } : { duration: 0.3 },\n        zIndex: 1,\n      },\n      left: {\n        opacity: 0.8,\n        rotate: -12,\n        scale: 0.9,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"-130%\",\n        zIndex: 2,\n      },\n      right: {\n        opacity: 0.8,\n        rotate: 12,\n        scale: 0.9,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"30%\",\n        zIndex: 2,\n      },\n    }),\n    [shouldReduceMotion]\n  );\n  const explicitHeight = useResponsiveSize(\n    cardHeight,\n    calculateHeightFromWidth(responsiveWidth, aspectRatio)\n  );\n  const [calculatedHeight, setCalculatedHeight] = useState<string>(() =>\n    calculateHeightFromWidth(responsiveWidth, aspectRatio)\n  );\n\n  // Update calculated height when width changes (if using aspect ratio)\n  useEffect(() => {\n    if (cardHeight === undefined) {\n      setCalculatedHeight(\n        calculateHeightFromWidth(responsiveWidth, aspectRatio)\n      );\n    }\n  }, [responsiveWidth, aspectRatio, cardHeight]);\n\n  const responsiveHeight =\n    cardHeight === undefined ? calculatedHeight : explicitHeight;\n\n  // Calculate responsive sizes based on card width\n  const cardWidthNum = parseSize(responsiveWidth);\n  const scaleFactor = cardWidthNum / DEFAULT_CARD_WIDTH;\n\n  // Responsive sizes for internal content\n  const badgeFontSize = Math.max(\n    MIN_BADGE_FONT_SIZE,\n    Math.round(BASE_BADGE_FONT_SIZE * scaleFactor)\n  );\n  const badgePaddingX = Math.max(\n    MIN_BADGE_PADDING_X,\n    Math.round(BASE_BADGE_PADDING_X * scaleFactor)\n  );\n  // Use a more aggressive scaling for vertical padding to keep it compact\n  // Scale padding Y less aggressively to keep badges more compact\n  const badgePaddingY = Math.max(\n    MIN_BADGE_PADDING_Y,\n    Math.round(\n      BASE_BADGE_PADDING_Y * scaleFactor * BADGE_PADDING_Y_SCALE_FACTOR\n    )\n  );\n  const badgeIconSize = Math.max(\n    MIN_BADGE_ICON_SIZE,\n    Math.round(BASE_BADGE_ICON_SIZE * scaleFactor)\n  );\n  const titleFontSize = Math.max(\n    MIN_TITLE_FONT_SIZE,\n    Math.round(BASE_TITLE_FONT_SIZE * scaleFactor)\n  );\n  const subtitleFontSize = Math.max(\n    MIN_SUBTITLE_FONT_SIZE,\n    Math.round(BASE_SUBTITLE_FONT_SIZE * scaleFactor)\n  );\n  const locationFontSize = Math.max(\n    MIN_LOCATION_FONT_SIZE,\n    Math.round(BASE_LOCATION_FONT_SIZE * scaleFactor)\n  );\n  const avatarSize = Math.max(\n    MIN_AVATAR_SIZE,\n    Math.round(BASE_AVATAR_SIZE * scaleFactor)\n  );\n  const contentPadding = Math.max(\n    MIN_CONTENT_PADDING,\n    Math.round(BASE_CONTENT_PADDING * scaleFactor)\n  );\n  const badgeTop = Math.max(\n    MIN_BADGE_TOP,\n    Math.round(BASE_BADGE_TOP * scaleFactor)\n  );\n  const badgeLeft = Math.max(\n    MIN_BADGE_LEFT,\n    Math.round(BASE_BADGE_LEFT * scaleFactor)\n  );\n  const badgeGap = Math.max(\n    MIN_BADGE_GAP,\n    Math.round(BASE_BADGE_GAP * scaleFactor)\n  );\n  const avatarGap = Math.max(\n    MIN_AVATAR_GAP,\n    Math.round(BASE_AVATAR_GAP * scaleFactor)\n  );\n  const avatarMarginBottom = Math.max(\n    MIN_AVATAR_MARGIN_BOTTOM,\n    Math.round(BASE_AVATAR_MARGIN_BOTTOM * scaleFactor)\n  );\n  const titleMarginBottom = Math.max(\n    MIN_TITLE_MARGIN_BOTTOM,\n    Math.round(BASE_TITLE_MARGIN_BOTTOM * scaleFactor)\n  );\n\n  const page = controlledIndex === undefined ? internalPage : controlledIndex;\n  const setPage = (val: number, dir: number) => {\n    if (onChange) {\n      onChange(val);\n    } else {\n      setInternalPage(val);\n      setDirection(dir);\n    }\n  };\n\n  const activeIndex = wrap(0, events.length, page);\n  const setPageRef = useRef(setPage);\n\n  useEffect(() => {\n    setPageRef.current = setPage;\n  });\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      setPageRef.current(page + 1, 1);\n    }, interval);\n    return () => clearInterval(timer);\n  }, [page, interval]);\n\n  const visibleEvents = [-1, 0, 1].map(\n    (offset) => events[wrap(0, events.length, activeIndex + offset)]\n  );\n\n  const getVariant = (index: number) => {\n    if (index === 1) {\n      return \"center\";\n    }\n    if (index === 0) {\n      return \"left\";\n    }\n    return \"right\";\n  };\n\n  const renderBackground = (event: Event) => {\n    if (event.backgroundClassName) {\n      return <div className={`h-full w-full ${event.backgroundClassName}`} />;\n    }\n    if (event.image) {\n      return (\n        <img\n          alt={event.title || \"\"}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n          height={400}\n          src={event.image}\n          width={400}\n        />\n      );\n    }\n    return null;\n  };\n\n  return (\n    <div\n      className={`relative flex h-full w-full items-center justify-center ${className}`}\n    >\n      <AnimatePresence custom={direction} initial={false}>\n        {visibleEvents.map((event, index) => (\n          <motion.div\n            animate={getVariant(index)}\n            className={`absolute top-1/2 left-1/2 origin-center -translate-y-1/2 ${cardClassName}`}\n            custom={direction}\n            exit=\"hidden\"\n            initial=\"hidden\"\n            key={event.id}\n            style={{\n              height: responsiveHeight,\n              width: responsiveWidth,\n            }}\n            variants={variants}\n          >\n            <div className=\"relative h-full w-full overflow-hidden rounded-3xl bg-primary\">\n              {renderBackground(event)}\n              {/* Badge */}\n              <div\n                className=\"absolute z-3\"\n                style={{\n                  left: `${badgeLeft}px`,\n                  top: `${badgeTop}px`,\n                }}\n              >\n                <span\n                  className=\"flex flex-row items-center rounded-full bg-black/30 font-medium text-white backdrop-blur-xl\"\n                  style={{\n                    fontSize: `${badgeFontSize}px`,\n                    gap: `${badgeGap}px`,\n                    paddingBottom: `${badgePaddingY}px`,\n                    paddingLeft: `${badgePaddingX}px`,\n                    paddingRight: `${badgePaddingX}px`,\n                    paddingTop: `${badgePaddingY}px`,\n                  }}\n                >\n                  <Crown size={badgeIconSize} />\n                  {event.badge}\n                </span>\n              </div>\n              {/* Content */}\n              <div\n                className=\"absolute bottom-0 z-3 w-full rounded-b-3xl text-white\"\n                style={{ padding: `${contentPadding}px` }}\n              >\n                {/* Participant Avatars */}\n                <div\n                  className=\"mx-auto flex items-center justify-center\"\n                  style={{\n                    gap: `${avatarGap}px`,\n                    marginBottom: `${avatarMarginBottom}px`,\n                  }}\n                >\n                  {event.participants?.map((participant, idx) => (\n                    <img\n                      alt={`Participant ${idx + 1}`}\n                      className=\"rounded-full\"\n                      draggable={false}\n                      height={avatarSize}\n                      key={`participant-${participant.avatar}-${idx}`}\n                      src={participant.avatar}\n                      style={{\n                        height: `${avatarSize}px`,\n                        width: `${avatarSize}px`,\n                      }}\n                      width={avatarSize}\n                    />\n                  ))}\n                </div>\n                {event.title ? (\n                  <p\n                    className=\"wrap-break-word text-center font-bold\"\n                    style={{\n                      fontSize: `${titleFontSize}px`,\n                      lineHeight: BASE_LINE_HEIGHT,\n                      marginBottom: `${titleMarginBottom}px`,\n                    }}\n                  >\n                    {event.title}\n                  </p>\n                ) : null}\n                {event.subtitle ? (\n                  <p\n                    className=\"wrap-break-word text-center opacity-90\"\n                    style={{\n                      fontSize: `${subtitleFontSize}px`,\n                      lineHeight: BASE_LINE_HEIGHT,\n                    }}\n                  >\n                    {event.subtitle}\n                  </p>\n                ) : null}\n                <p\n                  className=\"wrap-break-word text-center opacity-90\"\n                  style={{\n                    fontSize: `${locationFontSize}px`,\n                    lineHeight: BASE_LINE_HEIGHT,\n                  }}\n                >\n                  {event.location}\n                </p>\n              </div>\n              <div className=\"fixed inset-x-0 bottom-0 isolate z-2 h-1/2\">\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[1px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[2px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[3px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[6px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[12px]\" />\n              </div>\n            </div>\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/apple-invites/index.tsx","type":"registry:ui"}],"name":"apple-invites","registryDependencies":[],"title":"Apple Invites","type":"registry:ui"}