{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unplugged",
  "type": "registry:component",
  "title": "Unplugged",
  "description": "Offline empty-state art: a loose plug sways beside its socket and sparks now and then, until Reconnect pushes it home with a click and the status light turns green.",
  "categories": [
    "illustration",
    "Empty States"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/unplugged.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, Loader2Icon, RefreshCwIcon } 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 UnpluggedHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\nexport type UnpluggedStatus = \"offline\" | \"connecting\" | \"online\";\n\nconst hues: Record<UnpluggedHue, 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 UnpluggedProps {\n  status?: UnpluggedStatus;\n  onReconnect?: () => void;\n  title?: string;\n  description?: string;\n  onlineTitle?: string;\n  onlineDescription?: string;\n  actionLabel?: string;\n  hue?: UnpluggedHue;\n  className?: string;\n}\n\nconst W = 400;\nconst H = 170;\nconst CY = 80;\nconst FACE = 250;\nconst REACH = 72;\n\nconst poses = {\n  offline: { x: 130, y: CY + 8, r: 9 },\n  connecting: { x: FACE - REACH - 14, y: CY + 2, r: 2 },\n  online: { x: FACE - REACH, y: CY, r: 0 },\n};\n\nconst pill: Record<UnpluggedStatus, { label: string; dot: string }> = {\n  offline: { label: \"Offline\", dot: \"bg-destructive\" },\n  connecting: { label: \"Reconnecting\", dot: \"bg-warning\" },\n  online: { label: \"Connected\", dot: \"bg-success\" },\n};\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction Plug({ gradient }: { gradient: string }) {\n  return (\n    <g>\n      <g className=\"fill-neutral-300 stroke-black/20 dark:fill-neutral-400\" strokeWidth={1}>\n        <rect height={6} rx={2.5} width={24} x={REACH - 6} y={-13} />\n        <rect height={6} rx={2.5} width={24} x={REACH - 6} y={7} />\n      </g>\n      <g className=\"fill-white/60\">\n        <rect height={1.5} rx={0.75} width={16} x={REACH} y={-12} />\n        <rect height={1.5} rx={0.75} width={16} x={REACH} y={8} />\n      </g>\n      <path className=\"fill-(--hue-deep)\" d={`M2 -7 L24 -15 V15 L2 7 Z`} />\n      <g className=\"stroke-black/15\" strokeLinecap=\"round\" strokeWidth={1.5}>\n        <path d=\"M10 -8 V8\" />\n        <path d=\"M16 -10 V10\" />\n      </g>\n      <rect fill={`url(#${gradient})`} height={42} rx={11} width={52} x={22} y={-21} />\n      <rect className=\"fill-black/12\" height={34} rx={2.5} width={6} x={REACH - 6} y={-17} />\n      <rect className=\"fill-white/30\" height={4} rx={2} width={30} x={30} y={-16} />\n      <g className=\"stroke-white/30\" strokeLinecap=\"round\" strokeWidth={2}>\n        <path d=\"M36 -7 V9\" />\n        <path d=\"M43 -7 V9\" />\n        <path d=\"M50 -7 V9\" />\n      </g>\n    </g>\n  );\n}\n\nexport function Unplugged({\n  status: statusProp,\n  onReconnect,\n  title = \"You're offline\",\n  description = \"Your edits are saved on this device and will sync once you're back.\",\n  onlineTitle = \"You're back online\",\n  onlineDescription = \"All 12 changes synced. Pick up right where you left off.\",\n  actionLabel = \"Reconnect\",\n  hue = \"sky\",\n  className,\n}: UnpluggedProps) {\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<UnpluggedStatus>(\"offline\");\n  const status = statusProp ?? own;\n  const [clicks, setClicks] = useState(0);\n  const settled = useRef(false);\n\n  useEffect(() => {\n    if (statusProp !== undefined || own !== \"connecting\") return;\n    const t = setTimeout(() => setOwn(\"online\"), 1100);\n    return () => clearTimeout(t);\n  }, [statusProp, own]);\n\n  const px = useMotionValue(poses.offline.x);\n  const py = useMotionValue(poses.offline.y);\n  const rot = useMotionValue(poses.offline.r);\n  const sway = useMotionValue(0);\n  const angle = useTransform(() => rot.get() + sway.get());\n  const cable = useTransform(() => {\n    const a = (angle.get() * Math.PI) / 180;\n    const x = px.get();\n    const y = py.get();\n    return `M-80 ${CY + 74} C 20 ${CY + 76} ${x - 50 * Math.cos(a)} ${y - 50 * Math.sin(a)} ${x} ${y}`;\n  });\n\n  useEffect(() => {\n    const pose = poses[status];\n    if (reduce || !settled.current) {\n      px.set(pose.x);\n      py.set(pose.y);\n      rot.set(pose.r);\n      settled.current = true;\n      return;\n    }\n    const spring =\n      status === \"online\"\n        ? { type: \"spring\" as const, stiffness: 520, damping: 34 }\n        : status === \"connecting\"\n          ? { type: \"spring\" as const, stiffness: 160, damping: 22 }\n          : { type: \"spring\" as const, stiffness: 140, damping: 11, mass: 0.9 };\n    const runs = [animate(px, pose.x, spring), animate(py, pose.y, spring), animate(rot, pose.r, spring)];\n    if (status === \"online\") setClicks((c) => c + 1);\n    return () => runs.forEach((r) => r.stop());\n  }, [status, reduce, px, py, rot]);\n\n  useEffect(() => {\n    if (reduce || status !== \"offline\" || !inView) {\n      const c = animate(sway, 0, { duration: 0.3, ease: \"easeOut\" });\n      return () => c.stop();\n    }\n    const c = animate(sway, [0, -4, 0], { delay: 0.9, duration: 3.4, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY });\n    return () => c.stop();\n  }, [status, reduce, inView, sway]);\n\n  const reconnect = () => {\n    if (status !== \"offline\") return;\n    if (statusProp === undefined) setOwn(\"connecting\");\n    onReconnect?.();\n  };\n\n  const sparking = status === \"offline\" && inView && !reduce;\n  const online = status === \"online\";\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[440px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        \"[--cable:var(--color-neutral-700)] dark:[--cable:var(--color-neutral-500)]\",\n        hues[hue],\n        className,\n      )}\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 className=\"relative mx-auto mb-2 flex w-fit items-center gap-1.5 rounded-full border bg-popover py-1 pr-2.5 pl-2 text-xs shadow-xs/5\">\n        <span className=\"relative flex size-2\">\n          {status === \"connecting\" && !reduce && (\n            <span className=\"absolute inset-0 animate-ping rounded-full bg-warning opacity-60\" />\n          )}\n          <span className={cn(\"relative size-2 rounded-full transition-colors duration-300\", pill[status].dot)} />\n        </span>\n        <span aria-live=\"polite\" className=\"font-medium text-foreground\">\n          {pill[status].label}\n        </span>\n      </div>\n\n      <div\n        aria-label={online ? \"Cable plugged in, connected\" : \"Cable unplugged from its socket\"}\n        className=\"relative mx-auto aspect-[400/170] w-full max-w-[400px]\"\n        role=\"img\"\n      >\n        <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 0 ${W} ${H}`}>\n          <defs>\n            <linearGradient id={`${uid}-plug`} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n              <stop offset=\"0.5\" 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-emerald-400)]\" offset=\"0\" stopOpacity={0.7} />\n              <stop className=\"[stop-color:var(--color-emerald-400)]\" offset=\"1\" stopOpacity={0} />\n            </radialGradient>\n          </defs>\n\n          <path\n            className=\"fill-none stroke-(--cable)\"\n            d={`M${FACE + 74} ${CY} C ${FACE + 104} ${CY} ${FACE + 108} ${CY + 60} ${FACE + 220} ${CY + 66}`}\n            strokeLinecap=\"round\"\n            strokeWidth={9}\n          />\n          <motion.path className=\"fill-none stroke-(--cable)\" d={cable} strokeLinecap=\"round\" strokeWidth={9} />\n\n          <motion.g style={{ originX: \"0px\", originY: \"0px\", rotate: angle, transformBox: \"view-box\", x: px, y: py }}>\n            <Plug gradient={`${uid}-plug`} />\n          </motion.g>\n\n          <motion.g\n            animate={clicks > 0 && !reduce ? { x: [0, 3, 0] } : { x: 0 }}\n            key={`socket-${clicks}`}\n            transition={{ duration: 0.22, ease: \"easeOut\", delay: 0.08 }}\n          >\n            <path className=\"fill-(--hue-deep)\" d={`M${FACE + 52} ${CY - 15} L${FACE + 74} ${CY - 6} V${CY + 6} L${FACE + 52} ${CY + 15} Z`} />\n            <g className=\"stroke-black/15\" strokeLinecap=\"round\" strokeWidth={1.5}>\n              <path d={`M${FACE + 60} ${CY - 9} V${CY + 9}`} />\n              <path d={`M${FACE + 66} ${CY - 7} V${CY + 7}`} />\n            </g>\n            <rect className=\"fill-(--hue-deep)\" height={48} rx={12} width={58} x={FACE - 2} y={CY - 24} />\n            <rect className=\"fill-white/15\" height={4} rx={2} width={34} x={FACE + 10} y={CY - 19} />\n            <rect className=\"fill-black/20\" height={40} rx={3} width={7} x={FACE - 3} y={CY - 20} />\n            <g className=\"fill-black/55\">\n              <rect height={8} rx={2} width={4} x={FACE - 2} y={CY - 14} />\n              <rect height={8} rx={2} width={4} x={FACE - 2} y={CY + 6} />\n            </g>\n            <rect className=\"fill-black/30\" height={10} rx={5} width={22} x={FACE + 18} y={CY - 5} />\n            <motion.circle\n              animate={{ opacity: online ? 1 : 0, scale: online ? 1 : 0.6 }}\n              cx={FACE + 29}\n              cy={CY}\n              fill={`url(#${uid}-glow)`}\n              initial={false}\n              r={16}\n              style={{ originX: \"50%\", originY: \"50%\" }}\n              transition={{ duration: 0.35, ease: [0.23, 1, 0.32, 1] }}\n            />\n            <circle\n              className={cn(\n                \"transition-[fill] duration-300\",\n                status === \"online\" ? \"fill-emerald-400\" : status === \"connecting\" ? \"fill-amber-400\" : \"fill-red-400\",\n              )}\n              cx={FACE + 29}\n              cy={CY}\n              r={3}\n            />\n          </motion.g>\n\n          {clicks > 0 && !reduce && (\n            <motion.g\n              animate={{ opacity: [0, 1, 0] }}\n              className=\"fill-none stroke-(--hue)\"\n              key={`click-${clicks}`}\n              strokeLinecap=\"round\"\n              strokeWidth={2.5}\n              transition={{ duration: 0.5, ease: \"easeOut\", delay: 0.06 }}\n            >\n              {[-1, 1].map((side) => (\n                <motion.g\n                  animate={{ y: [0, side * 5] }}\n                  key={side}\n                  transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1], delay: 0.06 }}\n                >\n                  <path d={`M${FACE - 4} ${CY + side * 30} v${side * 7}`} />\n                  <path d={`M${FACE - 14} ${CY + side * 27} l${-4} ${side * 6}`} />\n                  <path d={`M${FACE + 6} ${CY + side * 27} l${4} ${side * 6}`} />\n                </motion.g>\n              ))}\n            </motion.g>\n          )}\n\n          {sparking && (\n            <g transform={`translate(${FACE - 16} ${CY + 6})`}>\n              <motion.g\n                animate={{ opacity: [0, 1, 0.2, 1, 0], scale: [0.5, 1.1, 0.8, 1, 0.6] }}\n                style={{ originX: \"0px\", originY: \"0px\" }}\n                transition={{ duration: 0.6, ease: \"easeOut\", repeat: Number.POSITIVE_INFINITY, repeatDelay: 2.2, delay: 1.2 }}\n              >\n                <path className=\"fill-amber-400\" d=\"M0 -11 L2.4 -2.4 L11 0 L2.4 2.4 L0 11 L-2.4 2.4 L-11 0 L-2.4 -2.4 Z\" />\n                <circle className=\"fill-white\" r={1.6} />\n                <g className=\"stroke-amber-400\" strokeLinecap=\"round\" strokeWidth={1.5}>\n                  <path d=\"M9 -10 l4 -4\" />\n                  <path d=\"M-10 -9 l-4 -4\" />\n                  <path d=\"M8 10 l4 4\" />\n                </g>\n              </motion.g>\n            </g>\n          )}\n        </svg>\n      </div>\n\n      <div className=\"relative mt-3 grid text-center [&>*]:col-start-1 [&>*]:row-start-1\">\n        {[\n          { key: \"offline\", show: !online, t: title, d: description },\n          { key: \"online\", show: online, t: onlineTitle, d: onlineDescription },\n        ].map((copy) => (\n          <motion.div\n            animate={{ opacity: copy.show ? 1 : 0, y: copy.show || reduce ? 0 : copy.key === \"online\" ? 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 className=\"text-balance font-medium text-foreground text-sm\">{copy.t}</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          aria-disabled={status !== \"offline\"}\n          className=\"min-w-32 transition-[box-shadow,transform,opacity] duration-150 ease-out active:scale-[0.97] aria-disabled:pointer-events-none aria-disabled:opacity-72\"\n          onClick={reconnect}\n          size=\"sm\"\n          variant=\"outline\"\n        >\n          {status === \"online\" ? (\n            <CheckIcon aria-hidden className=\"text-success-foreground\" />\n          ) : status === \"connecting\" ? (\n            <Loader2Icon aria-hidden className=\"animate-spin\" />\n          ) : (\n            <RefreshCwIcon aria-hidden />\n          )}\n          {status === \"online\" ? \"Connected\" : status === \"connecting\" ? \"Connecting\" : actionLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}