{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["controls","colorful"],"dependencies":["@number-flow/react","motion"],"description":"Twitter style like button","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { useState } from \"react\";\nimport NumberFlow from \"@number-flow/react\";\nimport { motion } from \"motion/react\";\n\nconst HEART_PATH =\n  \"m18.199 2.04c-2.606-.284-4.262.961-6.199 3.008-2.045-2.047-3.593-3.292-6.199-3.008-3.544.388-6.321 4.43-5.718 7.96.966 5.659 5.944 9 11.917 12 5.973-3 10.951-6.341 11.917-12 .603-3.53-2.174-7.572-5.718-7.96z\";\n\nconst CIRCLE_RADIUS = 20;\n\nconst CUBIC_OUT = [0.33, 1, 0.68, 1] as const;\nconst QUAD_IN = [0.55, 0.085, 0.68, 0.53] as const;\nconst QUINT_OUT = [0.23, 1, 0.32, 1] as const;\n\nconst CircleAnimation = () => (\n  <svg\n    className=\"pointer-events-none absolute -top-3 -left-3\"\n    style={{\n      height: CIRCLE_RADIUS * 2,\n      width: CIRCLE_RADIUS * 2,\n    }}\n  >\n    <motion.circle\n      cx={CIRCLE_RADIUS}\n      cy={CIRCLE_RADIUS}\n      r={CIRCLE_RADIUS - 2}\n      fill=\"none\"\n      initial={{\n        scale: 0,\n        stroke: \"#E5214A\",\n        strokeWidth: CIRCLE_RADIUS * 2,\n      }}\n      animate={{\n        scale: 1,\n        stroke: \"#CC8EF5\",\n        strokeWidth: 0,\n      }}\n      transition={{\n        duration: 0.4,\n        ease: CUBIC_OUT,\n      }}\n    />\n  </svg>\n);\n\n// One entry per particle; each fades from `from` to `to` over its flight.\nconst PARTICLE_COLOR_PAIRS = [\n  { from: \"#9EC9F5\", to: \"#9ED8C6\" },\n  { from: \"#91D3F7\", to: \"#9AE4CF\" },\n  { from: \"#DC93CF\", to: \"#E3D36B\" },\n  { from: \"#CF8EEF\", to: \"#CBEB98\" },\n  { from: \"#87E9C6\", to: \"#1FCC93\" },\n  { from: \"#A7ECD0\", to: \"#9AE4CF\" },\n  { from: \"#87E9C6\", to: \"#A635D9\" },\n  { from: \"#D58EB3\", to: \"#E0B6F5\" },\n  { from: \"#F48BA2\", to: \"#CF8EEF\" },\n  { from: \"#91D3F7\", to: \"#A635D9\" },\n  { from: \"#CF8EEF\", to: \"#CBEB98\" },\n  { from: \"#87E9C6\", to: \"#A635D9\" },\n  { from: \"#9EC9F5\", to: \"#9ED8C6\" },\n  { from: \"#91D3F7\", to: \"#9AE4CF\" },\n].map((colors, index, pairs) => ({\n  angle: (index / pairs.length) * 360 + 45,\n  from: colors.from,\n  to: colors.to,\n}));\n\nconst BURST_RADIUS = 32;\nconst START_RADIUS = 4;\nconst PATH_SCALE_FACTOR = 0.8;\n\ninterface Flight {\n  angle: number;\n  from: string;\n  to: string;\n  burstDistance: number;\n  duration: number;\n}\n\n// Rolled once per burst, in the click handler rather than in render: a re-render\n// mid-flight would otherwise hand motion new targets and jolt the particles.\nconst rollBurst = (): Flight[] =>\n  PARTICLE_COLOR_PAIRS.map((colors) => ({\n    ...colors,\n    // Add randomness to the burst distance (±15%)\n    burstDistance: BURST_RADIUS * (0.85 + Math.random() * 0.3),\n    // Randomize duration between 500-700ms\n    duration: 500 + Math.random() * 200,\n  }));\n\nconst Particle = ({ from: fromColor, to: toColor, angle, burstDistance, duration }: Flight) => {\n  const radians = (angle * Math.PI) / 180;\n\n  // Calculate the degree shift (13 degrees in radians)\n  const degreeShift = (13 * Math.PI) / 180;\n\n  return (\n    <motion.div\n      className=\"pointer-events-none absolute size-1.5 rounded-full\"\n      style={{ backgroundColor: fromColor, opacity: 0 }}\n      initial={{\n        backgroundColor: fromColor,\n        opacity: 0,\n        scale: 1,\n        x: Math.cos(radians) * START_RADIUS * PATH_SCALE_FACTOR,\n        y: Math.sin(radians) * START_RADIUS * PATH_SCALE_FACTOR,\n      }}\n      animate={{\n        backgroundColor: toColor,\n        opacity: [0, 1, 1, 0],\n        scale: 0,\n        x: Math.cos(radians + degreeShift) * burstDistance * PATH_SCALE_FACTOR,\n        y: Math.sin(radians + degreeShift) * burstDistance * PATH_SCALE_FACTOR,\n      }}\n      transition={{\n        backgroundColor: {\n          delay: 0.3,\n          duration: duration / 1000,\n        },\n        opacity: {\n          delay: 0.4,\n          duration: duration / 1000,\n          times: [0, 0.01, 0.99, 1],\n        },\n        scale: {\n          delay: 0.3,\n          duration: duration / 1000,\n          ease: QUAD_IN,\n        },\n        x: {\n          delay: 0.3,\n          duration: duration / 1000,\n          ease: QUINT_OUT,\n        },\n        y: {\n          delay: 0.3,\n          duration: duration / 1000,\n          ease: QUINT_OUT,\n        },\n      }}\n    />\n  );\n};\n\nconst BurstAnimation = ({ flights }: { flights: Flight[] }) => (\n  <div className=\"pointer-events-none absolute -top-3 -left-3 grid size-10 place-items-center\">\n    {flights.map((flight) => (\n      <Particle key={flight.angle} {...flight} />\n    ))}\n  </div>\n);\n\nexport const LikeButton = () => {\n  const [isLiked, setIsLiked] = useState(false);\n  const [burst, setBurst] = useState<Flight[] | null>(null);\n  const isAnimating = burst !== null;\n\n  const toggleLike = () => {\n    if (isLiked) {\n      setIsLiked(false);\n    } else {\n      setIsLiked(true);\n      setBurst(rollBurst());\n    }\n  };\n\n  return (\n    <button\n      type=\"button\"\n      className=\"hover:bg-accent relative flex h-8 cursor-pointer items-center gap-1.5 rounded-lg p-2 transition\"\n      onClick={toggleLike}\n    >\n      <div className=\"relative\">\n        {isAnimating && <CircleAnimation />}\n        {burst && <BurstAnimation flights={burst} />}\n        {isAnimating ? (\n          <motion.svg\n            key=\"animating-heart\"\n            initial={{ scale: 0 }}\n            animate={{ scale: 1 }}\n            transition={{\n              damping: 10,\n              delay: 0.3,\n              stiffness: 300,\n              type: \"spring\",\n            }}\n            onAnimationComplete={() => setBurst(null)}\n            className=\"text-red-500\"\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"currentColor\"\n          >\n            <path d={HEART_PATH} />\n          </motion.svg>\n        ) : (\n          <svg\n            className={isLiked ? \"text-red-500\" : \"text-inherit\"}\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n            fill=\"currentColor\"\n          >\n            <path d={HEART_PATH} />\n          </svg>\n        )}\n      </div>\n      <span className=\"min-w-[0.75rem]\">\n        <NumberFlow value={isLiked ? 1 : 0} />\n        <span className=\"sr-only\"> likes, click to {isLiked ? \"unlike\" : \"like\"}</span>\n      </span>\n    </button>\n  );\n};\n","path":"/like-button.tsx","target":"uicapsule/like-button/like-button.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { LikeButton } from \"./like-button\";\n\nconst Preview = () => (\n  <div className=\"flex h-screen items-center justify-center text-gray-400\">\n    <LikeButton />\n  </div>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/like-button/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/like-button","name":"like-button","registryDependencies":[],"title":"Like Button","type":"registry:block"}