{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-bag",
  "type": "registry:component",
  "title": "Empty Bag",
  "description": "Empty-cart art: a shopping bag slumps and sighs, perks up when a price tag dangles in, and puffs up proudly with a bounce when you add something.",
  "categories": [
    "illustration",
    "Empty States"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/empty-bag.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { PlusIcon } from \"lucide-react\";\nimport { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type EmptyBagHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<EmptyBagHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-300)] [--hue:var(--color-sky-500)] [--hue-deep:var(--color-sky-700)]\",\n  emerald: \"[--hue-hi:var(--color-emerald-300)] [--hue:var(--color-emerald-500)] [--hue-deep:var(--color-emerald-700)]\",\n  amber: \"[--hue-hi:var(--color-amber-300)] [--hue:var(--color-amber-500)] [--hue-deep:var(--color-amber-700)]\",\n  violet: \"[--hue-hi:var(--color-violet-300)] [--hue:var(--color-violet-500)] [--hue-deep:var(--color-violet-700)]\",\n  rose: \"[--hue-hi:var(--color-rose-300)] [--hue:var(--color-rose-500)] [--hue-deep:var(--color-rose-700)]\",\n  orange: \"[--hue-hi:var(--color-orange-300)] [--hue:var(--color-orange-500)] [--hue-deep:var(--color-orange-700)]\",\n  indigo: \"[--hue-hi:var(--color-indigo-300)] [--hue:var(--color-indigo-500)] [--hue-deep:var(--color-indigo-700)]\",\n  teal: \"[--hue-hi:var(--color-teal-300)] [--hue:var(--color-teal-500)] [--hue-deep:var(--color-teal-700)]\",\n};\n\nexport interface EmptyBagProps {\n  count?: number;\n  onAdd?: () => void;\n  title?: string;\n  description?: string;\n  filledTitle?: string;\n  filledDescription?: string;\n  actionLabel?: string;\n  price?: string;\n  hue?: EmptyBagHue;\n  className?: string;\n}\n\nconst W = 400;\nconst H = 175;\nconst TOP = 30;\nconst FLOOR = 196;\nconst TAG_X = 236;\n\ntype Pt = [number, number];\n\nconst lerp = (a: number, b: number, f: number) => a + (b - a) * f;\nconst mix = (a: Pt, b: Pt, f: number): Pt => [lerp(a[0], b[0], f), lerp(a[1], b[1], f)];\nconst pt = (p: Pt, dy = 0) => `${p[0].toFixed(2)} ${(p[1] + dy).toFixed(2)}`;\n\nfunction topEdge(f: number) {\n  return {\n    tl: mix([146, 90], [142, 78], f),\n    c1: mix([170, 104], [171, 78], f),\n    m: mix([199, 95], [200, 78], f),\n    c2: mix([226, 86], [229, 78], f),\n    tr: mix([254, 92], [258, 78], f),\n  };\n}\n\nfunction frontPath(f: number) {\n  const { tl, c1, m, c2, tr } = topEdge(f);\n  const sideR: Pt = [lerp(248, 261, f), 136];\n  const sideL: Pt = [lerp(152, 139, f), 136];\n  return `M${pt(tl)} Q${pt(c1)} ${pt(m)} Q${pt(c2)} ${pt(tr)} Q${pt(sideR)} 264 ${FLOOR - 7} Q264 ${FLOOR} 257 ${FLOOR} L143 ${FLOOR} Q136 ${FLOOR} 136 ${FLOOR - 7} Q${pt(sideL)} ${pt(tl)} Z`;\n}\n\nfunction rimPath(f: number) {\n  const { tl, c1, m, c2, tr } = topEdge(f);\n  return `M${pt(tl)} Q${pt(c1)} ${pt(m)} Q${pt(c2)} ${pt(tr)} L${pt(tr, 12)} Q${pt(c2, 12)} ${pt(m, 12)} Q${pt(c1, 12)} ${pt(tl, 12)} Z`;\n}\n\nfunction backPath(f: number) {\n  const l = mix([150, 100], [143, 79], f);\n  const m = mix([200, 104], [200, 62], f);\n  const r = mix([250, 98], [257, 79], f);\n  return `M${pt(l)} Q${pt(m)} ${pt(r)} L${r[0]} 150 L${l[0]} 150 Z`;\n}\n\nfunction anchors(f: number) {\n  const { tl, m, tr } = topEdge(f);\n  return { a: [176, lerp(tl[1], m[1], 0.6) + 15] as Pt, b: [224, lerp(m[1], tr[1], 0.4) + 15] as Pt };\n}\n\nfunction frontHandle(f: number) {\n  const { a, b } = anchors(f);\n  const lift = lerp(-34, -58, f);\n  const lean = lerp(-34, 0, f);\n  return `M${pt(a)} C${a[0] - 2 + lean} ${a[1] + lift} ${b[0] + 2 + lean} ${b[1] + lift} ${pt(b)}`;\n}\n\nfunction backHandle(f: number) {\n  const y = lerp(106, 74, f);\n  const lift = lerp(-24, -66, f);\n  const lean = lerp(24, 0, f);\n  return `M182 ${y} C${180 + lean} ${y + lift} ${220 + lean} ${y + lift} 218 ${y}`;\n}\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport function EmptyBag({\n  count: countProp,\n  onAdd,\n  title = \"Your bag is empty\",\n  description = \"Nothing in here yet. Find something you love and it'll wait for you.\",\n  filledTitle,\n  filledDescription = \"Held for 48 hours. Shipping is free over $50 and returns are on us.\",\n  actionLabel = \"Add to bag\",\n  price = \"$48\",\n  hue = \"violet\",\n  className,\n}: EmptyBagProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const [own, setOwn] = useState(0);\n  const count = countProp ?? own;\n  const full = count > 0;\n  const [peek, setPeek] = useState(false);\n  const first = useRef(true);\n\n  const fill = useMotionValue(full ? 1 : 0);\n  const breath = useMotionValue(0);\n  const f = useTransform(() => fill.get() + breath.get());\n  const front = useTransform(f, frontPath);\n  const rim = useTransform(f, rimPath);\n  const back = useTransform(f, backPath);\n  const handleFront = useTransform(f, frontHandle);\n  const handleBack = useTransform(f, backHandle);\n  const grommetA = useTransform(f, (v) => anchors(v).a);\n  const grommetB = useTransform(f, (v) => anchors(v).b);\n  const ax = useTransform(grommetA, (p) => p[0]);\n  const ay = useTransform(grommetA, (p) => p[1]);\n  const bx = useTransform(grommetB, (p) => p[0]);\n  const by = useTransform(grommetB, (p) => p[1]);\n  const shadow = useTransform(f, (v) => lerp(58, 74, Math.min(1, Math.max(0, v))));\n\n  useEffect(() => {\n    if (first.current || reduce) {\n      fill.set(full ? 1 : 0);\n      first.current = false;\n      return;\n    }\n    const c = full\n      ? animate(fill, 1, { type: \"spring\", stiffness: 360, damping: 14, mass: 0.8, delay: 0.3 })\n      : animate(fill, 0, { type: \"spring\", stiffness: 90, damping: 18 });\n    return () => c.stop();\n  }, [full, reduce, fill]);\n\n  const sighing = !full && !peek && inView && !reduce;\n  useEffect(() => {\n    if (!sighing) {\n      const c = animate(breath, 0, { duration: 0.3, ease: \"easeOut\" });\n      return () => c.stop();\n    }\n    const c = animate(breath, [0, 0.14, -0.05, 0], {\n      delay: 1.2,\n      duration: 2.4,\n      ease: \"easeInOut\",\n      repeat: Number.POSITIVE_INFINITY,\n      repeatDelay: 2.6,\n      times: [0, 0.4, 0.75, 1],\n    });\n    return () => c.stop();\n  }, [sighing, breath]);\n\n  const add = () => {\n    if (countProp === undefined) setOwn((n) => n + 1);\n    setPeek(false);\n    onAdd?.();\n  };\n\n  const hover = (on: boolean) => setPeek(on && !full);\n  const looking = peek && !full;\n  const heading = filledTitle ?? `${count} ${count === 1 ? \"item\" : \"items\"} in your bag`;\n\n  return (\n    <div\n      ref={root}\n      className={cn(\"relative w-full max-w-[420px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\", hues[hue], className)}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n\n      <div\n        aria-label={full ? `Shopping bag with ${count} ${count === 1 ? \"item\" : \"items\"}` : \"An empty shopping bag, sagging\"}\n        className=\"relative mx-auto aspect-[400/175] w-full max-w-[380px]\"\n        onPointerEnter={(e) => e.pointerType === \"mouse\" && hover(true)}\n        onPointerLeave={() => hover(false)}\n        role=\"img\"\n      >\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 ${TOP} ${W} ${H}`}>\n          <defs>\n            <linearGradient id={`${uid}-bag`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0.3\">\n              <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n              <stop offset=\"0.35\" stopColor=\"var(--hue)\" />\n              <stop offset=\"1\" stopColor=\"var(--hue-deep)\" />\n            </linearGradient>\n          </defs>\n\n          <motion.ellipse className=\"fill-foreground/8\" cx={200} cy={FLOOR + 2} rx={shadow} ry={5} />\n\n          <motion.path className=\"fill-none stroke-(--hue-deep)\" d={handleBack} strokeLinecap=\"round\" strokeOpacity={0.8} strokeWidth={5} />\n          <motion.path className=\"fill-(--hue-deep)\" d={back} />\n\n          <motion.g\n            animate={looking ? { opacity: 1, y: 0, rotate: [0, -7, 5, -2, 0] } : { opacity: 0, y: -90, rotate: 0 }}\n            initial={false}\n            style={{ originX: `${TAG_X}px`, originY: \"-60px\", transformBox: \"view-box\" }}\n            transition={\n              reduce\n                ? { duration: 0.15 }\n                : looking\n                  ? { y: { type: \"spring\", stiffness: 220, damping: 20 }, opacity: { duration: 0.15 }, rotate: { duration: 1.1, ease: \"easeOut\" } }\n                  : { duration: 0.22, ease: [0.23, 1, 0.32, 1] }\n            }\n          >\n            <path className=\"stroke-foreground/30\" d={`M${TAG_X} -60 V64`} strokeWidth={1.2} />\n            <g transform={`translate(${TAG_X} 64) rotate(8)`}>\n              <path className=\"fill-card stroke-foreground/20\" d=\"M-12 6 L0 -6 L12 6 V38 Q12 42 8 42 H-8 Q-12 42 -12 38 Z\" strokeWidth={1.2} />\n              <circle className=\"fill-none stroke-foreground/30\" r={2.4} strokeWidth={1.2} />\n              <text className=\"fill-foreground font-mono font-semibold\" fontSize={8.5} textAnchor=\"middle\" x={0} y={24}>\n                {price}\n              </text>\n            </g>\n          </motion.g>\n\n          <motion.g\n            animate={full ? { opacity: 1, y: 0 } : { opacity: 0, y: reduce ? 0 : -160 }}\n            initial={false}\n            transition={\n              reduce\n                ? { duration: 0.2 }\n                : full\n                  ? { y: { duration: 0.34, ease: [0.55, 0, 1, 0.45] }, opacity: { duration: 0.1 } }\n                  : { duration: 0.2, ease: \"easeOut\" }\n            }\n          >\n            <path className=\"fill-amber-300\" d=\"M173 54 H187 Q200 68 213 54 H227 Q235 54 235 62 V124 H165 V62 Q165 54 173 54 Z\" />\n            <path className=\"fill-amber-400\" d=\"M187 54 Q200 68 213 54 Q200 61 187 54 Z\" />\n            <g className=\"fill-white/45\">\n              <rect height={5} rx={2.5} width={70} x={165} y={78} />\n              <rect height={5} rx={2.5} width={70} x={165} y={90} />\n            </g>\n            <g className=\"stroke-amber-700/25\" strokeLinecap=\"round\" strokeWidth={1.5}>\n              <path d=\"M180 64 V120\" />\n              <path d=\"M220 64 V120\" />\n            </g>\n          </motion.g>\n\n          <motion.path d={front} fill={`url(#${uid}-bag)`} />\n          <motion.path className=\"fill-white/15\" d={rim} />\n          <g className=\"stroke-black/8\" strokeLinecap=\"round\" strokeWidth={1.5}>\n            <path d={`M158 118 L150 ${FLOOR - 4}`} />\n            <path d={`M242 118 L250 ${FLOOR - 4}`} />\n          </g>\n\n          <motion.path className=\"fill-none stroke-(--hue-deep)\" d={handleFront} strokeLinecap=\"round\" strokeWidth={5} />\n          <motion.circle className=\"fill-white/70\" cx={ax} cy={ay} r={3} />\n          <motion.circle className=\"fill-white/70\" cx={bx} cy={by} r={3} />\n\n          <g className=\"fill-none stroke-white\" strokeLinecap=\"round\" strokeWidth={3.5}>\n            <motion.g animate={{ opacity: full ? 0 : 1 }} initial={false} transition={{ duration: 0.15 }}>\n              <motion.g\n                animate={looking ? { x: 3, y: -3 } : { x: 0, y: 0 }}\n                initial={false}\n                transition={{ type: \"spring\", stiffness: 300, damping: 24 }}\n              >\n                <circle className=\"fill-white stroke-none\" cx={182} cy={150} r={4.5} />\n                <circle className=\"fill-white stroke-none\" cx={218} cy={150} r={4.5} />\n              </motion.g>\n              <motion.g animate={{ y: looking ? -4 : 0 }} initial={false} strokeWidth={3} transition={{ duration: 0.2, ease: \"easeOut\" }}>\n                <path d=\"M172 140 L188 135\" />\n                <path d=\"M212 135 L228 140\" />\n              </motion.g>\n              <motion.path animate={{ opacity: looking ? 0 : 1 }} d=\"M193 168 q7 -5 14 0\" initial={false} transition={{ duration: 0.15 }} />\n              <motion.circle animate={{ opacity: looking ? 1 : 0 }} cx={200} cy={167} initial={false} r={3} strokeWidth={2.5} transition={{ duration: 0.15 }} />\n            </motion.g>\n            <motion.g animate={{ opacity: full ? 1 : 0 }} initial={false} transition={{ duration: 0.15, delay: full ? 0.3 : 0 }}>\n              <path d=\"M174 152 q8 -9 16 0\" />\n              <path d=\"M210 152 q8 -9 16 0\" />\n              <path className=\"fill-white\" d=\"M190 162 q10 12 20 0 Z\" strokeLinejoin=\"round\" />\n            </motion.g>\n          </g>\n\n          {sighing && (\n            <motion.g\n              animate={{ opacity: [0, 1, 0], x: [0, -6, -12], y: [0, -10, -20] }}\n              className=\"fill-none stroke-foreground/30\"\n              strokeLinecap=\"round\"\n              strokeWidth={2.5}\n              transition={{ delay: 2.1, duration: 1.3, ease: \"easeOut\", repeat: Number.POSITIVE_INFINITY, repeatDelay: 3.7 }}\n            >\n              <path d=\"M160 82 q-5 -6 0 -12\" />\n              <path d=\"M150 88 q-6 -8 0 -16\" />\n            </motion.g>\n          )}\n\n          <motion.g\n            animate={full ? { opacity: 1, scale: 1 } : { opacity: 0, scale: reduce ? 1 : 0.6 }}\n            initial={false}\n            style={{ originX: \"262px\", originY: \"80px\", transformBox: \"view-box\" }}\n            transition={reduce ? { duration: 0.15 } : full ? { type: \"spring\", stiffness: 500, damping: 22, delay: 0.42 } : { duration: 0.15 }}\n          >\n            <circle className=\"fill-foreground stroke-card\" cx={262} cy={80} r={12} strokeWidth={3} />\n            <text\n              className=\"fill-background font-semibold tabular-nums\"\n              dominantBaseline=\"central\"\n              fontSize={11}\n              textAnchor=\"middle\"\n              x={262}\n              y={80.5}\n            >\n              {Math.min(count, 99)}\n            </text>\n          </motion.g>\n        </svg>\n      </div>\n\n      <div className=\"relative mt-3 grid text-center [&>*]:col-start-1 [&>*]:row-start-1\">\n        {[\n          { key: \"empty\", show: !full, t: title, d: description },\n          { key: \"full\", show: full, t: heading, d: filledDescription },\n        ].map((copy) => (\n          <motion.div\n            animate={{ opacity: copy.show ? 1 : 0, y: copy.show || reduce ? 0 : copy.key === \"full\" ? 4 : -4 }}\n            aria-hidden={!copy.show}\n            initial={false}\n            key={copy.key}\n            transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <p aria-live={copy.key === \"full\" ? \"polite\" : undefined} className=\"text-balance font-medium text-foreground text-sm tabular-nums\">\n              {copy.t}\n            </p>\n            <p className=\"mx-auto mt-1 max-w-[34ch] text-pretty text-muted-foreground text-xs\">{copy.d}</p>\n          </motion.div>\n        ))}\n      </div>\n\n      <div className=\"relative mt-4 flex justify-center\">\n        <Button\n          className=\"transition-[box-shadow,transform] duration-150 ease-out active:scale-[0.97]\"\n          onBlur={() => hover(false)}\n          onClick={add}\n          onFocus={(e) => e.currentTarget.matches(\":focus-visible\") && hover(true)}\n          onPointerEnter={(e) => e.pointerType === \"mouse\" && hover(true)}\n          onPointerLeave={() => hover(false)}\n          size=\"sm\"\n          variant=\"outline\"\n        >\n          <PlusIcon aria-hidden />\n          {actionLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}