{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gooey-morph",
  "title": "Gooey Morph",
  "description": "Status dots that merge into one body and split apart again like a liquid, so the eye tracks one thing changing.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/gooey-morph.tsx",
      "content": "import type * as React from \"react\"\nimport { useId } from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface GooeyItem {\n  id: string\n  label?: string\n}\n\nconst DEFAULT_DOT_PX = 14\nconst DEFAULT_GAP_PX = 10\n/** Blur as a share of the dot, then an alpha ramp steep enough to re-solidify it: touching dots read as one body. */\nconst BLUR_SHARE = 0.3\nconst ALPHA_SLOPE = 18\nconst ALPHA_OFFSET = -7\n/** Room on each side, in dot widths, for the blur and for dots mid-flight. */\nconst BLEED_DOTS = 1\n/** Merged dots keep a third of a dot between centres, so the body reads as \"all of them\", not \"one left\". */\nconst MERGED_OVERLAP_SHARE = 0.35\n\ninterface GooeyMorphProps extends React.ComponentProps<\"div\"> {\n  items: ReadonlyArray<GooeyItem>\n  /** True gathers every dot into one body at the centre; false spreads them `gap` apart. */\n  merged?: boolean\n  /** Dot diameter in px. */\n  size?: number\n  /** Space between dots in px when spread. */\n  gap?: number\n}\n\n/** Horizontal offset of dot `index` from the centre: `gap` apart when spread, overlapping when merged. */\nfunction dotOffset(index: number, count: number, size: number, gap: number, merged: boolean) {\n  const pitch = merged ? size * MERGED_OVERLAP_SHARE : size + gap\n  return (index - (count - 1) / 2) * pitch\n}\n\n/** useId may contain characters that are awkward inside url(#...); keep the id to safe ones. */\nfunction useFilterId() {\n  return `gooey-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`\n}\n\nfunction GooeyMorph({\n  items,\n  merged = false,\n  size = DEFAULT_DOT_PX,\n  gap = DEFAULT_GAP_PX,\n  className,\n  style,\n  ...props\n}: GooeyMorphProps) {\n  const reduceMotion = useReducedMotion()\n  const filterId = useFilterId()\n  const count = items.length\n  const rowWidth = count * size + Math.max(0, count - 1) * gap\n  const bleed = size * BLEED_DOTS\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={items.map((item) => item.label ?? item.id).join(\", \")}\n      data-slot=\"gooey-morph\"\n      data-merged={merged ? \"true\" : \"false\"}\n      data-reduced={reduceMotion ? \"true\" : \"false\"}\n      className={cn(\"relative inline-block\", className)}\n      style={{ ...style, width: rowWidth + bleed * 2, height: size + bleed * 2 }}\n      {...props}\n    >\n      {reduceMotion ? null : <GooeyFilter id={filterId} blur={size * BLUR_SHARE} />}\n      <div\n        data-testid=\"gooey-field\"\n        className=\"absolute inset-0\"\n        style={reduceMotion ? undefined : { filter: `url(#${filterId})` }}\n      >\n        {items.map((item, index) => (\n          <motion.span\n            key={item.id}\n            data-testid=\"gooey-dot\"\n            className=\"absolute top-1/2 left-1/2 rounded-full bg-mise\"\n            style={{ width: size, height: size, marginLeft: -size / 2, marginTop: -size / 2 }}\n            initial={false}\n            animate={{ x: dotOffset(index, count, size, gap, merged) }}\n            transition={reduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n\nfunction GooeyFilter({ id, blur }: { id: string; blur: number }) {\n  return (\n    <svg aria-hidden=\"true\" className=\"absolute size-0\">\n      <defs>\n        <filter id={id} colorInterpolationFilters=\"sRGB\">\n          <feGaussianBlur in=\"SourceGraphic\" stdDeviation={blur} result=\"blur\" />\n          <feColorMatrix\n            in=\"blur\"\n            type=\"matrix\"\n            values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${ALPHA_SLOPE} ${ALPHA_OFFSET}`}\n          />\n        </filter>\n      </defs>\n    </svg>\n  )\n}\n\nexport { GooeyMorph, type GooeyMorphProps, type GooeyItem }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}