{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-transition",
  "title": "Icon Transition",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/icon-transition/icon-transition.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\n\n// Self-contained icon-swap control. Click runs an optional async `action`,\n// then flips from `defaultIcon` to `activeIcon` with a spring pop. By default\n// it auto-reverts after 1500ms; pass `resetAfter={0}` to keep the active\n// icon, then call `ref.current.reset()` manually.\n//\n// Two operating modes:\n//   1. Self-managed (most common): pass `defaultIcon`, `activeIcon`, and\n//      either `action` or just listen to `onActivate`. The component runs\n//      the action, manages its own `active` state, and auto-resets.\n//   2. Externally controlled: pass `active={boolean}` and the component\n//      will mirror that prop, ignoring its internal flag.\n//\n// Renders as a <button> by default. Pass `as=\"span\"` for a non-interactive\n// icon (e.g. inside a parent button) — in that mode the parent is responsible\n// for triggering `trigger()` via the exposed ref.\n\ntype IconComponent = React.ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>\n\nexport interface IconTransitionProps {\n  defaultIcon: IconComponent\n  activeIcon: IconComponent\n  /** Tailwind size/color applied to the icons. */\n  iconClass?: string\n  /** Async work executed on click before the icon flips. Return `false` to skip the flip. */\n  action?: () => boolean | void | Promise<boolean | void>\n  /** ms before reverting to defaultIcon. Set to `0` (or null) to stay active. */\n  resetAfter?: number | null\n  /** Pop animation duration in ms (also scales the leave fade). */\n  duration?: number\n  /** aria-label shown in default state. */\n  label?: string\n  /** aria-label shown after activation; falls back to `label`. */\n  activeLabel?: string\n  /** Tailwind class applied while active (e.g. \"text-success\"). */\n  activeClass?: string\n  /** Render element. `button` adds click handler + focus styling; `span` is purely visual. */\n  as?: 'button' | 'span'\n  /** External control. When provided, this prop wins over internal state. */\n  active?: boolean\n  onActivate?: () => void\n  onReset?: () => void\n}\n\nexport interface IconTransitionHandle {\n  trigger: () => Promise<void>\n  reset: () => void\n}\n\n// Scoped styles injected once. Mirrors the Vue SFC's scoped <style> block 1:1\n// (pop on enter, fade on leave, reduced-motion guard). Scoped to the\n// `.icon-transition` ancestor so the keyframes don't leak.\nconst STYLE_ID = 'uipkge-icon-transition-styles'\nconst STYLES = `\n.icon-transition .icon-transition-slot {\n  grid-area: 1 / 1;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n.icon-transition .icon-transition-enter-active,\n.icon-transition .icon-transition-leave-active {\n  transform-origin: center;\n}\n.icon-transition .icon-transition-enter-active {\n  animation: icon-transition-pop var(--it-duration, 240ms) cubic-bezier(0.34, 1.56, 0.64, 1) both;\n}\n.icon-transition .icon-transition-leave-active {\n  animation: icon-transition-fade calc(var(--it-duration, 240ms) * 0.66) ease-in both;\n}\n@keyframes icon-transition-pop {\n  0% {\n    opacity: 0;\n    transform: scale(0.5) rotate(-12deg);\n  }\n  60% {\n    opacity: 1;\n    transform: scale(1.18) rotate(2deg);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1) rotate(0deg);\n  }\n}\n@keyframes icon-transition-fade {\n  to {\n    opacity: 0;\n    transform: scale(0.85);\n  }\n}\n@media (prefers-reduced-motion: reduce) {\n  .icon-transition .icon-transition-enter-active,\n  .icon-transition .icon-transition-leave-active {\n    animation-duration: 0ms !important;\n  }\n}\n`\n\nfunction useInjectedStyles() {\n  React.useEffect(() => {\n    if (typeof document === 'undefined') return\n    if (document.getElementById(STYLE_ID)) return\n    const el = document.createElement('style')\n    el.id = STYLE_ID\n    el.textContent = STYLES\n    document.head.appendChild(el)\n  }, [])\n}\n\nconst IconTransition = React.forwardRef<IconTransitionHandle, IconTransitionProps>(function IconTransition(\n  {\n    defaultIcon: DefaultIcon,\n    activeIcon: ActiveIcon,\n    iconClass = 'size-4',\n    action,\n    resetAfter = 1500,\n    duration = 240,\n    label,\n    activeLabel,\n    activeClass = 'text-success',\n    as = 'button',\n    active,\n    onActivate,\n    onReset,\n  },\n  ref,\n) {\n  useInjectedStyles()\n\n  const [internalActive, setInternalActive] = React.useState(false)\n  const isActive = active !== undefined ? active : internalActive\n\n  // Track the previous active state so we can render an outgoing (leaving)\n  // icon alongside the incoming (entering) one for the cross-fade — Vue's\n  // <Transition> does this for free; React needs us to hold the prior key.\n  const [prevActive, setPrevActive] = React.useState<boolean | null>(null)\n  const prevIsActiveRef = React.useRef(isActive)\n  React.useEffect(() => {\n    if (prevIsActiveRef.current !== isActive) {\n      setPrevActive(prevIsActiveRef.current)\n      prevIsActiveRef.current = isActive\n    }\n  }, [isActive])\n\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const clearTimer = React.useCallback(() => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current)\n      timerRef.current = null\n    }\n  }, [])\n\n  const reset = React.useCallback(() => {\n    setInternalActive(false)\n    onReset?.()\n    clearTimer()\n  }, [onReset, clearTimer])\n\n  const trigger = React.useCallback(async () => {\n    if (action) {\n      const result = await action()\n      if (result === false) return\n    }\n    setInternalActive(true)\n    onActivate?.()\n    clearTimer()\n    if (resetAfter && resetAfter > 0) {\n      timerRef.current = setTimeout(reset, resetAfter)\n    }\n  }, [action, onActivate, clearTimer, resetAfter, reset])\n\n  React.useImperativeHandle(ref, () => ({ trigger, reset }), [trigger, reset])\n\n  React.useEffect(() => clearTimer, [clearTimer])\n\n  const Comp = as\n\n  const renderSlot = (slotActive: boolean, phase: 'enter' | 'leave', key: string) => {\n    const SlotIcon = slotActive ? ActiveIcon : DefaultIcon\n    const phaseClass = phase === 'enter' ? 'icon-transition-enter-active' : 'icon-transition-leave-active'\n    return (\n      <span key={key} className={`icon-transition-slot ${phaseClass}`}>\n        <SlotIcon className={iconClass} aria-hidden=\"true\" />\n      </span>\n    )\n  }\n\n  return (\n    <Comp\n      {...(as === 'button' ? { type: 'button' as const } : {})}\n      aria-label={isActive ? (activeLabel ?? label) : label}\n      aria-live={isActive ? 'polite' : undefined}\n      className={[\n        'icon-transition focus-visible:ring-ring inline-grid place-items-center transition-colors focus-visible:ring-2 focus-visible:outline-none',\n        isActive ? activeClass : '',\n      ].join(' ')}\n      style={{ ['--it-duration' as string]: `${duration}ms` }}\n      onClick={as === 'button' ? () => void trigger() : undefined}\n    >\n      {prevActive !== null && prevActive !== isActive && renderSlot(prevActive, 'leave', `leave-${prevActive}`)}\n      {renderSlot(isActive, 'enter', `enter-${isActive}`)}\n    </Comp>\n  )\n})\n\nexport { IconTransition }\n",
      "type": "registry:ui",
      "target": "~/components/ui/icon-transition/icon-transition.tsx"
    },
    {
      "path": "packages/registry-react/components/icon-transition/index.ts",
      "content": "export { IconTransition, type IconTransitionProps, type IconTransitionHandle } from './icon-transition'\n",
      "type": "registry:ui",
      "target": "~/components/ui/icon-transition/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Animated icon swap for click acknowledgements — copy → check, bookmark → bookmark-filled, follow → follower. Pass two icon components and an async `action`; clicking runs the action then springs the active icon in. Auto-reverts after `resetAfter` ms (default 1500), or pass `0` to keep the active icon and call the exposed `reset()` method to flip it back. Renders as a `<button>` by default; use `as=\"span\"` plus the `active` prop when you want an externally controlled, non-interactive icon swap inside another control. Honors `prefers-reduced-motion`.",
  "categories": [
    "feedback"
  ]
}