{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-icons",
  "type": "registry:component",
  "title": "Morph Icons",
  "description": "Menu to close, play to pause, plus to check, sun to moon: each pair is one set of paths in two poses, so lines rotate and stretch into place on a spring and reverse cleanly mid-way.",
  "categories": [
    "motion",
    "Surfaces"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/morph-icons.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { type MotionValue, motion, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { type ComponentProps, useEffect, useId } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MorphIconName = \"menu\" | \"play\" | \"plus\" | \"theme\";\n\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\nconst clamp01 = (t: number) => Math.min(Math.max(t, 0), 1);\n\ntype Seg = { x: number; y: number; a: number; l: number };\n\nfunction segPath(from: Seg, to: Seg, t: number) {\n  const x = lerp(from.x, to.x, t);\n  const y = lerp(from.y, to.y, t);\n  const a = (lerp(from.a, to.a, t) * Math.PI) / 180;\n  const l = Math.max(lerp(from.l, to.l, t), 0) / 2;\n  const dx = Math.cos(a) * l;\n  const dy = Math.sin(a) * l;\n  return `M${(x - dx).toFixed(2)} ${(y - dy).toFixed(2)}L${(x + dx).toFixed(2)} ${(y + dy).toFixed(2)}`;\n}\n\ntype Pt = [number, number];\nfunction polyPath(from: Pt[], to: Pt[], t: number) {\n  return `${from.map((p, i) => `${i ? \"L\" : \"M\"}${lerp(p[0], to[i][0], t).toFixed(2)} ${lerp(p[1], to[i][1], t).toFixed(2)}`).join(\"\")}Z`;\n}\n\nfunction SegPath({ p, start, end, opacity }: { p: MotionValue<number>; start: Seg; end: Seg; opacity?: MotionValue<number> }) {\n  const d = useTransform(p, (t) => segPath(start, end, t));\n  return <motion.path d={d} opacity={opacity} />;\n}\n\nfunction PolyPath({ p, from, to }: { p: MotionValue<number>; from: Pt[]; to: Pt[] }) {\n  const d = useTransform(p, (t) => polyPath(from, to, t));\n  return <motion.path d={d} />;\n}\n\nfunction MenuClose({ p }: { p: MotionValue<number> }) {\n  const mid = useTransform(p, [0, 0.45], [1, 0]);\n  return (\n    <>\n      <SegPath start={{ x: 12, y: 6, a: 0, l: 16 }} p={p} end={{ x: 12, y: 12, a: 45, l: 17 }} />\n      <SegPath start={{ x: 12, y: 12, a: 0, l: 16 }} opacity={mid} p={p} end={{ x: 12, y: 12, a: 0, l: 4 }} />\n      <SegPath start={{ x: 12, y: 18, a: 0, l: 16 }} p={p} end={{ x: 12, y: 12, a: -45, l: 17 }} />\n    </>\n  );\n}\n\nconst PLAY: [Pt[], Pt[]] = [\n  [\n    [8.5, 5],\n    [14, 8.5],\n    [14, 15.5],\n    [8.5, 19],\n  ],\n  [\n    [14, 8.5],\n    [19.5, 12],\n    [19.5, 12],\n    [14, 15.5],\n  ],\n];\nconst PAUSE: [Pt[], Pt[]] = [\n  [\n    [6.5, 5],\n    [10, 5],\n    [10, 19],\n    [6.5, 19],\n  ],\n  [\n    [14, 5],\n    [17.5, 5],\n    [17.5, 19],\n    [14, 19],\n  ],\n];\n\nfunction PlayPause({ p }: { p: MotionValue<number> }) {\n  return (\n    <g fill=\"currentColor\" strokeWidth={1.5}>\n      <PolyPath from={PLAY[0]} p={p} to={PAUSE[0]} />\n      <PolyPath from={PLAY[1]} p={p} to={PAUSE[1]} />\n    </g>\n  );\n}\n\nfunction PlusCheck({ p }: { p: MotionValue<number> }) {\n  return (\n    <>\n      <SegPath start={{ x: 12, y: 12, a: 0, l: 14 }} p={p} end={{ x: 7.25, y: 14.75, a: 45, l: 6.4 }} />\n      <SegPath start={{ x: 12, y: 12, a: 90, l: 14 }} p={p} end={{ x: 14.25, y: 12, a: 133.5, l: 13.8 }} />\n    </>\n  );\n}\n\nconst RAYS = Array.from({ length: 8 }, (_, i) => i * 45);\n\nfunction SunMoon({ p }: { p: MotionValue<number> }) {\n  const maskId = `morph-moon-${useId().replace(/:/g, \"\")}`;\n  const t = useTransform(p, clamp01);\n  const r = useTransform(p, (v) => lerp(4.5, 8, v));\n  const mx = useTransform(t, (v) => lerp(26, 17, v));\n  const my = useTransform(t, (v) => lerp(-2, 7, v));\n  const spin = useTransform(p, (v) => v * 90);\n  const rayOpacity = useTransform(t, [0, 0.6], [1, 0]);\n  return (\n    <>\n      <mask id={maskId}>\n        <rect fill=\"white\" height=\"24\" width=\"24\" />\n        <motion.circle cx={mx} cy={my} fill=\"black\" r={7} />\n      </mask>\n      <motion.circle cx={12} cy={12} fill=\"currentColor\" mask={`url(#${maskId})`} r={r} stroke=\"none\" />\n      <motion.g opacity={rayOpacity} style={{ rotate: spin }}>\n        {RAYS.map((a) => (\n          <SegPath\n            start={{ x: 12 + Math.cos((a * Math.PI) / 180) * 8.75, y: 12 + Math.sin((a * Math.PI) / 180) * 8.75, a, l: 2.5 }}\n            key={a}\n            p={t}\n            end={{ x: 12 + Math.cos((a * Math.PI) / 180) * 8, y: 12 + Math.sin((a * Math.PI) / 180) * 8, a, l: 0 }}\n          />\n        ))}\n      </motion.g>\n    </>\n  );\n}\n\nconst ICONS = { menu: MenuClose, play: PlayPause, plus: PlusCheck, theme: SunMoon };\n\nexport interface MorphIconProps extends Omit<ComponentProps<\"svg\">, \"children\"> {\n  name: MorphIconName;\n  morphed: boolean;\n}\n\nexport function MorphIcon({ name, morphed, className, ...props }: MorphIconProps) {\n  const reduce = useReducedMotion();\n  const p = useSpring(morphed ? 1 : 0, { stiffness: 420, damping: 28, mass: 0.8 });\n\n  useEffect(() => {\n    if (reduce) p.jump(morphed ? 1 : 0);\n    else p.set(morphed ? 1 : 0);\n  }, [morphed, reduce, p]);\n\n  const Icon = ICONS[name];\n  return (\n    <svg\n      aria-hidden\n      className={cn(\"size-6 shrink-0\", className)}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth={2}\n      viewBox=\"0 0 24 24\"\n      {...props}\n    >\n      <Icon p={p} />\n    </svg>\n  );\n}\n"
    }
  ]
}