{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gift-box",
  "type": "registry:component",
  "title": "Gift Box",
  "description": "A referral reward that wobbles with anticipation: the bow unties, the lid pops off and lands beside it, and a credit coupon rises out in a warm glow with a few sparkles.",
  "categories": [
    "illustration",
    "Celebrations"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/gift-box.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon, GiftIcon } from \"lucide-react\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type GiftBoxHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<GiftBoxHue, 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 GiftBoxProps {\n  amount?: string;\n  code?: string;\n  label?: string;\n  title?: string;\n  description?: string;\n  openTitle?: string;\n  openDescription?: string;\n  hue?: GiftBoxHue;\n  autoPlay?: boolean;\n  className?: string;\n}\n\nconst W = 400;\nconst VY = 34;\nconst H = 192;\nconst CX = 200;\nconst BODY = { x: 144, y: 132, w: 112, h: 82 };\nconst LID = { x: 136, y: 112, w: 128, h: 24 };\nconst COUPON = { w: 152, h: 84, rest: 136, up: 44, tucked: 0.68 };\n\nconst sparkles = [\n  { x: 108, y: 66, s: 1, d: 0 },\n  { x: 296, y: 58, s: 0.8, d: 0.12 },\n  { x: 94, y: 128, s: 0.65, d: 0.24 },\n  { x: 312, y: 120, s: 0.9, d: 0.06 },\n  { x: 150, y: 44, s: 0.55, d: 0.3 },\n  { x: 258, y: 40, s: 0.6, d: 0.18 },\n];\n\nconst couponPath = (w: number, h: number, notch: number) =>\n  `M9 0 H${notch - 5} A5 5 0 0 0 ${notch + 5} 0 H${w - 9} Q${w} 0 ${w} 9 V${h - 9} Q${w} ${h} ${w - 9} ${h} H${notch + 5} A5 5 0 0 0 ${notch - 5} ${h} H9 Q0 ${h} 0 ${h - 9} V9 Q0 0 9 0 Z`;\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 GiftBox({\n  amount = \"$20\",\n  code = \"MIRA-20\",\n  label = \"Referral credit\",\n  title = \"You've got a reward\",\n  description = \"Mira joined with your invite link. Open it up to see what you earned.\",\n  openTitle = \"$20 credit unlocked\",\n  openDescription = \"It comes off your next invoice. Share the code to give a friend the same.\",\n  hue = \"rose\",\n  autoPlay = true,\n  className,\n}: GiftBoxProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const [open, setOpen] = useState(false);\n  const [touched, setTouched] = useState(false);\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    if (!autoPlay || touched || !inView || reduce) return;\n    const t = setTimeout(() => setOpen((o) => !o), open ? 4200 : 2600);\n    return () => clearTimeout(t);\n  }, [autoPlay, touched, inView, open, reduce]);\n\n  useEffect(() => {\n    if (!copied) return;\n    const t = setTimeout(() => setCopied(false), 1600);\n    return () => clearTimeout(t);\n  }, [copied]);\n\n  const toggle = () => {\n    setTouched(true);\n    setOpen((o) => !o);\n  };\n\n  const action = async () => {\n    setTouched(true);\n    if (!open) {\n      setOpen(true);\n      return;\n    }\n    try {\n      await navigator.clipboard?.writeText(code);\n    } catch {}\n    setCopied(true);\n  };\n\n  const wobbling = !open && inView && !reduce;\n  const instant = { duration: reduce ? 0 : 0.2 };\n  const notch = 108;\n\n  return (\n    <div\n      ref={root}\n      className={cn(\"relative w-full max-w-[440px] 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      <button\n        aria-label={open ? `Close the gift. ${amount} ${label}, code ${code}` : `Open your gift: ${title}`}\n        className=\"group/gift relative mx-auto block aspect-[400/192] w-full max-w-[400px] cursor-pointer rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        onClick={toggle}\n        type=\"button\"\n      >\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 ${VY} ${W} ${H}`}>\n          <defs>\n            <linearGradient id={`${uid}-box`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0.4\">\n              <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n              <stop offset=\"0.4\" stopColor=\"var(--hue)\" />\n              <stop offset=\"1\" stopColor=\"var(--hue-deep)\" />\n            </linearGradient>\n            <radialGradient id={`${uid}-glow`}>\n              <stop className=\"[stop-color:var(--color-amber-300)]\" offset=\"0\" stopOpacity={0.6} />\n              <stop className=\"[stop-color:var(--color-amber-300)]\" offset=\"1\" stopOpacity={0} />\n            </radialGradient>\n            <clipPath id={`${uid}-inside`}>\n              <rect height={BODY.y - VY + 40} width={W} x={0} y={VY - 40} />\n              <rect height={BODY.h} width={BODY.w} x={BODY.x} y={BODY.y} />\n            </clipPath>\n          </defs>\n\n          <g className=\"dark:opacity-55\">\n            <motion.circle\n              animate={{ opacity: open ? 1 : 0, scale: open || reduce ? 1 : 0.8 }}\n              cx={CX}\n              cy={COUPON.up + COUPON.h / 2}\n              fill={`url(#${uid}-glow)`}\n              initial={false}\n              r={110}\n              style={{ originX: \"50%\", originY: \"50%\" }}\n              transition={reduce ? instant : { duration: 0.6, ease: [0.23, 1, 0.32, 1], delay: open ? 0.75 : 0 }}\n            />\n          </g>\n\n          <ellipse className=\"fill-foreground/8\" cx={CX} cy={BODY.y + BODY.h + 3} rx={70} ry={5} />\n\n          <motion.g\n            animate={wobbling ? { rotate: [0, -4, 4, -3, 2, 0], y: [0, -5, 0, -2, 0, 0] } : { rotate: 0, y: 0 }}\n            style={{ originX: `${CX}px`, originY: `${BODY.y + BODY.h}px`, transformBox: \"view-box\" }}\n            transition={\n              wobbling\n                ? { duration: 0.7, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY, repeatDelay: 2.1, delay: 0.8 }\n                : { duration: 0.2 }\n            }\n          >\n            <rect className=\"fill-(--hue-deep)\" height={16} rx={3} width={BODY.w - 4} x={BODY.x + 2} y={BODY.y - 8} />\n            <rect className=\"fill-black/25\" height={16} rx={3} width={BODY.w - 4} x={BODY.x + 2} y={BODY.y - 8} />\n\n            <g clipPath={`url(#${uid}-inside)`}>\n              <motion.g\n                animate={{ scale: open ? 1 : COUPON.tucked, y: open ? COUPON.up - COUPON.rest : 0 }}\n                initial={false}\n                style={{ originX: `${CX}px`, originY: `${COUPON.rest + COUPON.h}px`, transformBox: \"view-box\" }}\n                transition={\n                  reduce\n                    ? instant\n                    : open\n                      ? { type: \"spring\", stiffness: 170, damping: 17, delay: 0.72 }\n                      : { duration: 0.3, ease: [0.65, 0, 0.35, 1] }\n                }\n              >\n                <g transform={`translate(${CX - COUPON.w / 2} ${COUPON.rest})`}>\n                  <path className=\"fill-black/6\" d={couponPath(COUPON.w, COUPON.h, notch)} transform=\"translate(0 5)\" />\n                  <path className=\"fill-black/8\" d={couponPath(COUPON.w, COUPON.h, notch)} transform=\"translate(0 1.5)\" />\n                  <path className=\"fill-card stroke-foreground/12\" d={couponPath(COUPON.w, COUPON.h, notch)} strokeWidth={1} />\n                  <path className=\"stroke-foreground/15\" d={`M${notch} 8 V${COUPON.h - 8}`} strokeDasharray=\"3 3\" strokeWidth={1.2} />\n                  <text className=\"fill-foreground font-semibold tabular-nums\" fontSize={26} x={14} y={36}>\n                    {amount}\n                  </text>\n                  <text className=\"fill-muted-foreground font-medium\" fontSize={9.5} x={14} y={52}>\n                    {label}\n                  </text>\n                  <rect className=\"fill-(--hue)/15\" height={15} rx={4} width={Math.max(52, code.length * 6 + 12)} x={14} y={60} />\n                  <text className=\"fill-(--hue-deep) font-medium font-mono dark:fill-(--hue-hi)\" dominantBaseline=\"central\" fontSize={8.5} x={20} y={67.8}>\n                    {code}\n                  </text>\n                  <GiftIcon className=\"text-(--hue)\" height={18} width={18} x={notch + (COUPON.w - notch) / 2 - 9} y={COUPON.h / 2 - 9} />\n                </g>\n              </motion.g>\n            </g>\n\n            <rect fill={`url(#${uid}-box)`} height={BODY.h} rx={8} width={BODY.w} x={BODY.x} y={BODY.y} />\n            <rect className=\"fill-black/10\" height={6} width={BODY.w} x={BODY.x} y={BODY.y} />\n            <rect className=\"fill-amber-300\" height={BODY.h} width={18} x={CX - 9} y={BODY.y} />\n            <rect className=\"fill-amber-500/40\" height={BODY.h} width={4} x={CX + 5} y={BODY.y} />\n\n            <motion.g\n              animate={\n                open\n                  ? { x: [0, 26, 122, 122], y: [0, -66, 86, 81], rotate: [0, 14, 9, 8] }\n                  : { x: 0, y: 0, rotate: 0 }\n              }\n              initial={false}\n              style={{ originX: `${CX}px`, originY: `${LID.y + LID.h / 2}px`, transformBox: \"view-box\" }}\n              transition={\n                reduce\n                  ? instant\n                  : open\n                    ? { duration: 0.9, times: [0, 0.32, 0.8, 1], ease: [\"easeOut\", \"easeIn\", \"easeOut\"], delay: 0.28 }\n                    : { type: \"spring\", stiffness: 200, damping: 24 }\n              }\n            >\n              <rect fill={`url(#${uid}-box)`} height={LID.h} rx={7} width={LID.w} x={LID.x} y={LID.y} />\n              <rect className=\"fill-white/20\" height={4} rx={2} width={LID.w - 28} x={LID.x + 10} y={LID.y + 4} />\n              <rect className=\"fill-amber-300\" height={LID.h} width={18} x={CX - 9} y={LID.y} />\n              <rect className=\"fill-amber-500/40\" height={LID.h} width={4} x={CX + 5} y={LID.y} />\n            </motion.g>\n\n            <motion.g\n              animate={open ? { opacity: 0, y: reduce ? 0 : 12 } : { opacity: 1, y: 0 }}\n              className=\"fill-amber-300\"\n              initial={false}\n              transition={reduce ? instant : { duration: 0.24, ease: open ? \"easeIn\" : \"easeOut\", delay: open ? 0.08 : 0.5 }}\n            >\n              <path d={`M${CX - 4} ${LID.y + 2} L${CX - 16} ${LID.y + 24} L${CX - 10} ${LID.y + 22} L${CX - 7} ${LID.y + 27} L${CX + 3} ${LID.y + 4} Z`} />\n              <path d={`M${CX + 4} ${LID.y + 2} L${CX + 16} ${LID.y + 24} L${CX + 10} ${LID.y + 22} L${CX + 7} ${LID.y + 27} L${CX - 3} ${LID.y + 4} Z`} />\n            </motion.g>\n\n            {[-1, 1].map((side) => (\n              <motion.g\n                animate={\n                  open\n                    ? { opacity: 0, rotate: reduce ? 0 : side * 50, scale: reduce ? 1 : 0.9, x: reduce ? 0 : side * 14, y: reduce ? 0 : -6 }\n                    : { opacity: 1, rotate: 0, scale: 1, x: 0, y: 0 }\n                }\n                initial={false}\n                key={side}\n                style={{ originX: `${CX}px`, originY: `${LID.y - 1}px`, transformBox: \"view-box\" }}\n                transition={reduce ? instant : open ? { duration: 0.32, ease: [0.23, 1, 0.32, 1] } : { type: \"spring\", stiffness: 380, damping: 22, delay: 0.5 }}\n              >\n                <path\n                  className=\"fill-amber-300\"\n                  d={`M${CX} ${LID.y - 1} C${CX + side * 12} ${LID.y - 20} ${CX + side * 38} ${LID.y - 18} ${CX + side * 32} ${LID.y - 5} C${CX + side * 28} ${LID.y + 4} ${CX + side * 10} ${LID.y + 2} ${CX} ${LID.y - 1} Z`}\n                />\n                <path\n                  className=\"fill-amber-500/45\"\n                  d={`M${CX + side * 6} ${LID.y - 3} C${CX + side * 14} ${LID.y - 13} ${CX + side * 28} ${LID.y - 12} ${CX + side * 26} ${LID.y - 6} C${CX + side * 22} ${LID.y - 2} ${CX + side * 12} ${LID.y - 2} ${CX + side * 6} ${LID.y - 3} Z`}\n                />\n              </motion.g>\n            ))}\n            <motion.rect\n              animate={{ opacity: open ? 0 : 1, scale: open && !reduce ? 0.8 : 1 }}\n              className=\"fill-amber-400\"\n              height={12}\n              initial={false}\n              rx={4.5}\n              style={{ originX: \"50%\", originY: \"50%\" }}\n              transition={reduce ? instant : { duration: 0.2, delay: open ? 0.1 : 0.55 }}\n              width={14}\n              x={CX - 7}\n              y={LID.y - 7}\n            />\n          </motion.g>\n\n          {sparkles.map((sp) => (\n            <g key={`${sp.x}-${sp.y}`} transform={`translate(${sp.x} ${sp.y}) scale(${sp.s})`}>\n              <motion.path\n                animate={\n                  open && !reduce\n                    ? { opacity: [0, 1, 0.35, 1, 0], scale: [0.4, 1, 0.7, 0.9, 0.4] }\n                    : { opacity: open ? 1 : 0, scale: open ? 0.8 : 0.4 }\n                }\n                className=\"fill-amber-400\"\n                d=\"M0 -8 C0.8 -2 2 -0.8 8 0 C2 0.8 0.8 2 0 8 C-0.8 2 -2 0.8 -8 0 C-2 -0.8 -0.8 -2 0 -8 Z\"\n                initial={false}\n                style={{ originX: \"0px\", originY: \"0px\" }}\n                transition={\n                  open && !reduce\n                    ? { duration: 1.6, delay: 1 + sp.d, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY, repeatDelay: 0.8 + sp.d * 4 }\n                    : { duration: 0.2 }\n                }\n              />\n            </g>\n          ))}\n        </svg>\n      </button>\n\n      <div className=\"relative mt-3 grid text-center [&>*]:col-start-1 [&>*]:row-start-1\">\n        {[\n          { key: \"closed\", show: !open, t: title, d: description },\n          { key: \"open\", show: open, t: openTitle, d: openDescription },\n        ].map((copy) => (\n          <motion.div\n            animate={{ opacity: copy.show ? 1 : 0, y: copy.show || reduce ? 0 : copy.key === \"open\" ? 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], delay: open && !reduce ? 0.6 : 0 }}\n          >\n            <p aria-live={copy.key === \"open\" ? \"polite\" : undefined} className=\"text-balance font-medium text-foreground text-sm\">\n              {copy.t}\n            </p>\n            <p className=\"mx-auto mt-1 max-w-[36ch] 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=\"min-w-32 transition-[box-shadow,transform] duration-150 ease-out active:scale-[0.97]\"\n          onClick={action}\n          size=\"sm\"\n          variant={open ? \"outline\" : \"default\"}\n        >\n          {!open ? <GiftIcon aria-hidden /> : copied ? <CheckIcon aria-hidden className=\"text-success-foreground\" /> : <CopyIcon aria-hidden />}\n          {!open ? \"Open gift\" : copied ? \"Copied\" : `Copy ${code}`}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}