{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggles-particle",
  "title": "Particle toggles",
  "description": "Styled toggles: the Particle set. 5 decorative toggles variations (ParticleToggle, SparkToggle, TrailToggle, ConfettiToggle, CometToggle) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/toggles-particle.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Particle toggle family: 5 decorative switches. role=\"switch\", keyboard accessible.\n   Color comes ONLY from tokens. The thumb slides with the framer-motion `layout`;\n   under reduced-motion it switches instantly and the particle/trail effects are\n   disabled. Used uncontrolled (defaultChecked) or controlled\n   (checked/onCheckedChange). */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst trackSize: Record<StyledSize, string> = {\n  sm: \"h-5 w-9 p-0.5\",\n  md: \"h-6 w-11 p-0.5\",\n  lg: \"h-7 w-[52px] p-1\",\n  xl: \"h-8 w-[60px] p-1\",\n}\nconst thumbSize: Record<StyledSize, string> = {\n  sm: \"size-4\",\n  md: \"size-5\",\n  lg: \"size-5\",\n  xl: \"size-6\",\n}\n\nconst trackBase =\n  \"relative inline-flex shrink-0 cursor-pointer items-center outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50\"\n\ninterface ToggleProps extends Omit<React.ComponentProps<\"button\">, \"onChange\"> {\n  size?: StyledSize\n  checked?: boolean\n  defaultChecked?: boolean\n  onCheckedChange?: (checked: boolean) => void\n}\n\nfunction useToggle(props: ToggleProps) {\n  const { checked, defaultChecked, onCheckedChange } = props\n  const reduce = useReducedMotion()\n  const [internal, setInternal] = React.useState(defaultChecked ?? false)\n  const on = checked ?? internal\n  const toggle = () => {\n    if (checked === undefined) setInternal((v) => !v)\n    onCheckedChange?.(!on)\n  }\n  const spring = reduce ? { duration: 0 } : ({ type: \"spring\", stiffness: 500, damping: 30 } as const)\n  return { on, toggle, spring }\n}\n\nfunction stripToggleProps(props: ToggleProps) {\n  const { checked, defaultChecked, onCheckedChange, size, className, children, ...rest } = props\n  return rest\n}\n\n/* Deterministik tanecik yonleri (radyan). */\nconst BURST = [-1.1, -0.55, 0, 0.55, 1.1, Math.PI]\n\n/* Particle: acilinca basparmaktan token tanecikleri fisikirir. */\nexport function ParticleToggle(props: ToggleProps) {\n  const { on, toggle, spring } = useToggle(props)\n  const reduce = useReducedMotion()\n  const size = props.size ?? \"md\"\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      data-slot=\"styled-toggle\"\n      onClick={toggle}\n      className={cn(trackBase, trackSize[size], \"rounded-full transition-colors\", on ? \"justify-end bg-primary\" : \"justify-start bg-input\", props.className)}\n      {...stripToggleProps(props)}\n    >\n      <motion.span layout transition={spring} className={cn(thumbSize[size], \"relative rounded-full bg-background shadow-sm\")}>\n        <AnimatePresence>\n          {on && !reduce && (\n            <span className=\"pointer-events-none absolute inset-0\" aria-hidden>\n              {BURST.map((angle, i) => (\n                <motion.span\n                  key={i}\n                  className=\"absolute top-1/2 left-1/2 size-1 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary\"\n                  initial={{ opacity: 0.9, x: 0, y: 0, scale: 1 }}\n                  animate={{ opacity: 0, x: Math.cos(angle) * 12, y: Math.sin(angle) * 12, scale: 0.4 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 0.5, ease: \"easeOut\" }}\n                />\n              ))}\n            </span>\n          )}\n        </AnimatePresence>\n      </motion.span>\n    </button>\n  )\n}\n\n/* Spark: the thumb leaves a spark trail behind it as it slides. */\nexport function SparkToggle(props: ToggleProps) {\n  const { on, toggle, spring } = useToggle(props)\n  const reduce = useReducedMotion()\n  const size = props.size ?? \"md\"\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      data-slot=\"styled-toggle\"\n      onClick={toggle}\n      className={cn(trackBase, trackSize[size], \"rounded-full transition-colors\", on ? \"justify-end bg-info\" : \"justify-start bg-input\", props.className)}\n      {...stripToggleProps(props)}\n    >\n      <AnimatePresence>\n        {on && !reduce && (\n          <motion.span\n            key=\"spark\"\n            aria-hidden\n            className=\"pointer-events-none absolute top-1/2 left-2 h-0.5 w-6 -translate-y-1/2 rounded-full\"\n            style={{ background: \"linear-gradient(90deg, transparent, var(--info))\" }}\n            initial={{ opacity: 0, scaleX: 0 }}\n            animate={{ opacity: [0, 1, 0], scaleX: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.45, ease: \"easeOut\" }}\n          />\n        )}\n      </AnimatePresence>\n      <motion.span layout transition={spring} className={cn(thumbSize[size], \"relative z-10 rounded-full bg-background shadow-sm\")} />\n    </button>\n  )\n}\n\n/* Trail: basparmagin arkasinda solan token izi. */\nexport function TrailToggle(props: ToggleProps) {\n  const { on, toggle, spring } = useToggle(props)\n  const reduce = useReducedMotion()\n  const size = props.size ?? \"md\"\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      data-slot=\"styled-toggle\"\n      onClick={toggle}\n      className={cn(trackBase, trackSize[size], \"overflow-hidden rounded-full transition-colors\", on ? \"justify-end bg-success\" : \"justify-start bg-input\", props.className)}\n      {...stripToggleProps(props)}\n    >\n      <AnimatePresence>\n        {on && !reduce && (\n          <motion.span\n            key=\"trail\"\n            aria-hidden\n            className=\"pointer-events-none absolute inset-y-1 left-1 rounded-full\"\n            style={{ background: \"color-mix(in oklab, var(--success) 55%, transparent)\" }}\n            initial={{ opacity: 0.7, width: \"80%\" }}\n            animate={{ opacity: 0, width: \"18%\" }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.55, ease: \"easeOut\" }}\n          />\n        )}\n      </AnimatePresence>\n      <motion.span layout transition={spring} className={cn(thumbSize[size], \"relative z-10 rounded-full bg-background shadow-sm\")} />\n    </button>\n  )\n}\n\n/* Deterministik konfeti ofsetleri. */\nconst CONFETTI = [\n  { x: -10, y: -9, r: -30 },\n  { x: -4, y: -12, r: 20 },\n  { x: 3, y: -11, r: -12 },\n  { x: 9, y: -8, r: 40 },\n  { x: 12, y: -3, r: -20 },\n]\n\n/* Confetti: acilinca basparmaktan minik token konfetileri firlar. */\nexport function ConfettiToggle(props: ToggleProps) {\n  const { on, toggle, spring } = useToggle(props)\n  const reduce = useReducedMotion()\n  const size = props.size ?? \"md\"\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      data-slot=\"styled-toggle\"\n      onClick={toggle}\n      className={cn(trackBase, trackSize[size], \"rounded-full transition-colors\", on ? \"justify-end bg-primary\" : \"justify-start bg-input\", props.className)}\n      {...stripToggleProps(props)}\n    >\n      <motion.span layout transition={spring} className={cn(thumbSize[size], \"relative rounded-full bg-background shadow-sm\")}>\n        <AnimatePresence>\n          {on && !reduce && (\n            <span className=\"pointer-events-none absolute inset-0\" aria-hidden>\n              {CONFETTI.map((c, i) => (\n                <motion.span\n                  key={i}\n                  className={cn(\"absolute top-1/2 left-1/2 h-1.5 w-1 -translate-x-1/2 -translate-y-1/2 rounded-[1px]\", i % 2 === 0 ? \"bg-primary\" : \"bg-success\")}\n                  initial={{ opacity: 1, x: 0, y: 0, rotate: 0 }}\n                  animate={{ opacity: 0, x: c.x, y: c.y, rotate: c.r }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 0.6, ease: \"easeOut\" }}\n                />\n              ))}\n            </span>\n          )}\n        </AnimatePresence>\n      </motion.span>\n    </button>\n  )\n}\n\n/* Comet: the thumb, like a comet with a tail. */\nexport function CometToggle(props: ToggleProps) {\n  const { on, toggle, spring } = useToggle(props)\n  const size = props.size ?? \"md\"\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      data-slot=\"styled-toggle\"\n      onClick={toggle}\n      className={cn(trackBase, trackSize[size], \"overflow-hidden rounded-full transition-colors\", on ? \"justify-end bg-info\" : \"justify-start bg-input\", props.className)}\n      {...stripToggleProps(props)}\n    >\n      <motion.span layout transition={spring} className={cn(thumbSize[size], \"relative rounded-full bg-background shadow-sm\")}>\n        <span\n          aria-hidden\n          className={cn(\n            \"pointer-events-none absolute top-1/2 right-full h-1 w-5 -translate-y-1/2 rounded-full transition-opacity duration-300\",\n            on ? \"opacity-100\" : \"opacity-0\"\n          )}\n          style={{ background: \"linear-gradient(90deg, transparent, var(--info))\" }}\n        />\n      </motion.span>\n    </button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/toggles-particle.tsx"
    }
  ],
  "categories": [
    "styled",
    "toggles"
  ],
  "type": "registry:component"
}