{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"icon","type":"registry:ui","title":"Icon","description":"Icon from react-fancy","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":[],"files":[{"path":"components\/fancy\/icon\/Icon.tsx","content":"import { forwardRef, useSyncExternalStore } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { resolveIcon, subscribeIconResolution, getIconResolutionVersion } from \".\/icon-config\";\r\nimport type { IconProps } from \".\/Icon.types\";\r\n\r\nconst sizeClasses: Record<NonNullable<IconProps[\"size\"]>, string> = {\r\n  xs: \"h-3 w-3\",\r\n  sm: \"h-4 w-4\",\r\n  md: \"h-5 w-5\",\r\n  lg: \"h-6 w-6\",\r\n  xl: \"h-8 w-8\",\r\n};\r\n\r\nconst sizePixels: Record<NonNullable<IconProps[\"size\"]>, number> = {\r\n  xs: 12,\r\n  sm: 16,\r\n  md: 20,\r\n  lg: 24,\r\n  xl: 32,\r\n};\r\n\r\nexport const Icon = forwardRef<HTMLSpanElement, IconProps>(\r\n  ({ size = \"md\", className, children, name, iconSet, ...props }, ref) => {\r\n    \/\/ Re-render when the lazy lucide barrel lands, so `<Icon name>` auto-resolved\r\n    \/\/ from lucide appears once loaded. Registered icons resolve synchronously and\r\n    \/\/ never trigger the lazy load. SSR snapshot is the same getter (version 0).\r\n    useSyncExternalStore(\r\n      subscribeIconResolution,\r\n      getIconResolutionVersion,\r\n      getIconResolutionVersion,\r\n    );\r\n\r\n    let content = children;\r\n\r\n    if (name && !children) {\r\n      const ResolvedIcon = resolveIcon(name, iconSet);\r\n      if (ResolvedIcon) {\r\n        content = <ResolvedIcon size={sizePixels[size]} \/>;\r\n      }\r\n    }\r\n\r\n    return (\r\n      <span\r\n        ref={ref}\r\n        aria-hidden=\"true\"\r\n        data-react-fancy-icon=\"\"\r\n        className={cn(\r\n          \"inline-flex items-center justify-center flex-shrink-0\",\r\n          sizeClasses[size],\r\n          className,\r\n        )}\r\n        {...props}\r\n      >\r\n        {content}\r\n      <\/span>\r\n    );\r\n  },\r\n);\r\n\r\nIcon.displayName = \"Icon\";\r\n","type":"registry:ui","target":"components\/fancy\/icon\/Icon.tsx"},{"path":"components\/fancy\/icon\/Icon.types.ts","content":"import type { HTMLAttributes } from \"react\";\r\n\r\nexport interface IconProps extends HTMLAttributes<HTMLSpanElement> {\r\n  \/** Icon container size *\/\r\n  size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\r\n  \/** Icon name to resolve from the registered icon set (e.g., \"rocket\", \"arrow-right\") *\/\r\n  name?: string;\r\n  \/** Which registered icon set to use (defaults to the configured default) *\/\r\n  iconSet?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/icon\/Icon.types.ts"},{"path":"components\/fancy\/icon\/icon-config.ts","content":"import type { ComponentType } from \"react\";\r\nimport type { IconSet } from \".\/icon-config.types\";\r\n\r\ntype IconComponent = ComponentType<{ className?: string; size?: number }>;\r\n\r\nconst registry = new Map<string, IconSet>();\r\nlet defaultSetName = \"lucide\";\r\n\r\n\/**\r\n * Addendum layer \u2014 icon sets consulted across EVERY lookup, after the active\r\n * base set but before the lucide auto-fallback, regardless of `defaultSet`.\r\n * This is how a brand-icon pack (e.g. `@particle-academy\/fancy-brand-icons`)\r\n * layers in: `<Icon name=\"github\" \/>` resolves the brand mark on top of Lucide\r\n * (or any swapped base) without a `set=` query. Base set still wins; addenda\r\n * supplement. Empty by default \u21d2 behavior unchanged.\r\n *\/\r\nconst addenda: IconSet[] = [];\r\n\r\n\/** Register an icon set as an addendum (supplements every base set). *\/\r\nexport function registerIconAddendum(set: IconSet): void {\r\n  if (!addenda.includes(set)) addenda.push(set);\r\n}\r\n\r\nfunction resolveFromAddenda(name: string): IconComponent | null {\r\n  for (const set of addenda) {\r\n    const hit = set.resolve(name);\r\n    if (hit) return hit as IconComponent;\r\n  }\r\n  return null;\r\n}\r\n\r\nfunction kebabToPascal(str: string): string {\r\n  return str\r\n    .split(\"-\")\r\n    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\r\n    .join(\"\");\r\n}\r\n\r\n\/**\r\n * Lazy lucide barrel. The auto-fallback (`<Icon name=\"\u2026\" \/>` resolving ANY\r\n * lucide icon by kebab name) needs the whole namespace because names are\r\n * dynamic \u2014 un-tree-shakeable. Importing it eagerly forced all ~5400 icons\r\n * into react-fancy's main bundle for every consumer (~548KB). Instead we\r\n * dynamic-import it on first auto-resolve, so the barrel becomes a lazy chunk\r\n * loaded ONLY by apps that actually use `<Icon name>` with an unregistered\r\n * icon \u2014 never on the critical path. Consumers wanting sync resolution +\r\n * tree-shaking should `registerIcons({ ...named imports })` (those win and\r\n * never trigger the lazy load).\r\n *\/\r\nlet lucideModule: Record<string, IconComponent | undefined> | null = null;\r\nlet lucidePromise: Promise<unknown> | null = null;\r\nlet lucideVersion = 0;\r\nconst lucideListeners = new Set<() => void>();\r\n\r\nfunction ensureLucideLoaded(): void {\r\n  if (lucideModule || lucidePromise) return;\r\n  lucidePromise = import(\"lucide-react\")\r\n    .then((mod) => {\r\n      lucideModule = mod as unknown as Record<string, IconComponent | undefined>;\r\n      lucideVersion++;\r\n      for (const listener of lucideListeners) listener();\r\n    })\r\n    .catch(() => {\r\n      \/\/ Leave lucidePromise set so we don't hammer a failed import.\r\n    });\r\n}\r\n\r\n\/** Subscribe to the lazy lucide load so `<Icon>` can re-render once it lands. *\/\r\nexport function subscribeIconResolution(listener: () => void): () => void {\r\n  lucideListeners.add(listener);\r\n  return () => lucideListeners.delete(listener);\r\n}\r\n\r\n\/** Snapshot for useSyncExternalStore \u2014 bumps when the lucide barrel loads. *\/\r\nexport function getIconResolutionVersion(): number {\r\n  return lucideVersion;\r\n}\r\n\r\n\/**\r\n * Fallback resolver: look up any lucide-react icon by its kebab-case name\r\n * without requiring the consumer to call `registerIcons()` first. Returns null\r\n * until the lazy barrel has loaded (the first call kicks off the import; `<Icon>`\r\n * re-renders via `subscribeIconResolution` when it's ready). Overrides\r\n * registered via `registerIcons()` still win and resolve synchronously.\r\n *\/\r\nfunction resolveFromLucide(name: string): IconComponent | null {\r\n  \/\/ SSR-deterministic: never resolve the lazily-imported lucide barrel on the\r\n  \/\/ server. A long-running SSR process (e.g. Inertia's node SSR daemon) loads\r\n  \/\/ the barrel after its first request, then renders auto-fallback lucide icons\r\n  \/\/ that a fresh client (barrel not yet loaded) does NOT have at hydration \u2192\r\n  \/\/ React #418 (\"server HTML didn't match\"). Returning null on the server makes\r\n  \/\/ the server render match the client's FIRST render; the icon then appears\r\n  \/\/ once the client loads the barrel (via subscribeIconResolution). Registered\r\n  \/\/ icons + addenda (brand packs) still resolve synchronously on both sides.\r\n  if (typeof window === \"undefined\") return null;\r\n  if (!lucideModule) {\r\n    ensureLucideLoaded();\r\n    return null;\r\n  }\r\n  const pascal = kebabToPascal(name);\r\n  const icon = lucideModule[pascal];\r\n  return typeof icon === \"function\" || (icon && typeof icon === \"object\") ? (icon as IconComponent) : null;\r\n}\r\n\r\n\/**\r\n * Register individual icon components by their kebab-case name.\r\n *\r\n * ```tsx\r\n * import { registerIcons } from \"@particle-academy\/react-fancy\";\r\n * import { Home, Settings, Mail } from \"lucide-react\";\r\n *\r\n * registerIcons({ Home, Settings, Mail });\r\n * ```\r\n *\r\n * Icons are registered into the default \"lucide\" icon set and resolved\r\n * by the `<Icon name=\"home\" \/>` component using kebab-case names.\r\n *\/\r\nexport function registerIcons(\r\n  icons: Record<string, ComponentType<{ className?: string; size?: number }>>,\r\n): void {\r\n  let set = registry.get(\"lucide\");\r\n  if (!set) {\r\n    const map = new Map<string, ComponentType<{ className?: string; size?: number }>>();\r\n    set = {\r\n      resolve: (name: string) => {\r\n        const pascal = kebabToPascal(name);\r\n        return map.get(pascal) ?? null;\r\n      },\r\n      _map: map,\r\n    } as IconSet & { _map: Map<string, ComponentType<{ className?: string; size?: number }>> };\r\n    registry.set(\"lucide\", set);\r\n  }\r\n  const map = (set as IconSet & { _map: Map<string, ComponentType<{ className?: string; size?: number }>> })._map;\r\n  for (const [key, component] of Object.entries(icons)) {\r\n    map.set(key, component);\r\n  }\r\n}\r\n\r\nexport function registerIconSet(name: string, set: IconSet): void {\r\n  registry.set(name, set);\r\n}\r\n\r\nexport function configureIcons(options: { defaultSet?: string }): void {\r\n  if (options.defaultSet) {\r\n    defaultSetName = options.defaultSet;\r\n  }\r\n}\r\n\r\nexport function resolveIcon(\r\n  name: string,\r\n  setName?: string,\r\n): IconComponent | null {\r\n  const resolvedSet = setName ?? defaultSetName;\r\n  const set = registry.get(resolvedSet);\r\n  const registered = set ? set.resolve(name) : null;\r\n  if (registered) return registered;\r\n  \/\/ Addenda fill gaps after the base set, before the lucide auto-fallback \u2014 so a\r\n  \/\/ brand pack's `github` wins over lucide's auto-resolved one.\r\n  const fromAddenda = resolveFromAddenda(name);\r\n  if (fromAddenda) return fromAddenda;\r\n  if (resolvedSet === \"lucide\") return resolveFromLucide(name);\r\n  return null;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/icon\/icon-config.ts"},{"path":"components\/fancy\/icon\/icon-config.types.ts","content":"import type { ComponentType } from \"react\";\r\n\r\nexport interface IconSet {\r\n  resolve: (name: string) => ComponentType<{ className?: string; size?: number }> | null;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/icon\/icon-config.types.ts"},{"path":"components\/fancy\/icon\/index.ts","content":"export { Icon } from \".\/Icon\";\r\nexport type { IconProps } from \".\/Icon.types\";\r\nexport { registerIcons, registerIconSet, registerIconAddendum, configureIcons, resolveIcon } from \".\/icon-config\";\r\nexport type { IconSet } from \".\/icon-config.types\";\r\n","type":"registry:ui","target":"components\/fancy\/icon\/index.ts"}]}