{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","dependencies":["three"],"devDependencies":["@types/three"],"files":[{"content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport * as THREE from \"three/webgpu\";\nimport {\n  Fn,\n  If,\n  Break,\n  Loop,\n  uniform,\n  uv,\n  float,\n  vec2,\n  vec3,\n  mix,\n  clamp,\n  smoothstep,\n  length,\n  normalize,\n  dot,\n  pow,\n  exp,\n  abs,\n  min,\n  max,\n  sin,\n  cos,\n  sqrt,\n} from \"three/tsl\";\n\n/**\n * Chibi plants raymarched from signed distance fields — the same technique as\n * tobis.vision's \"critters\": every part (pot, body, leaves, arms) is an SDF\n * primitive smooth-blended into one continuous soft-vinyl surface, rendered\n * with three.js TSL on the WebGPU renderer (transparent WebGL2 fallback).\n * The face is painted in the shader; the whole plant turns toward your cursor,\n * its eyes lead the head, it blinks, and switching characters morphs the\n * surface rather than swapping models.\n */\n\nexport const CHIBI_VARIANTS = [\"pip\", \"momo\", \"fifi\", \"kiki\"] as const;\nexport type ChibiVariant = (typeof CHIBI_VARIANTS)[number];\n\nexport interface ChibiPlantsProps {\n  /** Which plant character to show. Switching morphs the SDF surface. @default \"pip\" */\n  variant?: ChibiVariant;\n  /** Override the pot color (CSS hex). Defaults to the variant's palette. */\n  potColor?: string;\n  /** Override the body color (CSS hex). Defaults to the variant's palette. */\n  bodyColor?: string;\n  /** Override the leaf/sprout color (CSS hex). Defaults to the variant's palette. */\n  leafColor?: string;\n  /** Blush color. @default \"#e8798f\" */\n  cheekColor?: string;\n  /** Background radial-gradient stops [inner, outer]. @default [\"#2e2a33\", \"#0b0a0e\"] */\n  background?: [string, string];\n  /** Eye size multiplier. @default 1 */\n  eyeScale?: number;\n  /** How strongly the head + eyes follow the cursor, 0–1.5. @default 1 */\n  gaze?: number;\n  /** Idle sway/breathing amount, 0–2. @default 1 */\n  wobble?: number;\n  /** Idle animation speed multiplier. @default 1 */\n  speed?: number;\n  /** Raymarch step count (read once at mount). Lower on weak GPUs. @default 80 */\n  raySteps?: number;\n  className?: string;\n}\n\n/** Morphable SDF + face parameters. Every key is spring-lerped, so any two\n * variants (or user tweaks) blend as one continuous surface deformation.\n * The key list is the single source of truth: PlantParams derives from it. */\nconst PARAM_KEYS = [\n  \"bodyR\",\n  \"squash\",\n  \"stemH\",\n  \"l0s\",\n  \"l0yaw\",\n  \"l0tilt\",\n  \"l0len\",\n  \"l0wid\",\n  \"l1s\",\n  \"l1yaw\",\n  \"l1tilt\",\n  \"l1len\",\n  \"l1wid\",\n  \"l2s\",\n  \"l2yaw\",\n  \"l2tilt\",\n  \"l2len\",\n  \"l2wid\",\n  \"armS\",\n  \"eyeR\",\n  \"eyeSep\",\n  \"eyeY\",\n  \"mouthW\",\n  \"cheek\",\n] as const;\ntype PlantParams = Record<(typeof PARAM_KEYS)[number], number>;\n\ninterface Palette {\n  body: string;\n  leaf: string;\n  pot: string;\n}\ninterface VariantDef {\n  label: string;\n  params: PlantParams;\n  palette: Palette;\n}\n\nexport const VARIANTS = {\n  fifi: {\n    label: \"Fifi\",\n    palette: { body: \"#7cc487\", leaf: \"#4e9e63\", pot: \"#e8e3da\" },\n    params: {\n      armS: 0.001,\n      bodyR: 0.34,\n      cheek: 0.55,\n      eyeR: 0.15,\n      eyeSep: 0.33,\n      eyeY: 0.22,\n      l0len: 0.24,\n      l0s: 1,\n      l0tilt: 0.85,\n      l0wid: 0.12,\n      l0yaw: 0,\n      l1len: 0.22,\n      l1s: 1,\n      l1tilt: 1.1,\n      l1wid: 0.11,\n      l1yaw: 2.1,\n      l2len: 0.22,\n      l2s: 1,\n      l2tilt: 1.1,\n      l2wid: 0.11,\n      l2yaw: -2.1,\n      mouthW: 1,\n      squash: 1.12,\n      stemH: 0.12,\n    },\n  },\n  kiki: {\n    label: \"Kiki\",\n    palette: { body: \"#7fae72\", leaf: \"#f193b4\", pot: \"#e2986a\" },\n    params: {\n      armS: 1,\n      bodyR: 0.32,\n      cheek: 0.7,\n      eyeR: 0.17,\n      eyeSep: 0.27,\n      eyeY: 0.12,\n      l0len: 0.12,\n      l0s: 0.7,\n      l0tilt: 0.15,\n      l0wid: 0.08,\n      l0yaw: 0,\n      l1len: 0.1,\n      l1s: 0.001,\n      l1tilt: 0.9,\n      l1wid: 0.07,\n      l1yaw: -1.57,\n      l2len: 0.1,\n      l2s: 0.001,\n      l2tilt: 0.9,\n      l2wid: 0.07,\n      l2yaw: 1.57,\n      mouthW: 0.7,\n      squash: 1.5,\n      stemH: 0.03,\n    },\n  },\n  momo: {\n    label: \"Momo\",\n    palette: { body: \"#f2a7bb\", leaf: \"#7cbf80\", pot: \"#ead9c8\" },\n    params: {\n      armS: 0.001,\n      bodyR: 0.43,\n      cheek: 1,\n      eyeR: 0.185,\n      eyeSep: 0.3,\n      eyeY: 0.02,\n      l0len: 0.15,\n      l0s: 0.85,\n      l0tilt: 0.55,\n      l0wid: 0.09,\n      l0yaw: 0.3,\n      l1len: 0.12,\n      l1s: 0.001,\n      l1tilt: 0.9,\n      l1wid: 0.08,\n      l1yaw: -1.57,\n      l2len: 0.12,\n      l2s: 0.001,\n      l2tilt: 0.9,\n      l2wid: 0.08,\n      l2yaw: 1.57,\n      mouthW: 0.55,\n      squash: 0.78,\n      stemH: 0.04,\n    },\n  },\n  pip: {\n    label: \"Pip\",\n    palette: { body: \"#9fd6a3\", leaf: \"#67b26f\", pot: \"#d98b71\" },\n    params: {\n      armS: 0.001,\n      bodyR: 0.34,\n      cheek: 0.8,\n      eyeR: 0.155,\n      eyeSep: 0.27,\n      eyeY: 0.06,\n      l0len: 0.17,\n      l0s: 1,\n      l0tilt: 0.95,\n      l0wid: 0.1,\n      l0yaw: 1.57,\n      l1len: 0.17,\n      l1s: 1,\n      l1tilt: 0.95,\n      l1wid: 0.1,\n      l1yaw: -1.57,\n      l2len: 0.12,\n      l2s: 0.001,\n      l2tilt: 0.5,\n      l2wid: 0.08,\n      l2yaw: 0,\n      mouthW: 0.8,\n      squash: 0.96,\n      stemH: 0.3,\n    },\n  },\n} satisfies Record<ChibiVariant, VariantDef>;\n\nconst DEFAULT_BG: [string, string] = [\"#2e2a33\", \"#0b0a0e\"];\n\nconst POT_H = 0.36;\nconst POT_R = 0.3;\n\nexport const ChibiPlants = ({\n  variant = \"pip\",\n  potColor,\n  bodyColor,\n  leafColor,\n  cheekColor = \"#e8798f\",\n  background = DEFAULT_BG,\n  eyeScale = 1,\n  gaze = 1,\n  wobble = 1,\n  speed = 1,\n  raySteps = 80,\n  className,\n}: ChibiPlantsProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  // Live tuning values, read every frame by the render loop.\n  const tuningRef = useRef({\n    background,\n    bodyColor,\n    cheekColor,\n    eyeScale,\n    gaze,\n    leafColor,\n    potColor,\n    speed,\n    variant,\n    wobble,\n  });\n  useEffect(() => {\n    tuningRef.current = {\n      background,\n      bodyColor,\n      cheekColor,\n      eyeScale,\n      gaze,\n      leafColor,\n      potColor,\n      speed,\n      variant,\n      wobble,\n    };\n  }, [\n    variant,\n    potColor,\n    bodyColor,\n    leafColor,\n    cheekColor,\n    background,\n    eyeScale,\n    gaze,\n    wobble,\n    speed,\n  ]);\n  const stepsRef = useRef(raySteps);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n    let disposed = false;\n    let raf = 0;\n\n    const renderer = new THREE.WebGPURenderer({ antialias: true });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));\n    renderer.domElement.style.width = \"100%\";\n    renderer.domElement.style.height = \"100%\";\n    renderer.domElement.style.display = \"block\";\n\n    // Start from the requested variant and colors — the morph springs are for\n    // later changes, not the initial mount.\n    const initialTuning = tuningRef.current;\n    const initialVariant = VARIANTS[initialTuning.variant] ?? VARIANTS.pip;\n    const initialParams = {\n      ...initialVariant.params,\n      eyeR: initialVariant.params.eyeR * initialTuning.eyeScale,\n    } satisfies PlantParams;\n    const initialBody = initialTuning.bodyColor ?? initialVariant.palette.body;\n    const initialLeaf = initialTuning.leafColor ?? initialVariant.palette.leaf;\n    const initialPot = initialTuning.potColor ?? initialVariant.palette.pot;\n\n    // ---- uniforms ----------------------------------------------------------\n    const uTime = uniform(0);\n    const uLook = uniform(new THREE.Vector2(0, 0));\n    const uBlink = uniform(0);\n    const uExcite = uniform(0);\n    const uGaze = uniform(1);\n    const uWobble = uniform(1);\n    const uRes = uniform(new THREE.Vector2(1, 1));\n    const uBg1 = uniform(new THREE.Color(initialTuning.background[0]));\n    const uBg2 = uniform(new THREE.Color(initialTuning.background[1]));\n    const uBodyC = uniform(new THREE.Color(initialBody));\n    const uLeafC = uniform(new THREE.Color(initialLeaf));\n    const uPotC = uniform(new THREE.Color(initialPot));\n    const uCheekC = uniform(new THREE.Color(initialTuning.cheekColor));\n\n    const uParams = {\n      armS: uniform(initialParams.armS),\n      bodyR: uniform(initialParams.bodyR),\n      cheek: uniform(initialParams.cheek),\n      eyeR: uniform(initialParams.eyeR),\n      eyeSep: uniform(initialParams.eyeSep),\n      eyeY: uniform(initialParams.eyeY),\n      l0len: uniform(initialParams.l0len),\n      l0s: uniform(initialParams.l0s),\n      l0tilt: uniform(initialParams.l0tilt),\n      l0wid: uniform(initialParams.l0wid),\n      l0yaw: uniform(initialParams.l0yaw),\n      l1len: uniform(initialParams.l1len),\n      l1s: uniform(initialParams.l1s),\n      l1tilt: uniform(initialParams.l1tilt),\n      l1wid: uniform(initialParams.l1wid),\n      l1yaw: uniform(initialParams.l1yaw),\n      l2len: uniform(initialParams.l2len),\n      l2s: uniform(initialParams.l2s),\n      l2tilt: uniform(initialParams.l2tilt),\n      l2wid: uniform(initialParams.l2wid),\n      l2yaw: uniform(initialParams.l2yaw),\n      mouthW: uniform(initialParams.mouthW),\n      squash: uniform(initialParams.squash),\n      stemH: uniform(initialParams.stemH),\n    };\n\n    // ---- TSL node helpers --------------------------------------------------\n    type Scalar = THREE.Node<\"float\">;\n    type Point = THREE.Node<\"vec3\">;\n    const smin = (a: Scalar, b: Scalar, k: number) => {\n      const h = clamp(\n        b\n          .sub(a)\n          .mul(0.5 / k)\n          .add(0.5),\n        0,\n        1,\n      );\n      return mix(b, a, h).sub(h.mul(h.oneMinus()).mul(k));\n    };\n    const sdEllipsoid = (p: Point, r: Point) => {\n      const k0 = length(p.div(r));\n      const k1 = length(p.div(r.mul(r)));\n      return k0.mul(k0.sub(1)).div(k1.add(1e-6));\n    };\n    const sdVCapsule = (p: Point, h: Scalar, r: Scalar) =>\n      length(vec3(p.x, p.y.sub(clamp(p.y, 0, h)), p.z)).sub(r);\n    // Rotate about Y / X by angle a (applied to sample points; negate a to\n    // rotate the object by +a).\n    const rotY = (p: Point, a: Scalar) => {\n      const c = cos(a);\n      const s = sin(a);\n      return vec3(p.x.mul(c).add(p.z.mul(s)), p.y, p.x.negate().mul(s).add(p.z.mul(c)));\n    };\n    const rotX = (p: Point, a: Scalar) => {\n      const c = cos(a);\n      const s = sin(a);\n      return vec3(p.x, p.y.mul(c).sub(p.z.mul(s)), p.y.mul(s).add(p.z.mul(c)));\n    };\n\n    // Breathing + idle sway, all driven by uTime so the plant is alive at rest.\n    const breathe = () => sin(uTime.mul(2.1)).mul(uWobble).toVar();\n\n    // Head orientation: cursor follow + idle sway.\n    const headAngles = () => {\n      const yaw = uLook.x.mul(uGaze.mul(-0.55)).add(sin(uTime.mul(0.6)).mul(0.07).mul(uWobble));\n      const pitch = uLook.y\n        .mul(uGaze.mul(0.38))\n        .add(sin(uTime.mul(0.83).add(1.7)).mul(0.05).mul(uWobble));\n      return { pitch: pitch.toVar(), yaw: yaw.toVar() };\n    };\n\n    // Transform a world point into the plant's head-local space (body center\n    // at origin, un-rotated). Face painting and every above-pot SDF use this.\n    const bodyCenterY = () => uParams.bodyR.mul(uParams.squash).mul(0.55).add(POT_H).toVar();\n\n    const toPlantLocal = (p: Point, yaw: Scalar, pitch: Scalar) => {\n      const q = vec3(p.x, p.y.sub(bodyCenterY()), p.z).toVar();\n      return rotX(rotY(q, yaw), pitch).toVar();\n    };\n\n    const sdPot = (p: Point) => {\n      const py = p.y.sub(POT_H * 0.5);\n      const ra = float(POT_R).mul(p.y.div(POT_H).sub(0.5).mul(0.3).add(1));\n      const dx = length(vec2(p.x, p.z)).sub(ra).add(0.03);\n      const dy = abs(py).sub(POT_H * 0.5 - 0.02);\n      const d2 = vec2(dx, dy).toVar();\n      const body = min(max(d2.x, d2.y), 0)\n        .add(length(max(d2, vec2(0, 0))))\n        .sub(0.03);\n      const rim = length(vec2(length(vec2(p.x, p.z)).sub(POT_R * 1.1), p.y.sub(POT_H))).sub(0.038);\n      return smin(body, rim, 0.02).toVar();\n    };\n\n    // One leaf slot: yaw around the stem, tilt outward, ellipsoid blade.\n    const sdLeaf = (\n      q: Point,\n      baseY: Scalar,\n      s: Scalar,\n      lyaw: Scalar,\n      tilt: Scalar,\n      len: Scalar,\n      wid: Scalar,\n    ) => {\n      const ll = len.mul(s).toVar();\n      const ww = wid.mul(s).toVar();\n      const lq = rotX(rotY(vec3(q.x, q.y.sub(baseY), q.z), lyaw), tilt.negate()).toVar();\n      return sdEllipsoid(\n        vec3(lq.x, lq.y.sub(ll.mul(0.55).add(0.02)), lq.z),\n        vec3(ww, ll.mul(0.6), ww.mul(0.45)),\n      );\n    };\n\n    // Part distances in plant-local space; combined by map(), re-queried at the\n    // hit point for smooth part-color weights (the \"one continuous surface\" look).\n    const plantParts = (q: Point) => {\n      const br = breathe();\n      const rB = uParams.bodyR.mul(br.mul(-0.012).add(1)).toVar();\n      const sq = uParams.squash.mul(br.mul(0.02).add(1)).toVar();\n      const dBody = sdEllipsoid(q, vec3(rB, rB.mul(sq), rB)).toVar();\n\n      const bodyTop = rB.mul(sq).sub(0.03).toVar();\n      const dStem = sdVCapsule(\n        vec3(q.x, q.y.sub(bodyTop), q.z),\n        uParams.stemH,\n        float(0.035),\n      ).toVar();\n      const knobR = clamp(uParams.stemH.mul(10), 0, 1).mul(0.05);\n      const dKnob = length(vec3(q.x, q.y.sub(bodyTop).sub(uParams.stemH), q.z)).sub(knobR);\n      const leafBase = bodyTop.sub(0.02).add(uParams.stemH.mul(0.92)).toVar();\n      const dL0 = sdLeaf(\n        q,\n        leafBase,\n        uParams.l0s,\n        uParams.l0yaw,\n        uParams.l0tilt,\n        uParams.l0len,\n        uParams.l0wid,\n      );\n      const dL1 = sdLeaf(\n        q,\n        leafBase,\n        uParams.l1s,\n        uParams.l1yaw,\n        uParams.l1tilt,\n        uParams.l1len,\n        uParams.l1wid,\n      );\n      const dL2 = sdLeaf(\n        q,\n        leafBase,\n        uParams.l2s,\n        uParams.l2yaw,\n        uParams.l2tilt,\n        uParams.l2len,\n        uParams.l2wid,\n      );\n      const dGreen = min(min(smin(dStem, dKnob, 0.03), dL0), min(dL1, dL2)).toVar();\n\n      const aq = vec3(abs(q.x), q.y, q.z).toVar();\n      const armR = uParams.armS.mul(0.095).toVar();\n      const armH = uParams.armS.mul(0.26).toVar();\n      const dArm = sdVCapsule(\n        vec3(aq.x.sub(rB.mul(0.92)), aq.y.add(rB.mul(sq).mul(0.05)), aq.z),\n        armH,\n        armR,\n      ).toVar();\n\n      return { dArm, dBody, dGreen };\n    };\n\n    const sceneSdf = (p: Point, yaw: Scalar, pitch: Scalar) => {\n      const dPot = sdPot(p);\n      const q = toPlantLocal(p, yaw, pitch);\n      const { dBody, dGreen, dArm } = plantParts(q);\n      let d = smin(dPot, dBody, 0.09);\n      d = smin(d, dGreen, 0.045);\n      d = smin(d, dArm, 0.07);\n      return d.toVar();\n    };\n\n    const mapFn = Fn(([p]: [Point]) => {\n      const { yaw, pitch } = headAngles();\n      return sceneSdf(p, yaw, pitch);\n    });\n\n    const calcNormal = (p: Point) => {\n      const h = 0.0045;\n      const e1 = vec3(1, -1, -1);\n      const e2 = vec3(-1, -1, 1);\n      const e3 = vec3(-1, 1, -1);\n      const e4 = vec3(1, 1, 1);\n      return normalize(\n        e1\n          .mul(mapFn(p.add(e1.mul(h))))\n          .add(e2.mul(mapFn(p.add(e2.mul(h)))))\n          .add(e3.mul(mapFn(p.add(e3.mul(h)))))\n          .add(e4.mul(mapFn(p.add(e4.mul(h))))),\n      ).toVar();\n    };\n\n    // ---- fragment ----------------------------------------------------------\n    const RO = new THREE.Vector3(0, 0.62, 2.35);\n    const TA = new THREE.Vector3(0, 0.52, 0);\n    const fw = TA.clone().sub(RO).normalize();\n    const ri = fw\n      .clone()\n      .cross(new THREE.Vector3(0, 1, 0))\n      .normalize();\n    const up = ri.clone().cross(fw);\n    const FOV_S = 0.36;\n\n    const fragment = Fn(() => {\n      const aspect = uRes.x.div(uRes.y);\n      const ndc = uv().mul(2).sub(1).toVar();\n      const nx = ndc.x.mul(aspect).toVar();\n      const ro = vec3(RO.x, RO.y, RO.z);\n      const rd = normalize(\n        vec3(ri.x, ri.y, ri.z)\n          .mul(nx.mul(FOV_S))\n          .add(vec3(up.x, up.y, up.z).mul(ndc.y.mul(FOV_S)))\n          .add(vec3(fw.x, fw.y, fw.z)),\n      ).toVar();\n\n      // Background: radial dusk gradient + soft contact shadow on the floor.\n      const bgT = smoothstep(0.05, 1.25, length(vec2(nx, ndc.y.mul(1.15).add(0.12))));\n      const col = mix(uBg1.rgb, uBg2.rgb, bgT).toVar();\n      const tg = ro.y.sub(0.001).div(max(rd.y.negate(), 1e-4)).toVar();\n      const gp = ro.add(rd.mul(tg)).toVar();\n      const shadowR = length(vec2(gp.x, gp.z.mul(1.35)));\n      const shadow = exp(shadowR.mul(shadowR).mul(-6))\n        .mul(0.42)\n        .mul(smoothstep(0, 0.02, rd.y.negate()));\n      col.assign(col.mul(shadow.oneMinus()));\n\n      // Bounding-sphere pre-test so empty pixels stay cheap.\n      const bc = vec3(0, 0.58, 0);\n      const oc = ro.sub(bc).toVar();\n      const bq = dot(oc, rd).toVar();\n      const bh = bq\n        .mul(bq)\n        .sub(dot(oc, oc).sub(1.32 * 1.32))\n        .toVar();\n\n      If(bh.greaterThan(0), () => {\n        const tStart = max(bq.negate().sub(sqrt(bh)), 0).toVar();\n        const tEnd = bq.negate().add(sqrt(bh)).toVar();\n        const t = tStart.toVar();\n        const hit = float(0).toVar();\n\n        Loop(stepsRef.current, () => {\n          const pos = ro.add(rd.mul(t));\n          const d = mapFn(pos).toVar();\n          If(d.lessThan(0.0013), () => {\n            hit.assign(1);\n            Break();\n          });\n          t.addAssign(d.mul(0.85));\n          If(t.greaterThan(tEnd), () => {\n            Break();\n          });\n        });\n\n        If(hit.greaterThan(0.5), () => {\n          const p = ro.add(rd.mul(t)).toVar();\n          const n = calcNormal(p);\n          const { yaw, pitch } = headAngles();\n          const q = toPlantLocal(p, yaw, pitch);\n          const { dBody, dGreen, dArm } = plantParts(q);\n          const dPot = sdPot(p);\n\n          // Soft part weights — colors bleed across the smin creases.\n          const wPot = exp(dPot.div(-0.028)).toVar();\n          const wBody = exp(dBody.div(-0.028)).toVar();\n          const wGreen = exp(dGreen.div(-0.028)).toVar();\n          const wArm = exp(dArm.div(-0.028)).toVar();\n          const wSum = wPot.add(wBody).add(wGreen).add(wArm).toVar();\n          const albedo = uPotC.rgb\n            .mul(wPot)\n            .add(uBodyC.rgb.mul(wBody))\n            .add(uLeafC.rgb.mul(wGreen))\n            .add(uBodyC.rgb.mul(0.92).mul(wArm))\n            .div(wSum)\n            .toVar();\n          const bodyW = wBody.div(wSum).toVar();\n\n          // ---- face, painted on the front of the body ----\n          const sq = uParams.squash;\n          const nd = normalize(q.div(vec3(1, sq, 1))).toVar();\n          const front = smoothstep(0.05, 0.4, nd.z)\n            .mul(smoothstep(0.55, 0.75, bodyW))\n            .toVar();\n          const fu = nd.x.toVar();\n          const fv = nd.y.toVar();\n          const leadX = uLook.x.mul(0.2).mul(uGaze).toVar();\n          const leadY = uLook.y.mul(0.16).mul(uGaze).toVar();\n\n          const eyeRr = uParams.eyeR\n            .mul(uExcite.mul(0.3).add(1))\n            .mul(uBlink.mul(0.3).oneMinus())\n            .toVar();\n          const blinkK = float(1).div(uBlink.mul(0.94).oneMinus()).toVar();\n          const eyeMask = float(0).toVar();\n          const hlMask = float(0).toVar();\n          for (const sign of [-1, 1]) {\n            const c = vec2(uParams.eyeSep.mul(sign).add(leadX), uParams.eyeY.add(leadY)).toVar();\n            const dv = vec2(fu.sub(c.x), fv.sub(c.y).mul(blinkK)).toVar();\n            eyeMask.assign(max(eyeMask, smoothstep(eyeRr, eyeRr.sub(0.015), length(dv))));\n            const h1 = vec2(fu.sub(c.x).add(eyeRr.mul(0.3)), fv.sub(c.y).sub(eyeRr.mul(0.34)));\n            hlMask.assign(\n              max(hlMask, smoothstep(eyeRr.mul(0.3), eyeRr.mul(0.3).sub(0.012), length(h1))),\n            );\n            const h2 = vec2(fu.sub(c.x).sub(eyeRr.mul(0.28)), fv.sub(c.y).add(eyeRr.mul(0.3)));\n            hlMask.assign(\n              max(hlMask, smoothstep(eyeRr.mul(0.14), eyeRr.mul(0.14).sub(0.012), length(h2))),\n            );\n          }\n          hlMask.assign(hlMask.mul(eyeMask).mul(uBlink.oneMinus()));\n\n          const mw = uParams.mouthW.mul(uExcite.mul(0.5).add(1)).mul(0.058).toVar();\n          const mdv = vec2(\n            fu.sub(leadX.mul(0.6)),\n            fv.sub(uParams.eyeY).add(0.14).sub(leadY.mul(0.5)).mul(1.4),\n          ).toVar();\n          const mouthMask = smoothstep(mw, mw.sub(0.012), length(mdv))\n            .mul(eyeMask.oneMinus())\n            .toVar();\n\n          const cheekMask = float(0).toVar();\n          for (const sign of [-1, 1]) {\n            const cc = vec2(\n              uParams.eyeSep.add(uParams.eyeR).add(0.09).mul(sign),\n              uParams.eyeY.sub(0.1),\n            );\n            cheekMask.assign(\n              max(cheekMask, smoothstep(0.085, 0.02, length(vec2(fu.sub(cc.x), fv.sub(cc.y))))),\n            );\n          }\n\n          albedo.assign(mix(albedo, uCheekC.rgb, cheekMask.mul(uParams.cheek).mul(0.5).mul(front)));\n          albedo.assign(mix(albedo, vec3(0.16, 0.11, 0.09), eyeMask.mul(front)));\n          albedo.assign(mix(albedo, vec3(0.28, 0.13, 0.11), mouthMask.mul(front)));\n          albedo.assign(mix(albedo, vec3(0.95, 0.95, 0.97), hlMask.mul(front)));\n\n          // ---- soft vinyl shading ----\n          const ld = normalize(vec3(0.55, 0.75, 0.5));\n          const dif = clamp(dot(n, ld).mul(0.5).add(0.5), 0, 1).toVar();\n          const skyT = n.y.mul(0.5).add(0.5);\n          const amb = mix(vec3(0.3, 0.27, 0.33), vec3(0.56, 0.58, 0.67), skyT).toVar();\n          const ao1 = clamp(mapFn(p.add(n.mul(0.05))).div(0.05), 0, 1);\n          const ao2 = clamp(mapFn(p.add(n.mul(0.16))).div(0.16), 0, 1);\n          const ao = ao1.mul(0.55).add(ao2.mul(0.45)).mul(0.7).add(0.3).toVar();\n          const lit = albedo.mul(amb.add(dif.mul(vec3(0.98, 0.9, 0.8)).mul(1.05)).mul(ao)).toVar();\n          const hv = normalize(ld.sub(rd));\n          const spec = pow(clamp(dot(n, hv), 0, 1), 50)\n            .mul(0.55)\n            .mul(ao);\n          const fres = pow(clamp(dot(n, rd.negate()), 0, 1).oneMinus(), 3.5).mul(0.3);\n          lit.assign(\n            lit\n              .add(vec3(1, 0.98, 0.95).mul(spec))\n              .add(mix(uBg1.rgb, vec3(0.6, 0.62, 0.7), 0.5).mul(fres)),\n          );\n\n          col.assign(lit);\n        });\n      });\n\n      return vec3(col);\n    });\n\n    const material = new THREE.MeshBasicNodeMaterial();\n    material.colorNode = fragment();\n    const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n    const scene = new THREE.Scene();\n    scene.add(quad);\n    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n    // ---- animation state ---------------------------------------------------\n    const cur = { ...initialParams };\n    const vel = { ...initialParams };\n    const target = { ...initialParams };\n    for (const key of PARAM_KEYS) {\n      vel[key] = 0;\n    }\n    const curCol = {\n      body: new THREE.Color(initialBody),\n      leaf: new THREE.Color(initialLeaf),\n      pot: new THREE.Color(initialPot),\n    };\n    const tgtCol = { body: new THREE.Color(), leaf: new THREE.Color(), pot: new THREE.Color() };\n\n    const look = { tx: 0, ty: 0.1, vx: 0, vy: 0, x: 0, y: 0 };\n    let lastPointer = -1e4;\n    let nextWander = 0;\n    let blink = 0;\n    let blinkTarget = 0;\n    let nextBlink = 1.2;\n    let excite = 0;\n    let elapsed = 0;\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n      const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n      const speedMag = Math.hypot(e.movementX ?? 0, e.movementY ?? 0);\n      excite = Math.min(1, excite + speedMag * 0.004);\n      look.tx = THREE.MathUtils.clamp(nx * 1.1, -1, 1);\n      look.ty = THREE.MathUtils.clamp(ny * 1.1, -1, 1);\n      lastPointer = elapsed;\n    };\n    window.addEventListener(\"pointermove\", onPointerMove);\n\n    const resize = () => {\n      const w = container.clientWidth || 1;\n      const h = container.clientHeight || 1;\n      renderer.setSize(w, h, false);\n      const dpr = Math.min(window.devicePixelRatio, 1.5);\n      uRes.value.set(w * dpr, h * dpr);\n    };\n    const ro2 = new ResizeObserver(resize);\n    ro2.observe(container);\n\n    let prev = performance.now();\n    const frame = () => {\n      raf = requestAnimationFrame(frame);\n      const now = performance.now();\n      const dt = Math.min(0.05, (now - prev) / 1000);\n      prev = now;\n      const tuning = tuningRef.current;\n      elapsed += dt * tuning.speed;\n      uTime.value = elapsed;\n      uGaze.value = tuning.gaze;\n      uWobble.value = tuning.wobble;\n\n      // Variant + prop overrides drive the morph targets.\n      const v = VARIANTS[tuning.variant] ?? VARIANTS.pip;\n      for (const k of PARAM_KEYS) {\n        target[k] = v.params[k];\n      }\n      target.eyeR = v.params.eyeR * tuning.eyeScale;\n      tgtCol.body.set(tuning.bodyColor ?? v.palette.body);\n      tgtCol.leaf.set(tuning.leafColor ?? v.palette.leaf);\n      tgtCol.pot.set(tuning.potColor ?? v.palette.pot);\n      uCheekC.value.set(tuning.cheekColor);\n      uBg1.value.set(tuning.background[0]);\n      uBg2.value.set(tuning.background[1]);\n\n      // Slightly underdamped springs make morphs land with a squishy overshoot.\n      const omega = 9;\n      const zeta = 0.72;\n      for (const k of PARAM_KEYS) {\n        const x = cur[k] - target[k];\n        const a = -omega * omega * x - 2 * zeta * omega * vel[k];\n        vel[k] += a * dt;\n        cur[k] += vel[k] * dt;\n        uParams[k].value = cur[k];\n      }\n      const cf = 1 - Math.exp(-dt * 7);\n      curCol.body.lerp(tgtCol.body, cf);\n      curCol.leaf.lerp(tgtCol.leaf, cf);\n      curCol.pot.lerp(tgtCol.pot, cf);\n      uBodyC.value.copy(curCol.body);\n      uLeafC.value.copy(curCol.leaf);\n      uPotC.value.copy(curCol.pot);\n\n      // Idle wander when the cursor has gone quiet.\n      if (elapsed - lastPointer > 2.4 && elapsed > nextWander) {\n        look.tx = (Math.random() * 2 - 1) * 0.85;\n        look.ty = Math.random() * 0.9 - 0.35;\n        nextWander = elapsed + 1.3 + Math.random() * 1.5;\n      }\n      const lo = 10;\n      const lz = 0.85;\n      const ax = -lo * lo * (look.x - look.tx) - 2 * lz * lo * look.vx;\n      const ay = -lo * lo * (look.y - look.ty) - 2 * lz * lo * look.vy;\n      look.vx += ax * dt;\n      look.vy += ay * dt;\n      look.x += look.vx * dt;\n      look.y += look.vy * dt;\n      uLook.value.set(look.x, look.y);\n\n      // Blinks: quick close, softer open, occasional double.\n      if (elapsed > nextBlink && blinkTarget === 0) {\n        blinkTarget = 1;\n        nextBlink = elapsed + 2 + Math.random() * 3.2 + (Math.random() < 0.18 ? -1.75 : 0);\n      }\n      blink += (blinkTarget - blink) * Math.min(1, dt * (blinkTarget === 1 ? 26 : 13));\n      if (blink > 0.93) {\n        blinkTarget = 0;\n      }\n      uBlink.value = blink;\n\n      excite = Math.max(0, excite - dt * 1.4);\n      uExcite.value = excite;\n\n      renderer.render(scene, camera);\n    };\n\n    container.append(renderer.domElement);\n    void (async () => {\n      try {\n        await renderer.init();\n      } catch (error) {\n        console.error(\"chibi-plants: renderer init failed\", error);\n        return;\n      }\n      if (disposed) {\n        return;\n      }\n      resize();\n      prev = performance.now();\n      raf = requestAnimationFrame(frame);\n    })();\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      ro2.disconnect();\n      window.removeEventListener(\"pointermove\", onPointerMove);\n      renderer.domElement.remove();\n      quad.geometry.dispose();\n      material.dispose();\n      renderer.dispose();\n    };\n    // The shader graph is built once; live values flow through uniforms/refs.\n  }, []);\n\n  return <div ref={containerRef} className={className ?? \"h-full w-full\"} />;\n};\n","path":"/chibi-plants.tsx","target":"uicapsule/chibi-plants/chibi-plants.tsx","type":"registry:file"},{"content":"{\n  \"name\": \"@uicapsule/chibi-plants\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .cache .turbo dist node_modules\"\n  },\n  \"dependencies\": {\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"three\": \"^0.186.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@types/three\": \"^0.186.0\"\n  }\n}\n","path":"/package.json","target":"uicapsule/chibi-plants/package.json","type":"registry:file"},{"content":"\"use client\";\n\nimport { useState } from \"react\";\n\nimport type { ChibiVariant } from \"./chibi-plants\";\nimport { CHIBI_VARIANTS, ChibiPlants, VARIANTS } from \"./chibi-plants\";\n\nconst POT_SWATCHES = [\"#d98b71\", \"#ead9c8\", \"#7f9bd1\", \"#4a4e59\"];\n\nconst Slider = ({\n  label,\n  value,\n  min,\n  max,\n  onChange,\n}: {\n  label: string;\n  value: number;\n  min: number;\n  max: number;\n  onChange: (v: number) => void;\n}) => (\n  <label className=\"flex items-center gap-2\">\n    <span className=\"w-12 text-[11px] tracking-wide text-white/45\">{label}</span>\n    <input\n      type=\"range\"\n      min={min}\n      max={max}\n      step={0.01}\n      value={value}\n      onChange={(e) => onChange(Number(e.target.value))}\n      className=\"h-1 w-24 cursor-pointer appearance-none rounded-full bg-white/15 accent-white/80\"\n    />\n  </label>\n);\n\nconst Preview = () => {\n  const [variant, setVariant] = useState<ChibiVariant>(\"pip\");\n  const [eyeScale, setEyeScale] = useState(1);\n  const [wobble, setWobble] = useState(1);\n  const [gaze, setGaze] = useState(1);\n  const [potColor, setPotColor] = useState<string | undefined>();\n\n  return (\n    <main className=\"relative h-dvh w-full overflow-hidden bg-[#0b0a0e]\">\n      <ChibiPlants\n        variant={variant}\n        eyeScale={eyeScale}\n        wobble={wobble}\n        gaze={gaze}\n        potColor={potColor}\n        className=\"absolute inset-0\"\n      />\n\n      <p className=\"pointer-events-none absolute top-6 left-1/2 -translate-x-1/2 text-xs tracking-wide text-white/30\">\n        move your cursor — they&apos;re watching\n      </p>\n\n      <div className=\"absolute bottom-6 left-1/2 flex w-max max-w-[calc(100vw-2rem)] -translate-x-1/2 flex-col items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-4 backdrop-blur-md\">\n        <div className=\"flex flex-wrap justify-center gap-2\">\n          {CHIBI_VARIANTS.map((v) => (\n            <button\n              key={v}\n              type=\"button\"\n              data-variant={v}\n              onClick={() => setVariant(v)}\n              className={`flex items-center gap-2 rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors ${\n                variant === v\n                  ? \"bg-white/90 text-neutral-900\"\n                  : \"bg-white/10 text-white/70 hover:bg-white/20\"\n              }`}\n            >\n              <span\n                className=\"size-2.5 rounded-full\"\n                style={{ background: VARIANTS[v].palette.body }}\n              />\n              {VARIANTS[v].label}\n            </button>\n          ))}\n        </div>\n        <div className=\"flex flex-wrap items-center justify-center gap-x-5 gap-y-2\">\n          <Slider label=\"eyes\" value={eyeScale} min={0.5} max={1.6} onChange={setEyeScale} />\n          <Slider label=\"wobble\" value={wobble} min={0} max={2} onChange={setWobble} />\n          <Slider label=\"gaze\" value={gaze} min={0} max={1.5} onChange={setGaze} />\n          <div className=\"flex items-center gap-1.5\">\n            {POT_SWATCHES.map((c) => (\n              <button\n                key={c}\n                type=\"button\"\n                aria-label={`pot ${c}`}\n                onClick={() => setPotColor((cur) => (cur === c ? undefined : c))}\n                className={`size-4 rounded-full transition-transform ${\n                  potColor === c ? \"scale-125 ring-2 ring-white/70\" : \"hover:scale-110\"\n                }`}\n                style={{ background: c }}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n};\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/chibi-plants/preview.tsx","type":"registry:file"}],"homepage":"https://uicapsule.com/ui/chibi-plants","name":"chibi-plants","registryDependencies":[],"type":"registry:block"}