{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"swap","type":"registry:ui","title":"Swap","description":"Content that changes in place: the old one fades out as the new one arrives, as a fade, a rise or a pop.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-motion.json"],"files":[{"path":"registry/dashral/swap.tsx","type":"registry:file","target":"components/dashral/swap.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { reducedMotion } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\n\ntype SwapEffect = \"fade\" | \"rise\" | \"pop\"\n\ntype SwapProps = {\n  /** What it shows now, as a key: a change swaps the old content out and the new one in. */\n  value: string | number | boolean\n  /** The content for this value. */\n  children?: React.ReactNode\n  /**\n   * How the new content arrives: fade, rise (up a few pixels as it fades in, for text and forms) or pop (from small,\n   * for an icon such as a check).\n   */\n  effect?: SwapEffect\n  /** The old content leaves before the new one comes, rather than both at once. */\n  wait?: boolean\n  /** The first content arrives with the effect too, as the page loads or as it's added. */\n  appear?: boolean\n  /** The element: \"span\" inside a line of text or a button, \"div\" for blocks. */\n  as?: \"span\" | \"div\"\n  /** Classes of each content's own element, e.g. its layout (\"flex items-center gap-2\"). */\n  className?: string\n}\n\nconst enter: Record<SwapEffect, string> = {\n  fade: \"animate-in fade-in-0\",\n  rise: \"animate-in fade-in-0 slide-in-from-bottom-[6px]\",\n  pop: \"animate-in fade-in-0 zoom-in-50\",\n}\nconst leave = \"animate-out fade-out-0 fill-mode-forwards\"\n\n/**\n * Content that changes in place, in one tag: `<Swap value={copied}>{copied ? <Check /> : <Copy />}</Swap>`. When value\n * changes, the old content fades out while the new one arrives with its effect, in the same place: an icon turning into\n * a check, a label after an action, a form giving way to a thank-you.\n */\nfunction Swap({ value, children, effect = \"fade\", wait = false, appear = false, as: Tag = \"span\", className }: SwapProps) {\n  // The value shown, and the content on its way out (kept until its animation ends). Swapped during render, from the\n  // value and the content last on screen, so the old one never blinks away first.\n  const [current, setCurrent] = React.useState({ value, entered: appear })\n  const [leaving, setLeaving] = React.useState<{ value: SwapProps[\"value\"]; children: React.ReactNode } | null>(null)\n  const onScreen = React.useRef(children)\n  if (current.value !== value) {\n    // With reduced motion nothing animates, so nothing would end the old content's leaving: it simply goes.\n    setLeaving(reducedMotion() ? null : { value: current.value, children: onScreen.current })\n    setCurrent({ value, entered: true })\n  }\n  React.useLayoutEffect(() => {\n    onScreen.current = children\n  })\n  const animate = current.entered\n\n  const item = cn(Tag === \"span\" ? \"inline-flex\" : \"block\", \"[grid-area:1/1] motion-reduce:animate-none\", className)\n  return (\n    <Tag data-slot=\"swap\" className={cn(Tag === \"span\" ? \"inline-grid\" : \"grid\", \"items-center\")}>\n      {leaving && (\n        <Tag\n          key={`leaving-${String(leaving.value)}`}\n          aria-hidden\n          className={cn(item, leave, \"pointer-events-none duration-(--duration-fast) ease-exit\")}\n          // Its own fade only: an animation inside it (an icon) bubbles up here too.\n          onAnimationEnd={(event) => event.target === event.currentTarget && setLeaving(null)}\n        >\n          {leaving.children}\n        </Tag>\n      )}\n      {!(wait && leaving) && (\n        <Tag key={String(value)} className={cn(item, animate && enter[effect], animate && \"duration-(--duration-standard) ease-enter\")}>\n          {children}\n        </Tag>\n      )}\n    </Tag>\n  )\n}\n\nexport { Swap, type SwapProps, type SwapEffect }\n"}],"cssVars":{"theme":{"ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{}}}